@asteby/metacore-runtime-react 46.4.0 → 47.1.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.
Files changed (123) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/action-modal-dispatcher.d.ts +10 -1
  3. package/dist/action-modal-dispatcher.d.ts.map +1 -1
  4. package/dist/action-modal-dispatcher.js +107 -19
  5. package/dist/audit-info.d.ts +54 -0
  6. package/dist/audit-info.d.ts.map +1 -0
  7. package/dist/audit-info.js +142 -0
  8. package/dist/business/catalog.d.ts.map +1 -1
  9. package/dist/business/catalog.js +24 -0
  10. package/dist/business/index.d.ts +3 -0
  11. package/dist/business/index.d.ts.map +1 -1
  12. package/dist/business/index.js +3 -0
  13. package/dist/business/print-send-dialog.d.ts +27 -0
  14. package/dist/business/print-send-dialog.d.ts.map +1 -0
  15. package/dist/business/print-send-dialog.js +55 -0
  16. package/dist/business/relate-documents.d.ts +41 -0
  17. package/dist/business/relate-documents.d.ts.map +1 -0
  18. package/dist/business/relate-documents.js +42 -0
  19. package/dist/business/vehicle-picker.d.ts +36 -0
  20. package/dist/business/vehicle-picker.d.ts.map +1 -0
  21. package/dist/business/vehicle-picker.js +61 -0
  22. package/dist/cfdi-stamp-panel.d.ts +29 -0
  23. package/dist/cfdi-stamp-panel.d.ts.map +1 -0
  24. package/dist/cfdi-stamp-panel.js +53 -0
  25. package/dist/dialogs/dynamic-record.d.ts.map +1 -1
  26. package/dist/dialogs/dynamic-record.js +10 -1
  27. package/dist/document-form-dialog.d.ts +23 -0
  28. package/dist/document-form-dialog.d.ts.map +1 -0
  29. package/dist/document-form-dialog.js +196 -0
  30. package/dist/dynamic-crud-page.d.ts +6 -0
  31. package/dist/dynamic-crud-page.d.ts.map +1 -1
  32. package/dist/dynamic-crud-page.js +4 -2
  33. package/dist/dynamic-form.d.ts.map +1 -1
  34. package/dist/dynamic-form.js +3 -0
  35. package/dist/dynamic-kanban.d.ts.map +1 -1
  36. package/dist/dynamic-kanban.js +11 -2
  37. package/dist/dynamic-line-items.d.ts.map +1 -1
  38. package/dist/dynamic-line-items.js +3 -0
  39. package/dist/dynamic-multi-select-field.d.ts.map +1 -1
  40. package/dist/dynamic-multi-select-field.js +2 -0
  41. package/dist/dynamic-select-field.d.ts.map +1 -1
  42. package/dist/dynamic-select-field.js +4 -0
  43. package/dist/dynamic-table.d.ts.map +1 -1
  44. package/dist/dynamic-table.js +6 -2
  45. package/dist/index.d.ts +8 -0
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +9 -0
  48. package/dist/nav-permissions.d.ts +19 -0
  49. package/dist/nav-permissions.d.ts.map +1 -0
  50. package/dist/nav-permissions.js +51 -0
  51. package/dist/navigation-builder.d.ts +6 -0
  52. package/dist/navigation-builder.d.ts.map +1 -1
  53. package/dist/navigation-builder.js +26 -1
  54. package/dist/option-filter.d.ts +31 -0
  55. package/dist/option-filter.d.ts.map +1 -0
  56. package/dist/option-filter.js +49 -0
  57. package/dist/permissions-manager.d.ts +9 -0
  58. package/dist/permissions-manager.d.ts.map +1 -1
  59. package/dist/permissions-manager.js +50 -17
  60. package/dist/print-document-button.d.ts.map +1 -1
  61. package/dist/print-document-button.js +9 -1
  62. package/dist/record-mutation-events.d.ts +14 -0
  63. package/dist/record-mutation-events.d.ts.map +1 -0
  64. package/dist/record-mutation-events.js +37 -0
  65. package/dist/types.d.ts +86 -0
  66. package/dist/types.d.ts.map +1 -1
  67. package/dist/use-options-resolver.d.ts +8 -0
  68. package/dist/use-options-resolver.d.ts.map +1 -1
  69. package/dist/use-options-resolver.js +8 -2
  70. package/dist/use-print-document.d.ts +15 -1
  71. package/dist/use-print-document.d.ts.map +1 -1
  72. package/dist/use-print-document.js +52 -1
  73. package/dist/use-record-mutation-tick.d.ts +6 -0
  74. package/dist/use-record-mutation-tick.d.ts.map +1 -0
  75. package/dist/use-record-mutation-tick.js +11 -0
  76. package/package.json +3 -3
  77. package/src/__tests__/action-modal-feedback.test.tsx +116 -0
  78. package/src/__tests__/action-modal-grid-layout.test.tsx +1 -1
  79. package/src/__tests__/apply-sidebar-layout.test.ts +1 -1
  80. package/src/__tests__/audit-info.test.tsx +103 -0
  81. package/src/__tests__/business-pickers.test.tsx +157 -0
  82. package/src/__tests__/dependent-options.test.tsx +12 -5
  83. package/src/__tests__/document-form-dialog.test.tsx +190 -0
  84. package/src/__tests__/dynamic-table-infinite-scroll.test.tsx +4 -0
  85. package/src/__tests__/dynamic-table-mutation-refresh.test.tsx +116 -0
  86. package/src/__tests__/line-items-lock-rows.test.tsx +2 -1
  87. package/src/__tests__/nav-permissions.test.tsx +76 -0
  88. package/src/__tests__/option-filter.test.tsx +115 -0
  89. package/src/__tests__/permissions-manager.test.tsx +21 -4
  90. package/src/__tests__/structured-view-value.test.tsx +5 -1
  91. package/src/__tests__/use-print-document-feedback.test.tsx +58 -0
  92. package/src/__tests__/wizard-action-modal.test.tsx +1 -1
  93. package/src/action-modal-dispatcher.tsx +124 -6
  94. package/src/audit-info.tsx +219 -0
  95. package/src/business/catalog.ts +24 -0
  96. package/src/business/index.ts +3 -0
  97. package/src/business/print-send-dialog.tsx +175 -0
  98. package/src/business/relate-documents.tsx +182 -0
  99. package/src/business/vehicle-picker.tsx +188 -0
  100. package/src/cfdi-stamp-panel.tsx +146 -0
  101. package/src/dialogs/dynamic-record.tsx +18 -0
  102. package/src/document-form-dialog.tsx +357 -0
  103. package/src/dropdown-menu-entry.test.ts +23 -0
  104. package/src/dynamic-crud-page.tsx +21 -2
  105. package/src/dynamic-form.tsx +3 -0
  106. package/src/dynamic-kanban.tsx +10 -4
  107. package/src/dynamic-line-items.tsx +3 -0
  108. package/src/dynamic-multi-select-field.tsx +2 -0
  109. package/src/dynamic-select-field.tsx +4 -0
  110. package/src/dynamic-table.tsx +6 -2
  111. package/src/index.ts +41 -0
  112. package/src/nav-permissions.ts +58 -0
  113. package/src/navigation-builder.tsx +27 -1
  114. package/src/option-filter.ts +88 -0
  115. package/src/permissions-manager.tsx +39 -26
  116. package/src/print-document-button.tsx +9 -1
  117. package/src/record-mutation-events.ts +50 -0
  118. package/src/types.ts +90 -0
  119. package/src/use-options-resolver.ts +19 -1
  120. package/src/use-print-document.ts +66 -3
  121. package/src/use-record-mutation-tick.ts +12 -0
  122. package/vitest.config.ts +1 -0
  123. package/vitest.setup.ts +7 -0
@@ -0,0 +1,19 @@
1
+ import { type CanFn } from './permissions-context';
2
+ /** Minimal structural shape of a sidebar leaf (matches `@asteby/metacore-ui` NavLinkItem). */
3
+ export interface NavLeafLike {
4
+ url: string;
5
+ requires?: string;
6
+ }
7
+ /** Model key of a `/m/<model>` url, or undefined for any other route. */
8
+ export declare function modelFromNavUrl(url: string | undefined): string | undefined;
9
+ /** Capability a leaf needs, or undefined when it is not gated by the SDK. */
10
+ export declare function capabilityForNavItem(item: NavLeafLike): string | undefined;
11
+ /** Pure visibility predicate for a leaf given a capability checker. */
12
+ export declare function isNavItemAllowed(item: NavLeafLike, can: CanFn): boolean;
13
+ /**
14
+ * Hook for the host's sidebar: returns a predicate for `<AppSidebar
15
+ * isItemVisible>` / `filterNavGroups`, or `undefined` when permission gating
16
+ * is not active (nothing to filter).
17
+ */
18
+ export declare function useNavItemVisible(): ((item: NavLeafLike) => boolean) | undefined;
19
+ //# sourceMappingURL=nav-permissions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nav-permissions.d.ts","sourceRoot":"","sources":["../src/nav-permissions.ts"],"names":[],"mappings":"AAYA,OAAO,EAAiD,KAAK,KAAK,EAAE,MAAM,uBAAuB,CAAA;AAEjG,8FAA8F;AAC9F,MAAM,WAAW,WAAW;IACxB,GAAG,EAAE,MAAM,CAAA;IACX,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAID,yEAAyE;AACzE,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAS3E;AAED,6EAA6E;AAC7E,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,CAI1E;AAED,uEAAuE;AACvE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,KAAK,GAAG,OAAO,CAGvE;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,CAAC,CAAC,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,GAAG,SAAS,CAKhF"}
@@ -0,0 +1,51 @@
1
+ // nav-permissions — the sidebar follows the role's EFFECTIVE permissions
2
+ // (PIT-044). Before, every installed addon's entries were listed for every
3
+ // role and only failed with a 403 after the click.
4
+ //
5
+ // Rules (same capability vocabulary as useCan/gateTableMetadata):
6
+ // - no <PermissionsProvider> mounted, or isAdmin / "*" -> nothing hidden
7
+ // (useCan() is always-true then, so the predicate is a no-op);
8
+ // - an entry with an explicit `requires` capability -> can(requires);
9
+ // - an entry pointing at a model screen (`/m/<model>`) -> can('<model>.index');
10
+ // - any other entry (custom pages, activity, settings…) -> kept. Those are
11
+ // gated by the backend / the host's own `requires`, never guessed here.
12
+ import { useCallback } from 'react';
13
+ import { modelCapability, useCan, usePermissionsActive } from './permissions-context';
14
+ const MODEL_URL_RE = /^\/(?:app\/)?m\/([^/?#]+)/i;
15
+ /** Model key of a `/m/<model>` url, or undefined for any other route. */
16
+ export function modelFromNavUrl(url) {
17
+ if (!url)
18
+ return undefined;
19
+ const m = MODEL_URL_RE.exec(url);
20
+ if (!m)
21
+ return undefined;
22
+ try {
23
+ return decodeURIComponent(m[1]);
24
+ }
25
+ catch {
26
+ return m[1];
27
+ }
28
+ }
29
+ /** Capability a leaf needs, or undefined when it is not gated by the SDK. */
30
+ export function capabilityForNavItem(item) {
31
+ if (item.requires)
32
+ return item.requires;
33
+ const model = modelFromNavUrl(item.url);
34
+ return model ? modelCapability(model, 'index') : undefined;
35
+ }
36
+ /** Pure visibility predicate for a leaf given a capability checker. */
37
+ export function isNavItemAllowed(item, can) {
38
+ const cap = capabilityForNavItem(item);
39
+ return cap ? can(cap) : true;
40
+ }
41
+ /**
42
+ * Hook for the host's sidebar: returns a predicate for `<AppSidebar
43
+ * isItemVisible>` / `filterNavGroups`, or `undefined` when permission gating
44
+ * is not active (nothing to filter).
45
+ */
46
+ export function useNavItemVisible() {
47
+ const can = useCan();
48
+ const active = usePermissionsActive();
49
+ const predicate = useCallback((item) => isNavItemAllowed(item, can), [can]);
50
+ return active ? predicate : undefined;
51
+ }
@@ -1,3 +1,4 @@
1
+ import { type CanFn } from './permissions-context';
1
2
  export interface NavItem {
2
3
  key: string;
3
4
  label: string;
@@ -22,6 +23,11 @@ export interface AddonNavigationContribution {
22
23
  * conflict the higher-priority wins, then the later contribution.
23
24
  */
24
25
  export declare function mergeNavigation(base: NavItem[], contributions: AddonNavigationContribution[]): NavItem[];
26
+ /**
27
+ * Drops items whose `requires` capability the user lacks (PIT-044). A parent
28
+ * left without children is dropped too, unless it links somewhere itself.
29
+ */
30
+ export declare function filterNavigationByCapability(items: NavItem[], can: CanFn): NavItem[];
25
31
  export declare function useNavigation(base: NavItem[], contributions: AddonNavigationContribution[]): NavItem[];
26
32
  export interface NavigationBuilderProps {
27
33
  base: NavItem[];
@@ -1 +1 @@
1
- {"version":3,"file":"navigation-builder.d.ts","sourceRoot":"","sources":["../src/navigation-builder.tsx"],"names":[],"mappings":"AAIA,MAAM,WAAW,OAAO;IACpB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,gDAAgD;IAChD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAA;IACpB,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,oDAAoD;IACpD,MAAM,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,2BAA2B;IACxC,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,OAAO,EAAE,CAAA;CACnB;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,aAAa,EAAE,2BAA2B,EAAE,GAAG,OAAO,EAAE,CA8BxG;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,aAAa,EAAE,2BAA2B,EAAE,GAAG,OAAO,EAAE,CAEtG;AAED,MAAM,WAAW,sBAAsB;IACnC,IAAI,EAAE,OAAO,EAAE,CAAA;IACf,aAAa,EAAE,2BAA2B,EAAE,CAAA;IAC5C,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,KAAK,CAAC,SAAS,CAAA;CAChD;AAED;6CAC6C;AAC7C,wBAAgB,iBAAiB,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,EAAE,sBAAsB,+BAGxF"}
1
+ {"version":3,"file":"navigation-builder.d.ts","sourceRoot":"","sources":["../src/navigation-builder.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAU,KAAK,KAAK,EAAE,MAAM,uBAAuB,CAAA;AAE1D,MAAM,WAAW,OAAO;IACpB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,gDAAgD;IAChD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAA;IACpB,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,oDAAoD;IACpD,MAAM,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,2BAA2B;IACxC,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,OAAO,EAAE,CAAA;CACnB;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,aAAa,EAAE,2BAA2B,EAAE,GAAG,OAAO,EAAE,CA8BxG;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,KAAK,GAAG,OAAO,EAAE,CAapF;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,aAAa,EAAE,2BAA2B,EAAE,GAAG,OAAO,EAAE,CAQtG;AAED,MAAM,WAAW,sBAAsB;IACnC,IAAI,EAAE,OAAO,EAAE,CAAA;IACf,aAAa,EAAE,2BAA2B,EAAE,CAAA;IAC5C,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,KAAK,CAAC,SAAS,CAAA;CAChD;AAED;6CAC6C;AAC7C,wBAAgB,iBAAiB,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,EAAE,sBAAsB,+BAGxF"}
@@ -2,6 +2,7 @@ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
2
  // NavigationBuilder — merges a host's base sidebar with `manifest.navigation`
3
3
  // contributions from every loaded addon. Pure function + a React hook.
4
4
  import { useMemo } from 'react';
5
+ import { useCan } from './permissions-context';
5
6
  /** Deep-merge nav trees by `key`. Children are merged recursively; on
6
7
  * conflict the higher-priority wins, then the later contribution.
7
8
  */
@@ -34,8 +35,32 @@ export function mergeNavigation(base, contributions) {
34
35
  .map(k => byKey.get(k))
35
36
  .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
36
37
  }
38
+ /**
39
+ * Drops items whose `requires` capability the user lacks (PIT-044). A parent
40
+ * left without children is dropped too, unless it links somewhere itself.
41
+ */
42
+ export function filterNavigationByCapability(items, can) {
43
+ const out = [];
44
+ for (const item of items) {
45
+ if (item.requires && !can(item.requires))
46
+ continue;
47
+ if (item.children) {
48
+ const children = filterNavigationByCapability(item.children, can);
49
+ if (children.length === 0 && item.children.length > 0 && !item.to)
50
+ continue;
51
+ out.push({ ...item, children });
52
+ }
53
+ else {
54
+ out.push(item);
55
+ }
56
+ }
57
+ return out;
58
+ }
37
59
  export function useNavigation(base, contributions) {
38
- return useMemo(() => mergeNavigation(base, contributions), [base, contributions]);
60
+ // useCan() allows everything without a <PermissionsProvider>, so hosts that
61
+ // never opt in keep the full tree.
62
+ const can = useCan();
63
+ return useMemo(() => filterNavigationByCapability(mergeNavigation(base, contributions), can), [base, contributions, can]);
39
64
  }
40
65
  /** Render-prop component for hosts that want the merge logic but render
41
66
  * the sidebar with their own primitives. */
@@ -0,0 +1,31 @@
1
+ import type { ResolvedOption } from './use-options-resolver';
2
+ export interface OptionFilterRule {
3
+ /** Option property to test: a `meta` key (e.g. `status`) or `id`/`value`/`label`/`name`/`description`/`color`/`icon`. */
4
+ field: string;
5
+ /** Keep only options whose value equals this. */
6
+ equals?: string | number | boolean;
7
+ /** snake_case alias of `notEquals`. */
8
+ not_equals?: string | number | boolean;
9
+ /** Hide options whose value equals this. */
10
+ notEquals?: string | number | boolean;
11
+ /** Keep only options whose value is one of these. */
12
+ in?: Array<string | number | boolean>;
13
+ /** Hide options whose value is one of these. */
14
+ not_in?: Array<string | number | boolean>;
15
+ /** camelCase alias of `not_in`. */
16
+ notIn?: Array<string | number | boolean>;
17
+ }
18
+ /** One rule, or a list of rules that must ALL pass (AND). */
19
+ export type OptionFilter = OptionFilterRule | OptionFilterRule[];
20
+ /** Reads the filter from a field (camelCase or the snake_case the kernel serves). */
21
+ export declare function getOptionFilter(field: unknown): OptionFilterRule[];
22
+ /** True when the option satisfies the rule. Comparison is trimmed + case-insensitive. */
23
+ export declare function optionPassesRule(opt: ResolvedOption, rule: OptionFilterRule): boolean;
24
+ /**
25
+ * Applies the field's filter to resolved options. Returns the SAME array when
26
+ * there is nothing to filter. `keepValue` (the current selection) is never
27
+ * hidden, so an existing record keeps showing its label after the option
28
+ * became ineligible.
29
+ */
30
+ export declare function applyOptionFilter(options: ResolvedOption[], rules: OptionFilterRule[], keepValue?: unknown): ResolvedOption[];
31
+ //# sourceMappingURL=option-filter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"option-filter.d.ts","sourceRoot":"","sources":["../src/option-filter.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAE5D,MAAM,WAAW,gBAAgB;IAC7B,yHAAyH;IACzH,KAAK,EAAE,MAAM,CAAA;IACb,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;IAClC,uCAAuC;IACvC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;IACtC,4CAA4C;IAC5C,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;IACrC,qDAAqD;IACrD,EAAE,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAA;IACrC,gDAAgD;IAChD,MAAM,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAA;IACzC,mCAAmC;IACnC,KAAK,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAA;CAC3C;AAED,6DAA6D;AAC7D,MAAM,MAAM,YAAY,GAAG,gBAAgB,GAAG,gBAAgB,EAAE,CAAA;AAIhE,qFAAqF;AACrF,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,gBAAgB,EAAE,CASlE;AAaD,yFAAyF;AACzF,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAarF;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC7B,OAAO,EAAE,cAAc,EAAE,EACzB,KAAK,EAAE,gBAAgB,EAAE,EACzB,SAAS,CAAC,EAAE,OAAO,GACpB,cAAc,EAAE,CAMlB"}
@@ -0,0 +1,49 @@
1
+ /** Reads the filter from a field (camelCase or the snake_case the kernel serves). */
2
+ export function getOptionFilter(field) {
3
+ if (!field || typeof field !== 'object')
4
+ return [];
5
+ const f = field;
6
+ const raw = f.optionFilter ?? f.option_filter;
7
+ const list = Array.isArray(raw) ? raw : raw && typeof raw === 'object' ? [raw] : [];
8
+ return list.filter((r) => !!r && typeof r === 'object' && typeof r.field === 'string' && r.field !== '');
9
+ }
10
+ function norm(v) {
11
+ return String(v).trim().toLowerCase();
12
+ }
13
+ function readProp(opt, key) {
14
+ if (key === 'id' || key === 'value' || key === 'label' || key === 'name' || key === 'description' || key === 'color' || key === 'icon' || key === 'image') {
15
+ return opt[key];
16
+ }
17
+ return opt.meta?.[key];
18
+ }
19
+ /** True when the option satisfies the rule. Comparison is trimmed + case-insensitive. */
20
+ export function optionPassesRule(opt, rule) {
21
+ const raw = readProp(opt, rule.field);
22
+ const known = raw !== undefined && raw !== null;
23
+ const v = known ? norm(raw) : '';
24
+ const notEquals = rule.notEquals ?? rule.not_equals;
25
+ const notIn = rule.notIn ?? rule.not_in;
26
+ // Positive rules need the property to exist; negative rules cannot exclude
27
+ // what they cannot see, so an option lacking the property is kept.
28
+ if (rule.equals !== undefined && !(known && v === norm(rule.equals)))
29
+ return false;
30
+ if (Array.isArray(rule.in) && !(known && rule.in.some((x) => norm(x) === v)))
31
+ return false;
32
+ if (notEquals !== undefined && known && v === norm(notEquals))
33
+ return false;
34
+ if (Array.isArray(notIn) && known && notIn.some((x) => norm(x) === v))
35
+ return false;
36
+ return true;
37
+ }
38
+ /**
39
+ * Applies the field's filter to resolved options. Returns the SAME array when
40
+ * there is nothing to filter. `keepValue` (the current selection) is never
41
+ * hidden, so an existing record keeps showing its label after the option
42
+ * became ineligible.
43
+ */
44
+ export function applyOptionFilter(options, rules, keepValue) {
45
+ if (rules.length === 0)
46
+ return options;
47
+ const keep = keepValue === undefined || keepValue === null || keepValue === '' ? null : String(keepValue);
48
+ return options.filter((o) => (keep !== null && String(o.id) === keep) || rules.every((r) => optionPassesRule(o, r)));
49
+ }
@@ -141,6 +141,15 @@ export declare function capabilitySetsEqual(a: ReadonlySet<string>, b: ReadonlyS
141
141
  export declare function defaultActionIcon(actionKey: string, kind?: string): string;
142
142
  /** Suggest a Lucide icon from the role label (create-dialog default). */
143
143
  export declare function suggestRoleIcon(label: string): string;
144
+ /** Label of the synthetic module that hosts the general (non-model) flags. */
145
+ export declare const GENERAL_MODULE_LABEL = "Permisos generales";
146
+ /**
147
+ * The general flags (`general.work_after_hours`, …) as a synthetic module, so
148
+ * they are picked from the module combobox and edited in the same two-column
149
+ * action grid as any module's actions (#1021). Each flag keeps its full
150
+ * capability key through `PermissionActionDef.capability`.
151
+ */
152
+ export declare function generalPermissionsModule(general: GeneralPermissionDef[]): PermissionModuleDef;
144
153
  /**
145
154
  * Normalize whatever `loadModules` returned into the canonical grouped shape.
146
155
  *
@@ -1 +1 @@
1
- {"version":3,"file":"permissions-manager.d.ts","sourceRoot":"","sources":["../src/permissions-manager.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"permissions-manager.d.ts","sourceRoot":"","sources":["../src/permissions-manager.tsx"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // permissions-manager — "Permisos y Roles" pro view (rol × módulo × acción).
3
3
  //
4
4
  // Transport-agnostic: every read/write arrives via props (loaders/mutators),
@@ -29,7 +29,7 @@ import { Check, ChevronsUpDown, CheckCheck, ChevronDown, Eraser, Pencil, Plus, S
29
29
  import { toast } from 'sonner';
30
30
  import { extractServerError } from './server-error';
31
31
  import { cn } from '@asteby/metacore-ui/lib';
32
- import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Checkbox, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, Popover, PopoverContent, PopoverTrigger, Separator, Skeleton, Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@asteby/metacore-ui/primitives';
32
+ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Checkbox, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, Popover, PopoverContent, PopoverTrigger, Skeleton, Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@asteby/metacore-ui/primitives';
33
33
  import { DynamicIcon } from './dynamic-icon';
34
34
  import { IconPickerField } from './icon-picker-field';
35
35
  import { ColorPickerField, DEFAULT_ROLE_COLOR } from './color-picker-field';
@@ -159,6 +159,29 @@ export function suggestRoleIcon(label) {
159
159
  }
160
160
  return 'Shield';
161
161
  }
162
+ /** Label of the synthetic module that hosts the general (non-model) flags. */
163
+ export const GENERAL_MODULE_LABEL = 'Permisos generales';
164
+ /**
165
+ * The general flags (`general.work_after_hours`, …) as a synthetic module, so
166
+ * they are picked from the module combobox and edited in the same two-column
167
+ * action grid as any module's actions (#1021). Each flag keeps its full
168
+ * capability key through `PermissionActionDef.capability`.
169
+ */
170
+ export function generalPermissionsModule(general) {
171
+ return {
172
+ key: 'general',
173
+ label: GENERAL_MODULE_LABEL,
174
+ icon: 'SlidersHorizontal',
175
+ kind: 'model',
176
+ actions: general.map((g) => ({
177
+ key: g.key.replace(/^general\./, ''),
178
+ label: g.label,
179
+ description: g.description,
180
+ capability: g.key,
181
+ kind: 'custom',
182
+ })),
183
+ };
184
+ }
162
185
  /**
163
186
  * Normalize whatever `loadModules` returned into the canonical grouped shape.
164
187
  *
@@ -287,6 +310,16 @@ export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions
287
310
  const [deleting, setDeleting] = React.useState(false);
288
311
  const loading = groups === null || roles === null;
289
312
  const allModules = React.useMemo(() => (groups ? flattenGroups(groups) : []), [groups]);
313
+ // What the module picker lists: the catalog groups plus a trailing synthetic
314
+ // group for the general flags. `allModules` / "Otorgar todo" keep using the
315
+ // plain catalog, so that button's scope does not change.
316
+ const pickerGroups = React.useMemo(() => {
317
+ if (!groups)
318
+ return groups;
319
+ if (!general || general.length === 0)
320
+ return groups;
321
+ return [...groups, { title: '', modules: [generalPermissionsModule(general)] }];
322
+ }, [groups, general]);
290
323
  // ---- initial load: catalog + roles in parallel -------------------------
291
324
  React.useEffect(() => {
292
325
  let cancelled = false;
@@ -343,7 +376,7 @@ export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions
343
376
  // eslint-disable-next-line react-hooks/exhaustive-deps
344
377
  }, [activeRoleId]);
345
378
  const activeRole = React.useMemo(() => roles?.find((r) => r.id === activeRoleId) ?? null, [roles, activeRoleId]);
346
- const activeModule = React.useMemo(() => moduleAtEntry(groups, activeModuleKey)?.module ?? null, [groups, activeModuleKey]);
379
+ const activeModule = React.useMemo(() => moduleAtEntry(pickerGroups, activeModuleKey)?.module ?? null, [pickerGroups, activeModuleKey]);
347
380
  const dirty = baseline !== null && draft !== null && !capabilitySetsEqual(baseline, draft);
348
381
  // ---- capability edits ---------------------------------------------------
349
382
  const toggleCapability = React.useCallback((cap) => {
@@ -513,7 +546,7 @@ export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions
513
546
  const moduleGranted = activeModule && draft ? grantedCountForModule(draft, activeModule) : 0;
514
547
  const moduleTotal = activeModule?.actions.length ?? 0;
515
548
  const checksDisabled = !activeRole || !draft || loadingPerms || saving;
516
- const activeModuleGroupTitle = React.useMemo(() => moduleAtEntry(groups, activeModuleKey)?.group.title ?? '', [groups, activeModuleKey]);
549
+ const activeModuleGroupTitle = React.useMemo(() => moduleAtEntry(pickerGroups, activeModuleKey)?.group.title ?? '', [pickerGroups, activeModuleKey]);
517
550
  // ---- render --------------------------------------------------------------
518
551
  if (loadError) {
519
552
  return (_jsxs("div", { className: cn('flex flex-col items-center justify-center gap-2 py-16 text-muted-foreground', className), children: [_jsx(Shield, { className: "h-8 w-8 opacity-40" }), _jsx("p", { className: "text-sm", children: "No se pudo cargar el cat\u00E1logo de permisos." })] }));
@@ -528,23 +561,23 @@ export function PermissionsManager({ loadModules, loadRoles, loadRolePermissions
528
561
  color: DEFAULT_ROLE_COLOR,
529
562
  icon: 'Shield',
530
563
  grantAll: false,
531
- }), children: [_jsx(Plus, { className: "mr-1.5 h-4 w-4" }), " Nuevo rol"] })), _jsxs(Button, { variant: "outline", onClick: () => setAllPermissions(true), disabled: checksDisabled || !allCatalogCapabilities.length, title: "Marca todos los permisos de todos los m\u00F3dulos", children: [_jsx(CheckCheck, { className: "mr-1.5 h-4 w-4" }), " Otorgar todo"] }), _jsxs(Button, { variant: "outline", onClick: () => setAllPermissions(false), disabled: checksDisabled || !draft?.size, title: "Quita todos los permisos del rol", children: [_jsx(Eraser, { className: "mr-1.5 h-4 w-4" }), " Quitar todo"] }), _jsxs(Button, { onClick: handleSave, disabled: !dirty || saving || !activeRole, className: "bg-emerald-600 text-white hover:bg-emerald-700", children: [_jsx(Save, { className: "mr-1.5 h-4 w-4" }), saving ? 'Guardando…' : 'Guardar permisos'] })] })] }), _jsxs("div", { className: "grid items-start gap-4 lg:grid-cols-[340px_1fr]", children: [_jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { className: "text-base", children: "Rol" }), _jsx(CardDescription, { children: "Selecciona el rol a configurar." })] }), _jsxs(CardContent, { className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs(Popover, { open: roleOpen, onOpenChange: setRoleOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", role: "combobox", "aria-expanded": roleOpen, className: "min-w-0 flex-1 justify-between font-normal", children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [activeRole && (_jsx("span", { className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-md", style: {
532
- background: `${activeRole.color || '#64748b'}22`,
533
- color: activeRole.color || '#64748b',
534
- }, "aria-hidden": "true", children: _jsx(DynamicIcon, { name: activeRole.icon || 'Shield', className: "h-3.5 w-3.5" }) })), _jsx("span", { className: "truncate", children: activeRole
535
- ? activeRole.label || activeRole.name
536
- : 'Seleccionar rol…' })] }), _jsx(ChevronsUpDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })] }) }), _jsx(PopoverContent, { className: "w-[280px] p-0", align: "start", children: _jsxs(Command, { children: [_jsx(CommandInput, { placeholder: "Buscar rol\u2026" }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: "Sin resultados." }), _jsx(CommandGroup, { children: (roles ?? []).map((role) => (_jsxs(CommandItem, { value: `${role.label || ''} ${role.name}`, onSelect: () => {
537
- requestRoleSwitch(role.id);
538
- setRoleOpen(false);
539
- }, children: [_jsx("span", { className: "mr-2 flex h-6 w-6 shrink-0 items-center justify-center rounded-md", style: {
540
- background: `${role.color || '#64748b'}22`,
541
- color: role.color || '#64748b',
542
- }, "aria-hidden": "true", children: _jsx(DynamicIcon, { name: role.icon || 'Shield', className: "h-3.5 w-3.5" }) }), _jsx("span", { className: "truncate", children: role.label || role.name }), role.id === activeRoleId && (_jsx(Check, { className: "ml-auto h-4 w-4" }))] }, role.id))) })] })] }) })] }), updateRole && (_jsx(Button, { variant: "outline", size: "icon", className: "h-9 w-9 shrink-0", "aria-label": "Editar rol", disabled: !activeRole, onClick: openEditRole, children: _jsx(Pencil, { className: "h-4 w-4" }) })), deleteRole && (_jsx(Button, { variant: "outline", size: "icon", className: "h-9 w-9 shrink-0 text-destructive hover:text-destructive", "aria-label": "Eliminar rol", disabled: !activeRole, onClick: () => setDeleteOpen(true), children: _jsx(Trash2, { className: "h-4 w-4" }) }))] }), (general?.length ?? 0) > 0 && (_jsxs(_Fragment, { children: [_jsx(Separator, {}), _jsxs("div", { children: [_jsx("h3", { className: "mb-2 text-sm font-semibold", children: "Permisos Generales" }), _jsx("div", { className: "flex flex-col gap-2", children: general.map((g) => (_jsx(CapabilityCheck, { checked: draft?.has(g.key) ?? false, disabled: checksDisabled, onToggle: () => toggleCapability(g.key), label: g.label, description: g.description }, g.key))) })] })] }))] })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { className: "text-base", children: "M\u00F3dulo" }), _jsx(CardDescription, { children: "Elige el m\u00F3dulo cuyas acciones quieres configurar." })] }), _jsx(CardContent, { children: _jsxs(Popover, { open: moduleOpen, onOpenChange: setModuleOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", role: "combobox", "aria-expanded": moduleOpen, className: "w-full justify-between font-normal", children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [activeModule && (_jsx(DynamicIcon, { name: activeModule.icon ||
564
+ }), children: [_jsx(Plus, { className: "mr-1.5 h-4 w-4" }), " Nuevo rol"] })), _jsxs(Button, { variant: "outline", onClick: () => setAllPermissions(true), disabled: checksDisabled || !allCatalogCapabilities.length, title: "Marca todos los permisos de todos los m\u00F3dulos", children: [_jsx(CheckCheck, { className: "mr-1.5 h-4 w-4" }), " Otorgar todo"] }), _jsxs(Button, { variant: "outline", onClick: () => setAllPermissions(false), disabled: checksDisabled || !draft?.size, title: "Quita todos los permisos del rol", children: [_jsx(Eraser, { className: "mr-1.5 h-4 w-4" }), " Quitar todo"] }), _jsxs(Button, { onClick: handleSave, disabled: !dirty || saving || !activeRole, className: "bg-emerald-600 text-white hover:bg-emerald-700", children: [_jsx(Save, { className: "mr-1.5 h-4 w-4" }), saving ? 'Guardando…' : 'Guardar permisos'] })] })] }), _jsxs("div", { className: "grid items-start gap-4 lg:grid-cols-[340px_1fr]", children: [_jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { className: "text-base", children: "Rol" }), _jsx(CardDescription, { children: "Selecciona el rol a configurar." })] }), _jsx(CardContent, { className: "flex flex-col gap-3", children: _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs(Popover, { open: roleOpen, onOpenChange: setRoleOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", role: "combobox", "aria-expanded": roleOpen, className: "min-w-0 flex-1 justify-between font-normal", children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [activeRole && (_jsx("span", { className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-md", style: {
565
+ background: `${activeRole.color || '#64748b'}22`,
566
+ color: activeRole.color || '#64748b',
567
+ }, "aria-hidden": "true", children: _jsx(DynamicIcon, { name: activeRole.icon || 'Shield', className: "h-3.5 w-3.5" }) })), _jsx("span", { className: "truncate", children: activeRole
568
+ ? activeRole.label || activeRole.name
569
+ : 'Seleccionar rol…' })] }), _jsx(ChevronsUpDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })] }) }), _jsx(PopoverContent, { className: "w-[280px] p-0", align: "start", children: _jsxs(Command, { children: [_jsx(CommandInput, { placeholder: "Buscar rol\u2026" }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: "Sin resultados." }), _jsx(CommandGroup, { children: (roles ?? []).map((role) => (_jsxs(CommandItem, { value: `${role.label || ''} ${role.name}`, onSelect: () => {
570
+ requestRoleSwitch(role.id);
571
+ setRoleOpen(false);
572
+ }, children: [_jsx("span", { className: "mr-2 flex h-6 w-6 shrink-0 items-center justify-center rounded-md", style: {
573
+ background: `${role.color || '#64748b'}22`,
574
+ color: role.color || '#64748b',
575
+ }, "aria-hidden": "true", children: _jsx(DynamicIcon, { name: role.icon || 'Shield', className: "h-3.5 w-3.5" }) }), _jsx("span", { className: "truncate", children: role.label || role.name }), role.id === activeRoleId && (_jsx(Check, { className: "ml-auto h-4 w-4" }))] }, role.id))) })] })] }) })] }), updateRole && (_jsx(Button, { variant: "outline", size: "icon", className: "h-9 w-9 shrink-0", "aria-label": "Editar rol", disabled: !activeRole, onClick: openEditRole, children: _jsx(Pencil, { className: "h-4 w-4" }) })), deleteRole && (_jsx(Button, { variant: "outline", size: "icon", className: "h-9 w-9 shrink-0 text-destructive hover:text-destructive", "aria-label": "Eliminar rol", disabled: !activeRole, onClick: () => setDeleteOpen(true), children: _jsx(Trash2, { className: "h-4 w-4" }) }))] }) })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { className: "text-base", children: "M\u00F3dulo" }), _jsx(CardDescription, { children: "Elige el m\u00F3dulo cuyas acciones quieres configurar." })] }), _jsx(CardContent, { children: _jsxs(Popover, { open: moduleOpen, onOpenChange: setModuleOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", role: "combobox", "aria-expanded": moduleOpen, className: "w-full justify-between font-normal", children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [activeModule && (_jsx(DynamicIcon, { name: activeModule.icon ||
543
576
  (activeModule.kind === 'screen'
544
577
  ? 'Eye'
545
578
  : 'Square'), className: "h-4 w-4 shrink-0 opacity-70" })), _jsx("span", { className: "truncate", children: activeModule
546
579
  ? activeModule.label
547
- : 'Seleccionar módulo…' })] }), _jsx(ChevronsUpDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })] }) }), _jsx(PopoverContent, { className: "w-[var(--radix-popover-trigger-width)] min-w-[280px] p-0", align: "start", children: _jsxs(Command, { children: [_jsx(CommandInput, { placeholder: "Buscar m\u00F3dulo\u2026" }), _jsxs(CommandList, { className: "max-h-[360px]", children: [_jsx(CommandEmpty, { children: "Sin m\u00F3dulos." }), (groups ?? []).map((group, gi) => (_jsx(CommandGroup, { heading: group.title || undefined, children: group.modules.map((mod, mi) => (_jsxs(CommandItem, { value: `${group.title} ${mod.label} ${mod.key} ${moduleEntryId(gi, mi)}`, onSelect: () => {
580
+ : 'Seleccionar módulo…' })] }), _jsx(ChevronsUpDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })] }) }), _jsx(PopoverContent, { className: "w-[var(--radix-popover-trigger-width)] min-w-[280px] p-0", align: "start", children: _jsxs(Command, { children: [_jsx(CommandInput, { placeholder: "Buscar m\u00F3dulo\u2026" }), _jsxs(CommandList, { className: "max-h-[360px]", children: [_jsx(CommandEmpty, { children: "Sin m\u00F3dulos." }), (pickerGroups ?? []).map((group, gi) => (_jsx(CommandGroup, { heading: group.title || undefined, children: group.modules.map((mod, mi) => (_jsxs(CommandItem, { value: `${group.title} ${mod.label} ${mod.key} ${moduleEntryId(gi, mi)}`, onSelect: () => {
548
581
  setActiveModuleKey(moduleEntryId(gi, mi));
549
582
  setModuleOpen(false);
550
583
  }, children: [_jsx(DynamicIcon, { name: mod.icon ||
@@ -1 +1 @@
1
- {"version":3,"file":"print-document-button.d.ts","sourceRoot":"","sources":["../src/print-document-button.tsx"],"names":[],"mappings":"AASA,OAAO,KAAgC,MAAM,OAAO,CAAA;AACpD,OAAO,EAAoB,KAAK,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAE/E,MAAM,WAAW,wBACb,SAAQ,IAAI,CAAC,KAAK,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAAE,SAAS,CAAC;IACtE,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAA;IACb,iBAAiB;IACjB,EAAE,EAAE,MAAM,CAAA;IACV,4EAA4E;IAC5E,WAAW,EAAE,MAAM,CAAA;IACnB,gEAAgE;IAChE,IAAI,CAAC,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAA;IAChC,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,2EAA2E;IAC3E,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAA;IAChC,qEAAqE;IACrE,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC9B,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC7B;AAED,wBAAgB,mBAAmB,CAAC,EAChC,KAAK,EACL,EAAE,EACF,WAAW,EACX,IAAI,EACJ,QAAQ,EACR,OAAO,EACP,YAAY,EACZ,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,GAAG,IAAI,EACV,EAAE,wBAAwB,qBA+B1B"}
1
+ {"version":3,"file":"print-document-button.d.ts","sourceRoot":"","sources":["../src/print-document-button.tsx"],"names":[],"mappings":"AASA,OAAO,KAAgC,MAAM,OAAO,CAAA;AACpD,OAAO,EAAoB,KAAK,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAE/E,MAAM,WAAW,wBACb,SAAQ,IAAI,CAAC,KAAK,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAAE,SAAS,CAAC;IACtE,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAA;IACb,iBAAiB;IACjB,EAAE,EAAE,MAAM,CAAA;IACV,4EAA4E;IAC5E,WAAW,EAAE,MAAM,CAAA;IACnB,gEAAgE;IAChE,IAAI,CAAC,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAA;IAChC,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,2EAA2E;IAC3E,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAA;IAChC,qEAAqE;IACrE,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC9B,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC7B;AAED,wBAAgB,mBAAmB,CAAC,EAChC,KAAK,EACL,EAAE,EACF,WAAW,EACX,IAAI,EACJ,QAAQ,EACR,OAAO,EACP,YAAY,EACZ,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,GAAG,IAAI,EACV,EAAE,wBAAwB,qBAuC1B"}
@@ -19,7 +19,15 @@ export function PrintDocumentButton({ model, id, documentKey, mode, filename, on
19
19
  return;
20
20
  setBusy(true);
21
21
  try {
22
- await printDocument({ model, id, key: documentKey, mode, filename });
22
+ // A caller that handles errors itself keeps ownership of the error toast.
23
+ await printDocument({
24
+ model,
25
+ id,
26
+ key: documentKey,
27
+ mode,
28
+ filename,
29
+ feedback: onError ? { progress: true, success: true, error: false } : true,
30
+ });
23
31
  }
24
32
  catch (err) {
25
33
  onError?.(err);
@@ -0,0 +1,14 @@
1
+ export type RecordMutationKind = 'create' | 'update' | 'delete';
2
+ export declare const RECORD_MUTATION_EVENT = "metacore:record-mutated";
3
+ export interface RecordMutationDetail {
4
+ /** Model key as the emitter knows it (matched case-insensitively). */
5
+ model: string;
6
+ kind: RecordMutationKind;
7
+ }
8
+ /** Announce a successful mutation. Safe to call outside a browser (no-op). */
9
+ export declare function emitRecordMutation(model: string | undefined | null, kind: RecordMutationKind): void;
10
+ /**
11
+ * Listen for mutations of `model` (case-insensitive). Returns an unsubscribe.
12
+ */
13
+ export declare function subscribeRecordMutations(model: string | undefined | null, cb: (detail: RecordMutationDetail) => void): () => void;
14
+ //# sourceMappingURL=record-mutation-events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record-mutation-events.d.ts","sourceRoot":"","sources":["../src/record-mutation-events.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAE/D,eAAO,MAAM,qBAAqB,4BAA4B,CAAA;AAE9D,MAAM,WAAW,oBAAoB;IACjC,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,kBAAkB,CAAA;CAC3B;AAID,8EAA8E;AAC9E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EAAE,IAAI,EAAE,kBAAkB,GAAG,IAAI,CASnG;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CACpC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EAChC,EAAE,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,IAAI,GAC3C,MAAM,IAAI,CAUZ"}
@@ -0,0 +1,37 @@
1
+ // record-mutation-events — a tiny window-level bus that tells every mounted
2
+ // list (DynamicTable, DynamicKanban) "a record of model X just changed".
3
+ //
4
+ // Why: the lists only reloaded when the *caller* bumped `refreshTrigger`. A
5
+ // create/edit/delete done from a dialog the shell opens (the dynamic record
6
+ // dialog, a federated action modal, a wizard, a confirmation) never reached
7
+ // the table, so the row stayed stale until a manual refresh (#1020). Emitting
8
+ // here decouples the mutation site from the list that displays the model.
9
+ export const RECORD_MUTATION_EVENT = 'metacore:record-mutated';
10
+ const norm = (m) => (m ?? '').trim().toLowerCase();
11
+ /** Announce a successful mutation. Safe to call outside a browser (no-op). */
12
+ export function emitRecordMutation(model, kind) {
13
+ if (!model || typeof window === 'undefined' || typeof CustomEvent === 'undefined')
14
+ return;
15
+ try {
16
+ window.dispatchEvent(new CustomEvent(RECORD_MUTATION_EVENT, { detail: { model, kind } }));
17
+ }
18
+ catch {
19
+ /* a listener must never break the save flow */
20
+ }
21
+ }
22
+ /**
23
+ * Listen for mutations of `model` (case-insensitive). Returns an unsubscribe.
24
+ */
25
+ export function subscribeRecordMutations(model, cb) {
26
+ if (typeof window === 'undefined')
27
+ return () => { };
28
+ const want = norm(model);
29
+ const handler = (e) => {
30
+ const d = e.detail;
31
+ if (!d || !want || norm(d.model) !== want)
32
+ return;
33
+ cb(d);
34
+ };
35
+ window.addEventListener(RECORD_MUTATION_EVENT, handler);
36
+ return () => window.removeEventListener(RECORD_MUTATION_EVENT, handler);
37
+ }
package/dist/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { OptionFilter } from './option-filter';
1
2
  /** One spreadsheet column of a model's import template. Mirrors the kernel's
2
3
  * `modelbase.ImportColumn`. */
3
4
  export interface ImportColumnMeta {
@@ -17,6 +18,59 @@ export interface ImportSpecMeta {
17
18
  sheetName?: string;
18
19
  instructions?: string[];
19
20
  }
21
+ /** Paso de renglones de un tipo de documento (usa el editor de renglones del SDK). */
22
+ export interface DocumentFormLines {
23
+ /** Campo del payload que recibe los renglones. Default: `lines` (o `lines_field` del manifest). */
24
+ field?: string;
25
+ /** Columnas opcionales del editor (`discount`, `tax`, `unit`…). */
26
+ columns?: string[];
27
+ /** `sale` (precio de lista) o `cost` (compras). Default `sale`. */
28
+ price_source?: 'sale' | 'cost';
29
+ /** Al menos un renglón para guardar. Default true. */
30
+ required?: boolean;
31
+ /** Título del paso. Default «Renglones». */
32
+ title?: string;
33
+ }
34
+ /** Un tipo de documento (Factura, Nota de crédito, REP, Global, Traslado…). */
35
+ export interface DocumentFormType {
36
+ key: string;
37
+ label: string;
38
+ description?: string;
39
+ icon?: string;
40
+ /** Valor que se escribe en `type_field` (p. ej. `"E"`). Default: `key`. */
41
+ value?: string;
42
+ /** Campos del tipo, en el mismo vocabulario que los campos de una acción. */
43
+ fields: ActionFieldDef[];
44
+ /** Valores fijos que viajan en el alta de este tipo. */
45
+ defaults?: Record<string, unknown>;
46
+ /** Con esto el tipo tiene paso de renglones. */
47
+ lines?: DocumentFormLines | boolean;
48
+ /** Endpoint de alta propio de este tipo. Default: el del modelo. */
49
+ endpoint?: string;
50
+ /** Texto del botón final. Default «Crear». */
51
+ submit_label?: string;
52
+ }
53
+ export interface DocumentFormsManifest {
54
+ /** Columna discriminadora del modelo (p. ej. `"type"`). Sin ella no se escribe el tipo. */
55
+ type_field?: string;
56
+ /** Campo por defecto de los renglones. Default `lines`. */
57
+ lines_field?: string;
58
+ types: DocumentFormType[];
59
+ }
60
+ /**
61
+ * Audit columns of a model, served by the kernel (>= v0.188) on the table
62
+ * metadata. Each value is the COLUMN NAME (the key in the row), not a value;
63
+ * a key is omitted when the model lacks that column (append-only ledgers have
64
+ * only `created_*`). Purely additive — older kernels omit `audit` entirely.
65
+ */
66
+ export interface AuditMeta {
67
+ created_at?: string;
68
+ created_by?: string;
69
+ updated_at?: string;
70
+ updated_by?: string;
71
+ deleted_at?: string;
72
+ deleted_by?: string;
73
+ }
20
74
  export interface TableMetadata {
21
75
  title: string;
22
76
  endpoint: string;
@@ -31,6 +85,18 @@ export interface TableMetadata {
31
85
  canExport?: boolean;
32
86
  canImport?: boolean;
33
87
  canCreate?: boolean;
88
+ /**
89
+ * Audit columns (when/who) of the model. Feeds `<AuditInfo>`; absent on
90
+ * older kernels or models without audit columns.
91
+ */
92
+ audit?: AuditMeta;
93
+ /**
94
+ * Formularios de alta guiados por tipo de documento (FAC-12). Cuando trae
95
+ * al menos un tipo, `DynamicCRUDPage` abre `DocumentFormDialog` en lugar del
96
+ * formulario genérico del modelo. Lo sirve el kernel o el addon; el SDK no
97
+ * conoce campos fiscales.
98
+ */
99
+ document_forms?: DocumentFormsManifest;
34
100
  /**
35
101
  * Cómo se pinta el alta genérica. `generic` (o ausente) es el botón Crear.
36
102
  * `hidden` lo quita. `action` lo quita porque el módulo trae la suya
@@ -303,6 +369,16 @@ export interface ColumnDefinition {
303
369
  * widgets — `searchEndpoint` stays as the legacy escape hatch.
304
370
  */
305
371
  ref?: string;
372
+ /**
373
+ * Hides options of this relation/dynamic picker (e.g. cancelled invoices in
374
+ * the payments selector). One rule or a list (AND), tested client-side
375
+ * against the option's extra columns (`meta`): `{ field: 'status', not_in:
376
+ * ['cancelada'] }`. The options endpoint must return that column. Absent →
377
+ * the picker lists everything (retrocompat). See `option-filter.ts`.
378
+ */
379
+ optionFilter?: OptionFilter;
380
+ /** snake_case alias served by the kernel manifest for `optionFilter`. */
381
+ option_filter?: OptionFilter;
306
382
  /**
307
383
  * Write-time rules the SDK also pre-flights. Object form `{regex,min,max,custom}`
308
384
  * or a Laravel / go-playground string (`required|min:2|email`).
@@ -443,6 +519,16 @@ export interface ActionFieldDef {
443
519
  * `useOptionsResolver` against `/api/options/<ref>?field=id`.
444
520
  */
445
521
  ref?: string;
522
+ /**
523
+ * Hides options of this relation/dynamic picker (e.g. cancelled invoices in
524
+ * the payments selector). One rule or a list (AND), tested client-side
525
+ * against the option's extra columns (`meta`): `{ field: 'status', not_in:
526
+ * ['cancelada'] }`. The options endpoint must return that column. Absent →
527
+ * the picker lists everything (retrocompat). See `option-filter.ts`.
528
+ */
529
+ optionFilter?: OptionFilter;
530
+ /** snake_case alias served by the kernel manifest for `optionFilter`. */
531
+ option_filter?: OptionFilter;
446
532
  /**
447
533
  * snake_case aliases the kernel manifest may serve for a belongs_to FK
448
534
  * target instead of `ref`. Treated as equivalent to `ref` by the SDK so a