@webiny/app-admin 6.5.0-beta.0 → 6.5.0-beta.2

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.
Files changed (54) hide show
  1. package/base/Admin.js +2 -0
  2. package/base/Admin.js.map +1 -1
  3. package/base/Base/FieldRenderers/AutoCompleteRenderer.js +1 -0
  4. package/base/Base/FieldRenderers/AutoCompleteRenderer.js.map +1 -1
  5. package/base/Base/FieldRenderers/SelectRenderer.js +1 -0
  6. package/base/Base/FieldRenderers/SelectRenderer.js.map +1 -1
  7. package/features/debugger/Debugger.d.ts +8 -0
  8. package/features/debugger/Debugger.js +25 -0
  9. package/features/debugger/Debugger.js.map +1 -0
  10. package/features/debugger/DebuggerIndicator.d.ts +16 -0
  11. package/features/debugger/DebuggerIndicator.js +31 -0
  12. package/features/debugger/DebuggerIndicator.js.map +1 -0
  13. package/features/debugger/DebuggerStore.d.ts +55 -0
  14. package/features/debugger/DebuggerStore.js +65 -0
  15. package/features/debugger/DebuggerStore.js.map +1 -0
  16. package/features/debugger/DebuggerView.d.ts +4 -0
  17. package/features/debugger/DebuggerView.js +49 -0
  18. package/features/debugger/DebuggerView.js.map +1 -0
  19. package/features/debugger/GraphQLExtensionsDebugHandler.d.ts +15 -0
  20. package/features/debugger/GraphQLExtensionsDebugHandler.js +15 -0
  21. package/features/debugger/GraphQLExtensionsDebugHandler.js.map +1 -0
  22. package/features/debugger/debuggerLink.d.ts +12 -0
  23. package/features/debugger/debuggerLink.js +10 -0
  24. package/features/debugger/debuggerLink.js.map +1 -0
  25. package/features/debugger/feature.d.ts +1 -0
  26. package/features/debugger/feature.js +18 -0
  27. package/features/debugger/feature.js.map +1 -0
  28. package/features/debugger/index.d.ts +7 -0
  29. package/features/debugger/index.js +7 -0
  30. package/features/debugger/permissions.d.ts +68 -0
  31. package/features/debugger/permissions.js +27 -0
  32. package/features/debugger/permissions.js.map +1 -0
  33. package/features/debugger/report.d.ts +7 -0
  34. package/features/debugger/report.js +22 -0
  35. package/features/debugger/report.js.map +1 -0
  36. package/features/debugger/routes.d.ts +4 -0
  37. package/features/debugger/routes.js +10 -0
  38. package/features/debugger/routes.js.map +1 -0
  39. package/features/formModel/createFieldRenderer.js +3 -4
  40. package/features/formModel/createFieldRenderer.js.map +1 -1
  41. package/hooks/useHotkeys.js +3 -1
  42. package/hooks/useHotkeys.js.map +1 -1
  43. package/index.d.ts +1 -0
  44. package/index.js +3 -2
  45. package/package.json +19 -19
  46. package/presentation/listPresenter/FolderAwareDataSource.d.ts +5 -0
  47. package/presentation/listPresenter/FolderAwareDataSource.js +4 -3
  48. package/presentation/listPresenter/FolderAwareDataSource.js.map +1 -1
  49. package/presentation/listPresenter/QueryMatcher.d.ts +28 -5
  50. package/presentation/listPresenter/QueryMatcher.js +72 -2
  51. package/presentation/listPresenter/QueryMatcher.js.map +1 -1
  52. package/presentation/listPresenter/QueryMatcher.test.d.ts +1 -0
  53. package/presentation/listPresenter/QueryMatcher.test.js +345 -0
  54. package/presentation/listPresenter/QueryMatcher.test.js.map +1 -0
@@ -0,0 +1,27 @@
1
+ import { useAuthentication } from "../../presentation/security/hooks/useAuthentication.js";
2
+ import { createPermissionSchema, createPermissionsAbstraction, createPermissionsFeature, createUsePermissions } from "../../permissions/index.js";
3
+ const DEBUGGER_PERMISSIONS_SCHEMA = createPermissionSchema({
4
+ prefix: "dev-tools",
5
+ fullAccess: true,
6
+ entities: [
7
+ {
8
+ id: "debug",
9
+ title: "Debugger",
10
+ permission: "dev-tools.debug",
11
+ scopes: [
12
+ "full"
13
+ ]
14
+ }
15
+ ]
16
+ });
17
+ const DebuggerPermissions = createPermissionsAbstraction(DEBUGGER_PERMISSIONS_SCHEMA);
18
+ const DebuggerPermissionsFeature = createPermissionsFeature(DEBUGGER_PERMISSIONS_SCHEMA, DebuggerPermissions);
19
+ const useDebuggerPermissions = createUsePermissions(DebuggerPermissions);
20
+ const DEBUG_PERMISSION_NAME = "dev-tools.debug";
21
+ const useCanCaptureDebugData = ()=>{
22
+ const { identity } = useAuthentication();
23
+ return Boolean(identity.getPermission(DEBUG_PERMISSION_NAME, true));
24
+ };
25
+ export { DEBUGGER_PERMISSIONS_SCHEMA, DEBUG_PERMISSION_NAME, DebuggerPermissions, DebuggerPermissionsFeature, useCanCaptureDebugData, useDebuggerPermissions };
26
+
27
+ //# sourceMappingURL=permissions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"features/debugger/permissions.js","sources":["../../../src/features/debugger/permissions.ts"],"sourcesContent":["import { useAuthentication } from \"~/presentation/security/hooks/useAuthentication.js\";\nimport {\n createPermissionSchema,\n createPermissionsAbstraction,\n createPermissionsFeature,\n createUsePermissions\n} from \"~/permissions/index.js\";\n\n/**\n * Registered under its own name rather than joining the `dev-tools` group declared by the\n * playgrounds: `Security.Permissions` is keyed by name, so a second registration using the same name\n * would replace theirs and hide their entities from the role editor.\n *\n * The permission itself is `dev-tools.debug`, so it still sits under the Dev Tools prefix - and note\n * the consequence: `dev-tools.*` held by a Dev Tools full-access role matches it.\n */\nexport const DEBUGGER_PERMISSIONS_SCHEMA = createPermissionSchema({\n prefix: \"dev-tools\",\n fullAccess: true,\n entities: [\n {\n id: \"debug\",\n title: \"Debugger\",\n permission: \"dev-tools.debug\",\n scopes: [\"full\"]\n }\n ]\n});\n\nexport const DebuggerPermissions = createPermissionsAbstraction(DEBUGGER_PERMISSIONS_SCHEMA);\n\nexport const DebuggerPermissionsFeature = createPermissionsFeature(\n DEBUGGER_PERMISSIONS_SCHEMA,\n DebuggerPermissions\n);\n\nexport const useDebuggerPermissions = createUsePermissions(DebuggerPermissions);\n\nexport const DEBUG_PERMISSION_NAME = \"dev-tools.debug\";\n\n/**\n * Whether the current identity may capture debug data.\n *\n * An exact match, deliberately: `*` and `dev-tools.*` do not grant it. Capture runs automatically\n * for whoever holds this, so a wildcard would leave it permanently on for every full-access\n * administrator, collecting record content on every request for people who never asked for it.\n *\n * Full access is therefore not enough. The entity has to be granted deliberately, and that act is\n * the opt-in.\n */\nexport const useCanCaptureDebugData = (): boolean => {\n const { identity } = useAuthentication();\n return Boolean(identity.getPermission(DEBUG_PERMISSION_NAME, true));\n};\n"],"names":["DEBUGGER_PERMISSIONS_SCHEMA","createPermissionSchema","DebuggerPermissions","createPermissionsAbstraction","DebuggerPermissionsFeature","createPermissionsFeature","useDebuggerPermissions","createUsePermissions","DEBUG_PERMISSION_NAME","useCanCaptureDebugData","identity","useAuthentication","Boolean"],"mappings":";;AAgBO,MAAMA,8BAA8BC,uBAAuB;IAC9D,QAAQ;IACR,YAAY;IACZ,UAAU;QACN;YACI,IAAI;YACJ,OAAO;YACP,YAAY;YACZ,QAAQ;gBAAC;aAAO;QACpB;KACH;AACL;AAEO,MAAMC,sBAAsBC,6BAA6BH;AAEzD,MAAMI,6BAA6BC,yBACtCL,6BACAE;AAGG,MAAMI,yBAAyBC,qBAAqBL;AAEpD,MAAMM,wBAAwB;AAY9B,MAAMC,yBAAyB;IAClC,MAAM,EAAEC,QAAQ,EAAE,GAAGC;IACrB,OAAOC,QAAQF,SAAS,aAAa,CAACF,uBAAuB;AACjE"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Hands the collected sessions to the user as a file.
3
+ *
4
+ * Nothing is uploaded anywhere. The report leaves the browser only when the person chooses to send
5
+ * it, which is the moment they decide this data may leave their organisation.
6
+ */
7
+ export declare const downloadReport: () => void;
@@ -0,0 +1,22 @@
1
+ import { debuggerStore } from "./DebuggerStore.js";
2
+ const buildReport = ()=>({
3
+ generatedAt: new Date().toISOString(),
4
+ namespaces: debuggerStore.namespaces,
5
+ sessions: debuggerStore.sessions
6
+ });
7
+ const downloadReport = ()=>{
8
+ const blob = new Blob([
9
+ JSON.stringify(buildReport(), null, 2)
10
+ ], {
11
+ type: "application/json"
12
+ });
13
+ const url = URL.createObjectURL(blob);
14
+ const link = document.createElement("a");
15
+ link.href = url;
16
+ link.download = `webiny-debug-${new Date().toISOString()}.json`;
17
+ link.click();
18
+ URL.revokeObjectURL(url);
19
+ };
20
+ export { downloadReport };
21
+
22
+ //# sourceMappingURL=report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"features/debugger/report.js","sources":["../../../src/features/debugger/report.ts"],"sourcesContent":["import { debuggerStore } from \"./DebuggerStore.js\";\n\nconst buildReport = () => {\n return {\n generatedAt: new Date().toISOString(),\n namespaces: debuggerStore.namespaces,\n sessions: debuggerStore.sessions\n };\n};\n\n/**\n * Hands the collected sessions to the user as a file.\n *\n * Nothing is uploaded anywhere. The report leaves the browser only when the person chooses to send\n * it, which is the moment they decide this data may leave their organisation.\n */\nexport const downloadReport = () => {\n const blob = new Blob([JSON.stringify(buildReport(), null, 2)], {\n type: \"application/json\"\n });\n const url = URL.createObjectURL(blob);\n\n const link = document.createElement(\"a\");\n link.href = url;\n link.download = `webiny-debug-${new Date().toISOString()}.json`;\n link.click();\n\n URL.revokeObjectURL(url);\n};\n"],"names":["buildReport","Date","debuggerStore","downloadReport","blob","Blob","JSON","url","URL","link","document"],"mappings":";AAEA,MAAMA,cAAc,IACT;QACH,aAAa,IAAIC,OAAO,WAAW;QACnC,YAAYC,cAAc,UAAU;QACpC,UAAUA,cAAc,QAAQ;IACpC;AASG,MAAMC,iBAAiB;IAC1B,MAAMC,OAAO,IAAIC,KAAK;QAACC,KAAK,SAAS,CAACN,eAAe,MAAM;KAAG,EAAE;QAC5D,MAAM;IACV;IACA,MAAMO,MAAMC,IAAI,eAAe,CAACJ;IAEhC,MAAMK,OAAOC,SAAS,aAAa,CAAC;IACpCD,KAAK,IAAI,GAAGF;IACZE,KAAK,QAAQ,GAAG,CAAC,aAAa,EAAE,IAAIR,OAAO,WAAW,GAAG,KAAK,CAAC;IAC/DQ,KAAK,KAAK;IAEVD,IAAI,eAAe,CAACD;AACxB"}
@@ -0,0 +1,4 @@
1
+ import { Route } from "@webiny/app";
2
+ export declare const DebuggerRoutes: {
3
+ Debugger: Route<undefined>;
4
+ };
@@ -0,0 +1,10 @@
1
+ import { Route } from "@webiny/app";
2
+ const DebuggerRoutes = {
3
+ Debugger: new Route({
4
+ name: "Debugger",
5
+ path: "/debugger"
6
+ })
7
+ };
8
+ export { DebuggerRoutes };
9
+
10
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"features/debugger/routes.js","sources":["../../../src/features/debugger/routes.ts"],"sourcesContent":["import { Route } from \"@webiny/app\";\n\nexport const DebuggerRoutes = {\n Debugger: new Route({\n name: \"Debugger\",\n path: \"/debugger\"\n })\n};\n"],"names":["DebuggerRoutes","Route"],"mappings":";AAEO,MAAMA,iBAAiB;IAC1B,UAAU,IAAIC,MAAM;QAChB,MAAM;QACN,MAAM;IACV;AACJ"}
@@ -3,6 +3,7 @@ import { observer } from "mobx-react-lite";
3
3
  import { makeDecoratable } from "@webiny/react-composition";
4
4
  const FormFieldWrapper = makeDecoratable("FormFieldWrapper", ({ children })=>/*#__PURE__*/ react.createElement(react.Fragment, null, children));
5
5
  const FOCUS_HIGHLIGHT_MS = 1800;
6
+ const FOCUS_HIGHLIGHT_SHADOW = "0 0 0 6px var(--color-primary-100), 0 0 0 8px var(--color-primary)";
6
7
  const ScrollOnFocus = observer(({ field, children })=>{
7
8
  const ref = useRef(null);
8
9
  const [highlighted, setHighlighted] = useState(false);
@@ -30,11 +31,9 @@ const ScrollOnFocus = observer(({ field, children })=>{
30
31
  ref: ref,
31
32
  "data-field-path": field.qualifiedName,
32
33
  style: {
33
- padding: 6,
34
- margin: -6,
35
- borderRadius: 8,
34
+ borderRadius: 2,
36
35
  transition: "box-shadow .35s ease, background-color .35s ease",
37
- boxShadow: highlighted ? "0 0 0 2px var(--color-primary)" : void 0,
36
+ boxShadow: highlighted ? FOCUS_HIGHLIGHT_SHADOW : void 0,
38
37
  backgroundColor: highlighted ? "var(--color-primary-100)" : void 0
39
38
  }
40
39
  }, /*#__PURE__*/ react.createElement(FormFieldWrapper, {
@@ -1 +1 @@
1
- {"version":3,"file":"features/formModel/createFieldRenderer.js","sources":["../../../src/features/formModel/createFieldRenderer.tsx"],"sourcesContent":["import React, { useEffect, useRef, useState } from \"react\";\nimport { observer } from \"mobx-react-lite\";\nimport { makeDecoratable } from \"@webiny/react-composition\";\nimport type { IFieldVM, IObjectFieldVM, FieldRendererSettings } from \"./abstractions.js\";\n\n/**\n * Decoratable wrapper rendered around every leaf field's content. Features can decorate it to\n * inject per-field UI (e.g. a comment marker) keyed on `field.qualifiedName`, without forking\n * the form. By default it renders the field content unchanged.\n */\nexport const FormFieldWrapper = makeDecoratable(\n \"FormFieldWrapper\",\n ({ children }: { field: IFieldVM; children?: React.ReactNode }) => {\n return <>{children}</>;\n }\n);\n\ntype RendererField<TName extends string> = IFieldVM & {\n rendererSettings: FieldRendererSettings<TName>;\n};\n\ntype ObjectRendererField<TName extends string> = IObjectFieldVM & {\n rendererSettings: FieldRendererSettings<TName>;\n};\n\ntype FieldRendererFn<TName extends string> = (props: {\n field: RendererField<TName>;\n}) => React.ReactNode;\n\n// How long the field stays visually highlighted after focus lands, so the user's eye is drawn\n// to where \"jump to field\" (a comment locator, a form error) scrolled to.\nconst FOCUS_HIGHLIGHT_MS = 1800;\n\nconst ScrollOnFocus = observer(\n ({ field, children }: { field: IFieldVM; children: React.ReactNode }) => {\n const ref = useRef<HTMLDivElement>(null);\n const [highlighted, setHighlighted] = useState(false);\n const highlightTimer = useRef<number | null>(null);\n\n useEffect(() => {\n if (field.focusRequested && ref.current) {\n setTimeout(() => {\n ref.current?.scrollIntoView({ behavior: \"smooth\", block: \"center\" });\n }, 150);\n // Flash a highlight ring so it's obvious where focus landed. The timer is kept in a\n // ref (not tied to the effect cleanup) so clearing the focus request below — which\n // re-runs this effect — doesn't cut the highlight short.\n setHighlighted(true);\n if (highlightTimer.current) {\n window.clearTimeout(highlightTimer.current);\n }\n highlightTimer.current = window.setTimeout(\n () => setHighlighted(false),\n FOCUS_HIGHLIGHT_MS\n );\n }\n field.clearFocusRequest();\n }, [field.focusRequested]);\n\n useEffect(() => {\n return () => {\n if (highlightTimer.current) {\n window.clearTimeout(highlightTimer.current);\n }\n };\n }, []);\n\n return (\n <div\n ref={ref}\n data-field-path={field.qualifiedName}\n style={{\n // Padding gives the highlight ring/tint breathing room from the field content;\n // the matching negative margin keeps the field's layout position unchanged.\n padding: 6,\n margin: -6,\n borderRadius: 8,\n transition: \"box-shadow .35s ease, background-color .35s ease\",\n boxShadow: highlighted ? \"0 0 0 2px var(--color-primary)\" : undefined,\n backgroundColor: highlighted ? \"var(--color-primary-100)\" : undefined\n }}\n >\n <FormFieldWrapper field={field}>{children}</FormFieldWrapper>\n </div>\n );\n }\n);\n\nexport function createFieldRenderer<TName extends string = string>(\n render: FieldRendererFn<TName>\n): React.ComponentType<{ field: IFieldVM }> {\n const Inner = observer(render as FieldRendererFn<string>) as unknown as React.ComponentType<{\n field: IFieldVM;\n }>;\n\n const Wrapper = observer(({ field }: { field: IFieldVM }) => (\n <ScrollOnFocus field={field}>\n <Inner field={field} />\n </ScrollOnFocus>\n ));\n\n return Wrapper as unknown as React.ComponentType<{ field: IFieldVM }>;\n}\n\nexport function createObjectFieldRenderer<TName extends string = string>(\n render: (props: { field: ObjectRendererField<TName> }) => React.ReactNode\n): React.ComponentType<{ field: IFieldVM }> {\n const Wrapped = (props: { field: IFieldVM }) => {\n if (props.field.type !== \"object\") {\n return null;\n }\n return (render as (p: { field: IFieldVM }) => React.ReactNode)(props);\n };\n return observer(Wrapped) as unknown as React.ComponentType<{ field: IFieldVM }>;\n}\n"],"names":["FormFieldWrapper","makeDecoratable","children","FOCUS_HIGHLIGHT_MS","ScrollOnFocus","observer","field","ref","useRef","highlighted","setHighlighted","useState","highlightTimer","useEffect","setTimeout","window","undefined","createFieldRenderer","render","Inner","Wrapper","createObjectFieldRenderer","Wrapped","props"],"mappings":";;;AAUO,MAAMA,mBAAmBC,gBAC5B,oBACA,CAAC,EAAEC,QAAQ,EAAmD,GACnD,WAAP,GAAO,0CAAGA;AAkBlB,MAAMC,qBAAqB;AAE3B,MAAMC,gBAAgBC,SAClB,CAAC,EAAEC,KAAK,EAAEJ,QAAQ,EAAkD;IAChE,MAAMK,MAAMC,OAAuB;IACnC,MAAM,CAACC,aAAaC,eAAe,GAAGC,SAAS;IAC/C,MAAMC,iBAAiBJ,OAAsB;IAE7CK,UAAU;QACN,IAAIP,MAAM,cAAc,IAAIC,IAAI,OAAO,EAAE;YACrCO,WAAW;gBACPP,IAAI,OAAO,EAAE,eAAe;oBAAE,UAAU;oBAAU,OAAO;gBAAS;YACtE,GAAG;YAIHG,eAAe;YACf,IAAIE,eAAe,OAAO,EACtBG,OAAO,YAAY,CAACH,eAAe,OAAO;YAE9CA,eAAe,OAAO,GAAGG,OAAO,UAAU,CACtC,IAAML,eAAe,QACrBP;QAER;QACAG,MAAM,iBAAiB;IAC3B,GAAG;QAACA,MAAM,cAAc;KAAC;IAEzBO,UAAU,IACC;YACH,IAAID,eAAe,OAAO,EACtBG,OAAO,YAAY,CAACH,eAAe,OAAO;QAElD,GACD,EAAE;IAEL,OAAO,WAAP,GACI,oBAAC;QACG,KAAKL;QACL,mBAAiBD,MAAM,aAAa;QACpC,OAAO;YAGH,SAAS;YACT,QAAQ;YACR,cAAc;YACd,YAAY;YACZ,WAAWG,cAAc,mCAAmCO;YAC5D,iBAAiBP,cAAc,6BAA6BO;QAChE;qBAEA,oBAAChB,kBAAgBA;QAAC,OAAOM;OAAQJ;AAG7C;AAGG,SAASe,oBACZC,MAA8B;IAE9B,MAAMC,QAAQd,SAASa;IAIvB,MAAME,UAAUf,SAAS,CAAC,EAAEC,KAAK,EAAuB,iBACpD,oBAACF,eAAaA;YAAC,OAAOE;yBAClB,oBAACa,OAAAA;YAAM,OAAOb;;IAItB,OAAOc;AACX;AAEO,SAASC,0BACZH,MAAyE;IAEzE,MAAMI,UAAU,CAACC;QACb,IAAIA,AAAqB,aAArBA,MAAM,KAAK,CAAC,IAAI,EAChB,OAAO;QAEX,OAAQL,OAAuDK;IACnE;IACA,OAAOlB,SAASiB;AACpB"}
1
+ {"version":3,"file":"features/formModel/createFieldRenderer.js","sources":["../../../src/features/formModel/createFieldRenderer.tsx"],"sourcesContent":["import React, { useEffect, useRef, useState } from \"react\";\nimport { observer } from \"mobx-react-lite\";\nimport { makeDecoratable } from \"@webiny/react-composition\";\nimport type { IFieldVM, IObjectFieldVM, FieldRendererSettings } from \"./abstractions.js\";\n\n/**\n * Decoratable wrapper rendered around every leaf field's content. Features can decorate it to\n * inject per-field UI (e.g. a comment marker) keyed on `field.qualifiedName`, without forking\n * the form. By default it renders the field content unchanged.\n */\nexport const FormFieldWrapper = makeDecoratable(\n \"FormFieldWrapper\",\n ({ children }: { field: IFieldVM; children?: React.ReactNode }) => {\n return <>{children}</>;\n }\n);\n\ntype RendererField<TName extends string> = IFieldVM & {\n rendererSettings: FieldRendererSettings<TName>;\n};\n\ntype ObjectRendererField<TName extends string> = IObjectFieldVM & {\n rendererSettings: FieldRendererSettings<TName>;\n};\n\ntype FieldRendererFn<TName extends string> = (props: {\n field: RendererField<TName>;\n}) => React.ReactNode;\n\n// How long the field stays visually highlighted after focus lands, so the user's eye is drawn\n// to where \"jump to field\" (a comment locator, a form error) scrolled to.\nconst FOCUS_HIGHLIGHT_MS = 1800;\n\n// A 6px tint around the field, then a 2px ring outside it.\nconst FOCUS_HIGHLIGHT_SHADOW = \"0 0 0 6px var(--color-primary-100), 0 0 0 8px var(--color-primary)\";\n\nconst ScrollOnFocus = observer(\n ({ field, children }: { field: IFieldVM; children: React.ReactNode }) => {\n const ref = useRef<HTMLDivElement>(null);\n const [highlighted, setHighlighted] = useState(false);\n const highlightTimer = useRef<number | null>(null);\n\n useEffect(() => {\n if (field.focusRequested && ref.current) {\n setTimeout(() => {\n ref.current?.scrollIntoView({ behavior: \"smooth\", block: \"center\" });\n }, 150);\n // Flash a highlight ring so it's obvious where focus landed. The timer is kept in a\n // ref (not tied to the effect cleanup) so clearing the focus request below — which\n // re-runs this effect — doesn't cut the highlight short.\n setHighlighted(true);\n if (highlightTimer.current) {\n window.clearTimeout(highlightTimer.current);\n }\n highlightTimer.current = window.setTimeout(\n () => setHighlighted(false),\n FOCUS_HIGHLIGHT_MS\n );\n }\n field.clearFocusRequest();\n }, [field.focusRequested]);\n\n useEffect(() => {\n return () => {\n if (highlightTimer.current) {\n window.clearTimeout(highlightTimer.current);\n }\n };\n }, []);\n\n return (\n <div\n ref={ref}\n data-field-path={field.qualifiedName}\n style={{\n /*\n * The highlight is drawn with shadows, which paint around the field without\n * taking up room. Padding with a matching negative margin looked the same, but\n * the padded box still stuck out of the field, so a scroll area around a form\n * always had 6px to scroll.\n */\n borderRadius: 2,\n transition: \"box-shadow .35s ease, background-color .35s ease\",\n boxShadow: highlighted ? FOCUS_HIGHLIGHT_SHADOW : undefined,\n backgroundColor: highlighted ? \"var(--color-primary-100)\" : undefined\n }}\n >\n <FormFieldWrapper field={field}>{children}</FormFieldWrapper>\n </div>\n );\n }\n);\n\nexport function createFieldRenderer<TName extends string = string>(\n render: FieldRendererFn<TName>\n): React.ComponentType<{ field: IFieldVM }> {\n const Inner = observer(render as FieldRendererFn<string>) as unknown as React.ComponentType<{\n field: IFieldVM;\n }>;\n\n const Wrapper = observer(({ field }: { field: IFieldVM }) => (\n <ScrollOnFocus field={field}>\n <Inner field={field} />\n </ScrollOnFocus>\n ));\n\n return Wrapper as unknown as React.ComponentType<{ field: IFieldVM }>;\n}\n\nexport function createObjectFieldRenderer<TName extends string = string>(\n render: (props: { field: ObjectRendererField<TName> }) => React.ReactNode\n): React.ComponentType<{ field: IFieldVM }> {\n const Wrapped = (props: { field: IFieldVM }) => {\n if (props.field.type !== \"object\") {\n return null;\n }\n return (render as (p: { field: IFieldVM }) => React.ReactNode)(props);\n };\n return observer(Wrapped) as unknown as React.ComponentType<{ field: IFieldVM }>;\n}\n"],"names":["FormFieldWrapper","makeDecoratable","children","FOCUS_HIGHLIGHT_MS","FOCUS_HIGHLIGHT_SHADOW","ScrollOnFocus","observer","field","ref","useRef","highlighted","setHighlighted","useState","highlightTimer","useEffect","setTimeout","window","undefined","createFieldRenderer","render","Inner","Wrapper","createObjectFieldRenderer","Wrapped","props"],"mappings":";;;AAUO,MAAMA,mBAAmBC,gBAC5B,oBACA,CAAC,EAAEC,QAAQ,EAAmD,GACnD,WAAP,GAAO,0CAAGA;AAkBlB,MAAMC,qBAAqB;AAG3B,MAAMC,yBAAyB;AAE/B,MAAMC,gBAAgBC,SAClB,CAAC,EAAEC,KAAK,EAAEL,QAAQ,EAAkD;IAChE,MAAMM,MAAMC,OAAuB;IACnC,MAAM,CAACC,aAAaC,eAAe,GAAGC,SAAS;IAC/C,MAAMC,iBAAiBJ,OAAsB;IAE7CK,UAAU;QACN,IAAIP,MAAM,cAAc,IAAIC,IAAI,OAAO,EAAE;YACrCO,WAAW;gBACPP,IAAI,OAAO,EAAE,eAAe;oBAAE,UAAU;oBAAU,OAAO;gBAAS;YACtE,GAAG;YAIHG,eAAe;YACf,IAAIE,eAAe,OAAO,EACtBG,OAAO,YAAY,CAACH,eAAe,OAAO;YAE9CA,eAAe,OAAO,GAAGG,OAAO,UAAU,CACtC,IAAML,eAAe,QACrBR;QAER;QACAI,MAAM,iBAAiB;IAC3B,GAAG;QAACA,MAAM,cAAc;KAAC;IAEzBO,UAAU,IACC;YACH,IAAID,eAAe,OAAO,EACtBG,OAAO,YAAY,CAACH,eAAe,OAAO;QAElD,GACD,EAAE;IAEL,OAAO,WAAP,GACI,oBAAC;QACG,KAAKL;QACL,mBAAiBD,MAAM,aAAa;QACpC,OAAO;YAOH,cAAc;YACd,YAAY;YACZ,WAAWG,cAAcN,yBAAyBa;YAClD,iBAAiBP,cAAc,6BAA6BO;QAChE;qBAEA,oBAACjB,kBAAgBA;QAAC,OAAOO;OAAQL;AAG7C;AAGG,SAASgB,oBACZC,MAA8B;IAE9B,MAAMC,QAAQd,SAASa;IAIvB,MAAME,UAAUf,SAAS,CAAC,EAAEC,KAAK,EAAuB,iBACpD,oBAACF,eAAaA;YAAC,OAAOE;yBAClB,oBAACa,OAAAA;YAAM,OAAOb;;IAItB,OAAOc;AACX;AAEO,SAASC,0BACZH,MAAyE;IAEzE,MAAMI,UAAU,CAACC;QACb,IAAIA,AAAqB,aAArBA,MAAM,KAAK,CAAC,IAAI,EAChB,OAAO;QAEX,OAAQL,OAAuDK;IACnE;IACA,OAAOlB,SAASiB;AACpB"}
@@ -44,6 +44,8 @@ function useHotkeys(props) {
44
44
  const { disabled, zIndex, keys } = props;
45
45
  const prevPropsRef = useRef();
46
46
  const firstRenderRef = useRef(true);
47
+ const propsRef = useRef(props);
48
+ propsRef.current = props;
47
49
  useEffect(function() {
48
50
  if (firstRenderRef.current || prevPropsRef.current?.disabled !== disabled) {
49
51
  firstRenderRef.current = false;
@@ -59,7 +61,7 @@ function useHotkeys(props) {
59
61
  ]);
60
62
  useEffect(function() {
61
63
  return function() {
62
- if (!disabled) unregisterZIndex(props);
64
+ if (!propsRef.current.disabled) unregisterZIndex(propsRef.current);
63
65
  };
64
66
  }, []);
65
67
  }
@@ -1 +1 @@
1
- {"version":3,"file":"hooks/useHotkeys.js","sources":["../../src/hooks/useHotkeys.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\nimport { isHotkey } from \"is-hotkey\";\n\ntype HookProps = {\n disabled?: boolean;\n zIndex: number;\n keys?: { [key: string]: (e: KeyboardEvent) => void };\n};\n\ntype State = {\n listenerAttached: boolean;\n zIndex: null | number;\n handlers: { [zIndex: number]: { [key: string]: (e: KeyboardEvent) => void } };\n};\n\nconst state: State = {\n listenerAttached: false,\n zIndex: null,\n handlers: {}\n};\n\nfunction triggerHotkeys(e: KeyboardEvent) {\n if (state.zIndex === null) {\n return;\n }\n\n const keys = state.handlers[state.zIndex];\n for (const key in keys) {\n if (isHotkey(key, e)) {\n keys[key](e);\n break;\n }\n }\n}\n\nfunction registerZIndex({ zIndex, keys }: HookProps) {\n if (state.zIndex === null || state.zIndex < zIndex) {\n state.zIndex = zIndex;\n }\n\n if (!state.handlers[zIndex]) {\n state.handlers[zIndex] = {};\n }\n\n if (!keys || Object.keys(keys).length === 0) {\n return;\n }\n\n for (const key in keys) {\n if (key in state.handlers[zIndex]) {\n throw Error(`Shortcut \"${key}\" already registered on zIndex ${zIndex}.`);\n }\n state.handlers[zIndex][key] = keys[key];\n }\n\n if (!state.listenerAttached) {\n document.body.addEventListener(\"keydown\", triggerHotkeys);\n state.listenerAttached = true;\n }\n}\n\nfunction unregisterZIndex({ zIndex, keys }: HookProps) {\n if (state.handlers && state.handlers[zIndex]) {\n for (const key in keys) {\n delete state.handlers[zIndex][key];\n }\n\n if (Object.keys(state.handlers[zIndex]).length === 0) {\n delete state.handlers[zIndex];\n }\n }\n\n if (Object.keys(state.handlers).length > 0) {\n state.zIndex = Math.max(...Object.keys(state.handlers).map(Number));\n } else {\n state.zIndex = null;\n\n if (state.listenerAttached) {\n document.body.removeEventListener(\"keydown\", triggerHotkeys);\n state.listenerAttached = false;\n }\n }\n}\n\nexport function useHotkeys(props: HookProps) {\n const { disabled, zIndex, keys } = props;\n\n const prevPropsRef = useRef<HookProps | undefined>();\n const firstRenderRef = useRef(true);\n\n useEffect(\n function () {\n if (firstRenderRef.current || prevPropsRef.current?.disabled !== disabled) {\n firstRenderRef.current = false;\n if (disabled) {\n unregisterZIndex(props);\n } else {\n registerZIndex(props);\n }\n }\n\n if (!disabled && typeof keys === \"object\") {\n Object.assign(state.handlers[zIndex], keys);\n }\n prevPropsRef.current = { ...props };\n },\n [keys]\n );\n\n useEffect(function () {\n return function () {\n if (!disabled) {\n unregisterZIndex(props);\n }\n };\n }, []);\n}\n"],"names":["state","triggerHotkeys","e","keys","key","isHotkey","registerZIndex","zIndex","Object","Error","document","unregisterZIndex","Math","Number","useHotkeys","props","disabled","prevPropsRef","useRef","firstRenderRef","useEffect"],"mappings":";;AAeA,MAAMA,QAAe;IACjB,kBAAkB;IAClB,QAAQ;IACR,UAAU,CAAC;AACf;AAEA,SAASC,eAAeC,CAAgB;IACpC,IAAIF,AAAiB,SAAjBA,MAAM,MAAM,EACZ;IAGJ,MAAMG,OAAOH,MAAM,QAAQ,CAACA,MAAM,MAAM,CAAC;IACzC,IAAK,MAAMI,OAAOD,KACd,IAAIE,SAASD,KAAKF,IAAI;QAClBC,IAAI,CAACC,IAAI,CAACF;QACV;IACJ;AAER;AAEA,SAASI,eAAe,EAAEC,MAAM,EAAEJ,IAAI,EAAa;IAC/C,IAAIH,AAAiB,SAAjBA,MAAM,MAAM,IAAaA,MAAM,MAAM,GAAGO,QACxCP,MAAM,MAAM,GAAGO;IAGnB,IAAI,CAACP,MAAM,QAAQ,CAACO,OAAO,EACvBP,MAAM,QAAQ,CAACO,OAAO,GAAG,CAAC;IAG9B,IAAI,CAACJ,QAAQK,AAA6B,MAA7BA,OAAO,IAAI,CAACL,MAAM,MAAM,EACjC;IAGJ,IAAK,MAAMC,OAAOD,KAAM;QACpB,IAAIC,OAAOJ,MAAM,QAAQ,CAACO,OAAO,EAC7B,MAAME,MAAM,CAAC,UAAU,EAAEL,IAAI,+BAA+B,EAAEG,OAAO,CAAC,CAAC;QAE3EP,MAAM,QAAQ,CAACO,OAAO,CAACH,IAAI,GAAGD,IAAI,CAACC,IAAI;IAC3C;IAEA,IAAI,CAACJ,MAAM,gBAAgB,EAAE;QACzBU,SAAS,IAAI,CAAC,gBAAgB,CAAC,WAAWT;QAC1CD,MAAM,gBAAgB,GAAG;IAC7B;AACJ;AAEA,SAASW,iBAAiB,EAAEJ,MAAM,EAAEJ,IAAI,EAAa;IACjD,IAAIH,MAAM,QAAQ,IAAIA,MAAM,QAAQ,CAACO,OAAO,EAAE;QAC1C,IAAK,MAAMH,OAAOD,KACd,OAAOH,MAAM,QAAQ,CAACO,OAAO,CAACH,IAAI;QAGtC,IAAII,AAA+C,MAA/CA,OAAO,IAAI,CAACR,MAAM,QAAQ,CAACO,OAAO,EAAE,MAAM,EAC1C,OAAOP,MAAM,QAAQ,CAACO,OAAO;IAErC;IAEA,IAAIC,OAAO,IAAI,CAACR,MAAM,QAAQ,EAAE,MAAM,GAAG,GACrCA,MAAM,MAAM,GAAGY,KAAK,GAAG,IAAIJ,OAAO,IAAI,CAACR,MAAM,QAAQ,EAAE,GAAG,CAACa;SACxD;QACHb,MAAM,MAAM,GAAG;QAEf,IAAIA,MAAM,gBAAgB,EAAE;YACxBU,SAAS,IAAI,CAAC,mBAAmB,CAAC,WAAWT;YAC7CD,MAAM,gBAAgB,GAAG;QAC7B;IACJ;AACJ;AAEO,SAASc,WAAWC,KAAgB;IACvC,MAAM,EAAEC,QAAQ,EAAET,MAAM,EAAEJ,IAAI,EAAE,GAAGY;IAEnC,MAAME,eAAeC;IACrB,MAAMC,iBAAiBD,OAAO;IAE9BE,UACI;QACI,IAAID,eAAe,OAAO,IAAIF,aAAa,OAAO,EAAE,aAAaD,UAAU;YACvEG,eAAe,OAAO,GAAG;YACzB,IAAIH,UACAL,iBAAiBI;iBAEjBT,eAAeS;QAEvB;QAEA,IAAI,CAACC,YAAY,AAAgB,YAAhB,OAAOb,MACpBK,OAAO,MAAM,CAACR,MAAM,QAAQ,CAACO,OAAO,EAAEJ;QAE1Cc,aAAa,OAAO,GAAG;YAAE,GAAGF,KAAK;QAAC;IACtC,GACA;QAACZ;KAAK;IAGViB,UAAU;QACN,OAAO;YACH,IAAI,CAACJ,UACDL,iBAAiBI;QAEzB;IACJ,GAAG,EAAE;AACT"}
1
+ {"version":3,"file":"hooks/useHotkeys.js","sources":["../../src/hooks/useHotkeys.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\nimport { isHotkey } from \"is-hotkey\";\n\ntype HookProps = {\n disabled?: boolean;\n zIndex: number;\n keys?: { [key: string]: (e: KeyboardEvent) => void };\n};\n\ntype State = {\n listenerAttached: boolean;\n zIndex: null | number;\n handlers: { [zIndex: number]: { [key: string]: (e: KeyboardEvent) => void } };\n};\n\nconst state: State = {\n listenerAttached: false,\n zIndex: null,\n handlers: {}\n};\n\nfunction triggerHotkeys(e: KeyboardEvent) {\n if (state.zIndex === null) {\n return;\n }\n\n const keys = state.handlers[state.zIndex];\n for (const key in keys) {\n if (isHotkey(key, e)) {\n keys[key](e);\n break;\n }\n }\n}\n\nfunction registerZIndex({ zIndex, keys }: HookProps) {\n if (state.zIndex === null || state.zIndex < zIndex) {\n state.zIndex = zIndex;\n }\n\n if (!state.handlers[zIndex]) {\n state.handlers[zIndex] = {};\n }\n\n if (!keys || Object.keys(keys).length === 0) {\n return;\n }\n\n for (const key in keys) {\n if (key in state.handlers[zIndex]) {\n throw Error(`Shortcut \"${key}\" already registered on zIndex ${zIndex}.`);\n }\n state.handlers[zIndex][key] = keys[key];\n }\n\n if (!state.listenerAttached) {\n document.body.addEventListener(\"keydown\", triggerHotkeys);\n state.listenerAttached = true;\n }\n}\n\nfunction unregisterZIndex({ zIndex, keys }: HookProps) {\n if (state.handlers && state.handlers[zIndex]) {\n for (const key in keys) {\n delete state.handlers[zIndex][key];\n }\n\n if (Object.keys(state.handlers[zIndex]).length === 0) {\n delete state.handlers[zIndex];\n }\n }\n\n if (Object.keys(state.handlers).length > 0) {\n state.zIndex = Math.max(...Object.keys(state.handlers).map(Number));\n } else {\n state.zIndex = null;\n\n if (state.listenerAttached) {\n document.body.removeEventListener(\"keydown\", triggerHotkeys);\n state.listenerAttached = false;\n }\n }\n}\n\nexport function useHotkeys(props: HookProps) {\n const { disabled, zIndex, keys } = props;\n\n const prevPropsRef = useRef<HookProps | undefined>();\n const firstRenderRef = useRef(true);\n\n // The unmount cleanup below runs once, so it must not close over the props of the first\n // render. Without this, a component unmounted while its shortcuts are registered (for\n // example, an open drawer navigating away) would leak them into the shared registry, and\n // the next component to register the same shortcut would throw.\n const propsRef = useRef(props);\n propsRef.current = props;\n\n useEffect(\n function () {\n if (firstRenderRef.current || prevPropsRef.current?.disabled !== disabled) {\n firstRenderRef.current = false;\n if (disabled) {\n unregisterZIndex(props);\n } else {\n registerZIndex(props);\n }\n }\n\n if (!disabled && typeof keys === \"object\") {\n Object.assign(state.handlers[zIndex], keys);\n }\n prevPropsRef.current = { ...props };\n },\n [keys]\n );\n\n useEffect(function () {\n return function () {\n if (!propsRef.current.disabled) {\n unregisterZIndex(propsRef.current);\n }\n };\n }, []);\n}\n"],"names":["state","triggerHotkeys","e","keys","key","isHotkey","registerZIndex","zIndex","Object","Error","document","unregisterZIndex","Math","Number","useHotkeys","props","disabled","prevPropsRef","useRef","firstRenderRef","propsRef","useEffect"],"mappings":";;AAeA,MAAMA,QAAe;IACjB,kBAAkB;IAClB,QAAQ;IACR,UAAU,CAAC;AACf;AAEA,SAASC,eAAeC,CAAgB;IACpC,IAAIF,AAAiB,SAAjBA,MAAM,MAAM,EACZ;IAGJ,MAAMG,OAAOH,MAAM,QAAQ,CAACA,MAAM,MAAM,CAAC;IACzC,IAAK,MAAMI,OAAOD,KACd,IAAIE,SAASD,KAAKF,IAAI;QAClBC,IAAI,CAACC,IAAI,CAACF;QACV;IACJ;AAER;AAEA,SAASI,eAAe,EAAEC,MAAM,EAAEJ,IAAI,EAAa;IAC/C,IAAIH,AAAiB,SAAjBA,MAAM,MAAM,IAAaA,MAAM,MAAM,GAAGO,QACxCP,MAAM,MAAM,GAAGO;IAGnB,IAAI,CAACP,MAAM,QAAQ,CAACO,OAAO,EACvBP,MAAM,QAAQ,CAACO,OAAO,GAAG,CAAC;IAG9B,IAAI,CAACJ,QAAQK,AAA6B,MAA7BA,OAAO,IAAI,CAACL,MAAM,MAAM,EACjC;IAGJ,IAAK,MAAMC,OAAOD,KAAM;QACpB,IAAIC,OAAOJ,MAAM,QAAQ,CAACO,OAAO,EAC7B,MAAME,MAAM,CAAC,UAAU,EAAEL,IAAI,+BAA+B,EAAEG,OAAO,CAAC,CAAC;QAE3EP,MAAM,QAAQ,CAACO,OAAO,CAACH,IAAI,GAAGD,IAAI,CAACC,IAAI;IAC3C;IAEA,IAAI,CAACJ,MAAM,gBAAgB,EAAE;QACzBU,SAAS,IAAI,CAAC,gBAAgB,CAAC,WAAWT;QAC1CD,MAAM,gBAAgB,GAAG;IAC7B;AACJ;AAEA,SAASW,iBAAiB,EAAEJ,MAAM,EAAEJ,IAAI,EAAa;IACjD,IAAIH,MAAM,QAAQ,IAAIA,MAAM,QAAQ,CAACO,OAAO,EAAE;QAC1C,IAAK,MAAMH,OAAOD,KACd,OAAOH,MAAM,QAAQ,CAACO,OAAO,CAACH,IAAI;QAGtC,IAAII,AAA+C,MAA/CA,OAAO,IAAI,CAACR,MAAM,QAAQ,CAACO,OAAO,EAAE,MAAM,EAC1C,OAAOP,MAAM,QAAQ,CAACO,OAAO;IAErC;IAEA,IAAIC,OAAO,IAAI,CAACR,MAAM,QAAQ,EAAE,MAAM,GAAG,GACrCA,MAAM,MAAM,GAAGY,KAAK,GAAG,IAAIJ,OAAO,IAAI,CAACR,MAAM,QAAQ,EAAE,GAAG,CAACa;SACxD;QACHb,MAAM,MAAM,GAAG;QAEf,IAAIA,MAAM,gBAAgB,EAAE;YACxBU,SAAS,IAAI,CAAC,mBAAmB,CAAC,WAAWT;YAC7CD,MAAM,gBAAgB,GAAG;QAC7B;IACJ;AACJ;AAEO,SAASc,WAAWC,KAAgB;IACvC,MAAM,EAAEC,QAAQ,EAAET,MAAM,EAAEJ,IAAI,EAAE,GAAGY;IAEnC,MAAME,eAAeC;IACrB,MAAMC,iBAAiBD,OAAO;IAM9B,MAAME,WAAWF,OAAOH;IACxBK,SAAS,OAAO,GAAGL;IAEnBM,UACI;QACI,IAAIF,eAAe,OAAO,IAAIF,aAAa,OAAO,EAAE,aAAaD,UAAU;YACvEG,eAAe,OAAO,GAAG;YACzB,IAAIH,UACAL,iBAAiBI;iBAEjBT,eAAeS;QAEvB;QAEA,IAAI,CAACC,YAAY,AAAgB,YAAhB,OAAOb,MACpBK,OAAO,MAAM,CAACR,MAAM,QAAQ,CAACO,OAAO,EAAEJ;QAE1Cc,aAAa,OAAO,GAAG;YAAE,GAAGF,KAAK;QAAC;IACtC,GACA;QAACZ;KAAK;IAGVkB,UAAU;QACN,OAAO;YACH,IAAI,CAACD,SAAS,OAAO,CAAC,QAAQ,EAC1BT,iBAAiBS,SAAS,OAAO;QAEzC;IACJ,GAAG,EAAE;AACT"}
package/index.d.ts CHANGED
@@ -98,3 +98,4 @@ import "./base/Base/FieldRenderers/ObjectRenderer/ObjectAccordionMultipleRendere
98
98
  import "./base/Base/FieldRenderers/ObjectRenderer/DynamicZoneRenderer.js";
99
99
  import "./base/Base/FieldRenderers/ObjectRenderer/KeyValueTagsRenderer.js";
100
100
  export type { FieldRenderers, FieldRendererComponent, LayoutRenderers, TabsNodeRendererProps } from "./features/formModel/FormView.js";
101
+ export { Debugger, DebuggerIndicator, DebuggerRoutes, debuggerStore } from "./features/debugger/index.js";
package/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import "./base/Base/FieldRenderers/MultiSelectRenderer.js";
1
2
  import "./base/Base/FieldRenderers/ObjectRenderer/KeyValueTagsRenderer.js";
2
3
  import "./base/Base/FieldRenderers/ObjectRenderer/DynamicZoneRenderer.js";
3
4
  import "./base/Base/FieldRenderers/ObjectRenderer/ObjectAccordionMultipleRenderer.js";
@@ -19,7 +20,6 @@ import "./base/Base/FieldRenderers/DateTimeRenderer.js";
19
20
  import "./base/Base/FieldRenderers/CheckboxesRenderer.js";
20
21
  import "./base/Base/FieldRenderers/RadioButtonsRenderer.js";
21
22
  import "./base/Base/FieldRenderers/TagsRenderer.js";
22
- import "./base/Base/FieldRenderers/TextareasRenderer.js";
23
23
  import "./features/formModel/renderers.js";
24
24
  import "./features/formModel/fieldTypes/TextFieldType.js";
25
25
  import "./features/formModel/fieldTypes/NumberFieldType.js";
@@ -29,12 +29,12 @@ import "./features/formModel/fieldTypes/FileFieldType.js";
29
29
  import "./features/formModel/fieldTypes/FileUrlFieldType.js";
30
30
  import "./features/formModel/fieldTypes/ObjectFieldType.js";
31
31
  import "./features/formModel/fieldTypes/LexicalFieldType.js";
32
+ import "./base/Base/FieldRenderers/TextareasRenderer.js";
32
33
  import "./base/Base/FieldRenderers/TextInputsRenderer.js";
33
34
  import "./base/Base/FieldRenderers/NumberInputsRenderer.js";
34
35
  import "./base/Base/FieldRenderers/NumberInputRenderer.js";
35
36
  import "./base/Base/FieldRenderers/SwitchRenderer.js";
36
37
  import "./base/Base/FieldRenderers/TextareaRenderer.js";
37
- import "./base/Base/FieldRenderers/MultiSelectRenderer.js";
38
38
  import "./base/Base/FieldRenderers/InputRenderer.js";
39
39
  import "./base/Base/FieldRenderers/SelectRenderer.js";
40
40
  export * from "@webiny/app";
@@ -83,3 +83,4 @@ export { FormErrors } from "./features/formModel/FormErrors.js";
83
83
  export { PresenterErrors } from "./features/formModel/PresenterErrors.js";
84
84
  export { useFieldRenderers } from "./features/formModel/useFieldRenderers.js";
85
85
  export { useLayoutRenderers } from "./features/formModel/useLayoutRenderers.js";
86
+ export { Debugger, DebuggerIndicator, DebuggerRoutes, debuggerStore } from "./features/debugger/index.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webiny/app-admin",
3
- "version": "6.5.0-beta.0",
3
+ "version": "6.5.0-beta.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./index.js",
@@ -26,24 +26,24 @@
26
26
  "@iconify/json": "2.2.503",
27
27
  "@lexical/utils": "0.48.0",
28
28
  "@types/react": "18.3.31",
29
- "@webiny/admin-ui": "6.5.0-beta.0",
30
- "@webiny/app": "6.5.0-beta.0",
29
+ "@webiny/admin-ui": "6.5.0-beta.2",
30
+ "@webiny/app": "6.5.0-beta.2",
31
31
  "@webiny/di": "1.0.2",
32
- "@webiny/feature": "6.5.0-beta.0",
33
- "@webiny/form": "6.5.0-beta.0",
34
- "@webiny/icons": "6.5.0-beta.0",
35
- "@webiny/lexical-converter": "6.5.0-beta.0",
36
- "@webiny/lexical-editor": "6.5.0-beta.0",
37
- "@webiny/lexical-nodes": "6.5.0-beta.0",
38
- "@webiny/lexical-theme": "6.5.0-beta.0",
39
- "@webiny/plugins": "6.5.0-beta.0",
40
- "@webiny/react-composition": "6.5.0-beta.0",
41
- "@webiny/react-properties": "6.5.0-beta.0",
42
- "@webiny/sdk": "6.5.0-beta.0",
43
- "@webiny/telemetry": "6.5.0-beta.0",
44
- "@webiny/utils": "6.5.0-beta.0",
45
- "@webiny/validation": "6.5.0-beta.0",
46
- "@webiny/wcp": "6.5.0-beta.0",
32
+ "@webiny/feature": "6.5.0-beta.2",
33
+ "@webiny/form": "6.5.0-beta.2",
34
+ "@webiny/icons": "6.5.0-beta.2",
35
+ "@webiny/lexical-converter": "6.5.0-beta.2",
36
+ "@webiny/lexical-editor": "6.5.0-beta.2",
37
+ "@webiny/lexical-nodes": "6.5.0-beta.2",
38
+ "@webiny/lexical-theme": "6.5.0-beta.2",
39
+ "@webiny/plugins": "6.5.0-beta.2",
40
+ "@webiny/react-composition": "6.5.0-beta.2",
41
+ "@webiny/react-properties": "6.5.0-beta.2",
42
+ "@webiny/sdk": "6.5.0-beta.2",
43
+ "@webiny/telemetry": "6.5.0-beta.2",
44
+ "@webiny/utils": "6.5.0-beta.2",
45
+ "@webiny/validation": "6.5.0-beta.2",
46
+ "@webiny/wcp": "6.5.0-beta.2",
47
47
  "apollo-cache": "1.3.5",
48
48
  "apollo-client": "2.6.10",
49
49
  "apollo-link": "1.2.14",
@@ -83,7 +83,7 @@
83
83
  "@types/react-transition-group": "4.4.12",
84
84
  "@types/store": "2.0.5",
85
85
  "@types/tinycolor2": "1.4.6",
86
- "@webiny/build-tools": "6.5.0-beta.0",
86
+ "@webiny/build-tools": "6.5.0-beta.2",
87
87
  "rimraf": "6.1.3",
88
88
  "typescript": "7.0.2",
89
89
  "vitest": "4.1.10"
@@ -10,6 +10,11 @@ export interface FolderAwareDataSourceConfig<TRow> {
10
10
  };
11
11
  keyField: keyof TRow & string;
12
12
  localFilters?: Record<string, (item: TRow, value: unknown) => boolean>;
13
+ /**
14
+ * Reads the value of a sort field from an item, for ordering rows locally.
15
+ * See `QueryMatcherConfig.getSortValue`.
16
+ */
17
+ getSortValue?: (item: TRow, field: string) => unknown;
13
18
  }
14
19
  export interface FetchParams {
15
20
  where?: Record<string, unknown>;
@@ -28,7 +28,8 @@ class FolderAwareDataSource {
28
28
  };
29
29
  this.queryMatcher = new QueryMatcher({
30
30
  keyField: config.keyField,
31
- localFilters
31
+ localFilters,
32
+ getSortValue: config.getSortValue
32
33
  });
33
34
  makeObservable(this, {
34
35
  _meta: observable,
@@ -59,7 +60,7 @@ class FolderAwareDataSource {
59
60
  });
60
61
  runInAction(()=>{
61
62
  this.onQueryResult(result.data);
62
- this.queryMatcher.updateFromQuery(params, result.data.map((item)=>String(item[this._keyField])));
63
+ this.queryMatcher.updateFromQuery(params, result.data.map((item)=>String(item[this._keyField])), result.meta.hasMoreItems);
63
64
  this._meta = {
64
65
  cursor: result.meta.cursor,
65
66
  hasMoreItems: result.meta.hasMoreItems,
@@ -85,7 +86,7 @@ class FolderAwareDataSource {
85
86
  });
86
87
  runInAction(()=>{
87
88
  this.onLoadMoreResult(result.data);
88
- this.queryMatcher.appendResultKeys(result.data.map((item)=>String(item[this._keyField])));
89
+ this.queryMatcher.appendResultKeys(result.data.map((item)=>String(item[this._keyField])), result.meta.hasMoreItems);
89
90
  this._meta = {
90
91
  cursor: result.meta.cursor,
91
92
  hasMoreItems: result.meta.hasMoreItems,
@@ -1 +1 @@
1
- {"version":3,"file":"presentation/listPresenter/FolderAwareDataSource.js","sources":["../../../src/presentation/listPresenter/FolderAwareDataSource.ts"],"sourcesContent":["import { makeObservable, observable, computed, runInAction, action } from \"mobx\";\nimport type { IDataSource, IDataSourceQuery, IDataSourceMeta } from \"./abstractions.js\";\nimport { QueryMatcher } from \"./QueryMatcher.js\";\n\nexport interface FolderAwareDataSourceConfig<TRow> {\n locationField?: string;\n registeredFilterNames?: string[];\n getDescendantFolders?: { execute(folderId: string): { id: string }[] };\n keyField: keyof TRow & string;\n localFilters?: Record<string, (item: TRow, value: unknown) => boolean>;\n}\n\nexport interface FetchParams {\n where?: Record<string, unknown>;\n sort?: string[];\n search?: string;\n limit?: number;\n after?: string;\n}\n\nexport interface FetchResult<TRow> {\n data: TRow[];\n meta: { cursor: string | null; hasMoreItems: boolean; totalCount: number };\n}\n\nexport abstract class FolderAwareDataSource<TRow> implements IDataSource<TRow> {\n private _meta: IDataSourceMeta = { cursor: null, hasMoreItems: false, totalCount: 0 };\n private _loading = false;\n private _locationField: string;\n private _registeredFilterNames: Set<string>;\n private _getDescendantFolders: FolderAwareDataSourceConfig<TRow>[\"getDescendantFolders\"];\n private _keyField: keyof TRow & string;\n protected queryMatcher: QueryMatcher<TRow>;\n\n constructor(config: FolderAwareDataSourceConfig<TRow>) {\n this._locationField = config.locationField ?? \"location\";\n this._registeredFilterNames = new Set([\n ...(config.registeredFilterNames ?? []),\n ...Object.keys(config.localFilters ?? {})\n ]);\n this._getDescendantFolders = config.getDescendantFolders;\n this._keyField = config.keyField;\n\n const locationField = this._locationField;\n const localFilters: Record<string, (item: TRow, value: unknown) => boolean> = {\n folderId: (item, value) => {\n const folderId = value as string;\n const location = (item as Record<string, any>)[locationField];\n const itemFolderId = location?.folderId as string | undefined;\n if (folderId && folderId !== \"root\") {\n return itemFolderId === folderId;\n }\n return !itemFolderId || itemFolderId === \"root\";\n },\n ...config.localFilters\n };\n\n this.queryMatcher = new QueryMatcher<TRow>({\n keyField: config.keyField,\n localFilters\n });\n\n makeObservable<FolderAwareDataSource<TRow>, \"_meta\" | \"_loading\">(this, {\n _meta: observable,\n _loading: observable,\n meta: computed,\n loading: computed,\n query: action,\n loadMore: action\n });\n }\n\n abstract get rows(): TRow[];\n\n abstract fetch(params: FetchParams): Promise<FetchResult<TRow>>;\n\n protected onQueryResult(_data: TRow[]): void {}\n\n protected onLoadMoreResult(_data: TRow[]): void {}\n\n get meta(): IDataSourceMeta {\n return this._meta;\n }\n\n get loading(): boolean {\n return this._loading;\n }\n\n async query(params: IDataSourceQuery): Promise<void> {\n this._loading = true;\n\n try {\n const result = await this.fetch({\n where: this.buildWhere(params),\n sort: this.buildSort(params),\n search: params.search,\n limit: params.limit,\n after: params.cursor\n });\n\n runInAction(() => {\n this.onQueryResult(result.data);\n this.queryMatcher.updateFromQuery(\n params,\n result.data.map(item => String(item[this._keyField]))\n );\n this._meta = {\n cursor: result.meta.cursor,\n hasMoreItems: result.meta.hasMoreItems,\n totalCount: result.meta.totalCount\n };\n });\n } finally {\n runInAction(() => {\n this._loading = false;\n });\n }\n }\n\n async loadMore(params: IDataSourceQuery): Promise<void> {\n if (!this._meta.hasMoreItems || this._loading) {\n return;\n }\n\n this._loading = true;\n\n try {\n const result = await this.fetch({\n where: this.buildWhere(params),\n sort: this.buildSort(params),\n search: params.search,\n limit: params.limit,\n after: this._meta.cursor ?? undefined\n });\n\n runInAction(() => {\n this.onLoadMoreResult(result.data);\n this.queryMatcher.appendResultKeys(\n result.data.map(item => String(item[this._keyField]))\n );\n this._meta = {\n cursor: result.meta.cursor,\n hasMoreItems: result.meta.hasMoreItems,\n totalCount: result.meta.totalCount\n };\n });\n } finally {\n runInAction(() => {\n this._loading = false;\n });\n }\n }\n\n protected customizeWhere(\n _where: Record<string, unknown>,\n _filters: Record<string, unknown>\n ): void {}\n\n protected shouldExpandFolders(_filters: Record<string, unknown>): boolean {\n return false;\n }\n\n private buildWhere(params: IDataSourceQuery): Record<string, unknown> | undefined {\n const where: Record<string, unknown> = {};\n const filters = params.filters ?? {};\n const folderId = (filters.folderId as string | undefined) || \"root\";\n const isRoot = folderId === \"root\";\n const hasAdvancedFilters = Object.keys(filters).some(\n k => k !== \"folderId\" && !this._registeredFilterNames.has(k)\n );\n const isSearching =\n !!params.search || hasAdvancedFilters || this.shouldExpandFolders(filters);\n\n if (isSearching && isRoot) {\n // Search/filter from root: no folder filter — search all folders.\n } else if (isSearching && this._getDescendantFolders) {\n const descendants = this._getDescendantFolders.execute(folderId);\n const folderIds = descendants.map(f => f.id);\n where[this._locationField] = { folderId_in: folderIds };\n } else {\n where[this._locationField] = { folderId };\n }\n\n for (const [key, value] of Object.entries(filters)) {\n if (key === \"folderId\") {\n continue;\n }\n where[key] = value;\n }\n\n this.customizeWhere(where, filters);\n\n return Object.keys(where).length > 0 ? where : undefined;\n }\n\n private buildSort(params: IDataSourceQuery): string[] | undefined {\n if (!params.sort) {\n return undefined;\n }\n return [`${params.sort.field}_${params.sort.direction}`];\n }\n}\n"],"names":["FolderAwareDataSource","config","Set","Object","locationField","localFilters","item","value","folderId","location","itemFolderId","QueryMatcher","makeObservable","observable","computed","action","_data","params","result","runInAction","String","undefined","_where","_filters","where","filters","isRoot","hasAdvancedFilters","k","isSearching","descendants","folderIds","f","key"],"mappings":"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"}
1
+ {"version":3,"file":"presentation/listPresenter/FolderAwareDataSource.js","sources":["../../../src/presentation/listPresenter/FolderAwareDataSource.ts"],"sourcesContent":["import { makeObservable, observable, computed, runInAction, action } from \"mobx\";\nimport type { IDataSource, IDataSourceQuery, IDataSourceMeta } from \"./abstractions.js\";\nimport { QueryMatcher } from \"./QueryMatcher.js\";\n\nexport interface FolderAwareDataSourceConfig<TRow> {\n locationField?: string;\n registeredFilterNames?: string[];\n getDescendantFolders?: { execute(folderId: string): { id: string }[] };\n keyField: keyof TRow & string;\n localFilters?: Record<string, (item: TRow, value: unknown) => boolean>;\n /**\n * Reads the value of a sort field from an item, for ordering rows locally.\n * See `QueryMatcherConfig.getSortValue`.\n */\n getSortValue?: (item: TRow, field: string) => unknown;\n}\n\nexport interface FetchParams {\n where?: Record<string, unknown>;\n sort?: string[];\n search?: string;\n limit?: number;\n after?: string;\n}\n\nexport interface FetchResult<TRow> {\n data: TRow[];\n meta: { cursor: string | null; hasMoreItems: boolean; totalCount: number };\n}\n\nexport abstract class FolderAwareDataSource<TRow> implements IDataSource<TRow> {\n private _meta: IDataSourceMeta = { cursor: null, hasMoreItems: false, totalCount: 0 };\n private _loading = false;\n private _locationField: string;\n private _registeredFilterNames: Set<string>;\n private _getDescendantFolders: FolderAwareDataSourceConfig<TRow>[\"getDescendantFolders\"];\n private _keyField: keyof TRow & string;\n protected queryMatcher: QueryMatcher<TRow>;\n\n constructor(config: FolderAwareDataSourceConfig<TRow>) {\n this._locationField = config.locationField ?? \"location\";\n this._registeredFilterNames = new Set([\n ...(config.registeredFilterNames ?? []),\n ...Object.keys(config.localFilters ?? {})\n ]);\n this._getDescendantFolders = config.getDescendantFolders;\n this._keyField = config.keyField;\n\n const locationField = this._locationField;\n const localFilters: Record<string, (item: TRow, value: unknown) => boolean> = {\n folderId: (item, value) => {\n const folderId = value as string;\n const location = (item as Record<string, any>)[locationField];\n const itemFolderId = location?.folderId as string | undefined;\n if (folderId && folderId !== \"root\") {\n return itemFolderId === folderId;\n }\n return !itemFolderId || itemFolderId === \"root\";\n },\n ...config.localFilters\n };\n\n this.queryMatcher = new QueryMatcher<TRow>({\n keyField: config.keyField,\n localFilters,\n getSortValue: config.getSortValue\n });\n\n makeObservable<FolderAwareDataSource<TRow>, \"_meta\" | \"_loading\">(this, {\n _meta: observable,\n _loading: observable,\n meta: computed,\n loading: computed,\n query: action,\n loadMore: action\n });\n }\n\n abstract get rows(): TRow[];\n\n abstract fetch(params: FetchParams): Promise<FetchResult<TRow>>;\n\n protected onQueryResult(_data: TRow[]): void {}\n\n protected onLoadMoreResult(_data: TRow[]): void {}\n\n get meta(): IDataSourceMeta {\n return this._meta;\n }\n\n get loading(): boolean {\n return this._loading;\n }\n\n async query(params: IDataSourceQuery): Promise<void> {\n this._loading = true;\n\n try {\n const result = await this.fetch({\n where: this.buildWhere(params),\n sort: this.buildSort(params),\n search: params.search,\n limit: params.limit,\n after: params.cursor\n });\n\n runInAction(() => {\n this.onQueryResult(result.data);\n this.queryMatcher.updateFromQuery(\n params,\n result.data.map(item => String(item[this._keyField])),\n result.meta.hasMoreItems\n );\n this._meta = {\n cursor: result.meta.cursor,\n hasMoreItems: result.meta.hasMoreItems,\n totalCount: result.meta.totalCount\n };\n });\n } finally {\n runInAction(() => {\n this._loading = false;\n });\n }\n }\n\n async loadMore(params: IDataSourceQuery): Promise<void> {\n if (!this._meta.hasMoreItems || this._loading) {\n return;\n }\n\n this._loading = true;\n\n try {\n const result = await this.fetch({\n where: this.buildWhere(params),\n sort: this.buildSort(params),\n search: params.search,\n limit: params.limit,\n after: this._meta.cursor ?? undefined\n });\n\n runInAction(() => {\n this.onLoadMoreResult(result.data);\n this.queryMatcher.appendResultKeys(\n result.data.map(item => String(item[this._keyField])),\n result.meta.hasMoreItems\n );\n this._meta = {\n cursor: result.meta.cursor,\n hasMoreItems: result.meta.hasMoreItems,\n totalCount: result.meta.totalCount\n };\n });\n } finally {\n runInAction(() => {\n this._loading = false;\n });\n }\n }\n\n protected customizeWhere(\n _where: Record<string, unknown>,\n _filters: Record<string, unknown>\n ): void {}\n\n protected shouldExpandFolders(_filters: Record<string, unknown>): boolean {\n return false;\n }\n\n private buildWhere(params: IDataSourceQuery): Record<string, unknown> | undefined {\n const where: Record<string, unknown> = {};\n const filters = params.filters ?? {};\n const folderId = (filters.folderId as string | undefined) || \"root\";\n const isRoot = folderId === \"root\";\n const hasAdvancedFilters = Object.keys(filters).some(\n k => k !== \"folderId\" && !this._registeredFilterNames.has(k)\n );\n const isSearching =\n !!params.search || hasAdvancedFilters || this.shouldExpandFolders(filters);\n\n if (isSearching && isRoot) {\n // Search/filter from root: no folder filter — search all folders.\n } else if (isSearching && this._getDescendantFolders) {\n const descendants = this._getDescendantFolders.execute(folderId);\n const folderIds = descendants.map(f => f.id);\n where[this._locationField] = { folderId_in: folderIds };\n } else {\n where[this._locationField] = { folderId };\n }\n\n for (const [key, value] of Object.entries(filters)) {\n if (key === \"folderId\") {\n continue;\n }\n where[key] = value;\n }\n\n this.customizeWhere(where, filters);\n\n return Object.keys(where).length > 0 ? where : undefined;\n }\n\n private buildSort(params: IDataSourceQuery): string[] | undefined {\n if (!params.sort) {\n return undefined;\n }\n return [`${params.sort.field}_${params.sort.direction}`];\n }\n}\n"],"names":["FolderAwareDataSource","config","Set","Object","locationField","localFilters","item","value","folderId","location","itemFolderId","QueryMatcher","makeObservable","observable","computed","action","_data","params","result","runInAction","String","undefined","_where","_filters","where","filters","isRoot","hasAdvancedFilters","k","isSearching","descendants","folderIds","f","key"],"mappings":";;AA8BO,MAAeA;IASlB,YAAYC,MAAyC,CAAE;aAR/C,KAAK,GAAoB;YAAE,QAAQ;YAAM,cAAc;YAAO,YAAY;QAAE;aAC5E,QAAQ,GAAG;QAQf,IAAI,CAAC,cAAc,GAAGA,OAAO,aAAa,IAAI;QAC9C,IAAI,CAAC,sBAAsB,GAAG,IAAIC,IAAI;eAC9BD,OAAO,qBAAqB,IAAI,EAAE;eACnCE,OAAO,IAAI,CAACF,OAAO,YAAY,IAAI,CAAC;SAC1C;QACD,IAAI,CAAC,qBAAqB,GAAGA,OAAO,oBAAoB;QACxD,IAAI,CAAC,SAAS,GAAGA,OAAO,QAAQ;QAEhC,MAAMG,gBAAgB,IAAI,CAAC,cAAc;QACzC,MAAMC,eAAwE;YAC1E,UAAU,CAACC,MAAMC;gBACb,MAAMC,WAAWD;gBACjB,MAAME,WAAYH,IAA4B,CAACF,cAAc;gBAC7D,MAAMM,eAAeD,UAAU;gBAC/B,IAAID,YAAYA,AAAa,WAAbA,UACZ,OAAOE,iBAAiBF;gBAE5B,OAAO,CAACE,gBAAgBA,AAAiB,WAAjBA;YAC5B;YACA,GAAGT,OAAO,YAAY;QAC1B;QAEA,IAAI,CAAC,YAAY,GAAG,IAAIU,aAAmB;YACvC,UAAUV,OAAO,QAAQ;YACzBI;YACA,cAAcJ,OAAO,YAAY;QACrC;QAEAW,eAAkE,IAAI,EAAE;YACpE,OAAOC;YACP,UAAUA;YACV,MAAMC;YACN,SAASA;YACT,OAAOC;YACP,UAAUA;QACd;IACJ;IAMU,cAAcC,KAAa,EAAQ,CAAC;IAEpC,iBAAiBA,KAAa,EAAQ,CAAC;IAEjD,IAAI,OAAwB;QACxB,OAAO,IAAI,CAAC,KAAK;IACrB;IAEA,IAAI,UAAmB;QACnB,OAAO,IAAI,CAAC,QAAQ;IACxB;IAEA,MAAM,MAAMC,MAAwB,EAAiB;QACjD,IAAI,CAAC,QAAQ,GAAG;QAEhB,IAAI;YACA,MAAMC,SAAS,MAAM,IAAI,CAAC,KAAK,CAAC;gBAC5B,OAAO,IAAI,CAAC,UAAU,CAACD;gBACvB,MAAM,IAAI,CAAC,SAAS,CAACA;gBACrB,QAAQA,OAAO,MAAM;gBACrB,OAAOA,OAAO,KAAK;gBACnB,OAAOA,OAAO,MAAM;YACxB;YAEAE,YAAY;gBACR,IAAI,CAAC,aAAa,CAACD,OAAO,IAAI;gBAC9B,IAAI,CAAC,YAAY,CAAC,eAAe,CAC7BD,QACAC,OAAO,IAAI,CAAC,GAAG,CAACZ,CAAAA,OAAQc,OAAOd,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,IACnDY,OAAO,IAAI,CAAC,YAAY;gBAE5B,IAAI,CAAC,KAAK,GAAG;oBACT,QAAQA,OAAO,IAAI,CAAC,MAAM;oBAC1B,cAAcA,OAAO,IAAI,CAAC,YAAY;oBACtC,YAAYA,OAAO,IAAI,CAAC,UAAU;gBACtC;YACJ;QACJ,SAAU;YACNC,YAAY;gBACR,IAAI,CAAC,QAAQ,GAAG;YACpB;QACJ;IACJ;IAEA,MAAM,SAASF,MAAwB,EAAiB;QACpD,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,IAAI,IAAI,CAAC,QAAQ,EACzC;QAGJ,IAAI,CAAC,QAAQ,GAAG;QAEhB,IAAI;YACA,MAAMC,SAAS,MAAM,IAAI,CAAC,KAAK,CAAC;gBAC5B,OAAO,IAAI,CAAC,UAAU,CAACD;gBACvB,MAAM,IAAI,CAAC,SAAS,CAACA;gBACrB,QAAQA,OAAO,MAAM;gBACrB,OAAOA,OAAO,KAAK;gBACnB,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,IAAII;YAChC;YAEAF,YAAY;gBACR,IAAI,CAAC,gBAAgB,CAACD,OAAO,IAAI;gBACjC,IAAI,CAAC,YAAY,CAAC,gBAAgB,CAC9BA,OAAO,IAAI,CAAC,GAAG,CAACZ,CAAAA,OAAQc,OAAOd,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,IACnDY,OAAO,IAAI,CAAC,YAAY;gBAE5B,IAAI,CAAC,KAAK,GAAG;oBACT,QAAQA,OAAO,IAAI,CAAC,MAAM;oBAC1B,cAAcA,OAAO,IAAI,CAAC,YAAY;oBACtC,YAAYA,OAAO,IAAI,CAAC,UAAU;gBACtC;YACJ;QACJ,SAAU;YACNC,YAAY;gBACR,IAAI,CAAC,QAAQ,GAAG;YACpB;QACJ;IACJ;IAEU,eACNG,MAA+B,EAC/BC,QAAiC,EAC7B,CAAC;IAEC,oBAAoBA,QAAiC,EAAW;QACtE,OAAO;IACX;IAEQ,WAAWN,MAAwB,EAAuC;QAC9E,MAAMO,QAAiC,CAAC;QACxC,MAAMC,UAAUR,OAAO,OAAO,IAAI,CAAC;QACnC,MAAMT,WAAYiB,QAAQ,QAAQ,IAA2B;QAC7D,MAAMC,SAASlB,AAAa,WAAbA;QACf,MAAMmB,qBAAqBxB,OAAO,IAAI,CAACsB,SAAS,IAAI,CAChDG,CAAAA,IAAKA,AAAM,eAANA,KAAoB,CAAC,IAAI,CAAC,sBAAsB,CAAC,GAAG,CAACA;QAE9D,MAAMC,cACF,CAAC,CAACZ,OAAO,MAAM,IAAIU,sBAAsB,IAAI,CAAC,mBAAmB,CAACF;QAEtE,IAAII,eAAeH;aAEZ,IAAIG,eAAe,IAAI,CAAC,qBAAqB,EAAE;YAClD,MAAMC,cAAc,IAAI,CAAC,qBAAqB,CAAC,OAAO,CAACtB;YACvD,MAAMuB,YAAYD,YAAY,GAAG,CAACE,CAAAA,IAAKA,EAAE,EAAE;YAC3CR,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,GAAG;gBAAE,aAAaO;YAAU;QAC1D,OACIP,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,GAAG;YAAEhB;QAAS;QAG5C,KAAK,MAAM,CAACyB,KAAK1B,MAAM,IAAIJ,OAAO,OAAO,CAACsB,SACtC,IAAIQ,AAAQ,eAARA,KAGJT,KAAK,CAACS,IAAI,GAAG1B;QAGjB,IAAI,CAAC,cAAc,CAACiB,OAAOC;QAE3B,OAAOtB,OAAO,IAAI,CAACqB,OAAO,MAAM,GAAG,IAAIA,QAAQH;IACnD;IAEQ,UAAUJ,MAAwB,EAAwB;QAC9D,IAAI,CAACA,OAAO,IAAI,EACZ;QAEJ,OAAO;YAAC,GAAGA,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAEA,OAAO,IAAI,CAAC,SAAS,EAAE;SAAC;IAC5D;AACJ"}
@@ -1,8 +1,15 @@
1
1
  import type { IDataSourceQuery } from "./abstractions.js";
2
2
  type ItemPredicate<TRow> = (item: TRow, value: unknown) => boolean;
3
+ type SortValueGetter<TRow> = (item: TRow, field: string) => unknown;
3
4
  export interface QueryMatcherConfig<TRow> {
4
5
  keyField: keyof TRow & string;
5
6
  localFilters: Record<string, ItemPredicate<TRow>>;
7
+ /**
8
+ * Reads the value of a sort field from an item. Defaults to a dot-path lookup
9
+ * (e.g. `savedOn`, `values.title`). Override when the query's sort field name
10
+ * does not match the item's shape.
11
+ */
12
+ getSortValue?: SortValueGetter<TRow>;
6
13
  }
7
14
  /**
8
15
  * Decides which cached items belong in the current view.
@@ -34,10 +41,11 @@ export interface QueryMatcherConfig<TRow> {
34
41
  * });
35
42
  *
36
43
  * // In DataSource.query(), after fetching:
37
- * matcher.updateFromQuery(params, result.data.map(item => item.id));
44
+ * matcher.updateFromQuery(params, result.data.map(item => item.id), result.meta.hasMoreItems);
38
45
  *
39
- * // In DataSource.rows getter:
40
- * return cache.getItems().filter(matcher.matcher);
46
+ * // In DataSource.rows getter (filters, sorts by the applied sort, and cuts off at the
47
+ * // last loaded item):
48
+ * return matcher.select(cache.getItems());
41
49
  * ```
42
50
  */
43
51
  export declare class QueryMatcher<TRow> {
@@ -45,12 +53,27 @@ export declare class QueryMatcher<TRow> {
45
53
  private _queryResultKeys;
46
54
  private _matcher;
47
55
  private localFilterKeys;
56
+ private getSortValue;
57
+ private _view;
48
58
  constructor(config: QueryMatcherConfig<TRow>);
49
59
  get matcher(): (item: TRow) => boolean;
50
60
  filter(items: TRow[]): TRow[];
61
+ /**
62
+ * Returns the items that belong in the current view, in the view's order.
63
+ *
64
+ * The cache is shared by views with different sorts, so its order means nothing to
65
+ * this view. Instead, matching items are sorted by the applied sort (ties keep the
66
+ * server order), and the result is cut off after the last item the server returned
67
+ * for this view. That keeps locally created or updated items in their sort position,
68
+ * and hides cached items that sort past the loaded pages until pagination reaches them.
69
+ *
70
+ * While searching, the server's (relevance) order is kept as is.
71
+ */
72
+ select(items: TRow[]): TRow[];
73
+ private getKey;
51
74
  hasServerSideFilters(params: IDataSourceQuery): boolean;
52
- updateFromQuery(params: IDataSourceQuery, resultKeys: string[]): void;
53
- appendResultKeys(keys: string[]): void;
75
+ updateFromQuery(params: IDataSourceQuery, resultKeys: string[], hasMore?: boolean): void;
76
+ appendResultKeys(keys: string[], hasMore?: boolean): void;
54
77
  private buildMatcher;
55
78
  }
56
79
  export {};
@@ -4,9 +4,12 @@ class QueryMatcher {
4
4
  this.config = config;
5
5
  this._queryResultKeys = null;
6
6
  this._matcher = ()=>true;
7
+ this._view = null;
7
8
  this.localFilterKeys = new Set(Object.keys(config.localFilters));
9
+ this.getSortValue = config.getSortValue ?? getValueByPath;
8
10
  makeObservable(this, {
9
11
  _matcher: observable.ref,
12
+ _view: observable.ref,
10
13
  matcher: computed,
11
14
  updateFromQuery: action,
12
15
  appendResultKeys: action
@@ -18,18 +21,65 @@ class QueryMatcher {
18
21
  filter(items) {
19
22
  return items.filter(this._matcher);
20
23
  }
24
+ select(items) {
25
+ const matched = this.filter(items);
26
+ const view = this._view;
27
+ if (!view) return matched;
28
+ const serverIndex = new Map(view.resultKeys.map((key, index)=>[
29
+ key,
30
+ index
31
+ ]));
32
+ const indexOf = (item)=>serverIndex.get(this.getKey(item)) ?? 1 / 0;
33
+ const sort = view.searching ? void 0 : view.sort;
34
+ const sorted = [
35
+ ...matched
36
+ ].sort((a, b)=>{
37
+ if (sort) {
38
+ const result = compareValues(this.getSortValue(a, sort.field), this.getSortValue(b, sort.field));
39
+ if (0 !== result) return "ASC" === sort.direction ? result : -result;
40
+ }
41
+ const left = indexOf(a);
42
+ const right = indexOf(b);
43
+ return left === right ? 0 : left < right ? -1 : 1;
44
+ });
45
+ if (!view.hasMore) return sorted;
46
+ let lastLoaded = -1;
47
+ sorted.forEach((item, index)=>{
48
+ if (serverIndex.has(this.getKey(item))) lastLoaded = index;
49
+ });
50
+ return lastLoaded < 0 ? sorted : sorted.slice(0, lastLoaded + 1);
51
+ }
52
+ getKey(item) {
53
+ return String(item[this.config.keyField]);
54
+ }
21
55
  hasServerSideFilters(params) {
22
56
  if (params.search) return true;
23
57
  const filters = params.filters ?? {};
24
58
  return Object.keys(filters).some((key)=>!this.localFilterKeys.has(key));
25
59
  }
26
- updateFromQuery(params, resultKeys) {
60
+ updateFromQuery(params, resultKeys, hasMore = false) {
27
61
  if (this.hasServerSideFilters(params)) this._queryResultKeys = new Set(resultKeys);
28
62
  else this._queryResultKeys = null;
29
63
  this._matcher = this.buildMatcher(params);
64
+ this._view = {
65
+ resultKeys: [
66
+ ...resultKeys
67
+ ],
68
+ sort: params.sort,
69
+ searching: !!params.search,
70
+ hasMore
71
+ };
30
72
  }
31
- appendResultKeys(keys) {
73
+ appendResultKeys(keys, hasMore = false) {
32
74
  if (this._queryResultKeys) for (const key of keys)this._queryResultKeys.add(key);
75
+ if (this._view) this._view = {
76
+ ...this._view,
77
+ resultKeys: [
78
+ ...this._view.resultKeys,
79
+ ...keys
80
+ ],
81
+ hasMore
82
+ };
33
83
  }
34
84
  buildMatcher(params) {
35
85
  const filters = params.filters ?? {};
@@ -54,6 +104,26 @@ class QueryMatcher {
54
104
  };
55
105
  }
56
106
  }
107
+ function getValueByPath(item, path) {
108
+ let current = item;
109
+ for (const part of path.split(".")){
110
+ if (null == current || "object" != typeof current) return;
111
+ current = current[part];
112
+ }
113
+ return current;
114
+ }
115
+ function compareValues(a, b) {
116
+ const aEmpty = null == a;
117
+ const bEmpty = null == b;
118
+ if (aEmpty || bEmpty) return aEmpty === bEmpty ? 0 : aEmpty ? 1 : -1;
119
+ const left = a instanceof Date ? a.getTime() : a;
120
+ const right = b instanceof Date ? b.getTime() : b;
121
+ if ("number" == typeof left && "number" == typeof right) return left - right;
122
+ if ("boolean" == typeof left && "boolean" == typeof right) return Number(left) - Number(right);
123
+ const leftString = String(left);
124
+ const rightString = String(right);
125
+ return leftString < rightString ? -1 : leftString > rightString ? 1 : 0;
126
+ }
57
127
  export { QueryMatcher };
58
128
 
59
129
  //# sourceMappingURL=QueryMatcher.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"presentation/listPresenter/QueryMatcher.js","sources":["../../../src/presentation/listPresenter/QueryMatcher.ts"],"sourcesContent":["import { makeObservable, observable, computed, action } from \"mobx\";\nimport type { IDataSourceQuery } from \"./abstractions.js\";\n\ntype ItemPredicate<TRow> = (item: TRow, value: unknown) => boolean;\n\nexport interface QueryMatcherConfig<TRow> {\n keyField: keyof TRow & string;\n localFilters: Record<string, ItemPredicate<TRow>>;\n}\n\n/**\n * Decides which cached items belong in the current view.\n *\n * A DataSource backed by a shared cache (e.g., per-model entry cache) needs to\n * filter cached items to only those matching the active query. QueryMatcher does\n * this by splitting filters into two categories:\n *\n * - **Local filters** (e.g., folderId, status) — evaluated client-side against\n * each item using predicates provided via `localFilters` config. Used when\n * browsing without search or server-side filters.\n *\n * - **Server-side filters** (search, advanced search, any filter key not in\n * `localFilters`) — cannot be evaluated locally. Instead, QueryMatcher tracks\n * the set of item keys returned by the last server query (`_queryResultKeys`)\n * and restricts the view to those items.\n *\n * The `matcher` getter is MobX-computed, so any DataSource `rows` getter that\n * reads it will recompute when the matcher changes after a query.\n *\n * Usage:\n * ```ts\n * const matcher = new QueryMatcher<MyItem>({\n * keyField: \"id\",\n * localFilters: {\n * folderId: (item, value) => item.folderId === value,\n * status: (item, value) => item.status === value,\n * }\n * });\n *\n * // In DataSource.query(), after fetching:\n * matcher.updateFromQuery(params, result.data.map(item => item.id));\n *\n * // In DataSource.rows getter:\n * return cache.getItems().filter(matcher.matcher);\n * ```\n */\nexport class QueryMatcher<TRow> {\n private _queryResultKeys: Set<string> | null = null;\n private _matcher: (item: TRow) => boolean = () => true;\n private localFilterKeys: Set<string>;\n\n constructor(private config: QueryMatcherConfig<TRow>) {\n this.localFilterKeys = new Set(Object.keys(config.localFilters));\n\n makeObservable<QueryMatcher<TRow>, \"_matcher\">(this, {\n _matcher: observable.ref,\n matcher: computed,\n updateFromQuery: action,\n appendResultKeys: action\n });\n }\n\n get matcher(): (item: TRow) => boolean {\n return this._matcher;\n }\n\n filter(items: TRow[]): TRow[] {\n return items.filter(this._matcher);\n }\n\n hasServerSideFilters(params: IDataSourceQuery): boolean {\n if (params.search) {\n return true;\n }\n const filters = params.filters ?? {};\n return Object.keys(filters).some(key => !this.localFilterKeys.has(key));\n }\n\n updateFromQuery(params: IDataSourceQuery, resultKeys: string[]): void {\n if (this.hasServerSideFilters(params)) {\n this._queryResultKeys = new Set(resultKeys);\n } else {\n this._queryResultKeys = null;\n }\n this._matcher = this.buildMatcher(params);\n }\n\n appendResultKeys(keys: string[]): void {\n if (this._queryResultKeys) {\n for (const key of keys) {\n this._queryResultKeys.add(key);\n }\n }\n }\n\n private buildMatcher(params: IDataSourceQuery): (item: TRow) => boolean {\n const filters = params.filters ?? {};\n const queryResultKeys = this._queryResultKeys;\n const isSearching = !!params.search;\n const { keyField, localFilters } = this.config;\n\n const activeLocalFilters: Array<{ predicate: ItemPredicate<TRow>; value: unknown }> = [];\n for (const [key, predicate] of Object.entries(localFilters)) {\n const value = filters[key];\n if (value !== undefined && value !== null) {\n activeLocalFilters.push({ predicate, value });\n }\n }\n\n return (item: TRow) => {\n if (queryResultKeys) {\n if (!queryResultKeys.has(String(item[keyField]))) {\n return false;\n }\n } else if (!isSearching) {\n for (const { predicate, value } of activeLocalFilters) {\n if (!predicate(item, value)) {\n return false;\n }\n }\n }\n\n return true;\n };\n }\n}\n"],"names":["QueryMatcher","config","Set","Object","makeObservable","observable","computed","action","items","params","filters","key","resultKeys","keys","queryResultKeys","isSearching","keyField","localFilters","activeLocalFilters","predicate","value","item","String"],"mappings":";AA8CO,MAAMA;IAKT,YAAoBC,MAAgC,CAAE;aAAlCA,MAAM,GAANA;aAJZ,gBAAgB,GAAuB;aACvC,QAAQ,GAA4B,IAAM;QAI9C,IAAI,CAAC,eAAe,GAAG,IAAIC,IAAIC,OAAO,IAAI,CAACF,OAAO,YAAY;QAE9DG,eAA+C,IAAI,EAAE;YACjD,UAAUC,WAAW,GAAG;YACxB,SAASC;YACT,iBAAiBC;YACjB,kBAAkBA;QACtB;IACJ;IAEA,IAAI,UAAmC;QACnC,OAAO,IAAI,CAAC,QAAQ;IACxB;IAEA,OAAOC,KAAa,EAAU;QAC1B,OAAOA,MAAM,MAAM,CAAC,IAAI,CAAC,QAAQ;IACrC;IAEA,qBAAqBC,MAAwB,EAAW;QACpD,IAAIA,OAAO,MAAM,EACb,OAAO;QAEX,MAAMC,UAAUD,OAAO,OAAO,IAAI,CAAC;QACnC,OAAON,OAAO,IAAI,CAACO,SAAS,IAAI,CAACC,CAAAA,MAAO,CAAC,IAAI,CAAC,eAAe,CAAC,GAAG,CAACA;IACtE;IAEA,gBAAgBF,MAAwB,EAAEG,UAAoB,EAAQ;QAClE,IAAI,IAAI,CAAC,oBAAoB,CAACH,SAC1B,IAAI,CAAC,gBAAgB,GAAG,IAAIP,IAAIU;aAEhC,IAAI,CAAC,gBAAgB,GAAG;QAE5B,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,YAAY,CAACH;IACtC;IAEA,iBAAiBI,IAAc,EAAQ;QACnC,IAAI,IAAI,CAAC,gBAAgB,EACrB,KAAK,MAAMF,OAAOE,KACd,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAACF;IAGtC;IAEQ,aAAaF,MAAwB,EAA2B;QACpE,MAAMC,UAAUD,OAAO,OAAO,IAAI,CAAC;QACnC,MAAMK,kBAAkB,IAAI,CAAC,gBAAgB;QAC7C,MAAMC,cAAc,CAAC,CAACN,OAAO,MAAM;QACnC,MAAM,EAAEO,QAAQ,EAAEC,YAAY,EAAE,GAAG,IAAI,CAAC,MAAM;QAE9C,MAAMC,qBAAgF,EAAE;QACxF,KAAK,MAAM,CAACP,KAAKQ,UAAU,IAAIhB,OAAO,OAAO,CAACc,cAAe;YACzD,MAAMG,QAAQV,OAAO,CAACC,IAAI;YAC1B,IAAIS,QAAAA,OACAF,mBAAmB,IAAI,CAAC;gBAAEC;gBAAWC;YAAM;QAEnD;QAEA,OAAO,CAACC;YACJ,IAAIP,iBACA;gBAAA,IAAI,CAACA,gBAAgB,GAAG,CAACQ,OAAOD,IAAI,CAACL,SAAS,IAC1C,OAAO;YACX,OACG,IAAI,CAACD,aACR;gBAAA,KAAK,MAAM,EAAEI,SAAS,EAAEC,KAAK,EAAE,IAAIF,mBAC/B,IAAI,CAACC,UAAUE,MAAMD,QACjB,OAAO;YAEf;YAGJ,OAAO;QACX;IACJ;AACJ"}
1
+ {"version":3,"file":"presentation/listPresenter/QueryMatcher.js","sources":["../../../src/presentation/listPresenter/QueryMatcher.ts"],"sourcesContent":["import { makeObservable, observable, computed, action } from \"mobx\";\nimport type { IDataSourceQuery } from \"./abstractions.js\";\n\ntype ItemPredicate<TRow> = (item: TRow, value: unknown) => boolean;\n\ntype SortValueGetter<TRow> = (item: TRow, field: string) => unknown;\n\nexport interface QueryMatcherConfig<TRow> {\n keyField: keyof TRow & string;\n localFilters: Record<string, ItemPredicate<TRow>>;\n /**\n * Reads the value of a sort field from an item. Defaults to a dot-path lookup\n * (e.g. `savedOn`, `values.title`). Override when the query's sort field name\n * does not match the item's shape.\n */\n getSortValue?: SortValueGetter<TRow>;\n}\n\n/**\n * Decides which cached items belong in the current view.\n *\n * A DataSource backed by a shared cache (e.g., per-model entry cache) needs to\n * filter cached items to only those matching the active query. QueryMatcher does\n * this by splitting filters into two categories:\n *\n * - **Local filters** (e.g., folderId, status) — evaluated client-side against\n * each item using predicates provided via `localFilters` config. Used when\n * browsing without search or server-side filters.\n *\n * - **Server-side filters** (search, advanced search, any filter key not in\n * `localFilters`) — cannot be evaluated locally. Instead, QueryMatcher tracks\n * the set of item keys returned by the last server query (`_queryResultKeys`)\n * and restricts the view to those items.\n *\n * The `matcher` getter is MobX-computed, so any DataSource `rows` getter that\n * reads it will recompute when the matcher changes after a query.\n *\n * Usage:\n * ```ts\n * const matcher = new QueryMatcher<MyItem>({\n * keyField: \"id\",\n * localFilters: {\n * folderId: (item, value) => item.folderId === value,\n * status: (item, value) => item.status === value,\n * }\n * });\n *\n * // In DataSource.query(), after fetching:\n * matcher.updateFromQuery(params, result.data.map(item => item.id), result.meta.hasMoreItems);\n *\n * // In DataSource.rows getter (filters, sorts by the applied sort, and cuts off at the\n * // last loaded item):\n * return matcher.select(cache.getItems());\n * ```\n */\nexport class QueryMatcher<TRow> {\n private _queryResultKeys: Set<string> | null = null;\n private _matcher: (item: TRow) => boolean = () => true;\n private localFilterKeys: Set<string>;\n private getSortValue: SortValueGetter<TRow>;\n\n // Ordering state of the current view: the keys the server returned for the active\n // query (in server order, extended by `appendResultKeys`), the applied sort, and\n // whether the server has more items beyond the loaded ones.\n private _view: {\n resultKeys: string[];\n sort: IDataSourceQuery[\"sort\"];\n searching: boolean;\n hasMore: boolean;\n } | null = null;\n\n constructor(private config: QueryMatcherConfig<TRow>) {\n this.localFilterKeys = new Set(Object.keys(config.localFilters));\n this.getSortValue = config.getSortValue ?? getValueByPath;\n\n makeObservable<QueryMatcher<TRow>, \"_matcher\" | \"_view\">(this, {\n _matcher: observable.ref,\n _view: observable.ref,\n matcher: computed,\n updateFromQuery: action,\n appendResultKeys: action\n });\n }\n\n get matcher(): (item: TRow) => boolean {\n return this._matcher;\n }\n\n filter(items: TRow[]): TRow[] {\n return items.filter(this._matcher);\n }\n\n /**\n * Returns the items that belong in the current view, in the view's order.\n *\n * The cache is shared by views with different sorts, so its order means nothing to\n * this view. Instead, matching items are sorted by the applied sort (ties keep the\n * server order), and the result is cut off after the last item the server returned\n * for this view. That keeps locally created or updated items in their sort position,\n * and hides cached items that sort past the loaded pages until pagination reaches them.\n *\n * While searching, the server's (relevance) order is kept as is.\n */\n select(items: TRow[]): TRow[] {\n const matched = this.filter(items);\n const view = this._view;\n if (!view) {\n return matched;\n }\n\n const serverIndex = new Map(view.resultKeys.map((key, index) => [key, index]));\n // Items the server did not return for this view (cached by other views, or created\n // locally) sort after the server's items among equal sort values.\n const indexOf = (item: TRow) =>\n serverIndex.get(this.getKey(item)) ?? Number.POSITIVE_INFINITY;\n\n const sort = view.searching ? undefined : view.sort;\n const sorted = [...matched].sort((a, b) => {\n if (sort) {\n const result = compareValues(\n this.getSortValue(a, sort.field),\n this.getSortValue(b, sort.field)\n );\n if (result !== 0) {\n return sort.direction === \"ASC\" ? result : -result;\n }\n }\n const left = indexOf(a);\n const right = indexOf(b);\n return left === right ? 0 : left < right ? -1 : 1;\n });\n\n if (!view.hasMore) {\n return sorted;\n }\n\n let lastLoaded = -1;\n sorted.forEach((item, index) => {\n if (serverIndex.has(this.getKey(item))) {\n lastLoaded = index;\n }\n });\n\n return lastLoaded < 0 ? sorted : sorted.slice(0, lastLoaded + 1);\n }\n\n private getKey(item: TRow): string {\n return String(item[this.config.keyField]);\n }\n\n hasServerSideFilters(params: IDataSourceQuery): boolean {\n if (params.search) {\n return true;\n }\n const filters = params.filters ?? {};\n return Object.keys(filters).some(key => !this.localFilterKeys.has(key));\n }\n\n updateFromQuery(params: IDataSourceQuery, resultKeys: string[], hasMore = false): void {\n if (this.hasServerSideFilters(params)) {\n this._queryResultKeys = new Set(resultKeys);\n } else {\n this._queryResultKeys = null;\n }\n this._matcher = this.buildMatcher(params);\n this._view = {\n resultKeys: [...resultKeys],\n sort: params.sort,\n searching: !!params.search,\n hasMore\n };\n }\n\n appendResultKeys(keys: string[], hasMore = false): void {\n if (this._queryResultKeys) {\n for (const key of keys) {\n this._queryResultKeys.add(key);\n }\n }\n if (this._view) {\n this._view = {\n ...this._view,\n resultKeys: [...this._view.resultKeys, ...keys],\n hasMore\n };\n }\n }\n\n private buildMatcher(params: IDataSourceQuery): (item: TRow) => boolean {\n const filters = params.filters ?? {};\n const queryResultKeys = this._queryResultKeys;\n const isSearching = !!params.search;\n const { keyField, localFilters } = this.config;\n\n const activeLocalFilters: Array<{ predicate: ItemPredicate<TRow>; value: unknown }> = [];\n for (const [key, predicate] of Object.entries(localFilters)) {\n const value = filters[key];\n if (value !== undefined && value !== null) {\n activeLocalFilters.push({ predicate, value });\n }\n }\n\n return (item: TRow) => {\n if (queryResultKeys) {\n if (!queryResultKeys.has(String(item[keyField]))) {\n return false;\n }\n } else if (!isSearching) {\n for (const { predicate, value } of activeLocalFilters) {\n if (!predicate(item, value)) {\n return false;\n }\n }\n }\n\n return true;\n };\n }\n}\n\nfunction getValueByPath(item: unknown, path: string): unknown {\n let current: unknown = item;\n for (const part of path.split(\".\")) {\n if (current === null || current === undefined || typeof current !== \"object\") {\n return undefined;\n }\n current = (current as Record<string, unknown>)[part];\n }\n return current;\n}\n\n/**\n * Compares two sort values in ascending order, the same way the DDB and SQL storage sort\n * entries: `null` and `undefined` sort last (so first in descending order), and an empty\n * string is a regular string. Strings are compared by code point, which keeps ISO dates in\n * chronological order.\n */\nfunction compareValues(a: unknown, b: unknown): number {\n const aEmpty = a === null || a === undefined;\n const bEmpty = b === null || b === undefined;\n if (aEmpty || bEmpty) {\n return aEmpty === bEmpty ? 0 : aEmpty ? 1 : -1;\n }\n\n const left = a instanceof Date ? a.getTime() : a;\n const right = b instanceof Date ? b.getTime() : b;\n\n if (typeof left === \"number\" && typeof right === \"number\") {\n return left - right;\n }\n if (typeof left === \"boolean\" && typeof right === \"boolean\") {\n return Number(left) - Number(right);\n }\n\n const leftString = String(left);\n const rightString = String(right);\n return leftString < rightString ? -1 : leftString > rightString ? 1 : 0;\n}\n"],"names":["QueryMatcher","config","Set","Object","getValueByPath","makeObservable","observable","computed","action","items","matched","view","serverIndex","Map","key","index","indexOf","item","Number","sort","undefined","sorted","a","b","result","compareValues","left","right","lastLoaded","String","params","filters","resultKeys","hasMore","keys","queryResultKeys","isSearching","keyField","localFilters","activeLocalFilters","predicate","value","path","current","part","aEmpty","bEmpty","Date","leftString","rightString"],"mappings":"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"}