@kenyaemr/esm-admin-app 5.4.4-pre.299 → 5.4.4-pre.3
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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/100.js +1 -0
- package/dist/100.js.map +1 -0
- package/dist/1442.js +1 -0
- package/dist/1442.js.map +1 -0
- package/dist/1462.js +1 -1
- package/dist/1972.js +1 -1
- package/dist/2016.js +1 -1
- package/dist/2080.js +1 -1
- package/dist/2080.js.map +1 -1
- package/dist/2096.js +1 -1
- package/dist/2096.js.map +1 -1
- package/dist/2270.js +1 -1
- package/dist/2270.js.map +1 -1
- package/dist/2294.js +1 -1
- package/dist/2294.js.map +1 -1
- package/dist/2467.js +1 -0
- package/dist/2467.js.map +1 -0
- package/dist/257.js +1 -1
- package/dist/257.js.map +1 -1
- package/dist/2948.js +1 -1
- package/dist/3380.js +1 -0
- package/dist/3380.js.map +1 -0
- package/dist/3548.js +1 -1
- package/dist/3816.js +1 -1
- package/dist/4337.js +1 -1
- package/dist/4687.js +1 -0
- package/dist/4687.js.map +1 -0
- package/dist/4858.js +1 -1
- package/dist/487.js +1 -1
- package/dist/5297.js +1 -0
- package/dist/5297.js.map +1 -0
- package/dist/5910.js +1 -0
- package/dist/5910.js.map +1 -0
- package/dist/5940.js +1 -1
- package/dist/5940.js.map +1 -1
- package/dist/6253.js +1 -0
- package/dist/6253.js.map +1 -0
- package/dist/6455.js +1 -0
- package/dist/6455.js.map +1 -0
- package/dist/6456.js +1 -1
- package/dist/6925.js +1 -0
- package/dist/6925.js.map +1 -0
- package/dist/7607.js +1 -1
- package/dist/7737.js +1 -1
- package/dist/7739.js +1 -1
- package/dist/7866.js +1 -1
- package/dist/7866.js.map +1 -1
- package/dist/8206.js +1 -0
- package/dist/8206.js.map +1 -0
- package/dist/8376.js +1 -1
- package/dist/846.js +17 -0
- package/dist/846.js.map +1 -0
- package/dist/{4047.js → 8487.js} +1 -1
- package/dist/8487.js.map +1 -0
- package/dist/8528.js +1 -0
- package/dist/8528.js.map +1 -0
- package/dist/8727.js +1 -1
- package/dist/9182.js +1 -1
- package/dist/9446.js +1 -1
- package/dist/9585.js +1 -0
- package/dist/9585.js.map +1 -0
- package/dist/9647.js +1 -0
- package/dist/9647.js.map +1 -0
- package/dist/kenyaemr-esm-admin-app.js +3 -3
- package/dist/kenyaemr-esm-admin-app.js.buildmanifest.json +329 -306
- package/dist/kenyaemr-esm-admin-app.js.map +1 -1
- package/dist/main.js +3 -3
- package/dist/main.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +1 -1
- package/src/components/confirm-modal/confirmation-operation.test.tsx +8 -19
- package/src/components/dashboard/dashboard.component.tsx +4 -1
- package/src/components/empty-state/empty-state-log.test.tsx +3 -4
- package/src/components/global-property/dashboard/global-property-dashboard.component.tsx +23 -0
- package/src/components/global-property/dashboard/global-property-dashboard.scss +6 -0
- package/src/components/global-property/hooks/useGlobalProperty.ts +64 -0
- package/src/components/global-property/index.ts +14 -0
- package/src/components/global-property/modal/delete-global-property-modal.component.tsx +71 -0
- package/src/components/global-property/modal/delete-global-property-modal.test.tsx +131 -0
- package/src/components/global-property/table/global-property-table.component.tsx +249 -0
- package/src/components/global-property/table/global-property-table.scss +34 -0
- package/src/components/global-property/table/global-property-table.test.tsx +198 -0
- package/src/components/global-property/workspace/global-property-form-schema.ts +32 -0
- package/src/components/global-property/workspace/global-property.workspace.scss +40 -0
- package/src/components/global-property/workspace/global-property.workspace.test.tsx +172 -0
- package/src/components/global-property/workspace/global-property.workspace.tsx +260 -0
- package/src/components/logs-table/operation-log-table.component.tsx +87 -75
- package/src/components/logs-table/operation-log.test.tsx +134 -28
- package/src/index.ts +9 -1
- package/src/root.component.tsx +2 -0
- package/src/routes.json +20 -1
- package/src/types/index.ts +11 -2
- package/translations/am.json +39 -2
- package/translations/en.json +62 -24
- package/translations/fr.json +167 -128
- package/translations/sw.json +311 -275
- package/dist/12.js +0 -17
- package/dist/12.js.map +0 -1
- package/dist/1201.js +0 -1
- package/dist/1201.js.map +0 -1
- package/dist/2402.js +0 -1
- package/dist/2402.js.map +0 -1
- package/dist/3366.js +0 -1
- package/dist/3366.js.map +0 -1
- package/dist/3852.js +0 -1
- package/dist/3852.js.map +0 -1
- package/dist/4047.js.map +0 -1
- package/dist/552.js +0 -1
- package/dist/552.js.map +0 -1
- package/dist/6092.js +0 -1
- package/dist/6092.js.map +0 -1
- package/dist/6399.js +0 -1
- package/dist/6399.js.map +0 -1
- package/dist/6676.js +0 -1
- package/dist/6676.js.map +0 -1
- package/dist/6976.js +0 -1
- package/dist/6976.js.map +0 -1
- package/dist/7916.js +0 -1
- package/dist/7916.js.map +0 -1
- package/dist/9406.js +0 -1
- package/dist/9406.js.map +0 -1
- package/dist/9711.js +0 -1
- package/dist/9711.js.map +0 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
@use '@carbon/layout';
|
|
2
|
+
|
|
3
|
+
.dataTableContainer {
|
|
4
|
+
height: 100%;
|
|
5
|
+
margin: layout.$layout-01;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.tableHeaderSection {
|
|
9
|
+
display: flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.actionsCell {
|
|
14
|
+
display: flex;
|
|
15
|
+
justify-content: flex-end;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.visuallyHidden {
|
|
19
|
+
position: absolute;
|
|
20
|
+
width: 1px;
|
|
21
|
+
height: 1px;
|
|
22
|
+
padding: 0;
|
|
23
|
+
margin: -1px;
|
|
24
|
+
overflow: hidden;
|
|
25
|
+
clip: rect(0, 0, 0, 0);
|
|
26
|
+
white-space: nowrap;
|
|
27
|
+
border: 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.emptyState {
|
|
31
|
+
padding: 2rem;
|
|
32
|
+
text-align: center;
|
|
33
|
+
color: var(--cds-text-secondary);
|
|
34
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
|
+
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
|
5
|
+
import GlobalPropertyTable from './global-property-table.component';
|
|
6
|
+
|
|
7
|
+
const mockGoTo = vi.fn();
|
|
8
|
+
const mockMutate = vi.fn();
|
|
9
|
+
const mockLaunchWorkspace2 = vi.fn();
|
|
10
|
+
const mockShowModal = vi.fn(() => vi.fn());
|
|
11
|
+
|
|
12
|
+
vi.mock('react-i18next', () => ({
|
|
13
|
+
useTranslation: () => ({ t: (_key: string, fallback: string, opts?: Record<string, unknown>) => fallback }),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock('@openmrs/esm-framework', () => ({
|
|
17
|
+
useLayoutType: vi.fn(() => 'desktop'),
|
|
18
|
+
isDesktop: vi.fn(() => true),
|
|
19
|
+
launchWorkspace2: (...args: unknown[]) => mockLaunchWorkspace2(...args),
|
|
20
|
+
showModal: (...args: unknown[]) => mockShowModal(...args),
|
|
21
|
+
useDebounce: vi.fn((val: string) => val),
|
|
22
|
+
usePaginationInfo: vi.fn(() => ({ pageSizes: [10, 20, 50] })),
|
|
23
|
+
ErrorCard: ({ headerTitle }: { headerTitle: string }) => <div data-testid="error-card">{headerTitle}</div>,
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
vi.mock('@carbon/react', async (importOriginal) => {
|
|
27
|
+
const original = await importOriginal<typeof import('@carbon/react')>();
|
|
28
|
+
return {
|
|
29
|
+
...original,
|
|
30
|
+
DataTable: ({
|
|
31
|
+
children,
|
|
32
|
+
rows,
|
|
33
|
+
headers,
|
|
34
|
+
}: {
|
|
35
|
+
children: (props: any) => React.ReactNode;
|
|
36
|
+
rows: any[];
|
|
37
|
+
headers: any[];
|
|
38
|
+
}) => {
|
|
39
|
+
if (typeof children === 'function') {
|
|
40
|
+
const renderRows = rows.map((row) => ({
|
|
41
|
+
...row,
|
|
42
|
+
cells: headers.map((h) => ({
|
|
43
|
+
id: `${row.id}:${h.key}`,
|
|
44
|
+
value: row[h.key],
|
|
45
|
+
info: { header: h.key },
|
|
46
|
+
})),
|
|
47
|
+
}));
|
|
48
|
+
return children({
|
|
49
|
+
rows: renderRows,
|
|
50
|
+
headers,
|
|
51
|
+
getTableProps: () => ({}),
|
|
52
|
+
getHeaderProps: ({ header }: any) => ({ key: header.key }),
|
|
53
|
+
getRowProps: ({ row }: any) => ({ key: row.id }),
|
|
54
|
+
getCellProps: ({ cell }: any) => ({ key: cell.id }),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return children;
|
|
58
|
+
},
|
|
59
|
+
Button: ({ children, onClick, disabled, iconDescription, hasIconOnly, type }: any) => (
|
|
60
|
+
<button
|
|
61
|
+
onClick={onClick}
|
|
62
|
+
disabled={disabled}
|
|
63
|
+
aria-label={hasIconOnly ? iconDescription : undefined}
|
|
64
|
+
type={type || 'button'}>
|
|
65
|
+
{hasIconOnly ? null : children}
|
|
66
|
+
</button>
|
|
67
|
+
),
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
vi.mock('../hooks/useGlobalProperty', () => ({
|
|
72
|
+
useGlobalProperties: vi.fn(),
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
import { useGlobalProperties } from '../hooks/useGlobalProperty';
|
|
76
|
+
|
|
77
|
+
const mockProperties = [
|
|
78
|
+
{ uuid: 'uuid-1', property: 'setting.one', value: 'value1', description: 'First setting' },
|
|
79
|
+
{ uuid: 'uuid-2', property: 'setting.two', value: 'value2', description: 'Second setting' },
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
function setupMock(overrides = {}) {
|
|
83
|
+
(useGlobalProperties as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
84
|
+
isLoading: false,
|
|
85
|
+
data: mockProperties,
|
|
86
|
+
error: null,
|
|
87
|
+
goTo: mockGoTo,
|
|
88
|
+
currentPage: 1,
|
|
89
|
+
totalCount: 2,
|
|
90
|
+
mutate: mockMutate,
|
|
91
|
+
...overrides,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
describe('GlobalPropertyTable', () => {
|
|
96
|
+
beforeEach(() => {
|
|
97
|
+
vi.clearAllMocks();
|
|
98
|
+
});
|
|
99
|
+
|
|
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
|
+
it('renders the table with property and value columns', () => {
|
|
113
|
+
setupMock();
|
|
114
|
+
render(<GlobalPropertyTable />);
|
|
115
|
+
expect(screen.getByText('Property')).toBeInTheDocument();
|
|
116
|
+
expect(screen.getByText('Value')).toBeInTheDocument();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('renders all global property rows', () => {
|
|
120
|
+
setupMock();
|
|
121
|
+
render(<GlobalPropertyTable />);
|
|
122
|
+
expect(screen.getByText('setting.one')).toBeInTheDocument();
|
|
123
|
+
expect(screen.getByText('value1')).toBeInTheDocument();
|
|
124
|
+
expect(screen.getByText('setting.two')).toBeInTheDocument();
|
|
125
|
+
expect(screen.getByText('value2')).toBeInTheDocument();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('opens the add workspace when "Add new global property" button is clicked', () => {
|
|
129
|
+
setupMock();
|
|
130
|
+
render(<GlobalPropertyTable />);
|
|
131
|
+
fireEvent.click(screen.getByText('Add new global property'));
|
|
132
|
+
expect(mockLaunchWorkspace2).toHaveBeenCalledWith(
|
|
133
|
+
'global-property-workspace',
|
|
134
|
+
expect.objectContaining({
|
|
135
|
+
systemSetting: undefined,
|
|
136
|
+
mutateGlobalProperty: mockMutate,
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('opens the edit workspace with the correct property when edit is clicked', () => {
|
|
142
|
+
setupMock();
|
|
143
|
+
render(<GlobalPropertyTable />);
|
|
144
|
+
const editButtons = screen.getAllByRole('button', { name: 'Edit' });
|
|
145
|
+
fireEvent.click(editButtons[0]);
|
|
146
|
+
expect(mockLaunchWorkspace2).toHaveBeenCalledWith(
|
|
147
|
+
'global-property-workspace',
|
|
148
|
+
expect.objectContaining({
|
|
149
|
+
systemSetting: mockProperties[0],
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('opens the delete modal with the correct property when delete is clicked', () => {
|
|
155
|
+
setupMock();
|
|
156
|
+
render(<GlobalPropertyTable />);
|
|
157
|
+
const deleteButtons = screen.getAllByRole('button', { name: 'Delete' });
|
|
158
|
+
fireEvent.click(deleteButtons[0]);
|
|
159
|
+
expect(mockShowModal).toHaveBeenCalledWith(
|
|
160
|
+
'delete-global-property-modal',
|
|
161
|
+
expect.objectContaining({
|
|
162
|
+
property: 'setting.one',
|
|
163
|
+
uuid: 'uuid-1',
|
|
164
|
+
}),
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('filters results and resets to page 1 when user types in the search box', async () => {
|
|
169
|
+
setupMock({ currentPage: 2 });
|
|
170
|
+
render(<GlobalPropertyTable />);
|
|
171
|
+
const searchInput = screen.getByPlaceholderText('Search global property by name');
|
|
172
|
+
fireEvent.change(searchInput, { target: { value: 'setting' } });
|
|
173
|
+
await waitFor(() => {
|
|
174
|
+
expect(mockGoTo).toHaveBeenCalledWith(1);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('shows "no matching" message when search yields no results', () => {
|
|
179
|
+
setupMock({ data: [] });
|
|
180
|
+
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();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('renders an error card when the hook returns an error', () => {
|
|
187
|
+
setupMock({ error: new Error('Network error'), data: [] });
|
|
188
|
+
render(<GlobalPropertyTable />);
|
|
189
|
+
expect(screen.getByTestId('error-card')).toBeInTheDocument();
|
|
190
|
+
expect(screen.getByText('Global property')).toBeInTheDocument();
|
|
191
|
+
});
|
|
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
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { TFunction } from 'i18next';
|
|
3
|
+
|
|
4
|
+
export const createGlobalPropertyFormSchema = (t: TFunction) =>
|
|
5
|
+
z.object({
|
|
6
|
+
property: z.string().min(1, { message: t('gpPropertyNameRequired', 'Property name is required') }),
|
|
7
|
+
description: z.string().optional(),
|
|
8
|
+
datatypeClassname: z.string().optional(),
|
|
9
|
+
datatypeConfig: z.string().optional(),
|
|
10
|
+
preferredHandlerClassname: z.string().optional(),
|
|
11
|
+
handlerConfig: z.string().nullable().optional(),
|
|
12
|
+
value: z.string().min(1, { message: t('gpValueRequired', 'Value is required') }),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export type GlobalPropertyFormType = z.infer<ReturnType<typeof createGlobalPropertyFormSchema>>;
|
|
16
|
+
|
|
17
|
+
export const openmrsCustomDatatypes = [
|
|
18
|
+
'org.openmrs.customdatatype.datatype.BooleanDatatype',
|
|
19
|
+
'org.openmrs.customdatatype.datatype.DateDatatype',
|
|
20
|
+
'org.openmrs.customdatatype.datatype.DateTimeDatatype',
|
|
21
|
+
'org.openmrs.customdatatype.datatype.FloatDatatype',
|
|
22
|
+
'org.openmrs.customdatatype.datatype.FreeTextDatatype',
|
|
23
|
+
'org.openmrs.customdatatype.datatype.LongFreeTextDatatype',
|
|
24
|
+
'org.openmrs.customdatatype.datatype.RegexValidatedTextDatatype',
|
|
25
|
+
'org.openmrs.customdatatype.datatype.SpecifiedTextOptionsDatatype',
|
|
26
|
+
'org.openmrs.customdatatype.datatype.ConceptDatatype',
|
|
27
|
+
'org.openmrs.customdatatype.datatype.LocationDatatype',
|
|
28
|
+
'org.openmrs.customdatatype.datatype.ProgramDatatype',
|
|
29
|
+
'org.openmrs.customdatatype.datatype.ProviderDatatype',
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
export type OpenmrsCustomDatatype = (typeof openmrsCustomDatatypes)[number];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
@use '@carbon/colors';
|
|
2
|
+
@use '@carbon/layout';
|
|
3
|
+
|
|
4
|
+
.form {
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-direction: column;
|
|
7
|
+
justify-content: space-between;
|
|
8
|
+
height: 100%;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.formContainer {
|
|
12
|
+
margin: layout.$spacing-05;
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
gap: layout.$spacing-05;
|
|
16
|
+
overflow-y: auto;
|
|
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
|
+
& button {
|
|
28
|
+
max-width: 50% !important;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.buttonContainer {
|
|
33
|
+
max-width: 50%;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.inlineLoading {
|
|
37
|
+
display: flex;
|
|
38
|
+
align-items: center;
|
|
39
|
+
gap: layout.$spacing-03;
|
|
40
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import '@testing-library/jest-dom';
|
|
5
|
+
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
|
6
|
+
import GlobalPropertyWorkspace from './global-property.workspace';
|
|
7
|
+
|
|
8
|
+
const mockShowSnackbar = vi.fn();
|
|
9
|
+
const mockSaveOrUpdateGlobalProperty = vi.fn();
|
|
10
|
+
const mockCloseWorkspace = vi.fn();
|
|
11
|
+
const mockMutateGlobalProperty = vi.fn();
|
|
12
|
+
|
|
13
|
+
vi.mock('react-i18next', () => ({
|
|
14
|
+
useTranslation: () => ({ t: (_key: string, fallback: string) => fallback }),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock('@openmrs/esm-framework', () => ({
|
|
18
|
+
useLayoutType: vi.fn(() => 'desktop'),
|
|
19
|
+
showSnackbar: (...args: unknown[]) => mockShowSnackbar(...args),
|
|
20
|
+
ResponsiveWrapper: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
21
|
+
Workspace2: ({ children, title }: { children: React.ReactNode; title: string }) => (
|
|
22
|
+
<div>
|
|
23
|
+
<h2>{title}</h2>
|
|
24
|
+
{children}
|
|
25
|
+
</div>
|
|
26
|
+
),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
vi.mock('../hooks/useGlobalProperty', () => ({
|
|
30
|
+
saveOrUpdateGlobalProperty: (...args: unknown[]) => mockSaveOrUpdateGlobalProperty(...args),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
vi.mock('@hookform/resolvers/zod', () => ({
|
|
34
|
+
zodResolver: () => async (values: unknown) => ({ values, errors: {} }),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
const baseProps = {
|
|
38
|
+
closeWorkspace: mockCloseWorkspace,
|
|
39
|
+
workspaceProps: {
|
|
40
|
+
systemSetting: null,
|
|
41
|
+
mutateGlobalProperty: mockMutateGlobalProperty,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
describe('GlobalPropertyWorkspace', () => {
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
vi.clearAllMocks();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('shows "Add global property" title in add mode', () => {
|
|
51
|
+
render(<GlobalPropertyWorkspace {...baseProps} />);
|
|
52
|
+
expect(screen.getByText('Add global property')).toBeInTheDocument();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('shows "Edit global property" title when editing an existing property', () => {
|
|
56
|
+
const props = {
|
|
57
|
+
...baseProps,
|
|
58
|
+
workspaceProps: {
|
|
59
|
+
systemSetting: { uuid: 'uuid-1', property: 'setting.one', value: 'value1' },
|
|
60
|
+
mutateGlobalProperty: mockMutateGlobalProperty,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
render(<GlobalPropertyWorkspace {...props} />);
|
|
64
|
+
expect(screen.getByText('Edit global property')).toBeInTheDocument();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('pre-fills form fields when editing an existing property', () => {
|
|
68
|
+
const props = {
|
|
69
|
+
...baseProps,
|
|
70
|
+
workspaceProps: {
|
|
71
|
+
systemSetting: { uuid: 'uuid-1', property: 'my.setting', value: 'my-value' },
|
|
72
|
+
mutateGlobalProperty: mockMutateGlobalProperty,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
render(<GlobalPropertyWorkspace {...props} />);
|
|
76
|
+
expect(screen.getByDisplayValue('my.setting')).toBeInTheDocument();
|
|
77
|
+
expect(screen.getByDisplayValue('my-value')).toBeInTheDocument();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('disables the property field when editing an existing property', () => {
|
|
81
|
+
const props = {
|
|
82
|
+
...baseProps,
|
|
83
|
+
workspaceProps: {
|
|
84
|
+
systemSetting: { uuid: 'uuid-1', property: 'setting.one', value: 'value1' },
|
|
85
|
+
mutateGlobalProperty: mockMutateGlobalProperty,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
render(<GlobalPropertyWorkspace {...props} />);
|
|
89
|
+
expect(screen.getByLabelText(/Property/i)).toBeDisabled();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('enables the property field in add mode', () => {
|
|
93
|
+
render(<GlobalPropertyWorkspace {...baseProps} />);
|
|
94
|
+
expect(screen.getByLabelText(/Property/i)).not.toBeDisabled();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('renders the Cancel and Save & close buttons', () => {
|
|
98
|
+
render(<GlobalPropertyWorkspace {...baseProps} />);
|
|
99
|
+
expect(screen.getByText('Cancel')).toBeInTheDocument();
|
|
100
|
+
expect(screen.getByText('Save & close')).toBeInTheDocument();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('calls closeWorkspace when Cancel is clicked', () => {
|
|
104
|
+
render(<GlobalPropertyWorkspace {...baseProps} />);
|
|
105
|
+
fireEvent.click(screen.getByText('Cancel'));
|
|
106
|
+
expect(mockCloseWorkspace).toHaveBeenCalledTimes(1);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('shows validation error when form is submitted with empty required fields', async () => {
|
|
110
|
+
render(<GlobalPropertyWorkspace {...baseProps} />);
|
|
111
|
+
const saveButton = screen.getByText('Save & close');
|
|
112
|
+
fireEvent.click(saveButton);
|
|
113
|
+
await waitFor(() => {
|
|
114
|
+
expect(mockSaveOrUpdateGlobalProperty).not.toHaveBeenCalled();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('saves a new property with the entered values', async () => {
|
|
119
|
+
mockSaveOrUpdateGlobalProperty.mockResolvedValue(undefined);
|
|
120
|
+
render(<GlobalPropertyWorkspace {...baseProps} />);
|
|
121
|
+
|
|
122
|
+
await userEvent.type(screen.getByLabelText(/Property/i), 'new.setting');
|
|
123
|
+
await userEvent.type(screen.getByLabelText(/Value/i), 'new-value');
|
|
124
|
+
|
|
125
|
+
fireEvent.submit(screen.getByLabelText(/Property/i).closest('form')!);
|
|
126
|
+
|
|
127
|
+
await waitFor(() => {
|
|
128
|
+
expect(mockSaveOrUpdateGlobalProperty).toHaveBeenCalledWith(
|
|
129
|
+
expect.objectContaining({ property: 'new.setting', value: 'new-value' }),
|
|
130
|
+
undefined,
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('shows a success snackbar and closes workspace after saving', async () => {
|
|
136
|
+
mockSaveOrUpdateGlobalProperty.mockResolvedValue(undefined);
|
|
137
|
+
render(<GlobalPropertyWorkspace {...baseProps} />);
|
|
138
|
+
|
|
139
|
+
await userEvent.type(screen.getByLabelText(/Property/i), 'new.setting');
|
|
140
|
+
await userEvent.type(screen.getByLabelText(/Value/i), 'new-value');
|
|
141
|
+
|
|
142
|
+
fireEvent.submit(screen.getByLabelText(/Property/i).closest('form')!);
|
|
143
|
+
|
|
144
|
+
await waitFor(() => {
|
|
145
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith(expect.objectContaining({ kind: 'success' }));
|
|
146
|
+
expect(mockMutateGlobalProperty).toHaveBeenCalled();
|
|
147
|
+
expect(mockCloseWorkspace).toHaveBeenCalled();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('shows an error snackbar when saving fails', async () => {
|
|
152
|
+
mockSaveOrUpdateGlobalProperty.mockRejectedValue(new Error('Failed to save'));
|
|
153
|
+
render(<GlobalPropertyWorkspace {...baseProps} />);
|
|
154
|
+
|
|
155
|
+
await userEvent.type(screen.getByLabelText(/Property/i), 'new.setting');
|
|
156
|
+
await userEvent.type(screen.getByLabelText(/Value/i), 'new-value');
|
|
157
|
+
|
|
158
|
+
fireEvent.submit(screen.getByLabelText(/Property/i).closest('form')!);
|
|
159
|
+
|
|
160
|
+
await waitFor(() => {
|
|
161
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith(expect.objectContaining({ kind: 'error' }));
|
|
162
|
+
expect(mockCloseWorkspace).not.toHaveBeenCalled();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('renders optional fields: description, datatype config, handler config', () => {
|
|
167
|
+
render(<GlobalPropertyWorkspace {...baseProps} />);
|
|
168
|
+
expect(screen.getByLabelText(/Description/i)).toBeInTheDocument();
|
|
169
|
+
expect(screen.getByLabelText(/Datatype config/i)).toBeInTheDocument();
|
|
170
|
+
expect(screen.getByLabelText(/Handler config/i)).toBeInTheDocument();
|
|
171
|
+
});
|
|
172
|
+
});
|