From eb0ff2e3b3cc17d5351f156bd8f58c3d44685e8d Mon Sep 17 00:00:00 2001 From: yinlianghui Date: Thu, 30 Jul 2026 23:13:50 +0800 Subject: [PATCH] fix(views): remove the dead form.data provider from all twelve views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `pnpm verify` on main reported 12 `liveness-dead-property` warnings — one per view file — for a `data: { provider: 'object', object: 'crm_*' }` block on the form view. A form binds to its object and record through the route context, so the block only looked like it was wiring the form to an object. Measured before removing, because the same validator produces false positives elsewhere (it cannot see view names inside the twelve grouped `views` exports, so it reports live routes as dead — see #544). With every `form.data` deleted, opening a lead record's edit form on 16.1.0 still binds correctly: 8 inputs, 7 populated from the record (first_name "Mira", company "Atlas Construction", status "contacted", lead_source "cold_call"), no console errors. Two guards added: - no view may declare `form.data` again (fails red on the pre-fix tree); - every view still resolves its object from `list.data.object`. objectOf() used to fall back to `form.data.object`; without that check, a view that lost both would make every rule keyed on objectOf() silently skip it instead of failing. Left alone deliberately: named forms under `formViews` (quick_create, lead_conversion_wizard, …) still carry `data` blocks. The liveness rule does not flag those and they are unmeasured — a create form has no record in the route context, so the same reasoning may not hold. Documented in the guard's comment. verify: exit 0, 0 errors, 15 warnings → 3 (two pre-existing campaign_enrollment flow-variable notes plus their summary line). 140/140 tests. Co-Authored-By: Claude Opus 5 --- .changeset/dead-form-data-provider.md | 13 ++++++++ src/views/account.view.ts | 1 - src/views/campaign.view.ts | 1 - src/views/case.view.ts | 1 - src/views/contact.view.ts | 1 - src/views/contract.view.ts | 1 - src/views/forecast.view.ts | 1 - src/views/knowledge_article.view.ts | 1 - src/views/lead.view.ts | 8 ----- src/views/opportunity.view.ts | 1 - src/views/product.view.ts | 1 - src/views/quote.view.ts | 1 - src/views/task.view.ts | 1 - test/metadata-references.test.ts | 45 +++++++++++++++++++++++++++ 14 files changed, 58 insertions(+), 19 deletions(-) create mode 100644 .changeset/dead-form-data-provider.md diff --git a/.changeset/dead-form-data-provider.md b/.changeset/dead-form-data-provider.md new file mode 100644 index 00000000..12428c9c --- /dev/null +++ b/.changeset/dead-form-data-provider.md @@ -0,0 +1,13 @@ +--- +'hotcrm': patch +--- + +Remove the `form.data` provider from all twelve view files. A form binds to its +object and record through the route context, so the block never wired anything — +the platform's own `liveness-dead-property` rule flagged every one of them. +Verified before removal, not taken on the validator's word: with the blocks gone, +a lead record's edit form still binds on 16.1.0 (8 inputs, 7 populated from the +record). Adds two guards — no view may reintroduce `form.data`, and every view +must still resolve its object from the list provider now that the `form.data` +fallback is gone. Clears the last 12 validate warnings; `pnpm verify` goes from +15 warnings to 3 (all pre-existing). diff --git a/src/views/account.view.ts b/src/views/account.view.ts index a6e204df..91f80cae 100644 --- a/src/views/account.view.ts +++ b/src/views/account.view.ts @@ -196,7 +196,6 @@ export const AccountViews = defineView({ form: { type: 'tabbed', - data: { provider: 'object', object: 'crm_account' }, sections: [ { label: 'Profile', diff --git a/src/views/campaign.view.ts b/src/views/campaign.view.ts index 4bbb293c..679edf95 100644 --- a/src/views/campaign.view.ts +++ b/src/views/campaign.view.ts @@ -96,7 +96,6 @@ export const CampaignViews = defineView({ form: { type: 'tabbed', - data: { provider: 'object', object: 'crm_campaign' }, sections: [ { label: 'Overview', diff --git a/src/views/case.view.ts b/src/views/case.view.ts index bdc0a443..26b88d0a 100644 --- a/src/views/case.view.ts +++ b/src/views/case.view.ts @@ -161,7 +161,6 @@ export const CaseViews = defineView({ form: { type: 'tabbed', - data: { provider: 'object', object: 'crm_case' }, sections: [ { label: 'Case', diff --git a/src/views/contact.view.ts b/src/views/contact.view.ts index 446c1af1..75204962 100644 --- a/src/views/contact.view.ts +++ b/src/views/contact.view.ts @@ -71,7 +71,6 @@ export const ContactViews = defineView({ form: { type: 'tabbed', - data: { provider: 'object', object: 'crm_contact' }, sections: [ { label: 'Identity', diff --git a/src/views/contract.view.ts b/src/views/contract.view.ts index ca7b6f41..39654360 100644 --- a/src/views/contract.view.ts +++ b/src/views/contract.view.ts @@ -91,7 +91,6 @@ export const ContractViews = defineView({ form: { type: 'tabbed', - data: { provider: 'object', object: 'crm_contract' }, sections: [ { label: 'Parties', diff --git a/src/views/forecast.view.ts b/src/views/forecast.view.ts index 1d551741..3f88caca 100644 --- a/src/views/forecast.view.ts +++ b/src/views/forecast.view.ts @@ -79,7 +79,6 @@ export const ForecastViews = defineView({ form: { type: 'simple', - data: { provider: 'object', object: 'crm_forecast' }, sections: [ { label: 'Snapshot', diff --git a/src/views/knowledge_article.view.ts b/src/views/knowledge_article.view.ts index 9ba36239..def0237d 100644 --- a/src/views/knowledge_article.view.ts +++ b/src/views/knowledge_article.view.ts @@ -90,7 +90,6 @@ export const KnowledgeArticleViews = defineView({ form: { type: 'simple', - data: { provider: 'object', object: 'crm_knowledge_article' }, sections: [ { label: 'Article', diff --git a/src/views/lead.view.ts b/src/views/lead.view.ts index 18989a36..3f5a3dc1 100644 --- a/src/views/lead.view.ts +++ b/src/views/lead.view.ts @@ -113,10 +113,6 @@ export const LeadViews = defineView({ */ form: { type: 'simple', - data: { - provider: 'object', - object: 'crm_lead', - }, sections: [ { @@ -401,10 +397,6 @@ export const LeadViews = defineView({ */ detail_form: { type: 'tabbed', - data: { - provider: 'object', - object: 'crm_lead', - }, sections: [ { label: 'General', diff --git a/src/views/opportunity.view.ts b/src/views/opportunity.view.ts index 68831fc4..a9d2ae59 100644 --- a/src/views/opportunity.view.ts +++ b/src/views/opportunity.view.ts @@ -222,7 +222,6 @@ export const OpportunityViews = defineView({ form: { type: 'tabbed', - data: { provider: 'object', object: 'crm_opportunity' }, sections: [ { label: 'Overview', diff --git a/src/views/product.view.ts b/src/views/product.view.ts index 54fe33e4..530b98a9 100644 --- a/src/views/product.view.ts +++ b/src/views/product.view.ts @@ -72,7 +72,6 @@ export const ProductViews = defineView({ form: { type: 'simple', - data: { provider: 'object', object: 'crm_product' }, sections: [ { label: 'Product Info', diff --git a/src/views/quote.view.ts b/src/views/quote.view.ts index 744d635b..7fb85eea 100644 --- a/src/views/quote.view.ts +++ b/src/views/quote.view.ts @@ -74,7 +74,6 @@ export const QuoteViews = defineView({ form: { type: 'tabbed', - data: { provider: 'object', object: 'crm_quote' }, sections: [ { label: 'Quote', diff --git a/src/views/task.view.ts b/src/views/task.view.ts index 75f0592f..873fa887 100644 --- a/src/views/task.view.ts +++ b/src/views/task.view.ts @@ -170,7 +170,6 @@ export const TaskViews = defineView({ form: { type: 'simple', - data: { provider: 'object', object: 'crm_task' }, sections: [ { label: 'Task', diff --git a/test/metadata-references.test.ts b/test/metadata-references.test.ts index 2ffece1b..b445ae35 100644 --- a/test/metadata-references.test.ts +++ b/test/metadata-references.test.ts @@ -1105,3 +1105,48 @@ describe('action labels are translated in every locale', () => { expect(bad, `action strings declared in code but not translated:\n ${bad.join('\n ')}`).toEqual([]); }); }); + +/** + * `form.data` is a data provider the form renderer never reads: a form binds to + * its object and record through the route context, so the block only *looked* + * like it was wiring the form to an object. The platform's own liveness rule + * (`liveness-dead-property`) flagged twelve of them, one per view file. + * + * Verified before removing them, rather than taken on the validator's word — + * the same validator reports false positives elsewhere (it cannot see view + * names inside the twelve grouped `views` exports, so it calls live routes + * dead). With every `form.data` deleted, opening a lead record's edit form on + * 16.1.0 still binds correctly: 8 inputs, 7 populated from the record + * (`first_name: "Mira"`, `company: "Atlas Construction"`, `status: "contacted"`, + * …), no console errors. + * + * Scope note: named forms under `formViews` (`quick_create`, + * `lead_conversion_wizard`, …) still carry `data` blocks. The liveness rule does + * NOT flag those and they have not been measured — a create form has no record + * in the route context, so the same reasoning may not hold. They are left alone + * deliberately; measure first if you plan to remove them. + */ +describe('form views do not declare a dead data provider', () => { + it('no view sets form.data', () => { + const bad: string[] = []; + for (const v of views) { + if (!v.form?.data) continue; + const objectName = v.list?.data?.object ?? v.object ?? '(unknown object)'; + bad.push(`${objectName}: form.data = ${JSON.stringify(v.form.data)}`); + } + expect( + bad, + `form.data is not consumed — a form binds via the route context:\n ${bad.join('\n ')}`, + ).toEqual([]); + }); + + it('every view still resolves its object without the form.data fallback', () => { + // objectOf() used to fall back to `form.data.object`. With those blocks gone + // the list provider is the only source, so a view that lost both would make + // every rule keyed on objectOf() silently skip it. + const unresolved = views + .filter((v) => !(v.list?.data?.object ?? v.object)) + .map((v) => v.list?.name ?? v.form?.type ?? '(unnamed view)'); + expect(unresolved, `views whose object no longer resolves:\n ${unresolved.join('\n ')}`).toEqual([]); + }); +});