passbolt-styleguide 5.14.0 → 5.14.2

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 (61) hide show
  1. package/RELEASE_NOTES.md +3 -2
  2. package/build/css/help.min.css +2 -2
  3. package/build/css/public.min.css +2 -2
  4. package/build/css/themes/default/api_authentication.min.css +2 -2
  5. package/build/css/themes/default/api_cloud.min.css +2 -2
  6. package/build/css/themes/default/api_main.min.css +2 -2
  7. package/build/css/themes/default/api_reports.min.css +2 -2
  8. package/build/css/themes/default/api_webinstaller.min.css +2 -2
  9. package/build/css/themes/default/ext_app.min.css +2 -2
  10. package/build/css/themes/default/ext_authentication.min.css +2 -2
  11. package/build/css/themes/default/ext_external.min.css +2 -2
  12. package/build/css/themes/default/ext_in_form_cta.min.css +2 -2
  13. package/build/css/themes/default/ext_in_form_menu.min.css +2 -2
  14. package/build/css/themes/default/ext_quickaccess.min.css +2 -2
  15. package/build/css/themes/midgar/api_authentication.min.css +2 -2
  16. package/build/css/themes/midgar/api_main.min.css +2 -2
  17. package/build/css/themes/midgar/api_reports.min.css +2 -2
  18. package/build/css/themes/midgar/ext_app.min.css +2 -2
  19. package/build/css/themes/midgar/ext_authentication.min.css +2 -2
  20. package/build/css/themes/midgar/ext_in_form_cta.min.css +2 -2
  21. package/build/css/themes/midgar/ext_in_form_menu.min.css +2 -2
  22. package/build/css/themes/midgar/ext_quickaccess.min.css +2 -2
  23. package/build/css/themes/solarized_dark/api_authentication.min.css +2 -2
  24. package/build/css/themes/solarized_dark/api_main.min.css +2 -2
  25. package/build/css/themes/solarized_dark/api_reports.min.css +2 -2
  26. package/build/css/themes/solarized_dark/ext_app.min.css +2 -2
  27. package/build/css/themes/solarized_dark/ext_authentication.min.css +2 -2
  28. package/build/css/themes/solarized_dark/ext_in_form_cta.min.css +2 -2
  29. package/build/css/themes/solarized_dark/ext_in_form_menu.min.css +2 -2
  30. package/build/css/themes/solarized_dark/ext_quickaccess.min.css +2 -2
  31. package/build/css/themes/solarized_light/api_authentication.min.css +2 -2
  32. package/build/css/themes/solarized_light/api_main.min.css +2 -2
  33. package/build/css/themes/solarized_light/api_reports.min.css +2 -2
  34. package/build/css/themes/solarized_light/ext_app.min.css +2 -2
  35. package/build/css/themes/solarized_light/ext_authentication.min.css +2 -2
  36. package/build/css/themes/solarized_light/ext_in_form_cta.min.css +2 -2
  37. package/build/css/themes/solarized_light/ext_in_form_menu.min.css +2 -2
  38. package/build/css/themes/solarized_light/ext_quickaccess.min.css +2 -2
  39. package/build/js/dist/api-app.js +1 -1
  40. package/build/locales/pl-PL/common.json +2 -2
  41. package/package.json +1 -1
  42. package/src/locales/pl-PL/common.json +2 -2
  43. package/src/react-extension/components/Administration/DowngradeToCe/DowngradeToCe.js +1 -1
  44. package/src/react-extension/components/Resource/CreateResource/CreateResource.js +11 -1
  45. package/src/react-extension/components/Resource/CreateResource/CreateResource.test.data.js +2 -0
  46. package/src/react-extension/components/Resource/CreateResource/CreateResource.test.js +2 -2
  47. package/src/react-extension/components/Resource/EditResource/EditResource.js +17 -41
  48. package/src/react-extension/components/Resource/EditResource/EditResource.test.data.js +2 -0
  49. package/src/react-extension/components/Resource/EditResource/EditResource.test.js +56 -170
  50. package/src/react-extension/components/Resource/HandlePermissionWorkflow/flows/ResourceCreationFlow.js +31 -7
  51. package/src/react-extension/components/Resource/HandlePermissionWorkflow/flows/ResourceCreationFlow.test.js +4 -3
  52. package/src/react-extension/components/Resource/HandlePermissionWorkflow/flows/ResourceEditFlow.js +40 -17
  53. package/src/react-extension/components/Resource/HandlePermissionWorkflow/flows/ResourceEditFlow.test.js +14 -8
  54. package/src/react-extension/components/Share/GroupPermissionItem.js +6 -3
  55. package/src/react-extension/components/Share/ShareDialog.js +14 -1
  56. package/src/react-extension/components/Share/UserPermissionItem.js +6 -3
  57. package/src/react-extension/components/Share/Utility/ShareChanges.js +12 -0
  58. package/src/shared/context/ResourceTypesLocalStorageContext/ResourceTypesLocalStorageContext.js +2 -2
  59. package/src/shared/context/ResourceTypesLocalStorageContext/ResourceTypesLocalStorageContext.test.js +3 -3
  60. package/src/shared/services/permission/permissionChangesService.js +1 -5
  61. package/src/shared/services/permission/permissionChangesService.test.js +18 -42
@@ -61,10 +61,12 @@ export class ResourceCreationFlow extends AbstractPermissionFlow {
61
61
  this.formSubmitted = false;
62
62
  this.shareConfirmed = false;
63
63
  this.pendingResourceFormEntity = null;
64
+ this.createResourceDialogFocusBackListener = null;
64
65
  this.handleCreateResourceSubmit = this.handleCreateResourceSubmit.bind(this);
65
66
  this.handleCreateResourceClose = this.handleCreateResourceClose.bind(this);
66
67
  this.handleShareDialogConfirm = this.handleShareDialogConfirm.bind(this);
67
68
  this.handleShareDialogClose = this.handleShareDialogClose.bind(this);
69
+ this.setFocusBackListener = this.setFocusBackListener.bind(this);
68
70
  }
69
71
 
70
72
  /**
@@ -74,6 +76,7 @@ export class ResourceCreationFlow extends AbstractPermissionFlow {
74
76
  get defaultState() {
75
77
  return {
76
78
  status: RESOURCE_CREATION_FLOW_STATUS.INITIALIZING,
79
+ createResourceDialogId: null,
77
80
  snapshot: null,
78
81
  };
79
82
  }
@@ -100,13 +103,17 @@ export class ResourceCreationFlow extends AbstractPermissionFlow {
100
103
  * Open the CreateResource dialog and transition to the CREATE_RESOURCE_OPEN state.
101
104
  */
102
105
  openCreateResourceDialog() {
103
- this.props.dialogContext.open(CreateResource, {
106
+ const createResourceDialogId = this.props.dialogContext.open(CreateResource, {
104
107
  resourceType: this.props.resourceType,
105
108
  folderParentId: this.props.folderParentId,
106
109
  onSubmit: this.handleCreateResourceSubmit,
107
110
  onClose: this.handleCreateResourceClose,
111
+ setFocusBackListener: this.setFocusBackListener,
112
+ });
113
+ this.setState({
114
+ status: RESOURCE_CREATION_FLOW_STATUS.CREATE_RESOURCE_OPEN,
115
+ createResourceDialogId: createResourceDialogId,
108
116
  });
109
- this.setState({ status: RESOURCE_CREATION_FLOW_STATUS.CREATE_RESOURCE_OPEN });
110
117
  }
111
118
 
112
119
  /**
@@ -122,6 +129,7 @@ export class ResourceCreationFlow extends AbstractPermissionFlow {
122
129
  permissions: this.state.snapshot.permissions,
123
130
  },
124
131
  ],
132
+ initialChanges: this.state.snapshot.permissions.items,
125
133
  initialGroups: this.state.snapshot.groups,
126
134
  initialUsers: this.state.snapshot.users,
127
135
  onConfirm: this.handleShareDialogConfirm,
@@ -150,6 +158,8 @@ export class ResourceCreationFlow extends AbstractPermissionFlow {
150
158
  this.props.t("The resource has been added successfully"),
151
159
  `/app/passwords/view/${created.id}`,
152
160
  );
161
+ this.closeCreateResourceDialog();
162
+ this.terminate();
153
163
  } catch (error) {
154
164
  this.handleError(error);
155
165
  }
@@ -161,9 +171,6 @@ export class ResourceCreationFlow extends AbstractPermissionFlow {
161
171
  * dialog or API call); ignore it. Otherwise it's a cancellation: terminate.
162
172
  */
163
173
  handleCreateResourceClose() {
164
- if (this.formSubmitted) {
165
- return;
166
- }
167
174
  this.terminate();
168
175
  }
169
176
 
@@ -196,13 +203,14 @@ export class ResourceCreationFlow extends AbstractPermissionFlow {
196
203
  // exist yet. The extension stamps real ids server-side as part of its create-then-share
197
204
  // orchestration, so passing null here is fine.
198
205
  null,
199
- this.props.context.loggedInUser.id,
200
206
  );
201
207
  const created = await this.createResource(this.pendingResourceFormEntity, finalChanges);
202
208
  await this.finalizeSuccess(
203
209
  this.props.t("The resource has been added successfully"),
204
210
  `/app/passwords/view/${created.id}`,
205
211
  );
212
+ this.closeCreateResourceDialog();
213
+ this.terminate();
206
214
  } catch (error) {
207
215
  this.handleError(error);
208
216
  }
@@ -217,7 +225,23 @@ export class ResourceCreationFlow extends AbstractPermissionFlow {
217
225
  if (this.shareConfirmed) {
218
226
  return;
219
227
  }
220
- this.terminate();
228
+ this.createResourceDialogFocusBackListener?.();
229
+ }
230
+
231
+ /**
232
+ * Closes the currently opened create resource dialog.
233
+ */
234
+ closeCreateResourceDialog() {
235
+ this.props.dialogContext.close(this.state.createResourceDialogId);
236
+ }
237
+
238
+ /**
239
+ * Sets the callback for when the create resource dialog needs to get the focus back.
240
+ * It's necessary for when the operator cancels the "share" process.
241
+ * @param {function} listener
242
+ */
243
+ setFocusBackListener(listener) {
244
+ this.createResourceDialogFocusBackListener = listener;
221
245
  }
222
246
 
223
247
  /**
@@ -165,9 +165,10 @@ describe("ResourceCreationFlow", () => {
165
165
  // the operator's autocomplete addition; the operator's own row is excluded.
166
166
  const createCall = props.context.port.request.mock.calls.find(([event]) => event === "passbolt.resources.create");
167
167
  const createPermissionsArg = createCall[3];
168
- expect(createPermissionsArg).toHaveLength(2);
168
+ expect(createPermissionsArg).toHaveLength(3);
169
169
  expect(createPermissionsArg).toEqual(
170
170
  expect.arrayContaining([
171
+ expect.objectContaining({ is_new: true, aro_foreign_key: operatorId, type: 15 }),
171
172
  expect.objectContaining({ is_new: true, aro_foreign_key: readerId, type: 1 }),
172
173
  expect.objectContaining({ is_new: true, aro_foreign_key: newAroId, type: 1 }),
173
174
  ]),
@@ -342,7 +343,7 @@ describe("ResourceCreationFlow", () => {
342
343
  );
343
344
  });
344
345
 
345
- it("As LU cancelling ShareDialog should terminate the workflow without creating the resource", async () => {
346
+ it("As LU cancelling ShareDialog should go back to the resource creation dialog without creating the resource", async () => {
346
347
  expect.assertions(2);
347
348
  const props = defaultProps();
348
349
  const operatorId = props.context.loggedInUser.id;
@@ -381,7 +382,7 @@ describe("ResourceCreationFlow", () => {
381
382
  const shareProps = dialogPropsFor(props.dialogContext, ShareDialog);
382
383
  shareProps.onClose();
383
384
 
384
- expect(props.onStop).toHaveBeenCalledTimes(1);
385
+ expect(props.onStop).not.toHaveBeenCalled();
385
386
  expect(props.context.port.request).not.toHaveBeenCalledWith(
386
387
  "passbolt.resources.create",
387
388
  expect.anything(),
@@ -20,8 +20,8 @@ import { withActionFeedback } from "../../../../contexts/ActionFeedbackContext";
20
20
  import EditResource from "../../EditResource/EditResource";
21
21
  import ShareDialog from "../../../Share/ShareDialog";
22
22
  import PermissionEntity from "../../../../../shared/models/entity/permission/permissionEntity";
23
- import { RESOURCE_TYPE_PASSWORD_STRING_SLUG } from "../../../../../shared/models/entity/resourceType/resourceTypeSchemasDefinition";
24
23
  import { AbstractPermissionFlow, PERMISSION_FLOW_STATUS } from "./AbstractPermissionFlow";
24
+ import { withResourceWorkspace } from "../../../../contexts/ResourceWorkspaceContext";
25
25
 
26
26
  /**
27
27
  * Status values driving the resource-edition flow state machine.
@@ -61,10 +61,13 @@ export class ResourceEditFlow extends AbstractPermissionFlow {
61
61
  this.formSubmitted = false;
62
62
  this.shareConfirmed = false;
63
63
  this.pendingResourceFormEntity = null;
64
+ this.pendingResourceSecret = null;
65
+ this.editResourceDialogFocusBackListener = null;
64
66
  this.handleEditResourceSubmit = this.handleEditResourceSubmit.bind(this);
65
67
  this.handleEditResourceClose = this.handleEditResourceClose.bind(this);
66
68
  this.handleShareDialogConfirm = this.handleShareDialogConfirm.bind(this);
67
69
  this.handleShareDialogClose = this.handleShareDialogClose.bind(this);
70
+ this.setFocusBackListener = this.setFocusBackListener.bind(this);
68
71
  }
69
72
 
70
73
  /**
@@ -74,6 +77,7 @@ export class ResourceEditFlow extends AbstractPermissionFlow {
74
77
  get defaultState() {
75
78
  return {
76
79
  status: RESOURCE_EDIT_FLOW_STATUS.INITIALIZING,
80
+ editResourceDialogId: null,
77
81
  snapshot: null,
78
82
  };
79
83
  }
@@ -108,12 +112,13 @@ export class ResourceEditFlow extends AbstractPermissionFlow {
108
112
  * Open the EditResource dialog and transition to the EDIT_RESOURCE_OPEN state.
109
113
  */
110
114
  openEditResourceDialog() {
111
- this.props.dialogContext.open(EditResource, {
115
+ const editResourceDialogId = this.props.dialogContext.open(EditResource, {
112
116
  resource: this.props.resource,
113
117
  onSubmit: this.handleEditResourceSubmit,
114
118
  onClose: this.handleEditResourceClose,
119
+ setFocusBackListener: this.setFocusBackListener,
115
120
  });
116
- this.setState({ status: RESOURCE_EDIT_FLOW_STATUS.EDIT_RESOURCE_OPEN });
121
+ this.setState({ status: RESOURCE_EDIT_FLOW_STATUS.EDIT_RESOURCE_OPEN, editResourceDialogId: editResourceDialogId });
117
122
  }
118
123
 
119
124
  /**
@@ -146,19 +151,22 @@ export class ResourceEditFlow extends AbstractPermissionFlow {
146
151
  * @param {ResourceFormEntity} resourceFormEntity The validated form entity.
147
152
  * @returns {Promise<void>}
148
153
  */
149
- async handleEditResourceSubmit(resourceFormEntity) {
154
+ async handleEditResourceSubmit(resourceFormEntity, secretDto) {
150
155
  this.formSubmitted = true;
151
156
  this.pendingResourceFormEntity = resourceFormEntity;
157
+ this.pendingResourceSecret = secretDto;
152
158
  try {
153
159
  if (this.isShared(this.state.snapshot)) {
154
160
  this.openShareDialog();
155
161
  return;
156
162
  }
157
- await this.updateResource(resourceFormEntity);
163
+ await this.updateResource(resourceFormEntity, secretDto);
158
164
  await this.finalizeSuccess(
159
165
  this.props.t("The resource has been updated successfully"),
160
166
  `/app/passwords/view/${this.props.resource.id}`,
161
167
  );
168
+ this.closeEditResourceDialog();
169
+ this.terminate();
162
170
  } catch (error) {
163
171
  this.handleError(error);
164
172
  }
@@ -170,9 +178,6 @@ export class ResourceEditFlow extends AbstractPermissionFlow {
170
178
  * or API call); ignore it. Otherwise it's a cancellation: terminate.
171
179
  */
172
180
  handleEditResourceClose() {
173
- if (this.formSubmitted) {
174
- return;
175
- }
176
181
  this.terminate();
177
182
  }
178
183
 
@@ -200,11 +205,14 @@ export class ResourceEditFlow extends AbstractPermissionFlow {
200
205
  ),
201
206
  );
202
207
  }
203
- await this.updateResource(this.pendingResourceFormEntity, permissionChanges);
208
+ await this.updateResource(this.pendingResourceFormEntity, this.pendingResourceSecret, permissionChanges);
204
209
  await this.finalizeSuccess(
205
210
  this.props.t("The resource has been updated successfully"),
206
211
  `/app/passwords/view/${this.props.resource.id}`,
207
212
  );
213
+ this.closeEditResourceDialog();
214
+ this.terminate();
215
+ this.props.resourceWorkspaceContext.onResourceEdited();
208
216
  } catch (error) {
209
217
  this.handleError(error);
210
218
  }
@@ -219,7 +227,24 @@ export class ResourceEditFlow extends AbstractPermissionFlow {
219
227
  if (this.shareConfirmed) {
220
228
  return;
221
229
  }
222
- this.terminate();
230
+ this.editResourceDialogFocusBackListener?.();
231
+ }
232
+
233
+ /**
234
+ * Closes the currently opened edit resource dialog.
235
+ */
236
+ closeEditResourceDialog() {
237
+ this.pendingResourceSecret = null;
238
+ this.props.dialogContext.close(this.state.editResourceDialogId);
239
+ }
240
+
241
+ /**
242
+ * Sets the callback for when the edit resource dialog needs to get the focus back.
243
+ * It's necessary for when the operator cancels the "share" process.
244
+ * @param {function} listener
245
+ */
246
+ setFocusBackListener(listener) {
247
+ this.editResourceDialogFocusBackListener = listener;
223
248
  }
224
249
 
225
250
  /**
@@ -230,13 +255,8 @@ export class ResourceEditFlow extends AbstractPermissionFlow {
230
255
  * @param {Array<object>} [permissionChanges] Operator-confirmed permission changes.
231
256
  * @returns {Promise<Object>} The updated resource DTO.
232
257
  */
233
- updateResource(resourceFormEntity, permissionChanges) {
258
+ updateResource(resourceFormEntity, secretDto, permissionChanges) {
234
259
  const resourceDto = resourceFormEntity.toResourceDto();
235
- const resourceType = resourceFormEntity.resourceTypeId
236
- ? this.props.context.resourceTypesCollection?.getFirstById(resourceFormEntity.resourceTypeId)
237
- : null;
238
- const isV4PasswordString = resourceType?.slug === RESOURCE_TYPE_PASSWORD_STRING_SLUG;
239
- const secretDto = isV4PasswordString ? resourceFormEntity.toSecretDto().password : resourceFormEntity.toSecretDto();
240
260
  return this.permissionServiceWorkerService.updateResource(resourceDto, secretDto, permissionChanges);
241
261
  }
242
262
  }
@@ -244,6 +264,9 @@ export class ResourceEditFlow extends AbstractPermissionFlow {
244
264
  ResourceEditFlow.propTypes = {
245
265
  ...AbstractPermissionFlow.propTypes,
246
266
  resource: PropTypes.object.isRequired, // the resource DTO being edited (carries the operator's own permission)
267
+ resourceWorkspaceContext: PropTypes.any, // The resource workspace context
247
268
  };
248
269
 
249
- export default withAppContext(withDialog(withActionFeedback(withRouter(withTranslation("common")(ResourceEditFlow)))));
270
+ export default withAppContext(
271
+ withDialog(withActionFeedback(withRouter(withResourceWorkspace(withTranslation("common")(ResourceEditFlow))))),
272
+ );
@@ -64,6 +64,7 @@ function dialogPropsFor(dialogContext, DialogComponent) {
64
64
  }
65
65
 
66
66
  const fakeResourceFormEntity = { toResourceDto: () => ({}), toSecretDto: () => ({}) };
67
+ const fakeSecretDto = { object_type: "PASSBOLT_SECRET_DATA" };
67
68
 
68
69
  /**
69
70
  * Mount the flow and wait until the EditResource dialog is open.
@@ -110,7 +111,7 @@ describe("ResourceEditFlow", () => {
110
111
 
111
112
  // Submit the form: ShareDialog must follow because the resource is shared.
112
113
  const editProps = dialogPropsFor(props.dialogContext, EditResource);
113
- await act(() => editProps.onSubmit(fakeResourceFormEntity));
114
+ await act(() => editProps.onSubmit(fakeResourceFormEntity, fakeSecretDto));
114
115
  await waitFor(() => {
115
116
  if (page._instance.state.status !== RESOURCE_EDIT_FLOW_STATUS.SHARE_DIALOG_OPEN) {
116
117
  throw new Error("ShareDialog not yet opened");
@@ -168,7 +169,7 @@ describe("ResourceEditFlow", () => {
168
169
 
169
170
  const page = await mountUntilEditOpen(props);
170
171
  const editProps = dialogPropsFor(props.dialogContext, EditResource);
171
- await act(() => editProps.onSubmit(fakeResourceFormEntity));
172
+ await act(() => editProps.onSubmit(fakeResourceFormEntity, fakeSecretDto));
172
173
  await waitFor(() => {
173
174
  if (page._instance.state.status !== RESOURCE_EDIT_FLOW_STATUS.SHARE_DIALOG_OPEN) {
174
175
  throw new Error("ShareDialog not yet opened");
@@ -214,7 +215,7 @@ describe("ResourceEditFlow", () => {
214
215
 
215
216
  const page = await mountUntilEditOpen(props);
216
217
  const editProps = dialogPropsFor(props.dialogContext, EditResource);
217
- await act(() => editProps.onSubmit(fakeResourceFormEntity));
218
+ await act(() => editProps.onSubmit(fakeResourceFormEntity, fakeSecretDto));
218
219
  await waitFor(() => {
219
220
  if (page._instance.state.status !== RESOURCE_EDIT_FLOW_STATUS.SHARE_DIALOG_OPEN) {
220
221
  throw new Error("ShareDialog not yet opened");
@@ -255,7 +256,7 @@ describe("ResourceEditFlow", () => {
255
256
  jest
256
257
  .spyOn(props.context.port, "request")
257
258
  .mockImplementation((event) => (event === "passbolt.resources.update" ? { id: props.resource.id } : undefined));
258
- await act(() => editProps.onSubmit(fakeResourceFormEntity));
259
+ await act(() => editProps.onSubmit(fakeResourceFormEntity, fakeSecretDto));
259
260
 
260
261
  expect(props.context.port.request).toHaveBeenCalledWith(
261
262
  "passbolt.resources.update",
@@ -292,8 +293,8 @@ describe("ResourceEditFlow", () => {
292
293
  );
293
294
  });
294
295
 
295
- it("As LU cancelling ShareDialog should terminate the workflow without updating the resource", async () => {
296
- expect.assertions(2);
296
+ it("As LU cancelling ShareDialog should return to the edit dialog without updating the resource", async () => {
297
+ expect.assertions(3);
297
298
  const props = defaultProps();
298
299
  const operatorId = props.context.loggedInUser.id;
299
300
  wireSnapshotListeners(props.context.port, {
@@ -305,7 +306,10 @@ describe("ResourceEditFlow", () => {
305
306
 
306
307
  const page = await mountUntilEditOpen(props);
307
308
  const editProps = dialogPropsFor(props.dialogContext, EditResource);
308
- await act(() => editProps.onSubmit(fakeResourceFormEntity));
309
+ // The edit dialog registers a focus-back listener the flow calls to refocus it on share cancel.
310
+ const focusBackListener = jest.fn();
311
+ editProps.setFocusBackListener(focusBackListener);
312
+ await act(() => editProps.onSubmit(fakeResourceFormEntity, fakeSecretDto));
309
313
  await waitFor(() => {
310
314
  if (page._instance.state.status !== RESOURCE_EDIT_FLOW_STATUS.SHARE_DIALOG_OPEN) {
311
315
  throw new Error("ShareDialog not yet opened");
@@ -316,7 +320,9 @@ describe("ResourceEditFlow", () => {
316
320
  const shareProps = dialogPropsFor(props.dialogContext, ShareDialog);
317
321
  shareProps.onClose();
318
322
 
319
- expect(props.onStop).toHaveBeenCalledTimes(1);
323
+ // Cancelling the share step returns to the edit dialog: the workflow is not terminated.
324
+ expect(focusBackListener).toHaveBeenCalledTimes(1);
325
+ expect(props.onStop).not.toHaveBeenCalled();
320
326
  expect(props.context.port.request).not.toHaveBeenCalledWith(
321
327
  "passbolt.resources.update",
322
328
  expect.anything(),
@@ -157,9 +157,11 @@ class GroupPermissionItem extends Component {
157
157
  />
158
158
  </div>
159
159
 
160
- <div className="actions">
161
- <SharePermissionDeleteButton onClose={this.handleDelete} disabled={isInputDisabled} />
162
- </div>
160
+ {!this.props.isReadOnly && (
161
+ <div className="actions">
162
+ <SharePermissionDeleteButton onClose={this.handleDelete} disabled={isInputDisabled} />
163
+ </div>
164
+ )}
163
165
  </li>
164
166
  );
165
167
  }
@@ -183,6 +185,7 @@ GroupPermissionItem.propTypes = {
183
185
  shouldDisplayGroupMembers: PropTypes.bool,
184
186
  canDisplayGroupMembers: PropTypes.bool,
185
187
  permissionType: PropTypes.number,
188
+ isReadOnly: PropTypes.bool,
186
189
  t: PropTypes.func, // The translation function
187
190
  };
188
191
 
@@ -82,6 +82,15 @@ class ShareDialog extends Component {
82
82
 
83
83
  this.shareChanges = new ShareChanges(this.resources, this.folders);
84
84
  const permissions = this.shareChanges.aggregatePermissionsByAro();
85
+
86
+ const permissionsMap = new Map(permissions.map((p) => [p.aro.id, p]));
87
+ this.props.initialChanges?.forEach((change) => {
88
+ const permission = permissionsMap.get(change.aroForeignKey);
89
+ if (permission) {
90
+ this.shareChanges.markPermissionHasChanged(permission);
91
+ }
92
+ });
93
+
85
94
  this.setState({ loading: false, name: "", permissions: permissions }, () => {
86
95
  // scroll at the top of the permission list
87
96
  this.permissionListRef.current.scrollTo(0);
@@ -326,7 +335,7 @@ class ShareDialog extends Component {
326
335
 
327
336
  // TODO restore to original permission if any
328
337
  const permission = this.shareChanges.addAroPermissions(aro);
329
- permission.updated = true;
338
+ permission.updated = this.shareChanges.hasChanges(aro.id);
330
339
  const permissions = this.state.permissions;
331
340
  permissions.push(permission);
332
341
  this.setState({ permissions: permissions }, () => {
@@ -718,6 +727,7 @@ class ShareDialog extends Component {
718
727
  onToggleGroupMemberVisibility={this.handleToggleGroupMemberVisibility}
719
728
  shouldDisplayGroupMembers={this.state.expandedGroupIds.includes(permission.aro.id)}
720
729
  canDisplayGroupMembers={this.isControlledMode()}
730
+ isReadOnly={this.props.readOnly}
721
731
  />
722
732
  );
723
733
  }
@@ -733,6 +743,7 @@ class ShareDialog extends Component {
733
743
  disabled={this.hasAllInputDisabled() || this.isReadOnly()}
734
744
  onUpdate={this.handlePermissionUpdate}
735
745
  onDelete={this.handlePermissionDelete}
746
+ isReadOnly={this.props.readOnly}
736
747
  />
737
748
  );
738
749
  }
@@ -851,6 +862,7 @@ class ShareDialog extends Component {
851
862
  ShareDialog.defaultProps = {
852
863
  listMinSize: 4,
853
864
  isPermissionConfirmationMode: true,
865
+ initialChanges: [],
854
866
  };
855
867
 
856
868
  ShareDialog.propTypes = {
@@ -862,6 +874,7 @@ ShareDialog.propTypes = {
862
874
  listMinSize: PropTypes.number, // The minimum size to be renderered in the permission list
863
875
  isPermissionConfirmationMode: PropTypes.bool, // Is the dialog used to confirm permissions
864
876
  initialResources: PropTypes.array, // Controlled mode: the ACOs to seed the dialog with instead of fetching from the API, each as { id, metadata, permission, permissions: PermissionsCollection }
877
+ initialChanges: PropTypes.array, // Set of permission to mark them as "modified" in the initial list
865
878
  acoType: PropTypes.string, // Controlled mode: the ACO type of the seeded entries (PermissionEntity.ACO_RESOURCE, default, or ACO_FOLDER)
866
879
  initialGroups: PropTypes.object, // Controlled mode: GroupsCollection providing the groups referenced by the resources' permissions
867
880
  initialUsers: PropTypes.object, // Controlled mode: UsersCollection providing the users referenced by the resources' permissions
@@ -185,9 +185,11 @@ class UserPermissionItem extends Component {
185
185
  />
186
186
  </div>
187
187
 
188
- <div className="actions">
189
- <SharePermissionDeleteButton onClose={this.handleDelete} disabled={isInputDisabled} />
190
- </div>
188
+ {!this.props.isReadOnly && (
189
+ <div className="actions">
190
+ <SharePermissionDeleteButton onClose={this.handleDelete} disabled={isInputDisabled} />
191
+ </div>
192
+ )}
191
193
  </li>
192
194
  );
193
195
  }
@@ -203,6 +205,7 @@ UserPermissionItem.propTypes = {
203
205
  onUpdate: PropTypes.func,
204
206
  onDelete: PropTypes.func,
205
207
  permissionType: PropTypes.number,
208
+ isReadOnly: PropTypes.bool,
206
209
  t: PropTypes.func, // The translation function
207
210
  };
208
211
 
@@ -206,6 +206,18 @@ export default class ShareChanges {
206
206
  });
207
207
  }
208
208
 
209
+ /**
210
+ * Used to initialise the ShareChanges list with already "changed" permissions.
211
+ * For example when creating a new shared resource, the permissions must be shown as "new"
212
+ * instead of "already existing".
213
+ * @param {PermissionEntity} permission
214
+ */
215
+ markPermissionHasChanged(permission) {
216
+ const permissionChange = this._buildChange(permission.permissions[0].aco, permission.aro, permission.type);
217
+ this._changes.push(permissionChange);
218
+ permission.updated = true;
219
+ }
220
+
209
221
  /**
210
222
  * Delete aro's permissions.
211
223
  * @param {string} aroId The aro to delete the permissions for
@@ -83,8 +83,8 @@ export class ResourceTypesLocalStorageContextProvider extends React.Component {
83
83
  * Handles update of the resource types in the local storage.
84
84
  */
85
85
  handleStorageChange(changes) {
86
- if (changes.resourceTypes) {
87
- this.set(changes.resourceTypes.newValue);
86
+ if (changes[this.storageKey] && changes[this.storageKey].newValue) {
87
+ this.set(changes[this.storageKey].newValue);
88
88
  }
89
89
  }
90
90
 
@@ -80,7 +80,7 @@ describe("ResourceTypesLocalStorageContext", () => {
80
80
  const expectedResourceTypes = resourceTypesCollectionDto();
81
81
 
82
82
  contextProvider.handleStorageChange({
83
- resourceTypes: {
83
+ [contextProvider.storageKey]: {
84
84
  newValue: expectedResourceTypes,
85
85
  },
86
86
  });
@@ -112,7 +112,7 @@ describe("ResourceTypesLocalStorageContext", () => {
112
112
  const expectedResourceTypes = resourceTypesCollectionDto();
113
113
 
114
114
  contextProvider.handleStorageChange({
115
- resourceTypes: {
115
+ [contextProvider.storageKey]: {
116
116
  newValue: expectedResourceTypes,
117
117
  },
118
118
  });
@@ -127,7 +127,7 @@ describe("ResourceTypesLocalStorageContext", () => {
127
127
  mockComponentSetState(contextProvider);
128
128
 
129
129
  contextProvider.handleStorageChange({
130
- resourceTypes: {
130
+ [contextProvider.storageKey]: {
131
131
  newValue: [],
132
132
  },
133
133
  });
@@ -27,15 +27,11 @@ export default class PermissionChangesService {
27
27
  * @param {PermissionSnapshotEntity} snapshot The snapshot shown in ShareDialog.
28
28
  * @param {Array<object>} dialogChanges Deltas as `ShareChanges.getResourcesChanges()` emits them.
29
29
  * @param {string} resourceId Id of the freshly-created resource.
30
- * @param {string} operatorId Id of the logged-in user (excluded from output).
31
30
  * @returns {Array<object>} `is_new`-flagged permission DTOs in the shape `share.resources.save` accepts.
32
31
  */
33
- buildResourcePermissionChanges(snapshot, dialogChanges, resourceId, operatorId) {
32
+ buildResourcePermissionChanges(snapshot, dialogChanges, resourceId) {
34
33
  const finalByAroId = new Map();
35
34
  for (const permission of snapshot.permissions.items) {
36
- if (permission.aroForeignKey === operatorId) {
37
- continue;
38
- }
39
35
  finalByAroId.set(permission.aroForeignKey, {
40
36
  is_new: true,
41
37
  aro: permission.aro,
@@ -38,7 +38,7 @@ describe("PermissionChangesService", () => {
38
38
  }
39
39
 
40
40
  describe("::buildResourcePermissionChanges", () => {
41
- it("emits every non-operator snapshot row as is_new targeting the new resource when there are no dialog edits", () => {
41
+ it("emits every row as is_new targeting the new resource when there are no dialog edits", () => {
42
42
  expect.assertions(3);
43
43
 
44
44
  const resourceId = uuidv4();
@@ -64,8 +64,16 @@ describe("PermissionChangesService", () => {
64
64
 
65
65
  const changes = service.buildResourcePermissionChanges(snapshot, [], resourceId, operatorId);
66
66
 
67
- expect(changes).toHaveLength(1);
67
+ expect(changes).toHaveLength(2);
68
68
  expect(changes[0]).toMatchObject({
69
+ is_new: true,
70
+ aro: "User",
71
+ aro_foreign_key: operatorId,
72
+ aco: "Resource",
73
+ aco_foreign_key: resourceId,
74
+ type: 15,
75
+ });
76
+ expect(changes[1]).toMatchObject({
69
77
  is_new: true,
70
78
  aro: "User",
71
79
  aro_foreign_key: readerId,
@@ -73,8 +81,6 @@ describe("PermissionChangesService", () => {
73
81
  aco_foreign_key: resourceId,
74
82
  type: 1,
75
83
  });
76
- // Operator row is the implicit owner on the newly-created resource and must not be emitted.
77
- expect(changes.find((change) => change.aro_foreign_key === operatorId)).toBeUndefined();
78
84
  });
79
85
 
80
86
  it("drops a snapshot row when the operator's dialog edits include a matching `delete` delta", () => {
@@ -94,11 +100,11 @@ describe("PermissionChangesService", () => {
94
100
 
95
101
  const changes = service.buildResourcePermissionChanges(snapshot, dialogChanges, resourceId, operatorId);
96
102
 
97
- expect(changes).toEqual([]);
103
+ expect(changes).toHaveLength(1);
98
104
  });
99
105
 
100
106
  it("patches the type of a snapshot row when the operator's dialog edits include a type-update delta", () => {
101
- expect.assertions(2);
107
+ expect.assertions(3);
102
108
 
103
109
  const folderId = uuidv4();
104
110
  const resourceId = uuidv4();
@@ -112,10 +118,11 @@ describe("PermissionChangesService", () => {
112
118
  { aro: "User", aro_foreign_key: readerId, aco: "Resource", aco_foreign_key: null, type: 15 },
113
119
  ];
114
120
 
115
- const changes = service.buildResourcePermissionChanges(snapshot, dialogChanges, resourceId, operatorId);
121
+ const changes = service.buildResourcePermissionChanges(snapshot, dialogChanges, resourceId);
116
122
 
117
- expect(changes).toHaveLength(1);
118
- expect(changes[0]).toMatchObject({ is_new: true, aro_foreign_key: readerId, type: 15 });
123
+ expect(changes).toHaveLength(2);
124
+ expect(changes[0]).toMatchObject({ is_new: true, aro_foreign_key: operatorId, type: 15 });
125
+ expect(changes[1]).toMatchObject({ is_new: true, aro_foreign_key: readerId, type: 15 });
119
126
  });
120
127
 
121
128
  it("appends a brand-new aro from the operator's dialog edits with aco_foreign_key stamped", () => {
@@ -134,8 +141,8 @@ describe("PermissionChangesService", () => {
134
141
 
135
142
  const changes = service.buildResourcePermissionChanges(snapshot, dialogChanges, resourceId, operatorId);
136
143
 
137
- expect(changes).toHaveLength(1);
138
- expect(changes[0]).toMatchObject({
144
+ expect(changes).toHaveLength(2);
145
+ expect(changes[1]).toMatchObject({
139
146
  is_new: true,
140
147
  aro_foreign_key: newAroId,
141
148
  aco: "Resource",
@@ -143,36 +150,5 @@ describe("PermissionChangesService", () => {
143
150
  type: 1,
144
151
  });
145
152
  });
146
-
147
- it("excludes the operator's own row from the output even when the snapshot lists it explicitly", () => {
148
- expect.assertions(2);
149
-
150
- const folderId = uuidv4();
151
- const resourceId = uuidv4();
152
- const operatorId = uuidv4();
153
- const readerId = uuidv4();
154
- const snapshot = snapshotWithPermissions([
155
- defaultPermissionDto({ aco: "Folder", aco_foreign_key: folderId, aro_foreign_key: operatorId, type: 15 }),
156
- defaultPermissionDto({ aco: "Folder", aco_foreign_key: folderId, aro_foreign_key: readerId, type: 1 }),
157
- ]);
158
-
159
- const changes = service.buildResourcePermissionChanges(snapshot, [], resourceId, operatorId);
160
-
161
- expect(changes).toHaveLength(1);
162
- expect(changes.find((change) => change.aro_foreign_key === operatorId)).toBeUndefined();
163
- });
164
-
165
- it("returns an empty array when the snapshot has only the operator and the dialog has no edits", () => {
166
- expect.assertions(1);
167
-
168
- const folderId = uuidv4();
169
- const resourceId = uuidv4();
170
- const operatorId = uuidv4();
171
- const snapshot = snapshotWithPermissions([
172
- defaultPermissionDto({ aco: "Folder", aco_foreign_key: folderId, aro_foreign_key: operatorId, type: 15 }),
173
- ]);
174
-
175
- expect(service.buildResourcePermissionChanges(snapshot, [], resourceId, operatorId)).toEqual([]);
176
- });
177
153
  });
178
154
  });