@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
|
@@ -944,53 +944,62 @@ export function DocFormScreen({
|
|
|
944
944
|
})
|
|
945
945
|
: toolbar}
|
|
946
946
|
|
|
947
|
-
{engine.wasDeleted && (
|
|
948
|
-
<
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
947
|
+
{(engine.wasDeleted || validationSummary || engine.conflict) && (
|
|
948
|
+
<YStack paddingHorizontal="$3">
|
|
949
|
+
{engine.wasDeleted && (
|
|
950
|
+
<XStack
|
|
951
|
+
role="alert"
|
|
952
|
+
{...knobProps.containerRadius}
|
|
953
|
+
{...knobProps.panelPadding}
|
|
954
|
+
backgroundColor="$red3"
|
|
955
|
+
>
|
|
956
|
+
<Text color="$red11" fontSize="$3">
|
|
957
|
+
{t(
|
|
958
|
+
"deletedBanner",
|
|
959
|
+
"This document was deleted on the server. Fields are read-only.",
|
|
960
|
+
)}
|
|
961
|
+
</Text>
|
|
962
|
+
</XStack>
|
|
963
|
+
)}
|
|
960
964
|
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
)}
|
|
965
|
+
{validationSummary && !engine.wasDeleted && (
|
|
966
|
+
<XStack
|
|
967
|
+
role="alert"
|
|
968
|
+
{...knobProps.containerRadius}
|
|
969
|
+
{...knobProps.panelPadding}
|
|
970
|
+
backgroundColor="$red3"
|
|
971
|
+
testID="doc-form-validation-summary"
|
|
972
|
+
>
|
|
973
|
+
<Text color="$red11" fontSize="$3">
|
|
974
|
+
{validationSummary}
|
|
975
|
+
</Text>
|
|
976
|
+
</XStack>
|
|
977
|
+
)}
|
|
975
978
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
979
|
+
{engine.conflict && !engine.wasDeleted && (
|
|
980
|
+
<XStack
|
|
981
|
+
role="status"
|
|
982
|
+
alignItems="center"
|
|
983
|
+
justifyContent="space-between"
|
|
984
|
+
{...knobProps.containerRadius}
|
|
985
|
+
{...knobProps.panelPadding}
|
|
986
|
+
backgroundColor="$orange3"
|
|
987
|
+
gap="$3"
|
|
988
|
+
>
|
|
989
|
+
<Text color="$orange11" fontSize="$3" flex={1}>
|
|
990
|
+
{t("conflictBanner", "This document changed on the server while you were editing.")}
|
|
991
|
+
</Text>
|
|
992
|
+
<Button
|
|
993
|
+
size="$2"
|
|
994
|
+
outlined
|
|
995
|
+
aria-label={t("reloadDoc", "Reload")}
|
|
996
|
+
onPress={engine.reload}
|
|
997
|
+
>
|
|
998
|
+
{t("reloadDoc", "Reload")}
|
|
999
|
+
</Button>
|
|
1000
|
+
</XStack>
|
|
1001
|
+
)}
|
|
1002
|
+
</YStack>
|
|
994
1003
|
)}
|
|
995
1004
|
|
|
996
1005
|
<ScrollView flex={1}>
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// The desk Connections tab (MPO-414): meta.links grouped with get_open_count
|
|
2
|
+
// badges, rows open the filtered target list, + opens a prefilled new doc,
|
|
3
|
+
// and a failed get_open_count falls back to get_count per external link.
|
|
4
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
5
|
+
import { fireEvent, screen, waitFor, within } from "@testing-library/react";
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
import { BackofficeProvider } from "../config/BackofficeProvider";
|
|
8
|
+
import { createMemoryNavigator } from "../navigation/navigator";
|
|
9
|
+
import type { DoctypeMeta } from "../shared/useDoctypeMeta";
|
|
10
|
+
import { FormConnections } from "./FormConnections";
|
|
11
|
+
|
|
12
|
+
const projectMeta: DoctypeMeta = {
|
|
13
|
+
name: "QA Project",
|
|
14
|
+
fields: [],
|
|
15
|
+
links: [
|
|
16
|
+
{ link_doctype: "Contact", link_fieldname: "project", group: "Profile" },
|
|
17
|
+
{ link_doctype: "Blogger", link_fieldname: "project", group: "Profile" },
|
|
18
|
+
{ link_doctype: "Access Log", link_fieldname: "project", group: "Logs" },
|
|
19
|
+
{ link_doctype: "ToDo", link_fieldname: "project" },
|
|
20
|
+
{ link_doctype: "Note", link_fieldname: "project", group: "Logs", hidden: 1 },
|
|
21
|
+
{
|
|
22
|
+
link_doctype: "QA Invoice Item",
|
|
23
|
+
link_fieldname: "qa_invoice",
|
|
24
|
+
parent_doctype: "QA Invoice",
|
|
25
|
+
table_fieldname: "items",
|
|
26
|
+
is_child_table: 1,
|
|
27
|
+
group: "Billing",
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const openCount = {
|
|
33
|
+
message: {
|
|
34
|
+
count: {
|
|
35
|
+
external_links_found: [
|
|
36
|
+
{ doctype: "Contact", count: 3, open_count: 1 },
|
|
37
|
+
{ doctype: "Blogger", count: 0, open_count: 0 },
|
|
38
|
+
{ doctype: "Access Log", count: 120, open_count: 0 },
|
|
39
|
+
{ doctype: "ToDo", count: 5, open_count: 2 },
|
|
40
|
+
],
|
|
41
|
+
internal_links_found: [
|
|
42
|
+
{ doctype: "QA Invoice", count: 2, open_count: 0, names: ["QA-INV-001", "QA-INV-002"] },
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
let host = 0;
|
|
49
|
+
let openCountFails = false;
|
|
50
|
+
let requests: URL[] = [];
|
|
51
|
+
|
|
52
|
+
function json(body: unknown, status = 200) {
|
|
53
|
+
return new Response(JSON.stringify(body), {
|
|
54
|
+
status,
|
|
55
|
+
headers: { "content-type": "application/json" },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
host += 1;
|
|
61
|
+
openCountFails = false;
|
|
62
|
+
requests = [];
|
|
63
|
+
vi.stubGlobal("fetch", async (input: string | URL | Request) => {
|
|
64
|
+
const url = new URL(String(input instanceof Request ? input.url : input));
|
|
65
|
+
requests.push(url);
|
|
66
|
+
if (url.pathname === "/api/method/frappe.desk.notifications.get_open_count") {
|
|
67
|
+
return openCountFails ? json({ exc_type: "PermissionError" }, 403) : json(openCount);
|
|
68
|
+
}
|
|
69
|
+
if (url.pathname === "/api/method/frappe.client.get_count") {
|
|
70
|
+
return json({ message: url.searchParams.get("doctype") === "Contact" ? 4 : 0 });
|
|
71
|
+
}
|
|
72
|
+
return json({ message: null });
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
afterEach(() => {
|
|
77
|
+
vi.restoreAllMocks();
|
|
78
|
+
vi.unstubAllGlobals();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
function renderConnections(meta: DoctypeMeta = projectMeta) {
|
|
82
|
+
const navigation = createMemoryNavigator("/backoffice/qa-project/QA-PROJ-0001");
|
|
83
|
+
renderWithProviders(
|
|
84
|
+
<BackofficeProvider
|
|
85
|
+
frappe={{ baseURL: `https://connections-${host}.screen.spec.test` }}
|
|
86
|
+
navigation={navigation}
|
|
87
|
+
>
|
|
88
|
+
<FormConnections doctype="QA Project" name="QA-PROJ-0001" meta={meta} />
|
|
89
|
+
</BackofficeProvider>,
|
|
90
|
+
);
|
|
91
|
+
return navigation;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function row(doctype: string): HTMLElement {
|
|
95
|
+
return screen.getByRole("link", { name: doctype });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
describe("FormConnections", () => {
|
|
99
|
+
it("groups visible links and badges their counts", async () => {
|
|
100
|
+
renderConnections();
|
|
101
|
+
await waitFor(() => expect(within(row("Contact")).getByText("3")).toBeTruthy());
|
|
102
|
+
for (const heading of ["Profile", "Logs", "Reference", "Billing"]) {
|
|
103
|
+
expect(screen.getByText(heading)).toBeTruthy();
|
|
104
|
+
}
|
|
105
|
+
expect(screen.queryByRole("link", { name: "Note" })).toBeNull();
|
|
106
|
+
expect(within(row("Contact")).getByText("1")).toBeTruthy();
|
|
107
|
+
expect(within(row("Access Log")).getByText("99+")).toBeTruthy();
|
|
108
|
+
expect(row("Blogger").textContent).toBe("Blogger");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("opens the target list filtered to this document", async () => {
|
|
112
|
+
const navigation = renderConnections();
|
|
113
|
+
await waitFor(() => expect(within(row("Contact")).getByText("3")).toBeTruthy());
|
|
114
|
+
fireEvent.click(row("Contact"));
|
|
115
|
+
const location = navigation.getLocation();
|
|
116
|
+
expect(location.path).toBe("/backoffice/contact");
|
|
117
|
+
expect(JSON.parse(String(location.params.f))).toEqual([["project", "=", "QA-PROJ-0001"]]);
|
|
118
|
+
fireEvent.click(row("QA Invoice"));
|
|
119
|
+
expect(JSON.parse(String(navigation.getLocation().params.f))).toEqual([
|
|
120
|
+
["name", "in", ["QA-INV-001", "QA-INV-002"]],
|
|
121
|
+
]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("creates a linked document with the back-reference prefilled", async () => {
|
|
125
|
+
const navigation = renderConnections();
|
|
126
|
+
await waitFor(() => expect(within(row("Contact")).getByText("3")).toBeTruthy());
|
|
127
|
+
fireEvent.click(screen.getByLabelText("New Contact"));
|
|
128
|
+
const location = navigation.getLocation();
|
|
129
|
+
expect(location.path).toBe("/backoffice/contact/new");
|
|
130
|
+
expect(JSON.parse(String(location.params.d))).toEqual({ project: "QA-PROJ-0001" });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("falls back to get_count and disables the uncounted internal link", async () => {
|
|
134
|
+
openCountFails = true;
|
|
135
|
+
const navigation = renderConnections();
|
|
136
|
+
await waitFor(() => expect(within(row("Contact")).getByText("4")).toBeTruthy(), {
|
|
137
|
+
timeout: 5000,
|
|
138
|
+
});
|
|
139
|
+
const counted = requests
|
|
140
|
+
.filter((url) => url.pathname === "/api/method/frappe.client.get_count")
|
|
141
|
+
.map((url) => url.searchParams.get("doctype"))
|
|
142
|
+
.sort();
|
|
143
|
+
expect(counted).toEqual(["Access Log", "Blogger", "Contact", "ToDo"]);
|
|
144
|
+
fireEvent.click(row("QA Invoice"));
|
|
145
|
+
expect(navigation.getLocation().path).toBe("/backoffice/qa-project/QA-PROJ-0001");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("says so when the doctype links nowhere", async () => {
|
|
149
|
+
renderConnections({ name: "QA Loner", fields: [], links: [] });
|
|
150
|
+
expect(await screen.findByText("No connections")).toBeTruthy();
|
|
151
|
+
expect(requests).toHaveLength(0);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// The form menu's input dialogs (MPO-414): each confirm stays gated until
|
|
2
|
+
// its input is usable, then hands the trimmed value to the caller.
|
|
3
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
4
|
+
import { fireEvent, screen } from "@testing-library/react";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { BackofficeProvider } from "../config/BackofficeProvider";
|
|
7
|
+
import { createMemoryNavigator } from "../navigation/navigator";
|
|
8
|
+
import { AssignDialog, EmailDialog, JumpToFieldDialog, RenameDialog } from "./FormDialogs";
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
vi.restoreAllMocks();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
function button(label: string): HTMLElement {
|
|
15
|
+
const found = Array.from(document.body.querySelectorAll('[role="button"], button')).find((node) =>
|
|
16
|
+
node.textContent?.trim().startsWith(label),
|
|
17
|
+
);
|
|
18
|
+
if (!found) throw new Error(`no button ${label}`);
|
|
19
|
+
return found as HTMLElement;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function input(label: string): HTMLInputElement | HTMLTextAreaElement {
|
|
23
|
+
const field = screen.getByLabelText(label);
|
|
24
|
+
return (
|
|
25
|
+
field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement
|
|
26
|
+
? field
|
|
27
|
+
: field.querySelector("input, textarea")
|
|
28
|
+
) as HTMLInputElement;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function gated(label: string): boolean {
|
|
32
|
+
return button(label).getAttribute("aria-disabled") === "true";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("RenameDialog", () => {
|
|
36
|
+
it("renames only to a different, trimmed name", () => {
|
|
37
|
+
const onRename = vi.fn();
|
|
38
|
+
renderWithProviders(
|
|
39
|
+
<RenameDialog open onOpenChange={() => {}} currentName="TODO-0001" onRename={onRename} />,
|
|
40
|
+
);
|
|
41
|
+
expect(gated("Rename")).toBe(true);
|
|
42
|
+
fireEvent.change(input("New name"), { target: { value: " TODO-0001 " } });
|
|
43
|
+
expect(gated("Rename")).toBe(true);
|
|
44
|
+
fireEvent.change(input("New name"), { target: { value: " TODO-0042 " } });
|
|
45
|
+
expect(gated("Rename")).toBe(false);
|
|
46
|
+
fireEvent.click(button("Rename"));
|
|
47
|
+
expect(onRename).toHaveBeenCalledWith("TODO-0042");
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("EmailDialog", () => {
|
|
52
|
+
it("sends only with recipients and a subject", () => {
|
|
53
|
+
const onSend = vi.fn();
|
|
54
|
+
renderWithProviders(
|
|
55
|
+
<EmailDialog open onOpenChange={() => {}} defaultSubject="ToDo TODO-0001" onSend={onSend} />,
|
|
56
|
+
);
|
|
57
|
+
expect(input("Subject").value).toBe("ToDo TODO-0001");
|
|
58
|
+
expect(gated("Send")).toBe(true);
|
|
59
|
+
fireEvent.change(input("To (comma-separated emails)"), {
|
|
60
|
+
target: { value: " jane@example.com " },
|
|
61
|
+
});
|
|
62
|
+
fireEvent.change(input("Message"), { target: { value: "See attached." } });
|
|
63
|
+
fireEvent.click(button("Send"));
|
|
64
|
+
expect(onSend).toHaveBeenCalledWith({
|
|
65
|
+
recipients: "jane@example.com",
|
|
66
|
+
subject: "ToDo TODO-0001",
|
|
67
|
+
content: "See attached.",
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("AssignDialog", () => {
|
|
73
|
+
it("will not assign before a user is picked", () => {
|
|
74
|
+
const onAssign = vi.fn();
|
|
75
|
+
renderWithProviders(
|
|
76
|
+
<BackofficeProvider
|
|
77
|
+
frappe={{ baseURL: "https://form-dialogs.screen.spec.test" }}
|
|
78
|
+
navigation={createMemoryNavigator("/backoffice")}
|
|
79
|
+
>
|
|
80
|
+
<AssignDialog open onOpenChange={() => {}} onAssign={onAssign} />
|
|
81
|
+
</BackofficeProvider>,
|
|
82
|
+
);
|
|
83
|
+
expect(screen.getByText("Assign to")).toBeTruthy();
|
|
84
|
+
expect(gated("Assign")).toBe(true);
|
|
85
|
+
fireEvent.click(button("Assign"));
|
|
86
|
+
expect(onAssign).not.toHaveBeenCalled();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("JumpToFieldDialog", () => {
|
|
91
|
+
const fields = [
|
|
92
|
+
{ name: "customer", label: "Customer", tabKey: "details", tabLabel: "Details" },
|
|
93
|
+
{ name: "due_date", label: "Due Date", tabKey: "details", tabLabel: "Details" },
|
|
94
|
+
{ name: "terms", label: "Payment Terms", tabKey: "terms", tabLabel: "Terms" },
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
it("filters fields by label or fieldname and jumps to a picked one", () => {
|
|
98
|
+
const onJump = vi.fn();
|
|
99
|
+
renderWithProviders(
|
|
100
|
+
<JumpToFieldDialog open onOpenChange={() => {}} fields={fields} onJump={onJump} />,
|
|
101
|
+
);
|
|
102
|
+
fireEvent.change(input("Search fields…"), { target: { value: "due" } });
|
|
103
|
+
expect(screen.queryByText("Customer")).toBeNull();
|
|
104
|
+
fireEvent.click(screen.getByText("Due Date"));
|
|
105
|
+
expect(onJump).toHaveBeenCalledWith(fields[1]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("jumps to the first match on confirm and gates an empty result", () => {
|
|
109
|
+
const onJump = vi.fn();
|
|
110
|
+
renderWithProviders(
|
|
111
|
+
<JumpToFieldDialog open onOpenChange={() => {}} fields={fields} onJump={onJump} />,
|
|
112
|
+
);
|
|
113
|
+
fireEvent.change(input("Search fields…"), { target: { value: "terms" } });
|
|
114
|
+
fireEvent.click(button("Jump"));
|
|
115
|
+
expect(onJump).toHaveBeenCalledWith(fields[2]);
|
|
116
|
+
fireEvent.change(input("Search fields…"), { target: { value: "nothing like it" } });
|
|
117
|
+
expect(gated("Jump")).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// The desk form toolbar (MPO-414): one lifecycle primary per docstatus, no
|
|
2
|
+
// primary the session's rights withhold (MPO-390), the status pill, the ⋯
|
|
3
|
+
// menu and the prev/next pager.
|
|
4
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
5
|
+
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
|
6
|
+
import { describe, expect, it, vi } from "vitest";
|
|
7
|
+
import {
|
|
8
|
+
FormToolbar,
|
|
9
|
+
resolveIndicator,
|
|
10
|
+
type FormToolbarMenuItem,
|
|
11
|
+
type FormToolbarProps,
|
|
12
|
+
} from "./FormToolbar";
|
|
13
|
+
|
|
14
|
+
function props(overrides: Partial<FormToolbarProps> = {}): FormToolbarProps {
|
|
15
|
+
return {
|
|
16
|
+
breadcrumbs: [
|
|
17
|
+
{ label: "Home", href: "/backoffice" },
|
|
18
|
+
{ label: "Sales Order", href: "/backoffice/sales-order" },
|
|
19
|
+
{ label: "SO-0001" },
|
|
20
|
+
],
|
|
21
|
+
onNavigate: vi.fn(),
|
|
22
|
+
title: "SO-0001",
|
|
23
|
+
indicator: "draft",
|
|
24
|
+
isSaving: false,
|
|
25
|
+
canSave: true,
|
|
26
|
+
canSubmit: true,
|
|
27
|
+
canCancel: false,
|
|
28
|
+
canAmend: false,
|
|
29
|
+
onSave: vi.fn(),
|
|
30
|
+
onSubmit: vi.fn(),
|
|
31
|
+
onCancel: vi.fn(),
|
|
32
|
+
onAmend: vi.fn(),
|
|
33
|
+
menuItems: [],
|
|
34
|
+
...overrides,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const primaries = ["save", "submit", "cancel", "amend"] as const;
|
|
39
|
+
|
|
40
|
+
function visiblePrimaries(): string[] {
|
|
41
|
+
return primaries.filter((action) => screen.queryByTestId(`form-toolbar-${action}`));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe("FormToolbar", () => {
|
|
45
|
+
it("offers Save on a dirty draft and Submit once it is saved", () => {
|
|
46
|
+
const onSave = vi.fn();
|
|
47
|
+
const onSubmit = vi.fn();
|
|
48
|
+
const { rerender } = renderWithProviders(
|
|
49
|
+
<FormToolbar {...props({ indicator: "notSaved", onSave, onSubmit })} />,
|
|
50
|
+
);
|
|
51
|
+
expect(visiblePrimaries()).toEqual(["save"]);
|
|
52
|
+
fireEvent.click(screen.getByTestId("form-toolbar-save"));
|
|
53
|
+
expect(onSave).toHaveBeenCalledTimes(1);
|
|
54
|
+
rerender(<FormToolbar {...props({ indicator: "draft", onSave, onSubmit })} />);
|
|
55
|
+
expect(visiblePrimaries()).toEqual(["submit"]);
|
|
56
|
+
fireEvent.click(screen.getByTestId("form-toolbar-submit"));
|
|
57
|
+
expect(onSubmit).toHaveBeenCalledTimes(1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("offers Cancel on a submitted doc and Amend on a cancelled one", () => {
|
|
61
|
+
const onCancel = vi.fn();
|
|
62
|
+
const { rerender } = renderWithProviders(
|
|
63
|
+
<FormToolbar
|
|
64
|
+
{...props({ indicator: "submitted", canSave: false, canSubmit: false, canCancel: true })}
|
|
65
|
+
onCancel={onCancel}
|
|
66
|
+
/>,
|
|
67
|
+
);
|
|
68
|
+
expect(visiblePrimaries()).toEqual(["cancel"]);
|
|
69
|
+
fireEvent.click(screen.getByTestId("form-toolbar-cancel"));
|
|
70
|
+
expect(onCancel).toHaveBeenCalledTimes(1);
|
|
71
|
+
rerender(
|
|
72
|
+
<FormToolbar
|
|
73
|
+
{...props({ indicator: "cancelled", canSave: false, canSubmit: false, canAmend: true })}
|
|
74
|
+
/>,
|
|
75
|
+
);
|
|
76
|
+
expect(visiblePrimaries()).toEqual(["amend"]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("draws no primary a reader's rights withhold", () => {
|
|
80
|
+
renderWithProviders(
|
|
81
|
+
<FormToolbar {...props({ indicator: "notSaved", canSave: false, canSubmit: false })} />,
|
|
82
|
+
);
|
|
83
|
+
expect(visiblePrimaries()).toEqual([]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("pills the status from meta.states before guessing a color", () => {
|
|
87
|
+
const { rerender } = renderWithProviders(<FormToolbar {...props({ indicator: "new" })} />);
|
|
88
|
+
expect(screen.getByText("Not Saved")).toBeTruthy();
|
|
89
|
+
const approved = resolveIndicator({
|
|
90
|
+
isCreate: false,
|
|
91
|
+
isDirty: false,
|
|
92
|
+
docstatus: 1,
|
|
93
|
+
isSubmittable: true,
|
|
94
|
+
statusValue: "Approved",
|
|
95
|
+
states: [{ title: "Approved", color: "Green" }],
|
|
96
|
+
});
|
|
97
|
+
expect(approved).toEqual({ label: "Approved", color: "green" });
|
|
98
|
+
rerender(<FormToolbar {...props({ indicator: approved })} />);
|
|
99
|
+
expect(screen.getByText("Approved")).toBeTruthy();
|
|
100
|
+
expect(screen.queryByText("Submitted")).toBeNull();
|
|
101
|
+
rerender(<FormToolbar {...props({ indicator: "saved" })} />);
|
|
102
|
+
for (const label of ["Not Saved", "Draft", "Submitted", "Cancelled", "Approved"]) {
|
|
103
|
+
expect(screen.queryByText(label)).toBeNull();
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("runs ⋯ menu items and holds the disabled ones", async () => {
|
|
108
|
+
const onDuplicate = vi.fn();
|
|
109
|
+
const onDelete = vi.fn();
|
|
110
|
+
const menuItems: FormToolbarMenuItem[] = [
|
|
111
|
+
{ key: "duplicate", label: "Duplicate", icon: "duplicate", onSelect: onDuplicate },
|
|
112
|
+
{
|
|
113
|
+
key: "delete",
|
|
114
|
+
label: "Delete",
|
|
115
|
+
icon: "delete",
|
|
116
|
+
destructive: true,
|
|
117
|
+
disabled: true,
|
|
118
|
+
disabledReason: "No delete permission",
|
|
119
|
+
onSelect: onDelete,
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
renderWithProviders(<FormToolbar {...props({ menuItems })} />);
|
|
123
|
+
fireEvent.click(screen.getByTestId("form-toolbar-menu"));
|
|
124
|
+
const item = (label: string) =>
|
|
125
|
+
Array.from(document.querySelectorAll('[role="menuitem"]')).find((node) =>
|
|
126
|
+
node.textContent?.startsWith(label),
|
|
127
|
+
) as HTMLElement;
|
|
128
|
+
await waitFor(() => expect(item("Delete")).toBeTruthy());
|
|
129
|
+
expect(item("Delete").getAttribute("aria-disabled")).toBe("true");
|
|
130
|
+
fireEvent.click(item("Delete"));
|
|
131
|
+
expect(onDelete).not.toHaveBeenCalled();
|
|
132
|
+
fireEvent.click(item("Duplicate"));
|
|
133
|
+
expect(onDuplicate).toHaveBeenCalledTimes(1);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("pages prev/next and disables the boundary arrow instead of hiding it", () => {
|
|
137
|
+
const onPrevDoc = vi.fn();
|
|
138
|
+
const onNextDoc = vi.fn();
|
|
139
|
+
const { rerender } = renderWithProviders(<FormToolbar {...props()} />);
|
|
140
|
+
expect(screen.queryByTestId("form-toolbar-prev")).toBeNull();
|
|
141
|
+
rerender(<FormToolbar {...props({ onPrevDoc, onNextDoc, prevDocDisabled: true })} />);
|
|
142
|
+
fireEvent.click(screen.getByTestId("form-toolbar-prev"));
|
|
143
|
+
fireEvent.click(screen.getByTestId("form-toolbar-next"));
|
|
144
|
+
expect(onPrevDoc).not.toHaveBeenCalled();
|
|
145
|
+
expect(onNextDoc).toHaveBeenCalledTimes(1);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
* initials fallback — desk parity for user chips.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { Avatar, Button, Text, XStack, YStack } from "@multiplatform.one/components";
|
|
8
|
+
import { Avatar, Button, styled, Text, XStack, YStack } from "@multiplatform.one/components";
|
|
9
9
|
import { useResolvedKnobs } from "@multiplatform.one/theme";
|
|
10
10
|
import { XIcon } from "@phosphor-icons/react";
|
|
11
11
|
import { useTranslation } from "react-i18next";
|
|
12
12
|
import { BACKOFFICE_I18N_NAMESPACE } from "../i18n";
|
|
13
13
|
|
|
14
|
+
const SidebarAvatarSlot = styled(XStack, { name: "SidebarAvatarSlot" });
|
|
15
|
+
|
|
14
16
|
export interface SidebarPersonItem {
|
|
15
17
|
/** Stable row id (assignment/share row name, or the user id). */
|
|
16
18
|
id: string;
|
|
@@ -104,12 +106,12 @@ export function SidebarAvatarStrip({ items, max }: SidebarAvatarStripProps) {
|
|
|
104
106
|
return (
|
|
105
107
|
<XStack alignItems="center">
|
|
106
108
|
{visible.map((item, index) => (
|
|
107
|
-
<
|
|
109
|
+
<SidebarAvatarSlot key={item.id} marginInlineStart={index === 0 ? 0 : -6}>
|
|
108
110
|
<Avatar size="$1" src={item.image} name={item.label} alt={item.label} />
|
|
109
|
-
</
|
|
111
|
+
</SidebarAvatarSlot>
|
|
110
112
|
))}
|
|
111
113
|
{overflow > 0 && (
|
|
112
|
-
<
|
|
114
|
+
<SidebarAvatarSlot marginInlineStart={-6}>
|
|
113
115
|
<Avatar
|
|
114
116
|
size="$1"
|
|
115
117
|
alt={`+${overflow}`}
|
|
@@ -119,7 +121,7 @@ export function SidebarAvatarStrip({ items, max }: SidebarAvatarStripProps) {
|
|
|
119
121
|
</Text>
|
|
120
122
|
}
|
|
121
123
|
/>
|
|
122
|
-
</
|
|
124
|
+
</SidebarAvatarSlot>
|
|
123
125
|
)}
|
|
124
126
|
</XStack>
|
|
125
127
|
);
|