@palladium-ethiopia/esm-admin-app 5.4.2-pre.100

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 (149) hide show
  1. package/.turbo/turbo-build.log +15 -0
  2. package/README.md +12 -0
  3. package/dist/117.js +1 -0
  4. package/dist/152.js +1 -0
  5. package/dist/152.js.map +1 -0
  6. package/dist/209.js +1 -0
  7. package/dist/209.js.map +1 -0
  8. package/dist/41.js +1 -0
  9. package/dist/41.js.map +1 -0
  10. package/dist/442.js +1 -0
  11. package/dist/442.js.map +1 -0
  12. package/dist/466.js +1 -0
  13. package/dist/466.js.map +1 -0
  14. package/dist/555.js +1 -0
  15. package/dist/555.js.map +1 -0
  16. package/dist/61.js +1 -0
  17. package/dist/61.js.map +1 -0
  18. package/dist/672.js +15 -0
  19. package/dist/672.js.map +1 -0
  20. package/dist/689.js +1 -0
  21. package/dist/689.js.map +1 -0
  22. package/dist/710.js +1 -0
  23. package/dist/710.js.map +1 -0
  24. package/dist/712.js +1 -0
  25. package/dist/712.js.map +1 -0
  26. package/dist/771.js +1 -0
  27. package/dist/771.js.map +1 -0
  28. package/dist/789.js +1 -0
  29. package/dist/789.js.map +1 -0
  30. package/dist/806.js +1 -0
  31. package/dist/826.js +1 -0
  32. package/dist/826.js.map +1 -0
  33. package/dist/914.js +27 -0
  34. package/dist/914.js.map +1 -0
  35. package/dist/926.js +17 -0
  36. package/dist/926.js.map +1 -0
  37. package/dist/ethiopia-esm-admin-app.js +6 -0
  38. package/dist/ethiopia-esm-admin-app.js.buildmanifest.json +556 -0
  39. package/dist/ethiopia-esm-admin-app.js.map +1 -0
  40. package/dist/main.js +32 -0
  41. package/dist/main.js.map +1 -0
  42. package/dist/routes.json +1 -0
  43. package/jest.config.js +8 -0
  44. package/package.json +52 -0
  45. package/rspack.config.js +1 -0
  46. package/src/components/confirm-modal/confirmation-operation-modal.component.tsx +43 -0
  47. package/src/components/confirm-modal/confirmation-operation.test.tsx +69 -0
  48. package/src/components/dashboard/dashboard.component.tsx +131 -0
  49. package/src/components/dashboard/dashboard.scss +38 -0
  50. package/src/components/dashboard/etl-dashboard.component.tsx +11 -0
  51. package/src/components/empty-state/empty-state-log.components.tsx +20 -0
  52. package/src/components/empty-state/empty-state-log.scss +28 -0
  53. package/src/components/empty-state/empty-state-log.test.tsx +24 -0
  54. package/src/components/facility-setup/card.component.tsx +16 -0
  55. package/src/components/facility-setup/facility-info.component.tsx +142 -0
  56. package/src/components/facility-setup/facility-info.scss +87 -0
  57. package/src/components/facility-setup/facility-setup.component.tsx +21 -0
  58. package/src/components/facility-setup/facility-setup.resource.tsx +7 -0
  59. package/src/components/facility-setup/facility-setup.scss +38 -0
  60. package/src/components/facility-setup/header/header.component.tsx +23 -0
  61. package/src/components/facility-setup/header/header.scss +19 -0
  62. package/src/components/header/header-illustration.component.tsx +13 -0
  63. package/src/components/header/header.component.tsx +28 -0
  64. package/src/components/header/header.scss +19 -0
  65. package/src/components/hook/healthWorkerAdapter.ts +213 -0
  66. package/src/components/hook/useFacilityInfo.tsx +37 -0
  67. package/src/components/hook/useSystemRoleSetting.tsx +33 -0
  68. package/src/components/locations/auto-suggest/autosuggest.component.tsx +149 -0
  69. package/src/components/locations/auto-suggest/autosuggest.scss +61 -0
  70. package/src/components/locations/auto-suggest/location-autosuggest.component.tsx +94 -0
  71. package/src/components/locations/auto-suggest/location-autosuggest.scss +48 -0
  72. package/src/components/locations/common/results-tile.component.tsx +45 -0
  73. package/src/components/locations/common/results-tile.scss +86 -0
  74. package/src/components/locations/forms/add-location/add-location.workspace.scss +34 -0
  75. package/src/components/locations/forms/add-location/add-location.workspace.tsx +200 -0
  76. package/src/components/locations/forms/search-location/search-location.workspace.scss +79 -0
  77. package/src/components/locations/forms/search-location/search-location.workspace.tsx +215 -0
  78. package/src/components/locations/header/header.component.tsx +48 -0
  79. package/src/components/locations/header/header.scss +58 -0
  80. package/src/components/locations/helpers/index.ts +16 -0
  81. package/src/components/locations/home/home-locations.component.tsx +18 -0
  82. package/src/components/locations/home/home-locations.scss +8 -0
  83. package/src/components/locations/hooks/UseFacilityLocations.ts +12 -0
  84. package/src/components/locations/hooks/useLocation.ts +18 -0
  85. package/src/components/locations/hooks/useLocationTags.ts +15 -0
  86. package/src/components/locations/tables/locations-table.component.tsx +243 -0
  87. package/src/components/locations/tables/locations-table.resource.ts +26 -0
  88. package/src/components/locations/tables/locations-table.scss +115 -0
  89. package/src/components/locations/types/index.ts +120 -0
  90. package/src/components/locations/utils/index.ts +5 -0
  91. package/src/components/logs-table/operation-log-resource.ts +41 -0
  92. package/src/components/logs-table/operation-log-table.component.tsx +120 -0
  93. package/src/components/logs-table/operation-log.scss +10 -0
  94. package/src/components/logs-table/operation-log.test.tsx +47 -0
  95. package/src/components/modal/hwr-confirmation.modal.scss +21 -0
  96. package/src/components/modal/hwr-confirmation.modal.tsx +170 -0
  97. package/src/components/modal/hwr-empty.modal.component.tsx +54 -0
  98. package/src/components/modal/hwr-sync.modal.scss +30 -0
  99. package/src/components/modal/hwr-sync.modal.tsx +209 -0
  100. package/src/components/modal/hwr-sync.resource.ts +23 -0
  101. package/src/components/provider-banner/provider-banner.component.tsx +106 -0
  102. package/src/components/provider-banner/provider-banner.module.scss +51 -0
  103. package/src/components/provider-banner/provider-banner.resource.ts +29 -0
  104. package/src/components/side-menu/left-panel.scss +42 -0
  105. package/src/components/side-menu/left-pannel.component.tsx +22 -0
  106. package/src/components/users/header/header.scss +90 -0
  107. package/src/components/users/header/user-management-header.component.tsx +42 -0
  108. package/src/components/users/manage-users/hooks/useProviderAttributeMapping.ts +110 -0
  109. package/src/components/users/manage-users/hooks/useUserFormSteps.ts +119 -0
  110. package/src/components/users/manage-users/hooks/useUserFormSubmission.ts +264 -0
  111. package/src/components/users/manage-users/hooks/useUserManagementForm.ts +122 -0
  112. package/src/components/users/manage-users/manage-user-role-scope/user-role-scope-list/user-role-scope-list.component.tsx +177 -0
  113. package/src/components/users/manage-users/manage-user-role-scope/user-role-scope-workspace/user-role-fields.scss +117 -0
  114. package/src/components/users/manage-users/manage-user-role-scope/user-role-scope-workspace/user-role-scope-fields.component.tsx +290 -0
  115. package/src/components/users/manage-users/manage-user-role-scope/user-role-scope-workspace/user-role-scope.workspace.tsx +316 -0
  116. package/src/components/users/manage-users/manage-user-role-scope/user-role-scope-workspace/userRoleScopeFormSchema.tsx +43 -0
  117. package/src/components/users/manage-users/manage-user.component.tsx +19 -0
  118. package/src/components/users/manage-users/manage-user.scss +31 -0
  119. package/src/components/users/manage-users/provider-autosuggest.component.tsx +117 -0
  120. package/src/components/users/manage-users/provider-search.resource.ts +34 -0
  121. package/src/components/users/manage-users/sections/demographic-section.component.tsx +156 -0
  122. package/src/components/users/manage-users/sections/login-section.component.tsx +88 -0
  123. package/src/components/users/manage-users/sections/provider-section.component.tsx +270 -0
  124. package/src/components/users/manage-users/sections/roles-section.component.tsx +88 -0
  125. package/src/components/users/manage-users/user-details/user-detail.scss +75 -0
  126. package/src/components/users/manage-users/user-details/user-details.component.tsx +182 -0
  127. package/src/components/users/manage-users/user-list/user-list.component.tsx +378 -0
  128. package/src/components/users/manage-users/user-list/user-list.resource.ts +30 -0
  129. package/src/components/users/manage-users/user-list/user-list.scss +37 -0
  130. package/src/components/users/manage-users/user-management.constants.ts +20 -0
  131. package/src/components/users/manage-users/user-management.utils.ts +100 -0
  132. package/src/components/users/manage-users/user-management.workspace.scss +172 -0
  133. package/src/components/users/manage-users/user-management.workspace.tsx +334 -0
  134. package/src/components/users/userManagementFormSchema.tsx +179 -0
  135. package/src/config-schema.ts +142 -0
  136. package/src/constants.ts +50 -0
  137. package/src/declarations.d.ts +2 -0
  138. package/src/index.ts +55 -0
  139. package/src/left-pannel-link.component.tsx +40 -0
  140. package/src/root.component.tsx +39 -0
  141. package/src/root.scss +12 -0
  142. package/src/routes.json +100 -0
  143. package/src/setup-tests.ts +1 -0
  144. package/src/types/index.ts +385 -0
  145. package/src/user-management.resources.ts +232 -0
  146. package/src/utils/utils.ts +20 -0
  147. package/translations/am.json +159 -0
  148. package/translations/en.json +159 -0
  149. package/tsconfig.json +5 -0
@@ -0,0 +1,61 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@carbon/colors';
4
+
5
+ .label01 {
6
+ @include type.type-style('label-01');
7
+ }
8
+
9
+ .suggestions {
10
+ position: relative;
11
+ border-top-width: 0;
12
+ list-style: none;
13
+ margin-top: 0;
14
+ max-height: 400px;
15
+ overflow-y: auto;
16
+ padding-left: 0;
17
+ width: 100%;
18
+ position: absolute;
19
+ left: 0;
20
+ background-color: colors.$white;
21
+ margin-bottom: 20px;
22
+ z-index: 99;
23
+ box-shadow: layout.$spacing-03 layout.$spacing-03 layout.$spacing-03 layout.$spacing-03 colors.$gray-30;
24
+ }
25
+
26
+ .suggestions li {
27
+ color: colors.$gray-70;
28
+ border-bottom: 1px solid colors.$gray-50;
29
+ }
30
+
31
+ .displayText {
32
+ padding: layout.$spacing-05;
33
+ }
34
+
35
+ .suggestions li:not(:last-of-type) {
36
+ border-bottom: 1px solid colors.$gray-40-hover;
37
+ }
38
+
39
+ .autocomplete {
40
+ position: relative;
41
+ }
42
+
43
+ .autocompleteSearch {
44
+ width: 100%;
45
+ }
46
+
47
+ .suggestions a {
48
+ color: inherit;
49
+ text-decoration: none;
50
+ }
51
+
52
+ .invalid input {
53
+ outline: 2px solid var(--cds-support-error, colors.$red-60);
54
+ outline-offset: -2px;
55
+ margin-bottom: layout.$spacing-02;
56
+ }
57
+
58
+ .invalidMsg {
59
+ color: var(--cds-text-error, colors.$red-60);
60
+ font-size: small;
61
+ }
@@ -0,0 +1,94 @@
1
+ import { Tile } from '@carbon/react';
2
+ import React, { useCallback, useState } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import ResultsTile from '../common/results-tile.component';
5
+ import { searchLocation } from '../hooks/UseFacilityLocations';
6
+ import { LocationResponse } from '../types';
7
+ import { Autosuggest } from './autosuggest.component';
8
+ import styles from './location-autosuggest.scss';
9
+
10
+ interface LocationAutosuggestProps {
11
+ onLocationSelected: (locationUuid: string, locationData: LocationResponse) => void;
12
+ labelText?: string;
13
+ placeholder?: string;
14
+ invalid?: boolean;
15
+ invalidText?: string;
16
+ }
17
+
18
+ export const LocationAutosuggest: React.FC<LocationAutosuggestProps> = ({
19
+ onLocationSelected,
20
+ labelText = 'Select Location',
21
+ placeholder = 'Search for a location...',
22
+ invalid = false,
23
+ invalidText = 'Please select a valid location',
24
+ }) => {
25
+ const [searchResults, setSearchResults] = useState<LocationResponse[]>([]);
26
+ const { t } = useTranslation();
27
+
28
+ const getDisplayValue = useCallback((item: LocationResponse) => {
29
+ return item.display || item.name || '';
30
+ }, []);
31
+
32
+ const getFieldValue = useCallback((item: LocationResponse) => {
33
+ return item.uuid || '';
34
+ }, []);
35
+
36
+ const handleSuggestionSelected = useCallback(
37
+ (field: string, value: string) => {
38
+ if (value) {
39
+ const selected = searchResults.find((item) => item.uuid === value);
40
+ if (selected) {
41
+ onLocationSelected(value, selected);
42
+ }
43
+ }
44
+ },
45
+ [onLocationSelected, searchResults],
46
+ );
47
+
48
+ const handleSearchResults = useCallback(async (query: string) => {
49
+ const results = await searchLocation(query);
50
+ setSearchResults(results);
51
+ return results;
52
+ }, []);
53
+
54
+ const renderSuggestionItem = useCallback((item: LocationResponse) => {
55
+ return (
56
+ <div>
57
+ <ResultsTile location={item} />
58
+ </div>
59
+ );
60
+ }, []);
61
+
62
+ const renderEmptyState = useCallback((value: string) => {
63
+ if (!value) {
64
+ return null;
65
+ }
66
+
67
+ return (
68
+ <div className={styles.tileContainer}>
69
+ <Tile className={styles.tileNoContent}>
70
+ <div className={styles.tileContents}>
71
+ <p className={styles.content}>{t('searchNoResults', `Found no matching results`)}</p>
72
+ <p className={styles.helper}>{t('searchNoResultsHelper', 'Try searching for a different term')}</p>
73
+ </div>
74
+ </Tile>
75
+ </div>
76
+ );
77
+ }, []);
78
+
79
+ return (
80
+ <Autosuggest
81
+ id="location-autosuggest"
82
+ labelText={labelText}
83
+ placeholder={placeholder}
84
+ getDisplayValue={getDisplayValue}
85
+ getFieldValue={getFieldValue}
86
+ getSearchResults={handleSearchResults}
87
+ onSuggestionSelected={handleSuggestionSelected}
88
+ renderSuggestionItem={renderSuggestionItem}
89
+ renderEmptyState={renderEmptyState}
90
+ invalid={invalid}
91
+ invalidText={invalidText}
92
+ />
93
+ );
94
+ };
@@ -0,0 +1,48 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .tileContainer {
6
+ background-color: colors.$white-0;
7
+ border-top: 1px solid colors.$gray-20;
8
+ padding: layout.$spacing-09 0;
9
+ }
10
+
11
+ .tileNoContent {
12
+ margin: auto;
13
+ width: fit-content;
14
+ display: flex;
15
+ flex-direction: column;
16
+ align-items: center;
17
+ }
18
+
19
+ .tileContent {
20
+ display: flex;
21
+ flex-direction: row;
22
+ align-items: center;
23
+ gap: layout.$spacing-05;
24
+ }
25
+
26
+ .details {
27
+ display: flex;
28
+ flex-direction: column;
29
+ align-items: flex-start;
30
+ }
31
+
32
+ .content {
33
+ @include type.type-style('heading-compact-02');
34
+ color: colors.$gray-70;
35
+ margin-bottom: layout.$spacing-03;
36
+ }
37
+
38
+ .helper {
39
+ @include type.type-style('body-compact-01');
40
+ color: colors.$gray-70;
41
+ }
42
+
43
+ .illustrationPictogram {
44
+ width: layout.$spacing-07;
45
+ height: layout.$spacing-07;
46
+ fill: var(--brand-03);
47
+ flex-shrink: 0;
48
+ }
@@ -0,0 +1,45 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { type LocationResponse } from '../types';
4
+ import { Tag, Tile } from '@carbon/react';
5
+ import { Hospital } from '@carbon/pictograms-react';
6
+ import styles from './results-tile.scss';
7
+ import { Close } from '@carbon/react/icons';
8
+
9
+ interface ResultsTileProps {
10
+ location?: LocationResponse;
11
+ onClose?: () => void;
12
+ }
13
+
14
+ const ResultsTile: React.FC<ResultsTileProps> = ({ location, onClose }) => {
15
+ const { t } = useTranslation();
16
+
17
+ return (
18
+ <Tile className={styles.tile}>
19
+ <div className={styles.tileContent}>
20
+ {onClose && <Close size={16} className={styles.closeIcon} onClick={onClose} />}
21
+ <Hospital className={styles.illustrationPictogram} />
22
+ <div className={styles.details}>
23
+ <div className={styles.locationName}>{location.display || location.name}</div>
24
+ {location.description && <div className={styles.description}>{location.description}</div>}
25
+ {(location.stateProvince || location.country) && (
26
+ <div className={styles.locationDetails}>
27
+ {[location.stateProvince, location.country].filter(Boolean).join(', ')}
28
+ </div>
29
+ )}
30
+ {location.tags && (
31
+ <div className={styles.tags}>
32
+ {location.tags.map((tag, index) => (
33
+ <Tag key={index} className={styles.tag}>
34
+ {tag.display || tag.name}
35
+ </Tag>
36
+ ))}
37
+ </div>
38
+ )}
39
+ </div>
40
+ </div>
41
+ </Tile>
42
+ );
43
+ };
44
+
45
+ export default ResultsTile;
@@ -0,0 +1,86 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .tile {
6
+ position: relative;
7
+ }
8
+
9
+ .tileContainer {
10
+ background-color: colors.$white-0;
11
+ border-top: 1px solid colors.$gray-20;
12
+ padding: layout.$spacing-10 0;
13
+ }
14
+
15
+ .tileNoContent {
16
+ margin: auto;
17
+ width: fit-content;
18
+ display: flex;
19
+ flex-direction: column;
20
+ align-items: center;
21
+ }
22
+
23
+ .tileContent {
24
+ display: flex;
25
+ flex-direction: row;
26
+ align-items: center;
27
+ gap: layout.$spacing-05;
28
+ }
29
+
30
+ .details {
31
+ display: flex;
32
+ flex-direction: column;
33
+ align-items: flex-start;
34
+ }
35
+
36
+ .content {
37
+ @include type.type-style('heading-compact-02');
38
+ color: colors.$gray-70;
39
+ margin-bottom: layout.$spacing-03;
40
+ }
41
+
42
+ .helper {
43
+ @include type.type-style('body-compact-01');
44
+ color: colors.$gray-70;
45
+ }
46
+
47
+ .illustrationPictogram {
48
+ width: layout.$spacing-07;
49
+ height: layout.$spacing-07;
50
+ fill: var(--brand-03);
51
+ flex-shrink: 0;
52
+ }
53
+
54
+ .closeIcon {
55
+ position: absolute;
56
+ top: layout.$spacing-03;
57
+ right: layout.$spacing-03;
58
+ cursor: pointer;
59
+ color: colors.$gray-70;
60
+
61
+ &:hover {
62
+ color: colors.$gray-100;
63
+ }
64
+ }
65
+
66
+ .description {
67
+ @include type.type-style('body-compact-01');
68
+ color: colors.$gray-70;
69
+ margin-bottom: layout.$spacing-03;
70
+ font-size: layout.$spacing-01;
71
+ }
72
+ .locationName {
73
+ @include type.type-style('body-compact-01');
74
+ color: colors.$gray-70;
75
+ font-weight: bold;
76
+ }
77
+ .locationDetails {
78
+ @include type.type-style('body-compact-01');
79
+ color: colors.$gray-70;
80
+ font-size: layout.$spacing-01;
81
+ }
82
+ .tags {
83
+ @include type.type-style('body-compact-01');
84
+ color: colors.$gray-70;
85
+ font-size: layout.$spacing-01;
86
+ }
@@ -0,0 +1,34 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .form {
6
+ display: flex;
7
+ flex-direction: column;
8
+ justify-content: space-between;
9
+ height: 100%;
10
+ }
11
+
12
+ .formContainer {
13
+ margin: layout.$spacing-05;
14
+ display: flex;
15
+ flex-direction: column;
16
+ gap: layout.$spacing-05;
17
+ }
18
+
19
+ .tablet {
20
+ padding: layout.$spacing-06 layout.$spacing-05;
21
+ background-color: colors.$white;
22
+ }
23
+
24
+ .desktop {
25
+ padding: 0;
26
+ }
27
+ .buttonContainer {
28
+ max-width: 50%;
29
+ }
30
+ .inlineLoading {
31
+ display: flex;
32
+ align-items: center;
33
+ gap: layout.$spacing-03;
34
+ }
@@ -0,0 +1,200 @@
1
+ import React, { useEffect, useMemo } from 'react';
2
+ import {
3
+ type DefaultWorkspaceProps,
4
+ ResponsiveWrapper,
5
+ useLayoutType,
6
+ showSnackbar,
7
+ useConfig,
8
+ restBaseUrl,
9
+ } from '@openmrs/esm-framework';
10
+ import { useTranslation } from 'react-i18next';
11
+ import { Controller, useForm } from 'react-hook-form';
12
+ import {
13
+ ButtonSet,
14
+ Button,
15
+ InlineLoading,
16
+ TextInput,
17
+ FormGroup,
18
+ Stack,
19
+ Form,
20
+ FilterableMultiSelect,
21
+ } from '@carbon/react';
22
+ import classNames from 'classnames';
23
+ import { z } from 'zod';
24
+ import { zodResolver } from '@hookform/resolvers/zod';
25
+ import styles from './add-location.workspace.scss';
26
+ import { type LocationResponse } from '../../types';
27
+ import { extractErrorMessagesFromResponse } from '../../helpers';
28
+ import { useLocationTags } from '../../hooks/useLocationTags';
29
+ import { mutate } from 'swr';
30
+ import { saveOrUpdateLocation } from '../../hooks/useLocation';
31
+
32
+ type AddLocationWorkspaceProps = DefaultWorkspaceProps & {
33
+ location?: LocationResponse;
34
+ };
35
+
36
+ const locationFormSchema = z.object({
37
+ name: z.string().min(1, { message: 'Location name is required' }),
38
+ tags: z
39
+ .object({
40
+ uuid: z.string().uuid(),
41
+ display: z.string(),
42
+ })
43
+ .array()
44
+ .nonempty('At least one tag is required'),
45
+ });
46
+
47
+ type LocationFormType = z.infer<typeof locationFormSchema>;
48
+
49
+ const AddLocationWorkspace: React.FC<AddLocationWorkspaceProps> = ({
50
+ closeWorkspace,
51
+ closeWorkspaceWithSavedChanges,
52
+ promptBeforeClosing,
53
+ location,
54
+ }) => {
55
+ const { t } = useTranslation();
56
+ const isTablet = useLayoutType() === 'tablet';
57
+ const { locationTagList: Tags } = useLocationTags();
58
+
59
+ const hasLocationAttributes = useMemo(() => {
60
+ return location?.attributes && location.attributes.length > 0;
61
+ }, [location?.attributes]);
62
+
63
+ const handleMutation = () => {
64
+ const url = `${restBaseUrl}/location`;
65
+ mutate((key) => typeof key === 'string' && key.startsWith(url), undefined, { revalidate: true });
66
+ };
67
+
68
+ const {
69
+ handleSubmit,
70
+ control,
71
+ getValues,
72
+ formState: { isSubmitting, isDirty, errors },
73
+ } = useForm<LocationFormType>({
74
+ resolver: zodResolver(locationFormSchema),
75
+ defaultValues: {
76
+ name: location?.name || '',
77
+ tags: location?.tags || [],
78
+ },
79
+ });
80
+
81
+ const onSubmit = async (data: LocationFormType) => {
82
+ const formDataFormSubmission = getValues();
83
+
84
+ const locationTagsUuid = formDataFormSubmission?.tags?.map((tag) => tag.uuid) || [];
85
+
86
+ const locationPayload = {
87
+ name: formDataFormSubmission.name,
88
+ tags: locationTagsUuid,
89
+ };
90
+
91
+ try {
92
+ const locationUuid = location?.uuid && typeof location.uuid === 'string' ? location.uuid : undefined;
93
+
94
+ await saveOrUpdateLocation(locationPayload, locationUuid);
95
+
96
+ showSnackbar({
97
+ title: t('success', 'Success'),
98
+ kind: 'success',
99
+ subtitle: locationUuid
100
+ ? t('locationUpdated', 'Location {{locationName}} was updated successfully.', {
101
+ locationName: data.name,
102
+ })
103
+ : t('locationCreated', 'Location {{locationName}} was created successfully.', {
104
+ locationName: data.name,
105
+ }),
106
+ });
107
+ handleMutation();
108
+ closeWorkspaceWithSavedChanges();
109
+ } catch (error: any) {
110
+ const errorMessages = extractErrorMessagesFromResponse(error);
111
+ showSnackbar({
112
+ title: t('error', 'Error'),
113
+ kind: 'error',
114
+ subtitle: errorMessages.join(', ') || t('locationSaveError', 'Error saving location'),
115
+ });
116
+ }
117
+ };
118
+
119
+ useEffect(() => {
120
+ promptBeforeClosing(() => isDirty);
121
+ }, [isDirty, promptBeforeClosing]);
122
+
123
+ return (
124
+ <Form onSubmit={handleSubmit(onSubmit)} className={styles.form}>
125
+ <div className={styles.formContainer}>
126
+ <Stack gap={3}>
127
+ <ResponsiveWrapper>
128
+ <FormGroup legendText="">
129
+ <Controller
130
+ control={control}
131
+ name="name"
132
+ render={({ field }) => (
133
+ <TextInput
134
+ id="locationName"
135
+ placeholder={t('locationPlaceholder', 'Add a location')}
136
+ labelText={t('locationName', 'Location Name')}
137
+ value={field.value}
138
+ onChange={field.onChange}
139
+ invalid={!!errors.name?.message}
140
+ invalidText={errors.name?.message}
141
+ disabled={hasLocationAttributes}
142
+ />
143
+ )}
144
+ />
145
+ </FormGroup>
146
+ </ResponsiveWrapper>
147
+
148
+ <ResponsiveWrapper>
149
+ <FormGroup legendText="">
150
+ <Controller
151
+ control={control}
152
+ name="tags"
153
+ render={({ field: { onChange, value, ref } }) => (
154
+ <FilterableMultiSelect
155
+ id="locationTags"
156
+ titleText={t('selectTags', 'Select tag(s)')}
157
+ placeholder={t('selectTagPlaceholder', 'Select a tag')}
158
+ items={Tags || []}
159
+ selectedItems={(value || []).map(
160
+ (selected) => Tags?.find((tag) => tag.uuid === selected.uuid) || selected,
161
+ )}
162
+ onChange={({ selectedItems }) => onChange(selectedItems || [])}
163
+ itemToString={(item) => (item && typeof item === 'object' ? item.display : '')}
164
+ selectionFeedback="top-after-reopen"
165
+ invalid={!!errors.tags?.message}
166
+ invalidText={errors.tags?.message}
167
+ disabled={!Tags?.length}
168
+ ref={ref}
169
+ />
170
+ )}
171
+ />
172
+ </FormGroup>
173
+ </ResponsiveWrapper>
174
+ </Stack>
175
+ </div>
176
+
177
+ <ButtonSet
178
+ className={classNames({
179
+ [styles.tablet]: isTablet,
180
+ [styles.desktop]: !isTablet,
181
+ })}>
182
+ <Button className={styles.buttonContainer} kind="secondary" onClick={() => closeWorkspace()}>
183
+ {t('cancel', 'Cancel')}
184
+ </Button>
185
+ <Button className={styles.buttonContainer} disabled={isSubmitting || !isDirty} kind="primary" type="submit">
186
+ {isSubmitting ? (
187
+ <span className={styles.inlineLoading}>
188
+ {t('submitting', 'Submitting' + '...')}
189
+ <InlineLoading status="active" iconDescription="Loading" />
190
+ </span>
191
+ ) : (
192
+ t('saveAndClose', 'Save & close')
193
+ )}
194
+ </Button>
195
+ </ButtonSet>
196
+ </Form>
197
+ );
198
+ };
199
+
200
+ export default AddLocationWorkspace;
@@ -0,0 +1,79 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .form {
6
+ display: flex;
7
+ flex-direction: column;
8
+ justify-content: space-between;
9
+ height: 100%;
10
+ }
11
+
12
+ .formContainer {
13
+ margin: layout.$spacing-05;
14
+ display: flex;
15
+ flex-direction: column;
16
+ gap: layout.$spacing-05;
17
+ }
18
+
19
+ .tablet {
20
+ padding: layout.$spacing-06 layout.$spacing-05;
21
+ background-color: colors.$white;
22
+ }
23
+
24
+ .desktop {
25
+ padding: 0;
26
+ }
27
+ .buttonContainer {
28
+ max-width: 50%;
29
+ }
30
+ .inlineLoading {
31
+ display: flex;
32
+ align-items: center;
33
+ gap: layout.$spacing-03;
34
+ }
35
+
36
+ .tileContainer {
37
+ background-color: colors.$white-0;
38
+ border-top: 1px solid colors.$gray-20;
39
+ padding: layout.$spacing-09 0;
40
+ }
41
+
42
+ .tileNoContent {
43
+ margin: auto;
44
+ width: fit-content;
45
+ display: flex;
46
+ flex-direction: column;
47
+ align-items: center;
48
+ }
49
+
50
+ .tileContent {
51
+ display: flex;
52
+ flex-direction: row; // Changed from column to row
53
+ align-items: center; // Center items vertically
54
+ gap: layout.$spacing-05; // Add gap between pictogram and details
55
+ }
56
+
57
+ .details {
58
+ display: flex;
59
+ flex-direction: column;
60
+ align-items: flex-start;
61
+ }
62
+
63
+ .content {
64
+ @include type.type-style('heading-compact-02');
65
+ color: colors.$gray-70;
66
+ margin-bottom: layout.$spacing-03;
67
+ }
68
+
69
+ .helper {
70
+ @include type.type-style('body-compact-01');
71
+ color: colors.$gray-70;
72
+ }
73
+
74
+ .illustrationPictogram {
75
+ width: layout.$spacing-07;
76
+ height: layout.$spacing-07;
77
+ fill: var(--brand-03);
78
+ flex-shrink: 0;
79
+ }