@multiplatform.one/backoffice 7.23.2 → 7.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/chrome/awesomebar-data.cjs +4 -2
- package/dist/cjs/chrome/awesomebar-data.native.js +7 -4
- package/dist/cjs/chrome/awesomebar-data.native.js.map +1 -1
- package/dist/cjs/chrome/help-about-dialog.cjs +2 -2
- package/dist/cjs/chrome/help-about-dialog.native.js +2 -2
- package/dist/cjs/chrome/help-shortcuts-dialog.cjs +2 -2
- package/dist/cjs/chrome/help-shortcuts-dialog.native.js +2 -2
- package/dist/cjs/chrome/notifications-panel.cjs +8 -6
- package/dist/cjs/chrome/notifications-panel.native.js +8 -6
- package/dist/cjs/chrome/notifications-panel.native.js.map +1 -1
- package/dist/cjs/form/DocFormScreen.cjs +43 -43
- package/dist/cjs/form/DocFormScreen.native.js +43 -43
- package/dist/cjs/form/DocFormScreen.native.js.map +1 -1
- package/dist/cjs/form/sidebar-people.cjs +5 -2
- package/dist/cjs/form/sidebar-people.native.js +5 -2
- package/dist/cjs/form/sidebar-people.native.js.map +1 -1
- package/dist/cjs/i18n/en.cjs +1 -1
- package/dist/cjs/i18n/en.native.js +1 -1
- package/dist/cjs/i18n/en.native.js.map +1 -1
- package/dist/cjs/list/ListBulkActionsBar.cjs +1 -1
- package/dist/cjs/list/ListBulkActionsBar.native.js +1 -2
- package/dist/cjs/list/ListBulkActionsBar.native.js.map +1 -1
- package/dist/cjs/shell/WorkspaceSidebar.cjs +9 -7
- package/dist/cjs/shell/WorkspaceSidebar.native.js +9 -7
- package/dist/cjs/shell/WorkspaceSidebar.native.js.map +1 -1
- package/dist/cjs/views/CalendarView.cjs +5 -1
- package/dist/cjs/views/CalendarView.native.js +7 -2
- package/dist/cjs/views/CalendarView.native.js.map +1 -1
- package/dist/cjs/views/DashboardView.cjs +9 -3
- package/dist/cjs/views/DashboardView.native.js +9 -3
- package/dist/cjs/views/DashboardView.native.js.map +1 -1
- package/dist/cjs/views/KanbanView.cjs +5 -2
- package/dist/cjs/views/KanbanView.native.js +7 -3
- package/dist/cjs/views/KanbanView.native.js.map +1 -1
- package/dist/cjs/views/ReportView.cjs +8 -5
- package/dist/cjs/views/ReportView.native.js +11 -6
- package/dist/cjs/views/ReportView.native.js.map +1 -1
- package/dist/cjs/views/ViewHeader.cjs +8 -1
- package/dist/cjs/views/ViewHeader.native.js +8 -1
- package/dist/cjs/views/ViewHeader.native.js.map +1 -1
- package/dist/cjs/views/dashboard-data.cjs +17 -11
- package/dist/cjs/views/dashboard-data.native.js +20 -13
- package/dist/cjs/views/dashboard-data.native.js.map +1 -1
- package/dist/esm/chrome/awesomebar-data.mjs +4 -2
- package/dist/esm/chrome/awesomebar-data.mjs.map +1 -1
- package/dist/esm/chrome/awesomebar-data.native.js +7 -4
- package/dist/esm/chrome/awesomebar-data.native.js.map +1 -1
- package/dist/esm/chrome/help-about-dialog.mjs +2 -2
- package/dist/esm/chrome/help-about-dialog.mjs.map +1 -1
- package/dist/esm/chrome/help-about-dialog.native.js +2 -2
- package/dist/esm/chrome/help-about-dialog.native.js.map +1 -1
- package/dist/esm/chrome/help-shortcuts-dialog.mjs +2 -2
- package/dist/esm/chrome/help-shortcuts-dialog.mjs.map +1 -1
- package/dist/esm/chrome/help-shortcuts-dialog.native.js +2 -2
- package/dist/esm/chrome/notifications-panel.mjs +8 -6
- package/dist/esm/chrome/notifications-panel.mjs.map +1 -1
- package/dist/esm/chrome/notifications-panel.native.js +8 -6
- package/dist/esm/chrome/notifications-panel.native.js.map +1 -1
- package/dist/esm/form/DocFormScreen.mjs +43 -43
- package/dist/esm/form/DocFormScreen.mjs.map +1 -1
- package/dist/esm/form/DocFormScreen.native.js +43 -43
- package/dist/esm/form/DocFormScreen.native.js.map +1 -1
- package/dist/esm/form/sidebar-people.mjs +6 -3
- package/dist/esm/form/sidebar-people.mjs.map +1 -1
- package/dist/esm/form/sidebar-people.native.js +6 -3
- package/dist/esm/form/sidebar-people.native.js.map +1 -1
- package/dist/esm/i18n/en.mjs +1 -1
- package/dist/esm/i18n/en.native.js +1 -1
- package/dist/esm/list/ListBulkActionsBar.mjs +2 -2
- package/dist/esm/list/ListBulkActionsBar.mjs.map +1 -1
- package/dist/esm/list/ListBulkActionsBar.native.js +2 -3
- package/dist/esm/list/ListBulkActionsBar.native.js.map +1 -1
- package/dist/esm/shell/WorkspaceSidebar.mjs +9 -7
- package/dist/esm/shell/WorkspaceSidebar.mjs.map +1 -1
- package/dist/esm/shell/WorkspaceSidebar.native.js +9 -7
- package/dist/esm/shell/WorkspaceSidebar.native.js.map +1 -1
- package/dist/esm/views/CalendarView.mjs +6 -2
- package/dist/esm/views/CalendarView.mjs.map +1 -1
- package/dist/esm/views/CalendarView.native.js +8 -3
- package/dist/esm/views/CalendarView.native.js.map +1 -1
- package/dist/esm/views/DashboardView.mjs +9 -3
- package/dist/esm/views/DashboardView.mjs.map +1 -1
- package/dist/esm/views/DashboardView.native.js +9 -3
- package/dist/esm/views/DashboardView.native.js.map +1 -1
- package/dist/esm/views/KanbanView.mjs +6 -3
- package/dist/esm/views/KanbanView.mjs.map +1 -1
- package/dist/esm/views/KanbanView.native.js +8 -4
- package/dist/esm/views/KanbanView.native.js.map +1 -1
- package/dist/esm/views/ReportView.mjs +8 -5
- package/dist/esm/views/ReportView.mjs.map +1 -1
- package/dist/esm/views/ReportView.native.js +11 -6
- package/dist/esm/views/ReportView.native.js.map +1 -1
- package/dist/esm/views/ViewHeader.mjs +8 -1
- package/dist/esm/views/ViewHeader.mjs.map +1 -1
- package/dist/esm/views/ViewHeader.native.js +8 -1
- package/dist/esm/views/ViewHeader.native.js.map +1 -1
- package/dist/esm/views/dashboard-data.mjs +17 -11
- package/dist/esm/views/dashboard-data.mjs.map +1 -1
- package/dist/esm/views/dashboard-data.native.js +20 -13
- package/dist/esm/views/dashboard-data.native.js.map +1 -1
- package/dist/jsx/chrome/awesomebar-data.mjs +4 -2
- package/dist/jsx/chrome/awesomebar-data.mjs.map +1 -1
- package/dist/jsx/chrome/awesomebar-data.native.js +7 -4
- package/dist/jsx/chrome/awesomebar-data.native.js.map +1 -1
- package/dist/jsx/chrome/help-about-dialog.mjs +2 -2
- package/dist/jsx/chrome/help-about-dialog.mjs.map +1 -1
- package/dist/jsx/chrome/help-about-dialog.native.js +2 -2
- package/dist/jsx/chrome/help-shortcuts-dialog.mjs +2 -2
- package/dist/jsx/chrome/help-shortcuts-dialog.mjs.map +1 -1
- package/dist/jsx/chrome/help-shortcuts-dialog.native.js +2 -2
- package/dist/jsx/chrome/help-shortcuts-dialog.native.js.map +1 -1
- package/dist/jsx/chrome/notifications-panel.mjs +8 -6
- package/dist/jsx/chrome/notifications-panel.mjs.map +1 -1
- package/dist/jsx/chrome/notifications-panel.native.js +8 -6
- package/dist/jsx/chrome/notifications-panel.native.js.map +1 -1
- package/dist/jsx/form/DocFormScreen.mjs +43 -43
- package/dist/jsx/form/DocFormScreen.mjs.map +1 -1
- package/dist/jsx/form/DocFormScreen.native.js +43 -43
- package/dist/jsx/form/DocFormScreen.native.js.map +1 -1
- package/dist/jsx/form/sidebar-people.mjs +6 -3
- package/dist/jsx/form/sidebar-people.mjs.map +1 -1
- package/dist/jsx/form/sidebar-people.native.js +5 -2
- package/dist/jsx/form/sidebar-people.native.js.map +1 -1
- package/dist/jsx/i18n/en.mjs +1 -1
- package/dist/jsx/i18n/en.native.js +1 -1
- package/dist/jsx/i18n/en.native.js.map +1 -1
- package/dist/jsx/list/ListBulkActionsBar.mjs +2 -2
- package/dist/jsx/list/ListBulkActionsBar.mjs.map +1 -1
- package/dist/jsx/list/ListBulkActionsBar.native.js +1 -2
- package/dist/jsx/list/ListBulkActionsBar.native.js.map +1 -1
- package/dist/jsx/shell/WorkspaceSidebar.mjs +9 -7
- package/dist/jsx/shell/WorkspaceSidebar.mjs.map +1 -1
- package/dist/jsx/shell/WorkspaceSidebar.native.js +9 -7
- package/dist/jsx/shell/WorkspaceSidebar.native.js.map +1 -1
- package/dist/jsx/views/CalendarView.mjs +6 -2
- package/dist/jsx/views/CalendarView.mjs.map +1 -1
- package/dist/jsx/views/CalendarView.native.js +7 -2
- package/dist/jsx/views/CalendarView.native.js.map +1 -1
- package/dist/jsx/views/DashboardView.mjs +9 -3
- package/dist/jsx/views/DashboardView.mjs.map +1 -1
- package/dist/jsx/views/DashboardView.native.js +9 -3
- package/dist/jsx/views/DashboardView.native.js.map +1 -1
- package/dist/jsx/views/KanbanView.mjs +6 -3
- package/dist/jsx/views/KanbanView.mjs.map +1 -1
- package/dist/jsx/views/KanbanView.native.js +7 -3
- package/dist/jsx/views/KanbanView.native.js.map +1 -1
- package/dist/jsx/views/ReportView.mjs +8 -5
- package/dist/jsx/views/ReportView.mjs.map +1 -1
- package/dist/jsx/views/ReportView.native.js +11 -6
- package/dist/jsx/views/ReportView.native.js.map +1 -1
- package/dist/jsx/views/ViewHeader.mjs +8 -1
- package/dist/jsx/views/ViewHeader.mjs.map +1 -1
- package/dist/jsx/views/ViewHeader.native.js +8 -1
- package/dist/jsx/views/ViewHeader.native.js.map +1 -1
- package/dist/jsx/views/dashboard-data.mjs +17 -11
- package/dist/jsx/views/dashboard-data.mjs.map +1 -1
- package/dist/jsx/views/dashboard-data.native.js +20 -13
- package/dist/jsx/views/dashboard-data.native.js.map +1 -1
- package/package.json +12 -12
- package/src/chrome/NotificationsButton.screen.spec.tsx +162 -0
- package/src/chrome/UserMenu.screen.spec.tsx +107 -0
- package/src/chrome/awesomebar-data.auth.spec.ts +56 -0
- package/src/chrome/awesomebar-data.tsx +4 -2
- package/src/chrome/help-about-dialog.tsx +2 -2
- package/src/chrome/help-shortcuts-dialog.tsx +2 -2
- package/src/chrome/notifications-panel.tsx +8 -9
- package/src/form/DocFormScreen.tsx +54 -45
- package/src/form/FormConnections.screen.spec.tsx +153 -0
- package/src/form/FormDialogs.screen.spec.tsx +119 -0
- package/src/form/FormToolbar.screen.spec.tsx +147 -0
- package/src/form/sidebar-people.tsx +7 -5
- package/src/i18n/en.ts +1 -1
- package/src/list/ListBulkActionsBar.screen.spec.tsx +181 -0
- package/src/list/ListBulkActionsBar.tsx +6 -2
- package/src/list/ListFilterBar.screen.spec.tsx +176 -0
- package/src/list/ListSidebar.screen.spec.tsx +202 -0
- package/src/quickEntry/QuickEntryDialog.screen.spec.tsx +143 -0
- package/src/report/DoctypeReportView.screen.spec.tsx +154 -0
- package/src/shell/WorkspaceSidebar.tsx +10 -9
- package/src/views/CalendarView.screen.spec.tsx +241 -0
- package/src/views/CalendarView.tsx +11 -3
- package/src/views/DashboardView.screen.spec.tsx +144 -0
- package/src/views/DashboardView.tsx +7 -3
- package/src/views/ImageView.screen.spec.tsx +108 -0
- package/src/views/KanbanView.screen.spec.tsx +282 -0
- package/src/views/KanbanView.tsx +7 -3
- package/src/views/ReportView.screen.spec.tsx +108 -0
- package/src/views/ReportView.tsx +10 -5
- package/src/views/ViewHeader.screen.spec.tsx +185 -0
- package/src/views/ViewHeader.tsx +4 -1
- package/src/views/dashboard-data.tsx +21 -12
- package/types/chrome/awesomebar-data.d.ts.map +1 -1
- package/types/chrome/notifications-panel.d.ts.map +1 -1
- package/types/form/DocFormScreen.d.ts.map +1 -1
- package/types/form/sidebar-people.d.ts.map +1 -1
- package/types/i18n/en.d.ts +1 -1
- package/types/list/ListBulkActionsBar.d.ts.map +1 -1
- package/types/shell/WorkspaceSidebar.d.ts.map +1 -1
- package/types/views/CalendarView.d.ts.map +1 -1
- package/types/views/DashboardView.d.ts.map +1 -1
- package/types/views/KanbanView.d.ts.map +1 -1
- package/types/views/ReportView.d.ts.map +1 -1
- package/types/views/ViewHeader.d.ts.map +1 -1
- package/types/views/dashboard-data.d.ts +4 -2
- package/types/views/dashboard-data.d.ts.map +1 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// The per-doctype dashboard (MPO-414): KPI counts over the rolling windows,
|
|
2
|
+
// a card per status option that opens the pre-filtered list, the weekly
|
|
3
|
+
// chart's empty state, and the desk scope ANDed into every query (GL-5).
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { InMemoryFixtureProvider } from "@multiplatform.one/frappe";
|
|
6
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
7
|
+
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
import { BackofficeProvider } from "../config/BackofficeProvider";
|
|
10
|
+
import { DeskScopeProvider } from "../desk/scope";
|
|
11
|
+
import { createMemoryNavigator, type MemoryNavigator } from "../navigation/navigator";
|
|
12
|
+
import type { FrappeFilters } from "../shared/types";
|
|
13
|
+
import { DashboardView } from "./DashboardView";
|
|
14
|
+
|
|
15
|
+
function dashboardMeta(name: string, withStatus = true) {
|
|
16
|
+
return {
|
|
17
|
+
name,
|
|
18
|
+
doctype: "DocType",
|
|
19
|
+
module: "QA",
|
|
20
|
+
title_field: "subject",
|
|
21
|
+
fields: [
|
|
22
|
+
{ fieldname: "subject", fieldtype: "Data", label: "Subject", in_list_view: 1 },
|
|
23
|
+
...(withStatus
|
|
24
|
+
? [
|
|
25
|
+
{
|
|
26
|
+
fieldname: "status",
|
|
27
|
+
fieldtype: "Select",
|
|
28
|
+
label: "Status",
|
|
29
|
+
options: "Open\nWorking\nDone",
|
|
30
|
+
in_list_view: 1,
|
|
31
|
+
},
|
|
32
|
+
]
|
|
33
|
+
: []),
|
|
34
|
+
{ fieldname: "owner_name", fieldtype: "Data", label: "Owner", in_list_view: 1 },
|
|
35
|
+
],
|
|
36
|
+
permissions: [{ role: "All", read: 1 }],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function daysAgo(days: number): string {
|
|
41
|
+
const date = new Date();
|
|
42
|
+
date.setDate(date.getDate() - days);
|
|
43
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
44
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} 10:00:00`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// bo's records are recent; ana's were created and last touched long ago.
|
|
48
|
+
const docs = [
|
|
49
|
+
["QAD-001", "Open", "bo", 2, 1],
|
|
50
|
+
["QAD-002", "Working", "bo", 10, 3],
|
|
51
|
+
["QAD-003", "Working", "bo", 40, 20],
|
|
52
|
+
["QAD-004", "Done", "ana", 200, 150],
|
|
53
|
+
["QAD-005", "Done", "ana", 300, 250],
|
|
54
|
+
].map(([name, status, owner, created, modified]) => ({
|
|
55
|
+
name: String(name),
|
|
56
|
+
doctype: "QaDash",
|
|
57
|
+
subject: `Doc ${name}`,
|
|
58
|
+
status,
|
|
59
|
+
owner_name: owner,
|
|
60
|
+
creation: daysAgo(Number(created)),
|
|
61
|
+
modified: daysAgo(Number(modified)),
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
let run = 0;
|
|
65
|
+
|
|
66
|
+
function renderDashboard(doctype: string, scope?: FrappeFilters): MemoryNavigator {
|
|
67
|
+
run += 1;
|
|
68
|
+
const navigation = createMemoryNavigator("/backoffice");
|
|
69
|
+
renderWithProviders(
|
|
70
|
+
<BackofficeProvider
|
|
71
|
+
frappe={{
|
|
72
|
+
baseURL: `https://dashboard-view-${run}.spec.test`,
|
|
73
|
+
fixtures: new InMemoryFixtureProvider({
|
|
74
|
+
DocType: [dashboardMeta("QaDash"), dashboardMeta("QaDashPlain", false)],
|
|
75
|
+
QaDash: docs,
|
|
76
|
+
QaDashPlain: docs.map((doc) => ({ ...doc, doctype: "QaDashPlain" })),
|
|
77
|
+
}),
|
|
78
|
+
}}
|
|
79
|
+
navigation={navigation}
|
|
80
|
+
>
|
|
81
|
+
<DeskScopeProvider scope={scope}>
|
|
82
|
+
<DashboardView doctype={doctype} />
|
|
83
|
+
</DeskScopeProvider>
|
|
84
|
+
</BackofficeProvider>,
|
|
85
|
+
);
|
|
86
|
+
return navigation;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function kpi(title: string, value: string) {
|
|
90
|
+
await waitFor(
|
|
91
|
+
() => {
|
|
92
|
+
let node: HTMLElement | null = screen.getAllByText(title)[0] ?? null;
|
|
93
|
+
for (let depth = 0; node && depth < 6; depth += 1) {
|
|
94
|
+
const text = node.textContent ?? "";
|
|
95
|
+
if (text !== title && /\d/.test(text.replace(title, ""))) {
|
|
96
|
+
expect(text.replace(title, "").trim()).toBe(value);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
node = node.parentElement;
|
|
100
|
+
}
|
|
101
|
+
throw new Error(`no value drawn next to ${title}`);
|
|
102
|
+
},
|
|
103
|
+
{ timeout: 5000 },
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
describe("<DashboardView />", () => {
|
|
108
|
+
it("counts the records in each rolling window", async () => {
|
|
109
|
+
renderDashboard("QaDash");
|
|
110
|
+
await kpi("Total QaDash", "5");
|
|
111
|
+
await kpi("Created (last 30 days)", "2");
|
|
112
|
+
await kpi("Modified (last 7 days)", "2");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("draws a card per status option that opens the list filtered to it", async () => {
|
|
116
|
+
const navigation = renderDashboard("QaDash");
|
|
117
|
+
await kpi("Open", "1");
|
|
118
|
+
await kpi("Working", "2");
|
|
119
|
+
await kpi("Done", "2");
|
|
120
|
+
fireEvent.click(screen.getAllByText("Working")[0] as HTMLElement);
|
|
121
|
+
expect(navigation.getLocation().path).toBe("/backoffice/qadash");
|
|
122
|
+
expect(JSON.parse(String(navigation.getLocation().params.f))).toEqual([
|
|
123
|
+
["status", "=", "Working"],
|
|
124
|
+
]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("leaves the status cards and chart out for a doctype with no Select field", async () => {
|
|
128
|
+
renderDashboard("QaDashPlain");
|
|
129
|
+
await kpi("Total QaDashPlain", "5");
|
|
130
|
+
expect(screen.queryByText("Working")).toBeNull();
|
|
131
|
+
expect(screen.queryByText("By Status")).toBeNull();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("ANDs the desk scope into the counts and both charts", async () => {
|
|
135
|
+
renderDashboard("QaDash", [["owner_name", "=", "ana"]]);
|
|
136
|
+
await kpi("Total QaDash", "2");
|
|
137
|
+
await kpi("Created (last 30 days)", "0");
|
|
138
|
+
await kpi("Done", "2");
|
|
139
|
+
await kpi("Working", "0");
|
|
140
|
+
expect(await screen.findByText("No recent activity", {}, { timeout: 5000 })).toBeTruthy();
|
|
141
|
+
await waitFor(() => expect(screen.getAllByText("Done")).toHaveLength(2), { timeout: 5000 });
|
|
142
|
+
expect(screen.getAllByText("Working")).toHaveLength(1);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -81,7 +81,7 @@ export function DashboardView({ doctype }: DashboardViewProps) {
|
|
|
81
81
|
const paths = useBackofficePaths();
|
|
82
82
|
const connection = useViewConnection();
|
|
83
83
|
const { meta, isLoading: isMetaLoading, error: metaError } = useViewMeta(doctype);
|
|
84
|
-
const weekly = useWeeklyCreated(connection, doctype);
|
|
84
|
+
const weekly = useWeeklyCreated(connection, doctype, WEEKS_WINDOW, deskScope);
|
|
85
85
|
// Rolling-window anchors resolve once per mount so the aggregate query
|
|
86
86
|
// identities stay stable across re-renders (no refetch churn).
|
|
87
87
|
const [anchors] = useState(() => ({
|
|
@@ -109,7 +109,7 @@ export function DashboardView({ doctype }: DashboardViewProps) {
|
|
|
109
109
|
value: 0,
|
|
110
110
|
format: "number",
|
|
111
111
|
}}
|
|
112
|
-
dataSource={{ doctype }}
|
|
112
|
+
dataSource={{ doctype, ...(deskScope.length > 0 && { filters: deskScope }) }}
|
|
113
113
|
/>
|
|
114
114
|
</YStack>
|
|
115
115
|
<YStack flexGrow={1} flexBasis={kpiColumnWidth} minWidth={kpiColumnWidth}>
|
|
@@ -220,7 +220,11 @@ export function DashboardView({ doctype }: DashboardViewProps) {
|
|
|
220
220
|
type: "pie",
|
|
221
221
|
height: CHART_HEIGHT,
|
|
222
222
|
}}
|
|
223
|
-
dataSource={{
|
|
223
|
+
dataSource={{
|
|
224
|
+
doctype,
|
|
225
|
+
groupBy: statusField.fieldname,
|
|
226
|
+
...(deskScope.length > 0 && { filters: deskScope }),
|
|
227
|
+
}}
|
|
224
228
|
/>
|
|
225
229
|
</YStack>
|
|
226
230
|
)}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// The image view (MPO-414): a card per record over the doctype's image field,
|
|
2
|
+
// card press opens the form, the desk scope narrows the grid, and a doctype
|
|
3
|
+
// with no image field gets the unsupported state with a way back to the list.
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { InMemoryFixtureProvider } from "@multiplatform.one/frappe";
|
|
6
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
7
|
+
import { fireEvent, screen } from "@testing-library/react";
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
import { BackofficeProvider } from "../config/BackofficeProvider";
|
|
10
|
+
import { DeskScopeProvider } from "../desk/scope";
|
|
11
|
+
import { createMemoryNavigator, type MemoryNavigator } from "../navigation/navigator";
|
|
12
|
+
import type { FrappeFilters } from "../shared/types";
|
|
13
|
+
import { ImageView } from "./ImageView";
|
|
14
|
+
|
|
15
|
+
function pokedexMeta(name: string) {
|
|
16
|
+
return {
|
|
17
|
+
name,
|
|
18
|
+
doctype: "DocType",
|
|
19
|
+
module: "App",
|
|
20
|
+
title_field: "pokemon_name",
|
|
21
|
+
image_field: "sprite_front",
|
|
22
|
+
fields: [
|
|
23
|
+
{ fieldname: "pokemon_name", fieldtype: "Data", label: "Pokemon Name", in_list_view: 1 },
|
|
24
|
+
{ fieldname: "poke_type", fieldtype: "Data", label: "Type", in_list_view: 1 },
|
|
25
|
+
{ fieldname: "sprite_front", fieldtype: "Attach Image", label: "Sprite (Front)" },
|
|
26
|
+
],
|
|
27
|
+
permissions: [{ role: "All", read: 1 }],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const plainMeta = {
|
|
32
|
+
name: "QaImagePlain",
|
|
33
|
+
doctype: "DocType",
|
|
34
|
+
module: "QA",
|
|
35
|
+
fields: [{ fieldname: "subject", fieldtype: "Data", label: "Subject", in_list_view: 1 }],
|
|
36
|
+
permissions: [{ role: "All", read: 1 }],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const sprite = "data:image/gif;base64,R0lGODlhAQABAAAAACw=";
|
|
40
|
+
|
|
41
|
+
const pokemon = [
|
|
42
|
+
["bulbasaur", "grass"],
|
|
43
|
+
["charmander", "fire"],
|
|
44
|
+
["squirtle", "water"],
|
|
45
|
+
].map(([name, type]) => ({
|
|
46
|
+
name,
|
|
47
|
+
doctype: "QaImageDex",
|
|
48
|
+
pokemon_name: name,
|
|
49
|
+
poke_type: type,
|
|
50
|
+
sprite_front: sprite,
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
let run = 0;
|
|
54
|
+
|
|
55
|
+
function renderImageView(doctype: string, scope?: FrappeFilters): MemoryNavigator {
|
|
56
|
+
run += 1;
|
|
57
|
+
const navigation = createMemoryNavigator("/backoffice");
|
|
58
|
+
renderWithProviders(
|
|
59
|
+
<BackofficeProvider
|
|
60
|
+
frappe={{
|
|
61
|
+
baseURL: `https://image-view-${run}.spec.test`,
|
|
62
|
+
fixtures: new InMemoryFixtureProvider({
|
|
63
|
+
DocType: [pokedexMeta("QaImageDex"), pokedexMeta("QaImageEmpty"), plainMeta],
|
|
64
|
+
QaImageDex: pokemon,
|
|
65
|
+
QaImageEmpty: [],
|
|
66
|
+
}),
|
|
67
|
+
}}
|
|
68
|
+
navigation={navigation}
|
|
69
|
+
>
|
|
70
|
+
<DeskScopeProvider scope={scope}>
|
|
71
|
+
<ImageView doctype={doctype} />
|
|
72
|
+
</DeskScopeProvider>
|
|
73
|
+
</BackofficeProvider>,
|
|
74
|
+
);
|
|
75
|
+
return navigation;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
describe("<ImageView />", () => {
|
|
79
|
+
it("draws a titled card per record and opens the form on press", async () => {
|
|
80
|
+
const navigation = renderImageView("QaImageDex");
|
|
81
|
+
expect(
|
|
82
|
+
(await screen.findByTestId("qaimagedex-name-bulbasaur", {}, { timeout: 5000 })).textContent,
|
|
83
|
+
).toBe("bulbasaur");
|
|
84
|
+
expect(screen.getByTestId("qaimagedex-subtitle-charmander").textContent).toBe("fire");
|
|
85
|
+
fireEvent.click(screen.getByTestId("qaimagedex-card-squirtle"));
|
|
86
|
+
expect(navigation.getLocation().path).toBe("/backoffice/qaimagedex/squirtle");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("shows only the records inside the desk scope", async () => {
|
|
90
|
+
renderImageView("QaImageDex", [["poke_type", "=", "fire"]]);
|
|
91
|
+
await screen.findByTestId("qaimagedex-card-charmander", {}, { timeout: 5000 });
|
|
92
|
+
expect(screen.queryByTestId("qaimagedex-card-bulbasaur")).toBeNull();
|
|
93
|
+
expect(screen.queryByTestId("qaimagedex-card-squirtle")).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("says so when an image doctype has no records", async () => {
|
|
97
|
+
renderImageView("QaImageEmpty");
|
|
98
|
+
expect(await screen.findByText("No items found", {}, { timeout: 5000 })).toBeTruthy();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("offers the list back when the doctype has no image field", async () => {
|
|
102
|
+
const navigation = renderImageView("QaImagePlain");
|
|
103
|
+
expect(await screen.findByText("Image view not available", {}, { timeout: 5000 })).toBeTruthy();
|
|
104
|
+
expect(screen.getByText("QaImagePlain has no image field.")).toBeTruthy();
|
|
105
|
+
fireEvent.click(screen.getByText("Back to list"));
|
|
106
|
+
expect(navigation.getLocation().path).toBe("/backoffice/qaimageplain");
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
// The kanban view and its board controls (MPO-414): a saved Kanban Board
|
|
2
|
+
// drives the columns, the picker switches and creates boards, a doctype with
|
|
3
|
+
// no board falls back to its Select field, the column "+" prefills the new
|
|
4
|
+
// record, and cards move or get added only as the session's rights allow
|
|
5
|
+
// (MPO-390).
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { InMemoryFixtureProvider } from "@multiplatform.one/frappe";
|
|
8
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
9
|
+
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
|
|
10
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
11
|
+
import { BackofficeProvider } from "../config/BackofficeProvider";
|
|
12
|
+
import { createMemoryNavigator } from "../navigation/navigator";
|
|
13
|
+
import { KanbanView } from "./KanbanView";
|
|
14
|
+
|
|
15
|
+
// happy-dom has no layout, so the column virtualizer would draw no cards.
|
|
16
|
+
vi.mock("@tanstack/react-virtual", () => ({
|
|
17
|
+
useVirtualizer: (opts: { count: number; estimateSize: () => number }) => {
|
|
18
|
+
const size = opts.estimateSize();
|
|
19
|
+
const items = Array.from({ length: opts.count }, (_, index) => ({
|
|
20
|
+
index,
|
|
21
|
+
key: String(index),
|
|
22
|
+
start: index * size,
|
|
23
|
+
end: (index + 1) * size,
|
|
24
|
+
size,
|
|
25
|
+
}));
|
|
26
|
+
return {
|
|
27
|
+
getVirtualItems: () => items,
|
|
28
|
+
getTotalSize: () => opts.count * size,
|
|
29
|
+
measureElement: () => {},
|
|
30
|
+
scrollToIndex: () => {},
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
function taskMeta(name: string, withSelect = true) {
|
|
36
|
+
return {
|
|
37
|
+
name,
|
|
38
|
+
doctype: "DocType",
|
|
39
|
+
module: "QA",
|
|
40
|
+
title_field: "subject",
|
|
41
|
+
fields: [
|
|
42
|
+
{ fieldname: "subject", fieldtype: "Data", label: "Subject", in_list_view: 1 },
|
|
43
|
+
...(withSelect
|
|
44
|
+
? [
|
|
45
|
+
{
|
|
46
|
+
fieldname: "status",
|
|
47
|
+
fieldtype: "Select",
|
|
48
|
+
label: "Status",
|
|
49
|
+
options: "Open\nWorking\nDone",
|
|
50
|
+
in_list_view: 1,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
fieldname: "priority",
|
|
54
|
+
fieldtype: "Select",
|
|
55
|
+
label: "Priority",
|
|
56
|
+
options: "High\nMedium\nLow",
|
|
57
|
+
in_list_view: 1,
|
|
58
|
+
},
|
|
59
|
+
]
|
|
60
|
+
: []),
|
|
61
|
+
],
|
|
62
|
+
permissions: [{ role: "All", read: 1 }],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const tasks = [
|
|
67
|
+
["QABT-001", "triage inbox", "Open", "High"],
|
|
68
|
+
["QABT-002", "ship release", "Working", "Medium"],
|
|
69
|
+
["QABT-003", "write docs", "Done", "Low"],
|
|
70
|
+
].map(([name, subject, status, priority]) => ({
|
|
71
|
+
name,
|
|
72
|
+
doctype: "QaBoardTask",
|
|
73
|
+
subject,
|
|
74
|
+
status,
|
|
75
|
+
priority,
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
const savedBoards = [
|
|
79
|
+
{
|
|
80
|
+
name: "QA Task Board",
|
|
81
|
+
doctype: "Kanban Board",
|
|
82
|
+
kanban_board_name: "QA Task Board",
|
|
83
|
+
reference_doctype: "QaBoardTask",
|
|
84
|
+
field_name: "status",
|
|
85
|
+
columns: [
|
|
86
|
+
{ column_name: "Done", status: "Active", indicator: "Green" },
|
|
87
|
+
{ column_name: "Open", status: "Active", indicator: "Blue" },
|
|
88
|
+
{ column_name: "Working", status: "Active", indicator: "Orange" },
|
|
89
|
+
{ column_name: "Retired", status: "Archived", indicator: "Gray" },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "Priority Lane",
|
|
94
|
+
doctype: "Kanban Board",
|
|
95
|
+
kanban_board_name: "Priority Lane",
|
|
96
|
+
reference_doctype: "QaBoardTask",
|
|
97
|
+
field_name: "priority",
|
|
98
|
+
columns: [
|
|
99
|
+
{ column_name: "High", status: "Active", indicator: "Red" },
|
|
100
|
+
{ column_name: "Medium", status: "Active", indicator: "Orange" },
|
|
101
|
+
{ column_name: "Low", status: "Active", indicator: "Gray" },
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
const moveMeta = {
|
|
107
|
+
name: "Poke Move",
|
|
108
|
+
doctype: "DocType",
|
|
109
|
+
title_field: "move_name",
|
|
110
|
+
fields: [
|
|
111
|
+
{ fieldname: "move_name", fieldtype: "Data", label: "Move Name", in_list_view: 1 },
|
|
112
|
+
{
|
|
113
|
+
fieldname: "category",
|
|
114
|
+
fieldtype: "Select",
|
|
115
|
+
label: "Category",
|
|
116
|
+
options: "Physical\nSpecial",
|
|
117
|
+
in_list_view: 1,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
permissions: [
|
|
121
|
+
{ role: "Pokemon Editor", read: 1, write: 1, create: 1, delete: 1 },
|
|
122
|
+
{ role: "Guest", read: 1 },
|
|
123
|
+
],
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
let run = 0;
|
|
127
|
+
|
|
128
|
+
afterEach(() => {
|
|
129
|
+
vi.restoreAllMocks();
|
|
130
|
+
vi.unstubAllGlobals();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
function renderFixtureKanban(doctype: string, board?: string) {
|
|
134
|
+
run += 1;
|
|
135
|
+
const fixtures = new InMemoryFixtureProvider({
|
|
136
|
+
DocType: [taskMeta("QaBoardTask"), taskMeta("QaBoardLoose"), taskMeta("QaBoardNone", false)],
|
|
137
|
+
"Kanban Board": savedBoards.map((entry) => ({ ...entry })),
|
|
138
|
+
QaBoardTask: tasks,
|
|
139
|
+
QaBoardLoose: tasks.map((task) => ({ ...task, doctype: "QaBoardLoose" })),
|
|
140
|
+
});
|
|
141
|
+
const navigation = createMemoryNavigator("/backoffice");
|
|
142
|
+
renderWithProviders(
|
|
143
|
+
<BackofficeProvider
|
|
144
|
+
frappe={{ baseURL: `https://kanban-view-${run}.spec.test`, fixtures }}
|
|
145
|
+
navigation={navigation}
|
|
146
|
+
>
|
|
147
|
+
<KanbanView doctype={doctype} board={board} />
|
|
148
|
+
</BackofficeProvider>,
|
|
149
|
+
);
|
|
150
|
+
return { navigation, fixtures };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function json(body: unknown) {
|
|
154
|
+
return new Response(JSON.stringify(body), {
|
|
155
|
+
status: 200,
|
|
156
|
+
headers: { "content-type": "application/json" },
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function renderBenchKanban(roles: string[]) {
|
|
161
|
+
run += 1;
|
|
162
|
+
let rolesAnswered = false;
|
|
163
|
+
vi.stubGlobal("fetch", async (input: string | URL | Request) => {
|
|
164
|
+
const url = new URL(String(input instanceof Request ? input.url : input));
|
|
165
|
+
const method = url.pathname.replace(/^\/api\/method\//, "");
|
|
166
|
+
if (method === "frappe.auth.get_logged_user") return json({ message: "user@pokemon.test" });
|
|
167
|
+
if (method === "frappe.core.doctype.user.user.get_roles") {
|
|
168
|
+
rolesAnswered = true;
|
|
169
|
+
return json({ message: roles });
|
|
170
|
+
}
|
|
171
|
+
if (method === "frappe.desk.form.load.getdoctype") return json({ docs: [moveMeta] });
|
|
172
|
+
if (url.pathname === `/api/resource/${encodeURIComponent("Poke Move")}`) {
|
|
173
|
+
return json({
|
|
174
|
+
data: [{ name: "m1", doctype: "Poke Move", move_name: "tackle", category: "Physical" }],
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return json({ data: [], message: [] });
|
|
178
|
+
});
|
|
179
|
+
renderWithProviders(
|
|
180
|
+
<BackofficeProvider
|
|
181
|
+
frappe={{ baseURL: `https://kanban-view-bench-${run}.spec.test` }}
|
|
182
|
+
navigation={createMemoryNavigator("/backoffice")}
|
|
183
|
+
>
|
|
184
|
+
<KanbanView doctype="Poke Move" />
|
|
185
|
+
</BackofficeProvider>,
|
|
186
|
+
);
|
|
187
|
+
return { rolesAnswered: () => rolesAnswered };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function draggable(title: string): boolean {
|
|
191
|
+
return Boolean(screen.getByText(title).closest('[data-kanban-draggable="true"]'));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function settle(ms = 300) {
|
|
195
|
+
await act(async () => {
|
|
196
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function precedes(first: HTMLElement, second: HTMLElement): boolean {
|
|
201
|
+
return Boolean(first.compareDocumentPosition(second) & Node.DOCUMENT_POSITION_FOLLOWING);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
describe("<KanbanView />", () => {
|
|
205
|
+
it("draws the saved board's active columns in its order and opens a card's form", async () => {
|
|
206
|
+
const { navigation } = renderFixtureKanban("QaBoardTask");
|
|
207
|
+
const card = await screen.findByText("ship release", {}, { timeout: 5000 });
|
|
208
|
+
const done = screen.getByLabelText("Add card to Done");
|
|
209
|
+
const open = screen.getByLabelText("Add card to Open");
|
|
210
|
+
const working = screen.getByLabelText("Add card to Working");
|
|
211
|
+
expect(precedes(done, open) && precedes(open, working)).toBe(true);
|
|
212
|
+
expect(screen.queryByLabelText("Add card to Retired")).toBeNull();
|
|
213
|
+
fireEvent.click(card);
|
|
214
|
+
await waitFor(() =>
|
|
215
|
+
expect(navigation.getLocation().path).toBe("/backoffice/qaboardtask/QABT-002"),
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("opens a new record prefilled with the column from its +", async () => {
|
|
220
|
+
const { navigation } = renderFixtureKanban("QaBoardTask");
|
|
221
|
+
fireEvent.click(await screen.findByLabelText("Add card to Working", {}, { timeout: 5000 }));
|
|
222
|
+
expect(navigation.getLocation().path).toBe("/backoffice/qaboardtask/new");
|
|
223
|
+
expect(JSON.parse(String(navigation.getLocation().params.d))).toEqual({ status: "Working" });
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("switches to another saved board from the picker", async () => {
|
|
227
|
+
const { navigation } = renderFixtureKanban("QaBoardTask");
|
|
228
|
+
fireEvent.click(await screen.findByLabelText("Switch board", {}, { timeout: 5000 }));
|
|
229
|
+
fireEvent.click(await screen.findByText("Priority Lane", {}, { timeout: 5000 }));
|
|
230
|
+
await waitFor(() => expect(navigation.getLocation().params.board).toBe("Priority Lane"));
|
|
231
|
+
expect(navigation.getLocation().path).toBe("/backoffice/qaboardtask/view/kanban");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("creates a board from the picker's New board dialog and switches to it", async () => {
|
|
235
|
+
const { navigation, fixtures } = renderFixtureKanban("QaBoardTask");
|
|
236
|
+
fireEvent.click(await screen.findByLabelText("Switch board", {}, { timeout: 5000 }));
|
|
237
|
+
fireEvent.click(await screen.findByText("New board…", {}, { timeout: 5000 }));
|
|
238
|
+
expect(await screen.findByText("New kanban board", {}, { timeout: 5000 })).toBeTruthy();
|
|
239
|
+
fireEvent.change(screen.getByRole("textbox", { name: /board name/i }), {
|
|
240
|
+
target: { value: "Release Train" },
|
|
241
|
+
});
|
|
242
|
+
fireEvent.click(screen.getByRole("button", { name: "Create board" }));
|
|
243
|
+
await waitFor(() => expect(navigation.getLocation().params.board).toBe("Release Train"));
|
|
244
|
+
const created = await fixtures.getDoc("Kanban Board", "Release Train");
|
|
245
|
+
expect(created).toMatchObject({ reference_doctype: "QaBoardTask", field_name: "status" });
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("falls back to the status Select with a create-board call to action", async () => {
|
|
249
|
+
renderFixtureKanban("QaBoardLoose");
|
|
250
|
+
expect(
|
|
251
|
+
await screen.findByText("No kanban board configured", {}, { timeout: 5000 }),
|
|
252
|
+
).toBeTruthy();
|
|
253
|
+
expect(screen.getByRole("button", { name: "Create board" })).toBeTruthy();
|
|
254
|
+
expect(await screen.findByLabelText("Add card to Open", {}, { timeout: 5000 })).toBeTruthy();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("offers the list back when there is no board and no Select field", async () => {
|
|
258
|
+
const { navigation } = renderFixtureKanban("QaBoardNone");
|
|
259
|
+
expect(
|
|
260
|
+
await screen.findByText("No Select field to board by", {}, { timeout: 5000 }),
|
|
261
|
+
).toBeTruthy();
|
|
262
|
+
fireEvent.click(screen.getByText("Back to list"));
|
|
263
|
+
expect(navigation.getLocation().path).toBe("/backoffice/qaboardnone");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("lets an editor move and add cards on a live bench", async () => {
|
|
267
|
+
const bench = renderBenchKanban(["Pokemon Editor", "Guest"]);
|
|
268
|
+
await screen.findByText("tackle", {}, { timeout: 5000 });
|
|
269
|
+
await waitFor(() => expect(bench.rolesAnswered()).toBe(true), { timeout: 5000 });
|
|
270
|
+
await waitFor(() => expect(draggable("tackle")).toBe(true));
|
|
271
|
+
expect(screen.getByLabelText("Add card to Physical")).toBeTruthy();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("lets a reader neither move nor add cards on a live bench", async () => {
|
|
275
|
+
const bench = renderBenchKanban(["Guest"]);
|
|
276
|
+
await screen.findByText("tackle", {}, { timeout: 5000 });
|
|
277
|
+
await settle();
|
|
278
|
+
expect(draggable("tackle")).toBe(false);
|
|
279
|
+
expect(screen.queryByLabelText("Add card to Physical")).toBeNull();
|
|
280
|
+
expect(bench.rolesAnswered()).toBe(true);
|
|
281
|
+
});
|
|
282
|
+
});
|
package/src/views/KanbanView.tsx
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
import { Alert, Button, Text, XStack, YStack, notify } from "@multiplatform.one/components";
|
|
17
17
|
import {
|
|
18
18
|
FrappeKanban,
|
|
19
|
+
useDocRights,
|
|
20
|
+
useFrappeConfig,
|
|
19
21
|
type FrappeKanbanProps,
|
|
20
22
|
type KanbanColumn,
|
|
21
23
|
} from "@multiplatform.one/frappe-ui";
|
|
@@ -117,6 +119,7 @@ export function KanbanView({
|
|
|
117
119
|
const connection = useViewConnection();
|
|
118
120
|
const { meta, isLoading: isMetaLoading, error: metaError } = useViewMeta(doctype);
|
|
119
121
|
const boards = useKanbanBoard(connection, doctype, boardOverride);
|
|
122
|
+
const { rights } = useDocRights(useFrappeConfig(), meta);
|
|
120
123
|
// Session-scoped grouping-axis override from the picker, keyed by doctype
|
|
121
124
|
// so navigating to another doctype never inherits a stale field.
|
|
122
125
|
const [axisChoice, setAxisChoice] = useState<{ doctype: string; fieldname: string } | null>(null);
|
|
@@ -385,13 +388,14 @@ export function KanbanView({
|
|
|
385
388
|
if (onCardOpen) onCardOpen(doctype, card.name);
|
|
386
389
|
else navigator.navigate(paths.form(doctype, card.name));
|
|
387
390
|
}}
|
|
388
|
-
readOnly={readOnly}
|
|
391
|
+
readOnly={readOnly || !rights.write}
|
|
389
392
|
// Desk parity: the per-column "+" (anchored in the column header by
|
|
390
393
|
// FrappeKanban) carries the clicked column into the create form —
|
|
391
394
|
// the active grouping axis is prefilled via the `?d=` param on the
|
|
392
395
|
// /new route (consumed by NewDocRouteScreen → createPrefill). The
|
|
393
|
-
// newDoc capability
|
|
394
|
-
|
|
396
|
+
// newDoc capability and the session's create right gate the
|
|
397
|
+
// affordance.
|
|
398
|
+
enableCardCreation={newDoc.enabled && rights.create}
|
|
395
399
|
onCreateCard={(columnValue) =>
|
|
396
400
|
navigator.navigate(paths.newDoc(doctype, { [statusField]: columnValue }))
|
|
397
401
|
}
|