@scality/data-browser-library 1.0.4 → 1.0.6

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 (60) hide show
  1. package/dist/components/DataBrowserUI.js +18 -8
  2. package/dist/components/__tests__/BucketCreate.test.js +60 -20
  3. package/dist/components/__tests__/BucketList.test.js +91 -6
  4. package/dist/components/__tests__/BucketNotificationFormPage.test.js +54 -19
  5. package/dist/components/__tests__/BucketReplicationFormPage.test.js +183 -61
  6. package/dist/components/__tests__/MetadataSearch.test.js +18 -12
  7. package/dist/components/__tests__/ObjectList.test.js +94 -2
  8. package/dist/components/buckets/BucketCreate.d.ts +1 -0
  9. package/dist/components/buckets/BucketCreate.js +57 -7
  10. package/dist/components/buckets/BucketDetails.js +0 -1
  11. package/dist/components/buckets/BucketLifecycleFormPage.js +209 -213
  12. package/dist/components/buckets/BucketList.js +25 -4
  13. package/dist/components/buckets/BucketReplicationFormPage.js +9 -3
  14. package/dist/components/buckets/DeleteBucketConfigRuleButton.js +1 -1
  15. package/dist/components/buckets/notifications/BucketNotificationList.js +1 -1
  16. package/dist/components/objects/DeleteObjectButton.d.ts +1 -0
  17. package/dist/components/objects/DeleteObjectButton.js +11 -5
  18. package/dist/components/objects/ObjectDetails/FormComponents.d.ts +9 -0
  19. package/dist/components/objects/ObjectDetails/FormComponents.js +37 -0
  20. package/dist/components/objects/ObjectDetails/ObjectMetadata.js +182 -204
  21. package/dist/components/objects/ObjectDetails/ObjectSummary.js +22 -5
  22. package/dist/components/objects/ObjectDetails/ObjectTags.js +109 -154
  23. package/dist/components/objects/ObjectDetails/__tests__/ObjectDetails.test.js +3 -3
  24. package/dist/components/objects/ObjectDetails/__tests__/ObjectMetadata.test.d.ts +1 -0
  25. package/dist/components/objects/ObjectDetails/__tests__/ObjectMetadata.test.js +230 -0
  26. package/dist/components/objects/ObjectDetails/__tests__/ObjectTags.test.d.ts +1 -0
  27. package/dist/components/objects/ObjectDetails/__tests__/ObjectTags.test.js +342 -0
  28. package/dist/components/objects/ObjectDetails/__tests__/formUtils.test.d.ts +1 -0
  29. package/dist/components/objects/ObjectDetails/__tests__/formUtils.test.js +202 -0
  30. package/dist/components/objects/ObjectDetails/index.d.ts +2 -1
  31. package/dist/components/objects/ObjectDetails/index.js +12 -16
  32. package/dist/components/objects/ObjectList.d.ts +3 -2
  33. package/dist/components/objects/ObjectList.js +204 -104
  34. package/dist/components/objects/ObjectLock/__tests__/ObjectLockSettings.test.js +78 -26
  35. package/dist/components/objects/ObjectPage.js +22 -5
  36. package/dist/components/ui/ArrayFieldActions.js +0 -2
  37. package/dist/components/ui/FilterFormSection.js +17 -36
  38. package/dist/components/ui/FormGrid.d.ts +7 -0
  39. package/dist/components/ui/FormGrid.js +37 -0
  40. package/dist/components/ui/Table.elements.js +1 -0
  41. package/dist/config/__tests__/factory.test.js +29 -1
  42. package/dist/config/factory.d.ts +2 -0
  43. package/dist/config/factory.js +3 -1
  44. package/dist/config/types.d.ts +45 -2
  45. package/dist/hooks/__tests__/usePresigningS3Client.test.d.ts +1 -0
  46. package/dist/hooks/__tests__/usePresigningS3Client.test.js +104 -0
  47. package/dist/hooks/factories/index.d.ts +1 -1
  48. package/dist/hooks/factories/index.js +2 -2
  49. package/dist/hooks/factories/useCreateS3MutationHook.d.ts +2 -1
  50. package/dist/hooks/factories/useCreateS3MutationHook.js +10 -3
  51. package/dist/hooks/factories/useCreateS3QueryHook.d.ts +1 -0
  52. package/dist/hooks/factories/useCreateS3QueryHook.js +9 -6
  53. package/dist/hooks/index.d.ts +1 -0
  54. package/dist/hooks/index.js +2 -1
  55. package/dist/hooks/presignedOperations.js +4 -4
  56. package/dist/hooks/useBucketLocations.d.ts +6 -0
  57. package/dist/hooks/useBucketLocations.js +45 -0
  58. package/dist/hooks/usePresigningS3Client.d.ts +13 -0
  59. package/dist/hooks/usePresigningS3Client.js +21 -0
  60. package/package.json +4 -3
@@ -1,12 +1,13 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { Icon, Loader, Text, spacing, useToast } from "@scality/core-ui";
3
- import { convertSizeToRem } from "@scality/core-ui/dist/components/inputv2/inputv2";
4
- import { Box, Button, Input } from "@scality/core-ui/dist/next";
2
+ import { Text, spacing, useToast } from "@scality/core-ui";
3
+ import { Box, Input } from "@scality/core-ui/dist/next";
5
4
  import { useEffect, useState } from "react";
6
5
  import { Controller, useFieldArray, useForm } from "react-hook-form";
7
6
  import { useDeleteObjectTagging, useObjectTagging, useSetObjectTagging } from "../../../hooks/index.js";
8
7
  import { useInvalidateQueries } from "../../providers/DataBrowserProvider.js";
9
8
  import { ArrayFieldActions } from "../../ui/ArrayFieldActions.js";
9
+ import { Table, TableContainer } from "../../ui/Table.elements.js";
10
+ import { FormCell, FormColumnHeaders, FormError, FormHeader, FormLoading, FormRow } from "./FormComponents.js";
10
11
  import { hasFormDataChanged } from "./formUtils.js";
11
12
  const MAX_TAGS_PER_OBJECT = 10;
12
13
  const MAX_TAG_KEY_LENGTH = 128;
@@ -100,6 +101,7 @@ const ObjectTags = ({ bucketName, objectKey, versionId })=>{
100
101
  }
101
102
  ]
102
103
  });
104
+ setIsInitialized(false);
103
105
  showToast({
104
106
  open: true,
105
107
  message: 'Tags saved successfully',
@@ -114,164 +116,117 @@ const ObjectTags = ({ bucketName, objectKey, versionId })=>{
114
116
  });
115
117
  }
116
118
  };
117
- if ('pending' === tagsStatus) return /*#__PURE__*/ jsx(Box, {
118
- display: "flex",
119
- justifyContent: "center",
120
- padding: spacing.r32,
121
- children: /*#__PURE__*/ jsx(Loader, {})
119
+ if ('pending' === tagsStatus) return /*#__PURE__*/ jsx(FormLoading, {});
120
+ if ('error' === tagsStatus) return /*#__PURE__*/ jsx(FormError, {
121
+ message: "Error loading tags"
122
122
  });
123
- if ('error' === tagsStatus) return /*#__PURE__*/ jsx(Box, {
124
- padding: spacing.r16,
125
- children: /*#__PURE__*/ jsx(Text, {
126
- color: "statusCritical",
127
- children: "Error loading tags"
128
- })
129
- });
130
- return /*#__PURE__*/ jsx(Box, {
123
+ return /*#__PURE__*/ jsx(TableContainer, {
131
124
  children: /*#__PURE__*/ jsxs("form", {
132
125
  onSubmit: handleSubmit(onSubmit),
126
+ style: {
127
+ display: 'flex',
128
+ flexDirection: 'column',
129
+ minHeight: 0,
130
+ flex: 1
131
+ },
133
132
  children: [
134
- /*#__PURE__*/ jsxs(Box, {
135
- display: "flex",
136
- justifyContent: "space-between",
137
- alignItems: "center",
138
- padding: spacing.r16,
139
- borderBottom: "1px solid",
140
- borderColor: "backgroundLevel3",
141
- children: [
142
- /*#__PURE__*/ jsx(Text, {
143
- isEmphazed: true,
144
- children: "Edit Tags"
145
- }),
146
- /*#__PURE__*/ jsx(Button, {
147
- variant: "primary",
148
- label: "Save",
149
- disabled: !isValid || isSubmitting || !hasChanges(),
150
- type: "submit",
151
- icon: /*#__PURE__*/ jsx(Icon, {
152
- name: "Save"
153
- })
154
- })
155
- ]
133
+ /*#__PURE__*/ jsx(FormHeader, {
134
+ disabled: !isValid || !hasChanges(),
135
+ isSubmitting: isSubmitting
156
136
  }),
157
- /*#__PURE__*/ jsxs(Box, {
158
- padding: spacing.r16,
159
- display: "flex",
160
- flexDirection: "column",
161
- gap: spacing.r12,
162
- children: [
163
- /*#__PURE__*/ jsxs(Box, {
164
- display: "flex",
165
- gap: spacing.r8,
166
- children: [
167
- /*#__PURE__*/ jsx(Box, {
168
- flex: "1",
169
- children: /*#__PURE__*/ jsx(Text, {
170
- color: "textSecondary",
171
- isEmphazed: true,
172
- children: "Key"
173
- })
174
- }),
175
- /*#__PURE__*/ jsx(Box, {
176
- flex: "1",
177
- children: /*#__PURE__*/ jsx(Text, {
178
- color: "textSecondary",
179
- isEmphazed: true,
180
- children: "Value"
181
- })
182
- }),
183
- /*#__PURE__*/ jsx(Box, {
184
- width: convertSizeToRem('1/3')
185
- })
186
- ]
187
- }),
188
- fields.map((field, index)=>/*#__PURE__*/ jsxs(Box, {
189
- children: [
190
- /*#__PURE__*/ jsxs(Box, {
191
- display: "flex",
192
- gap: spacing.r8,
193
- alignItems: "center",
194
- children: [
195
- /*#__PURE__*/ jsx(Controller, {
196
- control: control,
197
- name: `tags.${index}.key`,
198
- rules: {
199
- validate: (value, formValues)=>{
200
- if (1 === formValues.tags.length && !value.trim() && !formValues.tags[0].value.trim()) return true;
201
- if (!value.trim()) return 'Key cannot be empty or whitespace only';
202
- if (value.length > MAX_TAG_KEY_LENGTH) return `Key must be ${MAX_TAG_KEY_LENGTH} characters or less`;
203
- const keys = formValues.tags.map((t)=>t.key.trim());
204
- const duplicates = keys.filter((k)=>k === value.trim());
205
- return duplicates.length > 1 ? `Duplicate key "${value.trim()}"` : true;
206
- }
207
- },
208
- render: ({ field: { onChange, value } })=>/*#__PURE__*/ jsx(Input, {
209
- id: `key-${field.id}`,
210
- value: value,
211
- onChange: onChange,
212
- placeholder: "Key",
213
- "aria-label": `Tag ${index + 1} key`
137
+ /*#__PURE__*/ jsx(Table, {
138
+ children: /*#__PURE__*/ jsxs(Box, {
139
+ paddingTop: spacing.r16,
140
+ display: "flex",
141
+ flexDirection: "column",
142
+ gap: spacing.r12,
143
+ children: [
144
+ /*#__PURE__*/ jsx(FormColumnHeaders, {}),
145
+ fields.map((field, index)=>/*#__PURE__*/ jsxs(Box, {
146
+ children: [
147
+ /*#__PURE__*/ jsxs(FormRow, {
148
+ children: [
149
+ /*#__PURE__*/ jsx(FormCell, {
150
+ children: /*#__PURE__*/ jsx(Controller, {
151
+ control: control,
152
+ name: `tags.${index}.key`,
153
+ rules: {
154
+ validate: (value, formValues)=>{
155
+ if (!value.trim() && !formValues.tags[index].value.trim()) return true;
156
+ if (!value.trim()) return 'Key cannot be empty or whitespace only';
157
+ if (value.length > MAX_TAG_KEY_LENGTH) return `Key must be ${MAX_TAG_KEY_LENGTH} characters or less`;
158
+ const keys = formValues.tags.map((t)=>t.key.trim()).filter(Boolean);
159
+ const duplicates = keys.filter((k)=>k === value.trim());
160
+ return duplicates.length > 1 ? `Duplicate key "${value.trim()}"` : true;
161
+ }
162
+ },
163
+ render: ({ field: { onChange, value } })=>/*#__PURE__*/ jsx(Input, {
164
+ id: `key-${field.id}`,
165
+ value: value,
166
+ onChange: onChange,
167
+ placeholder: "Key",
168
+ "aria-label": `Tag ${index + 1} key`
169
+ })
214
170
  })
215
- }),
216
- /*#__PURE__*/ jsx(Text, {
217
- color: "textSecondary",
218
- children: ":"
219
- }),
220
- /*#__PURE__*/ jsx(Controller, {
221
- control: control,
222
- name: `tags.${index}.value`,
223
- rules: {
224
- validate: (value, formValues)=>{
225
- if (1 === formValues.tags.length && !value.trim() && !formValues.tags[0].key.trim()) return true;
226
- if (!value.trim()) return 'Value cannot be empty or whitespace only';
227
- if (value.length > MAX_TAG_VALUE_LENGTH) return `Value must be ${MAX_TAG_VALUE_LENGTH} characters or less`;
228
- return true;
229
- }
230
- },
231
- render: ({ field: { onChange, value } })=>/*#__PURE__*/ jsx(Input, {
232
- id: `value-${field.id}`,
233
- value: value,
234
- onChange: onChange,
235
- placeholder: "Value",
236
- "aria-label": `Tag ${index + 1} value`
171
+ }),
172
+ /*#__PURE__*/ jsx(FormCell, {
173
+ children: /*#__PURE__*/ jsx(Controller, {
174
+ control: control,
175
+ name: `tags.${index}.value`,
176
+ rules: {
177
+ validate: (value, formValues)=>{
178
+ if (!value.trim() && !formValues.tags[index].key.trim()) return true;
179
+ if (!value.trim()) return 'Value cannot be empty or whitespace only';
180
+ if (value.length > MAX_TAG_VALUE_LENGTH) return `Value must be ${MAX_TAG_VALUE_LENGTH} characters or less`;
181
+ return true;
182
+ }
183
+ },
184
+ render: ({ field: { onChange, value } })=>/*#__PURE__*/ jsx(Input, {
185
+ id: `value-${field.id}`,
186
+ value: value,
187
+ onChange: onChange,
188
+ placeholder: "Value",
189
+ "aria-label": `Tag ${index + 1} value`
190
+ })
237
191
  })
238
- }),
239
- /*#__PURE__*/ jsx(ArrayFieldActions, {
240
- showAdd: index === fields.length - 1,
241
- onRemove: ()=>{
242
- remove(index);
243
- if (1 === fields.length) append({
244
- key: '',
245
- value: ''
246
- });
247
- },
248
- onAdd: ()=>append({
249
- key: '',
250
- value: ''
251
- }),
252
- canRemove: !!watch(`tags.${index}.key`) && !!watch(`tags.${index}.value`),
253
- canAdd: fields.length < MAX_TAGS_PER_OBJECT && !!watch(`tags.${index}.key`) && !!watch(`tags.${index}.value`),
254
- removeLabel: "Remove tag",
255
- addLabel: "Add tag"
256
- })
257
- ]
258
- }),
259
- (errors.tags?.[index]?.key || errors.tags?.[index]?.value) && /*#__PURE__*/ jsxs(Box, {
260
- paddingTop: spacing.r4,
261
- children: [
262
- errors.tags?.[index]?.key && /*#__PURE__*/ jsx(Text, {
263
- color: "statusCritical",
264
- children: errors.tags[index]?.key?.message
265
- }),
266
- errors.tags?.[index]?.value && /*#__PURE__*/ jsx(Text, {
267
- color: "statusCritical",
268
- children: errors.tags[index]?.value?.message
269
- })
270
- ]
271
- })
272
- ]
273
- }, field.id))
274
- ]
192
+ }),
193
+ /*#__PURE__*/ jsx(ArrayFieldActions, {
194
+ showAdd: index === fields.length - 1,
195
+ onRemove: ()=>{
196
+ remove(index);
197
+ if (1 === fields.length) append({
198
+ key: '',
199
+ value: ''
200
+ });
201
+ },
202
+ onAdd: ()=>append({
203
+ key: '',
204
+ value: ''
205
+ }),
206
+ canRemove: !!watch(`tags.${index}.key`) || !!watch(`tags.${index}.value`),
207
+ canAdd: fields.length < MAX_TAGS_PER_OBJECT && !!watch(`tags.${index}.key`) && !!watch(`tags.${index}.value`),
208
+ removeLabel: "Remove tag",
209
+ addLabel: "Add tag"
210
+ })
211
+ ]
212
+ }),
213
+ (errors.tags?.[index]?.key || errors.tags?.[index]?.value) && /*#__PURE__*/ jsxs(Box, {
214
+ paddingTop: spacing.r4,
215
+ children: [
216
+ errors.tags?.[index]?.key && /*#__PURE__*/ jsx(Text, {
217
+ color: "statusCritical",
218
+ children: errors.tags[index]?.key?.message
219
+ }),
220
+ errors.tags?.[index]?.value && /*#__PURE__*/ jsx(Text, {
221
+ color: "statusCritical",
222
+ children: errors.tags[index]?.value?.message
223
+ })
224
+ ]
225
+ })
226
+ ]
227
+ }, field.id))
228
+ ]
229
+ })
275
230
  })
276
231
  ]
277
232
  })
@@ -5,7 +5,7 @@ import { cleanup, render, screen } from "@testing-library/react";
5
5
  import { MemoryRouter, Route, Routes } from "react-router";
6
6
  import { DataBrowserUICustomizationProvider } from "../../../../contexts/DataBrowserUICustomizationContext.js";
7
7
  var __webpack_modules__ = {
8
- "../index" (module) {
8
+ ".." (module) {
9
9
  module.exports = __rspack_external__index_js_95fdb65a;
10
10
  }
11
11
  };
@@ -19,7 +19,7 @@ function __webpack_require__(moduleId) {
19
19
  __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
20
20
  return module.exports;
21
21
  }
22
- var external_index_js_ = __webpack_require__("../index");
22
+ var external_index_js_ = __webpack_require__("..");
23
23
  jest.mock('../ObjectSummary', ()=>({
24
24
  ObjectSummary: ()=>/*#__PURE__*/ jsx("div", {
25
25
  "data-testid": "object-summary",
@@ -398,7 +398,7 @@ describe('ObjectDetails', ()=>{
398
398
  expect(screen.getByTestId('object-summary')).toBeInTheDocument();
399
399
  });
400
400
  it('should throw error when useObjectDetailsContext is used outside provider', ()=>{
401
- const { useObjectDetailsContext } = __webpack_require__("../index");
401
+ const { useObjectDetailsContext } = __webpack_require__("..");
402
402
  const TestComponent = ()=>{
403
403
  useObjectDetailsContext();
404
404
  return null;
@@ -0,0 +1,230 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { useToast } from "@scality/core-ui";
3
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4
+ import { render, screen, waitFor } from "@testing-library/react";
5
+ import user_event from "@testing-library/user-event";
6
+ import { DataBrowserUICustomizationProvider } from "../../../../contexts/DataBrowserUICustomizationContext.js";
7
+ import { useCopyObject, useObjectMetadata } from "../../../../hooks/index.js";
8
+ import { ObjectMetadata } from "../ObjectMetadata.js";
9
+ jest.mock('../../../../hooks');
10
+ jest.mock('@scality/core-ui', ()=>{
11
+ const actual = jest.requireActual('@scality/core-ui');
12
+ return {
13
+ ...actual,
14
+ useToast: jest.fn()
15
+ };
16
+ });
17
+ jest.mock('../../../providers/DataBrowserProvider', ()=>({
18
+ ...jest.requireActual('../../../providers/DataBrowserProvider'),
19
+ useInvalidateQueries: jest.fn(()=>jest.fn().mockResolvedValue(void 0))
20
+ }));
21
+ const mockUseObjectMetadata = jest.mocked(useObjectMetadata);
22
+ const mockUseCopyObject = jest.mocked(useCopyObject);
23
+ const mockUseToast = jest.mocked(useToast);
24
+ const mockShowToast = jest.fn();
25
+ const setupMockDefaults = (metadataOverrides = {})=>{
26
+ mockUseObjectMetadata.mockReturnValue({
27
+ data: {
28
+ ContentType: 'text/plain',
29
+ ...metadataOverrides
30
+ },
31
+ status: 'success'
32
+ });
33
+ mockUseCopyObject.mockReturnValue({
34
+ mutateAsync: jest.fn().mockResolvedValue({}),
35
+ reset: jest.fn()
36
+ });
37
+ mockUseToast.mockReturnValue({
38
+ showToast: mockShowToast
39
+ });
40
+ };
41
+ const renderWithProviders = (ui)=>{
42
+ const queryClient = new QueryClient({
43
+ defaultOptions: {
44
+ queries: {
45
+ retry: false
46
+ },
47
+ mutations: {
48
+ retry: false
49
+ }
50
+ }
51
+ });
52
+ return render(/*#__PURE__*/ jsx(QueryClientProvider, {
53
+ client: queryClient,
54
+ children: /*#__PURE__*/ jsx(DataBrowserUICustomizationProvider, {
55
+ config: {},
56
+ children: ui
57
+ })
58
+ }));
59
+ };
60
+ const defaultProps = {
61
+ bucketName: 'test-bucket',
62
+ objectKey: 'test-object.txt'
63
+ };
64
+ const getSaveButton = ()=>screen.getByRole('button', {
65
+ name: /save/i
66
+ });
67
+ describe('ObjectMetadata', ()=>{
68
+ beforeEach(()=>{
69
+ jest.clearAllMocks();
70
+ });
71
+ describe('Loading and Error States', ()=>{
72
+ it('should show loader when metadata is loading', ()=>{
73
+ mockUseObjectMetadata.mockReturnValue({
74
+ data: void 0,
75
+ status: 'pending'
76
+ });
77
+ mockUseCopyObject.mockReturnValue({
78
+ reset: jest.fn()
79
+ });
80
+ mockUseToast.mockReturnValue({
81
+ showToast: mockShowToast
82
+ });
83
+ const { container } = renderWithProviders(/*#__PURE__*/ jsx(ObjectMetadata, {
84
+ ...defaultProps
85
+ }));
86
+ expect(container.querySelectorAll('svg').length).toBeGreaterThan(0);
87
+ });
88
+ it('should show error message when metadata fails to load', ()=>{
89
+ mockUseObjectMetadata.mockReturnValue({
90
+ data: void 0,
91
+ status: 'error'
92
+ });
93
+ mockUseCopyObject.mockReturnValue({
94
+ reset: jest.fn()
95
+ });
96
+ mockUseToast.mockReturnValue({
97
+ showToast: mockShowToast
98
+ });
99
+ renderWithProviders(/*#__PURE__*/ jsx(ObjectMetadata, {
100
+ ...defaultProps
101
+ }));
102
+ expect(screen.getByText('Error loading metadata')).toBeInTheDocument();
103
+ });
104
+ });
105
+ describe('Empty Row Validation', ()=>{
106
+ it('should keep Save disabled when no metadata exists and nothing is changed', async ()=>{
107
+ setupMockDefaults({
108
+ ContentType: void 0,
109
+ Metadata: {}
110
+ });
111
+ renderWithProviders(/*#__PURE__*/ jsx(ObjectMetadata, {
112
+ ...defaultProps
113
+ }));
114
+ await waitFor(()=>{
115
+ expect(screen.getByText('Key')).toBeInTheDocument();
116
+ });
117
+ expect(getSaveButton()).toBeDisabled();
118
+ });
119
+ it('should allow empty rows alongside filled rows without blocking Save', async ()=>{
120
+ setupMockDefaults({
121
+ ContentType: 'text/plain',
122
+ Metadata: {
123
+ customKey: 'customValue'
124
+ }
125
+ });
126
+ renderWithProviders(/*#__PURE__*/ jsx(ObjectMetadata, {
127
+ ...defaultProps
128
+ }));
129
+ await waitFor(()=>{
130
+ expect(screen.getByText('Key')).toBeInTheDocument();
131
+ });
132
+ await waitFor(()=>{
133
+ expect(screen.getByDisplayValue('text/plain')).toBeInTheDocument();
134
+ });
135
+ const user = user_event.setup();
136
+ const valueInput = screen.getByDisplayValue('text/plain');
137
+ await user.clear(valueInput);
138
+ await user.type(valueInput, 'application/json');
139
+ await waitFor(()=>{
140
+ expect(getSaveButton()).toBeEnabled();
141
+ });
142
+ });
143
+ });
144
+ describe('Remove Button (canRemove)', ()=>{
145
+ it('should disable remove for a single empty placeholder row', async ()=>{
146
+ setupMockDefaults({
147
+ ContentType: void 0,
148
+ Metadata: {}
149
+ });
150
+ renderWithProviders(/*#__PURE__*/ jsx(ObjectMetadata, {
151
+ ...defaultProps
152
+ }));
153
+ await waitFor(()=>{
154
+ expect(screen.getByText('Key')).toBeInTheDocument();
155
+ });
156
+ expect(screen.getByRole('button', {
157
+ name: /remove metadata/i
158
+ })).toBeDisabled();
159
+ });
160
+ it('should enable remove for any row when there are multiple rows', async ()=>{
161
+ setupMockDefaults({
162
+ CacheControl: 'max-age=300',
163
+ ContentType: 'text/plain'
164
+ });
165
+ renderWithProviders(/*#__PURE__*/ jsx(ObjectMetadata, {
166
+ ...defaultProps
167
+ }));
168
+ await waitFor(()=>{
169
+ expect(screen.getByDisplayValue('max-age=300')).toBeInTheDocument();
170
+ });
171
+ const removeButtons = screen.getAllByRole('button', {
172
+ name: /remove metadata/i
173
+ });
174
+ expect(removeButtons[0]).toBeEnabled();
175
+ expect(removeButtons[1]).toBeEnabled();
176
+ });
177
+ it('should enable Save after removing one of two metadata entries', async ()=>{
178
+ setupMockDefaults({
179
+ CacheControl: 'max-age=300',
180
+ ContentType: 'text/plain'
181
+ });
182
+ renderWithProviders(/*#__PURE__*/ jsx(ObjectMetadata, {
183
+ ...defaultProps
184
+ }));
185
+ const user = user_event.setup();
186
+ await waitFor(()=>{
187
+ expect(screen.getByDisplayValue('max-age=300')).toBeInTheDocument();
188
+ });
189
+ const removeButtons = screen.getAllByRole('button', {
190
+ name: /remove metadata/i
191
+ });
192
+ await user.click(removeButtons[0]);
193
+ await waitFor(()=>{
194
+ expect(getSaveButton()).toBeEnabled();
195
+ });
196
+ });
197
+ });
198
+ describe('Save Button Change Detection', ()=>{
199
+ it('should disable Save when no changes are made to existing metadata', async ()=>{
200
+ setupMockDefaults({
201
+ ContentType: 'text/plain'
202
+ });
203
+ renderWithProviders(/*#__PURE__*/ jsx(ObjectMetadata, {
204
+ ...defaultProps
205
+ }));
206
+ await waitFor(()=>{
207
+ expect(screen.getByDisplayValue('text/plain')).toBeInTheDocument();
208
+ });
209
+ expect(getSaveButton()).toBeDisabled();
210
+ });
211
+ it('should enable Save when a metadata value is modified', async ()=>{
212
+ setupMockDefaults({
213
+ ContentType: 'text/plain'
214
+ });
215
+ renderWithProviders(/*#__PURE__*/ jsx(ObjectMetadata, {
216
+ ...defaultProps
217
+ }));
218
+ const user = user_event.setup();
219
+ await waitFor(()=>{
220
+ expect(screen.getByDisplayValue('text/plain')).toBeInTheDocument();
221
+ });
222
+ const valueInput = screen.getByDisplayValue('text/plain');
223
+ await user.clear(valueInput);
224
+ await user.type(valueInput, 'application/json');
225
+ await waitFor(()=>{
226
+ expect(getSaveButton()).toBeEnabled();
227
+ });
228
+ });
229
+ });
230
+ });