fix: send mail corrections

This commit is contained in:
DarkPhoenix2704
2026-01-31 08:22:04 +00:00
parent 147549a620
commit 94ddf8c4ed
58 changed files with 142 additions and 48 deletions

View File

@@ -56,12 +56,18 @@ const isSendButtonDisabled = computed(() => {
return selectedUserIds.value.size === 0
})
const MESSAGE_MAX_LENGTH = 1000
const customSubject = ref('')
watch(dialogShow, (newVal) => {
if (newVal) {
selectedUserIds.value = new Set()
customMessage.value = ''
sendCopyToSelf.value = false
searchQuery.value = ''
const displayName = extractUserDisplayNameOrEmail(user.value) || 'Someone'
customSubject.value = `${displayName} shared a record from "${props.meta.title}"`
}
})
@@ -103,6 +109,7 @@ const sendRecord = async () => {
viewId: props.view?.id,
rowId: props.rowId,
emails,
subject: customSubject.value || undefined,
message: customMessage.value || undefined,
sendCopyToSelf: sendCopyToSelf.value,
},
@@ -116,11 +123,6 @@ const sendRecord = async () => {
isLoading.value = false
}
}
const defaultSubject = computed(() => {
const displayName = user.value?.display_name || user.value?.email?.split('@')[0] || 'Someone'
return `${displayName} shared a record from "${props.meta.title}"`
})
</script>
<template>
@@ -132,63 +134,69 @@ const defaultSubject = computed(() => {
@keydown.esc="dialogShow = false"
>
<template #header>
<div class="flex flex-row text-2xl font-bold items-center gap-x-2">
{{ $t('activity.sendRecord') }}
<div class="flex flex-col gap-1">
<span class="text-lg font-semibold text-nc-content-gray">{{ $t('activity.sendRecord') }}</span>
<span class="text-sm text-nc-content-gray-subtle">{{ $t('msg.info.shareRecordWithTeam') }}</span>
</div>
</template>
<div class="flex flex-col gap-4 mt-2">
<div class="flex flex-col gap-1.5">
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-1">
<label class="text-nc-content-gray text-sm font-medium">{{ $t('labels.to') }}</label>
<div
v-if="selectedUsers.length > 0"
class="flex flex-wrap gap-1.5 p-2 border-1 border-nc-border-gray-medium rounded-lg mb-2"
>
<div
v-for="selectedUser in selectedUsers"
:key="selectedUser.id"
class="flex items-center gap-1 bg-nc-bg-gray-light rounded-md px-2 py-1"
>
<GeneralUserIcon :user="selectedUser" size="medium" />
<span class="text-sm text-nc-content-gray capitalize truncate max-w-32">
{{ extractUserDisplayNameOrEmail(selectedUser) }}
</span>
<NcButton type="text" size="xxsmall" class="!p-0" @click="removeUser(selectedUser.id)">
<GeneralIcon icon="close" class="w-3.5 h-3.5 text-nc-content-gray-subtle2" />
</NcButton>
</div>
</div>
<div class="border-1 border-nc-border-gray-medium rounded-lg overflow-hidden">
<div class="p-2.5 border-b border-nc-border-gray-medium bg-nc-bg-gray-extralight min-h-12">
<div v-if="selectedUsers.length > 0" class="flex flex-wrap gap-2">
<div
v-for="selectedUser in selectedUsers"
:key="selectedUser.id"
class="flex items-center gap-1.5 bg-white border-1 border-nc-border-gray-medium rounded-full pl-0.5 pr-1 py-0.5 shadow-xs"
>
<GeneralUserIcon :user="selectedUser" size="medium" />
<span class="text-sm text-nc-content-gray truncate max-w-32">
{{ extractUserDisplayNameOrEmail(selectedUser) }}
</span>
<NcButton type="text" size="xxsmall" class="!p-0 !w-4 !h-4 !min-w-4" @click.stop="removeUser(selectedUser.id)">
<GeneralIcon icon="close" class="w-3 h-3 text-nc-content-gray-subtle2 hover:text-nc-content-gray" />
</NcButton>
</div>
</div>
<span v-else class="text-sm text-nc-content-gray-muted">
{{ $t('labels.noRecipientsSelected') }}
</span>
</div>
<div class="p-2 border-b border-nc-border-gray-medium">
<a-input
v-model:value="searchQuery"
:placeholder="$t('placeholder.searchUsers')"
class="!rounded-lg"
:disabled="isLoading"
allow-clear
>
<template #prefix>
<GeneralIcon icon="search" class="text-nc-content-gray-muted" />
</template>
</a-input>
</div>
<div class="max-h-48 overflow-y-auto nc-scrollbar-md">
<div class="max-h-52 overflow-y-auto nc-scrollbar-md">
<div v-if="filteredUsers.length === 0" class="p-4 text-center text-nc-content-gray-muted text-sm">
{{ $t('labels.noUsersFound') }}
</div>
<div
v-for="baseUser in filteredUsers"
:key="baseUser.id"
class="flex items-center gap-3 px-3 py-2 hover:bg-nc-bg-gray-light cursor-pointer"
class="flex items-center gap-3 px-3 py-2.5 hover:bg-nc-bg-gray-light cursor-pointer transition-colors"
@click="toggleUser(baseUser.id)"
>
<NcCheckbox :checked="selectedUserIds.has(baseUser.id)" />
<GeneralUserIcon :user="baseUser" size="medium" />
<div class="flex flex-col min-w-0 flex-1">
<span class="text-sm text-nc-content-gray capitalize truncate">
<span class="text-sm text-nc-content-gray font-medium truncate">
{{ extractUserDisplayNameOrEmail(baseUser) }}
</span>
<span class="text-xs text-nc-content-gray-subtle truncate">
{{ baseUser.email }}
</span>
</div>
</div>
</div>
@@ -201,26 +209,36 @@ const defaultSubject = computed(() => {
<div class="flex flex-col gap-1.5">
<label class="text-nc-content-gray text-sm font-medium">{{ $t('labels.subject') }}</label>
<div class="px-3 py-2 bg-nc-bg-gray-light rounded-lg text-nc-content-gray-subtle text-sm">
{{ defaultSubject }}
</div>
<a-input
v-model:value="customSubject"
:placeholder="$t('placeholder.enterSubject')"
class="!rounded-lg"
:disabled="isLoading"
/>
<span class="text-nc-content-gray-subtle2 text-xs">
{{ $t('msg.info.subjectLineNotification') }}
</span>
</div>
<div class="flex flex-col gap-1.5">
<label class="text-nc-content-gray text-sm font-medium">{{ $t('labels.message') }} {{ $t('labels.optional') }}</label>
<div class="flex items-center justify-between">
<label class="text-nc-content-gray text-sm font-medium">{{ $t('labels.message') }}</label>
<span class="text-xs text-nc-content-gray-subtle2"> {{ customMessage.length }}/{{ MESSAGE_MAX_LENGTH }} </span>
</div>
<a-textarea
v-model:value="customMessage"
:placeholder="$t('placeholder.addPersonalMessage')"
:rows="3"
:maxlength="MESSAGE_MAX_LENGTH"
class="!rounded-lg !text-sm"
:disabled="isLoading"
/>
</div>
<div class="flex items-center gap-2">
<label class="flex items-center gap-2 cursor-pointer select-none">
<NcCheckbox v-model:checked="sendCopyToSelf" :disabled="isLoading" />
<span class="text-nc-content-gray text-sm">{{ $t('labels.sendCopyToMyself') }}</span>
</div>
</label>
</div>
<div class="flex mt-6 justify-end">

View File

@@ -1599,6 +1599,7 @@
"importUsers": "Import Users (by email)",
"noData": "No Data",
"noUsersFound": "No users found",
"noRecipientsSelected": "No recipients selected",
"goToDashboard": "Go to Dashboard",
"importing": "Importing",
"importingFromAirtable": "Importing from Airtable",
@@ -1939,7 +1940,7 @@
"setDisplay": "Set as display value",
"addRow": "Add new record",
"saveRow": "Save record",
"sendRecord": "Send Record",
"sendRecord": "Send record",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "Insert new record",
@@ -2237,6 +2238,7 @@
"enterEmailAddresses": "Enter email addresses",
"addPersonalMessage": "Add a personal message (optional)",
"searchUsers": "Search users",
"enterSubject": "Enter subject line",
"allowNegativeNumbers": "Allow negative numbers",
"searchProjectTree": "Search tables",
"searchFields": "Search fields",
@@ -2484,6 +2486,8 @@
"inviteSent": "Invitation sent successfully",
"separateEmailsWithComma": "Separate multiple email addresses with commas. Maximum 15 recipients.",
"selectBaseMembers": "Select base members to send the record to",
"subjectLineNotification": "This subject line will appear in the email notification",
"shareRecordWithTeam": "Share this record with team members",
"userRoleUpdated": "User role updated successfully",
"teamRoleUpdated": "Team role updated successfully",
"actionIrreversible": "This action is irreversible",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,6 +1,17 @@
import { Injectable } from '@nestjs/common';
import { isSystemColumn, UITypes, ColumnHelper } from 'nocodb-sdk';
import type { ColumnType, NcContext, NcRequest, TableType } from 'nocodb-sdk';
import {
isSystemColumn,
UITypes,
ColumnHelper,
RelationTypes,
} from 'nocodb-sdk';
import type {
ColumnType,
LinkToAnotherRecordType,
NcContext,
NcRequest,
TableType,
} from 'nocodb-sdk';
import type { OPERATION_SCOPES } from '~/controllers/internal/operationScopes';
import type {
InternalApiModule,
@@ -18,6 +29,7 @@ interface SendRecordEmailPayload {
viewId?: string;
rowId: string;
emails: string[];
subject?: string;
message?: string;
sendCopyToSelf?: boolean;
}
@@ -59,7 +71,8 @@ export class SendRecordEmailOperations
payload: SendRecordEmailPayload,
req: NcRequest,
) {
const { tableId, viewId, rowId, emails, message, sendCopyToSelf } = payload;
const { tableId, viewId, rowId, emails, subject, message, sendCopyToSelf } =
payload;
if (!emails || emails.length === 0) {
NcError.get(context).badRequest(
@@ -158,6 +171,7 @@ export class SendRecordEmailOperations
emails: finalEmails,
model,
base,
subject,
message,
recordData,
rowId,
@@ -184,11 +198,13 @@ export class SendRecordEmailOperations
parsedValue?: any;
columnTitle: string;
uidt: UITypes | string;
relationType?: RelationTypes;
}> {
const transformedData: Array<{
parsedValue?: any;
columnTitle: string;
uidt: UITypes | string;
relationType?: RelationTypes;
}> = [];
for (const col of columns) {
@@ -225,10 +241,20 @@ export class SendRecordEmailOperations
}
if (serializedValue !== undefined && serializedValue !== '') {
// Extract relation type for Links/LinkToAnotherRecord columns
let relationType: RelationTypes | undefined;
if (
col.uidt === UITypes.Links ||
col.uidt === UITypes.LinkToAnotherRecord
) {
relationType = (col.colOptions as LinkToAnotherRecordType)?.type;
}
transformedData.push({
parsedValue: serializedValue,
uidt: col.uidt,
columnTitle: col.title,
relationType,
});
}
}

View File

@@ -110,6 +110,7 @@ interface SendRecordPayload {
model: TableType;
base: BaseType;
message?: string;
subject?: string;
recordData: {
parsedValue?: any;
columnTitle: string;

View File

@@ -299,6 +299,7 @@ export class MailService {
emails,
model,
base,
subject,
message,
recordData,
rowId,
@@ -312,9 +313,12 @@ export class MailService {
rowId,
});
const emailSubject =
subject || `${senderName} shared a record from "${model.title}"`;
await mailerAdapter.mailSend({
to: emails.join(','),
subject: `${senderName} shared a record from "${model.title}"`,
subject: emailSubject,
html: await this.renderMail('SendRecord', {
senderName,
senderEmail,

View File

@@ -83,14 +83,14 @@ export const Footer = () => {
</Link>
</Column>
<Column className="border border-y-0 px-1 border-l-0 border-r-1 border-solid border-gray-200">
<Link href="https://blog.nocodb.com" target="_blank">
<Link href="https://nocodb.com/blog" target="_blank">
<Text className="text-center underline py-0 !my-0 text-gray-500 text-[13px]">
Blog
</Text>
</Link>
</Column>
<Column className="border px-1 border-y-0 border-l-0 border-r-1 border-solid border-gray-200">
<Link href="https://docs.nocodb.com/" target="_blank">
<Link href="https://nocodb.com/docs/" target="_blank">
<Text className="text-center underline py-0 !my-0 text-gray-500 text-[13px]">
Docs
</Text>

View File

@@ -0,0 +1,37 @@
import { RelationTypes, UITypes } from 'nocodb-sdk';
import { NC_EMAIL_ASSETS_BASE_URL } from '~/constants';
// Default icon for unknown/unsupported types
const DEFAULT_ICON = 'SingleLineText';
// Map relation types to their specific icons
const RELATION_ICON_MAP: Record<RelationTypes, string> = {
[RelationTypes.MANY_TO_MANY]: 'mm-solid',
[RelationTypes.HAS_MANY]: 'hm-solid',
[RelationTypes.BELONGS_TO]: 'bt-solid',
[RelationTypes.ONE_TO_ONE]: 'oo-solid',
};
/**
* Get the icon URL for a given UIType
* For Links/LinkToAnotherRecord, uses relation-specific icons if relationType is provided
* Falls back to SingleLineText icon for unknown types
*/
export function getFieldIconUrl(
uidt: UITypes | string,
relationType?: RelationTypes,
): string {
// Handle relationship icons
if (
(uidt === UITypes.Links || uidt === UITypes.LinkToAnotherRecord) &&
relationType
) {
const relationIcon = RELATION_ICON_MAP[relationType];
if (relationIcon) {
return `${NC_EMAIL_ASSETS_BASE_URL}/icons/${relationIcon}.png`;
}
}
const iconName = uidt || DEFAULT_ICON;
return `${NC_EMAIL_ASSETS_BASE_URL}/icons/${iconName}.png`;
}

View File

@@ -1,5 +1,6 @@
import { ContentWrapper } from '~/services/mail/templates/components/ContentWrapper';
import { Footer } from '~/services/mail/templates/components/Footer';
import { RootWrapper } from '~/services/mail/templates/components/RootWrapper';
import { getFieldIconUrl } from '~/services/mail/templates/components/iconUrl';
export { ContentWrapper, Footer, RootWrapper };
export { ContentWrapper, Footer, RootWrapper, getFieldIconUrl };

View File

@@ -17,6 +17,7 @@ import {
ContentWrapper,
Footer,
RootWrapper,
getFieldIconUrl,
} from '~/services/mail/templates/components';
import { NC_EMAIL_ASSETS_BASE_URL } from '~/constants';
@@ -108,7 +109,7 @@ const FormSubmission = ({
className="align-middle"
width={16}
height={16}
src={`${NC_EMAIL_ASSETS_BASE_URL}/icons/${s.uidt}.png`}
src={getFieldIconUrl(s.uidt)}
/>
<Section className="!ml-2 truncate inline-block text-[13px] !my-0 !mr-0 leading-4.5 text-gray-600 align-middle">
{s.columnTitle}

View File

@@ -12,11 +12,12 @@ import {
Text,
} from '@react-email/components';
import * as React from 'react';
import { UITypes } from 'nocodb-sdk';
import { RelationTypes, UITypes } from 'nocodb-sdk';
import {
ContentWrapper,
Footer,
RootWrapper,
getFieldIconUrl,
} from '~/services/mail/templates/components';
import { NC_EMAIL_ASSETS_BASE_URL } from '~/constants';
@@ -30,6 +31,7 @@ interface SendRecordTemplateProps {
parsedValue?: any;
columnTitle: string;
uidt: UITypes | string;
relationType?: RelationTypes;
}>;
recordUrl?: string;
}
@@ -123,7 +125,7 @@ const SendRecord = ({
className="align-middle"
width={16}
height={16}
src={`${NC_EMAIL_ASSETS_BASE_URL}/icons/${field.uidt}.png`}
src={getFieldIconUrl(field.uidt, field.relationType)}
/>
<Section className="!ml-2 truncate inline-block text-[13px] !my-0 !mr-0 leading-4.5 text-gray-600 align-middle">
{field.columnTitle}