@multiplatform.one/frappe-ui 7.11.0 → 7.15.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/README.md +2 -2
- package/dist/cjs/FrappeDashboard.cjs +8 -10
- package/dist/cjs/FrappeDashboard.native.js +8 -14
- package/dist/cjs/FrappeDashboard.native.js.map +1 -1
- package/dist/cjs/FrappeForm.cjs +465 -175
- package/dist/cjs/FrappeForm.native.js +611 -207
- package/dist/cjs/FrappeForm.native.js.map +1 -1
- package/dist/cjs/FrappeFormConnections.cjs +125 -0
- package/dist/cjs/FrappeFormConnections.native.js +149 -0
- package/dist/cjs/FrappeFormConnections.native.js.map +1 -0
- package/dist/cjs/FrappeImageView.cjs +7 -9
- package/dist/cjs/FrappeImageView.native.js +7 -13
- package/dist/cjs/FrappeImageView.native.js.map +1 -1
- package/dist/cjs/FrappeList.cjs +29 -4
- package/dist/cjs/FrappeList.native.js +37 -5
- package/dist/cjs/FrappeList.native.js.map +1 -1
- package/dist/cjs/FrappeListView.cjs +102 -29
- package/dist/cjs/FrappeListView.native.js +109 -29
- package/dist/cjs/FrappeListView.native.js.map +1 -1
- package/dist/cjs/FrappeProvider.cjs +7 -2
- package/dist/cjs/FrappeProvider.native.js +7 -2
- package/dist/cjs/FrappeProvider.native.js.map +1 -1
- package/dist/cjs/FrappeReportBuilder.cjs +10 -11
- package/dist/cjs/FrappeReportBuilder.native.js +10 -16
- package/dist/cjs/FrappeReportBuilder.native.js.map +1 -1
- package/dist/cjs/FrappeRowsReader.cjs +49 -0
- package/dist/cjs/FrappeRowsReader.native.js +53 -0
- package/dist/cjs/FrappeRowsReader.native.js.map +1 -0
- package/dist/cjs/FrappeTable.cjs +18 -6
- package/dist/cjs/FrappeTable.native.js +24 -6
- package/dist/cjs/FrappeTable.native.js.map +1 -1
- package/dist/cjs/fieldRegistry.cjs +36 -11
- package/dist/cjs/fieldRegistry.native.js +59 -11
- package/dist/cjs/fieldRegistry.native.js.map +1 -1
- package/dist/cjs/fields/Date/index.native.js +5 -2
- package/dist/cjs/fields/Date/index.native.js.map +1 -1
- package/dist/cjs/fields/Date/sheetKeyboard.cjs +2 -2
- package/dist/cjs/fields/Date/sheetKeyboard.native.js +2 -2
- package/dist/cjs/fields/Date/sheetKeyboard.native.js.map +1 -1
- package/dist/cjs/fields/DateRange/index.cjs +2 -1
- package/dist/cjs/fields/DateRange/index.native.js +4 -1
- package/dist/cjs/fields/DateRange/index.native.js.map +1 -1
- package/dist/cjs/formEngine/index.cjs +4 -2
- package/dist/cjs/formEngine/index.native.js +3 -1
- package/dist/cjs/formEngine/index.native.js.map +1 -1
- package/dist/cjs/formEngine/useSessionRoles.cjs +77 -0
- package/dist/cjs/formEngine/useSessionRoles.native.js +84 -0
- package/dist/cjs/formEngine/useSessionRoles.native.js.map +1 -0
- package/dist/cjs/frappeConnections.cjs +41 -0
- package/dist/cjs/frappeConnections.native.js +50 -0
- package/dist/cjs/frappeConnections.native.js.map +1 -0
- package/dist/cjs/frappeNavigation.cjs +107 -0
- package/dist/cjs/frappeNavigation.native.js +112 -0
- package/dist/cjs/frappeNavigation.native.js.map +1 -0
- package/dist/cjs/index.cjs +18 -1
- package/dist/cjs/index.native.js +18 -1
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/schemaForm/index.cjs +4 -1
- package/dist/cjs/schemaForm/index.native.js +4 -1
- package/dist/cjs/schemaForm/index.native.js.map +1 -1
- package/dist/cjs/useConnectionFilters.cjs +146 -0
- package/dist/cjs/useConnectionFilters.native.js +213 -0
- package/dist/cjs/useConnectionFilters.native.js.map +1 -0
- package/dist/cjs/useFrappePagination.cjs +37 -18
- package/dist/cjs/useFrappePagination.native.js +59 -23
- package/dist/cjs/useFrappePagination.native.js.map +1 -1
- package/dist/cjs/useFrappeRows.native.js.map +1 -1
- package/dist/esm/FrappeDashboard.mjs +9 -11
- package/dist/esm/FrappeDashboard.mjs.map +1 -1
- package/dist/esm/FrappeDashboard.native.js +9 -15
- package/dist/esm/FrappeDashboard.native.js.map +1 -1
- package/dist/esm/FrappeForm.mjs +471 -181
- package/dist/esm/FrappeForm.mjs.map +1 -1
- package/dist/esm/FrappeForm.native.js +599 -195
- package/dist/esm/FrappeForm.native.js.map +1 -1
- package/dist/esm/FrappeFormConnections.mjs +99 -0
- package/dist/esm/FrappeFormConnections.mjs.map +1 -0
- package/dist/esm/FrappeFormConnections.native.js +120 -0
- package/dist/esm/FrappeFormConnections.native.js.map +1 -0
- package/dist/esm/FrappeImageView.mjs +8 -10
- package/dist/esm/FrappeImageView.mjs.map +1 -1
- package/dist/esm/FrappeImageView.native.js +8 -14
- package/dist/esm/FrappeImageView.native.js.map +1 -1
- package/dist/esm/FrappeList.mjs +31 -6
- package/dist/esm/FrappeList.mjs.map +1 -1
- package/dist/esm/FrappeList.native.js +39 -7
- package/dist/esm/FrappeList.native.js.map +1 -1
- package/dist/esm/FrappeListView.mjs +103 -30
- package/dist/esm/FrappeListView.mjs.map +1 -1
- package/dist/esm/FrappeListView.native.js +110 -30
- package/dist/esm/FrappeListView.native.js.map +1 -1
- package/dist/esm/FrappeProvider.mjs +7 -2
- package/dist/esm/FrappeProvider.mjs.map +1 -1
- package/dist/esm/FrappeProvider.native.js +7 -2
- package/dist/esm/FrappeProvider.native.js.map +1 -1
- package/dist/esm/FrappeReportBuilder.mjs +11 -12
- package/dist/esm/FrappeReportBuilder.mjs.map +1 -1
- package/dist/esm/FrappeReportBuilder.native.js +11 -17
- package/dist/esm/FrappeReportBuilder.native.js.map +1 -1
- package/dist/esm/FrappeRowsReader.mjs +24 -0
- package/dist/esm/FrappeRowsReader.mjs.map +1 -0
- package/dist/esm/FrappeRowsReader.native.js +25 -0
- package/dist/esm/FrappeRowsReader.native.js.map +1 -0
- package/dist/esm/FrappeTable.mjs +18 -6
- package/dist/esm/FrappeTable.mjs.map +1 -1
- package/dist/esm/FrappeTable.native.js +24 -6
- package/dist/esm/FrappeTable.native.js.map +1 -1
- package/dist/esm/fieldRegistry.mjs +35 -12
- package/dist/esm/fieldRegistry.mjs.map +1 -1
- package/dist/esm/fieldRegistry.native.js +58 -12
- package/dist/esm/fieldRegistry.native.js.map +1 -1
- package/dist/esm/fields/Date/index.native.js +6 -3
- package/dist/esm/fields/Date/index.native.js.map +1 -1
- package/dist/esm/fields/Date/sheetKeyboard.mjs +2 -2
- package/dist/esm/fields/Date/sheetKeyboard.mjs.map +1 -1
- package/dist/esm/fields/Date/sheetKeyboard.native.js +2 -2
- package/dist/esm/fields/Date/sheetKeyboard.native.js.map +1 -1
- package/dist/esm/fields/DateRange/index.mjs +2 -1
- package/dist/esm/fields/DateRange/index.mjs.map +1 -1
- package/dist/esm/fields/DateRange/index.native.js +4 -1
- package/dist/esm/fields/DateRange/index.native.js.map +1 -1
- package/dist/esm/formEngine/index.mjs +2 -1
- package/dist/esm/formEngine/index.mjs.map +1 -1
- package/dist/esm/formEngine/index.native.js +2 -1
- package/dist/esm/formEngine/index.native.js.map +1 -1
- package/dist/esm/formEngine/useSessionRoles.mjs +52 -0
- package/dist/esm/formEngine/useSessionRoles.mjs.map +1 -0
- package/dist/esm/formEngine/useSessionRoles.native.js +56 -0
- package/dist/esm/formEngine/useSessionRoles.native.js.map +1 -0
- package/dist/esm/frappeConnections.mjs +15 -0
- package/dist/esm/frappeConnections.mjs.map +1 -0
- package/dist/esm/frappeConnections.native.js +21 -0
- package/dist/esm/frappeConnections.native.js.map +1 -0
- package/dist/esm/frappeNavigation.mjs +78 -0
- package/dist/esm/frappeNavigation.mjs.map +1 -0
- package/dist/esm/frappeNavigation.native.js +80 -0
- package/dist/esm/frappeNavigation.native.js.map +1 -0
- package/dist/esm/index.mjs +7 -3
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js +7 -3
- package/dist/esm/index.native.js.map +1 -1
- package/dist/esm/schemaForm/index.mjs +4 -1
- package/dist/esm/schemaForm/index.mjs.map +1 -1
- package/dist/esm/schemaForm/index.native.js +4 -1
- package/dist/esm/schemaForm/index.native.js.map +1 -1
- package/dist/esm/useConnectionFilters.mjs +121 -0
- package/dist/esm/useConnectionFilters.mjs.map +1 -0
- package/dist/esm/useConnectionFilters.native.js +185 -0
- package/dist/esm/useConnectionFilters.native.js.map +1 -0
- package/dist/esm/useFrappePagination.mjs +38 -19
- package/dist/esm/useFrappePagination.mjs.map +1 -1
- package/dist/esm/useFrappePagination.native.js +60 -24
- package/dist/esm/useFrappePagination.native.js.map +1 -1
- package/dist/esm/useFrappeRows.mjs.map +1 -1
- package/dist/esm/useFrappeRows.native.js.map +1 -1
- package/dist/jsx/FrappeDashboard.mjs +9 -11
- package/dist/jsx/FrappeDashboard.mjs.map +1 -1
- package/dist/jsx/FrappeDashboard.native.js +8 -14
- package/dist/jsx/FrappeDashboard.native.js.map +1 -1
- package/dist/jsx/FrappeForm.mjs +471 -181
- package/dist/jsx/FrappeForm.mjs.map +1 -1
- package/dist/jsx/FrappeForm.native.js +611 -207
- package/dist/jsx/FrappeForm.native.js.map +1 -1
- package/dist/jsx/FrappeFormConnections.mjs +99 -0
- package/dist/jsx/FrappeFormConnections.mjs.map +1 -0
- package/dist/jsx/FrappeFormConnections.native.js +149 -0
- package/dist/jsx/FrappeFormConnections.native.js.map +1 -0
- package/dist/jsx/FrappeImageView.mjs +8 -10
- package/dist/jsx/FrappeImageView.mjs.map +1 -1
- package/dist/jsx/FrappeImageView.native.js +7 -13
- package/dist/jsx/FrappeImageView.native.js.map +1 -1
- package/dist/jsx/FrappeList.mjs +31 -6
- package/dist/jsx/FrappeList.mjs.map +1 -1
- package/dist/jsx/FrappeList.native.js +37 -5
- package/dist/jsx/FrappeList.native.js.map +1 -1
- package/dist/jsx/FrappeListView.mjs +103 -30
- package/dist/jsx/FrappeListView.mjs.map +1 -1
- package/dist/jsx/FrappeListView.native.js +109 -29
- package/dist/jsx/FrappeListView.native.js.map +1 -1
- package/dist/jsx/FrappeProvider.mjs +7 -2
- package/dist/jsx/FrappeProvider.mjs.map +1 -1
- package/dist/jsx/FrappeProvider.native.js +7 -2
- package/dist/jsx/FrappeProvider.native.js.map +1 -1
- package/dist/jsx/FrappeReportBuilder.mjs +11 -12
- package/dist/jsx/FrappeReportBuilder.mjs.map +1 -1
- package/dist/jsx/FrappeReportBuilder.native.js +10 -16
- package/dist/jsx/FrappeReportBuilder.native.js.map +1 -1
- package/dist/jsx/FrappeRowsReader.mjs +24 -0
- package/dist/jsx/FrappeRowsReader.mjs.map +1 -0
- package/dist/jsx/FrappeRowsReader.native.js +53 -0
- package/dist/jsx/FrappeRowsReader.native.js.map +1 -0
- package/dist/jsx/FrappeTable.mjs +18 -6
- package/dist/jsx/FrappeTable.mjs.map +1 -1
- package/dist/jsx/FrappeTable.native.js +24 -6
- package/dist/jsx/FrappeTable.native.js.map +1 -1
- package/dist/jsx/fieldRegistry.mjs +35 -12
- package/dist/jsx/fieldRegistry.mjs.map +1 -1
- package/dist/jsx/fieldRegistry.native.js +59 -11
- package/dist/jsx/fieldRegistry.native.js.map +1 -1
- package/dist/jsx/fields/Date/index.native.js +5 -2
- package/dist/jsx/fields/Date/index.native.js.map +1 -1
- package/dist/jsx/fields/Date/sheetKeyboard.mjs +2 -2
- package/dist/jsx/fields/Date/sheetKeyboard.mjs.map +1 -1
- package/dist/jsx/fields/Date/sheetKeyboard.native.js +2 -2
- package/dist/jsx/fields/Date/sheetKeyboard.native.js.map +1 -1
- package/dist/jsx/fields/DateRange/index.mjs +2 -1
- package/dist/jsx/fields/DateRange/index.mjs.map +1 -1
- package/dist/jsx/fields/DateRange/index.native.js +4 -1
- package/dist/jsx/fields/DateRange/index.native.js.map +1 -1
- package/dist/jsx/formEngine/index.mjs +2 -1
- package/dist/jsx/formEngine/index.mjs.map +1 -1
- package/dist/jsx/formEngine/index.native.js +3 -1
- package/dist/jsx/formEngine/index.native.js.map +1 -1
- package/dist/jsx/formEngine/useSessionRoles.mjs +52 -0
- package/dist/jsx/formEngine/useSessionRoles.mjs.map +1 -0
- package/dist/jsx/formEngine/useSessionRoles.native.js +84 -0
- package/dist/jsx/formEngine/useSessionRoles.native.js.map +1 -0
- package/dist/jsx/frappeConnections.mjs +15 -0
- package/dist/jsx/frappeConnections.mjs.map +1 -0
- package/dist/jsx/frappeConnections.native.js +50 -0
- package/dist/jsx/frappeConnections.native.js.map +1 -0
- package/dist/jsx/frappeNavigation.mjs +78 -0
- package/dist/jsx/frappeNavigation.mjs.map +1 -0
- package/dist/jsx/frappeNavigation.native.js +112 -0
- package/dist/jsx/frappeNavigation.native.js.map +1 -0
- package/dist/jsx/index.js +7 -3
- package/dist/jsx/index.js.map +1 -1
- package/dist/jsx/index.mjs +7 -3
- package/dist/jsx/index.mjs.map +1 -1
- package/dist/jsx/index.native.js +18 -1
- package/dist/jsx/index.native.js.map +1 -1
- package/dist/jsx/schemaForm/index.mjs +4 -1
- package/dist/jsx/schemaForm/index.mjs.map +1 -1
- package/dist/jsx/schemaForm/index.native.js +4 -1
- package/dist/jsx/schemaForm/index.native.js.map +1 -1
- package/dist/jsx/useConnectionFilters.mjs +121 -0
- package/dist/jsx/useConnectionFilters.mjs.map +1 -0
- package/dist/jsx/useConnectionFilters.native.js +213 -0
- package/dist/jsx/useConnectionFilters.native.js.map +1 -0
- package/dist/jsx/useFrappePagination.mjs +38 -19
- package/dist/jsx/useFrappePagination.mjs.map +1 -1
- package/dist/jsx/useFrappePagination.native.js +59 -23
- package/dist/jsx/useFrappePagination.native.js.map +1 -1
- package/dist/jsx/useFrappeRows.mjs.map +1 -1
- package/dist/jsx/useFrappeRows.native.js.map +1 -1
- package/package.json +24 -20
- package/src/FileBrowser/AttachLibrary.stories.tsx +23 -0
- package/src/FrappeDashboard.tsx +11 -16
- package/src/FrappeForm.connections.spec.tsx +280 -0
- package/src/FrappeForm.connections.stories.tsx +93 -0
- package/src/FrappeForm.deskSemantics.spec.tsx +284 -0
- package/src/FrappeForm.tsx +560 -145
- package/src/FrappeFormConnections.tsx +148 -0
- package/src/FrappeGuestMeta.spec.tsx +143 -0
- package/src/FrappeImageView.tsx +8 -12
- package/src/FrappeList.tsx +71 -15
- package/src/FrappeListView.cards.spec.tsx +175 -0
- package/src/FrappeListView.cards.stories.tsx +131 -0
- package/src/FrappeListView.connections.spec.tsx +192 -0
- package/src/FrappeListView.connections.stories.tsx +99 -0
- package/src/FrappeListView.seed.spec.tsx +228 -0
- package/src/FrappeListView.tsx +177 -31
- package/src/FrappeProvider.tsx +13 -1
- package/src/FrappeReportBuilder.tsx +11 -19
- package/src/FrappeRowsReader.tsx +38 -0
- package/src/FrappeTable.optionalProps.spec.tsx +249 -0
- package/src/FrappeTable.spec.tsx +32 -42
- package/src/FrappeTable.stories.spec.tsx +55 -0
- package/src/FrappeTable.stories.tsx +28 -37
- package/src/FrappeTable.tsx +108 -45
- package/src/childTableDependsOn.spec.tsx +85 -0
- package/src/desk/DeskFormSidebar.spec.tsx +20 -0
- package/src/desk/DeskFormSidebar.stories.tsx +42 -0
- package/src/fieldRegistry.spec.tsx +27 -64
- package/src/fieldRegistry.ts +71 -14
- package/src/fields/Date/index.native.tsx +4 -2
- package/src/fields/Date/sheetKeyboard.spec.ts +14 -5
- package/src/fields/Date/sheetKeyboard.ts +12 -4
- package/src/fields/DateRange/DateRange.spec.tsx +67 -0
- package/src/fields/DateRange/index.tsx +5 -1
- package/src/fields/LinkTree.spec.tsx +8 -0
- package/src/fields/LinkTree.stories.tsx +28 -5
- package/src/fields/Select/Select.hydration.spec.tsx +177 -0
- package/src/formEngine/doctypeMeta.ts +15 -114
- package/src/formEngine/index.ts +2 -0
- package/src/formEngine/seam.spec.ts +23 -4
- package/src/formEngine/useSessionRoles.ts +73 -0
- package/src/frappeConnections.ts +50 -0
- package/src/frappeNavigation.tsx +112 -0
- package/src/frappeRoutes.spec.tsx +335 -0
- package/src/index.ts +28 -1
- package/src/schemaForm/index.tsx +1 -0
- package/src/useConnectionFilters.tsx +157 -0
- package/src/useFrappePagination.ts +70 -26
- package/src/useFrappeRows.sharedPath.spec.tsx +206 -0
- package/src/useFrappeRows.ts +5 -4
- package/types/FrappeDashboard.d.ts +3 -4
- package/types/FrappeDashboard.d.ts.map +1 -1
- package/types/FrappeForm.d.ts +57 -5
- package/types/FrappeForm.d.ts.map +1 -1
- package/types/FrappeFormConnections.d.ts +18 -0
- package/types/FrappeFormConnections.d.ts.map +1 -0
- package/types/FrappeImageView.d.ts +2 -3
- package/types/FrappeImageView.d.ts.map +1 -1
- package/types/FrappeList.d.ts +20 -2
- package/types/FrappeList.d.ts.map +1 -1
- package/types/FrappeListView.d.ts +53 -3
- package/types/FrappeListView.d.ts.map +1 -1
- package/types/FrappeProvider.d.ts +8 -1
- package/types/FrappeProvider.d.ts.map +1 -1
- package/types/FrappeReportBuilder.d.ts.map +1 -1
- package/types/FrappeRowsReader.d.ts +26 -0
- package/types/FrappeRowsReader.d.ts.map +1 -0
- package/types/FrappeTable.d.ts +39 -8
- package/types/FrappeTable.d.ts.map +1 -1
- package/types/fieldRegistry.d.ts +17 -3
- package/types/fieldRegistry.d.ts.map +1 -1
- package/types/fields/Date/index.native.d.ts.map +1 -1
- package/types/fields/Date/sheetKeyboard.d.ts +4 -3
- package/types/fields/Date/sheetKeyboard.d.ts.map +1 -1
- package/types/fields/DateRange/index.d.ts.map +1 -1
- package/types/formEngine/doctypeMeta.d.ts +10 -114
- package/types/formEngine/doctypeMeta.d.ts.map +1 -1
- package/types/formEngine/index.d.ts +1 -0
- package/types/formEngine/index.d.ts.map +1 -1
- package/types/formEngine/useSessionRoles.d.ts +19 -0
- package/types/formEngine/useSessionRoles.d.ts.map +1 -0
- package/types/frappeConnections.d.ts +33 -0
- package/types/frappeConnections.d.ts.map +1 -0
- package/types/frappeNavigation.d.ts +49 -0
- package/types/frappeNavigation.d.ts.map +1 -0
- package/types/index.d.ts +7 -3
- package/types/index.d.ts.map +1 -1
- package/types/schemaForm/index.d.ts.map +1 -1
- package/types/useConnectionFilters.d.ts +34 -0
- package/types/useConnectionFilters.d.ts.map +1 -0
- package/types/useFrappePagination.d.ts +18 -5
- package/types/useFrappePagination.d.ts.map +1 -1
- package/types/useFrappeRows.d.ts +5 -4
- package/types/useFrappeRows.d.ts.map +1 -1
- package/src/FileBrowser/AttachLibrary.spec.tsx +0 -74
|
@@ -16,16 +16,25 @@ import {
|
|
|
16
16
|
|
|
17
17
|
describe("dateSheetMaxHeight", () => {
|
|
18
18
|
it("uses the 85% detent when the keyboard is down", () => {
|
|
19
|
-
expect(dateSheetMaxHeight(800, 0)).toBe(Math.round(800 * 0.85));
|
|
19
|
+
expect(dateSheetMaxHeight(800, 0, 47)).toBe(Math.round(800 * 0.85));
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
it("caps to the space above the keyboard, less the 48pt backdrop strip", () => {
|
|
23
|
-
expect(dateSheetMaxHeight(800, 400)).toBe(800 - 400 - DATE_SHEET_BACKDROP_STRIP);
|
|
24
|
-
expect(dateSheetMaxHeight(800, 400)).toBeLessThan(dateSheetMaxHeight(800, 0));
|
|
23
|
+
expect(dateSheetMaxHeight(800, 400, 0)).toBe(800 - 400 - DATE_SHEET_BACKDROP_STRIP);
|
|
24
|
+
expect(dateSheetMaxHeight(800, 400, 0)).toBeLessThan(dateSheetMaxHeight(800, 0, 0));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("keeps the backdrop strip below the status bar, not inside it", () => {
|
|
28
|
+
const windowHeight = 874;
|
|
29
|
+
const keyboardInset = 335;
|
|
30
|
+
const topInset = 62;
|
|
31
|
+
const sheetTop =
|
|
32
|
+
windowHeight - keyboardInset - dateSheetMaxHeight(windowHeight, keyboardInset, topInset);
|
|
33
|
+
expect(sheetTop - topInset).toBe(DATE_SHEET_BACKDROP_STRIP);
|
|
25
34
|
});
|
|
26
35
|
|
|
27
36
|
it("never collapses below the minimum sheet height", () => {
|
|
28
|
-
expect(dateSheetMaxHeight(800, 700)).toBe(DATE_SHEET_MIN_HEIGHT);
|
|
37
|
+
expect(dateSheetMaxHeight(800, 700, 47)).toBe(DATE_SHEET_MIN_HEIGHT);
|
|
29
38
|
});
|
|
30
39
|
});
|
|
31
40
|
|
|
@@ -35,7 +44,7 @@ describe("datePickerHeight", () => {
|
|
|
35
44
|
});
|
|
36
45
|
|
|
37
46
|
it("shrinks the calendar so the sheet can sit above the keyboard", () => {
|
|
38
|
-
const maxHeight = dateSheetMaxHeight(800, 400);
|
|
47
|
+
const maxHeight = dateSheetMaxHeight(800, 400, 47);
|
|
39
48
|
const height = datePickerHeight("date", maxHeight);
|
|
40
49
|
expect(height).toBeLessThan(DATE_PICKER_CALENDAR_HEIGHT);
|
|
41
50
|
expect(height).toBeLessThanOrEqual(maxHeight);
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The Date iOS sheet used to give the calendar a fixed 360pt height inside a
|
|
5
5
|
* Modal. KeyboardAvoidingView cannot shrink that shape — it pads the container
|
|
6
|
-
* and the sheet overflows off the top. Cap the sheet to the space
|
|
7
|
-
* keyboard (less a strip of backdrop) and shrink the picker
|
|
6
|
+
* and the sheet overflows off the top. Cap the sheet to the space between the
|
|
7
|
+
* status bar and the keyboard (less a strip of backdrop) and shrink the picker
|
|
8
|
+
* to fit.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
export const DATE_SHEET_MAX_HEIGHT_RATIO = 0.85;
|
|
@@ -15,12 +16,19 @@ export const DATE_PICKER_CALENDAR_HEIGHT = 360;
|
|
|
15
16
|
export const DATE_PICKER_WHEEL_HEIGHT = 200;
|
|
16
17
|
const minPickerHeight = 120;
|
|
17
18
|
|
|
18
|
-
export function dateSheetMaxHeight(
|
|
19
|
+
export function dateSheetMaxHeight(
|
|
20
|
+
windowHeight: number,
|
|
21
|
+
keyboardInset: number,
|
|
22
|
+
topInset: number,
|
|
23
|
+
): number {
|
|
19
24
|
const available = Math.max(0, windowHeight - keyboardInset);
|
|
20
25
|
return Math.max(
|
|
21
26
|
DATE_SHEET_MIN_HEIGHT,
|
|
22
27
|
Math.round(
|
|
23
|
-
Math.min(
|
|
28
|
+
Math.min(
|
|
29
|
+
windowHeight * DATE_SHEET_MAX_HEIGHT_RATIO,
|
|
30
|
+
available - topInset - DATE_SHEET_BACKDROP_STRIP,
|
|
31
|
+
),
|
|
24
32
|
),
|
|
25
33
|
);
|
|
26
34
|
}
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* spec pass). Both are now wired, and the range is carried as the two calendar
|
|
9
9
|
* dates comma-separated — the shape the registry feeds in and reads back.
|
|
10
10
|
*/
|
|
11
|
+
import { Form } from "@multiplatform.one/forms";
|
|
11
12
|
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
13
|
+
import { useForm } from "@tanstack/react-form";
|
|
12
14
|
import { act, cleanup, fireEvent, waitFor } from "@testing-library/react";
|
|
13
15
|
import type React from "react";
|
|
14
16
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
@@ -181,6 +183,71 @@ describe("DateRangeField controlled value (MPO-137)", () => {
|
|
|
181
183
|
});
|
|
182
184
|
});
|
|
183
185
|
|
|
186
|
+
describe("DateRangeField uncontrolled and form-bound paths (MPO-137)", () => {
|
|
187
|
+
it("seeds an uncontrolled field from a wire defaultValue and still emits a pick", async () => {
|
|
188
|
+
const onChange = vi.fn();
|
|
189
|
+
const result = renderWithProviders(
|
|
190
|
+
<DateRangeField label="Period" defaultValue="2026-03-05,2026-03-31" onChange={onChange} />,
|
|
191
|
+
);
|
|
192
|
+
expect(trigger(result.container)?.value).toBe("Mar 5, 2026 – Mar 31, 2026");
|
|
193
|
+
|
|
194
|
+
await act(async () => {
|
|
195
|
+
fireEvent.click(trigger(result.container)!);
|
|
196
|
+
});
|
|
197
|
+
await waitFor(() => {
|
|
198
|
+
expect(getDayButton(15)).toBeTruthy();
|
|
199
|
+
});
|
|
200
|
+
await act(async () => {
|
|
201
|
+
fireEvent.click(getDayButton(15)!);
|
|
202
|
+
});
|
|
203
|
+
await act(async () => {
|
|
204
|
+
fireEvent.click(getDayButton(20)!);
|
|
205
|
+
});
|
|
206
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
207
|
+
expect(onChange).toHaveBeenCalledWith("2026-03-15,2026-03-20");
|
|
208
|
+
expect(trigger(result.container)?.value).toBe("Mar 15, 2026 – Mar 20, 2026");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("binds name/form inside a TanStack form: the form value paints and a pick writes back", async () => {
|
|
212
|
+
type Range = { start: Date | null; end: Date | null } | null;
|
|
213
|
+
let formApi: { state: { values: { period: Range } } } | undefined;
|
|
214
|
+
function Harness() {
|
|
215
|
+
const form = useForm({
|
|
216
|
+
defaultValues: {
|
|
217
|
+
period: { start: new Date(2026, 2, 5), end: new Date(2026, 2, 31) } as Range,
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
formApi = form as unknown as typeof formApi;
|
|
221
|
+
return (
|
|
222
|
+
<Form form={form}>
|
|
223
|
+
<DateRangeField label="Period" name="period" form={form} />
|
|
224
|
+
</Form>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
const result = renderWithProviders(<Harness />);
|
|
228
|
+
await waitFor(() => {
|
|
229
|
+
expect(trigger(result.container)?.value).toBe("Mar 5, 2026 – Mar 31, 2026");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
await act(async () => {
|
|
233
|
+
fireEvent.click(trigger(result.container)!);
|
|
234
|
+
});
|
|
235
|
+
await waitFor(() => {
|
|
236
|
+
expect(getDayButton(15)).toBeTruthy();
|
|
237
|
+
});
|
|
238
|
+
await act(async () => {
|
|
239
|
+
fireEvent.click(getDayButton(15)!);
|
|
240
|
+
});
|
|
241
|
+
await act(async () => {
|
|
242
|
+
fireEvent.click(getDayButton(20)!);
|
|
243
|
+
});
|
|
244
|
+
const period = formApi?.state.values.period;
|
|
245
|
+
expect(period?.start?.toDateString()).toBe(new Date(2026, 2, 15).toDateString());
|
|
246
|
+
expect(period?.end?.toDateString()).toBe(new Date(2026, 2, 20).toDateString());
|
|
247
|
+
expect(trigger(result.container)?.value).toBe("Mar 15, 2026 – Mar 20, 2026");
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
184
251
|
describe("registry resolution (Date Range fieldtype)", () => {
|
|
185
252
|
function propsFor(overrides: Partial<FieldRenderProps> = {}): FieldRenderProps {
|
|
186
253
|
return { value: "", onChange: () => {}, ...overrides };
|
|
@@ -97,6 +97,10 @@ export function DateRangeField<
|
|
|
97
97
|
// re-seeds its display state from every new `value` identity, which would
|
|
98
98
|
// otherwise discard a half-picked range on an unrelated re-render.
|
|
99
99
|
const rangeValue = useMemo(() => toPickerRange(wireValue), [wireValue]);
|
|
100
|
+
const rangeDefault = useMemo(
|
|
101
|
+
() => (typeof defaultValue === "string" ? toPickerRange(defaultValue) : defaultValue),
|
|
102
|
+
[defaultValue],
|
|
103
|
+
);
|
|
100
104
|
|
|
101
105
|
return (
|
|
102
106
|
<FormsDateRangePicker
|
|
@@ -105,7 +109,7 @@ export function DateRangeField<
|
|
|
105
109
|
validators={validators}
|
|
106
110
|
value={rangeValue}
|
|
107
111
|
onChange={(range) => emit?.(toWireValue(range))}
|
|
108
|
-
defaultValue={
|
|
112
|
+
defaultValue={rangeDefault}
|
|
109
113
|
label={fieldProps.label}
|
|
110
114
|
labelProps={fieldProps.labelProps}
|
|
111
115
|
error={fieldProps.error}
|
|
@@ -101,6 +101,14 @@ describe("LinkTree adapter (MPO-111)", () => {
|
|
|
101
101
|
expect(screen.queryByText("Should not win")).toBeNull();
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
+
it("still mounts TreeSelect with the placeholder when the forest is empty", () => {
|
|
105
|
+
mockUseLiveQuery.mockReturnValue({ data: [], isLoading: false, isError: false });
|
|
106
|
+
const result = renderTree();
|
|
107
|
+
expect(result.container.querySelector('[data-testid="tree-select"]')).toBeTruthy();
|
|
108
|
+
expect(result.container.querySelector('[data-testid="tree-select-trigger"]')).toBeTruthy();
|
|
109
|
+
expect(screen.getByText("Select Account")).toBeTruthy();
|
|
110
|
+
});
|
|
111
|
+
|
|
104
112
|
it("does not call onChange when disabled", () => {
|
|
105
113
|
const onChange = vi.fn();
|
|
106
114
|
renderTree({ value: "Assets", onChange, disabled: true });
|
|
@@ -114,12 +114,9 @@ function regionMeta(name: string) {
|
|
|
114
114
|
};
|
|
115
115
|
})();
|
|
116
116
|
|
|
117
|
-
function host(children: ReactNode) {
|
|
117
|
+
function host(children: ReactNode, docs: Region[] = regionDocs) {
|
|
118
118
|
return (
|
|
119
|
-
<FrappeProvider
|
|
120
|
-
baseURL={MOCK_HOST}
|
|
121
|
-
fixtures={new InMemoryFixtureProvider({ QaRegion: regionDocs })}
|
|
122
|
-
>
|
|
119
|
+
<FrappeProvider baseURL={MOCK_HOST} fixtures={new InMemoryFixtureProvider({ QaRegion: docs })}>
|
|
123
120
|
{children}
|
|
124
121
|
</FrappeProvider>
|
|
125
122
|
);
|
|
@@ -160,6 +157,32 @@ export const AllStates = () =>
|
|
|
160
157
|
</YStack>,
|
|
161
158
|
);
|
|
162
159
|
|
|
160
|
+
export const Empty = () => host(<ControlledTree label="Region (empty forest)" />, []);
|
|
161
|
+
Empty.parameters = {
|
|
162
|
+
docs: {
|
|
163
|
+
description: {
|
|
164
|
+
story:
|
|
165
|
+
"Tree DocType with no rows: TreeSelect still mounts with the placeholder and an empty forest, so the field never degrades to a blank or a spinner that never settles.",
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export const Nested = () =>
|
|
171
|
+
host(
|
|
172
|
+
<YStack gap="$4" width={400}>
|
|
173
|
+
<ControlledTree label="Leaf, three levels deep" value="North A1" />
|
|
174
|
+
<ControlledTree label="Branch, two levels deep" value="North A" />
|
|
175
|
+
</YStack>,
|
|
176
|
+
);
|
|
177
|
+
Nested.parameters = {
|
|
178
|
+
docs: {
|
|
179
|
+
description: {
|
|
180
|
+
story:
|
|
181
|
+
"North > North A > North A1: a stored link to a nested node resolves through the loaded forest, so the trigger shows the node's title_field, not its name.",
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
|
|
163
186
|
export const ViaLink = () => {
|
|
164
187
|
const [value, setValue] = useState("North A");
|
|
165
188
|
return host(
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
/**
|
|
3
|
+
* MPO-279: the Frappe `Select` field (the registry's SelectField) server-renders
|
|
4
|
+
* the sheet trigger at every width, because the server has no viewport. These
|
|
5
|
+
* hydrate that markup at a phone width and a desktop width, then drive the
|
|
6
|
+
* hydrated control by pointer and by keyboard and read its combobox ARIA.
|
|
7
|
+
*/
|
|
8
|
+
import { TestProviders } from "@multiplatform.one/test-utils";
|
|
9
|
+
import { fireEvent, waitFor } from "@testing-library/react";
|
|
10
|
+
import { act, useLayoutEffect, useState } from "react";
|
|
11
|
+
import { hydrateRoot, type Root } from "react-dom/client";
|
|
12
|
+
import { renderToString } from "react-dom/server";
|
|
13
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
14
|
+
import { Select } from ".";
|
|
15
|
+
|
|
16
|
+
const options = ["Alpha", "Beta", "Gamma", "Delta"];
|
|
17
|
+
let onChange = vi.fn();
|
|
18
|
+
let hydrationTrigger: Element | null = null;
|
|
19
|
+
|
|
20
|
+
function CommitProbe() {
|
|
21
|
+
useLayoutEffect(() => {
|
|
22
|
+
hydrationTrigger ??= document.querySelector('[role="combobox"]');
|
|
23
|
+
}, []);
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function Fixture() {
|
|
28
|
+
const [value, setValue] = useState("Beta");
|
|
29
|
+
return (
|
|
30
|
+
<TestProviders>
|
|
31
|
+
<output>{value}</output>
|
|
32
|
+
<Select
|
|
33
|
+
label="Choice"
|
|
34
|
+
options={options}
|
|
35
|
+
value={value}
|
|
36
|
+
onChange={(next: unknown) => {
|
|
37
|
+
onChange(next);
|
|
38
|
+
setValue(String(next));
|
|
39
|
+
}}
|
|
40
|
+
/>
|
|
41
|
+
<CommitProbe />
|
|
42
|
+
</TestProviders>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let root: Root | undefined;
|
|
47
|
+
let container: HTMLDivElement | undefined;
|
|
48
|
+
let errors: unknown[] = [];
|
|
49
|
+
|
|
50
|
+
async function hydrateAt(width: number) {
|
|
51
|
+
vi.spyOn(window, "scrollTo").mockImplementation(() => {});
|
|
52
|
+
Object.defineProperty(window, "innerWidth", { configurable: true, value: width, writable: true });
|
|
53
|
+
const browserWindow = window;
|
|
54
|
+
let html: string;
|
|
55
|
+
vi.stubGlobal("window", undefined);
|
|
56
|
+
try {
|
|
57
|
+
html = renderToString(<Fixture />);
|
|
58
|
+
} finally {
|
|
59
|
+
vi.stubGlobal("window", browserWindow);
|
|
60
|
+
}
|
|
61
|
+
container = document.createElement("div");
|
|
62
|
+
container.innerHTML = html;
|
|
63
|
+
document.body.append(container);
|
|
64
|
+
const serverTrigger = container.querySelector('[role="combobox"]');
|
|
65
|
+
errors = [];
|
|
66
|
+
hydrationTrigger = null;
|
|
67
|
+
onChange = vi.fn();
|
|
68
|
+
vi.spyOn(console, "error").mockImplementation((...args) => errors.push(args));
|
|
69
|
+
await act(async () => {
|
|
70
|
+
root = hydrateRoot(container!, <Fixture />, {
|
|
71
|
+
onRecoverableError: (error) => errors.push(error),
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
return { serverTrigger, view: container };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const hydrationErrors = () =>
|
|
78
|
+
errors.filter((error) => /hydration|hydrated|didn't match/i.test(String(error)));
|
|
79
|
+
const trigger = () => document.querySelector('[role="combobox"]') as HTMLElement;
|
|
80
|
+
const listOptions = () => [...document.querySelectorAll('[role="option"]')] as HTMLElement[];
|
|
81
|
+
const option = (label: string) => listOptions().find((node) => node.textContent?.includes(label))!;
|
|
82
|
+
|
|
83
|
+
/** One act per event: a browser flushes React between discrete events. */
|
|
84
|
+
async function press(node: Element) {
|
|
85
|
+
await act(async () => fireEvent.pointerDown(node, { button: 0, pointerId: 1 }));
|
|
86
|
+
await act(async () => fireEvent.mouseDown(node, { detail: 1 }));
|
|
87
|
+
await act(async () => fireEvent.mouseUp(node, { detail: 1 }));
|
|
88
|
+
await act(async () => fireEvent.click(node, { detail: 1 }));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const focused = () => document.activeElement as HTMLElement;
|
|
92
|
+
|
|
93
|
+
async function key(node: Element, name: string) {
|
|
94
|
+
await act(async () => {
|
|
95
|
+
fireEvent.keyDown(node, { key: name, code: name === " " ? "Space" : name });
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
afterEach(async () => {
|
|
100
|
+
if (root) await act(async () => root?.unmount());
|
|
101
|
+
root = undefined;
|
|
102
|
+
container?.remove();
|
|
103
|
+
container = undefined;
|
|
104
|
+
vi.restoreAllMocks();
|
|
105
|
+
vi.unstubAllGlobals();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("SelectField SSR to hydration (MPO-279)", () => {
|
|
109
|
+
it.each([390, 1200])(
|
|
110
|
+
"hydrates the server trigger at %i pixels with no recoverable error",
|
|
111
|
+
async (width) => {
|
|
112
|
+
const { serverTrigger } = await hydrateAt(width);
|
|
113
|
+
expect(serverTrigger).toBeTruthy();
|
|
114
|
+
expect(hydrationTrigger).toBe(serverTrigger);
|
|
115
|
+
expect(hydrationErrors()).toEqual([]);
|
|
116
|
+
await waitFor(() => expect(trigger().textContent).toContain("Beta"));
|
|
117
|
+
expect(trigger().getAttribute("aria-haspopup")).toBe("listbox");
|
|
118
|
+
expect(trigger().getAttribute("aria-expanded")).toBe("false");
|
|
119
|
+
if (width === 390) expect(trigger()).toBe(serverTrigger);
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
it("desktop: keyboard opens on the selected option, navigates, selects and closes", async () => {
|
|
124
|
+
await hydrateAt(1200);
|
|
125
|
+
await key(trigger(), "Enter");
|
|
126
|
+
await waitFor(() => expect(trigger().getAttribute("aria-expanded")).toBe("true"));
|
|
127
|
+
expect(listOptions().map((node) => node.textContent?.trim())).toEqual(options);
|
|
128
|
+
expect(option("Beta").getAttribute("aria-selected")).toBe("true");
|
|
129
|
+
expect(document.querySelector('[role="listbox"]')).toBeTruthy();
|
|
130
|
+
|
|
131
|
+
await waitFor(() => expect(focused()).toBe(option("Beta")));
|
|
132
|
+
await key(focused(), "ArrowDown");
|
|
133
|
+
await waitFor(() => expect(focused()).toBe(option("Gamma")));
|
|
134
|
+
await key(focused(), "Enter");
|
|
135
|
+
await waitFor(() => expect(trigger().getAttribute("aria-expanded")).toBe("false"));
|
|
136
|
+
expect(onChange).toHaveBeenCalledExactlyOnceWith("Gamma");
|
|
137
|
+
expect(container?.querySelector("output")?.textContent).toBe("Gamma");
|
|
138
|
+
expect(trigger().textContent).toContain("Gamma");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("desktop: Escape closes without changing the value", async () => {
|
|
142
|
+
await hydrateAt(1200);
|
|
143
|
+
await key(trigger(), "ArrowDown");
|
|
144
|
+
await waitFor(() => expect(trigger().getAttribute("aria-expanded")).toBe("true"));
|
|
145
|
+
await key(document.activeElement ?? document.body, "Escape");
|
|
146
|
+
await waitFor(() => expect(trigger().getAttribute("aria-expanded")).toBe("false"));
|
|
147
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
148
|
+
expect(trigger().textContent).toContain("Beta");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("desktop: pointer opens, selects, and an outside press dismisses", async () => {
|
|
152
|
+
await hydrateAt(1200);
|
|
153
|
+
await press(trigger());
|
|
154
|
+
await waitFor(() => expect(listOptions().length).toBe(options.length));
|
|
155
|
+
await press(option("Delta"));
|
|
156
|
+
await waitFor(() => expect(container?.querySelector("output")?.textContent).toBe("Delta"));
|
|
157
|
+
expect(onChange).toHaveBeenCalledExactlyOnceWith("Delta");
|
|
158
|
+
|
|
159
|
+
await press(trigger());
|
|
160
|
+
await waitFor(() => expect(trigger().getAttribute("aria-expanded")).toBe("true"));
|
|
161
|
+
await press(document.body);
|
|
162
|
+
await waitFor(() => expect(trigger().getAttribute("aria-expanded")).toBe("false"));
|
|
163
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("phone: the sheet opens by pointer and selects, and wires aria-controls while open", async () => {
|
|
167
|
+
await hydrateAt(390);
|
|
168
|
+
await press(trigger());
|
|
169
|
+
await waitFor(() => expect(trigger().getAttribute("aria-expanded")).toBe("true"));
|
|
170
|
+
const controls = trigger().getAttribute("aria-controls");
|
|
171
|
+
expect(controls).toBeTruthy();
|
|
172
|
+
expect(document.getElementById(controls!)?.getAttribute("role")).toBe("listbox");
|
|
173
|
+
await press(option("Alpha"));
|
|
174
|
+
await waitFor(() => expect(container?.querySelector("output")?.textContent).toBe("Alpha"));
|
|
175
|
+
expect(onChange).toHaveBeenCalledExactlyOnceWith("Alpha");
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -1,117 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* a lower-tier package must never reach up into backoffice for a type.
|
|
8
|
-
* `useDoctypeMeta` (the loader hook, which needs the backoffice app context)
|
|
9
|
-
* stays where it is and re-exports these.
|
|
10
|
-
*
|
|
11
|
-
* Deliberately loose: the server is the source of truth, `0 | 1` flags are
|
|
12
|
-
* what the RPC actually sends, and the index signature carries the keys no
|
|
13
|
-
* surface has needed yet. `fieldtype` stays `string` here; narrowing it to
|
|
14
|
-
* the `FrappeFieldType` union is the remaining half of MPO-15's meta-model
|
|
15
|
-
* criterion and is tracked there, not silently done in a move.
|
|
2
|
+
* The desk's names for the one DocType meta model, which
|
|
3
|
+
* `@multiplatform.one/frappe` declares (MPO-15). These are aliases, not
|
|
4
|
+
* shapes: the form engine, the backoffice surfaces and `useDocTypeMeta`
|
|
5
|
+
* all read the same `DocTypeField` / `DocTypeMeta`, so a field key the
|
|
6
|
+
* server sends reaches every one of them or none.
|
|
16
7
|
*/
|
|
8
|
+
import type {
|
|
9
|
+
DocTypeField,
|
|
10
|
+
DocTypeLink,
|
|
11
|
+
DocTypeMeta,
|
|
12
|
+
DocTypePermission,
|
|
13
|
+
} from "@multiplatform.one/frappe";
|
|
17
14
|
|
|
18
|
-
|
|
19
|
-
export
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
fieldtype: string;
|
|
23
|
-
options?: string;
|
|
24
|
-
reqd?: 0 | 1;
|
|
25
|
-
hidden?: 0 | 1;
|
|
26
|
-
read_only?: 0 | 1;
|
|
27
|
-
bold?: 0 | 1;
|
|
28
|
-
in_list_view?: 0 | 1;
|
|
29
|
-
in_standard_filter?: 0 | 1;
|
|
30
|
-
in_global_search?: 0 | 1;
|
|
31
|
-
allow_in_quick_entry?: 0 | 1;
|
|
32
|
-
allow_on_submit?: 0 | 1;
|
|
33
|
-
no_copy?: 0 | 1;
|
|
34
|
-
collapsible?: 0 | 1;
|
|
35
|
-
collapsible_depends_on?: string;
|
|
36
|
-
depends_on?: string;
|
|
37
|
-
mandatory_depends_on?: string;
|
|
38
|
-
read_only_depends_on?: string;
|
|
39
|
-
fetch_from?: string;
|
|
40
|
-
permlevel?: number;
|
|
41
|
-
length?: number;
|
|
42
|
-
default?: string;
|
|
43
|
-
description?: string;
|
|
44
|
-
precision?: string | number;
|
|
45
|
-
idx?: number;
|
|
46
|
-
[key: string]: unknown;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** DocType Link child row (Connections tab). Probed: `group` arrives null when unset. */
|
|
50
|
-
export interface DocLinkMeta {
|
|
51
|
-
name?: string;
|
|
52
|
-
link_doctype: string;
|
|
53
|
-
link_fieldname: string;
|
|
54
|
-
group?: string | null;
|
|
55
|
-
hidden?: 0 | 1;
|
|
56
|
-
is_child_table?: 0 | 1;
|
|
57
|
-
parent_doctype?: string;
|
|
58
|
-
table_fieldname?: string;
|
|
59
|
-
[key: string]: unknown;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Role permission row from the DocType meta. */
|
|
63
|
-
export interface DocPermissionRule {
|
|
64
|
-
role?: string;
|
|
65
|
-
permlevel?: number;
|
|
66
|
-
read?: 0 | 1;
|
|
67
|
-
write?: 0 | 1;
|
|
68
|
-
create?: 0 | 1;
|
|
69
|
-
delete?: 0 | 1;
|
|
70
|
-
submit?: 0 | 1;
|
|
71
|
-
cancel?: 0 | 1;
|
|
72
|
-
amend?: 0 | 1;
|
|
73
|
-
print?: 0 | 1;
|
|
74
|
-
email?: 0 | 1;
|
|
75
|
-
share?: 0 | 1;
|
|
76
|
-
if_owner?: 0 | 1;
|
|
77
|
-
[key: string]: unknown;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** DocType meta as returned by frappe.desk.form.load.getdoctype (loose). */
|
|
81
|
-
export interface DoctypeMeta {
|
|
82
|
-
doctype?: string;
|
|
83
|
-
name: string;
|
|
84
|
-
module?: string;
|
|
85
|
-
issingle?: 0 | 1;
|
|
86
|
-
istable?: 0 | 1;
|
|
87
|
-
is_submittable?: 0 | 1;
|
|
88
|
-
is_tree?: 0 | 1;
|
|
89
|
-
/** Desk treeview_settings asset (`{doctype}_tree.js`) — gates Tree. */
|
|
90
|
-
__tree_js?: string;
|
|
91
|
-
/** Desk calendar_settings asset (`{doctype}_calendar.js`) — gates Calendar. */
|
|
92
|
-
__calendar_js?: string;
|
|
93
|
-
is_virtual?: 0 | 1;
|
|
94
|
-
editable_grid?: 0 | 1;
|
|
95
|
-
track_changes?: 0 | 1;
|
|
96
|
-
quick_entry?: 0 | 1;
|
|
97
|
-
autoname?: string;
|
|
98
|
-
naming_rule?: string;
|
|
99
|
-
allow_rename?: 0 | 1;
|
|
100
|
-
allow_copy?: 0 | 1;
|
|
101
|
-
read_only?: 0 | 1;
|
|
102
|
-
in_create?: 0 | 1;
|
|
103
|
-
hide_toolbar?: 0 | 1;
|
|
104
|
-
max_attachments?: number;
|
|
105
|
-
title_field?: string;
|
|
106
|
-
image_field?: string;
|
|
107
|
-
search_fields?: string;
|
|
108
|
-
sort_field?: string;
|
|
109
|
-
sort_order?: string;
|
|
110
|
-
icon?: string;
|
|
111
|
-
document_type?: string;
|
|
112
|
-
fields: DocFieldMeta[];
|
|
113
|
-
permissions?: DocPermissionRule[];
|
|
114
|
-
links?: DocLinkMeta[];
|
|
115
|
-
states?: Array<{ title?: string; color?: string } & Record<string, unknown>>;
|
|
116
|
-
[key: string]: unknown;
|
|
117
|
-
}
|
|
15
|
+
export type DocFieldMeta = DocTypeField;
|
|
16
|
+
export type DocLinkMeta = DocTypeLink;
|
|
17
|
+
export type DocPermissionRule = DocTypePermission;
|
|
18
|
+
export type DoctypeMeta = DocTypeMeta;
|
package/src/formEngine/index.ts
CHANGED
|
@@ -20,9 +20,17 @@
|
|
|
20
20
|
* modules directly, so importing the engine never drags SchemaForm,
|
|
21
21
|
* FrappeTable and the whole table/native stack into a node-ish consumer.
|
|
22
22
|
*/
|
|
23
|
+
import type {
|
|
24
|
+
DocTypeField,
|
|
25
|
+
DocTypeLink,
|
|
26
|
+
DocTypeMeta,
|
|
27
|
+
DocTypePermission,
|
|
28
|
+
FrappeFieldType,
|
|
29
|
+
} from "@multiplatform.one/frappe";
|
|
23
30
|
import fs from "node:fs";
|
|
24
31
|
import path from "node:path";
|
|
25
|
-
import { describe, expect, it } from "vitest";
|
|
32
|
+
import { describe, expect, expectTypeOf, it } from "vitest";
|
|
33
|
+
import type { DocFieldMeta, DocLinkMeta, DocPermissionRule, DoctypeMeta } from "./doctypeMeta";
|
|
26
34
|
import * as formEngine from "./index";
|
|
27
35
|
|
|
28
36
|
const engineDir = path.resolve(__dirname);
|
|
@@ -129,10 +137,21 @@ describe("form-engine seam (MPO-15)", () => {
|
|
|
129
137
|
}
|
|
130
138
|
});
|
|
131
139
|
|
|
132
|
-
it("
|
|
140
|
+
it("takes the one meta model from @multiplatform.one/frappe and declares none of its own", () => {
|
|
141
|
+
expectTypeOf<DocFieldMeta>().toEqualTypeOf<DocTypeField>();
|
|
142
|
+
expectTypeOf<DoctypeMeta>().toEqualTypeOf<DocTypeMeta>();
|
|
143
|
+
expectTypeOf<DocPermissionRule>().toEqualTypeOf<DocTypePermission>();
|
|
144
|
+
expectTypeOf<DocLinkMeta>().toEqualTypeOf<DocTypeLink>();
|
|
145
|
+
expectTypeOf<DocFieldMeta["fieldtype"]>().toEqualTypeOf<FrappeFieldType>();
|
|
146
|
+
|
|
133
147
|
const meta = fs.readFileSync(path.resolve(engineDir, "doctypeMeta.ts"), "utf-8");
|
|
134
|
-
|
|
135
|
-
|
|
148
|
+
expect(meta).toMatch(/from ["']@multiplatform\.one\/frappe["']/);
|
|
149
|
+
for (const file of sourceFiles(srcDir)) {
|
|
150
|
+
if (file.includes(".spec.") || file.includes(".stories.")) continue;
|
|
151
|
+
const source = fs.readFileSync(file, "utf-8");
|
|
152
|
+
expect(source, file).not.toMatch(
|
|
153
|
+
/export interface (DocFieldMeta|DoctypeMeta|DocTypeField|DocTypeMeta)\b/,
|
|
154
|
+
);
|
|
136
155
|
}
|
|
137
156
|
// The shapes reach the engine by relative import, never through the barrel.
|
|
138
157
|
for (const file of [
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session user's role list — the client-side half of desk's
|
|
3
|
+
* `frappe.user_roles` (bootinfo). Fetched once per site via the whitelisted
|
|
4
|
+
* `frappe.core.doctype.user.user.get_roles` RPC (probed:
|
|
5
|
+
* `?uid=<user>` → `{"message": ["Role", ...]}`), keyed off
|
|
6
|
+
* `frappe.auth.get_logged_user`.
|
|
7
|
+
*
|
|
8
|
+
* Undefined while loading, on a fixtures-only connection, or when the RPC
|
|
9
|
+
* fails. The permlevel matrix then takes the conservative
|
|
10
|
+
* "permlevel > 0 → read-only" rule (fieldPermissions.ts).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useEffect, useRef, useState } from "react";
|
|
14
|
+
import { createHttpClient, type FrappeConnection } from "./formActions";
|
|
15
|
+
|
|
16
|
+
const rolesCache = new Map<string, Promise<string[]>>();
|
|
17
|
+
|
|
18
|
+
function fetchSessionRoles(connection: FrappeConnection): Promise<string[]> {
|
|
19
|
+
const http = createHttpClient(connection);
|
|
20
|
+
return http
|
|
21
|
+
.get<{ message?: string }>("/api/method/frappe.auth.get_logged_user")
|
|
22
|
+
.then((res) => {
|
|
23
|
+
const user = res.payload?.message;
|
|
24
|
+
if (!user) throw new Error("No session user");
|
|
25
|
+
return http.get<{ message?: string[] }>(
|
|
26
|
+
"/api/method/frappe.core.doctype.user.user.get_roles",
|
|
27
|
+
{ uid: user },
|
|
28
|
+
);
|
|
29
|
+
})
|
|
30
|
+
.then((res) => {
|
|
31
|
+
const list = res.payload?.message;
|
|
32
|
+
if (!Array.isArray(list)) throw new Error("No roles returned");
|
|
33
|
+
return list;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A connection that may not name a bench yet (a provider's config before it resolves). */
|
|
38
|
+
type SessionConnection = Omit<FrappeConnection, "baseURL"> & { baseURL?: string };
|
|
39
|
+
|
|
40
|
+
export function useSessionRoles(connection: SessionConnection | undefined): string[] | undefined {
|
|
41
|
+
const baseURL = connection?.baseURL;
|
|
42
|
+
const fixtures = connection?.fixtures;
|
|
43
|
+
const [roles, setRoles] = useState<string[] | undefined>(undefined);
|
|
44
|
+
const connectionRef = useRef(connection);
|
|
45
|
+
connectionRef.current = connection;
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const current = connectionRef.current;
|
|
49
|
+
if (!current || fixtures || !baseURL) {
|
|
50
|
+
setRoles(undefined);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
let cancelled = false;
|
|
54
|
+
let promise = rolesCache.get(baseURL);
|
|
55
|
+
if (!promise) {
|
|
56
|
+
promise = fetchSessionRoles({ ...current, baseURL });
|
|
57
|
+
rolesCache.set(baseURL, promise);
|
|
58
|
+
promise.catch(() => rolesCache.delete(baseURL));
|
|
59
|
+
}
|
|
60
|
+
promise
|
|
61
|
+
.then((list) => {
|
|
62
|
+
if (!cancelled) setRoles(list);
|
|
63
|
+
})
|
|
64
|
+
.catch(() => {
|
|
65
|
+
if (!cancelled) setRoles(undefined);
|
|
66
|
+
});
|
|
67
|
+
return () => {
|
|
68
|
+
cancelled = true;
|
|
69
|
+
};
|
|
70
|
+
}, [baseURL, fixtures]);
|
|
71
|
+
|
|
72
|
+
return roles;
|
|
73
|
+
}
|