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
12 changes: 12 additions & 0 deletions packages/app-shell/src/views/metadata-admin/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,12 @@ const ENGINE_STRINGS_EN: Record<string, string> = {
'designer.field.false': 'False',
'designer.field.duplicate': 'Duplicate field',
'designer.field.copySuffix': ' copy',
// Field power props (Tier 2 — conditional & validation)
'designer.field.helpText': 'Help text',
'designer.field.helpTextPlaceholder': 'Shown below the field on the form',
'designer.field.minLength': 'Min length',
'designer.field.conditionalRequired': 'Required when (CEL)',
'designer.field.conditionalRequiredHint': 'Field becomes required when this predicate is true.',
};

const ENGINE_STRINGS_ZH: Record<string, string> = {
Expand Down Expand Up @@ -1013,6 +1019,12 @@ const ENGINE_STRINGS_ZH: Record<string, string> = {
'designer.field.false': '否',
'designer.field.duplicate': '复制字段',
'designer.field.copySuffix': ' 副本',
// Field power props (Tier 2 — conditional & validation)
'designer.field.helpText': '帮助文本',
'designer.field.helpTextPlaceholder': '显示在表单字段下方',
'designer.field.minLength': '最小长度',
'designer.field.conditionalRequired': '条件必填 (CEL)',
'designer.field.conditionalRequiredHint': '当此表达式为真时,该字段变为必填。',
};

function pickTable(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,44 @@ describe('ObjectFieldInspector — default value', () => {
});
});

describe('ObjectFieldInspector — power props (conditional & validation)', () => {
it('commits inline help text', () => {
const { onPatch } = renderField({ note: { type: 'text', label: 'Note' } }, 'note');
fireEvent.change(controlFor('Help text'), { target: { value: 'Enter the note' } });
expect(onPatch.mock.calls.at(-1)![0].fields.note.inlineHelpText).toBe('Enter the note');
});

it('commits min length for text fields (alongside max length)', () => {
const { onPatch } = renderField({ note: { type: 'text' } }, 'note');
expect(screen.getByText('Min length')).toBeInTheDocument();
expect(screen.getByText('Max length')).toBeInTheDocument();
fireEvent.change(controlFor('Min length'), { target: { value: '3' } });
expect(onPatch.mock.calls.at(-1)![0].fields.note.minLength).toBe(3);
});

it('commits a conditional-required CEL predicate', () => {
const { onPatch } = renderField({ note: { type: 'text' } }, 'note');
fireEvent.change(controlFor('Required when (CEL)'), { target: { value: 'record.x == 1' } });
expect(onPatch.mock.calls.at(-1)![0].fields.note.conditionalRequired).toBe('record.x == 1');
});

it('offers conditional-required for non-text types too', () => {
renderField({ active: { type: 'boolean' } }, 'active');
expect(screen.getByText('Required when (CEL)')).toBeInTheDocument();
// Min length is text-only, so it should not show for a boolean.
expect(screen.queryByText('Min length')).not.toBeInTheDocument();
});
});

describe('ObjectFieldInspector — read-only', () => {
it('hides the duplicate action when read-only', () => {
renderField({ email: { type: 'email' } }, 'email', { readOnly: true });
expect(screen.queryByRole('button', { name: 'Duplicate field' })).not.toBeInTheDocument();
});

it('disables the power-prop inputs when read-only', () => {
renderField({ note: { type: 'text' } }, 'note', { readOnly: true });
expect(controlFor('Help text')).toBeDisabled();
expect(controlFor('Required when (CEL)')).toBeDisabled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,14 @@ export function ObjectFieldInspector({
locale={locale}
/>
)}
<TextareaField
label={tr('designer.field.helpText')}
value={typeof def.inlineHelpText === 'string' ? (def.inlineHelpText as string) : ''}
onCommit={(v) => patchDef({ inlineHelpText: v || undefined })}
disabled={readOnly}
rows={2}
placeholder={tr('designer.field.helpTextPlaceholder')}
/>
</Section>

{/* Type-specific */}
Expand Down Expand Up @@ -407,13 +415,22 @@ export function ObjectFieldInspector({
</div>
)}
{isTexty(type) && (
<InspectorNumberField
label={tr('designer.field.maxLength')}
value={typeof def.maxLength === 'number' ? (def.maxLength as number) : undefined}
onCommit={(v) => patchDef({ maxLength: v })}
disabled={readOnly}
placeholder="255"
/>
<div className="grid grid-cols-2 gap-2">
<InspectorNumberField
label={tr('designer.field.minLength')}
value={typeof def.minLength === 'number' ? (def.minLength as number) : undefined}
onCommit={(v) => patchDef({ minLength: v })}
disabled={readOnly}
placeholder="0"
/>
<InspectorNumberField
label={tr('designer.field.maxLength')}
value={typeof def.maxLength === 'number' ? (def.maxLength as number) : undefined}
onCommit={(v) => patchDef({ maxLength: v })}
disabled={readOnly}
placeholder="255"
/>
</div>
)}
</Section>
)}
Expand Down Expand Up @@ -458,6 +475,19 @@ export function ObjectFieldInspector({
onCommit={(v) => patchDef({ placeholder: v || undefined })}
disabled={readOnly}
/>
<div className="space-y-1">
<InspectorTextField
label={tr('designer.field.conditionalRequired')}
value={typeof def.conditionalRequired === 'string' ? (def.conditionalRequired as string) : ''}
onCommit={(v) => patchDef({ conditionalRequired: v || undefined })}
disabled={readOnly}
mono
placeholder="record.status == 'closed'"
/>
<p className="text-[11px] text-muted-foreground/80 px-0.5 leading-snug">
{tr('designer.field.conditionalRequiredHint')}
</p>
</div>
{fieldGroups.length > 0 && (
<InspectorSelectField
label={tr('designer.field.group')}
Expand Down
Loading