Skip to content

Commit bc9e945

Browse files
baozhoutaoclaude
andcommitted
fix(sharing): write the sharing-rule help text for the admin who reads it (#3821)
The field `description`s on `sys_sharing_rule` render as help text under each input in Setup. They were written as notes for the next engineer: 接收访问权限的主体类型——求值时会展开为用户授权。`business_unit` 会沿 parent_business_unit_id 树展开;`team` 为扁平结构(better-auth);`position` 展开为该岗位的任职人(岗位为扁平结构,ADR-0090 D3)…(ADR-0057 D5)。 ADR numbers, table and column names, a third-party library, and enum machine values the dropdown never displays — it shows 用户 / 团队 / 业务单元 / 岗位. Several were also stale, some of them because of this very issue's UI work: `recipient_id` still said "fill in a business-unit id / team id / position name" after it became a record picker; `object_name` still said "short object name (e.g. opportunity, account)" after it became an object picker; `criteria_json` still described hand-written `FilterCondition` JSON after it became a visual builder — and carried "(FilterCondition JSON)" in its LABEL. All of it rewritten for the reader who actually sees it, in en / zh-CN / ja-JP / es-ES. The engine detail was already in the object's doc comment, which is where it stays; a comment on the first field now says so, so the next author doesn't put it back. `active` can finally state what it does — turning it off withdraws the access — which only became true with the reconcile fix in this branch. The list page's subtitle and the `managed_by` / `customized` help got the same treatment. Browser-verified in the showcase Console: no ADR, table, column, enum or library name survives anywhere in the create dialog, the detail page or the list header. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 5ccfb70 commit bc9e945

6 files changed

Lines changed: 71 additions & 52 deletions

File tree

.changeset/sharing-rule-unknown-sort-and-stale-help.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,19 @@ through the plain data API used to orphan its grants. Seeding and package
4343
bootstrap write with `isSystem` and are skipped; `kernel:bootstrapped` already
4444
backfills those. Reconciliation is best-effort and never fails the write.
4545

46+
**The dialog's help text was engineering notes, shown to tenant admins.** The
47+
field descriptions on `sys_sharing_rule` render under each input in Setup, and
48+
they cited ADR numbers, table and column names (`parent_business_unit_id`,
49+
`sys_business_unit`), enum machine values the dropdown never shows
50+
(`business_unit`, `team`), a third-party library (better-auth), and engine
51+
vocabulary ("evaluation", "lifecycle"). Several were also stale: they still told
52+
admins to type an id or hand-write a `FilterCondition` after those inputs became
53+
a record picker and a visual builder. Rewritten for the reader who actually sees
54+
them — the implementation detail was already in the object's doc comment, which
55+
is where it stays. `criteria_json`'s LABEL loses its "(FilterCondition JSON)"
56+
suffix for the same reason, and `active` can finally say what it now does:
57+
turning it off withdraws the access.
58+
4659
Also refreshes the `sys_sharing_rule` help text in the zh-CN / ja-JP / es-ES
4760
translation bundles, which still described `recipient_type` in terms of
4861
`department` (the enum value is `business_unit`) and told admins to enter a

packages/plugins/plugin-sharing/src/objects/sys-sharing-rule.object.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export const SysSharingRule = ObjectSchema.create({
3737
// We still recommend `defineSharingRule({...})` for repo-controlled
3838
// baselines, but admins can safely create/edit/delete from the UI.
3939
userActions: { create: true, edit: true, delete: true, import: false },
40-
description: 'Declarative sharing rule that auto-materialises sys_record_share grants. Authored via defineSharingRule() in code or the Studio criteria builder.',
40+
description: 'Grants a group of people access to the records that match a condition.',
4141
displayNameField: 'name',
4242
nameField: 'name', // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField)
4343
titleFormat: '{label}',
@@ -99,7 +99,13 @@ export const SysSharingRule = ObjectSchema.create({
9999
label: 'Name',
100100
required: true,
101101
maxLength: 100,
102-
description: 'Unique snake_case rule name',
102+
// Field `description`s on this object are ADMIN-FACING help text rendered
103+
// under each input in Setup — not notes for the next engineer. They must
104+
// not name tables, columns, enum values, ADRs or third-party libraries:
105+
// the reader is a tenant admin who sees only the labels in the dropdown
106+
// (objectstack#3821). Implementation detail belongs in the doc comments
107+
// above, where it already is.
108+
description: 'Identifies the rule. Lowercase letters, digits and underscores only.',
103109
group: 'Identity',
104110
}),
105111

@@ -124,20 +130,20 @@ export const SysSharingRule = ObjectSchema.create({
124130
// instead of a free-text machine-name input. Falls back to a text input
125131
// when the `field:object-ref` widget is unavailable.
126132
widget: 'object-ref',
127-
description: 'Short object name (e.g. opportunity, account)',
133+
description: 'The object whose records this rule shares.',
128134
group: 'Target',
129135
}),
130136

131137
criteria_json: Field.textarea({
132-
label: 'Criteria (FilterCondition JSON)',
138+
label: 'Criteria',
133139
required: false,
134140
// Rendered as a visual criteria builder scoped to the selected object's
135141
// fields (dependsOn: object_name), storing the same JSON FilterCondition.
136142
// An "Edit as JSON" fallback keeps hand-authored / advanced filters
137143
// editable. Falls back to a textarea when the widget is unavailable.
138144
widget: 'filter-condition',
139145
dependsOn: ['object_name'],
140-
description: 'JSON FilterCondition matched against records of object_name. Empty = match all.',
146+
description: 'Which records to share. Leave empty to share every record of the object.',
141147
group: 'Target',
142148
}),
143149

@@ -150,7 +156,9 @@ export const SysSharingRule = ObjectSchema.create({
150156
label: 'Recipient Type',
151157
required: true,
152158
defaultValue: 'business_unit',
153-
description: 'Kind of principal that receives access — expanded to user grants at evaluation time. `business_unit` walks the parent_business_unit_id tree; `team` is flat (better-auth); `position` expands the position\'s holders (positions are flat, ADR-0090 D3); `unit_and_subordinates` expands the named business unit PLUS every descendant unit\'s members via the sys_business_unit tree (ADR-0057 D5).',
159+
// The engine detail this used to spell out (which tree is walked, which
160+
// expansion is flat, the ADRs behind each) lives in the class doc above.
161+
description: 'Who receives access. Picking a team, business unit or position gives access to everyone in it. "Business unit and subordinates" also covers every unit below the one you pick.',
154162
group: 'Recipient',
155163
},
156164
),
@@ -166,7 +174,7 @@ export const SysSharingRule = ObjectSchema.create({
166174
// back to a text input when the widget is unavailable.
167175
widget: 'recipient-picker',
168176
dependsOn: ['recipient_type'],
169-
description: 'business-unit id / team id / position name / user id depending on recipient_type',
177+
description: 'The specific user, team, business unit or position that receives access.',
170178
group: 'Recipient',
171179
}),
172180

@@ -184,7 +192,7 @@ export const SysSharingRule = ObjectSchema.create({
184192
label: 'Active',
185193
required: false,
186194
defaultValue: true,
187-
description: 'Only active rules participate in lifecycle evaluation',
195+
description: 'Turn off to withdraw the access this rule granted, without deleting the rule.',
188196
group: 'Lifecycle',
189197
}),
190198

@@ -203,8 +211,7 @@ export const SysSharingRule = ObjectSchema.create({
203211
readonly: true,
204212
defaultValue: 'admin',
205213
description:
206-
'Record provenance (unified tri-state, A4 #2920): platform = framework built-in / ' +
207-
'package = app/package-declared (boot-seeded) / admin = tenant-created in Setup.',
214+
'Where this rule came from: built into the platform, installed with an app, or created here in Setup.',
208215
options: [
209216
{ value: 'platform', label: 'Platform' },
210217
{ value: 'package', label: 'Package' },
@@ -219,8 +226,7 @@ export const SysSharingRule = ObjectSchema.create({
219226
readonly: true,
220227
defaultValue: false,
221228
description:
222-
'Set when an admin edits a package-declared rule; boot seeding will no longer ' +
223-
'overwrite the row (deactivations survive redeploys). Meaningless on admin rows.',
229+
'Set once you edit a rule that came with an app, so your changes are kept when the app is updated.',
224230
group: 'System',
225231
}),
226232

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

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
102102
sys_sharing_rule: {
103103
label: "Sharing Rule",
104104
pluralLabel: "Sharing Rules",
105-
description: "Declarative sharing rule that auto-materialises sys_record_share grants. Authored via defineSharingRule() in code or the Studio criteria builder.",
105+
description: "Grants a group of people access to the records that match a condition.",
106106
fields: {
107107
id: {
108108
label: "Rule ID"
@@ -113,7 +113,7 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
113113
},
114114
name: {
115115
label: "Name",
116-
help: "Unique snake_case rule name"
116+
help: "Identifies the rule. Lowercase letters, digits and underscores only."
117117
},
118118
label: {
119119
label: "Display Label"
@@ -123,15 +123,15 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
123123
},
124124
object_name: {
125125
label: "Object",
126-
help: "Short object name (e.g. opportunity, account)"
126+
help: "The object whose records this rule shares."
127127
},
128128
criteria_json: {
129-
label: "Criteria (FilterCondition JSON)",
130-
help: "JSON FilterCondition matched against records of object_name. Empty = match all."
129+
label: "Criteria",
130+
help: "Which records to share. Leave empty to share every record of the object."
131131
},
132132
recipient_type: {
133133
label: "Recipient Type",
134-
help: "Kind of principal that receives access — expanded to user grants at evaluation time. `business_unit` walks the parent_business_unit_id tree; `team` is flat (better-auth); `position` expands the position's holders (positions are flat, ADR-0090 D3); `unit_and_subordinates` expands the named business unit PLUS every descendant unit's members via the sys_business_unit tree (ADR-0057 D5).",
134+
help: "Who receives access. Picking a team, business unit or position gives access to everyone in it. \"Business unit and subordinates\" also covers every unit below the one you pick.",
135135
options: {
136136
user: "user",
137137
team: "team",
@@ -142,7 +142,7 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
142142
},
143143
recipient_id: {
144144
label: "Recipient",
145-
help: "business unit id / team id / position name / user id depending on recipient_type"
145+
help: "The specific user, team, business unit or position that receives access."
146146
},
147147
access_level: {
148148
label: "Access Level",
@@ -154,11 +154,11 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
154154
},
155155
active: {
156156
label: "Active",
157-
help: "Only active rules participate in lifecycle evaluation"
157+
help: "Turn off to withdraw the access this rule granted, without deleting the rule."
158158
},
159159
managed_by: {
160160
label: "Managed By",
161-
help: "Record provenance (unified tri-state, A4 #2920): platform = framework built-in / package = app/package-declared (boot-seeded) / admin = tenant-created in Setup.",
161+
help: "Where this rule came from: built into the platform, installed with an app, or created here in Setup.",
162162
options: {
163163
platform: "Platform",
164164
package: "Package",
@@ -167,7 +167,7 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
167167
},
168168
customized: {
169169
label: "Customized",
170-
help: "Set when an admin edits a package-declared rule; boot seeding will no longer overwrite the row (deactivations survive redeploys). Meaningless on admin rows."
170+
help: "Set once you edit a rule that came with an app, so your changes are kept when the app is updated."
171171
},
172172
created_at: {
173173
label: "Created At"

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

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
102102
sys_sharing_rule: {
103103
label: "Regla de compartición",
104104
pluralLabel: "Reglas de compartición",
105-
description: "Regla de compartición declarativa que materializa automáticamente concesiones de sys_record_share. Se define mediante defineSharingRule() en código o con el generador de criterios de Studio.",
105+
description: "Da acceso a un grupo de personas a los registros que cumplen una condición.",
106106
fields: {
107107
id: {
108108
label: "ID de regla"
@@ -113,7 +113,7 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
113113
},
114114
name: {
115115
label: "Nombre",
116-
help: "Nombre de regla snake_case único."
116+
help: "Identifica la regla. Solo minúsculas, dígitos y guiones bajos."
117117
},
118118
label: {
119119
label: "Nombre visible"
@@ -123,15 +123,15 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
123123
},
124124
object_name: {
125125
label: "Objeto",
126-
help: "Nombre corto del objeto (p. ej. opportunity, account)."
126+
help: "El objeto cuyos registros comparte esta regla."
127127
},
128128
criteria_json: {
129-
label: "Criterios (JSON de FilterCondition)",
130-
help: "FilterCondition JSON comparado con los registros de object_name. Vacío = coincide con todos."
129+
label: "Criterios",
130+
help: "Qué registros se comparten. Déjalo vacío para compartir todos los registros del objeto."
131131
},
132132
recipient_type: {
133133
label: "Tipo de destinatario",
134-
help: "Tipo de principal que recibe acceso; se expande a concesiones de usuario durante la evaluación. `business_unit` recorre el árbol parent_business_unit_id; `team` es plano (better-auth); `position` se expande a quienes ocupan el puesto (los puestos son planos, ADR-0090 D3); `unit_and_subordinates` expande la unidad de negocio indicada MÁS los miembros de todas sus unidades descendientes a través del árbol sys_business_unit (ADR-0057 D5).",
134+
help: "Quién recibe el acceso. Al elegir un equipo, una unidad de negocio o un puesto, lo reciben todas las personas que lo integran. «Unidad de negocio y subordinadas» abarca además todas las unidades por debajo de la elegida.",
135135
options: {
136136
user: "Usuario",
137137
team: "Equipo",
@@ -142,7 +142,7 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
142142
},
143143
recipient_id: {
144144
label: "Destinatario",
145-
help: "ID de unidad de negocio / ID de equipo / nombre del puesto / ID del usuario según recipient_type."
145+
help: "El usuario, equipo, unidad de negocio o puesto concreto que recibe el acceso."
146146
},
147147
access_level: {
148148
label: "Nivel de acceso",
@@ -154,11 +154,11 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
154154
},
155155
active: {
156156
label: "Activo",
157-
help: "Solo las reglas activas participan en la evaluación del ciclo de vida."
157+
help: "Desactívala para retirar el acceso que concedió, sin eliminar la regla."
158158
},
159159
managed_by: {
160160
label: "Gestionado por",
161-
help: "Procedencia del registro (tri-estado unificado, A4 #2920): platform = integrado en el framework / package = declarado por app o paquete (sembrado al arrancar) / admin = creado por el inquilino en Setup.",
161+
help: "De dónde viene esta regla: integrada en la plataforma, instalada con una app o creada aquí.",
162162
options: {
163163
platform: "Plataforma",
164164
package: "Paquete",
@@ -167,7 +167,7 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
167167
},
168168
customized: {
169169
label: "Personalizado",
170-
help: "Se establece cuando un administrador edita una regla declarada por un paquete; a partir de entonces el sembrado de arranque deja de sobrescribir la fila (las desactivaciones sobreviven a los redespliegues). Sin sentido en filas admin."
170+
help: "Se marca al editar una regla que vino con una app, para conservar tus cambios cuando la app se actualice."
171171
},
172172
created_at: {
173173
label: "Creado el"

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

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
102102
sys_sharing_rule: {
103103
label: "共有ルール",
104104
pluralLabel: "共有ルール",
105-
description: "sys_record_share 付与を自動マテリアライズする宣言的共有ルール。コードの defineSharingRule() または Studio の条件ビルダーで作成します。",
105+
description: "条件に一致するレコードへのアクセスを、指定した人たちに与えます。",
106106
fields: {
107107
id: {
108108
label: "ルール ID"
@@ -113,7 +113,7 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
113113
},
114114
name: {
115115
label: "名前",
116-
help: "一意の snake_case ルール名"
116+
help: "ルールの識別名。半角小文字・数字・アンダースコアのみ使用できます。"
117117
},
118118
label: {
119119
label: "表示名"
@@ -123,15 +123,15 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
123123
},
124124
object_name: {
125125
label: "オブジェクト",
126-
help: "短いオブジェクト名(例: opportunity、account)"
126+
help: "どのオブジェクトのレコードを共有するか。"
127127
},
128128
criteria_json: {
129-
label: "条件(FilterCondition JSON)",
130-
help: "object_name のレコードに対してマッチする JSON FilterCondition。空 = すべてにマッチ。"
129+
label: "条件",
130+
help: "どのレコードを共有するか。空欄にすると、そのオブジェクトのすべてのレコードを共有します。"
131131
},
132132
recipient_type: {
133133
label: "受信者タイプ",
134-
help: "アクセスを受け取るプリンシパルの種別 — 評価時にユーザー付与に展開されます。`business_unit` は parent_business_unit_id ツリーをたどります。`team` はフラット(better-auth)。`position` はそのポジションの在任者に展開されます(ポジションはフラット、ADR-0090 D3)。`unit_and_subordinates` は指定したビジネスユニットに加え、sys_business_unit ツリー上の全下位ユニットのメンバーにも展開されます(ADR-0057 D5)。",
134+
help: "誰にアクセスを与えるか。チーム・事業単位・役職を選ぶと、そこに属する全員がアクセスできます。「事業単位と下位組織」を選ぶと、その配下のすべての単位も対象になります。",
135135
options: {
136136
user: "ユーザー",
137137
team: "チーム",
@@ -142,7 +142,7 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
142142
},
143143
recipient_id: {
144144
label: "受信者",
145-
help: "recipient_type に応じたビジネスユニット ID / チーム ID / ポジション名 / ユーザー ID"
145+
help: "アクセスを与える具体的なユーザー・チーム・事業単位・役職。"
146146
},
147147
access_level: {
148148
label: "アクセスレベル",
@@ -154,11 +154,11 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
154154
},
155155
active: {
156156
label: "有効",
157-
help: "有効なルールのみがライフサイクル評価に参加します"
157+
help: "オフにすると、このルールが与えたアクセスをただちに取り消します(ルール自体は残ります)。"
158158
},
159159
managed_by: {
160160
label: "管理元",
161-
help: "レコードの出所(統一三状態、A4 #2920): platform = フレームワーク組み込み / package = アプリ・パッケージ宣言(起動時シード)/ admin = テナントが Setup で作成。",
161+
help: "このルールの出所:プラットフォーム内蔵、アプリと一緒に導入、またはここで作成。",
162162
options: {
163163
platform: "プラットフォーム",
164164
package: "パッケージ",
@@ -167,7 +167,7 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
167167
},
168168
customized: {
169169
label: "カスタマイズ済み",
170-
help: "管理者がパッケージ宣言のルールを編集すると設定されます。以後、起動時シードはこの行を上書きしません(無効化は再デプロイ後も保持)。admin 行では意味を持ちません。"
170+
help: "アプリ付属のルールを編集すると設定され、アプリ更新時も変更内容が保持されます。"
171171
},
172172
created_at: {
173173
label: "作成日時"

0 commit comments

Comments
 (0)