@overmap-ai/core 1.0.48-bulk-form-submission.2 → 1.0.48-bulk-form-submission.4

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.
@@ -804,6 +804,19 @@ function downloadInMemoryFile(filename, text) {
804
804
  element.click();
805
805
  document.body.removeChild(element);
806
806
  }
807
+ const constructUploadedFilePayloads = async (files) => {
808
+ const filePayloads = {};
809
+ for (const file of files) {
810
+ const sha1 = await hashFile(file);
811
+ filePayloads[sha1] = {
812
+ sha1,
813
+ extension: file.name.split(".").pop() || "",
814
+ file_type: file.type,
815
+ size: file.size
816
+ };
817
+ }
818
+ return Object.values(filePayloads);
819
+ };
807
820
  const fileToBlob = async (dataUrl) => {
808
821
  return (await fetch(dataUrl)).blob();
809
822
  };
@@ -1537,6 +1550,15 @@ function updateAttachment(state, action) {
1537
1550
  throw new Error(`Attachment ${action.payload.offline_id} does not exist.`);
1538
1551
  }
1539
1552
  }
1553
+ function updateAttachments(state, action) {
1554
+ for (const attachment of action.payload) {
1555
+ if (attachment.offline_id in state.attachments) {
1556
+ state.attachments[attachment.offline_id] = attachment;
1557
+ } else {
1558
+ throw new Error(`Attachment ${attachment.offline_id} does not exist.`);
1559
+ }
1560
+ }
1561
+ }
1540
1562
  function removeAttachment(state, action) {
1541
1563
  if (action.payload in state.attachments) {
1542
1564
  delete state.attachments[action.payload];
@@ -2130,6 +2152,7 @@ const issueSlice = createSlice({
2130
2152
  }
2131
2153
  },
2132
2154
  updateIssueAttachment: updateAttachment,
2155
+ updateIssueAttachments: updateAttachments,
2133
2156
  removeIssue: (state, action) => {
2134
2157
  if (action.payload in state.issues) {
2135
2158
  delete state.issues[action.payload];
@@ -2138,6 +2161,7 @@ const issueSlice = createSlice({
2138
2161
  }
2139
2162
  },
2140
2163
  removeIssueAttachment: removeAttachment,
2164
+ removeIssueAttachments: removeAttachments,
2141
2165
  removeIssueUpdate: (state, action) => {
2142
2166
  if (action.payload in state.updates) {
2143
2167
  delete state.updates[action.payload];
@@ -2247,6 +2271,7 @@ const {
2247
2271
  addToRecentIssues,
2248
2272
  cleanRecentIssues,
2249
2273
  removeIssueAttachment,
2274
+ removeIssueAttachments,
2250
2275
  removeAttachmentsOfIssue,
2251
2276
  removeIssue,
2252
2277
  removeIssueUpdate,
@@ -2260,6 +2285,7 @@ const {
2260
2285
  setVisibleStatuses,
2261
2286
  setVisibleUserIds,
2262
2287
  updateIssueAttachment,
2288
+ updateIssueAttachments,
2263
2289
  updateIssue,
2264
2290
  // Commments
2265
2291
  addIssueComment,
@@ -3823,6 +3849,16 @@ const formSubmissionSlice = createSlice({
3823
3849
  state.attachments[attachment.offline_id] = attachment;
3824
3850
  }
3825
3851
  },
3852
+ updateFormSubmissionAttachments: (state, action) => {
3853
+ for (const attachment of action.payload) {
3854
+ if (state.attachments[attachment.offline_id] === void 0) {
3855
+ throw new Error(`Attachment with offline_id ${attachment.offline_id} does not exist`);
3856
+ }
3857
+ }
3858
+ for (const attachment of action.payload) {
3859
+ state.attachments[attachment.offline_id] = attachment;
3860
+ }
3861
+ },
3826
3862
  // The delete actions for UserFormSubmissionAttachments are not used in the app, but are included for completeness
3827
3863
  // Could be used if editing a submission is ever supported, will be applicable for supporting tip tap content in submissions
3828
3864
  deleteFormSubmissionAttachment: (state, action) => {
@@ -3853,6 +3889,7 @@ const {
3853
3889
  addFormSubmissionAttachment,
3854
3890
  addFormSubmissionAttachments,
3855
3891
  setFormSubmissionAttachments,
3892
+ updateFormSubmissionAttachments,
3856
3893
  deleteFormSubmissionAttachment,
3857
3894
  deleteFormSubmissionAttachments
3858
3895
  } = formSubmissionSlice.actions;
@@ -4706,6 +4743,22 @@ class BaseApiService {
4706
4743
  }
4707
4744
  }
4708
4745
  class AttachmentService extends BaseApiService {
4746
+ processPresignedUrls(presignedUrls) {
4747
+ for (const [sha1, presignedUrl] of Object.entries(presignedUrls)) {
4748
+ void this.enqueueRequest({
4749
+ url: presignedUrl.url,
4750
+ description: "Upload file",
4751
+ method: HttpMethod.POST,
4752
+ isExternalUrl: true,
4753
+ isAuthNeeded: false,
4754
+ attachmentHash: sha1,
4755
+ // TODO: can we use the sha1 as the blocker?
4756
+ blockers: [`s3-${sha1}`],
4757
+ blocks: [sha1],
4758
+ s3url: presignedUrl
4759
+ });
4760
+ }
4761
+ }
4709
4762
  fetchAll(projectId) {
4710
4763
  const promise = this.enqueueRequest({
4711
4764
  description: "Fetch attachments",
@@ -4731,6 +4784,7 @@ class AttachmentService extends BaseApiService {
4731
4784
  }
4732
4785
  const offlineAttachment = {
4733
4786
  ...attachmentPayload,
4787
+ // TODO: just handle creating the objectURL in here, then the front end doesn't need to worry about it
4734
4788
  file: attachmentPayload.file.objectURL,
4735
4789
  file_name: attachmentPayload.file.name,
4736
4790
  file_type: attachmentPayload.file.type,
@@ -4764,6 +4818,7 @@ class AttachmentService extends BaseApiService {
4764
4818
  }
4765
4819
  const offlineAttachment = {
4766
4820
  ...attachmentPayload,
4821
+ // TODO: just handle creating the objectURL in here, then the front end doesn't need to worry about it
4767
4822
  file: attachmentPayload.file.objectURL,
4768
4823
  file_name: attachmentPayload.file.name,
4769
4824
  file_type: attachmentPayload.file.type,
@@ -4797,6 +4852,7 @@ class AttachmentService extends BaseApiService {
4797
4852
  }
4798
4853
  const offlineAttachment = {
4799
4854
  ...attachmentPayload,
4855
+ // TODO: just handle creating the objectURL in here, then the front end doesn't need to worry about it
4800
4856
  file: attachmentPayload.file.objectURL,
4801
4857
  file_name: attachmentPayload.file.name,
4802
4858
  file_type: attachmentPayload.file.type,
@@ -4849,7 +4905,7 @@ class AttachmentService extends BaseApiService {
4849
4905
  offline_id,
4850
4906
  project,
4851
4907
  description: description2 ?? "",
4852
- submitted_at: (/* @__PURE__ */ new Date()).getTime() / 1e3,
4908
+ submitted_at: (/* @__PURE__ */ new Date()).toISOString(),
4853
4909
  ...fileProps
4854
4910
  }
4855
4911
  });
@@ -4862,26 +4918,54 @@ class AttachmentService extends BaseApiService {
4862
4918
  /** the outer Promise is needed to await the hashing of each file, which is required before offline use. If wanting to
4863
4919
  * attach promise handlers to the request to add the attachment in the backend, apply it on the promise returned from the
4864
4920
  * OptimisticModelResult. */
4865
- attachFilesToIssue(filesToSubmit, issueId) {
4866
- return filesToSubmit.map((file) => {
4867
- if (!(file instanceof File)) {
4868
- throw new Error("Expected a File instance.");
4869
- }
4870
- const photoAttachmentPromise = async (file2) => {
4871
- const hash = await hashFile(file2);
4872
- const attachment = offline({
4873
- file: file2,
4874
- file_name: file2.name,
4875
- file_type: file2.type,
4876
- issue: issueId,
4877
- file_sha1: hash,
4878
- submitted_at: (/* @__PURE__ */ new Date()).toISOString(),
4879
- created_by: this.client.store.getState().userReducer.currentUser.id
4880
- });
4881
- return this.addIssueAttachment(attachment);
4882
- };
4883
- return photoAttachmentPromise(file);
4921
+ // note the method is only marked as async since files needs to be hashed
4922
+ async attachFilesToIssue(files, issueId) {
4923
+ const { store } = this.client;
4924
+ const offlineAttachments = [];
4925
+ const attachmentsPayload = [];
4926
+ const currentUser = store.getState().userReducer.currentUser;
4927
+ const submittedAt = (/* @__PURE__ */ new Date()).toISOString();
4928
+ for (const file of files) {
4929
+ const attachment = offline({
4930
+ file: URL.createObjectURL(file),
4931
+ file_name: file.name,
4932
+ file_type: file.type,
4933
+ file_sha1: await hashFile(file),
4934
+ description: "",
4935
+ submitted_at: submittedAt,
4936
+ created_by: currentUser.id,
4937
+ issue: issueId
4938
+ });
4939
+ attachmentsPayload.push({
4940
+ offline_id: attachment.offline_id,
4941
+ name: attachment.file_name,
4942
+ sha1: attachment.file_sha1,
4943
+ description: attachment.description,
4944
+ created_by: attachment.created_by,
4945
+ issue_id: attachment.issue
4946
+ });
4947
+ offlineAttachments.push(attachment);
4948
+ }
4949
+ store.dispatch(addIssueAttachments(offlineAttachments));
4950
+ const promise = this.enqueueRequest({
4951
+ description: "Attach files to issue",
4952
+ method: HttpMethod.POST,
4953
+ url: `/issues/${issueId}/attach/`,
4954
+ payload: {
4955
+ submitted_at: submittedAt,
4956
+ attachments: attachmentsPayload,
4957
+ files: await constructUploadedFilePayloads(files)
4958
+ },
4959
+ blocks: offlineAttachments.map((attachment) => attachment.offline_id),
4960
+ blockers: offlineAttachments.map((attachment) => attachment.file_sha1)
4961
+ });
4962
+ promise.then(({ attachments, presigned_urls }) => {
4963
+ store.dispatch(updateIssueAttachments(attachments));
4964
+ this.processPresignedUrls(presigned_urls);
4965
+ }).catch(() => {
4966
+ store.dispatch(removeIssueAttachments(offlineAttachments.map((attachment) => attachment.offline_id)));
4884
4967
  });
4968
+ return [offlineAttachments, promise.then(({ attachments }) => attachments)];
4885
4969
  }
4886
4970
  attachFilesToComponent(filesToSubmit, componentId) {
4887
4971
  return filesToSubmit.map((file) => {
@@ -7157,17 +7241,18 @@ class UserFormSubmissionService extends BaseApiService {
7157
7241
  }
7158
7242
  // Note currently the bulkAdd method is specific to form submissions for components
7159
7243
  // TODO: adapt the support bulk adding to any model type
7160
- bulkAdd(args) {
7161
- const { form_revision, values: argsValues, component_offline_ids } = args;
7244
+ async bulkAdd(args) {
7245
+ const { form_revision, values: argsValues, componentOfflineIds } = args;
7162
7246
  const { store } = this.client;
7163
- const submissions = [];
7247
+ const offlineSubmissions = [];
7248
+ const offlineAttachments = [];
7164
7249
  const submissionOfflineIds = [];
7165
- const offline_ids_to_component_ids = [];
7166
- let attachFilesPromises = [];
7250
+ const submissionsPayload = [];
7251
+ const attachmentsPayload = [];
7167
7252
  const { values, files } = separateFilesFromValues(argsValues);
7168
7253
  const submittedAt = (/* @__PURE__ */ new Date()).toISOString();
7169
7254
  const createdBy = store.getState().userReducer.currentUser.id;
7170
- for (const component_id of component_offline_ids) {
7255
+ for (const component_id of componentOfflineIds) {
7171
7256
  const submission = offline({
7172
7257
  form_revision,
7173
7258
  values,
@@ -7175,12 +7260,43 @@ class UserFormSubmissionService extends BaseApiService {
7175
7260
  submitted_at: submittedAt,
7176
7261
  component: component_id
7177
7262
  });
7178
- attachFilesPromises = attachFilesPromises.concat(this.getAttachFilesPromises(files, submission));
7179
7263
  submissionOfflineIds.push(submission.offline_id);
7180
- offline_ids_to_component_ids.push([submission.offline_id, component_id]);
7181
- submissions.push(submission);
7264
+ submissionsPayload.push({ offline_id: submission.offline_id, component_id });
7265
+ offlineSubmissions.push(submission);
7266
+ for (const [fieldIdentifier, fileArray] of Object.entries(files)) {
7267
+ for (const file of fileArray) {
7268
+ const sha1 = await hashFile(file);
7269
+ await this.client.files.addCache(file, sha1);
7270
+ const offlineAttachment = offline({
7271
+ file_name: file.name,
7272
+ file_sha1: sha1,
7273
+ file: URL.createObjectURL(file),
7274
+ submission: submission.offline_id,
7275
+ field_identifier: fieldIdentifier
7276
+ });
7277
+ offlineAttachments.push(offlineAttachment);
7278
+ attachmentsPayload.push({
7279
+ offline_id: offlineAttachment.offline_id,
7280
+ submission_id: submission.offline_id,
7281
+ sha1,
7282
+ name: file.name,
7283
+ field_identifier: fieldIdentifier
7284
+ });
7285
+ }
7286
+ }
7287
+ }
7288
+ const filesRecord = {};
7289
+ for (const file of Object.values(files).flat()) {
7290
+ const sha1 = await hashFile(file);
7291
+ filesRecord[sha1] = {
7292
+ sha1,
7293
+ extension: file.name.split(".").pop() || "",
7294
+ file_type: file.type,
7295
+ size: file.size
7296
+ };
7182
7297
  }
7183
- store.dispatch(addFormSubmissions(submissions));
7298
+ store.dispatch(addFormSubmissions(offlineSubmissions));
7299
+ store.dispatch(addFormSubmissionAttachments(offlineAttachments));
7184
7300
  const promise = this.enqueueRequest({
7185
7301
  description: "Bulk add form submissions",
7186
7302
  method: HttpMethod.POST,
@@ -7188,17 +7304,37 @@ class UserFormSubmissionService extends BaseApiService {
7188
7304
  payload: {
7189
7305
  form_data: values,
7190
7306
  submitted_at: submittedAt,
7191
- offline_ids_to_component_ids
7307
+ submissions: submissionsPayload,
7308
+ attachments: attachmentsPayload,
7309
+ files: Object.values(filesRecord)
7192
7310
  },
7193
- blockers: component_offline_ids,
7311
+ blockers: componentOfflineIds,
7194
7312
  blocks: submissionOfflineIds
7195
7313
  });
7196
- promise.then((createdSubmissions) => {
7197
- store.dispatch(updateFormSubmissions(createdSubmissions));
7314
+ promise.then(({ submissions, attachments, presigned_urls }) => {
7315
+ store.dispatch(updateFormSubmissions(submissions));
7316
+ store.dispatch(updateFormSubmissionAttachments(attachments));
7317
+ for (const [sha1, presigned_url] of Object.entries(presigned_urls)) {
7318
+ const file = filesRecord[sha1];
7319
+ if (!file)
7320
+ continue;
7321
+ void this.enqueueRequest({
7322
+ url: presigned_url.url,
7323
+ description: "Upload file",
7324
+ method: HttpMethod.POST,
7325
+ isExternalUrl: true,
7326
+ isAuthNeeded: false,
7327
+ attachmentHash: sha1,
7328
+ blockers: [`s3-${file.sha1}.${file.extension}`],
7329
+ blocks: [sha1],
7330
+ s3url: presigned_url
7331
+ });
7332
+ }
7198
7333
  }).catch(() => {
7199
7334
  store.dispatch(deleteFormSubmissions(submissionOfflineIds));
7335
+ store.dispatch(deleteFormSubmissionAttachments(offlineAttachments.map((x) => x.offline_id)));
7200
7336
  });
7201
- return [submissions, promise.then(() => Promise.all(attachFilesPromises).then(() => promise))];
7337
+ return [offlineSubmissions, promise.then(({ submissions }) => submissions)];
7202
7338
  }
7203
7339
  update(submission) {
7204
7340
  const { store } = this.client;
@@ -15831,6 +15967,7 @@ export {
15831
15967
  componentStageSlice,
15832
15968
  componentTypeReducer,
15833
15969
  componentTypeSlice,
15970
+ constructUploadedFilePayloads,
15834
15971
  coordinatesAreEqual,
15835
15972
  coordinatesToLiteral,
15836
15973
  coordinatesToPointGeometry,
@@ -15959,6 +16096,7 @@ export {
15959
16096
  removeFavouriteProjectId,
15960
16097
  removeIssue,
15961
16098
  removeIssueAttachment,
16099
+ removeIssueAttachments,
15962
16100
  removeIssueComment,
15963
16101
  removeIssueComments,
15964
16102
  removeIssueUpdate,
@@ -16223,9 +16361,11 @@ export {
16223
16361
  updateComponentTypeAttachment,
16224
16362
  updateDocuments,
16225
16363
  updateFormSubmission,
16364
+ updateFormSubmissionAttachments,
16226
16365
  updateFormSubmissions,
16227
16366
  updateIssue,
16228
16367
  updateIssueAttachment,
16368
+ updateIssueAttachments,
16229
16369
  updateLicense,
16230
16370
  updateOrCreateProject,
16231
16371
  updateOrganizationAccess,