From bfafa4689a24d943414fd17886c994d228860818 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 14:36:03 +0000 Subject: [PATCH] docs(guides): name real built-in views in search-and-navigation (#973) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The *Built-in vs personal vs shared* bullet taught the concept with two examples and neither was a view. *All Open Opportunities* splices the two real opportunity views — Open Deals (`open_opportunities`, default + pinned) and All Opportunities (`all_opportunities`, unfiltered). *Pending My Approval* is the phantom PR #969 removed from `revenue/approvals` and #960 from `quick-tour`; this page was its last landing place, and the approvals plugin ships My Pending / I Submitted / Completed / All on `sys_approval_request`. The zh pages were wrong in their own ways: zh-Hans quoted 待我审批, the navigation label of `nav_approval_requests` rather than a view; zh-Hant quoted 待我審核, which no locale pack carries at all (this app ships en / zh-CN / ja-JP / es-ES and no Traditional Chinese pack). Both now name the real views and quote the zh-CN labels the console resolves. Names swapped without a denial: this bullet is an example, not a navigation roster. `src/` untouched. `test/docs-search-navigation-views.test.ts` pins the bullet's names and glosses against the shipped metadata and the two retired spellings. Co-Authored-By: Claude Fable 5 --- .../search-navigation-built-in-views-real.md | 41 +++ content/docs/guides/search-and-navigation.mdx | 2 +- .../guides/search-and-navigation.zh-Hans.mdx | 2 +- .../guides/search-and-navigation.zh-Hant.mdx | 2 +- test/docs-search-navigation-views.test.ts | 241 ++++++++++++++++++ 5 files changed, 285 insertions(+), 3 deletions(-) create mode 100644 .changeset/search-navigation-built-in-views-real.md create mode 100644 test/docs-search-navigation-views.test.ts diff --git a/.changeset/search-navigation-built-in-views-real.md b/.changeset/search-navigation-built-in-views-real.md new file mode 100644 index 00000000..613409c5 --- /dev/null +++ b/.changeset/search-navigation-built-in-views-real.md @@ -0,0 +1,41 @@ +--- +'hotcrm': patch +--- + +Give the *Built-in vs personal vs shared* bullet in `guides/search-and-navigation` +examples that exist, in all three locales. + +The bullet that teaches what a **built-in** view IS held up two names, and neither +was a view: + +- ***All Open Opportunities*** is a splice of two real ones. The opportunity view + set in `src/views/opportunity.view.ts` ships **Open Deals** + (`open_opportunities` — the default, pinned tab) and **All Opportunities** + (`all_opportunities` — the unfiltered book). A reader who went looking for the + spliced third name found nothing and had no way to tell that the two halves + were both there under other spellings. +- ***Pending My Approval*** is the phantom PR #969 removed from + `content/docs/revenue/approvals.mdx` and #960 from + `getting-started/quick-tour`; this page was its last landing place. The + approvals plugin ships **My Pending** / **I Submitted** / **Completed** / + **All** on `sys_approval_request`. + +The zh pages were each wrong in their own way, so neither was fixed by +translating the English: zh-Hans said 「待我审批」, which is real but is the +**navigation** label of `nav_approval_requests` rather than a view; zh-Hant said +「待我審核」, which no surface can show at all — this app ships en / zh-CN / +ja-JP / es-ES and no Traditional Chinese pack. Both now name the real views and +quote the zh-CN labels the console actually resolves (「进行中商机」, +「全部商机」, 「我的待办」). + +The names were swapped without adding a denial: unlike the approvals page, this +bullet is an *example*, not a navigation roster, so there is no wrong list for a +reader to reconcile. `src/` is untouched. +`test/docs-search-navigation-views.test.ts` pins it from both ends — every +emphasised name in the bullet must be a label some view in this app or its +plugins really carries (bold or italic, since the phantoms were italic), every +「…」 gloss must be a string a locale pack really ships, and the source side pins +**Open Deals** as the default tab, **All Opportunities** as unfiltered, +**My Pending** on the plugin, and the zero-hit status of the two retired names — +so a future release that ships a view by one of them fails a test rather than +making the page accidentally right. diff --git a/content/docs/guides/search-and-navigation.mdx b/content/docs/guides/search-and-navigation.mdx index fe461485..08fa7570 100644 --- a/content/docs/guides/search-and-navigation.mdx +++ b/content/docs/guides/search-and-navigation.mdx @@ -53,7 +53,7 @@ Every object has list views (Leads, Opportunities, etc.). They share the same co ### Built-in vs personal vs shared -- **Built-in** views ship with the system (*All Open Opportunities*, *Pending My Approval*). +- **Built-in** views ship with the system — Opportunities opens on **Open Deals** and keeps the unfiltered **All Opportunities** one tab over; approval requests arrive with **My Pending**. - **Personal** views are saved for just you. - **Shared** views are saved for a group or the whole org (admin permission required). diff --git a/content/docs/guides/search-and-navigation.zh-Hans.mdx b/content/docs/guides/search-and-navigation.zh-Hans.mdx index 5cf50214..7a98d702 100644 --- a/content/docs/guides/search-and-navigation.zh-Hans.mdx +++ b/content/docs/guides/search-and-navigation.zh-Hans.mdx @@ -53,7 +53,7 @@ description: 在几秒钟内找到任何东西 —— 全局搜索、键盘快 ### 内置 vs 个人 vs 共享 -- **内置**视图随系统提供(*所有未结商机*、*待我审批*)。 +- **内置**视图随系统提供——商机默认打开 **Open Deals**(简体界面显示为「进行中商机」),不带筛选的 **All Opportunities**(「全部商机」)就在旁边一个标签页;审批请求带的是 **My Pending**(「我的待办」)。 - **个人**视图仅为你自己保存。 - **共享**视图为一个组或整个组织保存(需要管理员权限)。 diff --git a/content/docs/guides/search-and-navigation.zh-Hant.mdx b/content/docs/guides/search-and-navigation.zh-Hant.mdx index 05332866..b535a731 100644 --- a/content/docs/guides/search-and-navigation.zh-Hant.mdx +++ b/content/docs/guides/search-and-navigation.zh-Hant.mdx @@ -53,7 +53,7 @@ description: 在幾秒鐘內找到任何東西 —— 全域搜尋、鍵盤快 ### 內建 vs 個人 vs 共享 -- **內建**檢視隨系統提供(*所有未結商機*、*待我審核*)。 +- **內建**檢視隨系統提供——商機預設打開 **Open Deals**(本應用提供 en / zh-CN / ja-JP / es-ES 四種介面語言,沒有繁體語言包;簡體介面下顯示為「进行中商机」),不帶篩選的 **All Opportunities**(簡體「全部商机」)就在旁邊一個標籤頁;審批請求帶的是 **My Pending**(簡體「我的待办」)。 - **個人**檢視僅為你自己儲存。 - **共享**檢視為一個群組或整個組織儲存(需要管理員權限)。 diff --git a/test/docs-search-navigation-views.test.ts b/test/docs-search-navigation-views.test.ts new file mode 100644 index 00000000..e2d8c636 --- /dev/null +++ b/test/docs-search-navigation-views.test.ts @@ -0,0 +1,241 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { createRequire } from 'node:module'; +import { SysApprovalRequest } from '@objectstack/plugin-approvals'; +import { REPO_ROOT } from './helpers/repo-root'; +import { type AnyRec, packFor, views } from './helpers/metadata-fixtures'; + +/** + * `guides/search-and-navigation` › *Built-in vs personal vs shared*, pinned to + * source (#973). + * + * The bullet that teaches what a **built-in** view IS carried two examples and + * neither existed: + * + * - *All Open Opportunities* — a conflation of two real ones. The opportunity + * view set ships **Open Deals** (`open_opportunities`, the default and + * pinned tab) and **All Opportunities** (`all_opportunities`, unfiltered); + * the page spliced them into a third name that greps 0 in `src/`. + * - *Pending My Approval* — the same phantom PR #969 removed from + * `revenue/approvals` and #960 from `getting-started/quick-tour`. This page + * was its last landing place. The approvals plugin ships **My Pending** / + * **I Submitted** / **Completed** / **All** on `sys_approval_request`. + * + * The zh pages were wrong in two further ways, each its own kind of wrong: + * zh-Hans said 「待我审批」, which is real but is the **navigation** label of + * `nav_approval_requests`, not a view; zh-Hant said 「待我審核」, which is + * neither — this app ships en / zh-CN / ja-JP / es-ES and no Hant pack, so no + * surface can display it. + * + * Both were fixed by naming the real views, not by adding a denial: unlike + * `revenue/approvals`, this bullet is an *example*, not a navigation roster, so + * there is no wrong list for a reader to reconcile — hence this guard pins the + * names and the retired spellings, and asserts no prose. + * + * Nothing else checks this: `os validate` and `pnpm lint` walk authored + * metadata and never open `content/docs`, so — as with + * `docs-revenue-approvals-navigation.test.ts` (#963) — the check lives where + * the claim lives. + */ + +/** Every label a list view of this app shows a user, plus its tab labels. */ +const APP_VIEW_LABELS: string[] = views.flatMap((set: AnyRec) => { + const list = (set.list ?? {}) as AnyRec; + const extra = Object.values((set.listViews ?? {}) as Record); + // Tab labels count too: `my_open_deals` is labelled *My Open Deals* on the + // view and *My Deals* on the tab, and a reader naming either one is naming + // something they can actually see. + const tabs = ((list.tabs ?? []) as AnyRec[]).map((t) => t.label); + return [list.label, ...extra.map((v) => v.label), ...tabs].filter(Boolean) as string[]; +}); + +/** Built-in list-view labels the approvals plugin ships for the request object. */ +const APPROVAL_VIEW_LABELS: string[] = Object.values( + ((SysApprovalRequest as AnyRec).listViews ?? {}) as Record, +).map((v) => v.label as string); + +const REAL_VIEW_LABELS = new Set([...APP_VIEW_LABELS, ...APPROVAL_VIEW_LABELS]); + +/** zh-CN labels for this app's own views, from the pack the console resolves. */ +const APP_ZH_VIEW_LABELS = new Set( + Object.values(((packFor('zh-CN') as AnyRec)?.objects ?? {}) as Record).flatMap( + (obj) => + Object.values((obj._views ?? {}) as Record) + .map((v) => v.label as string) + .filter(Boolean), + ), +); + +/** + * The approvals plugin's shipped bundle, with `\uXXXX` escapes decoded. + * + * The package exports its schemas (`SysApprovalRequest`) but not its locale + * packs, so the zh-CN view labels the zh pages quote — 「我的待办」 for + * `my_pending` — are only reachable as text in the built bundle, where the + * bundler emits every non-ASCII character as an escape (the same reason a + * plain `grep 我的待办 dist/index.js` returns nothing and reads as "the plugin + * ships no zh-CN"). Decoding once here keeps the gloss check honest instead of + * unpinned. + */ +const PLUGIN_BUNDLE: string = (() => { + const require = createRequire(import.meta.url); + const entry = require.resolve('@objectstack/plugin-approvals'); + const raw = readFileSync(entry, 'utf8'); + return raw.replace(/\\u([0-9a-fA-F]{4})/g, (_m, hex) => String.fromCharCode(parseInt(hex, 16))); +})(); + +/** Does some surface of this app or its plugins ship `label` in zh-CN? */ +const shipsZhLabel = (label: string): boolean => + APP_ZH_VIEW_LABELS.has(label) || PLUGIN_BUNDLE.includes(`"${label}"`); + +const PAGES = [ + { + file: 'content/docs/guides/search-and-navigation.mdx', + lang: 'en', + heading: '### Built-in vs personal vs shared', + /** Verbatim fragments of the wrong bullet. None may return. */ + retired: ['All Open Opportunities', 'Pending My Approval'], + }, + { + file: 'content/docs/guides/search-and-navigation.zh-Hans.mdx', + lang: 'zh-Hans', + heading: '### 内置 vs 个人 vs 共享', + // 待我审批 is a real string — the zh-CN label of `nav_approval_requests` — + // but it names a sidebar entry, not a view, so it may not come back HERE. + retired: ['所有未结商机', '待我审批'], + }, + { + file: 'content/docs/guides/search-and-navigation.zh-Hant.mdx', + lang: 'zh-Hant', + heading: '### 內建 vs 個人 vs 共享', + retired: ['所有未結商機', '待我審核'], + }, +] as const; + +/** The `### …` section named by `heading`, up to the next heading of any level. */ +const sectionOf = (file: string, heading: string): string => { + const lines = readFileSync(join(REPO_ROOT, file), 'utf8').split('\n'); + const start = lines.findIndex((l) => l.trim() === heading); + expect(start, `${file}: heading '${heading}' not found`).toBeGreaterThanOrEqual(0); + const rest = lines.slice(start + 1); + const end = rest.findIndex((l) => /^#{1,6} /.test(l)); + return (end === -1 ? rest : rest.slice(0, end)).join('\n'); +}; + +/** The one bullet in the section that defines the built-in category. */ +const builtInBullet = (file: string, heading: string): string => { + const bullet = sectionOf(file, heading) + .split('\n') + .find((l) => l.startsWith('- **')); + expect(bullet, `${file}: the built-in bullet is gone from '${heading}'`).toBeTruthy(); + return bullet!; +}; + +/** + * Emphasised runs after the leading category word (**Built-in** / **内置** / + * **內建**) — the names the bullet holds up as examples. + * + * Bold *and* italic, deliberately: the two phantoms #973 removed were written + * in italics (`*All Open Opportunities*`), so a bold-only reader would go green + * on the very shape the defect took and catch a reintroduction only through the + * verbatim `retired` list below. + */ +const namesIn = (bullet: string): string[] => + [...bullet.matchAll(/\*+([^*]+)\*+/g)].map((m) => m[1].trim()).slice(1); + +/** `「…」` glosses — the zh pages' way of quoting a zh-CN interface string. */ +const glossesIn = (bullet: string): string[] => + [...bullet.matchAll(/「([^」]+)」/g)].map((m) => m[1].trim()); + +describe('the source facts the built-in-views example rests on (#973)', () => { + it('sees a non-trivial view set, a plugin that ships views, and a zh-CN pack', () => { + // Guards the guard: any of these coming back empty would make every + // assertion below pass by checking nothing. + expect(REAL_VIEW_LABELS.size, 'no view labels found at all').toBeGreaterThan(20); + expect(APPROVAL_VIEW_LABELS.length, 'sys_approval_request ships no list views').toBeGreaterThanOrEqual(4); + expect(APP_ZH_VIEW_LABELS.size, 'zh-CN pack exposes no view labels').toBeGreaterThan(20); + expect(PLUGIN_BUNDLE.length, 'approvals bundle read as empty').toBeGreaterThan(100_000); + }); + + it('Opportunities opens on Open Deals and keeps All Opportunities one tab over', () => { + const opp = views.find((v: AnyRec) => v.list?.name === 'open_opportunities') as AnyRec; + expect(opp, 'the opportunity view set is gone — this pin is out of date').toBeTruthy(); + expect(opp.list.label).toBe('Open Deals'); + const openTab = (opp.list.tabs as AnyRec[]).find((t) => t.view === 'open_opportunities'); + expect(openTab?.isDefault, 'Open Deals is no longer the default tab').toBe(true); + const all = (opp.listViews as AnyRec).all_opportunities as AnyRec; + expect(all.label).toBe('All Opportunities'); + // "unfiltered" is a claim the bullet makes in all three locales. + expect(all.filter, 'All Opportunities grew a filter — the bullet says it has none').toBeUndefined(); + }); + + it('the approvals plugin ships My Pending on approval requests', () => { + expect(APPROVAL_VIEW_LABELS).toContain('My Pending'); + }); + + it('*All Open Opportunities* and *Pending My Approval* are labels of nothing', () => { + // The reason the bullet changed. Either name becoming real is a product + // decision that should reopen this text, not something to discover from a + // reader's bug report. + expect([...REAL_VIEW_LABELS].filter((l) => /All Open Opportunities|Pending My Approval/.test(l))).toEqual([]); + }); + + it('zh-CN labels the two opportunity views 进行中商机 / 全部商机', () => { + const oppPack = ((packFor('zh-CN') as AnyRec)?.objects ?? {})['crm_opportunity'] as AnyRec; + expect(oppPack?._views?.open_opportunities?.label).toBe('进行中商机'); + expect(oppPack?._views?.all_opportunities?.label).toBe('全部商机'); + }); + + it('the approvals plugin labels my_pending 我的待办 in zh-CN', () => { + expect(shipsZhLabel('我的待办')).toBe(true); + }); +}); + +describe('guides/search-and-navigation names only views that exist (#973)', () => { + for (const { file, lang, heading, retired } of PAGES) { + it(`${lang}: every view the built-in bullet names is one this app ships`, () => { + const named = namesIn(builtInBullet(file, heading)); + // Vacuity guard: a bullet that stopped naming any view teaches the + // category with no anchor at all — a reader's only handle on the + // definition is the example. Held at two, not three, so that a bullet + // carrying the two wrong names is judged by the membership check below + // (which says WHICH name is fictional) rather than dismissed here. + expect(named.length, `${file}: the built-in bullet names no view`).toBeGreaterThanOrEqual(2); + const bad = named + .filter((n) => !REAL_VIEW_LABELS.has(n)) + .map((n) => `${file}: names a view "${n}" that nothing in this app ships`); + expect( + bad, + `documented views that do not exist:\n ${bad.join('\n ')}\n` + + 'Use a label off `src/views/*.view.ts` or a plugin\'s `listViews` — do not ' + + 'invent a name that reads plausible.', + ).toEqual([]); + }); + + if (lang !== 'en') { + it(`${lang}: every 「…」 gloss is a zh-CN string some surface really shows`, () => { + const glosses = glossesIn(builtInBullet(file, heading)); + expect(glosses.length, `${file}: the bullet quotes no zh-CN label`).toBeGreaterThanOrEqual(2); + const bad = glosses + .filter((g) => !shipsZhLabel(g)) + .map((g) => `${file}: quotes 「${g}」, which no zh-CN pack carries`); + expect(bad, `invented zh-CN labels:\n ${bad.join('\n ')}`).toEqual([]); + }); + } + + it(`${lang}: the retired spellings stay retired`, () => { + const section = sectionOf(file, heading); + const back = retired.filter((r) => section.includes(r)); + expect( + back, + `${file}: '${back.join("', '")}' is back in the built-in bullet. ` + + 'These name no view: *All Open Opportunities* and *Pending My Approval* exist ' + + 'nowhere, 待我审批 is the sidebar label of `nav_approval_requests`, and 待我審核 ' + + 'is in no locale pack at all.', + ).toEqual([]); + }); + } +});