Skip to content

Commit d0a1361

Browse files
fix(plugin-approvals): a new delegation starts with the Delegator set to you, and its help says why (#25420)
<!-- claude-projects-attribution: {"github_login":"os-sales"} --> _Requested by **Sales** · [project thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuACdPWFJTLDcTrByFCHLzqb)_ Fixes #25342 Clause-②: no Metadata-protocol: none ## What Setup → Delegations (OOO): the Delegator of `sys_approval_delegation` only ever offers the signed-in user, because `bindDelegationWriteGuard` admits nobody else. The field still opened at "Select…", and a search for a colleague answered "No options found" with no reason. - `delegator_id` now declares `defaultValue: 'current_user'`. A new delegation starts with its one admissible Delegator filled in. - The field's help now says why: "The user going out of office. You can delegate only your own approvals, so this is always you: it starts as you and offers no one else. Their individually-routed approver slots are rerouted while active." - The `lookupFilters`, the write guard and the approval-service filter are unchanged. `organization_id` is not touched; queued #25280 edits that field's help. ## Why this spelling I checked how the platform defaults a person field to the current user, on `origin/main` `830a63a680`: - `packages/spec/src/data/field.zod.ts` `defaultValue` says `current_user` is legal "on `user` or `lookup` with `reference: 'sys_user'` only". The spec's token table is `defaultValueTokenIssue` in `packages/spec/src/data/default-value-shape.ts`, and the constant is `DEFAULT_VALUE_TOKEN_CURRENT_USER` in `default-value-tokens.ts`. - The engine resolves it at insert time to the acting user's id: `packages/objectql/src/engine.ts`, the `isCurrentUserDefaultToken` branch of the field-default pass. With no authenticated user it leaves the field unset. - The one in-repo declaration is `examples/app-showcase/src/data/objects/field-zoo.object.ts` `f_owner` (`Field.user({ defaultValue: 'current_user' })`). No other platform object defaults a lookup to the current user, so this is the first such platform field. It uses the spelling the spec, the engine and the docs (`content/docs/data-modeling/field-types.mdx`) all name. `packages/spec` is not edited. ## Translations - `en` was regenerated with `node scripts/check-i18n-bundles.mjs --write`. - `es-ES` and `ja-JP` were byte copies of the old English help. They now carry translations, and the next extract dropped their two `source-hashes` entries. - `zh-CN` already had a translated help, so I updated it to the new meaning too. It has no hash entry, so nothing would have flagged it as stale. ## Tests `packages/plugins/plugin-approvals/src/sys-approval-delegation-delegator-picker.test.ts` keeps every existing pin. It gains one for the default: the declaration and the registered body carry `DEFAULT_VALUE_TOKEN_CURRENT_USER`, and a full `FieldSchema.safeParse` of the field is green, so the spec admits the token on this lookup. - `pnpm --filter @objectstack/plugin-approvals test`: Test Files 94 passed (94), Tests 1215 passed (1215). - `pnpm --filter @objectstack/plugin-approvals typecheck`: exit 0. `check:test-typecheck` reports OK. - Ablation: deleting `defaultValue: 'current_user',` (`scripts/ablation-replace.mjs`, anchor x1 -> x0) turned the new pin red (`expected undefined to be 'current_user'`, 1 failed | 9 passed). The file was restored to its HEAD blob and `git diff HEAD` was empty. The test imports the object from `src/`, so no build was needed. My first attempt was a refused anchor miss, so nothing ran on it. - Derived gate families: run locally on this head. The PR's check-runs carry the full set. ## Acceptance notes - **The console's form prefill is NOT MEASURED.** objectui answers 403 from this container, so I could not read how its create form treats a `current_user` default. The server side is pinned. If the console does not prefill from this token, that gap belongs to objectui and would hit the showcase `f_owner` field too. It is not a reason to change the spelling here. - Server-side writes behave as before. The engine default and the write guard's omitted-delegator stamp both name the acting user. A system write with no user still leaves the field unset, so `required` refuses it, and the existing pin holds that. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016g7nYP9wdNx1GDa5XDr7Uy --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 77392e9 commit d0a1361

9 files changed

Lines changed: 47 additions & 10 deletions
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/plugin-approvals': patch
3+
---
4+
5+
fix: a new out-of-office delegation opens with the Delegator already set to the signed-in user, and the field's help says why no one else can be chosen
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The Delegator of `sys_approval_delegation` (Setup → Delegations) only ever offers the signed-in user, because the delegation write guard admits nobody else. The field still opened empty, and a search for a colleague answered "No options found" with no reason.
11+
12+
- `delegator_id` now declares `defaultValue: 'current_user'`, the spec's runtime token for a `sys_user` lookup, so the one admissible value is filled in from the start. The write guard is unchanged and stays the enforcement.
13+
- The field's help now reads: "The user going out of office. You can delegate only your own approvals, so this is always you: it starts as you and offers no one else." The `es-ES`, `ja-JP` and `zh-CN` bundles carry the translated text.

‎packages/plugins/plugin-approvals/src/sys-approval-delegation-delegator-picker.test.ts‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,21 @@
2424
* 4. the guard is not widened: a create or an update naming anyone else is
2525
* still refused, `FORBIDDEN` / 403, and nothing is stored;
2626
* 5. `required` stays: a write with no acting user and no delegator is
27-
* refused.
27+
* refused;
28+
* 6. the field defaults to the acting user with the spec's runtime token
29+
* `current_user`, which the spec admits on a `sys_user` lookup, so a new
30+
* delegation opens with its one admissible Delegator already filled.
2831
*/
2932

3033
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
3134
import { ObjectQL } from '@objectstack/objectql';
3235
import { SqlDriver } from '@objectstack/driver-sql';
33-
import type { EngineCountOptions, EngineQueryOptions } from '@objectstack/spec/data';
36+
import {
37+
FieldSchema,
38+
DEFAULT_VALUE_TOKEN_CURRENT_USER,
39+
type EngineCountOptions,
40+
type EngineQueryOptions,
41+
} from '@objectstack/spec/data';
3442
import { SysApprovalDelegation } from './sys-approval-delegation.object.js';
3543
import { bindDelegationWriteGuard } from './lifecycle-hooks.js';
3644
import { installDefaultOrganization } from './default-organization.testkit.js';
@@ -121,6 +129,17 @@ describe('sys_approval_delegation — the Delegator picker offers only what the
121129
expect(registered.lookupFilters).toEqual(ACTING_USER_ONLY);
122130
});
123131

132+
it('defaults to the acting user with the spec\'s `current_user` token, and the spec admits it on this field', () => {
133+
const field = delegatorField();
134+
expect(field.defaultValue).toBe(DEFAULT_VALUE_TOKEN_CURRENT_USER);
135+
expect((engine.registry.getObject(OBJECT) as any).fields.delegator_id.defaultValue)
136+
.toBe(DEFAULT_VALUE_TOKEN_CURRENT_USER);
137+
// A token on a field it cannot default is refused at parse time; a full
138+
// parse of this field must be green.
139+
const parsed = FieldSchema.safeParse({ name: 'delegator_id', ...field });
140+
expect(parsed.success, JSON.stringify(parsed.error?.issues)).toBe(true);
141+
});
142+
124143
it.each(USERS.map((u) => [u.id]))('the picker offers %s themselves and nobody else', async (caller) => {
125144
const candidates = await engine.find('sys_user', pickerQuery(), asUser(caller));
126145
expect(candidates.map((r: any) => r.id)).toEqual([caller]);

‎packages/plugins/plugin-approvals/src/sys-approval-delegation.object.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,20 @@ export const SysApprovalDelegation = ObjectSchema.create({
9494
// `lookupFilters` is the picker's candidate query, and the engine resolves
9595
// `{current_user_id}` against the caller on every read. Presentation only
9696
// — the guard stays the enforcement, and what a write may store is
97-
// unchanged. Pinned in `sys-approval-delegation-delegator-picker.test.ts`.
97+
// unchanged. Since that one value is always the acting user, the field
98+
// also defaults to it: `defaultValue: 'current_user'` is the spec's
99+
// runtime token for a `sys_user` lookup (`DEFAULT_VALUE_TOKENS`), so a new
100+
// delegation opens with the Delegator already filled instead of at an
101+
// empty picker. Pinned in `sys-approval-delegation-delegator-picker.test.ts`.
98102
delegator_id: Field.lookup('sys_user', {
99103
label: 'Delegator',
100104
required: true,
105+
defaultValue: 'current_user',
101106
lookupFilters: [{ field: 'id', operator: 'eq', value: '{current_user_id}' }],
102107
group: 'Delegation',
103-
description: 'The user going out of office; their individually-routed approver slots are rerouted while active.',
108+
description:
109+
'The user going out of office. You can delegate only your own approvals, so this is always you: '
110+
+ 'it starts as you and offers no one else. Their individually-routed approver slots are rerouted while active.',
104111
}),
105112

106113
delegate_id: Field.lookup('sys_user', {

‎packages/plugins/plugin-approvals/src/translations/en.objects.generated.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
354354
},
355355
delegator_id: {
356356
label: "Delegator",
357-
help: "The user going out of office; their individually-routed approver slots are rerouted while active."
357+
help: "The user going out of office. You can delegate only your own approvals, so this is always you: it starts as you and offers no one else. Their individually-routed approver slots are rerouted while active."
358358
},
359359
delegate_id: {
360360
label: "Delegate",

‎packages/plugins/plugin-approvals/src/translations/es-ES.objects.generated.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
354354
},
355355
delegator_id: {
356356
label: "Delegator",
357-
help: "The user going out of office; their individually-routed approver slots are rerouted while active."
357+
help: "El usuario que se ausenta. Solo puedes delegar tus propias aprobaciones, así que siempre eres tú: el campo empieza contigo y no ofrece a nadie más. Mientras la delegación está activa, los puestos de aprobador asignados a esa persona se reenrutan."
358358
},
359359
delegate_id: {
360360
label: "Delegate",

‎packages/plugins/plugin-approvals/src/translations/es-ES.source-hashes.generated.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ export const esESGeneratedSourceHashes: Readonly<Record<string, string>> = {
2626
"objects.sys_approval_delegation.fields.created_at.label": "1f02d416befb595b",
2727
"objects.sys_approval_delegation.fields.delegate_id.help": "9cfc641f7b0b04f9",
2828
"objects.sys_approval_delegation.fields.delegate_id.label": "afd6d8733dc5bc14",
29-
"objects.sys_approval_delegation.fields.delegator_id.help": "c4686c5c9f24e0be",
3029
"objects.sys_approval_delegation.fields.delegator_id.label": "f76b1f95f2fdabff",
3130
"objects.sys_approval_delegation.fields.display_title.help": "2831a1ffde72b425",
3231
"objects.sys_approval_delegation.fields.display_title.label": "70f7aadecce647a5",

‎packages/plugins/plugin-approvals/src/translations/ja-JP.objects.generated.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
354354
},
355355
delegator_id: {
356356
label: "Delegator",
357-
help: "The user going out of office; their individually-routed approver slots are rerouted while active."
357+
help: "不在になるユーザーです。委任できるのは自分の承認だけなので、常にあなた自身です。最初からあなたが設定され、ほかのユーザーは選べません。有効な間、その人に個別に割り当てられた承認者枠は振り替えられます。"
358358
},
359359
delegate_id: {
360360
label: "Delegate",

‎packages/plugins/plugin-approvals/src/translations/ja-JP.source-hashes.generated.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ export const jaJPGeneratedSourceHashes: Readonly<Record<string, string>> = {
2525
"objects.sys_approval_delegation.fields.created_at.label": "1f02d416befb595b",
2626
"objects.sys_approval_delegation.fields.delegate_id.help": "9cfc641f7b0b04f9",
2727
"objects.sys_approval_delegation.fields.delegate_id.label": "afd6d8733dc5bc14",
28-
"objects.sys_approval_delegation.fields.delegator_id.help": "c4686c5c9f24e0be",
2928
"objects.sys_approval_delegation.fields.delegator_id.label": "f76b1f95f2fdabff",
3029
"objects.sys_approval_delegation.fields.display_title.help": "2831a1ffde72b425",
3130
"objects.sys_approval_delegation.fields.display_title.label": "70f7aadecce647a5",

‎packages/plugins/plugin-approvals/src/translations/zh-CN.objects.generated.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
354354
},
355355
delegator_id: {
356356
label: "委派人",
357-
help: "即将不在岗的用户;规则生效期间,路由到其个人的审批人槽位将被改派。"
357+
help: "即将不在岗的用户。你只能委派自己的审批,所以这里始终是你:默认填入你本人,不提供其他人选。规则生效期间,路由到其个人的审批人槽位将被改派。"
358358
},
359359
delegate_id: {
360360
label: "候补人",

0 commit comments

Comments
 (0)