Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/2383-module-off-revenue-tile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"hotcrm": patch
---

Clause-②: no
Metadata-protocol: none

**The List Price by Category tile is left out when the revenue module is disabled (#2383).** The tile on the CRM Overview dashboard reads revenue's product catalogue. When revenue is disabled, the platform stops serving that module's objects and datasets, and the tile drew a red "Dataset "product_metrics" is not defined in this environment" error. It now asks for the product object first, so the dashboard simply leaves it out and the other tiles fill the page. With revenue enabled, nothing changes: the tile draws as before.

The other screens of the app that reach a disabled module were measured on ObjectStack 18.2.0 too. The Campaign lookup on the opportunity form and the Related Case lookup on tasks and events show their value on the record page. On the edit form they read empty, with an empty picker, and saving keeps the stored value. The related lists (campaign members on a lead, quotes and line items on an opportunity) read empty. None of these raises an error. **Add to Campaign** on leads and contacts and **Generate Quote** on opportunities still show when their module is off and fail when used. Hiding them needs a platform change, because the permission check those buttons read still grants a disabled module's objects. That change is reported to the platform.
6 changes: 6 additions & 0 deletions src/sales/dashboards/crm.dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,12 @@ export const CrmOverviewDashboard: Dashboard = {
// resolve — it just means nothing on a shared product catalog, where the
// column records whoever created the entry.
filterBindings: { dateRange: false, owner_id: false },
// `product_metrics` ships in the revenue module, so the tile asks for
// that dataset's object and is left out when revenue is disabled (#2383).
// Measured on 18.2.0: a disabled package's objects drop out of the served
// object list this key is checked against; without it the tile drew a
// red "Dataset "product_metrics" is not defined in this environment".
requiresObject: 'crm_product',
dataset: 'product_metrics', dimensions: ['category'], values: ['list_price_sum'],
layout: { x: 0, y: 6, w: 12, h: 4 },
chartConfig: {
Expand Down
19 changes: 19 additions & 0 deletions test/package-ownership.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,25 @@ describe('every object has exactly one owner', () => {
}
});

it('an app tile on a module dataset asks for that dataset\'s object, so a disabled module leaves it out (#2383)', () => {
// A disabled package's objects drop out of the served object list a
// widget's `requiresObject` is checked against (measured on 18.2.0), so
// the tile is omitted instead of drawing "dataset not defined". With the
// module on, the object is served and the tile draws as before.
const moduleDatasets = new Map<string, { owner: string; object: string }>();
for (const id of MODULE_IDS) {
for (const ds of (body(id).datasets ?? []) as AnyRec[]) moduleDatasets.set(ds.name, { owner: id, object: ds.object });
}
const tiles = ((body(APP_ID).dashboards ?? []) as AnyRec[]).flatMap((d) =>
((d.widgets ?? []) as AnyRec[]).filter((w) => moduleDatasets.has(w.dataset)).map((w) => ({ dashboard: d.name, w })));
expect(tiles.map(({ dashboard, w }) => `${dashboard}.${w.id}`)).toEqual(['crm_overview_dashboard.top_products']);
for (const { dashboard, w } of tiles) {
const { owner, object } = moduleDatasets.get(w.dataset)!;
expect(w.requiresObject, `${dashboard}.${w.id}`).toBe(object);
expect(namesOf(body(owner).objects), owner).toContain(object);
}
});

it('the permission sets stay whole in the app package (ADR-0130, 2026-09-02 addendum)', () => {
for (const id of MODULE_IDS) expect(namesOf(body(id).permissions), id).toEqual([]);
const grantingOn = (objects: string[]) => ((body(APP_ID).permissions ?? []) as AnyRec[]).filter((set) =>
Expand Down
Loading