@hortiview/shared-components 2.11.1 → 2.12.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 (41) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +135 -3
  3. package/dist/components/Allowed/Allowed.d.ts +21 -0
  4. package/dist/components/Allowed/Allowed.js +13 -0
  5. package/dist/components/Allowed/Allowed.test.d.ts +1 -0
  6. package/dist/components/Allowed/Allowed.test.js +49 -0
  7. package/dist/components/AllowedButton/AllowedButton.d.ts +51 -0
  8. package/dist/components/AllowedButton/AllowedButton.js +28 -0
  9. package/dist/components/AllowedButton/AllowedButton.test.d.ts +1 -0
  10. package/dist/components/AllowedButton/AllowedButton.test.js +126 -0
  11. package/dist/components/AllowedIconButton/AllowedIconButton.d.ts +43 -0
  12. package/dist/components/AllowedIconButton/AllowedIconButton.js +23 -0
  13. package/dist/components/AllowedIconButton/AllowedIconButton.test.d.ts +1 -0
  14. package/dist/components/AllowedIconButton/AllowedIconButton.test.js +121 -0
  15. package/dist/components/Filter/Filter.js +7 -4
  16. package/dist/components/FormComponents/FormDatePicker/FormDatePicker.js +2 -10
  17. package/dist/components/FormComponents/FormNumber/CustomTextField.js +1 -1
  18. package/dist/components/GenericTable/GenericTable.js +7 -4
  19. package/dist/components/ModulePadding/ModulePadding.js +7 -4
  20. package/dist/components/PermissionChecks/PermissionService.d.ts +35 -0
  21. package/dist/components/PermissionChecks/PermissionService.js +28 -0
  22. package/dist/components/PermissionChecks/PermissionService.test.d.ts +1 -0
  23. package/dist/components/PermissionChecks/PermissionService.test.js +83 -0
  24. package/dist/components/PermissionChecks/mockedPermissions.d.ts +24 -0
  25. package/dist/components/PermissionChecks/mockedPermissions.js +299 -0
  26. package/dist/components/SharedComponentsPermissionProvider/PermissionContext.d.ts +17 -0
  27. package/dist/components/SharedComponentsPermissionProvider/PermissionContext.js +11 -0
  28. package/dist/components/SharedComponentsPermissionProvider/SharedComponentsPermissionProvider.d.ts +32 -0
  29. package/dist/components/SharedComponentsPermissionProvider/SharedComponentsPermissionProvider.js +17 -0
  30. package/dist/components/Stepper/components/StepperHeader.js +4 -12
  31. package/dist/index-CuHybtft.js +51 -0
  32. package/dist/{isString-BNaBRq3S.js → isString-BifemsUQ.js} +1 -1
  33. package/dist/main.d.ts +8 -0
  34. package/dist/main.js +74 -58
  35. package/dist/{omit-uTAyrUTm.js → omit-Tf2F0V8l.js} +183 -183
  36. package/dist/services/UtilService.js +16 -13
  37. package/dist/test-utils.d.ts +82 -0
  38. package/dist/test-utils.js +32 -0
  39. package/dist/types/Permission.d.ts +153 -0
  40. package/dist/types/Permission.js +4 -0
  41. package/package.json +2 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ ## [2.12.0](https://dev.azure.com/sdundc/HV%20Platform/_git/HortiView-Frontend-Shared/compare/v2.11.2...v2.12.0) (2026-02-02)
2
+
3
+ ### Features
4
+
5
+ * add a permission context and provider to be able to access permissions inside of shared components ([7462a97](https://dev.azure.com/sdundc/HV%20Platform/_git/HortiView-Frontend-Shared/commit/7462a970c02a715eb8edb526d5afc4c50f829de3))
6
+ * add loadingspinner to AllowedButton ([a999ba5](https://dev.azure.com/sdundc/HV%20Platform/_git/HortiView-Frontend-Shared/commit/a999ba53f1ae6806ec6a39b1443dc419fc0c2587))
7
+ * add permission types and enum from main repo ([f3da7fb](https://dev.azure.com/sdundc/HV%20Platform/_git/HortiView-Frontend-Shared/commit/f3da7fb003158daf2e0901fe37cb90af5982af1d))
8
+ * add permissionService ([fabef22](https://dev.azure.com/sdundc/HV%20Platform/_git/HortiView-Frontend-Shared/commit/fabef220f5f85773bc4044b069c8241b42987fad))
9
+ * add storybooks for newly introduced components ([7d79ea3](https://dev.azure.com/sdundc/HV%20Platform/_git/HortiView-Frontend-Shared/commit/7d79ea3286f3775fe5ee30a432de7abf777c8995))
10
+ * addnew permission components to readMe + export new components from main ([1fbe431](https://dev.azure.com/sdundc/HV%20Platform/_git/HortiView-Frontend-Shared/commit/1fbe4316e5e424b9c2bbcfc27e3ae03e427b1e31))
11
+ * introduce button components that are permission-gated ([c811d31](https://dev.azure.com/sdundc/HV%20Platform/_git/HortiView-Frontend-Shared/commit/c811d3198bb72f32007db3a37c8eb822b341f1f9))
12
+ * introduce get permissionFromModulePermissionToken ([523ff1b](https://dev.azure.com/sdundc/HV%20Platform/_git/HortiView-Frontend-Shared/commit/523ff1b8d9c08af701a0318efb0e260cbf436431))
13
+
14
+ ## [2.11.2](https://dev.azure.com/sdundc/HV%20Platform/_git/HortiView-Frontend-Shared/compare/v2.11.1...v2.11.2) (2026-01-27)
15
+
16
+ ### Bug Fixes
17
+
18
+ * adjusted stepperHeader invalid prop values ([7542b35](https://dev.azure.com/sdundc/HV%20Platform/_git/HortiView-Frontend-Shared/commit/7542b358125e8b4460ededdc4c2e08d00499c189)), closes [#13800](https://dev.azure.com/sdundc/HV%20Platform/_git/HortiView-Frontend-Shared/issues/13800)
19
+
1
20
  ## [2.11.1](https://dev.azure.com/sdundc/HV%20Platform/_git/HortiView-Frontend-Shared/compare/v2.11.0...v2.11.1) (2026-01-22)
2
21
 
3
22
  ### Styles
package/README.md CHANGED
@@ -54,6 +54,7 @@ Additionally the library provides form components using [react-hook-form](https:
54
54
  1. [OfflineView](#offlineview)
55
55
  1. [OnboardingBanner](#onboardingbanner)
56
56
  1. [OverflowTooltip](#overflowtooltip)
57
+ 1. [Permissions](#permissions)
57
58
  1. [SafeForm](#safeform)
58
59
  1. [ScrollBar](#scrollbar)
59
60
  1. [SearchBar](#searchbar)
@@ -932,6 +933,137 @@ const longText =
932
933
  </OverflowTooltip>;
933
934
  ```
934
935
 
936
+ ### Permissions
937
+
938
+ This shared library provides different hooks and component that can be used to check wether a user is allowed to proceed with an action, view a component or not.
939
+
940
+ In order for this component to correctly determine whether a user has the appropriate permissions, a basic setup must be implemented. The following section explains how:
941
+
942
+ #### Setup
943
+
944
+ #### 1. Wrap your app with the SharedComponentsPermissionProvider
945
+
946
+ In your main application file (e.g., `App.tsx` or `main.tsx`), wrap your app with the `PermissionProvider`:
947
+
948
+ ```tsx
949
+ import { SharedComponentsPermissionProvider } from '@hortiview/shared-components';
950
+
951
+ function App() {
952
+ // Get these values from your store or state management
953
+ // In this example `Zustand` stores are used
954
+ const userPermissions = useClaimsIdentityStore(state => state.userPermissions);
955
+ const activeOrganizationId = useActiveOrganizationStore(state => state.activeOrganization?.id);
956
+
957
+ return (
958
+ <SharedComponentsPermissionProvider
959
+ userPermissions={userPermissions}
960
+ activeOrganizationId={activeOrganizationId}
961
+ >
962
+ <YourAppComponents />
963
+ </SharedComponentsPermissionProvider>
964
+ );
965
+ }
966
+ ```
967
+
968
+ #### 2.1 Use the Allowed component anywhere in your app
969
+
970
+ Once the provider is set up, you can use `<Allowed />` anywhere in your component tree without passing props. The wrapped component is conditionally rendered depending on the user permissions:
971
+
972
+ ```tsx
973
+ import { Allowed } from '@hortiview/shared-components';
974
+
975
+ function MyComponent() {
976
+ return (
977
+ <Allowed neededPermissions={["organization-read", "organization-edit"]} entityId={organizationId}>
978
+ <OrganizationSettings />
979
+ <Allowed/>
980
+ );
981
+ }
982
+ ```
983
+
984
+ #### 2.2 Use the useIsAllowed-hook anywhere in your app
985
+
986
+ The `useIsAllowed`-hook works similar to the `<Allowed />` component. Instead of being used as a wrapper it returns a boolean. That value can be used to conditionally render components or control application logic based on user permissions.
987
+
988
+ ```tsx
989
+ import { useIsAllowed } from '@hortiview/shared-components';
990
+
991
+ function MyComponent() {
992
+ const isAllowed = useIsAllowed();
993
+
994
+ // Check if user has permission to edit members for the current organization
995
+ const canEditMembers = isAllowed(['member-edit', 'member-create'], orgId);
996
+
997
+ return <MembersList isDisabled={!canEditMembers} />;
998
+ }
999
+ ```
1000
+
1001
+ #### 2.3 Use the AllowedButton or AllowedIconButton anywhere in your app
1002
+
1003
+ Once the provider is set up, you can use `AllowedButton` and `AllowedIconButton` anywhere in your component tree without passing props. Both buttons expect a permissions array and call the `useIsAllowed` under the hood.
1004
+
1005
+ > **Note:** Both components will automatically hide (return `null`) if the user doesn't have the required permissions. The button will only render when the user is authorized.
1006
+
1007
+ **AllowedButton Example:**
1008
+
1009
+ ```jsx
1010
+ import { AllowedButton, Permission } from '@hortiview/shared-components';
1011
+
1012
+ function FarmActions({ farmId }) {
1013
+ return (
1014
+ <div>
1015
+ <AllowedButton
1016
+ data-testid='edit-season-button'
1017
+ label='Edit Season'
1018
+ permissions={['season-edit']}
1019
+ entityId={organizationId}
1020
+ buttonVariant='filled'
1021
+ onClick={() => handleEditSeason(seasonId)}
1022
+ />
1023
+
1024
+ <AllowedButton
1025
+ data-testid='delete-season-button'
1026
+ label='Delete Season'
1027
+ permissions={['season-delete']}
1028
+ entityId={organizationId}
1029
+ buttonVariant='danger'
1030
+ onClick={() => handleDeleteSeason(seasonId)}
1031
+ />
1032
+ </div>
1033
+ );
1034
+ }
1035
+ ```
1036
+
1037
+ **AllowedIconButton Example:**
1038
+
1039
+ ```jsx
1040
+ import { AllowedIconButton, Permission } from '@hortiview/shared-components';
1041
+
1042
+ function BlockActions({ blockId, farmId }) {
1043
+ return (
1044
+ <div>
1045
+ <AllowedIconButton
1046
+ data-testid='edit-block-icon'
1047
+ icon='edit'
1048
+ permissions={['block-edit', 'block-create']}
1049
+ entityId={farmId}
1050
+ iconButtonVariant='filled-primary'
1051
+ onClick={() => handleEditBlock(blockId)}
1052
+ />
1053
+
1054
+ <AllowedIconButton
1055
+ data-testid='delete-block-icon'
1056
+ icon='delete'
1057
+ permissions={['block-delete']}
1058
+ entityId={farmId}
1059
+ iconButtonVariant='filled-danger'
1060
+ onClick={() => handleDeleteBlock(blockId)}
1061
+ />
1062
+ </div>
1063
+ );
1064
+ }
1065
+ ```
1066
+
935
1067
  ### SafeForm
936
1068
 
937
1069
  A wrapper around a form that prevents accidental submission when pressing Enter in input fields.
@@ -1018,10 +1150,10 @@ import { Select } from '@hortiview/shared-components';
1018
1150
  ### Stepper
1019
1151
 
1020
1152
  Provides a simple stepper component, which contains the step indicators, a title text and buttons to navigate forwards and backwards.
1021
- The component is available in two types:
1153
+ The component is available in two types:
1022
1154
 
1023
- * 'default' (steps as dots and a centered step title below them)
1024
- * 'description' (n/n numerical progress, description/title right of it)
1155
+ - 'default' (steps as dots and a centered step title below them)
1156
+ - 'description' (n/n numerical progress, description/title right of it)
1025
1157
 
1026
1158
  **Be aware that you need to control the Stepper flow by your own, like:**
1027
1159
 
@@ -0,0 +1,21 @@
1
+ import { PropsWithChildren } from 'react';
2
+ import { HVPermissions } from '../../types/Permission';
3
+
4
+ type AllowedProps = {
5
+ neededPermissions: HVPermissions[];
6
+ entityId: string | undefined;
7
+ disclaimer?: boolean;
8
+ disclaimerText?: string;
9
+ };
10
+ /**
11
+ * A component wrapper which takes permissions and an entity id (as a fallback the activeOrganization id is used) the permission is for.
12
+ * Returns the children if the user has the permission for the entity, nothing otherwise (or an optional disclaimer)
13
+ * @param children - the children to return if allowed to see it
14
+ * @param entityId - the entity id, the permission is for (FarmOrganizationId, VendorOrganizationId or FarmId)
15
+ * @param neededPermissions - the permissions needed to see the children
16
+ * @param disclaimer - if true a disclaimer is shown, otherwise not. (default: false)
17
+ * @param disclaimerText - the text to show in the disclaimer (default: 'You are not allowed to see the content of this section.')
18
+ * @returns
19
+ */
20
+ export declare const Allowed: ({ children, neededPermissions, entityId, disclaimer, disclaimerText, }: PropsWithChildren<AllowedProps>) => string | number | boolean | import("react/jsx-runtime").JSX.Element | Iterable<import('react').ReactNode> | null | undefined;
21
+ export {};
@@ -0,0 +1,13 @@
1
+ import { jsx as s } from "react/jsx-runtime";
2
+ import { Disclaimer as i } from "../Disclaimer/Disclaimer.js";
3
+ import { useIsAllowed as n } from "../PermissionChecks/PermissionService.js";
4
+ const d = ({
5
+ children: o,
6
+ neededPermissions: e,
7
+ entityId: t,
8
+ disclaimer: r = !1,
9
+ disclaimerText: l = "You are not allowed to see the content of this section."
10
+ }) => n()(e, t) ? o : r ? /* @__PURE__ */ s(i, { text: l }) : null;
11
+ export {
12
+ d as Allowed
13
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,49 @@
1
+ import { jsx as e } from "react/jsx-runtime";
2
+ import { s as n } from "../../react.esm-BbMrgZCM.js";
3
+ import { renderWithPermissions as s } from "../../test-utils.js";
4
+ import { HVMainPermissions as o } from "../../types/Permission.js";
5
+ import { Allowed as i } from "./Allowed.js";
6
+ import { d as l, t as d, g as t } from "../../vi.CjhMlMwf-CKxPQtd6.js";
7
+ l("Test Allowed", () => {
8
+ d("should return the children if allowed to see them", () => {
9
+ const r = /* @__PURE__ */ e("div", { children: "Allowed to see children" });
10
+ s(
11
+ /* @__PURE__ */ e(i, { neededPermissions: [o.CurrentUserRead], entityId: "General", children: r }),
12
+ {
13
+ userPermissions: {
14
+ General: [o.CurrentUserRead]
15
+ }
16
+ }
17
+ ), t(n.getByText("Allowed to see children")).toBeInTheDocument();
18
+ }), d("should return empty wrapper when not allowed to see children", () => {
19
+ const r = /* @__PURE__ */ e("div", { children: "Allowed to see children" });
20
+ s(
21
+ /* @__PURE__ */ e(i, { neededPermissions: [o.CurrentUserRead], entityId: "General", children: r }),
22
+ {
23
+ userPermissions: {
24
+ General: []
25
+ }
26
+ }
27
+ ), t(n.queryByText("Allowed to see children")).not.toBeInTheDocument();
28
+ }), d("should return disclaimer when defined & not allowed to see children", () => {
29
+ const r = /* @__PURE__ */ e("div", { children: "Allowed to see children" });
30
+ s(
31
+ /* @__PURE__ */ e(
32
+ i,
33
+ {
34
+ disclaimer: !0,
35
+ neededPermissions: [o.CurrentUserRead],
36
+ entityId: "General",
37
+ children: r
38
+ }
39
+ ),
40
+ {
41
+ userPermissions: {
42
+ General: []
43
+ }
44
+ }
45
+ ), t(n.queryByText("Allowed to see children")).not.toBeInTheDocument(), t(
46
+ n.getByText("You are not allowed to see the content of this section.")
47
+ ).toBeInTheDocument();
48
+ });
49
+ });
@@ -0,0 +1,51 @@
1
+ import { ButtonProps } from '@element-public/react-button';
2
+ import { HVPermissions } from '../../types/Permission';
3
+
4
+ export type AllowedButtonProps = Omit<ButtonProps, 'buttonSize' | 'variant' | 'type' | 'label'> & {
5
+ /**
6
+ * This applies the html button type (button, submit, reset) or the anchor media type when used with the href property.
7
+ * Default: 'button'
8
+ */
9
+ buttonType?: 'button' | 'submit' | 'reset' | string;
10
+ /**
11
+ * Applies the selected style to the button.
12
+ * Default: 'filled'
13
+ */
14
+ variant?: 'filled' | 'outlined' | 'danger' | 'text' | 'success';
15
+ /**
16
+ * data-testid used for testing.
17
+ */
18
+ 'data-testid': string;
19
+ /**
20
+ * The entity id, the permission is for (FarmOrganizationId, VendorOrganizationId or FarmLocationId).
21
+ */
22
+ entityId?: string;
23
+ /**
24
+ * Id used for linking the button to a form.
25
+ */
26
+ formId?: string;
27
+ /**
28
+ * If true, the button shows a loading spinner instead of the label.
29
+ * While true, the button is disabled.
30
+ */
31
+ isLoading?: boolean;
32
+ /**
33
+ * Can be a string or JSX.Element for setting a loading spinner or custom component.
34
+ */
35
+ label: string | JSX.Element;
36
+ /**
37
+ * The permissions needed to enable the button. If the array is empty, the button is always enabled.
38
+ */
39
+ permissions: HVPermissions[];
40
+ /**
41
+ * Changes the the size of the button.
42
+ * Default: 'medium'
43
+ */
44
+ size?: 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge';
45
+ };
46
+ /**
47
+ * A permission-gated button component that only renders when the user has the required permissions.
48
+ * @param {AllowedButtonProps} props - Component properties
49
+ * @returns {JSX.Element | null} The button component or null if permissions are not met
50
+ */
51
+ export declare const AllowedButton: ({ buttonType, variant, "data-testid": testId, label, size, isLoading, ...props }: AllowedButtonProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,28 @@
1
+ import { jsx as e } from "react/jsx-runtime";
2
+ import { B as r } from "../../index.es-B0QNuIUR.js";
3
+ import { Allowed as s } from "../Allowed/Allowed.js";
4
+ import { LoadingSpinner as a } from "../LoadingSpinner/Default/LoadingSpinner.js";
5
+ const c = ({
6
+ buttonType: d = "button",
7
+ variant: n = "filled",
8
+ "data-testid": o,
9
+ label: m,
10
+ size: l = "medium",
11
+ isLoading: i = !1,
12
+ ...t
13
+ }) => /* @__PURE__ */ e(s, { neededPermissions: t.permissions, entityId: t.entityId, children: /* @__PURE__ */ e(
14
+ r,
15
+ {
16
+ buttonSize: l,
17
+ "data-testid": `allowed-button-${o}`,
18
+ disabled: t.disabled || i,
19
+ form: t.formId,
20
+ type: d,
21
+ variant: n,
22
+ ...t,
23
+ children: i ? /* @__PURE__ */ e(a, { size: "sm", spinnerOnly: !0 }) : m
24
+ }
25
+ ) });
26
+ export {
27
+ c as AllowedButton
28
+ };
@@ -0,0 +1,126 @@
1
+ import { jsx as s } from "react/jsx-runtime";
2
+ import { s as t, f as u } from "../../react.esm-BbMrgZCM.js";
3
+ import { renderWithPermissions as n } from "../../test-utils.js";
4
+ import { HVMainPermissions as i } from "../../types/Permission.js";
5
+ import { AllowedButton as d } from "./AllowedButton.js";
6
+ import { u as b } from "../../useBreakpoints-MzTZ0tCT.js";
7
+ import { v as l, d as m, b as c, t as r, g as e } from "../../vi.CjhMlMwf-CKxPQtd6.js";
8
+ l.mock("../../hooks/useBreakpoints", () => ({
9
+ useBreakpoints: l.fn()
10
+ }));
11
+ m("AllowedButton Test", () => {
12
+ c(() => {
13
+ l.mocked(b).mockReturnValue({
14
+ isDesktop: !0,
15
+ isMobile: !1,
16
+ isTablet: !1,
17
+ isDesktopNavbar: !0
18
+ });
19
+ }), r("renders button with no permissions required", () => {
20
+ n(
21
+ /* @__PURE__ */ s(d, { "data-testid": "test", label: "Add", permissions: [], entityId: void 0 })
22
+ ), e(t.getByText("Add")).toBeInTheDocument();
23
+ }), r("renders outlined button variant", () => {
24
+ n(
25
+ /* @__PURE__ */ s(
26
+ d,
27
+ {
28
+ "data-testid": "regular-outlined-button",
29
+ label: "Add",
30
+ variant: "outlined",
31
+ permissions: [],
32
+ entityId: void 0
33
+ }
34
+ )
35
+ );
36
+ const o = t.getAllByRole("button")[0];
37
+ e(o).toHaveClass("mdc-button--outlined"), e(t.getByTestId("allowed-button-regular-outlined-button")).toBeInTheDocument();
38
+ }), r("renders full-width button", () => {
39
+ n(
40
+ /* @__PURE__ */ s(
41
+ d,
42
+ {
43
+ "data-testid": "regular-button",
44
+ label: "Add",
45
+ fullWidth: !0,
46
+ permissions: [],
47
+ entityId: void 0
48
+ }
49
+ )
50
+ );
51
+ const o = t.getAllByRole("button")[0];
52
+ e(o).toHaveClass("lmnt-button--full-width"), e(t.getByTestId("allowed-button-regular-button")).toBeInTheDocument();
53
+ }), r("calls onClick when clicked", () => {
54
+ const o = l.fn();
55
+ n(
56
+ /* @__PURE__ */ s(
57
+ d,
58
+ {
59
+ "data-testid": "test-onclick-button",
60
+ label: "Add",
61
+ onClick: o,
62
+ permissions: [],
63
+ entityId: void 0
64
+ }
65
+ )
66
+ );
67
+ const a = t.getAllByRole("button")[0];
68
+ u.click(a), e(o).toHaveBeenCalled(), e(t.getByTestId("allowed-button-test-onclick-button")).toBeInTheDocument();
69
+ }), r("renders disabled button when prop is used", () => {
70
+ n(
71
+ /* @__PURE__ */ s(
72
+ d,
73
+ {
74
+ "data-testid": "disabled-button",
75
+ label: "Add",
76
+ permissions: [i.SuperUser],
77
+ entityId: "farm-123",
78
+ disabled: !0
79
+ }
80
+ ),
81
+ {
82
+ userPermissions: {
83
+ "farm-123": [i.SuperUser]
84
+ }
85
+ }
86
+ ), e(t.getByText("Add")).toBeInTheDocument(), e(t.getByTestId("allowed-button-disabled-button")).toBeInTheDocument();
87
+ const o = t.getAllByRole("button")[0];
88
+ e(o).toHaveAttribute("disabled");
89
+ }), r("hides button when permission is missing", () => {
90
+ n(
91
+ /* @__PURE__ */ s(
92
+ d,
93
+ {
94
+ "data-testid": "missing",
95
+ label: "Add",
96
+ permissions: [i.SuperUser],
97
+ entityId: "farm-123"
98
+ }
99
+ ),
100
+ {
101
+ userPermissions: {
102
+ "farm-123": [i.FarmsUpdate, i.FarmsRead]
103
+ }
104
+ }
105
+ ), e(t.queryByText("Add")).not.toBeInTheDocument(), e(t.queryByTestId("allowed-button-missing")).not.toBeInTheDocument();
106
+ }), r("renders button when permission exists", () => {
107
+ n(
108
+ /* @__PURE__ */ s(
109
+ d,
110
+ {
111
+ "data-testid": "enabled",
112
+ label: "Add",
113
+ permissions: [i.SuperUser],
114
+ entityId: "farm-123"
115
+ }
116
+ ),
117
+ {
118
+ userPermissions: {
119
+ "farm-123": [i.SuperUser]
120
+ }
121
+ }
122
+ ), e(t.getByText("Add")).toBeInTheDocument(), e(t.getByTestId("allowed-button-enabled")).toBeInTheDocument();
123
+ const o = t.getAllByRole("button")[0];
124
+ e(o).not.toHaveAttribute("disabled");
125
+ });
126
+ });
@@ -0,0 +1,43 @@
1
+ import { IconButtonProps } from '@element-public/react-icon-button';
2
+ import { HVPermissions } from '../../types/Permission';
3
+
4
+ export type AllowedIconButtonProps = Omit<IconButtonProps, 'variant' | 'iconType' | 'iconSize'> & {
5
+ /**
6
+ * data-testid used for testing.
7
+ */
8
+ 'data-testid': string;
9
+ /**
10
+ * The entity id, the permission is for (FarmOrganizationId, VendorOrganizationId or FarmLocationId).
11
+ */
12
+ entityId?: string;
13
+ /**
14
+ * Id used for linking the button to a form.
15
+ */
16
+ formId?: string;
17
+ /**
18
+ * Variants prefixed with filled- will add a background color to the icon according to the theme.
19
+ * Variants prefixed with color- will change the color of the icon according to the theme.
20
+ * Default: 'filled-primary'
21
+ */
22
+ variant?: 'filled-danger-alt' | 'filled-danger' | 'filled-primary' | 'filled-secondary' | 'color-primary' | 'color-secondary' | 'color-on-dark' | 'color-on-unknown-black' | 'secondary-on-surface';
23
+ /**
24
+ * The icon style to be used (filled, outlined, two-tone, rounded, sharp).
25
+ * Default: 'filled'
26
+ */
27
+ iconType?: 'filled' | 'outlined' | 'two-tone' | 'rounded' | 'sharp';
28
+ /**
29
+ * The permissions needed to enable the button. If the array is empty, the button is always enabled.
30
+ */
31
+ permissions: HVPermissions[];
32
+ /**
33
+ * Changes the the size of the icon button.
34
+ * Default: 'medium'
35
+ */
36
+ size?: 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge';
37
+ };
38
+ /**
39
+ * A permission-gated icon button component that only renders when the user has the required permissions.
40
+ * @param {AllowedIconButtonProps} props - Component properties
41
+ * @returns {JSX.Element | null} The icon button component or null if permissions are not met
42
+ */
43
+ export declare const AllowedIconButton: ({ "data-testid": testId, variant, iconType, size, ...props }: AllowedIconButtonProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,23 @@
1
+ import { jsx as i } from "react/jsx-runtime";
2
+ import { I as m } from "../../index.es-C1u7zUBz.js";
3
+ import { Allowed as l } from "../Allowed/Allowed.js";
4
+ const f = ({
5
+ "data-testid": o,
6
+ variant: e = "filled-primary",
7
+ iconType: d = "filled",
8
+ size: n = "medium",
9
+ ...t
10
+ }) => /* @__PURE__ */ i(l, { neededPermissions: t.permissions, entityId: t.entityId, children: /* @__PURE__ */ i(
11
+ m,
12
+ {
13
+ "data-testid": `allowed-icon-button-${o}`,
14
+ form: t.formId,
15
+ iconSize: n,
16
+ iconType: d,
17
+ variant: e,
18
+ ...t
19
+ }
20
+ ) });
21
+ export {
22
+ f as AllowedIconButton
23
+ };
@@ -0,0 +1,121 @@
1
+ import { jsx as s } from "react/jsx-runtime";
2
+ import { s as t, f as u } from "../../react.esm-BbMrgZCM.js";
3
+ import { renderWithPermissions as i } from "../../test-utils.js";
4
+ import { HVMainPermissions as n } from "../../types/Permission.js";
5
+ import { AllowedIconButton as r } from "./AllowedIconButton.js";
6
+ import { u as c } from "../../useBreakpoints-MzTZ0tCT.js";
7
+ import { v as a, d as m, b, t as d, g as e } from "../../vi.CjhMlMwf-CKxPQtd6.js";
8
+ a.mock("../../hooks/useBreakpoints", () => ({
9
+ useBreakpoints: a.fn()
10
+ }));
11
+ m("AllowedIconButton Test", () => {
12
+ b(() => {
13
+ a.mocked(c).mockReturnValue({
14
+ isDesktop: !0,
15
+ isMobile: !1,
16
+ isTablet: !1,
17
+ isDesktopNavbar: !0
18
+ });
19
+ }), d("renders icon button with no permissions required", () => {
20
+ i(
21
+ /* @__PURE__ */ s(
22
+ r,
23
+ {
24
+ "data-testid": "icon-button",
25
+ icon: "add",
26
+ permissions: [],
27
+ entityId: void 0
28
+ }
29
+ )
30
+ );
31
+ const o = t.getAllByRole("button")[0];
32
+ e(o).toHaveClass("lmnt-icon-button"), e(t.getByTestId("allowed-icon-button-icon-button")).toBeInTheDocument();
33
+ }), d("renders secondary variant icon button", () => {
34
+ i(
35
+ /* @__PURE__ */ s(
36
+ r,
37
+ {
38
+ "data-testid": "secondary-button",
39
+ icon: "add",
40
+ variant: "filled-secondary",
41
+ permissions: [],
42
+ entityId: void 0
43
+ }
44
+ )
45
+ );
46
+ const o = t.getAllByRole("button")[0];
47
+ e(o).toHaveClass("lmnt-icon-button--fill-secondary"), e(t.getByTestId("allowed-icon-button-secondary-button")).toBeInTheDocument();
48
+ }), d("calls onClick when clicked", () => {
49
+ const o = a.fn();
50
+ i(
51
+ /* @__PURE__ */ s(
52
+ r,
53
+ {
54
+ "data-testid": "test-onclick-button",
55
+ icon: "add",
56
+ onClick: o,
57
+ permissions: [],
58
+ entityId: void 0
59
+ }
60
+ )
61
+ );
62
+ const l = t.getAllByRole("button")[0];
63
+ u.click(l), e(o).toHaveBeenCalled(), e(t.getByTestId("allowed-icon-button-test-onclick-button")).toBeInTheDocument();
64
+ }), d("renders disabled icon button when prop is used", () => {
65
+ i(
66
+ /* @__PURE__ */ s(
67
+ r,
68
+ {
69
+ "data-testid": "disabled-button",
70
+ icon: "add",
71
+ permissions: [n.SuperUser],
72
+ entityId: "farm-123",
73
+ disabled: !0
74
+ }
75
+ ),
76
+ {
77
+ userPermissions: {
78
+ "farm-123": [n.SuperUser]
79
+ }
80
+ }
81
+ ), e(t.getByTestId("allowed-icon-button-disabled-button")).toBeInTheDocument();
82
+ const o = t.getAllByRole("button")[0];
83
+ e(o).toHaveAttribute("disabled");
84
+ }), d("hides icon button when permission is missing", () => {
85
+ i(
86
+ /* @__PURE__ */ s(
87
+ r,
88
+ {
89
+ "data-testid": "missing",
90
+ icon: "add",
91
+ permissions: [n.SuperUser],
92
+ entityId: "farm-123"
93
+ }
94
+ ),
95
+ {
96
+ userPermissions: {
97
+ "farm-123": [n.FarmsUpdate, n.FarmsRead]
98
+ }
99
+ }
100
+ ), e(t.queryByTestId("allowed-icon-button-missing")).not.toBeInTheDocument();
101
+ }), d("renders icon button when permission exists", () => {
102
+ i(
103
+ /* @__PURE__ */ s(
104
+ r,
105
+ {
106
+ "data-testid": "enabled",
107
+ icon: "add",
108
+ permissions: [n.SuperUser],
109
+ entityId: "farm-123"
110
+ }
111
+ ),
112
+ {
113
+ userPermissions: {
114
+ "farm-123": [n.SuperUser]
115
+ }
116
+ }
117
+ ), e(t.getByTestId("allowed-icon-button-enabled")).toBeInTheDocument();
118
+ const o = t.getAllByRole("button")[0];
119
+ e(o).not.toHaveAttribute("disabled");
120
+ });
121
+ });