diff --git a/.changeset/30944-skills-field-patches-example-hotcrm.md b/.changeset/30944-skills-field-patches-example-hotcrm.md new file mode 100644 index 00000000000..552d38f7b88 --- /dev/null +++ b/.changeset/30944-skills-field-patches-example-hotcrm.md @@ -0,0 +1,10 @@ +--- +'@objectstack/skills': patch +--- + +docs(skills): the `fieldPatches` example relabels HotCRM's `industry` and patches options only on a field that declares its own + +Clause-②: no +Metadata-protocol: none + +The `objectstack-data` skill's one `fieldPatches` example patched an option of `crm_account.industry`. HotCRM binds that field to the shared `industry` picklist, so a verbatim copy was refused at install and by `os validate` (`picklist-option`). The example now sets `industry`'s `label` only and patches an option of `type`, which declares its own `options` (`customer` → `Client`). The added bytes are paid by deleting a restatement in the same file: the `.custom` paragraph's closing clause "a base field changes only through `fieldPatches`", whose facts the relabel paragraph above it already states. No token ceiling moves. No schema, key or behaviour changes. diff --git a/.changeset/30944-spec-field-patches-example-hotcrm.md b/.changeset/30944-spec-field-patches-example-hotcrm.md new file mode 100644 index 00000000000..3584e0f9461 --- /dev/null +++ b/.changeset/30944-spec-field-patches-example-hotcrm.md @@ -0,0 +1,10 @@ +--- +'@objectstack/spec': patch +--- + +docs(spec): the `fieldPatches` description's example no longer patches an option of a picklist-bound field + +Clause-②: no +Metadata-protocol: none + +`ObjectExtensionSchema.fieldPatches`'s description and the `ObjectExtensionSchema` TSDoc taught `industry: { label: 'Sector', options: { technology: { label: 'Tech' } } }`. HotCRM's `crm_account.industry` takes its options from a shared picklist, so the install door and `os validate` refuse that patch (`picklist-option`). The description now says an option of a picklist-bound field is never patchable and only a field declaring its own `options` takes an option patch, and its example sets `industry`'s label and help text and patches `type`'s `customer` option. The generated `references/data/object.mdx` follows. Description text only: no schema, key or behaviour changes. diff --git a/content/docs/data-modeling/object-extensions.mdx b/content/docs/data-modeling/object-extensions.mdx index b892c60b09a..8c8bdd5276e 100644 --- a/content/docs/data-modeling/object-extensions.mdx +++ b/content/docs/data-modeling/object-extensions.mdx @@ -110,7 +110,10 @@ export const AccountWordingExtension = defineObjectExtension({ industry: { label: 'Sector', inlineHelpText: 'The market the account sells into.', - options: { technology: { label: 'Tech' } }, // addressed by the stored value + // no `options` here: `industry` takes its options from a shared picklist + }, + type: { + options: { customer: { label: 'Client' } }, // addressed by the stored value }, }, }); diff --git a/content/docs/references/data/object.mdx b/content/docs/references/data/object.mdx index 59ea4867d42..d3cf4b02a58 100644 --- a/content/docs/references/data/object.mdx +++ b/content/docs/references/data/object.mdx @@ -580,7 +580,7 @@ const result = ApiMethod.parse(data); | :--- | :--- | :--- | :--- | | **extend** | `string` | ✅ | Target object name (FQN) to extend | | **fields** | `Record; description?: string; … }>` | optional | Fields to add/override, keyed by field name (lowercase snake_case, the key grammar of an object's `fields`). | -| **fieldPatches** | `Record` | optional | Display patches of the target object's own fields, keyed by field name (lowercase snake_case, the key grammar of an object's `fields`): change a vendor field's display properties (BASE_FIELD_DISPLAY_WHITELIST: `label`, `description`, `inlineHelpText`, `placeholder` on the field, `label` on an option under `options`, keyed by its stored value) without taking the field over, so its type, values and rules stay the vendor's and its upgrades still apply (ADR-0146 D10). Applied by the registry merge (`mergeObjectDefinitions`) after the additive merges, on every object read: a tenant's own text keeps winning, and between two extensions the higher `priority` wins. A patch naming a field or option value the target does not have is refused at install and by `os validate`; a field the same extension replaces in `fields` is refused here. Example: `fieldPatches: { industry: { label: 'Sector', inlineHelpText: 'The market the account sells into', options: { technology: { label: 'Tech' } } } }`. | +| **fieldPatches** | `Record` | optional | Display patches of the target object's own fields, keyed by field name (lowercase snake_case, the key grammar of an object's `fields`): change a vendor field's display properties (BASE_FIELD_DISPLAY_WHITELIST: `label`, `description`, `inlineHelpText`, `placeholder` on the field, `label` on an option under `options`, keyed by its stored value) without taking the field over, so its type, values and rules stay the vendor's and its upgrades still apply (ADR-0146 D10). Applied by the registry merge (`mergeObjectDefinitions`) after the additive merges, on every object read: a tenant's own text keeps winning, and between two extensions the higher `priority` wins. A patch naming a field or option value the target does not have is refused at install and by `os validate`; a field the same extension replaces in `fields` is refused here. An option of a field bound to a shared `picklist` is never patchable: its labels are the picklist's, so only a field declaring its own `options` takes an option patch. Example: `fieldPatches: { industry: { label: 'Sector', inlineHelpText: 'The market the account sells into' }, type: { options: { customer: { label: 'Client' } } } }`. | | **label** | `string` | optional | Override label for the extended object | | **pluralLabel** | `string` | optional | Override plural label for the extended object | | **description** | `string` | optional | Override description for the extended object | diff --git a/packages/spec/src/data/object.zod.ts b/packages/spec/src/data/object.zod.ts index 0eca424ae49..5a496a8b13f 100644 --- a/packages/spec/src/data/object.zod.ts +++ b/packages/spec/src/data/object.zod.ts @@ -5112,8 +5112,10 @@ function fieldPatchesDescription(): string { '(`mergeObjectDefinitions`) after the additive merges, on every object read: a tenant\'s own text keeps ' + 'winning, and between two extensions the higher `priority` wins. A patch naming a field or option value ' + 'the target does not have is refused at install and by `os validate`; a field the same extension ' + - 'replaces in `fields` is refused here. Example: `fieldPatches: { industry: { label: \'Sector\', ' + - 'inlineHelpText: \'The market the account sells into\', options: { technology: { label: \'Tech\' } } } }`.' + 'replaces in `fields` is refused here. An option of a field bound to a shared `picklist` is never ' + + 'patchable: its labels are the picklist\'s, so only a field declaring its own `options` takes an option ' + + 'patch. Example: `fieldPatches: { industry: { label: \'Sector\', inlineHelpText: \'The market the ' + + 'account sells into\' }, type: { options: { customer: { label: \'Client\' } } } }`.' ); } @@ -5128,7 +5130,9 @@ function fieldPatchesDescription(): string { * fields: { sales_stage: Field.select([...]) }, * // A base field's display properties only (ADR-0146 D10), applied by the * // registry merge (`mergeObjectDefinitions`) — see `fieldPatches`. - * fieldPatches: { industry: { label: 'Sector', options: { technology: { label: 'Tech' } } } }, + * // Where `industry` is picklist-bound, as on HotCRM's `crm_account`, its options are not + * // patchable (its display keys are); an option patch goes on a field declaring its own options. + * fieldPatches: { industry: { label: 'Sector' }, type: { options: { customer: { label: 'Client' } } } }, * }] * ``` */ diff --git a/skills/objectstack-data/SKILL.md b/skills/objectstack-data/SKILL.md index ccadff8e984..64855998ebc 100644 --- a/skills/objectstack-data/SKILL.md +++ b/skills/objectstack-data/SKILL.md @@ -361,7 +361,7 @@ values and rules stay the owner's. A key the tenant edited keeps the tenant's text; between two extensions the higher `priority` wins. A patch naming a field or option value the target lacks is refused at install and by `os validate`, and a `picklist`-bound field's options are never patchable. Example: -`defineObjectExtension({ extend: 'crm_account', fieldPatches: { industry: { label: 'Sector', options: { technology: { label: 'Tech' } } } } })`. +`defineObjectExtension({ extend: 'crm_account', fieldPatches: { industry: { label: 'Sector' }, type: { options: { customer: { label: 'Client' } } } } })`. **An object an installed package ships** (ADR-0146 D10) is extended from the base's paired `.custom` package (`hotcrm.custom`; **objectstack-platform** @@ -369,7 +369,7 @@ base's paired `.custom` package (`hotcrm.custom`; **objectstack-platform** declaration. Every field the pair adds ends in `__c` (`region__c`). The suffix is reserved to the pair, so a `__c` field in any other package, or an unsuffixed field in a `.custom` extension, is refused at save, publish and install and by -`os validate` / `os build`; a base field changes only through `fieldPatches`. +`os validate` / `os build`. Studio's **Add field** on a base object writes the same extension, as an `object_extension` item of the pair.