From d547d4217cbb27c3b0dba74a8156be335c79c20a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 20 Sep 2026 06:27:59 +0000 Subject: [PATCH] feat(fields): give RecipientPickerField a picker mode for the `field` recipient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `sys_sharing_rule.recipient_type` carries a sixth value, `field` — the record-relative recipient (maintainer ruling objectstack#14103, executor objectstack#15072). Its `recipient_id` stores a field NAME, not a record id, so the kind has no row in `TYPE_TO_OBJECT` and fell through to the plain text input the header documents for unknown types: the stored value was already correct, but the admin had to know and type the machine name of the column by hand, with no list and no signal that the name existed. The widget now reads the shared object from the sibling `object_name` (the same dependency the `filter-condition` widget already reads), loads its schema through `dataSource.getObjectSchema`, and offers that object's user-valued columns, storing the column's name. The offered set is exactly the set the evaluator honours: the `user` type, or a `lookup` / `master_detail` whose `reference` is `sys_user`. Anything else the evaluator reads as "grants nobody" plus one warning per rule, so a wider picker would produce a rule that looks configured and authorises nobody. `hidden` is deliberately not filtered (the evaluator honours a hidden user column) and `reference_to` is deliberately not read (the protocol refuses that spelling by name, and so does the evaluator). Three states the mode names instead of leaving to be inferred: it asks for the shared object while `object_name` is unset; an object with no user columns says so rather than rendering a search-flavoured "No matches"; and a stored name that is not on the offered list stays visible and is marked, because the evaluator grants nobody for it. Unknown types keep degrading to the text input, and so does `field` itself when the data source cannot enumerate columns. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq --- .../7613-recipient-picker-field-kind.md | 53 ++++ .../src/widgets/RecipientPickerField.tsx | 193 ++++++++++++++- ...ntPickerField.fieldRecipient-7613.test.tsx | 231 ++++++++++++++++++ .../fields/src/widgets/useFieldTranslation.ts | 11 + packages/i18n/src/locales/ar.ts | 3 + packages/i18n/src/locales/de.ts | 3 + packages/i18n/src/locales/en.ts | 3 + packages/i18n/src/locales/es.ts | 3 + packages/i18n/src/locales/fr.ts | 3 + packages/i18n/src/locales/ja.ts | 3 + packages/i18n/src/locales/ko.ts | 3 + packages/i18n/src/locales/pt.ts | 3 + packages/i18n/src/locales/ru.ts | 3 + packages/i18n/src/locales/zh.ts | 3 + 14 files changed, 507 insertions(+), 11 deletions(-) create mode 100644 .changeset/7613-recipient-picker-field-kind.md create mode 100644 packages/fields/src/widgets/__tests__/RecipientPickerField.fieldRecipient-7613.test.tsx diff --git a/.changeset/7613-recipient-picker-field-kind.md b/.changeset/7613-recipient-picker-field-kind.md new file mode 100644 index 0000000000..4030a54ef4 --- /dev/null +++ b/.changeset/7613-recipient-picker-field-kind.md @@ -0,0 +1,53 @@ +--- +'@object-ui/fields': minor +'@object-ui/i18n': minor +--- + +`RecipientPickerField` gains a picker mode for the `field` sharing recipient +(objectui#7613; maintainer ruling objectstack#14103, executor objectstack#15072). + +`sys_sharing_rule.recipient_type` carries a sixth value, `field` — the +record-relative recipient. The recipients are whoever a user-valued column of the +SHARED object names on each matched record, so `recipient_id` stores a field +NAME (`assignees`, `owner_manager`) and not a record id. The widget's +`TYPE_TO_OBJECT` table had no entry for that kind, so it fell through to the +plain text input documented for unknown types: functional (the stored value was +already the right one), but the admin had to know and type the machine name of +the column by hand, with no list to choose from and nothing saying whether the +name existed. + +When `recipient_type` is `field`, the widget now reads the shared object from +the sibling `object_name` field — the same dependency the `filter-condition` +widget already reads — loads that object's schema through +`dataSource.getObjectSchema`, and offers its **user-valued** columns, storing the +column's name. + +**The offered set is exactly the set the evaluator honours**, and that agreement +is the point of the change rather than a detail of it. The sharing evaluator +reads a column as users when it is the `user` type, or a `lookup` / +`master_detail` whose `reference` is `sys_user`; anything else it treats as +"grants nobody" and warns about once per rule. A picker offering a wider set +would let an admin save a rule that looks configured and authorises nobody — +worse than a hand-typed name, because it has a credible appearance. `hidden` is +deliberately not filtered (the evaluator honours a hidden user column), and +`reference_to` is deliberately not read (the protocol refuses that spelling by +name, and so does the evaluator). + +Three things the mode says out loud rather than leaving to be inferred: it asks +for the shared object before offering anything when `object_name` is still +unset; an object with no user columns says so instead of rendering a +search-flavoured "No matches"; and a stored name that is not on the offered list +— a column deleted or retyped since the rule was saved — stays visible and is +marked, because the evaluator grants nobody for it and a control that merely +looked empty would hide a rule that still names it. + +Unknown recipient types keep degrading to the plain text input, and so does +`field` itself when the data source cannot enumerate an object's columns: a +hand-typed name is worse than a list, and better than a list that can never +fill. + +Three copy keys are added across all ten locale packs +(`fields.recipient.selectField`, `noUserFields`, `fieldNotUserTyped`). The +"select an object first" gate deliberately reuses the criteria builder's +existing sentence rather than adding a twin: it is the same sentence in the same +role on the same form, gating on the same sibling field. diff --git a/packages/fields/src/widgets/RecipientPickerField.tsx b/packages/fields/src/widgets/RecipientPickerField.tsx index 062ac5b308..a90a0e95f3 100644 --- a/packages/fields/src/widgets/RecipientPickerField.tsx +++ b/packages/fields/src/widgets/RecipientPickerField.tsx @@ -23,6 +23,11 @@ import { useFieldTranslation } from './useFieldTranslation.js'; * position → sys_position, store `name` (matched against * sys_user_position.position at evaluation time) * + * One kind is NOT a record picker and therefore has no row above: + * + * field → a user-valued COLUMN of the shared object, store + * the column's `name` (see FIELD_RECIPIENT_TYPE) + * * When `recipient_type` changes after mount the stored id is reset (an id valid * for one type is meaningless for another). Unknown types degrade to a plain * text input so nothing breaks. @@ -49,6 +54,84 @@ const TYPE_TO_OBJECT: Record = { position: { object: 'sys_position', storeField: 'name', labelFields: ['label', 'name'], placeholderKey: 'fields.recipient.selectPosition' }, }; +/** + * The RECORD-RELATIVE recipient kind (maintainer ruling objectstack#14103, + * executor objectstack#15072). It is deliberately absent from + * `TYPE_TO_OBJECT`: there is no target object to query and no record id to + * store. `recipient_id` holds the NAME of a user-valued column on the SHARED + * object, and the recipients are whoever that column names on each matched + * record — so this mode reads the object chosen in the sibling `object_name` + * field (the same dependency the `filter-condition` widget already reads) and + * offers that object's user-valued fields. + */ +const FIELD_RECIPIENT_TYPE = 'field'; + +/** A user-valued column offered by the `field` recipient mode. */ +interface UserFieldDef { + /** The machine name — this is what gets stored in `recipient_id`. */ + name: string; + label: string; +} + +/** + * Does this declared field hold USERS? + * + * ⭐ This predicate is the picker's half of a two-sided agreement with the + * sharing evaluator, which asks the same question of the same two spellings: + * the `user` type (whose target is fixed to `sys_user` by the type itself — + * `Field.user()` takes no target), and a `lookup` / `master_detail` whose + * `reference` is `sys_user`. + * + * ⛔ The two must not drift apart. The evaluator treats a column that is not + * user-typed as "grants nobody" and warns once per rule, so a picker that + * OFFERED a wider set would let an admin save a rule that looks configured and + * authorises nobody — worse than the hand-typed name this mode replaces, + * because it has a credible appearance. + * + * `reference` is the only spelling the protocol declares (objectui#6837): + * `FieldSchema` refuses `reference_to` by name, and the evaluator reads + * `reference` as well. + * + * @internal exported for tests + */ +export function fieldHoldsUsers(def: unknown): boolean { + if (!def || typeof def !== 'object') return false; + const d = def as { type?: unknown; reference?: unknown }; + return ( + d.type === 'user' || + ((d.type === 'lookup' || d.type === 'master_detail') && d.reference === 'sys_user') + ); +} + +/** + * The user-valued columns of an object schema, in declaration order. + * + * Accepts both shapes an object schema spells `fields` in — a name-keyed map + * and an array of definitions carrying their own `name`. + * + * ⛔ `hidden` is deliberately NOT filtered here, unlike the filter builder's + * own field derivation: the evaluator honours a hidden user column exactly + * like a visible one, and withholding it would break the agreement above in + * the other direction — an authorable, working configuration the picker + * refuses to offer. + * + * @internal exported for tests + */ +export function deriveUserFields(schema: any): UserFieldDef[] { + const raw = schema?.fields; + const entries: Array<[string, any]> = Array.isArray(raw) + ? raw.map((f: any) => [f?.name, f]) + : raw && typeof raw === 'object' + ? Object.entries(raw) + : []; + const out: UserFieldDef[] = []; + for (const [name, f] of entries) { + if (!name || !fieldHoldsUsers(f)) continue; + out.push({ name: String(name), label: f.label ? String(f.label) : String(name) }); + } + return out; +} + export function RecipientPickerField({ value, onChange, @@ -66,8 +149,17 @@ export function RecipientPickerField({ const dependentValues: Record = (props as any).dependentValues ?? {}; const recipientType = String(dependentValues.recipient_type ?? ''); const mapping = TYPE_TO_OBJECT[recipientType]; + const objectName = String(dependentValues.object_name ?? ''); + const isFieldRecipient = recipientType === FIELD_RECIPIENT_TYPE; + // Only this data source can answer "which columns does that object declare?". + // Without it the mode falls through to the degraded text input below rather + // than rendering a list that can never fill — the same "nothing breaks" + // promise the header makes for an unknown type. + const canListObjectFields = + isFieldRecipient && !!dataSource && typeof dataSource.getObjectSchema === 'function'; const [records, setRecords] = React.useState(null); + const [userFields, setUserFields] = React.useState(null); // Reset the stored recipient when the admin PICKS a different type (an id for // a user is not a valid team/business-unit id). @@ -109,6 +201,26 @@ export function RecipientPickerField({ }; }, [dataSource, mapping?.object]); + // The `field` mode's own load: the SHARED object's schema, keyed on the + // sibling `object_name`. Re-run when the admin switches object, so the + // offered columns always belong to the object the rule actually names. + React.useEffect(() => { + setUserFields(null); + if (!canListObjectFields || !objectName) return; + let cancelled = false; + (async () => { + try { + const schema = await dataSource.getObjectSchema(objectName); + if (!cancelled) setUserFields(deriveUserFields(schema)); + } catch { + if (!cancelled) setUserFields([]); + } + })(); + return () => { + cancelled = true; + }; + }, [dataSource, objectName, canListObjectFields]); + const labelOf = (r: any): string => { for (const f of mapping?.labelFields ?? ['name']) if (r?.[f]) return String(r[f]); return String(r?.id ?? ''); @@ -124,6 +236,21 @@ export function RecipientPickerField({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [records, value, mapping]); + const userFieldOptions = React.useMemo( + () => (userFields ?? []).map((f) => ({ value: f.name, label: f.label })), + [userFields], + ); + + // DOM pass-through onto the combobox trigger — the widget's real focusable + // control (objectui#3318). `name` is withheld: the trigger is a button, not + // a submission control (same reasoning as #3306's SelectTrigger). + // + // NOTE this widget stays on the #3318 ledger regardless: its dependency- + // gated state (no `recipient_type` chosen yet — the state a fresh form and + // the registry sweep render) is a plain hint paragraph with no focusable + // control, so there is nothing there to carry the attribute. + const { name: _domName, ...triggerDomProps } = toDomProps(props); + if (!recipientType) { return (

@@ -132,9 +259,63 @@ export function RecipientPickerField({ ); } + if (canListObjectFields) { + if (!objectName) { + // Deliberately the SAME key the criteria builder's gate sentence uses, + // not a `fields.recipient.*` twin: it is the same sentence, in the same + // role, on the same form, gating on the same sibling field — and a + // second spelling is precisely how two copies of one deliberately + // shared sentence come to read differently in a locale. + return ( +

+ {t('fields.filterCondition.selectObjectFirst')} +

+ ); + } + + // The stored name stays VISIBLE even when it is not on the offered list — + // a column deleted or retyped since the rule was saved. Dropping it would + // leave the control looking empty while the rule still names that column; + // the evaluator grants NOBODY for it, so the option says so instead of + // reading like any other choice. + const fieldOpts = + value && !userFieldOptions.some((o) => o.value === value) + ? [{ value, label: t('fields.recipient.fieldNotUserTyped', { name: value }) }, ...userFieldOptions] + : userFieldOptions; + + if (readonly) { + if (!value) return ; + return ( + {fieldOpts.find((o) => o.value === value)?.label ?? value} + ); + } + + return ( + onChange(v as any)} + placeholder={ + userFields === null ? t('fields.recipient.loading') : t('fields.recipient.selectField') + } + searchPlaceholder={t('fields.recipient.search')} + emptyText={ + userFields === null ? t('fields.recipient.loading') : t('fields.recipient.noUserFields') + } + disabled={disabled} + className={cn('w-full', className)} + // AFTER the spread so this widget's own computation wins (#3222). + aria-invalid={!!error} + /> + ); + } + if (!mapping) { // Unknown / unsupported recipient type — keep a plain text input so the - // field is never un-editable. + // field is never un-editable. `field` reaches here too when the data + // source cannot enumerate an object's columns: a hand-typed name is worse + // than a list, and better than a list that can never fill. return ( {options.find((o) => o.value === value)?.label ?? value}; } - // DOM pass-through onto the combobox trigger — the widget's real focusable - // control (objectui#3318). `name` is withheld: the trigger is a button, not - // a submission control (same reasoning as #3306's SelectTrigger). - // - // NOTE this widget stays on the #3318 ledger regardless: its dependency- - // gated state (no `recipient_type` chosen yet — the state a fresh form and - // the registry sweep render) is a plain hint paragraph with no focusable - // control, so there is nothing there to carry the attribute. - const { name: _domName, ...triggerDomProps } = toDomProps(props); - return ( , + ); + return { ...view, onChange }; +} + +describe('RecipientPickerField — the `field` kind offers the object\'s user columns', () => { + it('reads the object named by the sibling object_name, not a target object', async () => { + const ds = schemaDataSource(); + renderFieldPicker(ds); + await waitFor(() => expect(ds.getObjectSchema).toHaveBeenCalled()); + expect(ds.getObjectSchema).toHaveBeenCalledWith('account'); + // The five record-picking kinds query a target object; this one has none. + expect(ds.find).not.toHaveBeenCalled(); + }); + + it('offers exactly the user-valued columns and withholds the rest', async () => { + const ds = schemaDataSource(); + renderFieldPicker(ds); + await waitFor(() => expect(ds.getObjectSchema).toHaveBeenCalled()); + + fireEvent.click(screen.getByRole('combobox')); + // Same-subject control for the four zeros below: the three offered labels + // are rendered by the very same open list. + for (const label of OFFERED) { + expect(await screen.findByText(label)).toBeInTheDocument(); + } + for (const label of WITHHELD) { + expect(screen.queryByText(label)).not.toBeInTheDocument(); + } + }); + + it('stores the field NAME, never a record id', async () => { + const ds = schemaDataSource(); + const { onChange } = renderFieldPicker(ds); + await waitFor(() => expect(ds.getObjectSchema).toHaveBeenCalled()); + + fireEvent.click(screen.getByRole('combobox')); + fireEvent.click(await screen.findByText('Owner Manager')); + await waitFor(() => expect(onChange).toHaveBeenCalledWith('owner_manager')); + }); + + it('accepts an array-shaped `fields` declaration as well as a name-keyed map', () => { + const asArray = deriveUserFields({ + fields: [ + { name: 'assignees', type: 'user', label: 'Assignees' }, + { name: 'name', type: 'text', label: 'Name' }, + ], + }); + expect(asArray.map((f) => f.name)).toEqual(['assignees']); + // Control: the map shape of the SAME two columns answers identically. + expect( + deriveUserFields({ + fields: { assignees: { type: 'user', label: 'Assignees' }, name: { type: 'text' } }, + }).map((f) => f.name), + ).toEqual(['assignees']); + }); + + it('offers a hidden user column, because the evaluator honours one', () => { + const derived = deriveUserFields({ + fields: { + assignees: { type: 'user', label: 'Assignees', hidden: true }, + name: { type: 'text', label: 'Name', hidden: true }, + }, + }); + // Withholding it would break the agreement in the other direction: an + // authorable, working configuration the picker refuses to offer. + expect(derived.map((f) => f.name)).toEqual(['assignees']); + }); +}); + +describe('RecipientPickerField — the picker filter agrees with the evaluator', () => { + it('honours exactly the two spellings of a user-valued column', () => { + expect(fieldHoldsUsers({ type: 'user' })).toBe(true); + expect(fieldHoldsUsers({ type: 'lookup', reference: 'sys_user' })).toBe(true); + expect(fieldHoldsUsers({ type: 'master_detail', reference: 'sys_user' })).toBe(true); + }); + + it('refuses everything else, including a lookup to another object', () => { + expect(fieldHoldsUsers({ type: 'text' })).toBe(false); + expect(fieldHoldsUsers({ type: 'lookup', reference: 'sys_team' })).toBe(false); + expect(fieldHoldsUsers({ type: 'master_detail', reference: 'account' })).toBe(false); + expect(fieldHoldsUsers({ type: 'lookup' })).toBe(false); + expect(fieldHoldsUsers(null)).toBe(false); + expect(fieldHoldsUsers('user')).toBe(false); + }); + + it('reads `reference` only — `reference_to` is a spelling the protocol refuses', () => { + expect(fieldHoldsUsers({ type: 'lookup', reference_to: 'sys_user' })).toBe(false); + // Control in the same breath: the accepted spelling on the same shape. + expect(fieldHoldsUsers({ type: 'lookup', reference: 'sys_user' })).toBe(true); + }); +}); + +describe('RecipientPickerField — what the `field` kind says when it cannot offer a list', () => { + it('asks for the shared object before anything else when object_name is unset', async () => { + const ds = schemaDataSource(); + renderFieldPicker(ds, { objectName: '' }); + expect(await screen.findByText('Select an object first.')).toBeInTheDocument(); + expect(ds.getObjectSchema).not.toHaveBeenCalled(); + expect(screen.queryByRole('combobox')).not.toBeInTheDocument(); + }); + + it('says the object has no user columns rather than "No matches"', async () => { + const ds = schemaDataSource({ name: 'invoice', fields: { total: { type: 'number' } } }); + renderFieldPicker(ds); + await waitFor(() => expect(ds.getObjectSchema).toHaveBeenCalled()); + + fireEvent.click(screen.getByRole('combobox')); + expect(await screen.findByText('No user fields on this object')).toBeInTheDocument(); + expect(screen.queryByText('No matches')).not.toBeInTheDocument(); + }); + + it('keeps the stored name visible and marks it when it is not a user column', async () => { + const ds = schemaDataSource(); + renderFieldPicker(ds, { value: 'account_team' }); + await waitFor(() => expect(ds.getObjectSchema).toHaveBeenCalled()); + + // Marked, not silently dropped: the evaluator grants NOBODY for it, and a + // control that just looked empty would hide a rule that still names it. + expect(await screen.findByText('account_team — not a user field')).toBeInTheDocument(); + }); + + it('degrades to the plain text input when the data source cannot list columns', async () => { + // No `getObjectSchema` — the "nothing breaks" promise the header makes. + const ds = { find: vi.fn().mockResolvedValue({ data: [] }) } as any; + const onChange = vi.fn(); + render( + , + ); + const box = screen.getByRole('textbox') as HTMLInputElement; + expect(box.value).toBe('assignees'); + fireEvent.change(box, { target: { value: 'owner_manager' } }); + expect(onChange).toHaveBeenCalledWith('owner_manager'); + }); + + it('renders the column label, not the raw name, when readonly', async () => { + const ds = schemaDataSource(); + renderFieldPicker(ds, { value: 'assignees', readonly: true }); + expect(await screen.findByText('Assignees')).toBeInTheDocument(); + expect(screen.queryByRole('combobox')).not.toBeInTheDocument(); + }); +}); diff --git a/packages/fields/src/widgets/useFieldTranslation.ts b/packages/fields/src/widgets/useFieldTranslation.ts index ed85fc1152..71e418f7f6 100644 --- a/packages/fields/src/widgets/useFieldTranslation.ts +++ b/packages/fields/src/widgets/useFieldTranslation.ts @@ -75,6 +75,17 @@ const FIELD_DEFAULTS: Record = { 'fields.recipient.selectBusinessUnit': 'Select a business unit', 'fields.recipient.selectPosition': 'Select a position', 'fields.recipient.selectUnitAndSubordinates': 'Select a business unit', + // objectui#7613 — the `field` recipient kind (maintainer ruling + // objectstack#14103, executor objectstack#15072). It picks a user-valued + // COLUMN of the shared object rather than a record, so its three sentences + // are about columns and none of the per-type placeholders above fits. + // `noUserFields` exists rather than reusing `fields.recipient.empty` + // ("No matches") because an empty list here is not a failed search: it is + // the object having no column the evaluator could read as users, and an + // admin who is not told that has no way to act on it. + 'fields.recipient.selectField': 'Select a user field', + 'fields.recipient.noUserFields': 'No user fields on this object', + 'fields.recipient.fieldNotUserTyped': '{{name}} — not a user field', 'fields.filterCondition.selectObjectFirst': 'Select an object first.', // objectstack#3896 — this used to be 'All records'. An empty criteria never // meant "share everything"; it meant the predicate was missing, and the diff --git a/packages/i18n/src/locales/ar.ts b/packages/i18n/src/locales/ar.ts index 15929bbb44..8b457fe09a 100644 --- a/packages/i18n/src/locales/ar.ts +++ b/packages/i18n/src/locales/ar.ts @@ -264,6 +264,9 @@ const ar = { selectBusinessUnit: "اختر وحدة عمل", selectPosition: "اختر منصباً", selectUnitAndSubordinates: "اختر وحدة عمل", + selectField: "اختر حقل مستخدم", + noUserFields: "لا توجد حقول مستخدم في هذا الكائن", + fieldNotUserTyped: "{{name}} — ليس حقل مستخدم", }, filterCondition: { selectObjectFirst: "اختر كائناً أولاً.", diff --git a/packages/i18n/src/locales/de.ts b/packages/i18n/src/locales/de.ts index 4a904eec21..a91effaf62 100644 --- a/packages/i18n/src/locales/de.ts +++ b/packages/i18n/src/locales/de.ts @@ -260,6 +260,9 @@ const de = { selectBusinessUnit: "Geschäftseinheit auswählen", selectPosition: "Position auswählen", selectUnitAndSubordinates: "Geschäftseinheit auswählen", + selectField: "Benutzerfeld auswählen", + noUserFields: "Dieses Objekt hat keine Benutzerfelder", + fieldNotUserTyped: "{{name}} — kein Benutzerfeld", }, filterCondition: { selectObjectFirst: "Wählen Sie zuerst ein Objekt.", diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts index f20e20352a..4aefcad702 100644 --- a/packages/i18n/src/locales/en.ts +++ b/packages/i18n/src/locales/en.ts @@ -296,6 +296,9 @@ const en = { selectBusinessUnit: 'Select a business unit', selectPosition: 'Select a position', selectUnitAndSubordinates: 'Select a business unit', + selectField: 'Select a user field', + noUserFields: 'No user fields on this object', + fieldNotUserTyped: '{{name}} — not a user field', }, filterCondition: { selectObjectFirst: 'Select an object first.', diff --git a/packages/i18n/src/locales/es.ts b/packages/i18n/src/locales/es.ts index b67fae63ab..224ad4967e 100644 --- a/packages/i18n/src/locales/es.ts +++ b/packages/i18n/src/locales/es.ts @@ -264,6 +264,9 @@ const es = { selectBusinessUnit: "Seleccionar una unidad de negocio", selectPosition: "Seleccionar un puesto", selectUnitAndSubordinates: "Seleccionar una unidad de negocio", + selectField: "Seleccionar un campo de usuario", + noUserFields: "Este objeto no tiene campos de usuario", + fieldNotUserTyped: "{{name}} — no es un campo de usuario", }, filterCondition: { selectObjectFirst: "Selecciona primero un objeto.", diff --git a/packages/i18n/src/locales/fr.ts b/packages/i18n/src/locales/fr.ts index d64f5638ac..ed808dbd2d 100644 --- a/packages/i18n/src/locales/fr.ts +++ b/packages/i18n/src/locales/fr.ts @@ -260,6 +260,9 @@ const fr = { selectBusinessUnit: "Sélectionner une unité opérationnelle", selectPosition: "Sélectionner un poste", selectUnitAndSubordinates: "Sélectionner une unité opérationnelle", + selectField: "Sélectionner un champ utilisateur", + noUserFields: "Cet objet n'a aucun champ utilisateur", + fieldNotUserTyped: "{{name}} — pas un champ utilisateur", }, filterCondition: { selectObjectFirst: "Sélectionnez d'abord un objet.", diff --git a/packages/i18n/src/locales/ja.ts b/packages/i18n/src/locales/ja.ts index 5199ceef9f..ef18164a89 100644 --- a/packages/i18n/src/locales/ja.ts +++ b/packages/i18n/src/locales/ja.ts @@ -260,6 +260,9 @@ const ja = { selectBusinessUnit: "事業単位を選択", selectPosition: "役職を選択", selectUnitAndSubordinates: "事業単位を選択", + selectField: "ユーザーフィールドを選択", + noUserFields: "このオブジェクトにユーザーフィールドはありません", + fieldNotUserTyped: "{{name}} — ユーザーフィールドではありません", }, filterCondition: { selectObjectFirst: "先にオブジェクトを選択してください。", diff --git a/packages/i18n/src/locales/ko.ts b/packages/i18n/src/locales/ko.ts index 67577be735..984b969778 100644 --- a/packages/i18n/src/locales/ko.ts +++ b/packages/i18n/src/locales/ko.ts @@ -260,6 +260,9 @@ const ko = { selectBusinessUnit: "사업 단위 선택", selectPosition: "직위 선택", selectUnitAndSubordinates: "사업 단위 선택", + selectField: "사용자 필드 선택", + noUserFields: "이 객체에는 사용자 필드가 없습니다", + fieldNotUserTyped: "{{name}} — 사용자 필드가 아닙니다", }, filterCondition: { selectObjectFirst: "먼저 객체를 선택하세요.", diff --git a/packages/i18n/src/locales/pt.ts b/packages/i18n/src/locales/pt.ts index f0b15ec16f..6f5078a5aa 100644 --- a/packages/i18n/src/locales/pt.ts +++ b/packages/i18n/src/locales/pt.ts @@ -259,6 +259,9 @@ const pt = { selectBusinessUnit: "Selecionar uma unidade de negócio", selectPosition: "Selecionar um cargo", selectUnitAndSubordinates: "Selecionar uma unidade de negócio", + selectField: "Selecionar um campo de usuário", + noUserFields: "Este objeto não tem campos de usuário", + fieldNotUserTyped: "{{name}} — não é um campo de usuário", }, filterCondition: { selectObjectFirst: "Selecione primeiro um objeto.", diff --git a/packages/i18n/src/locales/ru.ts b/packages/i18n/src/locales/ru.ts index 8bdacb6c2e..b58374786f 100644 --- a/packages/i18n/src/locales/ru.ts +++ b/packages/i18n/src/locales/ru.ts @@ -266,6 +266,9 @@ const ru = { selectBusinessUnit: "Выберите бизнес-подразделение", selectPosition: "Выберите должность", selectUnitAndSubordinates: "Выберите бизнес-подразделение", + selectField: "Выберите поле пользователя", + noUserFields: "В этом объекте нет полей пользователя", + fieldNotUserTyped: "{{name}} — не поле пользователя", }, filterCondition: { selectObjectFirst: "Сначала выберите объект.", diff --git a/packages/i18n/src/locales/zh.ts b/packages/i18n/src/locales/zh.ts index b68797f23f..f9360c2d3c 100644 --- a/packages/i18n/src/locales/zh.ts +++ b/packages/i18n/src/locales/zh.ts @@ -267,6 +267,9 @@ const zh = { selectBusinessUnit: '请选择业务单元', selectPosition: '请选择岗位', selectUnitAndSubordinates: '请选择业务单元', + selectField: '请选择用户字段', + noUserFields: '此对象没有用户字段', + fieldNotUserTyped: '{{name}} — 不是用户字段', }, filterCondition: { selectObjectFirst: '请先选择对象。',