Skip to content

Commit 9a9823d

Browse files
fix(example-crm): full-name contact titles, a complete zh-CN bundle, declared highlight strips and a user-typed lead owner (#28886)
domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y <!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9Ubb23RtUpCQPUEM3SvkuwNQzr)_ Fixes #28870 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:改 `examples/app-crm` 的元数据,另把 `scripts/i18n-coverage-baseline.json` 里 app-crm 的未翻译上限从 101 写成 0。联系人标题改为全名(`nameField: 'full_name'`,公式声明 `returnType: 'text'`);五个对象的复数名去掉「列表」;zh-CN 词条补齐导航、字段、选项、仪表板、数据集、对象描述、校验提示和欢迎页(`os lint --i18n-strict` 未翻译数 101 → 0);客户、联系人、商机、线索、活动各声明 `highlightFields`(不含阶段字段,阶段已由阶段路径显示);线索「分配给」改为用户字段;误挂在 Acme 下的 "Discovery Call — Bletchley Systems" 改名为 "Discovery Call — Acme Platform Renewal";仪表板说明改写成业务语言。 - **为什么改**:#26676 裁决 B 把 app-crm 定为预览面,它的元数据错误在评审里被当成平台缺陷。维护者裁决:「联系人标题只显示 “Ada”,不处理,这种就是元数据的bug」,所以平台不动,只修元数据。 - **风险与回滚**:风险低。示例包是私有包,不发布;平台代码零改动。种子不再给线索写 `assigned_to`(原值是客户联系人的邮箱,不是用户),新库里该列为空,要在界面上分配。回滚即 revert 本 PR。 - **先看**:① `examples/app-crm/src/objects/lead.object.ts`(`assigned_to` 改为 `Field.user`,`is_closed` 声明 `boolean`);② `examples/app-crm/src/data/index.ts`(去掉种子里的邮箱、活动改名);③ `examples/app-crm/src/objects/contact.object.ts`(标题字段)。 - **两分钟验证**:`pnpm dev:crm -- --fresh -p 38427`,浏览器语言设为中文后登录:侧栏是「线索 / 客户 / 联系人 / 商机 / 活动 / 销售漏斗」,联系人页标题是 "Ada Lovelace",页签没有「列表」二字。 - **需要你做的**:无,席位落地。 ## What changes Every metadata edit is in `examples/app-crm`, per the ruling: no platform change, no lint, no inference, no gate. The one other file is the app-crm row of the i18n coverage baseline. 1. **Contact title.** `crm_contact` declares `nameField: 'full_name'`, and the `full_name` formula declares `returnType: 'text'`, which makes it title-eligible. Title, lookups and pickers read "Ada Lovelace". 2. **Plural labels.** The zh-CN `pluralLabel` of account, contact, opportunity, lead and activity is the plain noun (客户, 联系人, 商机, 线索, 活动). 3. **zh-CN bundle.** Navigation entries keyed by id (`nav_leads` … `nav_pipeline_dashboard`), every declared field label, every select option (industry, stage, approval status, lead status and source, activity type and status), the line-item object, the `crm_convert_lead` action, the dashboard title, description and six widgets, the `opportunity_metrics` dataset (tile and axis names), object descriptions, the five validation messages and the welcome page. 4. **Highlight fields.** Picks, at most 6, never the title field, and never the field the record page already draws as the stage path (`detectStatusField`: opportunity `stage`, lead and activity `status`), so the stage is not shown twice once #28868 shows a declared strip as declared: - `crm_account`: `industry`, `annual_revenue`, `website` - `crm_contact`: `account`, `email` - `crm_opportunity`: `account`, `amount`, `close_date`, `probability` - `crm_lead`: `company`, `source`, `lead_score`, `assigned_to` - `crm_activity`: `type`, `due_date`, `contact`, `account` 5. **Lead owner.** `crm_lead.assigned_to` is `Field.user` (target `sys_user`). The seed no longer writes the four email addresses into it; see the deviation below. 6. **Seed ownership.** The activity "Discovery Call — Bletchley Systems" was linked to Acme Corp, Ada and the Acme renewal. It is renamed "Discovery Call — Acme Platform Renewal" to match its links. 7. **Dashboard copy.** The widget and dashboard descriptions are rewritten in business words (no `compareTo`, no chart-type notes) and translated. Also, as a bounded in-place fix of the same class as item 1 (a formula without its result type), `crm_lead.is_closed` declares `returnType: 'boolean'`. The zh-CN lead page showed the raw word "false" under 已关闭; it now shows 否. ### Deviation from the card (seat ruling B) The card asks for "seeded user references" on `assigned_to`. Measured on `45efba2a`: no `sys_user` row exists when app-crm's seed runs. The dev admin is minted at `kernel:ready`, after `AppPlugin.start()` seeds, and people otherwise come from sign-up (`examples/app-showcase/src/security/demo-personas.ts`; `content/docs/data-modeling/seed-data.mdx`, "Owner-style fields"). The only way to resolve a seeded user reference would be for app-crm to seed `sys_user` rows itself. Those are directory rows with no login, and `packages/plugins/plugin-auth/src/auth-manager.ts` (the `#14157` docblock) records that seeded people close the first-visitor bootstrap sign-up under `invite_only`. The seat ruled option B: `Field.user`, and the seed leaves the field unset, which is the documented owner-style pattern. The acceptance screenshot of the lead list was taken after assigning three leads in the UI. ## Tests - `pnpm --filter @objectstack/example-crm typecheck`: exit 0. - `pnpm --filter @objectstack/example-crm test`: 7 files, 48 tests passed. - `os lint --i18n-strict` on the app: 0 errors (was 101 untranslated declared strings). The 4 remaining `field-no-consumers` warnings were on the base too; the base had 9, and the highlight strips and the contact title now consume 5 of them. - `os validate`: exit 0. - Fresh boot (`pnpm dev:crm -- --fresh`): seed loads 28 rows with no unresolved-reference line. ## Acceptance notes - Screenshots (1440 px, zh-CN, fresh boot; 375 px contact page too) are held in the maintainer's project folder `issue-28870/` and are not uploaded to GitHub: `zh-account-page.png`, `zh-contact-page.png`, `zh-lead-list.png`, plus the lead, opportunity, activity and dashboard pages and the account and contact lists. The seat read them before landing. - `crm_contact.full_name` is a text formula, so the title is display-only: a sort by it is refused and search does not reach it. Search still finds a contact by `last_name` and `email` (both `searchable`). The contact list and record page load and render the full name; see the screenshots. A stored text field with an `expression` would make the title searchable; the card ruled the formula, so this PR keeps it. - zh-CN `assigned_to` reads 分配给, because the platform's `owner_id` already reads 负责人 on the same page. - `scripts/i18n-coverage-baseline.json`: the app-crm entry is written down from 101 to 0 by `node scripts/check-i18n-coverage.mjs --update` (claim amended to add this one entry), so a regression cannot creep back under the old ceiling. No other entry moved. - No changeset: `@objectstack/example-crm` is private and publishes nothing, so the PR carries `skip-changeset`. ## Verification <details><summary>Derived gates</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at the final head: 42 gates (34 for the app files, 8 more for the baseline file under `scripts/`), each run with exit 0 and reconciled with `--ran`, 0 NOT-MEASURED. The PR-body gates ran against this body through `GITHUB_EVENT_PATH`. </details> Dogfood battery: not owed (no `!` in the title and no `(narrowing)` arm). --- _Generated by [Claude Code](https://claude.ai/code/session_01QsR2jRrzBxwrREVnKhpyXn)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2c70f65 commit 9a9823d

9 files changed

Lines changed: 220 additions & 27 deletions

File tree

‎examples/app-crm/src/dashboards/pipeline.dashboard.ts‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import type { Dashboard } from '@objectstack/spec/ui';
3131
export const PipelineDashboard: Dashboard = {
3232
name: 'pipeline_dashboard',
3333
label: 'Pipeline Dashboard',
34-
description: 'Aggregate view of the sales pipeline with period-over-period comparisons.',
34+
description: 'The sales pipeline at a glance, compared with earlier periods.',
3535
columns: 12,
3636
widgets: [
3737
// --- Row 1: KPI tiles -------------------------------------------------
@@ -45,7 +45,7 @@ export const PipelineDashboard: Dashboard = {
4545
id: 'total_pipeline',
4646
type: 'metric',
4747
title: 'Total Pipeline',
48-
description: 'Sum of opportunity amounts across open stages.',
48+
description: 'Total value of all open opportunities.',
4949
filter: { stage: { $nin: ['closed_won', 'closed_lost'] } },
5050
dataset: 'opportunity_metrics',
5151
values: ['total_amount'],
@@ -55,7 +55,7 @@ export const PipelineDashboard: Dashboard = {
5555
id: 'won_this_quarter',
5656
type: 'metric',
5757
title: 'Won This Quarter',
58-
description: 'Revenue closed-won in the current quarter, compared to the previous quarter.',
58+
description: 'Value of deals won this quarter, compared with last quarter.',
5959
filter: {
6060
stage: 'closed_won',
6161
close_date: {
@@ -72,7 +72,7 @@ export const PipelineDashboard: Dashboard = {
7272
id: 'avg_deal_size_yoy',
7373
type: 'metric',
7474
title: 'Avg Deal Size (YoY)',
75-
description: 'Average won-deal value this year vs the same window last year.',
75+
description: 'Average value of deals won this year, compared with the same period last year.',
7676
filter: {
7777
stage: 'closed_won',
7878
close_date: {
@@ -91,7 +91,7 @@ export const PipelineDashboard: Dashboard = {
9191
id: 'pipeline_trend_90d',
9292
type: 'line',
9393
title: 'Pipeline Trend (12 months)',
94-
description: 'Opportunity count bucketed by close-month for the last year, with a sliding overlay of the prior year for compareTo.',
94+
description: 'Opportunities by expected close month over the last 12 months, compared with the year before.',
9595
filter: {
9696
close_date: { $gte: '{1_years_ago}', $lte: '{today}' },
9797
},
@@ -107,7 +107,7 @@ export const PipelineDashboard: Dashboard = {
107107
id: 'opportunities_by_stage',
108108
type: 'bar',
109109
title: 'Opportunities by Stage',
110-
description: 'Count grouped by stage with previous-quarter overlay (compareTo).',
110+
description: 'Opportunities expected to close this quarter, by stage, compared with last quarter.',
111111
filter: {
112112
close_date: {
113113
$gte: '{current_quarter_start}',
@@ -126,7 +126,7 @@ export const PipelineDashboard: Dashboard = {
126126
id: 'pipeline_by_industry',
127127
type: 'pie',
128128
title: 'Open Pipeline by Stage',
129-
description: 'Open-pipeline revenue split by pipeline stage. Pie/donut/funnel ignore `compareTo`.',
129+
description: 'Value of open opportunities, split by stage.',
130130
filter: { stage: { $nin: ['closed_won', 'closed_lost'] } },
131131
dataset: 'opportunity_metrics',
132132
dimensions: ['stage'],

‎examples/app-crm/src/data/index.ts‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,28 +59,28 @@ const opportunities = defineSeed(Opportunity, {
5959
const leads = defineSeed(Lead, {
6060
mode: 'upsert',
6161
externalId: 'email',
62+
// No `assigned_to` on these rows. It is a user field, and no user exists
63+
// when the seed runs: people come from sign-up, and the dev admin is
64+
// created after the seed. A rep picks up a lead from the record page.
6265
records: [
6366
// New / uncontacted
6467
{
6568
name: 'Marie Curie', email: 'marie@radium.example', company: 'Radium Labs',
6669
title: 'Director of Research', phone: '+1-555-0101',
6770
status: 'new', source: 'web', lead_score: 0,
68-
assigned_to: 'ada@acme.example',
6971
account: 'Acme Corp',
7072
},
7173
// In qualification — high-value referral lead
7274
{
7375
name: 'Alan Turing', email: 'alan@bletchley.example', company: 'Bletchley Systems',
7476
title: 'VP Engineering', phone: '+44-20-555-0202',
7577
status: 'qualifying', source: 'referral', lead_score: 75,
76-
assigned_to: 'linus@globex.example',
7778
},
7879
// Qualified — ready for conversion
7980
{
8081
name: 'Rosalind Franklin', email: 'ros@helix.example', company: 'Helix Analytics',
8182
title: 'CTO', phone: '+1-555-0303',
8283
status: 'qualified', source: 'event', lead_score: 85,
83-
assigned_to: 'grace@initech.example',
8484
account: 'Initech',
8585
},
8686
// Disqualified — poor fit
@@ -95,7 +95,6 @@ const leads = defineSeed(Lead, {
9595
name: 'Nikola Tesla', email: 'nikola@wardenclyffe.example', company: 'Wardenclyffe Corp',
9696
title: 'Chief Inventor', phone: '+1-555-0505',
9797
status: 'converted', source: 'partner', lead_score: 90,
98-
assigned_to: 'ada@acme.example',
9998
converted_opportunity: 'Acme — Q3 Platform Renewal',
10099
},
101100
],
@@ -106,7 +105,7 @@ const activities = defineSeed(Activity, {
106105
externalId: 'subject',
107106
records: [
108107
{
109-
subject: 'Discovery Call — Bletchley Systems',
108+
subject: 'Discovery Call — Acme Platform Renewal',
110109
type: 'call', status: 'completed',
111110
due_date: cel`daysAgo(3)`,
112111
contact: 'ada@acme.example',

‎examples/app-crm/src/objects/account.object.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export const Account = ObjectSchema.create({
1313
pluralLabel: 'Accounts',
1414
icon: 'building',
1515
description: 'A company that the org sells to or supports.',
16+
// What the record page shows at a glance, under the title.
17+
highlightFields: ['industry', 'annual_revenue', 'website'],
1618

1719
fields: {
1820
name: Field.text({

‎examples/app-crm/src/objects/activity.object.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ export const Activity = ObjectSchema.create({
1414
pluralLabel: 'Activities',
1515
icon: 'calendar-check',
1616
description: 'A logged interaction (call, email, meeting, or task) related to a CRM record.',
17+
// What the record page shows at a glance, under the title. The stage is
18+
// not here: the record page already draws it as the stage path.
19+
highlightFields: ['type', 'due_date', 'contact', 'account'],
1720

1821
fields: {
1922
subject: Field.text({

‎examples/app-crm/src/objects/contact.object.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ export const Contact = ObjectSchema.create({
1414
pluralLabel: 'Contacts',
1515
icon: 'user',
1616
description: 'A person associated with an account.',
17+
// The record's title is the person's full name. Without this pointer the
18+
// title is derived from the first name-ish field, which reads "Ada" on the
19+
// record page, in lookups and in pickers. A text formula is a display title:
20+
// search reaches the person through `last_name` and `email` instead.
21+
nameField: 'full_name',
22+
// What the record page shows at a glance, under the title.
23+
highlightFields: ['account', 'email'],
1724

1825
fields: {
1926
first_name: Field.text({
@@ -28,6 +35,8 @@ export const Contact = ObjectSchema.create({
2835
}),
2936
full_name: Field.formula({
3037
label: 'Full Name',
38+
// `text`, so the formula can back the record title (`nameField` above).
39+
returnType: 'text',
3140
expression: cel`(record.first_name == null ? '' : record.first_name + ' ') + record.last_name`,
3241
}),
3342
email: Field.email({

‎examples/app-crm/src/objects/lead.object.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ export const Lead = ObjectSchema.create({
1414
pluralLabel: 'Leads',
1515
icon: 'funnel',
1616
description: 'An inbound prospect not yet qualified as an opportunity.',
17+
// What the record page shows at a glance, under the title. The stage is
18+
// not here: the record page already draws it as the stage path.
19+
highlightFields: ['company', 'source', 'lead_score', 'assigned_to'],
1720

1821
fields: {
1922
name: Field.text({
@@ -73,9 +76,10 @@ export const Lead = ObjectSchema.create({
7376
max: 100,
7477
defaultValue: 0,
7578
}),
76-
assigned_to: Field.text({
79+
// The sales rep who works the lead: a person, so the list and the record
80+
// page show a name, never a raw email address.
81+
assigned_to: Field.user({
7782
label: 'Assigned To',
78-
maxLength: 200,
7983
}),
8084
account: Field.lookup('crm_account', {
8185
label: 'Account',
@@ -89,6 +93,8 @@ export const Lead = ObjectSchema.create({
8993
/** CEL formula: is this lead in a terminal converted/disqualified state? */
9094
is_closed: Field.formula({
9195
label: 'Is Closed',
96+
// `boolean`, so the record page draws a yes/no face, not the raw word.
97+
returnType: 'boolean',
9298
expression: cel`record.status == "converted" || record.status == "disqualified"`,
9399
}),
94100
},

‎examples/app-crm/src/objects/opportunity.object.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ export const Opportunity = ObjectSchema.create({
1414
pluralLabel: 'Opportunities',
1515
icon: 'trending-up',
1616
description: 'A potential sale tied to an account.',
17+
// What the record page shows at a glance, under the title. The stage is
18+
// not here: the record page already draws it as the stage path.
19+
highlightFields: ['account', 'amount', 'close_date', 'probability'],
1720

1821
fields: {
1922
name: Field.text({

0 commit comments

Comments
 (0)