Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions apps/nestjs-backend/src/types/i18n.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1644,6 +1644,12 @@ export type I18nTranslations = {
"oneWay": string;
"twoWay": string;
};
"button": {
"confirm": {
"title": string;
"description": string;
};
};
};
"permission": {
"actionDescription": {
Expand Down Expand Up @@ -2993,6 +2999,10 @@ export type I18nTranslations = {
"maxCount": string;
"automation": string;
"customAutomation": string;
"clickConfirm": string;
"confirmTitle": string;
"confirmDescription": string;
"confirmButtonText": string;
};
"formula": {
"title": string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@ import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language';
import { EditorState, StateField, StateEffect } from '@codemirror/state';
import type { DecorationSet } from '@codemirror/view';
import { EditorView, keymap, Decoration } from '@codemirror/view';
import { EditorView, keymap, Decoration, placeholder as cmPlaceholder } from '@codemirror/view';
import { useTheme } from '@teable/next-themes';
import { useFields } from '@teable/sdk/hooks';
import { cn } from '@teable/ui-lib/shadcn';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { darkTheme, FieldVariable, FieldVariableNavigation, lightTheme } from './extensions';
import type { IEditorThemeOptions } from './extensions/theme';

export interface IPromptEditorProps {
value: string;
height?: string;
className?: string;
placeholder?: string;
themeOptions?: IEditorThemeOptions;
onChange: (value: string) => void;
}

Expand All @@ -27,7 +29,8 @@ export type EditorViewRef = { current: EditorView | null };

export const PromptEditor = ({
value,
height,
themeOptions,
className,
placeholder,
editorViewRef,
onChange,
Expand Down Expand Up @@ -138,13 +141,13 @@ export const PromptEditor = ({
decorateFields(update.view);
}
}),
isLightTheme ? lightTheme({ height }) : darkTheme({ height }),
isLightTheme ? lightTheme(themeOptions) : darkTheme(themeOptions),
EditorView.lineWrapping,
EditorState.allowMultipleSelections.of(true),
placeholder ? EditorView.contentAttributes.of({ 'data-placeholder': placeholder }) : [],
placeholder ? cmPlaceholder(placeholder) : [],
EditorState.tabSize.of(2),
];
}, [fieldDecorationsState, isLightTheme, height, placeholder, onChange, decorateFields]);
}, [fieldDecorationsState, isLightTheme, themeOptions, placeholder, onChange, decorateFields]);

const createEditorView = useCallback(
(parent: HTMLElement) => {
Expand Down Expand Up @@ -197,7 +200,7 @@ export const PromptEditor = ({
}, [value, editorView, decorateFields]);

return (
<div className="h-full">
<div className={cn('h-full', className)}>
<div ref={editorRef} className="h-full cursor-text rounded-lg border shadow-sm" />
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,11 @@ export const PromptEditorContainer = (props: IPromptEditorContainerProps) => {
{fieldSelector}
</DialogHeader>
<div className="flex-1">
<PromptEditor {...props} height="280px" editorViewRef={dialogEditorViewRef} />
<PromptEditor
{...props}
themeOptions={{ height: '280px' }}
editorViewRef={dialogEditorViewRef}
/>
</div>

<DialogFooter>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { EditorView } from '@codemirror/view';
import type { CSSProperties } from 'react';
import colors from 'tailwindcss/colors';

interface IEditorThemeOptions {
export interface IEditorThemeOptions {
height?: string;
content?: CSSProperties;
}

const createEditorThemeBase = (options?: IEditorThemeOptions) => ({
'&': {
height: options?.height || '120px',
fontSize: '14px',
height: options?.height ?? '120px',
maxHeight: '320px',
fontSize: '14px',
backgroundColor: 'transparent',
},
'.cm-scroller': {
Expand All @@ -28,19 +30,33 @@ const createEditorThemeBase = (options?: IEditorThemeOptions) => ({
const EDITOR_LIGHT_THEME = (options?: IEditorThemeOptions) => ({
...createEditorThemeBase(options),
'.cm-content': {
padding: '8px 4px',
minHeight: options?.height || '120px',
...(options?.content ?? { padding: '8px 4px' }),
caretColor: colors.black,
},
'.cm-line': {
position: 'relative',
},
'.cm-placeholder': {
position: 'absolute',
paddingLeft: 'unset',
fontSize: 'inherit',
},
});

const EDITOR_DARK_THEME = (options?: IEditorThemeOptions) => ({
...createEditorThemeBase(options),
'.cm-content': {
padding: '8px 4px',
minHeight: options?.height || '120px',
...(options?.content ?? { padding: '8px 4px' }),
caretColor: colors.white,
},
'.cm-line': {
position: 'relative',
},
'.cm-placeholder': {
position: 'absolute',
paddingLeft: 'unset',
fontSize: 'inherit',
},
});

export const lightTheme = (options?: IEditorThemeOptions) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { Colors, ColorUtils } from '@teable/core';
import { Colors, ColorUtils, FieldType } from '@teable/core';
import type { IButtonFieldOptions } from '@teable/core';
import { Plus } from '@teable/icons';
import { FieldSelector } from '@teable/sdk/components';
import { useFields } from '@teable/sdk/hooks';
import {
Button,
Input,
Expand All @@ -14,11 +17,12 @@ import {
TooltipTrigger,
} from '@teable/ui-lib/shadcn';
import { PencilIcon, PlusIcon } from 'lucide-react';
import { useState } from 'react';
import { useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useWorkFlowPanelStore } from '@/features/app/automation/workflow-panel/useWorkFlowPaneStore';
import { useBaseUsage } from '@/features/app/hooks/useBaseUsage';
import { tableConfig } from '@/features/i18n/table.config';
import { PromptEditor, type EditorViewRef } from '../field-ai-config/components/prompt-editor';
import { ColorPicker } from './SelectOptions';

const AutomationTooltip = (props: { children: React.ReactNode }) => {
Expand All @@ -36,6 +40,137 @@ const AutomationTooltip = (props: { children: React.ReactNode }) => {
);
};

const ConfirmEditor = (props: {
options?: Partial<IButtonFieldOptions>;
onChange?: (options: Partial<IButtonFieldOptions>) => void;
}) => {
const { options, onChange } = props;
const { t } = useTranslation(tableConfig.i18nNamespaces);
const fields = useFields({ withHidden: true, withDenied: true });
const titleEditorViewRef = useRef(null) as EditorViewRef;
const descEditorViewRef = useRef(null) as EditorViewRef;
const confirmTextEditorViewRef = useRef(null) as EditorViewRef;
const confirmEnabled = Boolean(options?.confirm);
const confirm = options?.confirm;

const excludedFieldIds = useMemo(() => {
return fields.filter((field) => field.type === FieldType.Attachment).map((field) => field.id);
}, [fields]);

const onFieldSelect = (fieldId: string, editorViewRef: EditorViewRef) => {
const formatValue = `{${fieldId}}`;
const view = editorViewRef.current;

if (view) {
const { from, to } = view.state.selection.main;
view.dispatch({
changes: { from, to, insert: formatValue },
selection: { anchor: from + formatValue.length },
});
view.focus();
}
};

const updateConfirm = (key: keyof NonNullable<typeof confirm>, value: string) => {
onChange?.({
...options,
confirm: {
...confirm,
[key]: value,
},
});
};

return (
<div className="flex flex-col gap-2">
<div className="flex h-8 items-center gap-2">
<Switch
checked={confirmEnabled}
onCheckedChange={(checked) => {
onChange?.({
...options,
confirm: checked ? { title: '', description: '', confirmText: '' } : null,
});
}}
/>
<Label className="text-sm font-normal">
{t('table:field.default.button.clickConfirm')}
</Label>
</div>

{confirmEnabled && (
<div className="flex flex-col gap-2 rounded-md border-muted bg-muted p-3">
{/* Title */}
<div className="flex flex-col gap-1">
<div className="flex h-6 items-center justify-between">
<Label className="text-xs text-muted-foreground">
{t('table:field.default.button.confirmTitle')}
</Label>
<FieldSelector
excludedIds={excludedFieldIds}
onSelect={(fieldId) => onFieldSelect(fieldId, titleEditorViewRef)}
modal
>
<Button variant="ghost" size="xs">
<Plus className="size-4" />
</Button>
</FieldSelector>
</div>
<PromptEditor
themeOptions={{ height: 'auto', content: { padding: '6px 0px' } }}
value={confirm?.title ?? ''}
placeholder={t('sdk:field.button.confirm.title')}
editorViewRef={titleEditorViewRef}
onChange={(value) => updateConfirm('title', value)}
/>
</div>

{/* Description */}
<div className="flex flex-col gap-1">
<div className="flex h-6 items-center justify-between">
<Label className="text-xs text-muted-foreground">
{t('table:field.default.button.confirmDescription')}
</Label>
<FieldSelector
excludedIds={excludedFieldIds}
onSelect={(fieldId) => onFieldSelect(fieldId, descEditorViewRef)}
modal
>
<Button variant="ghost" size="xs">
<Plus className="size-4" />
</Button>
</FieldSelector>
</div>
<PromptEditor
themeOptions={{ content: { padding: '6px 0px' } }}
value={confirm?.description ?? ''}
placeholder={t('sdk:field.button.confirm.description')}
editorViewRef={descEditorViewRef}
onChange={(value) => updateConfirm('description', value)}
/>
</div>

{/* Confirm Button Text */}
<div className="flex flex-col gap-1">
<div className="flex h-6 items-center justify-between">
<Label className="text-xs text-muted-foreground">
{t('table:field.default.button.confirmButtonText')}
</Label>
</div>
<PromptEditor
themeOptions={{ height: 'auto', content: { padding: '6px 0px' } }}
value={confirm?.confirmText ?? ''}
placeholder={t('common:actions.confirm')}
editorViewRef={confirmTextEditorViewRef}
onChange={(value) => updateConfirm('confirmText', value)}
/>
</div>
</div>
)}
</div>
);
};

const WorkflowAction = (props: { options?: Partial<IButtonFieldOptions>; onSave?: () => void }) => {
const { options, onSave } = props;
const workflow = options?.workflow;
Expand Down Expand Up @@ -163,6 +298,8 @@ export const ButtonOptions = (props: {
/>
</div>
)}

<ConfirmEditor options={options} onChange={onChange} />
</div>
</div>
)}
Expand Down
6 changes: 6 additions & 0 deletions packages/common-i18n/src/locales/de/sdk.json
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,12 @@
"link": {
"oneWay": "Einfach",
"twoWay": "Doppelt"
},
"button": {
"confirm": {
"title": "Aktion bestätigen",
"description": "Sind Sie sicher, dass Sie diese Aktion ausführen möchten?"
}
}
},
"permission": {
Expand Down
6 changes: 5 additions & 1 deletion packages/common-i18n/src/locales/de/table.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,11 @@
"resetCount": "Zurücksetzen des Klickzählers erlauben",
"maxCount": "Maximale Klickzahl",
"automation": "Automatisierung",
"customAutomation": "Benutzerdefinierte Automatisierung"
"customAutomation": "Benutzerdefinierte Automatisierung",
"clickConfirm": "Klicken Sie vor dem Klicken bestätigen",
"confirmTitle": "Titel",
"confirmDescription": "Inhalt",
"confirmButtonText": "Bestätigungstext der Schaltfläche"
},
"formula": {
"title": "Berechnung",
Expand Down
6 changes: 6 additions & 0 deletions packages/common-i18n/src/locales/en/sdk.json
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,12 @@
"link": {
"oneWay": "One way",
"twoWay": "Two way"
},
"button": {
"confirm": {
"title": "Operation confirmation",
"description": "Are you sure you want to perform this button operation?"
}
}
},
"permission": {
Expand Down
6 changes: 5 additions & 1 deletion packages/common-i18n/src/locales/en/table.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,11 @@
"resetCount": "Allow reset",
"maxCount": "Max clicks",
"automation": "Automation",
"customAutomation": "Custom automation"
"customAutomation": "Custom automation",
"clickConfirm": "Confirm before clicking",
"confirmTitle": "Title",
"confirmDescription": "Content",
"confirmButtonText": "Confirm button text"
},
"formula": {
"title": "Calculation",
Expand Down
6 changes: 6 additions & 0 deletions packages/common-i18n/src/locales/es/sdk.json
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,12 @@
"link": {
"oneWay": "Unidireccional",
"twoWay": "Bidireccional"
},
"button": {
"confirm": {
"title": "Confirmación de acción",
"description": "¿Estás seguro de querer ejecutar esta acción?"
}
}
},
"permission": {
Expand Down
Loading