diff --git a/apps/docs/solutions/template-builder/api-reference.mdx b/apps/docs/solutions/template-builder/api-reference.mdx index 98f887be44..59ae3f3bbd 100644 --- a/apps/docs/solutions/template-builder/api-reference.mdx +++ b/apps/docs/solutions/template-builder/api-reference.mdx @@ -75,6 +75,10 @@ None - the component works with zero configuration. - `object` — full toolbar configuration (see ToolbarConfig) + + Lock mode for all inserted fields. Per-field `lockMode` overrides this. See [lock modes](/extensions/structured-content#lock-modes). + + Content Security Policy nonce for dynamically injected styles @@ -174,6 +178,7 @@ interface FieldDefinition { mode?: "inline" | "block"; // Insertion mode (default: "inline") group?: string; // Group ID for linked fields fieldType?: string; // Field type, e.g. "owner" or "signer" (default: "owner") + lockMode?: LockMode; // Lock mode for this field (overrides defaultLockMode) } ``` @@ -190,6 +195,7 @@ interface TemplateField { mode?: "inline" | "block"; // Rendering mode group?: string; // Group ID for linked fields fieldType?: string; // Field type, e.g. "owner" or "signer" + lockMode?: LockMode; // Current lock mode of this field } ``` @@ -426,6 +432,7 @@ import type { SuperDocTemplateBuilderHandle, FieldDefinition, TemplateField, + LockMode, TriggerEvent, ExportEvent, ExportConfig, diff --git a/apps/docs/solutions/template-builder/configuration.mdx b/apps/docs/solutions/template-builder/configuration.mdx index 1925afdf70..1fc48c3053 100644 --- a/apps/docs/solutions/template-builder/configuration.mdx +++ b/apps/docs/solutions/template-builder/configuration.mdx @@ -110,6 +110,26 @@ Allow users to create new fields while building templates: When enabled, the field menu shows a "Create New Field" option with inputs for name, mode (inline/block), and field type (owner/signer). +### Field locking + +Make fields read-only so end users can't edit their content: + +```tsx + +``` + +Per-field `lockMode` overrides `defaultLockMode`. The default field creation form includes a "Locked" checkbox that sets `contentLocked`. + +Template authors can always delete and manage fields regardless of lock mode — locking only affects end users interacting with the document. For advanced use cases, see [lock modes](/extensions/structured-content#lock-modes). + ### Linked fields When a user selects an existing field from the "Existing Fields" section in the menu, a linked copy is inserted. Both instances share a group ID and stay in sync. diff --git a/packages/template-builder/src/defaults/FieldList.tsx b/packages/template-builder/src/defaults/FieldList.tsx index db0621bde6..5a5fc0e8f7 100644 --- a/packages/template-builder/src/defaults/FieldList.tsx +++ b/packages/template-builder/src/defaults/FieldList.tsx @@ -124,6 +124,21 @@ const FieldItem: FC<{ {field.fieldType} )} + {field.lockMode && field.lockMode !== 'unlocked' && ( + + 🔒 + + )} diff --git a/packages/template-builder/src/defaults/FieldMenu.tsx b/packages/template-builder/src/defaults/FieldMenu.tsx index 40e9a9acd5..befaf6ff82 100644 --- a/packages/template-builder/src/defaults/FieldMenu.tsx +++ b/packages/template-builder/src/defaults/FieldMenu.tsx @@ -20,6 +20,7 @@ export const FieldMenu: React.FC = ({ const [newFieldName, setNewFieldName] = useState(''); const [fieldMode, setFieldMode] = useState<'inline' | 'block'>('inline'); const [fieldType, setFieldType] = useState('owner'); + const [fieldLocked, setFieldLocked] = useState(false); const [existingExpanded, setExistingExpanded] = useState(true); const [availableExpanded, setAvailableExpanded] = useState(true); @@ -29,6 +30,7 @@ export const FieldMenu: React.FC = ({ setNewFieldName(''); setFieldMode('inline'); setFieldType('owner'); + setFieldLocked(false); } }, [isVisible]); @@ -69,6 +71,7 @@ export const FieldMenu: React.FC = ({ label: trimmedName, mode: fieldMode, fieldType: fieldType, + ...(fieldLocked && { lockMode: 'contentLocked' as const }), }; try { @@ -83,6 +86,7 @@ export const FieldMenu: React.FC = ({ setNewFieldName(''); setFieldMode('inline'); setFieldType('owner'); + setFieldLocked(false); } }; @@ -131,6 +135,7 @@ export const FieldMenu: React.FC = ({ setIsCreating(false); setNewFieldName(''); setFieldMode('inline'); + setFieldLocked(false); } }} autoFocus @@ -227,6 +232,19 @@ export const FieldMenu: React.FC = ({ Signer +
+ +
= ({ setNewFieldName(''); setFieldMode('inline'); setFieldType('owner'); + setFieldLocked(false); }} style={{ padding: '4px 12px', diff --git a/packages/template-builder/src/index.tsx b/packages/template-builder/src/index.tsx index 060da1bd56..81d2fe073c 100644 --- a/packages/template-builder/src/index.tsx +++ b/packages/template-builder/src/index.tsx @@ -39,6 +39,7 @@ const getTemplateFieldsFromEditor = (editor: Editor): Types.TemplateField[] => { mode, group: structuredContentHelpers.getGroup?.(attrs.tag) ?? undefined, fieldType: parsedTag?.fieldType ?? 'owner', + lockMode: attrs.lockMode ?? undefined, } as Types.TemplateField; }); }; @@ -51,6 +52,7 @@ const SuperDocTemplateBuilder = forwardRef = { + alias: field.alias, + tag: tagData, + ...(lockMode != null && { lockMode }), + }; + const success = ( mode === 'inline' - ? editor.commands.insertStructuredContentInline?.({ - attrs: { - alias: field.alias, - tag: tagData, - }, - text: field.defaultValue || field.alias, - }) - : editor.commands.insertStructuredContentBlock?.({ - attrs: { - alias: field.alias, - tag: tagData, - }, - text: field.defaultValue || field.alias, - }) + ? editor.commands.insertStructuredContentInline?.({ attrs, text: field.defaultValue || field.alias }) + : editor.commands.insertStructuredContentBlock?.({ attrs, text: field.defaultValue || field.alias }) ) as boolean | undefined; if (success) { @@ -174,7 +172,7 @@ const SuperDocTemplateBuilder = forwardRef = { + alias: field.alias, + tag: tagWithGroup, + ...(lockMode != null && { lockMode }), + }; const success = mode === 'inline' - ? editor.commands.insertStructuredContentInline?.({ - attrs: { - alias: field.alias, - tag: tagWithGroup, - }, - text: field.alias, - }) - : editor.commands.insertStructuredContentBlock?.({ - attrs: { - alias: field.alias, - tag: tagWithGroup, - }, - text: field.alias, - }); + ? editor.commands.insertStructuredContentInline?.({ attrs, text: field.alias }) + : editor.commands.insertStructuredContentBlock?.({ attrs, text: field.alias }); if (success) { if (!field.group) { @@ -556,7 +551,7 @@ const SuperDocTemplateBuilder = forwardRef { diff --git a/packages/template-builder/src/tests/utils.test.ts b/packages/template-builder/src/tests/utils.test.ts index 5b5e411c10..bac98e92e7 100644 --- a/packages/template-builder/src/tests/utils.test.ts +++ b/packages/template-builder/src/tests/utils.test.ts @@ -72,6 +72,18 @@ describe('areTemplateFieldsEqual', () => { const b: TemplateField[] = [{ id: '1', alias: 'Name', fieldType: 'signer' }]; expect(areTemplateFieldsEqual(a, b)).toBe(false); }); + + it('returns false when lockMode differs', () => { + const a: TemplateField[] = [{ id: '1', alias: 'Name', lockMode: 'unlocked' }]; + const b: TemplateField[] = [{ id: '1', alias: 'Name', lockMode: 'sdtContentLocked' }]; + expect(areTemplateFieldsEqual(a, b)).toBe(false); + }); + + it('returns true when lockMode is the same', () => { + const a: TemplateField[] = [{ id: '1', alias: 'Name', lockMode: 'contentLocked' }]; + const b: TemplateField[] = [{ id: '1', alias: 'Name', lockMode: 'contentLocked' }]; + expect(areTemplateFieldsEqual(a, b)).toBe(true); + }); }); describe('resolveToolbar', () => { diff --git a/packages/template-builder/src/types.ts b/packages/template-builder/src/types.ts index 591d305431..f60d1b6447 100644 --- a/packages/template-builder/src/types.ts +++ b/packages/template-builder/src/types.ts @@ -1,5 +1,7 @@ import type { SuperDoc } from 'superdoc'; +export type LockMode = 'unlocked' | 'sdtLocked' | 'contentLocked' | 'sdtContentLocked'; + /** Field definition for template builder */ export interface FieldDefinition { id: string; @@ -9,6 +11,7 @@ export interface FieldDefinition { mode?: 'inline' | 'block'; group?: string; fieldType?: string; + lockMode?: LockMode; } /** Field instance in a template document */ @@ -20,6 +23,7 @@ export interface TemplateField { mode?: 'inline' | 'block'; group?: string; fieldType?: string; + lockMode?: LockMode; } export interface TriggerEvent { @@ -114,6 +118,9 @@ export interface SuperDocTemplateBuilderProps { list?: ListConfig; toolbar?: boolean | string | ToolbarConfig; + /** Lock mode applied to all inserted fields unless overridden per-field */ + defaultLockMode?: LockMode; + /** Content Security Policy nonce for dynamically injected styles */ cspNonce?: string; diff --git a/packages/template-builder/src/utils.ts b/packages/template-builder/src/utils.ts index 989944825e..8d6ef295af 100644 --- a/packages/template-builder/src/utils.ts +++ b/packages/template-builder/src/utils.ts @@ -18,7 +18,8 @@ export const areTemplateFieldsEqual = (a: TemplateField[], b: TemplateField[]): left.position !== right.position || left.mode !== right.mode || left.group !== right.group || - left.fieldType !== right.fieldType + left.fieldType !== right.fieldType || + left.lockMode !== right.lockMode ) { return false; }