@kenyaemr/esm-admin-app 5.4.4-pre.428 → 5.4.4-pre.429

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 (71) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/1462.js +1 -1
  3. package/dist/1972.js +1 -1
  4. package/dist/2016.js +1 -1
  5. package/dist/2270.js +1 -1
  6. package/dist/2270.js.map +1 -1
  7. package/dist/2294.js +1 -1
  8. package/dist/2294.js.map +1 -1
  9. package/dist/235.js +1 -0
  10. package/dist/235.js.map +1 -0
  11. package/dist/2467.js +1 -1
  12. package/dist/2467.js.map +1 -1
  13. package/dist/2763.js +1 -0
  14. package/dist/2763.js.map +1 -0
  15. package/dist/2948.js +1 -1
  16. package/dist/3380.js +1 -1
  17. package/dist/3380.js.map +1 -1
  18. package/dist/3548.js +1 -1
  19. package/dist/3816.js +1 -1
  20. package/dist/3852.js +1 -0
  21. package/dist/3852.js.map +1 -0
  22. package/dist/4157.js +1 -0
  23. package/dist/4157.js.map +1 -0
  24. package/dist/4337.js +1 -1
  25. package/dist/4858.js +1 -1
  26. package/dist/487.js +1 -1
  27. package/dist/552.js +1 -0
  28. package/dist/552.js.map +1 -0
  29. package/dist/6092.js +1 -0
  30. package/dist/6092.js.map +1 -0
  31. package/dist/6456.js +1 -1
  32. package/dist/7607.js +1 -1
  33. package/dist/7737.js +1 -1
  34. package/dist/7739.js +1 -1
  35. package/dist/8376.js +1 -1
  36. package/dist/8727.js +1 -1
  37. package/dist/9182.js +1 -1
  38. package/dist/9406.js +1 -0
  39. package/dist/9406.js.map +1 -0
  40. package/dist/9446.js +1 -1
  41. package/dist/9647.js +1 -1
  42. package/dist/kenyaemr-esm-admin-app.js +3 -3
  43. package/dist/kenyaemr-esm-admin-app.js.buildmanifest.json +208 -157
  44. package/dist/kenyaemr-esm-admin-app.js.map +1 -1
  45. package/dist/main.js +3 -3
  46. package/dist/main.js.map +1 -1
  47. package/dist/routes.json +1 -1
  48. package/package.json +1 -1
  49. package/src/components/global-property/hooks/useLogoUpload.ts +28 -0
  50. package/src/components/global-property/table/global-property-table.component.tsx +117 -125
  51. package/src/components/global-property/table/global-property-table.scss +10 -0
  52. package/src/components/global-property/table/global-property-table.test.tsx +30 -26
  53. package/src/components/global-property/workspace/upload-logo.workspace.scss +61 -0
  54. package/src/components/global-property/workspace/upload-logo.workspace.test.tsx +180 -0
  55. package/src/components/global-property/workspace/upload-logo.workspace.tsx +191 -0
  56. package/src/index.ts +4 -0
  57. package/src/routes.json +5 -0
  58. package/translations/am.json +22 -5
  59. package/translations/en.json +25 -8
  60. package/translations/fr.json +22 -5
  61. package/translations/sw.json +22 -5
  62. package/dist/1442.js +0 -1
  63. package/dist/1442.js.map +0 -1
  64. package/dist/5910.js +0 -1
  65. package/dist/5910.js.map +0 -1
  66. package/dist/6253.js +0 -1
  67. package/dist/6253.js.map +0 -1
  68. package/dist/6455.js +0 -1
  69. package/dist/6455.js.map +0 -1
  70. package/dist/6925.js +0 -1
  71. package/dist/6925.js.map +0 -1
package/dist/routes.json CHANGED
@@ -1 +1 @@
1
- {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"kenyaemrCharts":"^1.6.7"},"extensions":[{"component":"adminLeftPanelLink","name":"admin-left-panel-link","slot":"admin-left-panel-slot"},{"component":"userManagementLeftPannelLink","name":"user-management-left-panel-link","slot":"admin-left-panel-slot"},{"component":"etlAdministrationLeftPannelLink","name":"etl-administration-left-panel-link","slot":"admin-left-panel-slot"},{"component":"locationsLeftPanelLink","name":"locations-left-panel-link","slot":"admin-left-panel-slot"},{"component":"facilitySetupLeftPanelLink","name":"facility-setup-left-panel-link","slot":"admin-left-panel-slot"},{"name":"global-property-link","component":"globalPropertyLink","slot":"admin-left-panel-slot"},{"component":"providerBanner","name":"provider-banner","slot":"provider-banner-info-slot","order":1},{"component":"globalPropertyDashboard","name":"global-property-dashboard"}],"modals":[{"component":"operationConfirmationModal","name":"operation-confirmation-modal"},{"component":"hwrConfirmationModal","name":"hwr-confirmation-modal"},{"component":"hwrEmptyModal","name":"hwr-empty-modal"},{"component":"hwrSyncModal","name":"hwr-syncing-modal"},{"component":"deleteGlobalPropertyModal","name":"delete-global-property-modal"}],"workspaces2":[{"name":"manage-user-workspace","component":"manageUserWorkspace","window":"esm-admin-extra-wide-workspace-window"},{"name":"user-role-scope-workspace","component":"userRoleScopeWorkspace","window":"esm-admin-workspace-window"},{"name":"add-location-workspace","component":"addLocation","window":"esm-admin-workspace-window"},{"name":"search-location-workspace","component":"searchLocationWorkspace","window":"esm-admin-workspace-window"},{"name":"global-property-workspace","component":"globalPropertyWorkspace","window":"esm-admin-workspace-window"}],"workspaceWindows2":[{"name":"esm-admin-workspace-window","canMaximize":true,"group":"esm-admin-workspace-group","width":"wider"},{"name":"esm-admin-extra-wide-workspace-window","canMaximize":true,"group":"esm-admin-workspace-group","width":"extra-wide"}],"workspaceGroups2":[{"name":"esm-admin-workspace-group","scopePattern":"/admin","closeable":true}],"pages":[{"component":"root","route":"admin"}],"version":"5.4.4-pre.428"}
1
+ {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"kenyaemrCharts":"^1.6.7"},"extensions":[{"component":"adminLeftPanelLink","name":"admin-left-panel-link","slot":"admin-left-panel-slot"},{"component":"userManagementLeftPannelLink","name":"user-management-left-panel-link","slot":"admin-left-panel-slot"},{"component":"etlAdministrationLeftPannelLink","name":"etl-administration-left-panel-link","slot":"admin-left-panel-slot"},{"component":"locationsLeftPanelLink","name":"locations-left-panel-link","slot":"admin-left-panel-slot"},{"component":"facilitySetupLeftPanelLink","name":"facility-setup-left-panel-link","slot":"admin-left-panel-slot"},{"name":"global-property-link","component":"globalPropertyLink","slot":"admin-left-panel-slot"},{"component":"providerBanner","name":"provider-banner","slot":"provider-banner-info-slot","order":1},{"component":"globalPropertyDashboard","name":"global-property-dashboard"}],"modals":[{"component":"operationConfirmationModal","name":"operation-confirmation-modal"},{"component":"hwrConfirmationModal","name":"hwr-confirmation-modal"},{"component":"hwrEmptyModal","name":"hwr-empty-modal"},{"component":"hwrSyncModal","name":"hwr-syncing-modal"},{"component":"deleteGlobalPropertyModal","name":"delete-global-property-modal"}],"workspaces2":[{"name":"manage-user-workspace","component":"manageUserWorkspace","window":"esm-admin-extra-wide-workspace-window"},{"name":"user-role-scope-workspace","component":"userRoleScopeWorkspace","window":"esm-admin-workspace-window"},{"name":"add-location-workspace","component":"addLocation","window":"esm-admin-workspace-window"},{"name":"search-location-workspace","component":"searchLocationWorkspace","window":"esm-admin-workspace-window"},{"name":"global-property-workspace","component":"globalPropertyWorkspace","window":"esm-admin-workspace-window"},{"name":"upload-logo-workspace","component":"uploadLogoWorkspace","window":"esm-admin-workspace-window"}],"workspaceWindows2":[{"name":"esm-admin-workspace-window","canMaximize":true,"group":"esm-admin-workspace-group","width":"wider"},{"name":"esm-admin-extra-wide-workspace-window","canMaximize":true,"group":"esm-admin-workspace-group","width":"extra-wide"}],"workspaceGroups2":[{"name":"esm-admin-workspace-group","scopePattern":"/admin","closeable":true}],"pages":[{"component":"root","route":"admin"}],"version":"5.4.4-pre.429"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kenyaemr/esm-admin-app",
3
- "version": "5.4.4-pre.428",
3
+ "version": "5.4.4-pre.429",
4
4
  "description": "Facilitates the management of ETL tables",
5
5
  "keywords": [
6
6
  "openmrs"
@@ -0,0 +1,28 @@
1
+ import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
+
3
+ export const ALLOWED_LOGO_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/webp'];
4
+ export const MAX_LOGO_SIZE_BYTES = 2 * 1024 * 1024; // 2 MB
5
+
6
+ export type LogoTarget = 'prescription' | 'receipt';
7
+
8
+ export type UploadLogoResponse = {
9
+ savedPath: string;
10
+ message: string;
11
+ };
12
+
13
+ export async function uploadLogo(file: File, target: LogoTarget = 'prescription'): Promise<UploadLogoResponse> {
14
+ const formData = new FormData();
15
+ formData.append('file', file);
16
+
17
+ const response = await openmrsFetch(`${restBaseUrl}/palladiumemr/logo?target=${target}`, {
18
+ method: 'POST',
19
+ body: formData,
20
+ });
21
+
22
+ if (!response.ok) {
23
+ const errorData = await response.json().catch(() => ({}));
24
+ throw new Error(errorData?.error ?? errorData?.error?.message ?? 'Failed to upload image');
25
+ }
26
+
27
+ return response.json();
28
+ }
@@ -2,28 +2,25 @@ import React, { useCallback, useMemo, useState } from 'react';
2
2
  import {
3
3
  Button,
4
4
  DataTable,
5
- DataTableSkeleton,
5
+ OverflowMenu,
6
+ OverflowMenuItem,
6
7
  Pagination,
7
- Search,
8
8
  Table,
9
9
  TableBody,
10
10
  TableCell,
11
+ TableContainer,
11
12
  TableHead,
12
13
  TableHeader,
13
14
  TableRow,
15
+ TableToolbar,
16
+ TableToolbarContent,
17
+ TableToolbarSearch,
18
+ Tag,
14
19
  type DataTableHeader,
15
20
  } from '@carbon/react';
16
- import { Add, Edit, TrashCan } from '@carbon/react/icons';
21
+ import { Add, Upload } from '@carbon/react/icons';
17
22
  import { useTranslation } from 'react-i18next';
18
- import {
19
- ErrorCard,
20
- isDesktop,
21
- launchWorkspace2,
22
- showModal,
23
- useDebounce,
24
- useLayoutType,
25
- usePaginationInfo,
26
- } from '@openmrs/esm-framework';
23
+ import { ErrorCard, launchWorkspace2, showModal, useDebounce, usePaginationInfo } from '@openmrs/esm-framework';
27
24
 
28
25
  import { useGlobalProperties } from '../hooks/useGlobalProperty';
29
26
  import styles from './global-property-table.scss';
@@ -39,15 +36,12 @@ type GlobalPropertyRow = {
39
36
 
40
37
  const GlobalPropertyTable: React.FC = () => {
41
38
  const { t } = useTranslation();
42
- const layoutType = useLayoutType();
43
- const desktop = isDesktop(layoutType);
44
39
 
45
40
  const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
46
41
  const [searchTerm, setSearchTerm] = useState('');
47
42
  const debouncedSearchTerm = useDebounce(searchTerm, 300);
48
43
 
49
44
  const {
50
- isLoading,
51
45
  data: globalProperties = [],
52
46
  error,
53
47
  goTo,
@@ -66,7 +60,7 @@ const GlobalPropertyTable: React.FC = () => {
66
60
  [t],
67
61
  );
68
62
 
69
- const rows: Array<GlobalPropertyRow> = useMemo(
63
+ const tableRows: Array<GlobalPropertyRow> = useMemo(
70
64
  () =>
71
65
  globalProperties.map((gp, idx) => ({
72
66
  id: gp?.uuid ?? `gp-${idx}`,
@@ -77,8 +71,9 @@ const GlobalPropertyTable: React.FC = () => {
77
71
  );
78
72
 
79
73
  const handleSearchChange = useCallback(
80
- (event: React.ChangeEvent<HTMLInputElement>) => {
81
- setSearchTerm(event.target.value);
74
+ (event: React.ChangeEvent<HTMLInputElement> | '', value?: string) => {
75
+ const searchValue = typeof event === 'string' ? value ?? '' : event.target.value;
76
+ setSearchTerm(searchValue);
82
77
  if (currentPage !== 1) {
83
78
  goTo(1);
84
79
  }
@@ -106,6 +101,12 @@ const GlobalPropertyTable: React.FC = () => {
106
101
  [globalProperties, mutate],
107
102
  );
108
103
 
104
+ const openUploadImageWorkspace = useCallback(() => {
105
+ launchWorkspace2('upload-logo-workspace', {
106
+ mutateGlobalProperty: mutate,
107
+ });
108
+ }, [mutate]);
109
+
109
110
  const handleEdit = useCallback(
110
111
  (row: GlobalPropertyRow) => {
111
112
  const systemSetting = globalProperties.find((gp) => gp.uuid === row.id);
@@ -126,95 +127,6 @@ const GlobalPropertyTable: React.FC = () => {
126
127
  [mutate],
127
128
  );
128
129
 
129
- const renderContent = () => {
130
- if (isLoading) {
131
- return (
132
- <DataTableSkeleton
133
- aria-label={t('globalProperties', 'Global properties')}
134
- headers={headers}
135
- showHeader
136
- showToolbar
137
- />
138
- );
139
- }
140
-
141
- if (rows.length === 0) {
142
- return (
143
- <p className={styles.emptyState}>
144
- {debouncedSearchTerm
145
- ? t('noMatchingGlobalProperties', 'No global properties match your search')
146
- : t('noGlobalProperties', 'No global properties to display')}
147
- </p>
148
- );
149
- }
150
-
151
- return (
152
- <>
153
- <DataTable useZebraStyles size={desktop ? 'sm' : 'md'} rows={rows} headers={headers}>
154
- {({ rows: renderRows, headers: renderHeaders, getTableProps, getHeaderProps, getRowProps, getCellProps }) => (
155
- <Table {...getTableProps()}>
156
- <TableHead>
157
- <TableRow>
158
- {renderHeaders.map((header) => (
159
- <TableHeader {...getHeaderProps({ header })}>{header.header}</TableHeader>
160
- ))}
161
- <TableHeader>
162
- <span className={styles.visuallyHidden}>{t('actions', 'Actions')}</span>
163
- </TableHeader>
164
- </TableRow>
165
- </TableHead>
166
- <TableBody>
167
- {renderRows.map((row) => {
168
- const sourceRow = rows.find((r) => r.id === row.id)!;
169
- return (
170
- <TableRow {...getRowProps({ row })}>
171
- {row.cells.map((cell) => (
172
- <TableCell {...getCellProps({ cell })}>{cell.value}</TableCell>
173
- ))}
174
- <TableCell className={styles.actionsCell}>
175
- <Button
176
- kind="ghost"
177
- size="sm"
178
- renderIcon={Edit}
179
- iconDescription={t('edit', 'Edit')}
180
- hasIconOnly
181
- onClick={() => handleEdit(sourceRow)}
182
- />
183
- <Button
184
- kind="danger--ghost"
185
- size="sm"
186
- renderIcon={TrashCan}
187
- iconDescription={t('delete', 'Delete')}
188
- hasIconOnly
189
- onClick={() => handleDelete(sourceRow)}
190
- />
191
- </TableCell>
192
- </TableRow>
193
- );
194
- })}
195
- </TableBody>
196
- </Table>
197
- )}
198
- </DataTable>
199
-
200
- <Pagination
201
- itemsPerPageText={t('itemsPerPage', 'Items per page:')}
202
- forwardText={t('nextPage', 'Next page')}
203
- backwardText={t('previousPage', 'Previous page')}
204
- itemRangeText={(min, max, total) =>
205
- t('minMaxItems', '{{min}}-{{max}} of {{total}} items', { min, max, total })
206
- }
207
- pageRangeText={(_current, total) => t('pageRangeText', 'of {{count}} pages', { count: total })}
208
- page={currentPage}
209
- pageSize={pageSize}
210
- pageSizes={pageSizes?.length > 0 ? pageSizes : PAGE_SIZE_OPTIONS}
211
- totalItems={totalCount ?? 0}
212
- onChange={handlePaginationChange}
213
- />
214
- </>
215
- );
216
- };
217
-
218
130
  if (error) {
219
131
  return (
220
132
  <div className={styles.dataTableContainer}>
@@ -224,24 +136,104 @@ const GlobalPropertyTable: React.FC = () => {
224
136
  }
225
137
 
226
138
  return (
227
- <div className={styles.dataTableContainer}>
228
- <div className={styles.tableHeaderSection}>
229
- <Search
230
- id="global-property-search"
231
- labelText=""
232
- placeholder={t('searchGlobalPropertiesByName', 'Search global property by name')}
233
- closeButtonLabelText={t('clearSearchButton', 'Clear search button')}
234
- size={desktop ? 'md' : 'lg'}
235
- value={searchTerm}
236
- onChange={handleSearchChange}
237
- type="search"
238
- />
239
- <Button size={desktop ? 'md' : 'lg'} kind="ghost" renderIcon={Add} onClick={() => openWorkspace()}>
240
- {t('addGlobalProperty', 'Add new global property')}
241
- </Button>
242
- </div>
243
-
244
- {renderContent()}
139
+ <div className={styles.container}>
140
+ <DataTable size="sm" rows={tableRows} headers={headers} isSortable useZebraStyles>
141
+ {({
142
+ rows,
143
+ headers,
144
+ getHeaderProps,
145
+ getRowProps,
146
+ getBatchActionProps,
147
+ getToolbarProps,
148
+ getTableProps,
149
+ getTableContainerProps,
150
+ }) => {
151
+ const batchActionProps = getBatchActionProps();
152
+
153
+ return (
154
+ <TableContainer
155
+ title={t('globalProperty', 'Global Property')}
156
+ description={t(
157
+ 'globalPropertyDescription',
158
+ 'A list of all global properties for the system. Filter by search term to find specific properties.',
159
+ )}
160
+ {...getTableContainerProps()}>
161
+ <TableToolbar {...getToolbarProps()}>
162
+ <TableToolbarContent aria-hidden={batchActionProps.shouldShowBatchActions}>
163
+ <TableToolbarSearch
164
+ persistent
165
+ onChange={handleSearchChange}
166
+ placeholder={t('searchForGlobalProperties', 'Search for global properties')}
167
+ />
168
+ <Button renderIcon={Add} onClick={() => openWorkspace()} size="sm" kind="primary">
169
+ {t('addGlobalProperty', 'Add global property')}
170
+ </Button>
171
+ <Button renderIcon={Upload} onClick={openUploadImageWorkspace} size="sm" kind="tertiary">
172
+ {t('uploadImage', 'Upload image')}
173
+ </Button>
174
+ </TableToolbarContent>
175
+ </TableToolbar>
176
+
177
+ <Table {...getTableProps()} aria-label="Global properties">
178
+ <TableHead>
179
+ <TableRow>
180
+ {headers.map((header) => (
181
+ <TableHeader {...getHeaderProps({ header })}>{header.header}</TableHeader>
182
+ ))}
183
+ <TableHeader aria-label={t('rowActions', 'Row actions')} />
184
+ </TableRow>
185
+ </TableHead>
186
+
187
+ <TableBody>
188
+ {rows.map((row, index) => {
189
+ return (
190
+ <React.Fragment key={row.id}>
191
+ <TableRow {...getRowProps({ row })}>
192
+ {row.cells.map((cell) => {
193
+ if (cell.info.header === 'status') {
194
+ return (
195
+ <TableCell key={cell.id}>
196
+ <Tag size="sm">{cell.value}</Tag>
197
+ </TableCell>
198
+ );
199
+ }
200
+ return <TableCell key={cell.id}>{cell.value}</TableCell>;
201
+ })}
202
+ <TableCell className="cds--table-column-menu">
203
+ <OverflowMenu flipped aria-label="overflow-menu">
204
+ <OverflowMenuItem
205
+ onClick={() => handleEdit(tableRows[index])}
206
+ itemText={t('edit', 'Edit')}
207
+ />
208
+ <OverflowMenuItem
209
+ isDelete
210
+ onClick={() => handleDelete(tableRows[index])}
211
+ itemText={t('delete', 'Delete')}
212
+ />
213
+ </OverflowMenu>
214
+ </TableCell>
215
+ </TableRow>
216
+ </React.Fragment>
217
+ );
218
+ })}
219
+ </TableBody>
220
+ </Table>
221
+ </TableContainer>
222
+ );
223
+ }}
224
+ </DataTable>
225
+ <Pagination
226
+ itemsPerPageText={t('itemsPerPage', 'Items per page:')}
227
+ forwardText={t('nextPage', 'Next page')}
228
+ backwardText={t('previousPage', 'Previous page')}
229
+ itemRangeText={(min, max, total) => t('minMaxItems', '{{min}}-{{max}} of {{total}} items', { min, max, total })}
230
+ pageRangeText={(_current, total) => t('pageRangeText', 'of {{count}} pages', { count: total })}
231
+ page={currentPage}
232
+ pageSize={pageSize}
233
+ pageSizes={pageSizes?.length > 0 ? pageSizes : PAGE_SIZE_OPTIONS}
234
+ totalItems={totalCount ?? 0}
235
+ onChange={handlePaginationChange}
236
+ />
245
237
  </div>
246
238
  );
247
239
  };
@@ -1,4 +1,5 @@
1
1
  @use '@carbon/layout';
2
+ @use '@carbon/colors';
2
3
 
3
4
  .dataTableContainer {
4
5
  height: 100%;
@@ -32,3 +33,12 @@
32
33
  text-align: center;
33
34
  color: var(--cds-text-secondary);
34
35
  }
36
+
37
+ .container {
38
+ margin: layout.$layout-01;
39
+ border: 1px solid colors.$gray-20;
40
+
41
+ :global(.cds--table-toolbar) {
42
+ position: relative !important;
43
+ }
44
+ }
@@ -52,6 +52,9 @@ vi.mock('@carbon/react', async (importOriginal) => {
52
52
  getHeaderProps: ({ header }: any) => ({ key: header.key }),
53
53
  getRowProps: ({ row }: any) => ({ key: row.id }),
54
54
  getCellProps: ({ cell }: any) => ({ key: cell.id }),
55
+ getToolbarProps: () => ({}),
56
+ getTableContainerProps: () => ({}),
57
+ getBatchActionProps: () => ({ shouldShowBatchActions: false }),
55
58
  });
56
59
  }
57
60
  return children;
@@ -65,6 +68,8 @@ vi.mock('@carbon/react', async (importOriginal) => {
65
68
  {hasIconOnly ? null : children}
66
69
  </button>
67
70
  ),
71
+ OverflowMenu: ({ children }: any) => <div>{children}</div>,
72
+ OverflowMenuItem: ({ itemText, onClick }: any) => <button onClick={onClick}>{itemText}</button>,
68
73
  };
69
74
  });
70
75
 
@@ -97,18 +102,6 @@ describe('GlobalPropertyTable', () => {
97
102
  vi.clearAllMocks();
98
103
  });
99
104
 
100
- it('shows a loading skeleton while data is loading', () => {
101
- setupMock({ isLoading: true, data: [] });
102
- render(<GlobalPropertyTable />);
103
- expect(screen.getByRole('table')).toBeInTheDocument();
104
- });
105
-
106
- it('shows empty state message when no properties exist', () => {
107
- setupMock({ data: [] });
108
- render(<GlobalPropertyTable />);
109
- expect(screen.getByText('No global properties to display')).toBeInTheDocument();
110
- });
111
-
112
105
  it('renders the table with property and value columns', () => {
113
106
  setupMock();
114
107
  render(<GlobalPropertyTable />);
@@ -125,10 +118,17 @@ describe('GlobalPropertyTable', () => {
125
118
  expect(screen.getByText('value2')).toBeInTheDocument();
126
119
  });
127
120
 
128
- it('opens the add workspace when "Add new global property" button is clicked', () => {
121
+ it('renders the table without data rows when no properties exist', () => {
122
+ setupMock({ data: [], totalCount: 0 });
123
+ render(<GlobalPropertyTable />);
124
+ expect(screen.getByRole('table')).toBeInTheDocument();
125
+ expect(screen.queryByText('setting.one')).not.toBeInTheDocument();
126
+ });
127
+
128
+ it('opens the add workspace when "Add global property" button is clicked', () => {
129
129
  setupMock();
130
130
  render(<GlobalPropertyTable />);
131
- fireEvent.click(screen.getByText('Add new global property'));
131
+ fireEvent.click(screen.getByText('Add global property'));
132
132
  expect(mockLaunchWorkspace2).toHaveBeenCalledWith(
133
133
  'global-property-workspace',
134
134
  expect.objectContaining({
@@ -138,6 +138,18 @@ describe('GlobalPropertyTable', () => {
138
138
  );
139
139
  });
140
140
 
141
+ it('opens the upload image workspace when "Upload image" button is clicked', () => {
142
+ setupMock();
143
+ render(<GlobalPropertyTable />);
144
+ fireEvent.click(screen.getByText('Upload image'));
145
+ expect(mockLaunchWorkspace2).toHaveBeenCalledWith(
146
+ 'upload-logo-workspace',
147
+ expect.objectContaining({
148
+ mutateGlobalProperty: mockMutate,
149
+ }),
150
+ );
151
+ });
152
+
141
153
  it('opens the edit workspace with the correct property when edit is clicked', () => {
142
154
  setupMock();
143
155
  render(<GlobalPropertyTable />);
@@ -168,19 +180,17 @@ describe('GlobalPropertyTable', () => {
168
180
  it('filters results and resets to page 1 when user types in the search box', async () => {
169
181
  setupMock({ currentPage: 2 });
170
182
  render(<GlobalPropertyTable />);
171
- const searchInput = screen.getByPlaceholderText('Search global property by name');
183
+ const searchInput = screen.getByPlaceholderText('Search for global properties');
172
184
  fireEvent.change(searchInput, { target: { value: 'setting' } });
173
185
  await waitFor(() => {
174
186
  expect(mockGoTo).toHaveBeenCalledWith(1);
175
187
  });
176
188
  });
177
189
 
178
- it('shows "no matching" message when search yields no results', () => {
179
- setupMock({ data: [] });
190
+ it('renders a search input for filtering properties', () => {
191
+ setupMock();
180
192
  render(<GlobalPropertyTable />);
181
- const searchInput = screen.getByPlaceholderText('Search global property by name');
182
- fireEvent.change(searchInput, { target: { value: 'nonexistent' } });
183
- expect(screen.getByText('No global properties match your search')).toBeInTheDocument();
193
+ expect(screen.getByPlaceholderText('Search for global properties')).toBeInTheDocument();
184
194
  });
185
195
 
186
196
  it('renders an error card when the hook returns an error', () => {
@@ -189,10 +199,4 @@ describe('GlobalPropertyTable', () => {
189
199
  expect(screen.getByTestId('error-card')).toBeInTheDocument();
190
200
  expect(screen.getByText('Global property')).toBeInTheDocument();
191
201
  });
192
-
193
- it('renders a search input for filtering properties', () => {
194
- setupMock();
195
- render(<GlobalPropertyTable />);
196
- expect(screen.getByPlaceholderText('Search global property by name')).toBeInTheDocument();
197
- });
198
202
  });
@@ -0,0 +1,61 @@
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
+ overflow-y: auto;
18
+ }
19
+
20
+ .previewContainer {
21
+ display: flex;
22
+ flex-direction: column;
23
+ gap: layout.$spacing-03;
24
+ }
25
+
26
+ .previewLabel {
27
+ @include type.type-style('label-01');
28
+ color: colors.$gray-70;
29
+ }
30
+
31
+ .preview {
32
+ max-width: 100%;
33
+ max-height: 240px;
34
+ object-fit: contain;
35
+ border: 1px solid colors.$gray-20;
36
+ padding: layout.$spacing-03;
37
+ background-color: colors.$white;
38
+ }
39
+
40
+ .tablet {
41
+ padding: layout.$spacing-06 layout.$spacing-05;
42
+ background-color: colors.$white;
43
+ }
44
+
45
+ .desktop {
46
+ padding: 0;
47
+
48
+ & button {
49
+ max-width: 50% !important;
50
+ }
51
+ }
52
+
53
+ .buttonContainer {
54
+ max-width: 50%;
55
+ }
56
+
57
+ .inlineLoading {
58
+ display: flex;
59
+ align-items: center;
60
+ gap: layout.$spacing-03;
61
+ }