@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,143 @@
|
|
|
1
|
+
// Desk quick entry (MPO-414): required fields validate inline, Create inserts
|
|
2
|
+
// and hands back the server name, and doctypes that do not quick-enter route
|
|
3
|
+
// to the full form without drawing the dialog.
|
|
4
|
+
import { InMemoryFixtureProvider } from "@multiplatform.one/frappe";
|
|
5
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
6
|
+
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
|
7
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
8
|
+
import { BackofficeProvider, type BackofficeFrappeConfig } from "../config/BackofficeProvider";
|
|
9
|
+
import { createMemoryNavigator } from "../navigation/navigator";
|
|
10
|
+
import type { DoctypeMeta } from "../shared/useDoctypeMeta";
|
|
11
|
+
import { QuickEntryDialog } from "./QuickEntryDialog";
|
|
12
|
+
|
|
13
|
+
const todoMeta: DoctypeMeta = {
|
|
14
|
+
name: "ToDo",
|
|
15
|
+
module: "Desk",
|
|
16
|
+
quick_entry: 1,
|
|
17
|
+
autoname: "hash",
|
|
18
|
+
fields: [
|
|
19
|
+
{ fieldname: "description", fieldtype: "Small Text", label: "Description", reqd: 1 },
|
|
20
|
+
{
|
|
21
|
+
fieldname: "priority",
|
|
22
|
+
fieldtype: "Select",
|
|
23
|
+
label: "Priority",
|
|
24
|
+
options: "High\nMedium\nLow",
|
|
25
|
+
bold: 1,
|
|
26
|
+
default: "Medium",
|
|
27
|
+
},
|
|
28
|
+
{ fieldname: "status", fieldtype: "Select", label: "Status", options: "Open\nClosed" },
|
|
29
|
+
],
|
|
30
|
+
permissions: [{ role: "All", read: 1, write: 1, create: 1 }],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const invoiceMeta: DoctypeMeta = {
|
|
34
|
+
name: "Sales Invoice",
|
|
35
|
+
module: "Accounts",
|
|
36
|
+
quick_entry: 0,
|
|
37
|
+
fields: [{ fieldname: "customer", fieldtype: "Data", label: "Customer", reqd: 1 }],
|
|
38
|
+
permissions: [{ role: "All", read: 1, write: 1, create: 1 }],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
let host = 0;
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
vi.restoreAllMocks();
|
|
45
|
+
vi.unstubAllGlobals();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
function renderDialog(meta: DoctypeMeta, frappe: Partial<BackofficeFrappeConfig> = {}) {
|
|
49
|
+
host += 1;
|
|
50
|
+
const onOpenChange = vi.fn();
|
|
51
|
+
const onCreated = vi.fn();
|
|
52
|
+
const onEditFullForm = vi.fn();
|
|
53
|
+
const fixtures = new InMemoryFixtureProvider({ [meta.name]: [] });
|
|
54
|
+
renderWithProviders(
|
|
55
|
+
<BackofficeProvider
|
|
56
|
+
frappe={{ baseURL: `https://quick-entry-${host}.screen.spec.test`, fixtures, ...frappe }}
|
|
57
|
+
navigation={createMemoryNavigator("/backoffice")}
|
|
58
|
+
>
|
|
59
|
+
<QuickEntryDialog
|
|
60
|
+
doctype={meta.name}
|
|
61
|
+
open
|
|
62
|
+
onOpenChange={onOpenChange}
|
|
63
|
+
onCreated={onCreated}
|
|
64
|
+
onEditFullForm={onEditFullForm}
|
|
65
|
+
metaOverride={meta}
|
|
66
|
+
/>
|
|
67
|
+
</BackofficeProvider>,
|
|
68
|
+
);
|
|
69
|
+
return { fixtures, onOpenChange, onCreated, onEditFullForm };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function descriptionInput(): HTMLTextAreaElement | HTMLInputElement {
|
|
73
|
+
const field = screen.getByLabelText("Description", { exact: false });
|
|
74
|
+
return (
|
|
75
|
+
field instanceof HTMLTextAreaElement || field instanceof HTMLInputElement
|
|
76
|
+
? field
|
|
77
|
+
: field.querySelector("textarea, input")
|
|
78
|
+
) as HTMLTextAreaElement;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe("QuickEntryDialog", () => {
|
|
82
|
+
it("holds Create while a required field is empty", async () => {
|
|
83
|
+
const { onCreated, onOpenChange } = renderDialog(todoMeta);
|
|
84
|
+
fireEvent.click(await screen.findByTestId("quick-entry-create"));
|
|
85
|
+
expect(await screen.findByText("Description is required")).toBeTruthy();
|
|
86
|
+
expect(onCreated).not.toHaveBeenCalled();
|
|
87
|
+
expect(onOpenChange).not.toHaveBeenCalledWith(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("inserts the draft and hands back the created name", async () => {
|
|
91
|
+
const { fixtures, onCreated, onOpenChange } = renderDialog(todoMeta);
|
|
92
|
+
await screen.findByTestId("quick-entry-form");
|
|
93
|
+
fireEvent.change(descriptionInput(), { target: { value: "Call the vendor" } });
|
|
94
|
+
fireEvent.click(screen.getByTestId("quick-entry-create"));
|
|
95
|
+
await waitFor(() => expect(onCreated).toHaveBeenCalledTimes(1));
|
|
96
|
+
const created = await fixtures.getList("ToDo");
|
|
97
|
+
expect(created).toHaveLength(1);
|
|
98
|
+
expect(created[0]?.description).toBe("Call the vendor");
|
|
99
|
+
expect(onCreated).toHaveBeenCalledWith(created[0]?.name);
|
|
100
|
+
expect(onOpenChange).toHaveBeenLastCalledWith(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("shows a server rejection inline and stays open", async () => {
|
|
104
|
+
vi.stubGlobal("fetch", async (input: string | URL | Request) => {
|
|
105
|
+
const url = new URL(String(input instanceof Request ? input.url : input));
|
|
106
|
+
if (url.pathname.endsWith(".create_doc")) {
|
|
107
|
+
return new Response(
|
|
108
|
+
JSON.stringify({
|
|
109
|
+
exc_type: "ValidationError",
|
|
110
|
+
exception: "frappe.exceptions.ValidationError: Vendor calls are closed today",
|
|
111
|
+
}),
|
|
112
|
+
{ status: 417, headers: { "content-type": "application/json" } },
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
return new Response(JSON.stringify({ data: [] }), {
|
|
116
|
+
headers: { "content-type": "application/json" },
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
const { onCreated, onOpenChange } = renderDialog(todoMeta, { fixtures: undefined });
|
|
120
|
+
await screen.findByTestId("quick-entry-form");
|
|
121
|
+
fireEvent.change(descriptionInput(), { target: { value: "Call the vendor" } });
|
|
122
|
+
fireEvent.click(screen.getByTestId("quick-entry-create"));
|
|
123
|
+
const error = await screen.findByTestId("quick-entry-error", {}, { timeout: 5000 });
|
|
124
|
+
expect(error.textContent).toBe("Vendor calls are closed today");
|
|
125
|
+
expect(onCreated).not.toHaveBeenCalled();
|
|
126
|
+
expect(onOpenChange).not.toHaveBeenCalledWith(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("routes a doctype that does not quick-enter to the full form", async () => {
|
|
130
|
+
const { onEditFullForm, onOpenChange } = renderDialog(invoiceMeta);
|
|
131
|
+
await waitFor(() => expect(onEditFullForm).toHaveBeenCalledTimes(1));
|
|
132
|
+
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
133
|
+
expect(screen.queryByTestId("quick-entry-form")).toBeNull();
|
|
134
|
+
expect(screen.queryByText("New Sales Invoice")).toBeNull();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("escapes to the full form from the footer", async () => {
|
|
138
|
+
const { onEditFullForm, onOpenChange } = renderDialog(todoMeta);
|
|
139
|
+
fireEvent.click(await screen.findByTestId("quick-entry-edit-full-form"));
|
|
140
|
+
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
141
|
+
expect(onEditFullForm).toHaveBeenCalledTimes(1);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// The desk report view (MPO-414): rows from the live collection, a row press
|
|
2
|
+
// opens the form, the per-column filter row and the ?f= base filters narrow
|
|
3
|
+
// the result, the desk scope is ANDed in, and Save As round-trips ?report=.
|
|
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 { DoctypeReportView } from "./DoctypeReportView";
|
|
14
|
+
|
|
15
|
+
const orderMeta = {
|
|
16
|
+
name: "QaReportOrder",
|
|
17
|
+
doctype: "DocType",
|
|
18
|
+
module: "QA",
|
|
19
|
+
title_field: "customer",
|
|
20
|
+
sort_field: "modified",
|
|
21
|
+
sort_order: "DESC",
|
|
22
|
+
fields: [
|
|
23
|
+
{ fieldname: "customer", fieldtype: "Data", label: "Customer", in_list_view: 1 },
|
|
24
|
+
{ fieldname: "amount", fieldtype: "Currency", label: "Amount", in_list_view: 1 },
|
|
25
|
+
{
|
|
26
|
+
fieldname: "region",
|
|
27
|
+
fieldtype: "Select",
|
|
28
|
+
label: "Region",
|
|
29
|
+
options: "North\nSouth",
|
|
30
|
+
in_standard_filter: 1,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
fieldname: "status",
|
|
34
|
+
fieldtype: "Select",
|
|
35
|
+
label: "Status",
|
|
36
|
+
options: "Draft\nPaid",
|
|
37
|
+
in_list_view: 1,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
permissions: [{ role: "All", read: 1 }],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const orders = [
|
|
44
|
+
["QA-ORD-0001", "ACME Corp", "North", "Paid", 1],
|
|
45
|
+
["QA-ORD-0002", "Globex", "South", "Draft", 2],
|
|
46
|
+
["QA-ORD-0003", "Initech", "North", "Draft", 3],
|
|
47
|
+
].map(([name, customer, region, status, day]) => ({
|
|
48
|
+
name: String(name),
|
|
49
|
+
doctype: "QaReportOrder",
|
|
50
|
+
customer,
|
|
51
|
+
amount: 1000,
|
|
52
|
+
region,
|
|
53
|
+
status,
|
|
54
|
+
modified: `2026-08-0${day} 09:00:00`,
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
let run = 0;
|
|
58
|
+
|
|
59
|
+
function renderReport(
|
|
60
|
+
props: { filtersParam?: string; scope?: FrappeFilters; doctype?: string } = {},
|
|
61
|
+
): MemoryNavigator {
|
|
62
|
+
run += 1;
|
|
63
|
+
const navigation = createMemoryNavigator("/backoffice/qareportorder/view/report");
|
|
64
|
+
renderWithProviders(
|
|
65
|
+
<BackofficeProvider
|
|
66
|
+
frappe={{
|
|
67
|
+
baseURL: `https://doctype-report-${run}.spec.test`,
|
|
68
|
+
fixtures: new InMemoryFixtureProvider({
|
|
69
|
+
DocType: [orderMeta, { ...orderMeta, name: "QaReportEmpty" }],
|
|
70
|
+
QaReportOrder: orders,
|
|
71
|
+
QaReportEmpty: [],
|
|
72
|
+
}),
|
|
73
|
+
}}
|
|
74
|
+
navigation={navigation}
|
|
75
|
+
>
|
|
76
|
+
<DeskScopeProvider scope={props.scope}>
|
|
77
|
+
<DoctypeReportView
|
|
78
|
+
doctype={props.doctype ?? "QaReportOrder"}
|
|
79
|
+
filtersParam={props.filtersParam}
|
|
80
|
+
/>
|
|
81
|
+
</DeskScopeProvider>
|
|
82
|
+
</BackofficeProvider>,
|
|
83
|
+
);
|
|
84
|
+
return navigation;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function rowNames(): string[] {
|
|
88
|
+
return Array.from(
|
|
89
|
+
document.querySelectorAll('[data-report-row="true"]'),
|
|
90
|
+
(row) => row.getAttribute("aria-label") ?? "",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function rowsAre(names: string[]) {
|
|
95
|
+
await waitFor(() => expect(rowNames().sort()).toEqual([...names].sort()), { timeout: 5000 });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
describe("<DoctypeReportView />", () => {
|
|
99
|
+
it("draws a row per record and opens the form on press", async () => {
|
|
100
|
+
const navigation = renderReport();
|
|
101
|
+
await rowsAre(["QA-ORD-0001", "QA-ORD-0002", "QA-ORD-0003"]);
|
|
102
|
+
fireEvent.click(screen.getByText("Globex"));
|
|
103
|
+
expect(navigation.getLocation().path).toBe("/backoffice/qareportorder/QA-ORD-0002");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("narrows the rows from the column filter row and clears it again", async () => {
|
|
107
|
+
renderReport();
|
|
108
|
+
await rowsAre(["QA-ORD-0001", "QA-ORD-0002", "QA-ORD-0003"]);
|
|
109
|
+
fireEvent.change(await screen.findByRole("textbox", { name: "Filter Region" }), {
|
|
110
|
+
target: { value: "North" },
|
|
111
|
+
});
|
|
112
|
+
await rowsAre(["QA-ORD-0001", "QA-ORD-0003"]);
|
|
113
|
+
fireEvent.click(screen.getByRole("button", { name: /Clear column filters \(1\)/ }));
|
|
114
|
+
await rowsAre(["QA-ORD-0001", "QA-ORD-0002", "QA-ORD-0003"]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("applies the ?f= base filters and drops them from the address on clear", async () => {
|
|
118
|
+
const navigation = renderReport({
|
|
119
|
+
filtersParam: encodeURIComponent(JSON.stringify([["status", "=", "Draft"]])),
|
|
120
|
+
});
|
|
121
|
+
await rowsAre(["QA-ORD-0002", "QA-ORD-0003"]);
|
|
122
|
+
fireEvent.click(screen.getByRole("button", { name: "Clear filters" }));
|
|
123
|
+
await rowsAre(["QA-ORD-0001", "QA-ORD-0002", "QA-ORD-0003"]);
|
|
124
|
+
expect(navigation.getLocation().path).toBe("/backoffice/qareportorder/view/report");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("ANDs the desk scope in, with nothing to clear", async () => {
|
|
128
|
+
renderReport({ scope: [["region", "=", "South"]] });
|
|
129
|
+
await rowsAre(["QA-ORD-0002"]);
|
|
130
|
+
expect(screen.queryByRole("button", { name: "Clear filters" })).toBeNull();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("says so when the doctype has no records", async () => {
|
|
134
|
+
renderReport({ doctype: "QaReportEmpty" });
|
|
135
|
+
expect(await screen.findByText("No results", {}, { timeout: 5000 })).toBeTruthy();
|
|
136
|
+
expect(screen.getByText("There are no QaReportEmpty records yet")).toBeTruthy();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("saves the current report under a name and selects it", async () => {
|
|
140
|
+
const navigation = renderReport();
|
|
141
|
+
await rowsAre(["QA-ORD-0001", "QA-ORD-0002", "QA-ORD-0003"]);
|
|
142
|
+
fireEvent.click(screen.getByRole("button", { name: "Save As…" }));
|
|
143
|
+
fireEvent.change(await screen.findByRole("textbox", { name: "New report name" }), {
|
|
144
|
+
target: { value: "Weekly Northern" },
|
|
145
|
+
});
|
|
146
|
+
fireEvent.click(screen.getByRole("button", { name: "Save" }));
|
|
147
|
+
await waitFor(() => expect(navigation.getLocation().params.report).toBe("Weekly Northern"), {
|
|
148
|
+
timeout: 5000,
|
|
149
|
+
});
|
|
150
|
+
expect(
|
|
151
|
+
await screen.findByRole("button", { name: "Report: Weekly Northern" }, { timeout: 5000 }),
|
|
152
|
+
).toBeTruthy();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -167,15 +167,16 @@ function WorkspaceNav({ sections }: { sections: SidebarSection[] }) {
|
|
|
167
167
|
{sections.map((section, sectionIdx) => (
|
|
168
168
|
<YStack key={section.title ?? sectionIdx} {...knobProps.gap}>
|
|
169
169
|
{section.title ? (
|
|
170
|
-
<
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
170
|
+
<YStack paddingHorizontal={rowPad}>
|
|
171
|
+
<SizableText
|
|
172
|
+
size="$2"
|
|
173
|
+
fontWeight="400"
|
|
174
|
+
textTransform="uppercase"
|
|
175
|
+
color="$color11"
|
|
176
|
+
>
|
|
177
|
+
{section.title}
|
|
178
|
+
</SizableText>
|
|
179
|
+
</YStack>
|
|
179
180
|
) : null}
|
|
180
181
|
<YStack>
|
|
181
182
|
{section.items.map((item, itemIdx) => {
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// The calendar view (MPO-414): a Calendar View settings doc picks the date
|
|
2
|
+
// axis, an event opens its form, the selector writes ?field=, and a day click
|
|
3
|
+
// starts a record prefilled with that day only for a session that may create
|
|
4
|
+
// (MPO-390).
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { InMemoryFixtureProvider } from "@multiplatform.one/frappe";
|
|
7
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
8
|
+
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
|
|
9
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
10
|
+
import { BackofficeProvider } from "../config/BackofficeProvider";
|
|
11
|
+
import { createMemoryNavigator, type MemoryNavigator } from "../navigation/navigator";
|
|
12
|
+
import { CalendarView } from "./CalendarView";
|
|
13
|
+
|
|
14
|
+
const eventMeta = {
|
|
15
|
+
name: "QaCalEvent",
|
|
16
|
+
doctype: "DocType",
|
|
17
|
+
module: "QA",
|
|
18
|
+
fields: [
|
|
19
|
+
{ fieldname: "subject", fieldtype: "Data", label: "Subject", in_list_view: 1 },
|
|
20
|
+
{ fieldname: "created_on", fieldtype: "Date", label: "Created On" },
|
|
21
|
+
{ fieldname: "scheduled_at", fieldtype: "Datetime", label: "Scheduled At" },
|
|
22
|
+
{ fieldname: "wrap_up_at", fieldtype: "Datetime", label: "Wrap Up At" },
|
|
23
|
+
],
|
|
24
|
+
permissions: [{ role: "All", read: 1 }],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const multiMeta = {
|
|
28
|
+
name: "QaCalMulti",
|
|
29
|
+
doctype: "DocType",
|
|
30
|
+
module: "QA",
|
|
31
|
+
title_field: "title",
|
|
32
|
+
fields: [
|
|
33
|
+
{ fieldname: "title", fieldtype: "Data", label: "Title", in_list_view: 1 },
|
|
34
|
+
{ fieldname: "due_date", fieldtype: "Date", label: "Due Date" },
|
|
35
|
+
{ fieldname: "follow_up_on", fieldtype: "Date", label: "Follow Up On" },
|
|
36
|
+
],
|
|
37
|
+
permissions: [{ role: "All", read: 1 }],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const noDateMeta = {
|
|
41
|
+
name: "QaCalNone",
|
|
42
|
+
doctype: "DocType",
|
|
43
|
+
module: "QA",
|
|
44
|
+
fields: [{ fieldname: "subject", fieldtype: "Data", label: "Subject", in_list_view: 1 }],
|
|
45
|
+
permissions: [{ role: "All", read: 1 }],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const moveMeta = {
|
|
49
|
+
name: "Poke Move",
|
|
50
|
+
doctype: "DocType",
|
|
51
|
+
title_field: "move_name",
|
|
52
|
+
fields: [
|
|
53
|
+
{ fieldname: "move_name", fieldtype: "Data", label: "Move Name", in_list_view: 1 },
|
|
54
|
+
{ fieldname: "learned_on", fieldtype: "Date", label: "Learned On" },
|
|
55
|
+
],
|
|
56
|
+
permissions: [
|
|
57
|
+
{ role: "Pokemon Editor", read: 1, write: 1, create: 1, delete: 1 },
|
|
58
|
+
{ role: "Guest", read: 1 },
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function dayKey(date: Date): string {
|
|
63
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
64
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
65
|
+
return `${date.getFullYear()}-${month}-${day}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const today = dayKey(new Date());
|
|
69
|
+
|
|
70
|
+
const events = [
|
|
71
|
+
{
|
|
72
|
+
name: "QCE-001",
|
|
73
|
+
doctype: "QaCalEvent",
|
|
74
|
+
subject: "Team sync",
|
|
75
|
+
created_on: "2020-01-01",
|
|
76
|
+
scheduled_at: `${today} 09:00:00`,
|
|
77
|
+
wrap_up_at: `${today} 10:00:00`,
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
const calendarViewDoc = {
|
|
82
|
+
name: "QaCalEvent Calendar",
|
|
83
|
+
doctype: "Calendar View",
|
|
84
|
+
reference_doctype: "QaCalEvent",
|
|
85
|
+
subject_field: "subject",
|
|
86
|
+
start_date_field: "scheduled_at",
|
|
87
|
+
end_date_field: "wrap_up_at",
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
let run = 0;
|
|
91
|
+
|
|
92
|
+
afterEach(() => {
|
|
93
|
+
vi.restoreAllMocks();
|
|
94
|
+
vi.unstubAllGlobals();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
function renderFixtureCalendar(doctype: string, dateField?: string): MemoryNavigator {
|
|
98
|
+
run += 1;
|
|
99
|
+
const navigation = createMemoryNavigator("/backoffice");
|
|
100
|
+
renderWithProviders(
|
|
101
|
+
<BackofficeProvider
|
|
102
|
+
frappe={{
|
|
103
|
+
baseURL: `https://calendar-view-${run}.spec.test`,
|
|
104
|
+
fixtures: new InMemoryFixtureProvider({
|
|
105
|
+
DocType: [eventMeta, multiMeta, noDateMeta],
|
|
106
|
+
"Calendar View": [calendarViewDoc],
|
|
107
|
+
QaCalEvent: events,
|
|
108
|
+
QaCalMulti: [],
|
|
109
|
+
}),
|
|
110
|
+
}}
|
|
111
|
+
navigation={navigation}
|
|
112
|
+
>
|
|
113
|
+
<CalendarView doctype={doctype} dateField={dateField} />
|
|
114
|
+
</BackofficeProvider>,
|
|
115
|
+
);
|
|
116
|
+
return navigation;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function json(body: unknown) {
|
|
120
|
+
return new Response(JSON.stringify(body), {
|
|
121
|
+
status: 200,
|
|
122
|
+
headers: { "content-type": "application/json" },
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function renderBenchCalendar(roles: string[]) {
|
|
127
|
+
run += 1;
|
|
128
|
+
let rolesAnswered = false;
|
|
129
|
+
vi.stubGlobal("fetch", async (input: string | URL | Request) => {
|
|
130
|
+
const url = new URL(String(input instanceof Request ? input.url : input));
|
|
131
|
+
const method = url.pathname.replace(/^\/api\/method\//, "");
|
|
132
|
+
if (method === "frappe.auth.get_logged_user") return json({ message: "user@pokemon.test" });
|
|
133
|
+
if (method === "frappe.core.doctype.user.user.get_roles") {
|
|
134
|
+
rolesAnswered = true;
|
|
135
|
+
return json({ message: roles });
|
|
136
|
+
}
|
|
137
|
+
if (method === "frappe.desk.form.load.getdoctype") return json({ docs: [moveMeta] });
|
|
138
|
+
if (url.pathname === `/api/resource/${encodeURIComponent("Poke Move")}`) {
|
|
139
|
+
return json({
|
|
140
|
+
data: [{ name: "m1", doctype: "Poke Move", move_name: "tackle", learned_on: today }],
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return json({ data: [], message: [] });
|
|
144
|
+
});
|
|
145
|
+
const navigation = createMemoryNavigator("/backoffice");
|
|
146
|
+
renderWithProviders(
|
|
147
|
+
<BackofficeProvider
|
|
148
|
+
frappe={{ baseURL: `https://calendar-view-bench-${run}.spec.test` }}
|
|
149
|
+
navigation={navigation}
|
|
150
|
+
>
|
|
151
|
+
<CalendarView doctype="Poke Move" />
|
|
152
|
+
</BackofficeProvider>,
|
|
153
|
+
);
|
|
154
|
+
return { navigation, rolesAnswered: () => rolesAnswered };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function todayCell(): Promise<HTMLElement> {
|
|
158
|
+
return waitFor(
|
|
159
|
+
() => {
|
|
160
|
+
const cell = document.querySelector(`[data-calendar-day="${today}"]`);
|
|
161
|
+
expect(cell).toBeTruthy();
|
|
162
|
+
return cell as HTMLElement;
|
|
163
|
+
},
|
|
164
|
+
{ timeout: 5000 },
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function draggable(title: string): boolean {
|
|
169
|
+
let node: HTMLElement | null = screen.getByText(title);
|
|
170
|
+
for (let depth = 0; node && depth < 6; depth += 1) {
|
|
171
|
+
if (node.className.includes("_cur-grab")) return true;
|
|
172
|
+
node = node.parentElement;
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function settle(ms = 300) {
|
|
178
|
+
await act(async () => {
|
|
179
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
describe("<CalendarView />", () => {
|
|
184
|
+
it("lays events on the settings doc's axis and opens the form on press", async () => {
|
|
185
|
+
const navigation = renderFixtureCalendar("QaCalEvent");
|
|
186
|
+
expect(
|
|
187
|
+
await screen.findByLabelText("Choose calendar date field", {}, { timeout: 5000 }),
|
|
188
|
+
).toHaveProperty("textContent", "Scheduled At");
|
|
189
|
+
fireEvent.click(await screen.findByText("Team sync", {}, { timeout: 5000 }));
|
|
190
|
+
expect(navigation.getLocation().path).toBe("/backoffice/qacalevent/QCE-001");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("writes the picked date field into ?field=", async () => {
|
|
194
|
+
const navigation = renderFixtureCalendar("QaCalMulti");
|
|
195
|
+
fireEvent.click(
|
|
196
|
+
await screen.findByLabelText("Choose calendar date field", {}, { timeout: 5000 }),
|
|
197
|
+
);
|
|
198
|
+
fireEvent.click(await screen.findByText("Follow Up On", {}, { timeout: 5000 }));
|
|
199
|
+
await waitFor(() => expect(navigation.getLocation().params.field).toBe("follow_up_on"));
|
|
200
|
+
expect(navigation.getLocation().path).toBe("/backoffice/qacalmulti/view/calendar");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("starts a record prefilled with the clicked day", async () => {
|
|
204
|
+
const navigation = renderFixtureCalendar("QaCalMulti");
|
|
205
|
+
fireEvent.click(await todayCell());
|
|
206
|
+
await waitFor(() => expect(navigation.getLocation().path).toBe("/backoffice/qacalmulti/new"));
|
|
207
|
+
const prefill = navigation.getLocation().params.d;
|
|
208
|
+
expect(JSON.parse(String(prefill))).toEqual({ due_date: today });
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("offers the list back when the doctype has no date field", async () => {
|
|
212
|
+
const navigation = renderFixtureCalendar("QaCalNone");
|
|
213
|
+
expect(await screen.findByText("Calendar not available", {}, { timeout: 5000 })).toBeTruthy();
|
|
214
|
+
fireEvent.click(screen.getByText("Back to list"));
|
|
215
|
+
expect(navigation.getLocation().path).toBe("/backoffice/qacalnone");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("lets an editor move events and start a record from a day on a live bench", async () => {
|
|
219
|
+
const bench = renderBenchCalendar(["Pokemon Editor", "Guest"]);
|
|
220
|
+
const cell = await todayCell();
|
|
221
|
+
await screen.findByText("tackle", {}, { timeout: 5000 });
|
|
222
|
+
await waitFor(() => expect(bench.rolesAnswered()).toBe(true), { timeout: 5000 });
|
|
223
|
+
await waitFor(() => expect(draggable("tackle")).toBe(true));
|
|
224
|
+
fireEvent.click(cell);
|
|
225
|
+
await waitFor(() =>
|
|
226
|
+
expect(bench.navigation.getLocation().path).toBe("/backoffice/poke-move/new"),
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("neither moves events nor starts a record for a reader on a live bench", async () => {
|
|
231
|
+
const bench = renderBenchCalendar(["Guest"]);
|
|
232
|
+
const cell = await todayCell();
|
|
233
|
+
await screen.findByText("tackle", {}, { timeout: 5000 });
|
|
234
|
+
await settle();
|
|
235
|
+
expect(draggable("tackle")).toBe(false);
|
|
236
|
+
fireEvent.click(cell);
|
|
237
|
+
await settle(100);
|
|
238
|
+
expect(bench.navigation.getLocation().path).toBe("/backoffice");
|
|
239
|
+
expect(bench.rolesAnswered()).toBe(true);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
@@ -9,7 +9,12 @@
|
|
|
9
9
|
// Event click opens the form; day/slot click starts a new document
|
|
10
10
|
// prefilled with the clicked date (`?d=` param on the /new route).
|
|
11
11
|
import { XStack, YStack } from "@multiplatform.one/components";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
FrappeCalendar,
|
|
14
|
+
useDocRights,
|
|
15
|
+
useFrappeConfig,
|
|
16
|
+
type FrappeCalendarProps,
|
|
17
|
+
} from "@multiplatform.one/frappe-ui";
|
|
13
18
|
import { formatCalendarDate, useResolvedKnobs } from "@multiplatform.one/theme";
|
|
14
19
|
import type { ReactElement } from "react";
|
|
15
20
|
import { useTranslation } from "react-i18next";
|
|
@@ -73,6 +78,7 @@ export function CalendarView({
|
|
|
73
78
|
const connection = useViewConnection();
|
|
74
79
|
const { meta, isLoading: isMetaLoading, error: metaError } = useViewMeta(doctype);
|
|
75
80
|
const settings = useCalendarViewSettings(connection, doctype);
|
|
81
|
+
const { rights } = useDocRights(useFrappeConfig(), meta);
|
|
76
82
|
|
|
77
83
|
if (isMetaLoading || settings.isLoading) return <ViewLoading />;
|
|
78
84
|
if (metaError && !meta) return <ViewError doctype={doctype} error={metaError} />;
|
|
@@ -168,16 +174,18 @@ export function CalendarView({
|
|
|
168
174
|
defaultView="month"
|
|
169
175
|
showNavigation
|
|
170
176
|
showViewSwitcher
|
|
177
|
+
enableEventDrag={rights.write}
|
|
171
178
|
onEventClick={(doc) => {
|
|
172
179
|
if (doc.name) navigator.navigate(paths.form(doctype, doc.name));
|
|
173
180
|
}}
|
|
174
181
|
// Desk parity: desk seeds its new-event form with the clicked day
|
|
175
182
|
// (and slot time in week/day grids) — the date rides the `?d=`
|
|
176
183
|
// prefill param into the create form, shaped to the field's type.
|
|
177
|
-
// `onSlotCreate` overrides; the default is gated by `newDoc
|
|
184
|
+
// `onSlotCreate` overrides; the default is gated by `newDoc` and the
|
|
185
|
+
// session's create right.
|
|
178
186
|
onDateClick={
|
|
179
187
|
onSlotCreate ??
|
|
180
|
-
(newDoc.enabled
|
|
188
|
+
(newDoc.enabled && rights.create
|
|
181
189
|
? (date) =>
|
|
182
190
|
navigator.navigate(
|
|
183
191
|
paths.newDoc(doctype, { [dateField]: toFrappeDateValue(date, dateFieldType) }),
|