From 0e2d35bd5180b458e1db92d98191c22303fcacc4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 04:14:36 +0000 Subject: [PATCH 1/4] fix(nav): hide the opportunity-built entries from members who cannot read opportunities, and give Sales Home a sidebar entry A service agent was offered Home (Executive Overview), Sales Performance, CRM Overview and Pipeline Coverage, and each opened on "You don't have access" tiles. Those entries, and a new pinned Sales Home entry, now carry `visible: current_user.can("crm_opportunity", "read")`. Sales Home had no entry at all, only a typed URL. Docs that named the landing page or the audience are corrected in all three locales. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01P8mZuzuyZAHSrgQKmv6ASv --- content/docs/analytics/dashboards.mdx | 4 +- content/docs/analytics/dashboards.zh-Hans.mdx | 4 +- content/docs/analytics/dashboards.zh-Hant.mdx | 4 +- content/docs/getting-started/quick-tour.mdx | 6 ++- .../getting-started/quick-tour.zh-Hans.mdx | 6 ++- .../getting-started/quick-tour.zh-Hant.mdx | 6 ++- content/docs/sales/activities.mdx | 2 +- content/docs/sales/activities.zh-Hans.mdx | 2 +- content/docs/sales/activities.zh-Hant.mdx | 2 +- src/sales/apps/crm.app.ts | 43 +++++++++++++++++-- src/sales/translations/es-ES/app.ts | 1 + src/sales/translations/ja-JP/app.ts | 1 + src/sales/translations/zh-CN/app.ts | 1 + test/app-navigation-shape.test.ts | 39 +++++++++++++++-- test/docs-quick-tour-navigation.test.ts | 4 +- 15 files changed, 101 insertions(+), 24 deletions(-) diff --git a/content/docs/analytics/dashboards.mdx b/content/docs/analytics/dashboards.mdx index b4be9d719..a03dfdeb7 100644 --- a/content/docs/analytics/dashboards.mdx +++ b/content/docs/analytics/dashboards.mdx @@ -13,13 +13,13 @@ Every tile listed on this page is a tile the app actually ships. The lists are c | Dashboard | Built for | Answers | | --- | --- | --- | -| 🏠 **CRM Overview** | Everyone | "What's going on across the business today?" | +| 🏠 **CRM Overview** | Everyone who reads opportunities | "What's going on across the business today?" | | 📈 **Sales Performance** | Sales reps, managers | "How healthy is my pipeline, and are we winning?" | | ☎️ **Sales Activity** | Sales managers, account teams | "Who is talking to customers, and which accounts have gone quiet?" | | 🎧 **Customer Service** | Service agents, managers | "How are we doing on cases and SLAs?" | | 👔 **Executive Overview** | Execs, finance | "Are we hitting the number?" | -All five respect [sharing rules](/docs/administration/sharing-and-security) — what you see depends on your role. +All five respect [sharing rules](/docs/administration/sharing-and-security) — what you see depends on your role. The three built on opportunities — CRM Overview, Sales Performance and Executive Overview — are left out of the sidebar for a role that cannot read opportunities, such as a service agent. ### No tile shows a trend arrow diff --git a/content/docs/analytics/dashboards.zh-Hans.mdx b/content/docs/analytics/dashboards.zh-Hans.mdx index f4e23f154..48eb2b3be 100644 --- a/content/docs/analytics/dashboards.zh-Hans.mdx +++ b/content/docs/analytics/dashboards.zh-Hans.mdx @@ -13,13 +13,13 @@ HotCRM 内置五个仪表盘。每个都为特定角色而构建,回答该角 | 仪表盘 | 面向 | 回答 | | --- | --- | --- | -| 🏠 **CRM Overview** | 所有人 | "今天整个业务发生了什么?" | +| 🏠 **CRM Overview** | 所有能读取商机的人 | "今天整个业务发生了什么?" | | 📈 **Sales Performance** | 销售代表、经理 | "我的管道健康状况如何?我们赢单了吗?" | | ☎️ **Sales Activity** | 销售经理、客户团队 | "谁在和客户打交道?哪些客户已经没了动静?" | | 🎧 **Customer Service** | 服务客服、经理 | "我们在工单和 SLA 上表现如何?" | | 👔 **Executive Overview** | 高管、财务 | "我们达成目标了吗?" | -五者都遵守 [共享规则](/zh-Hans/docs/administration/sharing-and-security)——你看到的内容取决于你的角色。 +五者都遵守 [共享规则](/zh-Hans/docs/administration/sharing-and-security)——你看到的内容取决于你的角色。建立在商机之上的三块——CRM Overview、Sales Performance 与 Executive Overview——对不能读取商机的角色(例如服务坐席)不出现在侧边栏里。 ### 没有任何磁贴显示趋势箭头 diff --git a/content/docs/analytics/dashboards.zh-Hant.mdx b/content/docs/analytics/dashboards.zh-Hant.mdx index 37911a390..dccef7c2a 100644 --- a/content/docs/analytics/dashboards.zh-Hant.mdx +++ b/content/docs/analytics/dashboards.zh-Hant.mdx @@ -15,13 +15,13 @@ HotCRM 內建五個儀表板。每個都為特定角色而建構,回答該角 | 儀表板 | 面向 | 回答 | | --- | --- | --- | -| 🏠 **CRM Overview** | 所有人 | "今天整個業務發生了什麼?" | +| 🏠 **CRM Overview** | 所有能讀取商機的人 | "今天整個業務發生了什麼?" | | 📈 **Sales Performance** | 銷售代表、經理 | "我的管道健康狀況如何?我們贏單了嗎?" | | ☎️ **Sales Activity** | 銷售經理、客戶團隊 | "誰在和客戶往來?哪些客戶已經沒了動靜?" | | 🎧 **Customer Service** | 服務客服、經理 | "我們在工單和 SLA 上表現如何?" | | 👔 **Executive Overview** | 高階主管、財務 | "我們達成目標了嗎?" | -五者都遵守 [共享規則](/zh-Hant/docs/administration/sharing-and-security)——你看到的內容取決於你的角色。 +五者都遵守 [共享規則](/zh-Hant/docs/administration/sharing-and-security)——你看到的內容取決於你的角色。建立在商機之上的三塊——CRM Overview、Sales Performance 與 Executive Overview——對不能讀取商機的角色(例如服務專員)不出現在側邊欄裡。 ### 沒有任何磁貼顯示趨勢箭頭 diff --git a/content/docs/getting-started/quick-tour.mdx b/content/docs/getting-started/quick-tour.mdx index 55f7854a2..72a26c807 100644 --- a/content/docs/getting-started/quick-tour.mdx +++ b/content/docs/getting-started/quick-tour.mdx @@ -9,7 +9,7 @@ A guided tour through the screens you'll use every day. ## 1. The home dashboard -The left nav opens with one pinned entry, **Home**, and that is where you land. **Home** opens the **Executive Overview** dashboard — nine tiles of revenue, customer and pipeline numbers: +The left nav opens with two pinned entries, **Home** and **Sales Home**, and **Home** is where you land. **Home** opens the **Executive Overview** dashboard — nine tiles of revenue, customer and pipeline numbers: | Tile | What it shows | | --- | --- | @@ -46,7 +46,7 @@ Click the **app launcher** (top-left grid icon) to switch between apps: - **HotCRM** — the main app with all CRM functionality. - **Admin** — for system administrators. -Inside HotCRM, the left nav opens with one pinned item — **Home**, which is the **Executive Overview** dashboard — and everything else sits in six groups: +Inside HotCRM, the left nav opens with two pinned items — **Home**, which is the **Executive Overview** dashboard, and **Sales Home**, a rep's own page of pipeline figures, leads, open deals, open tasks and upcoming events — and everything else sits in six groups: | Group | Contents | | --- | --- | @@ -59,6 +59,8 @@ Inside HotCRM, the left nav opens with one pinned item — **Home**, which is th **Marketing** and **Insights** are collapsed when the app loads — only **Sales**, **My Work**, **Activity** and **Service** open themselves. Click a group open before deciding something isn't there. +Five entries are built on opportunities — **Home**, **Sales Home**, **Sales Performance**, **CRM Overview** and **Pipeline Coverage** — and the sidebar leaves them out for anyone who cannot read opportunities, a service agent for one. + One sidebar row per destination is deliberate. Where an object offers several ways of looking at the same records, those live as tabs across the top of that object's list page, not as extra rows in the nav — so the sidebar stays short enough to scan. Five group names this page used to list are not in the app at all, and several item names sit somewhere other than where it put them: diff --git a/content/docs/getting-started/quick-tour.zh-Hans.mdx b/content/docs/getting-started/quick-tour.zh-Hans.mdx index 4ca6b368d..8f8edf689 100644 --- a/content/docs/getting-started/quick-tour.zh-Hans.mdx +++ b/content/docs/getting-started/quick-tour.zh-Hans.mdx @@ -9,7 +9,7 @@ description: HotCRM 的 5 分钟演练 —— 主页、你的第一条线索、 ## 1. 主页仪表盘 -左侧导航最上面只有一个固定条目 **Home**,你落地的就是它。**Home** 打开的是 **Executive Overview** 仪表盘——9 个磁贴,讲收入、客户与管道: +左侧导航最上面有两个固定条目 **Home** 与 **Sales Home**,你落地的是 **Home**。**Home** 打开的是 **Executive Overview** 仪表盘——9 个磁贴,讲收入、客户与管道: | 磁贴 | 它显示什么 | | --- | --- | @@ -48,7 +48,7 @@ description: HotCRM 的 5 分钟演练 —— 主页、你的第一条线索、 - **HotCRM** —— 包含所有 CRM 功能的主应用。 - **Admin** —— 面向系统管理员。 -在 HotCRM 内,左侧导航最上面是一个固定条目——**Home**,它打开的是 **Executive Overview** 仪表盘;其余内容分在 6 个分组里: +在 HotCRM 内,左侧导航最上面是两个固定条目——**Home**,它打开的是 **Executive Overview** 仪表盘;**Sales Home**,销售代表自己的页面,汇总管道数字、线索、未结商机、未完成任务与即将到来的活动;其余内容分在 6 个分组里: | 分组 | 内容 | | --- | --- | @@ -63,6 +63,8 @@ description: HotCRM 的 5 分钟演练 —— 主页、你的第一条线索、 **Marketing**、**Insights** 这两个分组在应用加载时是折叠的——只有 **Sales**、**My Work**、**Activity**、**Service** 会自动展开。先把分组点开,再判断某样东西在不在这里。 +有五个条目建立在商机之上——**Home**、**Sales Home**、**Sales Performance**、**CRM Overview** 与 **Pipeline Coverage**——不能读取商机的人(例如服务坐席)在侧边栏里看不到它们。 + 一个去处只占一行,这是刻意的。同一个对象如果有多种查看方式,它们是该对象列表页顶部的标签页,而不是导航里另起一行——这样侧边栏才短到能一眼扫完。 本页过去列出的 5 个分组名,应用里根本不存在;另有几个条目名,真身并不在它说的位置: diff --git a/content/docs/getting-started/quick-tour.zh-Hant.mdx b/content/docs/getting-started/quick-tour.zh-Hant.mdx index f399b9304..8a7d953e8 100644 --- a/content/docs/getting-started/quick-tour.zh-Hant.mdx +++ b/content/docs/getting-started/quick-tour.zh-Hant.mdx @@ -11,7 +11,7 @@ description: HotCRM 的 5 分鐘演練 —— 主頁、你的第一條線索、 ## 1. 主頁儀表板 -左側導覽最上面只有一個固定條目 **Home**,你落地的就是它。**Home** 打開的是 **Executive Overview** 儀表板——9 個磁貼,講收入、客戶與管道: +左側導覽最上面有兩個固定條目 **Home** 與 **Sales Home**,你落地的是 **Home**。**Home** 打開的是 **Executive Overview** 儀表板——9 個磁貼,講收入、客戶與管道: | 磁貼 | 它顯示什麼 | | --- | --- | @@ -48,7 +48,7 @@ description: HotCRM 的 5 分鐘演練 —— 主頁、你的第一條線索、 - **HotCRM** —— 包含所有 CRM 功能的主應用。 - **Admin** —— 面向系統管理員。 -在 HotCRM 內,左側導覽最上面是一個固定條目——**Home**,它打開的是 **Executive Overview** 儀表板;其餘內容分在 6 個群組裡: +在 HotCRM 內,左側導覽最上面是兩個固定條目——**Home**,它打開的是 **Executive Overview** 儀表板;**Sales Home**,銷售代表自己的頁面,匯總管道數字、線索、未結商機、未完成任務與即將到來的活動;其餘內容分在 6 個群組裡: | 分組 | 內容 | | --- | --- | @@ -63,6 +63,8 @@ description: HotCRM 的 5 分鐘演練 —— 主頁、你的第一條線索、 **Marketing**、**Insights** 這兩個群組在應用載入時是摺疊的——只有 **Sales**、**My Work**、**Activity**、**Service** 會自動展開。先把群組點開,再判斷某樣東西在不在這裡。 +有五個條目建立在商機之上——**Home**、**Sales Home**、**Sales Performance**、**CRM Overview** 與 **Pipeline Coverage**——不能讀取商機的人(例如服務專員)在側邊欄裡看不到它們。 + 一個去處只佔一列,這是刻意的。同一個物件如果有多種檢視方式,它們是該物件列表頁頂部的標籤頁,而不是導覽裡另起一列——這樣側邊欄才短到能一眼掃完。 本頁過去列出的 5 個群組名,應用裡根本不存在;另有幾個條目名,真身並不在它說的位置: diff --git a/content/docs/sales/activities.mdx b/content/docs/sales/activities.mdx index 4604d464e..49d0a2c11 100644 --- a/content/docs/sales/activities.mdx +++ b/content/docs/sales/activities.mdx @@ -93,7 +93,7 @@ Activities show up in **three** places: 1. **The task list** — **My Work › My Tasks** opens it on *My Open Tasks*. The whole book is the **All Tasks** tab beside it, along with the six other view tabs described below; there is no separate *All Tasks* entry in the sidebar. 2. **The Activity tab on the related record** — open an account, a lead, an opportunity or a case and its Activity tab gathers the tasks and events hanging off it. -3. **The Sales Home page** — its **My Open Tasks** tab embeds that same *My Open Tasks* view, so what you owe is on the page you land on. Sales Home carries no activity *metric*: the four tiles at the top are all pipeline (Revenue (Won), Deals Won, Pipeline Value, Open Leads). Activity volume is reported on the Sales Activity dashboard — see below. +3. **The Sales Home page** — its **My Open Tasks** tab embeds that same *My Open Tasks* view, so what you owe is one click from where the app opens: **Sales Home** is pinned in the sidebar right under **Home**, the app's landing page. A user who cannot read opportunities does not get the entry. Sales Home carries no activity *metric*: the four tiles at the top are all pipeline (Revenue (Won), Deals Won, Pipeline Value, Open Leads). Activity volume is reported on the Sales Activity dashboard — see below. ## Standard list views diff --git a/content/docs/sales/activities.zh-Hans.mdx b/content/docs/sales/activities.zh-Hans.mdx index 4be18cdbd..bb858d39d 100644 --- a/content/docs/sales/activities.zh-Hans.mdx +++ b/content/docs/sales/activities.zh-Hans.mdx @@ -93,7 +93,7 @@ description: 任务——你仍然欠客户的事,挂在任意记录上。每 1. **任务列表**——**我的工作 › 我的任务**打开它,并落在*我的待办任务*上。完整的任务清单就是旁边的 **全部任务** 标签页,连同下面描述的其余六个视图标签页;侧边栏里没有单独的*全部任务*入口。 2. **相关记录上的活动标签页**——打开一个客户、线索、商机或工单,它的活动标签页会把挂在这条记录上的任务与活动(Event)汇总在一起。 -3. **销售主页**——主页上的 **My Open Tasks** 标签页内嵌的就是同一个*我的待办任务*视图,你欠下的事就摆在落地页上。销售主页上**没有**任何活动类**指标**:顶部四个磁贴全是管道指标(Revenue (Won)、Deals Won、Pipeline Value、Open Leads)。活动量的读数在 Sales Activity 仪表盘上——见下文。 +3. **销售主页**——主页上的 **My Open Tasks** 标签页内嵌的就是同一个*我的待办任务*视图,你欠下的事离应用打开的页面只有一次点击:**Sales Home** 固定在侧边栏里、紧挨着应用的落地页 **Home**。不能读取商机的用户看不到这个条目。销售主页上**没有**任何活动类**指标**:顶部四个磁贴全是管道指标(Revenue (Won)、Deals Won、Pipeline Value、Open Leads)。活动量的读数在 Sales Activity 仪表盘上——见下文。 ## 标准列表视图 diff --git a/content/docs/sales/activities.zh-Hant.mdx b/content/docs/sales/activities.zh-Hant.mdx index 3c7301747..f9f3eb458 100644 --- a/content/docs/sales/activities.zh-Hant.mdx +++ b/content/docs/sales/activities.zh-Hant.mdx @@ -95,7 +95,7 @@ description: 任務——你仍然欠客戶的事,掛在任意記錄上。每 1. **任務列表**——**My Work › My Tasks** 打開它,並落在*我的待辦任務*上。完整的任務清單就是旁邊的 **全部任務** 標籤頁,連同下面描述的其餘六個視圖標籤頁;側邊欄裡沒有單獨的*全部任務*入口。 2. **相關記錄上的活動標籤頁**——打開一個客戶、線索、商機或工單,它的活動標籤頁會把掛在這筆記錄上的任務與活動(Event)彙總在一起。 -3. **銷售主頁**——主頁上的 **My Open Tasks** 標籤頁內嵌的就是同一個*我的待辦任務*視圖,你欠下的事就擺在落地頁上。銷售主頁上**沒有**任何活動類**指標**:頂部四個磁貼全是管道指標(Revenue (Won)、Deals Won、Pipeline Value、Open Leads)。活動量的讀數在 Sales Activity 儀表板上——見下文。 +3. **銷售主頁**——主頁上的 **My Open Tasks** 標籤頁內嵌的就是同一個*我的待辦任務*視圖,你欠下的事離應用打開的頁面只有一次點擊:**Sales Home** 固定在側邊欄裡、緊挨著應用的落地頁 **Home**。不能讀取商機的使用者看不到這個條目。銷售主頁上**沒有**任何活動類**指標**:頂部四個磁貼全是管道指標(Revenue (Won)、Deals Won、Pipeline Value、Open Leads)。活動量的讀數在 Sales Activity 儀表板上——見下文。 ## 標準列表視圖 diff --git a/src/sales/apps/crm.app.ts b/src/sales/apps/crm.app.ts index e86b83f88..35d70620d 100644 --- a/src/sales/apps/crm.app.ts +++ b/src/sales/apps/crm.app.ts @@ -1,5 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. +import { P } from '@objectstack/spec'; import { App } from '@objectstack/spec/ui'; // The brand mark, inlined as a `data:` URI of `assets/icon.svg` (#2051), so it @@ -20,6 +21,27 @@ const HOTCRM_ICON = ' %0A' + '%0A'; +/** + * The read gate on the five entries built on `crm_opportunity` (#2060 item 5): + * Home (Executive Overview), Sales Home, Sales Performance, CRM Overview and + * Pipeline Coverage. A member whose permission sets do not read opportunities + * (`service_agent`, `service_manager`) got each one as a page of "You don't + * have access" tiles, one 403 per opportunity query. Object entries are pruned + * by read access already; dashboard, report and page entries are not. + * + * `visible`, not `requiredPermissions` — measured on 18.0.0-next.1: the server + * keeps an entry only when the caller's permission sets grant every + * `requiredPermissions` entry as a CAPABILITY (`systemPermissions`), so + * `['crm_opportunity:read']` hid the entry from every user, the admin + * included, and a capability of our own would be a grant change. This asks + * the effective object permission itself. + * + * Sales Activity (1 of 13 tiles) and Lead Inflow (leads) stay open: they work + * without opportunity read. The gate hides an entry, not the landing — the + * console opens the app's first served entry without reading `visible`. + */ +const READS_OPPORTUNITIES = P`current_user.can("crm_opportunity", "read")`; + /** * HotCRM — navigation. * @@ -76,13 +98,26 @@ export const CrmApp = App.create({ }, navigation: [ - // Pinned landing — single executive view, single click from launcher. + // Pinned landing — the console opens an app on its first entry, for + // every member alike (there is no per-member landing to declare). { id: 'nav_home', type: 'dashboard', dashboardName: 'executive_dashboard', label: 'Home', icon: 'home', + visible: READS_OPPORTUNITIES, + }, + // The rep's own page (`sales_home_page`): pipeline KPIs, my leads, open + // deals and open tasks, upcoming events. It had no entry, only a typed + // URL, while the activities docs sent readers to it. + { + id: 'nav_sales_home', + type: 'page', + pageName: 'sales_home_page', + label: 'Sales Home', + icon: 'layout-grid', + visible: READS_OPPORTUNITIES, }, { @@ -116,7 +151,7 @@ export const CrmApp = App.create({ // points at — a sales object that happened to be filed next to // campaigns. { id: 'nav_product', type: 'object', objectName: 'crm_product', label: 'Products', icon: 'package' }, - { id: 'nav_sales_dashboard', type: 'dashboard', dashboardName: 'sales_dashboard', label: 'Sales Performance', icon: 'chart-line' }, + { id: 'nav_sales_dashboard', type: 'dashboard', dashboardName: 'sales_dashboard', label: 'Sales Performance', icon: 'chart-line', visible: READS_OPPORTUNITIES }, ], }, @@ -280,9 +315,9 @@ export const CrmApp = App.create({ children: [ // Trimmed to three high-signal reports. Full report catalogue still // ships as metadata; admins can pin more from the report picker. - { id: 'nav_crm_dashboard', type: 'dashboard', dashboardName: 'crm_overview_dashboard', label: 'CRM Overview', icon: 'layout-dashboard' }, + { id: 'nav_crm_dashboard', type: 'dashboard', dashboardName: 'crm_overview_dashboard', label: 'CRM Overview', icon: 'layout-dashboard', visible: READS_OPPORTUNITIES }, { id: 'nav_forecast', type: 'object', objectName: 'crm_forecast', label: 'Forecasts', icon: 'trending-up' }, - { id: 'nav_report_pipeline_coverage', type: 'report', reportName: 'pipeline_coverage_by_quarter', label: 'Pipeline Coverage', icon: 'columns-3' }, + { id: 'nav_report_pipeline_coverage', type: 'report', reportName: 'pipeline_coverage_by_quarter', label: 'Pipeline Coverage', icon: 'columns-3', visible: READS_OPPORTUNITIES }, { id: 'nav_report_lead_inflow', type: 'report', reportName: 'lead_inflow_by_month_source', label: 'Lead Inflow', icon: 'trending-up' }, ], }, diff --git a/src/sales/translations/es-ES/app.ts b/src/sales/translations/es-ES/app.ts index 77131eb1d..85263495d 100644 --- a/src/sales/translations/es-ES/app.ts +++ b/src/sales/translations/es-ES/app.ts @@ -33,6 +33,7 @@ export const appSurface: Omit = { nav_activity_dashboard: { label: 'Actividad de Ventas' }, nav_my_calendar: { label: 'Mi Calendario' }, nav_home: { label: 'Inicio' }, + nav_sales_home: { label: 'Inicio de Ventas' }, group_sales: { label: 'Ventas' }, nav_lead: { label: 'Prospectos' }, diff --git a/src/sales/translations/ja-JP/app.ts b/src/sales/translations/ja-JP/app.ts index 352afb1e1..85bb95246 100644 --- a/src/sales/translations/ja-JP/app.ts +++ b/src/sales/translations/ja-JP/app.ts @@ -28,6 +28,7 @@ export const appSurface: Omit = { // Keyed by navigation-node `id` (a flat keyspace regardless of depth). navigation: { nav_home: { label: 'ホーム' }, + nav_sales_home: { label: '営業ホーム' }, group_sales: { label: '営業' }, nav_lead: { label: 'リード' }, diff --git a/src/sales/translations/zh-CN/app.ts b/src/sales/translations/zh-CN/app.ts index 971c525f8..6bcb15e4a 100644 --- a/src/sales/translations/zh-CN/app.ts +++ b/src/sales/translations/zh-CN/app.ts @@ -32,6 +32,7 @@ export const appSurface: Omit = { nav_activity_dashboard: { label: '销售活动' }, nav_my_calendar: { label: '我的日历' }, nav_home: { label: '首页' }, + nav_sales_home: { label: '销售主页' }, group_sales: { label: '销售' }, nav_lead: { label: '线索' }, diff --git a/test/app-navigation-shape.test.ts b/test/app-navigation-shape.test.ts index 7a4581122..eee25863b 100644 --- a/test/app-navigation-shape.test.ts +++ b/test/app-navigation-shape.test.ts @@ -2,6 +2,9 @@ import { describe, it, expect } from 'vitest'; import { CrmApp } from '../src/sales/apps/crm.app'; +import { SalesRepProfile } from '../src/sales/profiles/sales-rep.profile'; +import { SalesManagerProfile } from '../src/sales/profiles/sales-manager.profile'; +import { ServiceAgentProfile, ServiceManagerProfile } from '../src/sales/profiles/service-agent.profile'; /** * The two standing constraints on `src/apps/crm.app.ts`'s shape (#1259). @@ -20,9 +23,8 @@ import { CrmApp } from '../src/sales/apps/crm.app'; * 1. **A FLOOR on variety.** HotCRM is the exemplar app: an author reading it * is reading it to learn what each kind of nav item looks like. Nav items * come in six kinds and every one of them must keep at least one entry — - * `page` and `component` are down to a single exemplar each, so the next - * slimming pass is one deletion away from removing a demonstration - * silently. That is the failure this half exists to make loud. + * `component` is down to a single exemplar, so the next slimming pass is + * one deletion away from removing a demonstration silently. That is the failure this half exists to make loud. * * 2. **A CEILING on duplication.** No two entries may open the same * destination, and no object may hold more than one plain list entry. @@ -153,3 +155,34 @@ describe('the app navigation opens each destination once (#1259)', () => { expect(bad, `object entries with an empty viewName:\n ${bad.join('\n ')}`).toEqual([]); }); }); + +describe('entries built on opportunities are gated on reading them (#2060 item 5)', () => { + // Measured on 18.0.0-next.1 (PR body has the per-persona tables): without the + // gate a service agent was offered all five and each answered "You don't have + // access", one 403 per opportunity query. `requiredPermissions` cannot say + // this — the server reads it as capability names, and `crm_opportunity:read` + // hid the entry from every user, the admin included (see `crm.app.ts`). + const GATE = 'current_user.can("crm_opportunity", "read")'; + const GATED = ['nav_home', 'nav_sales_home', 'nav_sales_dashboard', 'nav_crm_dashboard', 'nav_report_pipeline_coverage']; + const sourceOf = (v: unknown): unknown => (v && typeof v === 'object' ? (v as AnyRec).source : v); + + it('exactly these entries carry the opportunity read gate', () => { + const gated = LEAVES.filter((n) => sourceOf(n.visible) === GATE).map((n) => n.id); + expect(gated).toEqual(GATED); + const other = LEAVES.filter((n) => n.visible !== undefined && sourceOf(n.visible) !== GATE).map((n) => n.id); + expect(other, 'entries gated on something else').toEqual([]); + }); + + it('the gate parts the personas the way the permission sets say', () => { + const reads = (set: AnyRec) => set.objects?.crm_opportunity?.allowRead === true; + expect([SalesRepProfile, SalesManagerProfile].map(reads)).toEqual([true, true]); + expect([ServiceAgentProfile, ServiceManagerProfile].map(reads)).toEqual([false, false]); + }); + + it('Sales Home has a sidebar entry, second after Home', () => { + expect(NAV.slice(0, 2).map((n) => [n.id, n.dashboardName ?? n.pageName])).toEqual([ + ['nav_home', 'executive_dashboard'], + ['nav_sales_home', 'sales_home_page'], + ]); + }); +}); diff --git a/test/docs-quick-tour-navigation.test.ts b/test/docs-quick-tour-navigation.test.ts index 059ef1c78..3851a849f 100644 --- a/test/docs-quick-tour-navigation.test.ts +++ b/test/docs-quick-tour-navigation.test.ts @@ -310,8 +310,8 @@ describe('getting-started/quick-tour names the navigation the app really ships ( }); describe('the source facts the quick-tour table now rests on (#960)', () => { - it('the app has one pinned top-level entry and six groups', () => { - expect(PINNED.map((p) => p.label)).toEqual(['Home']); + it('the app has two pinned top-level entries and six groups', () => { + expect(PINNED.map((p) => p.label)).toEqual(['Home', 'Sales Home']); expect(GROUP_LABELS).toEqual([ 'Sales', 'My Work', From 97d9ef688a015a9f0a38f05901bff441d29dc5a6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 04:31:30 +0000 Subject: [PATCH 2/4] chore(changeset): the sidebar entries a service agent cannot open, and Sales Home's entry Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01P8mZuzuyZAHSrgQKmv6ASv --- .changeset/2060-item-5-persona-home.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/2060-item-5-persona-home.md diff --git a/.changeset/2060-item-5-persona-home.md b/.changeset/2060-item-5-persona-home.md new file mode 100644 index 000000000..9e34c077c --- /dev/null +++ b/.changeset/2060-item-5-persona-home.md @@ -0,0 +1,5 @@ +--- +'hotcrm': patch +--- + +The sidebar no longer offers a service agent pages it cannot open. **Home** (Executive Overview), **Sales Performance**, **CRM Overview** and **Pipeline Coverage** are built on opportunities, and a user who cannot read opportunities (the Service Agent and Service Manager permission sets) used to get each of them as a page of "You don't have access" tiles. Those entries are now left out for such a user; everyone who reads opportunities sees the same sidebar as before. **Sales Home**, the rep's page of pipeline figures, leads, open deals, open tasks and upcoming events, is now pinned in the sidebar under **Home**. Before, it could only be reached by typing its address. The app still opens on **Home** for every user, a service agent included: the console has no way to give each role its own landing page yet. From 5ef6b46abfd7ddbfa8cc53d6f25a4f92f491fa93 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 04:43:18 +0000 Subject: [PATCH 3/4] fix(sales-home): remove the Quick Create card, which drew the sidebar again and created nothing The card was a `page:card` titled "Quick Create" around a bare `nav:menu`: 28 navigation links under a title promising create actions. The pinned Sales Home entry puts that page one click from every rep, so the card goes, with its four locale-pack keys. Nothing on 18.0.0-next.1 creates a record from a page block without hard-coding a console route; the comment in the region says what brings it back. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01P8mZuzuyZAHSrgQKmv6ASv --- .changeset/2060-item-5-persona-home.md | 2 +- src/sales/pages/home.page.ts | 28 +++++++++++--------------- src/sales/translations/en/app.ts | 1 - src/sales/translations/es-ES/app.ts | 1 - src/sales/translations/ja-JP/app.ts | 1 - src/sales/translations/zh-CN/app.ts | 1 - 6 files changed, 13 insertions(+), 21 deletions(-) diff --git a/.changeset/2060-item-5-persona-home.md b/.changeset/2060-item-5-persona-home.md index 9e34c077c..c471db390 100644 --- a/.changeset/2060-item-5-persona-home.md +++ b/.changeset/2060-item-5-persona-home.md @@ -2,4 +2,4 @@ 'hotcrm': patch --- -The sidebar no longer offers a service agent pages it cannot open. **Home** (Executive Overview), **Sales Performance**, **CRM Overview** and **Pipeline Coverage** are built on opportunities, and a user who cannot read opportunities (the Service Agent and Service Manager permission sets) used to get each of them as a page of "You don't have access" tiles. Those entries are now left out for such a user; everyone who reads opportunities sees the same sidebar as before. **Sales Home**, the rep's page of pipeline figures, leads, open deals, open tasks and upcoming events, is now pinned in the sidebar under **Home**. Before, it could only be reached by typing its address. The app still opens on **Home** for every user, a service agent included: the console has no way to give each role its own landing page yet. +The sidebar no longer offers a service agent pages it cannot open. **Home** (Executive Overview), **Sales Performance**, **CRM Overview** and **Pipeline Coverage** are built on opportunities, and a user who cannot read opportunities (the Service Agent and Service Manager permission sets) used to get each of them as a page of "You don't have access" tiles. Those entries are now left out for such a user; everyone who reads opportunities sees the same sidebar as before. **Sales Home**, the rep's page of pipeline figures, leads, open deals, open tasks and upcoming events, is now pinned in the sidebar under **Home**. Before, it could only be reached by typing its address. Its **Quick Create** card is gone: it drew a second copy of the sidebar's links and could create nothing, and the platform has no create-record block to put there yet. Each list page keeps its own **New** button. The app still opens on **Home** for every user, a service agent included: the console has no way to give each role its own landing page yet. diff --git a/src/sales/pages/home.page.ts b/src/sales/pages/home.page.ts index 84d1ee61b..314890a4f 100644 --- a/src/sales/pages/home.page.ts +++ b/src/sales/pages/home.page.ts @@ -129,22 +129,18 @@ export const SalesHomePage: Page = { name: 'left_sidebar', width: 'small', components: [ - { - type: 'page:card', - id: 'quick_create', - label: 'Quick Create', - properties: { - title: 'Quick Create', - bordered: true, - children: [ - { - type: 'nav:menu', - id: 'create_menu', - properties: {}, - }, - ], - }, - }, + // There is no `Quick Create` card here any more either (#2060 item 5), + // and re-adding one needs a block that creates records. + // + // It was a `page:card` titled "Quick Create" around a bare `nav:menu`, + // which draws the app's navigation: the sidebar a second time, 28 + // links, nothing that creates. The title lied, as below. On 18.0.0-next.1 + // `ComponentPropsMap` has no create block: the `action:*` blocks run + // `script` / `url` / `modal` / `flow` / `api` / `form`, where `form` + // opens a FormView at `/forms/NAME` and `modal` a page, so a "New Lead" + // button would hard-code a console route. Each list page has its own + // New button. When the platform ships a create block, it goes here. + // // There is no `Recent Items` card here any more, and re-adding one // needs a data source that does not exist yet. // diff --git a/src/sales/translations/en/app.ts b/src/sales/translations/en/app.ts index 36fc45149..f7d2ff58e 100644 --- a/src/sales/translations/en/app.ts +++ b/src/sales/translations/en/app.ts @@ -211,7 +211,6 @@ export const appSurface: Omit = { title: 'Sales Dashboard', subtitle: 'Welcome back', components: { - quick_create: { title: 'Quick Create', label: 'Quick Create' }, key_metrics: { title: 'Key Performance Indicators', label: 'Key Metrics' }, home_tabs: { label: 'Home Tabs' }, ai_briefing: { diff --git a/src/sales/translations/es-ES/app.ts b/src/sales/translations/es-ES/app.ts index 85263495d..39416210e 100644 --- a/src/sales/translations/es-ES/app.ts +++ b/src/sales/translations/es-ES/app.ts @@ -568,7 +568,6 @@ export const appSurface: Omit = { home_upcoming_events: { label: '📅 Próximos · Más cercanos primero', }, - quick_create: { title: 'Creación Rápida', label: 'Creación Rápida' }, key_metrics: { title: 'Indicadores Clave de Rendimiento', label: 'Métricas Clave' }, home_tabs: { label: 'Pestañas de Inicio' }, home_my_leads: { label: 'Mis Prospectos' }, diff --git a/src/sales/translations/ja-JP/app.ts b/src/sales/translations/ja-JP/app.ts index 85bb95246..8276d06fe 100644 --- a/src/sales/translations/ja-JP/app.ts +++ b/src/sales/translations/ja-JP/app.ts @@ -570,7 +570,6 @@ export const appSurface: Omit = { home_upcoming_events: { label: '📅 開催予定 · 直近順', }, - quick_create: { title: 'クイック作成', label: 'クイック作成' }, key_metrics: { title: '主要業績評価指標', label: '主要指標' }, home_tabs: { label: 'ホームタブ' }, home_my_leads: { label: '私のリード' }, diff --git a/src/sales/translations/zh-CN/app.ts b/src/sales/translations/zh-CN/app.ts index 6bcb15e4a..2f95f4d87 100644 --- a/src/sales/translations/zh-CN/app.ts +++ b/src/sales/translations/zh-CN/app.ts @@ -569,7 +569,6 @@ export const appSurface: Omit = { home_upcoming_events: { label: '📅 即将开始 · 最近优先', }, - quick_create: { title: '快速创建', label: '快速创建' }, key_metrics: { title: '关键绩效指标', label: '关键指标' }, home_tabs: { label: '主页标签页' }, home_my_leads: { label: '我的线索' }, From cab1440d535d81ab1ad7422fecc5dd17f519d02e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 04:53:18 +0000 Subject: [PATCH 4/4] fix(sales-home): the page description no longer promises quick actions With the Quick Create card gone, "key metrics and quick actions" named something the page does not have. The description, its aria text and the four locale packs now say what the page shows. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01P8mZuzuyZAHSrgQKmv6ASv --- src/sales/pages/home.page.ts | 5 ++--- src/sales/translations/en/app.ts | 2 +- src/sales/translations/es-ES/app.ts | 2 +- src/sales/translations/ja-JP/app.ts | 2 +- src/sales/translations/zh-CN/app.ts | 2 +- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/sales/pages/home.page.ts b/src/sales/pages/home.page.ts index 314890a4f..eada37e3b 100644 --- a/src/sales/pages/home.page.ts +++ b/src/sales/pages/home.page.ts @@ -58,12 +58,11 @@ const embeddedListView = (id: string, objectName: string, views: View, viewKey: * - Dashboard-style layout * - Multiple component regions * - Global search and notifications - * - Quick action cards */ export const SalesHomePage: Page = { name: 'sales_home_page', label: 'Sales Home', - description: 'Sales team home page with key metrics and quick actions', + description: 'Sales team home page with key metrics, my open work and upcoming events', type: 'home', @@ -349,6 +348,6 @@ export const SalesHomePage: Page = { aria: { ariaLabel: 'Sales Home Page', - ariaDescribedBy: 'Sales team home page with metrics, leads, and quick actions', + ariaDescribedBy: 'Sales team home page with metrics, my open work and upcoming events', }, }; diff --git a/src/sales/translations/en/app.ts b/src/sales/translations/en/app.ts index f7d2ff58e..7f720b890 100644 --- a/src/sales/translations/en/app.ts +++ b/src/sales/translations/en/app.ts @@ -207,7 +207,7 @@ export const appSurface: Omit = { }, sales_home_page: { label: 'Sales Home', - description: 'Sales team home page with key metrics and quick actions', + description: 'Sales team home page with key metrics, my open work and upcoming events', title: 'Sales Dashboard', subtitle: 'Welcome back', components: { diff --git a/src/sales/translations/es-ES/app.ts b/src/sales/translations/es-ES/app.ts index 39416210e..ce4312612 100644 --- a/src/sales/translations/es-ES/app.ts +++ b/src/sales/translations/es-ES/app.ts @@ -549,7 +549,7 @@ export const appSurface: Omit = { }, sales_home_page: { label: 'Inicio de Ventas', - description: 'Página de inicio del equipo comercial con métricas clave y acciones rápidas', + description: 'Página de inicio del equipo comercial con métricas clave, mi trabajo pendiente y próximos eventos', title: 'Panel de Ventas', subtitle: 'Bienvenido de nuevo', components: { diff --git a/src/sales/translations/ja-JP/app.ts b/src/sales/translations/ja-JP/app.ts index 8276d06fe..31308c1bf 100644 --- a/src/sales/translations/ja-JP/app.ts +++ b/src/sales/translations/ja-JP/app.ts @@ -551,7 +551,7 @@ export const appSurface: Omit = { }, sales_home_page: { label: '営業ホーム', - description: '主要指標とクイックアクションをまとめた営業チームのホームページ', + description: '主要指標、自分の未完了の仕事、今後の予定をまとめた営業チームのホームページ', title: '営業ダッシュボード', subtitle: 'おかえりなさい', components: { diff --git a/src/sales/translations/zh-CN/app.ts b/src/sales/translations/zh-CN/app.ts index 2f95f4d87..ec0d38a38 100644 --- a/src/sales/translations/zh-CN/app.ts +++ b/src/sales/translations/zh-CN/app.ts @@ -550,7 +550,7 @@ export const appSurface: Omit = { }, sales_home_page: { label: '销售主页', - description: '销售团队主页,汇总关键指标与快捷操作', + description: '销售团队主页,汇总关键指标、我的待办工作与即将到来的活动', title: '销售看板', subtitle: '欢迎回来', components: {