@overmap-ai/core 1.0.43-projects-licensing.5 → 1.0.43-tiptap.1

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.
@@ -1515,8 +1515,44 @@ const selectHiddenCategoryCount = (state) => {
1515
1515
  return hiddenCategoryCount;
1516
1516
  };
1517
1517
  const categoryReducer = categorySlice.reducer;
1518
+ function setAttachments(state, action) {
1519
+ for (const attachment of action.payload) {
1520
+ state.attachments[attachment.offline_id] = attachment;
1521
+ }
1522
+ }
1523
+ function addAttachment(state, action) {
1524
+ if (action.payload.offline_id in state.attachments) {
1525
+ throw new Error(`Attachment ${action.payload.offline_id} already exists.`);
1526
+ }
1527
+ state.attachments[action.payload.offline_id] = action.payload;
1528
+ }
1529
+ function addAttachments(state, action) {
1530
+ for (const attachment of action.payload) {
1531
+ state.attachments[attachment.offline_id] = attachment;
1532
+ }
1533
+ }
1534
+ function updateAttachment(state, action) {
1535
+ if (action.payload.offline_id in state.attachments) {
1536
+ state.attachments[action.payload.offline_id] = action.payload;
1537
+ } else {
1538
+ throw new Error(`Attachment ${action.payload.offline_id} does not exist.`);
1539
+ }
1540
+ }
1541
+ function removeAttachment(state, action) {
1542
+ if (action.payload in state.attachments) {
1543
+ delete state.attachments[action.payload];
1544
+ } else {
1545
+ throw new Error(`Attachment ${action.payload} does not exist.`);
1546
+ }
1547
+ }
1548
+ function removeAttachments(state, action) {
1549
+ for (const attachmentId of action.payload) {
1550
+ delete state.attachments[attachmentId];
1551
+ }
1552
+ }
1518
1553
  const initialState$k = {
1519
- components: {}
1554
+ components: {},
1555
+ attachments: {}
1520
1556
  };
1521
1557
  const componentSlice = createSlice({
1522
1558
  name: "components",
@@ -1535,6 +1571,12 @@ const componentSlice = createSlice({
1535
1571
  state.components = toOfflineIdRecord(action.payload);
1536
1572
  prevComponents = null;
1537
1573
  },
1574
+ setComponentAttachments: setAttachments,
1575
+ addComponentAttachment: addAttachment,
1576
+ addComponentAttachments: addAttachments,
1577
+ updateComponentAttachment: updateAttachment,
1578
+ removeComponentAttachment: removeAttachment,
1579
+ removeComponentAttachments: removeAttachments,
1538
1580
  updateComponent: (state, action) => {
1539
1581
  if (action.payload.offline_id in state.components) {
1540
1582
  state.components[action.payload.offline_id] = action.payload;
@@ -1624,12 +1666,48 @@ const selectComponentTypesFromIds = (componentTypeIds) => (state) => {
1624
1666
  return acc;
1625
1667
  }, []);
1626
1668
  };
1669
+ const selectComponentAttachmentMapping = (state) => state.componentReducer.attachments;
1670
+ const selectAllComponentAttachments = createSelector(
1671
+ [selectComponentAttachmentMapping],
1672
+ (mapping) => Object.values(mapping)
1673
+ );
1674
+ const selectAttachmentsOfComponent = restructureCreateSelectorWithArgs(
1675
+ createSelector(
1676
+ [selectAllComponentAttachments, (_state, componentId) => componentId],
1677
+ (attachments, componentId) => {
1678
+ return attachments.filter(({ component }) => componentId === component);
1679
+ }
1680
+ )
1681
+ );
1682
+ const selectAttachmentsOfComponentByType = restructureCreateSelectorWithArgs(
1683
+ createSelector(
1684
+ [selectAllComponentAttachments, (_state, componentId) => componentId],
1685
+ (attachments, componentId) => {
1686
+ const attachmentsOfComponent = attachments.filter(({ component }) => componentId === component);
1687
+ const fileAttachments = attachmentsOfComponent.filter(
1688
+ // this null check here is necessary, there are cases where file_type is null or undefined
1689
+ ({ file_type }) => !file_type || !file_type.startsWith("image/")
1690
+ );
1691
+ const imageAttachments = attachmentsOfComponent.filter(
1692
+ // this null check here is necessary, there are cases where file_type is null or undefined
1693
+ ({ file_type }) => file_type && file_type.startsWith("image/")
1694
+ );
1695
+ return { fileAttachments, imageAttachments };
1696
+ }
1697
+ )
1698
+ );
1627
1699
  const {
1628
1700
  addComponent,
1629
1701
  updateComponent,
1630
1702
  removeComponent,
1631
1703
  addComponentsInBatches,
1632
1704
  setComponents,
1705
+ setComponentAttachments,
1706
+ addComponentAttachment,
1707
+ addComponentAttachments,
1708
+ updateComponentAttachment,
1709
+ removeComponentAttachment,
1710
+ removeComponentAttachments,
1633
1711
  removeAllComponentsOfType
1634
1712
  } = componentSlice.actions;
1635
1713
  const componentReducer = componentSlice.reducer;
@@ -1789,7 +1867,8 @@ const { addStages, updateStages, removeStages, linkStageToForm, unlinkStageToFor
1789
1867
  const componentStageReducer = componentStageSlice.reducer;
1790
1868
  const initialState$h = {
1791
1869
  componentTypes: {},
1792
- hiddenComponentTypeIds: {}
1870
+ hiddenComponentTypeIds: {},
1871
+ attachments: {}
1793
1872
  };
1794
1873
  const componentTypeSlice = createSlice({
1795
1874
  name: "componentTypes",
@@ -1802,6 +1881,12 @@ const componentTypeSlice = createSlice({
1802
1881
  setComponentTypes: (state, action) => {
1803
1882
  state.componentTypes = toOfflineIdRecord(action.payload);
1804
1883
  },
1884
+ setComponentTypeAttachments: setAttachments,
1885
+ addComponentTypeAttachment: addAttachment,
1886
+ addComponentTypeAttachments: addAttachments,
1887
+ updateComponentTypeAttachment: updateAttachment,
1888
+ removeComponentTypeAttachment: removeAttachment,
1889
+ removeComponentTypeAttachments: removeAttachments,
1805
1890
  toggleComponentTypeVisibility: (state, action) => {
1806
1891
  state.hiddenComponentTypeIds[action.payload] = !state.hiddenComponentTypeIds[action.payload];
1807
1892
  },
@@ -1851,7 +1936,50 @@ const selectComponentTypesByName = restructureCreateSelectorWithArgs(
1851
1936
  )
1852
1937
  );
1853
1938
  const selectHiddenComponentTypeIds = (state) => state.componentTypeReducer.hiddenComponentTypeIds;
1854
- const { addComponentType, setComponentTypes, toggleComponentTypeVisibility, deleteComponentType } = componentTypeSlice.actions;
1939
+ const selectComponentTypeAttachmentMapping = (state) => state.componentTypeReducer.attachments;
1940
+ const selectAllComponentTypeAttachments = createSelector(
1941
+ [selectComponentTypeAttachmentMapping],
1942
+ (mapping) => Object.values(mapping)
1943
+ );
1944
+ const selectAttachmentsOfComponentType = restructureCreateSelectorWithArgs(
1945
+ createSelector(
1946
+ [selectAllComponentTypeAttachments, (_state, componentTypeId) => componentTypeId],
1947
+ (attachments, componentTypeId) => {
1948
+ return attachments.filter(({ component_type }) => componentTypeId === component_type);
1949
+ }
1950
+ )
1951
+ );
1952
+ const selectAttachmentsOfComponentTypeByType = restructureCreateSelectorWithArgs(
1953
+ createSelector(
1954
+ [selectAllComponentTypeAttachments, (_state, componentTypeId) => componentTypeId],
1955
+ (attachments, componentTypeId) => {
1956
+ const attachmentsOfComponent = attachments.filter(
1957
+ ({ component_type }) => component_type === componentTypeId
1958
+ );
1959
+ const fileAttachments = attachmentsOfComponent.filter(
1960
+ // this null check here is necessary, there are cases where file_type is null or undefined
1961
+ ({ file_type }) => !file_type || !file_type.startsWith("image/")
1962
+ );
1963
+ const imageAttachments = attachmentsOfComponent.filter(
1964
+ // this null check here is necessary, there are cases where file_type is null or undefined
1965
+ ({ file_type }) => file_type && file_type.startsWith("image/")
1966
+ );
1967
+ return { fileAttachments, imageAttachments };
1968
+ }
1969
+ )
1970
+ );
1971
+ const {
1972
+ addComponentType,
1973
+ setComponentTypes,
1974
+ setComponentTypeAttachments,
1975
+ addComponentTypeAttachment,
1976
+ addComponentTypeAttachments,
1977
+ updateComponentTypeAttachment,
1978
+ removeComponentTypeAttachment,
1979
+ removeComponentTypeAttachments,
1980
+ toggleComponentTypeVisibility,
1981
+ deleteComponentType
1982
+ } = componentTypeSlice.actions;
1855
1983
  const componentTypeReducer = componentTypeSlice.reducer;
1856
1984
  const initialState$g = {
1857
1985
  workspaces: {},
@@ -1941,11 +2069,7 @@ const issueSlice = createSlice({
1941
2069
  });
1942
2070
  },
1943
2071
  // TODO: Reusable function
1944
- setAttachments: (state, action) => {
1945
- for (const attachment of action.payload) {
1946
- state.attachments[attachment.offline_id] = attachment;
1947
- }
1948
- },
2072
+ setIssueAttachments: setAttachments,
1949
2073
  setActiveIssueId: (state, action) => {
1950
2074
  state.activeIssueId = action.payload;
1951
2075
  },
@@ -1956,17 +2080,8 @@ const issueSlice = createSlice({
1956
2080
  state.issues[action.payload.offline_id] = action.payload;
1957
2081
  },
1958
2082
  // TODO: Reusable function
1959
- addAttachment: (state, action) => {
1960
- if (action.payload.offline_id in state.attachments) {
1961
- throw new Error(`Attachment ${action.payload.offline_id} already exists.`);
1962
- }
1963
- state.attachments[action.payload.offline_id] = action.payload;
1964
- },
1965
- addAttachments: (state, action) => {
1966
- for (const attachment of action.payload) {
1967
- state.attachments[attachment.offline_id] = attachment;
1968
- }
1969
- },
2083
+ addIssueAttachment: addAttachment,
2084
+ addIssueAttachments: addAttachments,
1970
2085
  updateIssue: (state, action) => {
1971
2086
  if (action.payload.offline_id in state.issues) {
1972
2087
  state.issues[action.payload.offline_id] = {
@@ -1978,13 +2093,7 @@ const issueSlice = createSlice({
1978
2093
  }
1979
2094
  },
1980
2095
  // TODO: Reusable function
1981
- updateAttachment: (state, action) => {
1982
- if (action.payload.offline_id in state.attachments) {
1983
- state.attachments[action.payload.offline_id] = action.payload;
1984
- } else {
1985
- throw new Error(`Attachment ${action.payload.offline_id} does not exist.`);
1986
- }
1987
- },
2096
+ updateIssueAttachment: updateAttachment,
1988
2097
  removeIssue: (state, action) => {
1989
2098
  if (action.payload in state.issues) {
1990
2099
  delete state.issues[action.payload];
@@ -1992,15 +2101,9 @@ const issueSlice = createSlice({
1992
2101
  throw new Error(`Failed to remove issue because ID doesn't exist: ${action.payload}`);
1993
2102
  }
1994
2103
  },
1995
- removeAttachment: (state, action) => {
1996
- if (action.payload in state.attachments) {
1997
- delete state.attachments[action.payload];
1998
- } else {
1999
- throw new Error(`Attachment ${action.payload} does not exist.`);
2000
- }
2001
- },
2104
+ removeIssueAttachment: removeAttachment,
2002
2105
  removeAttachmentsOfIssue: (state, action) => {
2003
- const attachments = Object.values(state.attachments).filter((a) => a.issue_id === action.payload);
2106
+ const attachments = Object.values(state.attachments).filter((a) => a.issue === action.payload);
2004
2107
  for (const attachment of attachments) {
2005
2108
  delete state.attachments[attachment.offline_id];
2006
2109
  }
@@ -2052,25 +2155,25 @@ const issueSlice = createSlice({
2052
2155
  }
2053
2156
  });
2054
2157
  const {
2055
- addAttachment,
2056
- addAttachments,
2158
+ addIssueAttachment,
2159
+ addIssueAttachments,
2057
2160
  addIssue,
2058
2161
  addOrReplaceIssueComment,
2059
2162
  addToRecentIssues,
2060
2163
  cleanRecentIssues,
2061
- removeAttachment,
2164
+ removeIssueAttachment,
2062
2165
  removeAttachmentsOfIssue,
2063
2166
  removeIssue,
2064
2167
  removeIssueComment,
2065
2168
  removeRecentIssue,
2066
2169
  resetRecentIssues,
2067
2170
  setActiveIssueId,
2068
- setAttachments,
2171
+ setIssueAttachments,
2069
2172
  setIssueComments,
2070
2173
  setIssues,
2071
2174
  setVisibleStatuses,
2072
2175
  setVisibleUserIds,
2073
- updateAttachment,
2176
+ updateIssueAttachment,
2074
2177
  updateIssue
2075
2178
  } = issueSlice.actions;
2076
2179
  const selectIssueMapping = (state) => state.issueReducer.issues;
@@ -2130,10 +2233,8 @@ const selectPhotoAttachmentsOfIssue = restructureCreateSelectorWithArgs(
2130
2233
  createSelector(
2131
2234
  [selectIssueAttachmentMapping, (_state, issueId) => issueId],
2132
2235
  (attachmentMapping, issueId) => {
2133
- if (!issueId)
2134
- return void 0;
2135
2236
  return Object.values(attachmentMapping).filter(
2136
- (attachment) => attachment.issue_id === issueId && attachment.file_type && attachment.file_type.startsWith("image/")
2237
+ (attachment) => attachment.issue === issueId && attachment.file_type && attachment.file_type.startsWith("image/")
2137
2238
  );
2138
2239
  }
2139
2240
  )
@@ -2144,6 +2245,31 @@ const selectCommentsOfIssue = restructureCreateSelectorWithArgs(
2144
2245
  return Object.values(commentMapping).filter((comment) => comment.issue === issueId);
2145
2246
  })
2146
2247
  );
2248
+ const selectAttachmentsOfIssue = restructureCreateSelectorWithArgs(
2249
+ createSelector(
2250
+ [selectIssueAttachments, (_state, issueId) => issueId],
2251
+ (attachments, issueId) => {
2252
+ return attachments.filter(({ issue }) => issueId === issue);
2253
+ }
2254
+ )
2255
+ );
2256
+ const selectAttachmentsOfIssueByType = restructureCreateSelectorWithArgs(
2257
+ createSelector(
2258
+ [selectIssueAttachments, (_state, issueId) => issueId],
2259
+ (attachments, issueId) => {
2260
+ const attachmentsOfIssue = attachments.filter(({ issue }) => issue === issueId);
2261
+ const fileAttachments = attachmentsOfIssue.filter(
2262
+ // this null check here is necessary, there are cases where file_type is null or undefined
2263
+ ({ file_type }) => !file_type || !file_type.startsWith("image/")
2264
+ );
2265
+ const imageAttachments = attachmentsOfIssue.filter(
2266
+ // this null check here is necessary, there are cases where file_type is null or undefined
2267
+ ({ file_type }) => file_type && file_type.startsWith("image/")
2268
+ );
2269
+ return { fileAttachments, imageAttachments };
2270
+ }
2271
+ )
2272
+ );
2147
2273
  const selectFileAttachmentsOfIssue = restructureCreateSelectorWithArgs(
2148
2274
  createSelector(
2149
2275
  [selectIssueAttachmentMapping, (_state, issueId) => issueId],
@@ -2153,7 +2279,7 @@ const selectFileAttachmentsOfIssue = restructureCreateSelectorWithArgs(
2153
2279
  return Object.values(attachmentMapping).filter(
2154
2280
  (attachment) => (
2155
2281
  // Files with file_type that is null or not an image file
2156
- attachment.issue_id === issueId && (!attachment.file_type || !attachment.file_type.startsWith("image/"))
2282
+ attachment.issue === issueId && (!attachment.file_type || !attachment.file_type.startsWith("image/"))
2157
2283
  )
2158
2284
  );
2159
2285
  }
@@ -4082,12 +4208,16 @@ class AttachmentService extends BaseApiService {
4082
4208
  blocks: [],
4083
4209
  blockers: []
4084
4210
  });
4085
- const allAttachments = Object.values(this.client.store.getState().issueReducer.attachments);
4211
+ const allAttachments = {
4212
+ issue_attachments: Object.values(this.client.store.getState().issueReducer.attachments),
4213
+ component_attachments: Object.values(this.client.store.getState().componentReducer.attachments),
4214
+ component_type_attachments: Object.values(this.client.store.getState().componentTypeReducer.attachments)
4215
+ };
4086
4216
  return [allAttachments, promise];
4087
4217
  }
4088
4218
  // Attachments aren't models, so we use the OptimisticGenericResult type instead
4089
- async add(attachmentPayload) {
4090
- const { description: description2, issue_id, file_sha1, offline_id } = attachmentPayload;
4219
+ async addIssueAttachment(attachmentPayload) {
4220
+ const { description: description2, issue, file_sha1, offline_id } = attachmentPayload;
4091
4221
  if (!attachmentPayload.file.objectURL) {
4092
4222
  throw new Error("Expected attachmentPayload.file.objectURL to be defined.");
4093
4223
  }
@@ -4098,45 +4228,157 @@ class AttachmentService extends BaseApiService {
4098
4228
  file_type: attachmentPayload.file.type
4099
4229
  };
4100
4230
  await this.client.files.addCache(attachmentPayload.file, file_sha1);
4101
- this.client.store.dispatch(addAttachment(offlineAttachment));
4231
+ this.client.store.dispatch(addIssueAttachment(offlineAttachment));
4102
4232
  const [fileProps] = await this.client.files.uploadFileToS3(file_sha1);
4103
4233
  const promise = this.enqueueRequest({
4104
4234
  description: "Create attachment",
4105
4235
  method: HttpMethod.POST,
4106
- url: `/issues/${issue_id}/attach/`,
4107
- blocks: [offline_id, issue_id],
4236
+ url: `/issues/${issue}/attach/`,
4237
+ blocks: [offline_id, issue],
4108
4238
  blockers: [file_sha1],
4109
4239
  payload: {
4110
4240
  offline_id,
4111
- issue: issue_id,
4241
+ issue,
4112
4242
  description: description2 ?? "",
4113
4243
  submitted_at: (/* @__PURE__ */ new Date()).getTime() / 1e3,
4114
4244
  ...fileProps
4115
4245
  }
4116
4246
  });
4247
+ promise.catch((error2) => {
4248
+ this.client.store.dispatch(removeIssueAttachment(offlineAttachment.offline_id));
4249
+ throw error2;
4250
+ });
4117
4251
  return [offlineAttachment, promise];
4118
4252
  }
4119
- async attachFilesToIssue(filesToSubmit, issueId) {
4120
- return Promise.allSettled(
4121
- filesToSubmit.map((file) => {
4122
- if (!(file instanceof File)) {
4123
- throw new Error("Expected a File instance.");
4124
- }
4125
- const photoAttachmentPromise = async (file2) => {
4126
- const hash = await hashFile(file2);
4127
- const attachment = offline({
4128
- file: file2,
4129
- // No description for now
4130
- issue_id: issueId,
4131
- file_sha1: hash
4132
- });
4133
- return await this.add(attachment);
4134
- };
4135
- return photoAttachmentPromise(file);
4136
- })
4137
- );
4253
+ async addComponentAttachment(attachmentPayload) {
4254
+ const { description: description2, component, file_sha1, offline_id } = attachmentPayload;
4255
+ if (!attachmentPayload.file.objectURL) {
4256
+ throw new Error("Expected attachmentPayload.file.objectURL to be defined.");
4257
+ }
4258
+ const offlineAttachment = {
4259
+ ...attachmentPayload,
4260
+ file: attachmentPayload.file.objectURL,
4261
+ file_name: attachmentPayload.file.name,
4262
+ file_type: attachmentPayload.file.type
4263
+ };
4264
+ await this.client.files.addCache(attachmentPayload.file, file_sha1);
4265
+ this.client.store.dispatch(addComponentAttachment(offlineAttachment));
4266
+ const [fileProps] = await this.client.files.uploadFileToS3(file_sha1);
4267
+ const promise = this.enqueueRequest({
4268
+ description: "Create attachment",
4269
+ method: HttpMethod.POST,
4270
+ url: `/components/${component}/attach/`,
4271
+ blocks: [offline_id, component],
4272
+ blockers: [file_sha1],
4273
+ payload: {
4274
+ offline_id,
4275
+ component,
4276
+ description: description2 ?? "",
4277
+ submitted_at: (/* @__PURE__ */ new Date()).getTime() / 1e3,
4278
+ ...fileProps
4279
+ }
4280
+ });
4281
+ promise.catch((error2) => {
4282
+ this.client.store.dispatch(removeComponentAttachment(offlineAttachment.offline_id));
4283
+ throw error2;
4284
+ });
4285
+ return [offlineAttachment, promise];
4138
4286
  }
4139
- async replaceFile(attachmentId, newFile) {
4287
+ async addComponentTypeAttachment(attachmentPayload) {
4288
+ const { description: description2, component_type, file_sha1, offline_id } = attachmentPayload;
4289
+ if (!attachmentPayload.file.objectURL) {
4290
+ throw new Error("Expected attachmentPayload.file.objectURL to be defined.");
4291
+ }
4292
+ const offlineAttachment = {
4293
+ ...attachmentPayload,
4294
+ file: attachmentPayload.file.objectURL,
4295
+ file_name: attachmentPayload.file.name,
4296
+ file_type: attachmentPayload.file.type
4297
+ };
4298
+ await this.client.files.addCache(attachmentPayload.file, file_sha1);
4299
+ this.client.store.dispatch(addComponentTypeAttachment(offlineAttachment));
4300
+ const [fileProps] = await this.client.files.uploadFileToS3(file_sha1);
4301
+ const promise = this.enqueueRequest({
4302
+ description: "Create attachment",
4303
+ method: HttpMethod.POST,
4304
+ url: `/components/types/${component_type}/attach/`,
4305
+ blocks: [offline_id, component_type],
4306
+ blockers: [file_sha1],
4307
+ payload: {
4308
+ offline_id,
4309
+ component_type,
4310
+ description: description2 ?? "",
4311
+ submitted_at: (/* @__PURE__ */ new Date()).getTime() / 1e3,
4312
+ ...fileProps
4313
+ }
4314
+ });
4315
+ promise.catch((error2) => {
4316
+ this.client.store.dispatch(removeComponentTypeAttachment(offlineAttachment.offline_id));
4317
+ throw error2;
4318
+ });
4319
+ return [offlineAttachment, promise];
4320
+ }
4321
+ /** the outer Promise is needed to await the hashing of each file, which is required before offline use. If wanting to
4322
+ * attach promise handlers to the request to add the attachment in the backend, apply it on the promise returned from the
4323
+ * OptimisticModelResult. */
4324
+ attachFilesToIssue(filesToSubmit, issueId) {
4325
+ return filesToSubmit.map((file) => {
4326
+ if (!(file instanceof File)) {
4327
+ throw new Error("Expected a File instance.");
4328
+ }
4329
+ const photoAttachmentPromise = async (file2) => {
4330
+ const hash = await hashFile(file2);
4331
+ const attachment = offline({
4332
+ file: file2,
4333
+ file_name: file2.name,
4334
+ file_type: file2.type,
4335
+ issue: issueId,
4336
+ file_sha1: hash
4337
+ });
4338
+ return this.addIssueAttachment(attachment);
4339
+ };
4340
+ return photoAttachmentPromise(file);
4341
+ });
4342
+ }
4343
+ attachFilesToComponent(filesToSubmit, componentId) {
4344
+ return filesToSubmit.map((file) => {
4345
+ if (!(file instanceof File)) {
4346
+ throw new Error("Expected a File instance.");
4347
+ }
4348
+ const photoAttachmentPromise = async (file2) => {
4349
+ const hash = await hashFile(file2);
4350
+ const attachment = offline({
4351
+ file: file2,
4352
+ file_name: file2.name,
4353
+ file_type: file2.type,
4354
+ component: componentId,
4355
+ file_sha1: hash
4356
+ });
4357
+ return this.addComponentAttachment(attachment);
4358
+ };
4359
+ return photoAttachmentPromise(file);
4360
+ });
4361
+ }
4362
+ attachFilesToComponentType(filesToSubmit, componentTypeId) {
4363
+ return filesToSubmit.map((file) => {
4364
+ if (!(file instanceof File)) {
4365
+ throw new Error("Expected a File instance.");
4366
+ }
4367
+ const photoAttachmentPromise = async (file2) => {
4368
+ const hash = await hashFile(file2);
4369
+ const attachment = offline({
4370
+ file: file2,
4371
+ file_name: file2.name,
4372
+ file_type: file2.type,
4373
+ component_type: componentTypeId,
4374
+ file_sha1: hash
4375
+ });
4376
+ return this.addComponentTypeAttachment(attachment);
4377
+ };
4378
+ return photoAttachmentPromise(file);
4379
+ });
4380
+ }
4381
+ async replaceIssueAttachmentFile(attachmentId, newFile) {
4140
4382
  const { store } = this.client;
4141
4383
  const attachment = store.getState().issueReducer.attachments[attachmentId];
4142
4384
  if (!attachment)
@@ -4151,16 +4393,75 @@ class AttachmentService extends BaseApiService {
4151
4393
  if (!newFile.objectURL) {
4152
4394
  throw new Error(`newFile["objectURL"] is unexpectedly ${newFile.objectURL}`);
4153
4395
  }
4154
- store.dispatch(updateAttachment({ ...attachment, file_sha1: newSha1, file: URL.createObjectURL(newFile) }));
4396
+ store.dispatch(
4397
+ updateIssueAttachment({ ...attachment, file_sha1: newSha1, file: URL.createObjectURL(newFile) })
4398
+ );
4399
+ await this.client.files.addCache(newFile, newSha1);
4400
+ const [fileProps] = await this.client.files.uploadFileToS3(newSha1).catch((e) => {
4401
+ store.dispatch(updateIssueAttachment(attachment));
4402
+ throw e;
4403
+ });
4404
+ const promise2 = this.enqueueRequest({
4405
+ description: "Edit attachment",
4406
+ method: HttpMethod.PATCH,
4407
+ url: `/attachments/issues/${attachment.offline_id}/`,
4408
+ isResponseBlob: false,
4409
+ payload: fileProps,
4410
+ blockers: [attachmentId, newSha1],
4411
+ blocks: [attachmentId, newSha1]
4412
+ });
4413
+ try {
4414
+ const result = await promise2;
4415
+ void this.client.files.removeCache(attachment.file_sha1);
4416
+ return result;
4417
+ } catch (e) {
4418
+ if (oldFile) {
4419
+ store.dispatch(
4420
+ updateIssueAttachment({
4421
+ ...attachment,
4422
+ file_sha1: attachment.file_sha1,
4423
+ file: URL.createObjectURL(oldFile)
4424
+ })
4425
+ );
4426
+ }
4427
+ throw e;
4428
+ }
4429
+ };
4430
+ const offlineAttachment = {
4431
+ ...attachment,
4432
+ file_sha1: newSha1,
4433
+ file: URL.createObjectURL(newFile)
4434
+ };
4435
+ const promise = performRequest2();
4436
+ return [offlineAttachment, promise];
4437
+ }
4438
+ async replaceComponentAttachmentFile(attachmentId, newFile) {
4439
+ const { store } = this.client;
4440
+ const attachment = store.getState().componentReducer.attachments[attachmentId];
4441
+ if (!attachment)
4442
+ throw new Error(`Attachment ${attachmentId} not found`);
4443
+ let oldFile = void 0;
4444
+ const newSha1 = await hashFile(newFile);
4445
+ const performRequest2 = async () => {
4446
+ oldFile = await this.client.files.fetchCache(attachment.file_sha1);
4447
+ if (!oldFile) {
4448
+ console.error(`Failed to fetch old file from cache for sha1 ${attachment.file_sha1}.`);
4449
+ }
4450
+ if (!newFile.objectURL) {
4451
+ throw new Error(`newFile["objectURL"] is unexpectedly ${newFile.objectURL}`);
4452
+ }
4453
+ store.dispatch(
4454
+ updateComponentAttachment({ ...attachment, file_sha1: newSha1, file: URL.createObjectURL(newFile) })
4455
+ );
4155
4456
  await this.client.files.addCache(newFile, newSha1);
4156
4457
  const [fileProps] = await this.client.files.uploadFileToS3(newSha1).catch((e) => {
4157
- store.dispatch(updateAttachment(attachment));
4458
+ store.dispatch(updateComponentAttachment(attachment));
4158
4459
  throw e;
4159
4460
  });
4160
4461
  const promise2 = this.enqueueRequest({
4161
4462
  description: "Edit attachment",
4162
4463
  method: HttpMethod.PATCH,
4163
- url: `/attachments/${attachment.offline_id}/`,
4464
+ url: `/attachments/components/${attachment.offline_id}/`,
4164
4465
  isResponseBlob: false,
4165
4466
  payload: fileProps,
4166
4467
  blockers: [attachmentId, newSha1],
@@ -4173,7 +4474,68 @@ class AttachmentService extends BaseApiService {
4173
4474
  } catch (e) {
4174
4475
  if (oldFile) {
4175
4476
  store.dispatch(
4176
- updateAttachment({
4477
+ updateComponentAttachment({
4478
+ ...attachment,
4479
+ file_sha1: attachment.file_sha1,
4480
+ file: URL.createObjectURL(oldFile)
4481
+ })
4482
+ );
4483
+ }
4484
+ throw e;
4485
+ }
4486
+ };
4487
+ const offlineAttachment = {
4488
+ ...attachment,
4489
+ file_sha1: newSha1,
4490
+ file: URL.createObjectURL(newFile)
4491
+ };
4492
+ const promise = performRequest2();
4493
+ return [offlineAttachment, promise];
4494
+ }
4495
+ async replaceComponentTypeAttachmentFile(attachmentId, newFile) {
4496
+ const { store } = this.client;
4497
+ const attachment = store.getState().componentTypeReducer.attachments[attachmentId];
4498
+ if (!attachment)
4499
+ throw new Error(`Attachment ${attachmentId} not found`);
4500
+ let oldFile = void 0;
4501
+ const newSha1 = await hashFile(newFile);
4502
+ const performRequest2 = async () => {
4503
+ oldFile = await this.client.files.fetchCache(attachment.file_sha1);
4504
+ if (!oldFile) {
4505
+ console.error(`Failed to fetch old file from cache for sha1 ${attachment.file_sha1}.`);
4506
+ }
4507
+ if (!newFile.objectURL) {
4508
+ throw new Error(`newFile["objectURL"] is unexpectedly ${newFile.objectURL}`);
4509
+ }
4510
+ store.dispatch(
4511
+ updateComponentTypeAttachment({
4512
+ ...attachment,
4513
+ file_sha1: newSha1,
4514
+ file: URL.createObjectURL(newFile)
4515
+ })
4516
+ );
4517
+ await this.client.files.addCache(newFile, newSha1);
4518
+ const [fileProps] = await this.client.files.uploadFileToS3(newSha1).catch((e) => {
4519
+ store.dispatch(updateComponentTypeAttachment(attachment));
4520
+ throw e;
4521
+ });
4522
+ const promise2 = this.enqueueRequest({
4523
+ description: "Edit attachment",
4524
+ method: HttpMethod.PATCH,
4525
+ url: `/attachments/component_types/${attachment.offline_id}/`,
4526
+ isResponseBlob: false,
4527
+ payload: fileProps,
4528
+ blockers: [attachmentId, newSha1],
4529
+ blocks: [attachmentId, newSha1]
4530
+ });
4531
+ try {
4532
+ const result = await promise2;
4533
+ void this.client.files.removeCache(attachment.file_sha1);
4534
+ return result;
4535
+ } catch (e) {
4536
+ if (oldFile) {
4537
+ store.dispatch(
4538
+ updateComponentTypeAttachment({
4177
4539
  ...attachment,
4178
4540
  file_sha1: attachment.file_sha1,
4179
4541
  file: URL.createObjectURL(oldFile)
@@ -4193,23 +4555,54 @@ class AttachmentService extends BaseApiService {
4193
4555
  }
4194
4556
  /**
4195
4557
  * Deletes an attachment and associated data in the cloud, in the Redux store and the cache.
4196
- * @param attachmentId
4558
+ * @param issueAttachmentId
4197
4559
  */
4198
- delete(attachmentId) {
4560
+ deleteIssueAttachment(issueAttachmentId) {
4199
4561
  const { store } = this.client;
4200
- const storeStateIssueReducer = store.getState().issueReducer;
4201
- const attachment = storeStateIssueReducer.attachments[attachmentId];
4562
+ const attachment = selectIssueAttachmentMapping(store.getState())[issueAttachmentId];
4202
4563
  if (!attachment) {
4203
- throw new Error(`Attachment ${attachmentId} not found`);
4564
+ throw new Error(`Attachment ${issueAttachmentId} not found`);
4204
4565
  }
4205
- store.dispatch(removeAttachment(attachmentId));
4566
+ store.dispatch(removeIssueAttachment(issueAttachmentId));
4206
4567
  void this.client.files.removeCache(attachment.file_sha1);
4207
4568
  return this.enqueueRequest({
4208
4569
  description: "Delete attachment",
4209
4570
  method: HttpMethod.DELETE,
4210
- url: `/attachments/${attachmentId}/`,
4211
- blockers: [attachmentId],
4212
- blocks: [attachmentId]
4571
+ url: `/attachments/issues/${issueAttachmentId}/`,
4572
+ blockers: [issueAttachmentId],
4573
+ blocks: [issueAttachmentId]
4574
+ });
4575
+ }
4576
+ deleteComponentAttachment(componentAttachmentId) {
4577
+ const { store } = this.client;
4578
+ const attachment = selectComponentAttachmentMapping(store.getState())[componentAttachmentId];
4579
+ if (!attachment) {
4580
+ throw new Error(`Attachment ${componentAttachmentId} not found`);
4581
+ }
4582
+ store.dispatch(removeComponentAttachment(componentAttachmentId));
4583
+ void this.client.files.removeCache(attachment.file_sha1);
4584
+ return this.enqueueRequest({
4585
+ description: "Delete attachment",
4586
+ method: HttpMethod.DELETE,
4587
+ url: `/attachments/components/${componentAttachmentId}/`,
4588
+ blockers: [componentAttachmentId],
4589
+ blocks: [componentAttachmentId]
4590
+ });
4591
+ }
4592
+ deleteComponentTypeAttachment(componentTypeAttachmentId) {
4593
+ const { store } = this.client;
4594
+ const attachment = selectComponentTypeAttachmentMapping(store.getState())[componentTypeAttachmentId];
4595
+ if (!attachment) {
4596
+ throw new Error(`Attachment ${componentTypeAttachmentId} not found`);
4597
+ }
4598
+ store.dispatch(removeComponentTypeAttachment(componentTypeAttachmentId));
4599
+ void this.client.files.removeCache(attachment.file_sha1);
4600
+ return this.enqueueRequest({
4601
+ description: "Delete attachment",
4602
+ method: HttpMethod.DELETE,
4603
+ url: `/attachments/component_types/${componentTypeAttachmentId}/`,
4604
+ blockers: [componentTypeAttachmentId],
4605
+ blocks: [componentTypeAttachmentId]
4213
4606
  });
4214
4607
  }
4215
4608
  }
@@ -4601,13 +4994,26 @@ class ComponentService extends BaseApiService {
4601
4994
  return [component, promise];
4602
4995
  }
4603
4996
  async remove(id) {
4604
- this.client.store.dispatch(removeComponent(id));
4997
+ const { store } = this.client;
4998
+ const backupComponent = selectComponent(id)(store.getState());
4999
+ if (!backupComponent)
5000
+ throw new Error(`No component with id ${id} found in the store`);
5001
+ const attachmentsOfComponent = selectAttachmentsOfComponent(id)(store.getState());
5002
+ store.dispatch(removeComponent(id));
5003
+ if (attachmentsOfComponent.length > 0) {
5004
+ const attachmentsOfComponentIds = attachmentsOfComponent.map(({ offline_id }) => offline_id);
5005
+ store.dispatch(removeComponentAttachments(attachmentsOfComponentIds));
5006
+ }
4605
5007
  return this.enqueueRequest({
4606
5008
  description: "Delete issue",
4607
5009
  method: HttpMethod.DELETE,
4608
5010
  url: `/components/${id}/`,
4609
5011
  blockers: [id],
4610
5012
  blocks: []
5013
+ }).catch((err) => {
5014
+ store.dispatch(addComponent(backupComponent));
5015
+ store.dispatch(addComponentAttachments(attachmentsOfComponent));
5016
+ throw err;
4611
5017
  });
4612
5018
  }
4613
5019
  async deleteAllByComponentType(componentTypeId) {
@@ -4918,13 +5324,19 @@ class ComponentTypeService extends BaseApiService {
4918
5324
  if (!componentType) {
4919
5325
  throw new Error("Expected componentType to exist");
4920
5326
  }
4921
- const componentTypeStages = selectStagesFromComponentType(componentTypeId)(state) ?? [];
4922
- store.dispatch(
4923
- removeStages(
4924
- componentTypeStages.map((componentTypeStage) => componentTypeStage.offline_id)
4925
- )
4926
- );
5327
+ const stagesOfComponentType = selectStagesFromComponentType(componentTypeId)(state) ?? [];
5328
+ const attachmentsOfComponentType = selectAttachmentsOfComponentType(componentTypeId)(state);
4927
5329
  store.dispatch(deleteComponentType(componentTypeId));
5330
+ if (stagesOfComponentType.length > 0) {
5331
+ const stagesOfComponentTypeIds = stagesOfComponentType.map(
5332
+ (componentTypeStage) => componentTypeStage.offline_id
5333
+ );
5334
+ store.dispatch(removeStages(stagesOfComponentTypeIds));
5335
+ }
5336
+ if (attachmentsOfComponentType.length > 0) {
5337
+ const attachmentsOfComponentTypeIds = attachmentsOfComponentType.map(({ offline_id }) => offline_id);
5338
+ store.dispatch(removeComponentTypeAttachments(attachmentsOfComponentTypeIds));
5339
+ }
4928
5340
  return this.enqueueRequest({
4929
5341
  description: "Delete ComponentType",
4930
5342
  method: HttpMethod.DELETE,
@@ -4933,7 +5345,8 @@ class ComponentTypeService extends BaseApiService {
4933
5345
  blocks: []
4934
5346
  }).catch((e) => {
4935
5347
  store.dispatch(addComponentType(componentType));
4936
- store.dispatch(addStages(componentTypeStages));
5348
+ store.dispatch(addStages(stagesOfComponentType));
5349
+ store.dispatch(addComponentTypeAttachments(attachmentsOfComponentType));
4937
5350
  throw e;
4938
5351
  });
4939
5352
  }
@@ -5111,12 +5524,12 @@ class IssueService extends BaseApiService {
5111
5524
  if (!backup) {
5112
5525
  throw new Error(`No issue with id ${id} found in the store`);
5113
5526
  }
5114
- const attachments = Object.values(state.issueReducer.attachments).filter((a) => a.issue_id === id);
5115
- const attachmentsOfIssue = selectPhotoAttachmentsOfIssue(id)(state);
5116
- store.dispatch(removeIssue(id));
5527
+ const attachments = Object.values(state.issueReducer.attachments).filter((a) => a.issue === id);
5528
+ const attachmentsOfIssue = selectAttachmentsOfIssue(id)(state);
5529
+ this.client.store.dispatch(removeIssue(id));
5117
5530
  store.dispatch(addActiveProjectIssuesCount(-1));
5118
- if (attachmentsOfIssue) {
5119
- store.dispatch(removeAttachmentsOfIssue(id));
5531
+ if (attachmentsOfIssue.length > 0) {
5532
+ this.client.store.dispatch(removeAttachmentsOfIssue(id));
5120
5533
  }
5121
5534
  try {
5122
5535
  return await this.enqueueRequest({
@@ -5127,8 +5540,8 @@ class IssueService extends BaseApiService {
5127
5540
  blocks: []
5128
5541
  });
5129
5542
  } catch (e) {
5130
- store.dispatch(addIssue(backup));
5131
- store.dispatch(addAttachments(attachments));
5543
+ this.client.store.dispatch(addIssue(backup));
5544
+ this.client.store.dispatch(addIssueAttachments(attachments));
5132
5545
  store.dispatch(addActiveProjectIssuesCount(1));
5133
5546
  throw e;
5134
5547
  }
@@ -5304,7 +5717,10 @@ class MainService extends BaseApiService {
5304
5717
  if (currentProjectId) {
5305
5718
  const [_offlineAttachments, promise] = this.client.attachments.fetchAll(currentProjectId);
5306
5719
  void promise.then((result) => {
5307
- store.dispatch(setAttachments(result));
5720
+ const { issue_attachments, component_type_attachments, component_attachments } = result;
5721
+ store.dispatch(setIssueAttachments(issue_attachments));
5722
+ store.dispatch(setComponentAttachments(component_attachments));
5723
+ store.dispatch(setComponentTypeAttachments(component_type_attachments));
5308
5724
  });
5309
5725
  }
5310
5726
  store.dispatch(setIsFetchingInitialData(false));
@@ -6282,17 +6698,22 @@ class FileService extends BaseApiService {
6282
6698
  let promise = cachedRequestPromises[requestCacheKey];
6283
6699
  let isFirstRequest = true;
6284
6700
  if (!promise) {
6285
- promise = this.enqueueRequest({
6286
- description: "Download file",
6287
- method: HttpMethod.GET,
6288
- url,
6289
- // If in development, we should assume the files are saved at localhost by the Django development server.
6290
- // Setting this to true will lead to localhost:8000 being prepended to the URL.
6291
- isExternalUrl: true,
6292
- isResponseBlob: true,
6293
- isAuthNeeded: false,
6294
- blockers: [expectedSha1],
6295
- blocks: [expectedSha1]
6701
+ promise = new Promise((resolve) => {
6702
+ void this.enqueueRequest({
6703
+ description: "Download file",
6704
+ method: HttpMethod.GET,
6705
+ url,
6706
+ // If in development, we should assume the files are saved at localhost by the Django development server.
6707
+ // Setting this to true will lead to localhost:8000 being prepended to the URL.
6708
+ isExternalUrl: true,
6709
+ isResponseBlob: true,
6710
+ isAuthNeeded: false,
6711
+ blockers: [expectedSha1],
6712
+ blocks: [expectedSha1]
6713
+ }).then((blob) => {
6714
+ const blobToFile = new File([blob], downloadedName ?? expectedSha1, { type: blob.type });
6715
+ resolve(blobToFile);
6716
+ });
6296
6717
  });
6297
6718
  cachedRequestPromises[requestCacheKey] = promise;
6298
6719
  } else {
@@ -6698,8 +7119,8 @@ class BaseFormElement {
6698
7119
  }
6699
7120
  }
6700
7121
  const emptyBaseField = {
6701
- label: "Question",
6702
- description: "Optional description",
7122
+ label: "",
7123
+ description: "",
6703
7124
  required: false
6704
7125
  };
6705
7126
  class BaseField extends BaseFormElement {
@@ -12402,7 +12823,7 @@ const ImageCard = memo((props) => {
12402
12823
  gap: "0",
12403
12824
  ...rest,
12404
12825
  children: [
12405
- !file && /* @__PURE__ */ jsx(Flex, { width: "100%", height: "100%", align: "center", justify: "center", position: "absolute", children: /* @__PURE__ */ jsx(Spinner, {}) }),
12826
+ !file && !error2 && /* @__PURE__ */ jsx(Flex, { width: "100%", height: "100%", align: "center", justify: "center", position: "absolute", children: /* @__PURE__ */ jsx(Spinner, {}) }),
12406
12827
  /* @__PURE__ */ jsx(Inset, { className: styles$4.ImageInset, ref: imageInsetRef, clip: "padding-box", side: "y", pb: "0", children: file && !error2 && /* @__PURE__ */ jsx("img", { className: styles$4.Image, src: URL.createObjectURL(file), alt: alt ?? file.name }) }),
12407
12828
  /* @__PURE__ */ jsx(
12408
12829
  OvermapItem,
@@ -13495,14 +13916,16 @@ const FieldActions = memo((props) => {
13495
13916
  key: "duplicate",
13496
13917
  text: "Duplicate",
13497
13918
  buttonProps: { onClick: duplicate }
13498
- },
13499
- {
13919
+ }
13920
+ ];
13921
+ if (index2 === 0) {
13922
+ actions2.push({
13500
13923
  Icon: TrashIcon,
13501
13924
  key: "delete",
13502
13925
  text: "Delete",
13503
13926
  buttonProps: { onClick: remove2 }
13504
- }
13505
- ];
13927
+ });
13928
+ }
13506
13929
  if (type !== "section") {
13507
13930
  actions2.unshift({
13508
13931
  Icon: ImageIcon,
@@ -14676,15 +15099,19 @@ export {
14676
15099
  acceptProjectInvite,
14677
15100
  addActiveProjectFormSubmissionsCount,
14678
15101
  addActiveProjectIssuesCount,
14679
- addAttachment,
14680
- addAttachments,
14681
15102
  addCategory,
14682
15103
  addComponent,
15104
+ addComponentAttachment,
15105
+ addComponentAttachments,
14683
15106
  addComponentType,
15107
+ addComponentTypeAttachment,
15108
+ addComponentTypeAttachments,
14684
15109
  addComponentsInBatches,
14685
15110
  addEmailDomain,
14686
15111
  addFavouriteProjectId,
14687
15112
  addIssue,
15113
+ addIssueAttachment,
15114
+ addIssueAttachments,
14688
15115
  addLicenses,
14689
15116
  addOrReplaceCategories,
14690
15117
  addOrReplaceIssueComment,
@@ -14825,14 +15252,18 @@ export {
14825
15252
  rehydratedReducer,
14826
15253
  rehydratedSlice,
14827
15254
  removeAllComponentsOfType,
14828
- removeAttachment,
14829
15255
  removeAttachmentsOfIssue,
14830
15256
  removeCategory,
14831
15257
  removeColor,
14832
15258
  removeComponent,
15259
+ removeComponentAttachment,
15260
+ removeComponentAttachments,
15261
+ removeComponentTypeAttachment,
15262
+ removeComponentTypeAttachments,
14833
15263
  removeEmailDomain,
14834
15264
  removeFavouriteProjectId,
14835
15265
  removeIssue,
15266
+ removeIssueAttachment,
14836
15267
  removeIssueComment,
14837
15268
  removeOrganizationAccess,
14838
15269
  removeProjectAccess,
@@ -14868,7 +15299,15 @@ export {
14868
15299
  selectActiveWorkspace,
14869
15300
  selectActiveWorkspaceId,
14870
15301
  selectAllAttachments,
15302
+ selectAllComponentAttachments,
15303
+ selectAllComponentTypeAttachments,
14871
15304
  selectAppearance,
15305
+ selectAttachmentsOfComponent,
15306
+ selectAttachmentsOfComponentByType,
15307
+ selectAttachmentsOfComponentType,
15308
+ selectAttachmentsOfComponentTypeByType,
15309
+ selectAttachmentsOfIssue,
15310
+ selectAttachmentsOfIssueByType,
14872
15311
  selectCategories,
14873
15312
  selectCategoriesOfWorkspace,
14874
15313
  selectCategory,
@@ -14880,7 +15319,9 @@ export {
14880
15319
  selectCompletedStageIdsForComponent,
14881
15320
  selectCompletedStages,
14882
15321
  selectComponent,
15322
+ selectComponentAttachmentMapping,
14883
15323
  selectComponentType,
15324
+ selectComponentTypeAttachmentMapping,
14884
15325
  selectComponentTypeForm,
14885
15326
  selectComponentTypeFromComponent,
14886
15327
  selectComponentTypeFromComponents,
@@ -14992,9 +15433,10 @@ export {
14992
15433
  setActiveProjectId,
14993
15434
  setActiveWorkspaceId,
14994
15435
  setAppearance,
14995
- setAttachments,
14996
15436
  setCategories,
14997
15437
  setCenterMapToProject,
15438
+ setComponentAttachments,
15439
+ setComponentTypeAttachments,
14998
15440
  setComponentTypes,
14999
15441
  setComponents,
15000
15442
  setCreateProjectType,
@@ -15006,6 +15448,7 @@ export {
15006
15448
  setIsFetchingInitialData,
15007
15449
  setIsImportingProjectFile,
15008
15450
  setIsLoading,
15451
+ setIssueAttachments,
15009
15452
  setIssueComments,
15010
15453
  setIssues,
15011
15454
  setLicenses,
@@ -15045,9 +15488,11 @@ export {
15045
15488
  unhideCategory,
15046
15489
  unlinkStageToForm,
15047
15490
  updateActiveOrganization,
15048
- updateAttachment,
15049
15491
  updateComponent,
15492
+ updateComponentAttachment,
15493
+ updateComponentTypeAttachment,
15050
15494
  updateIssue,
15495
+ updateIssueAttachment,
15051
15496
  updateLicense,
15052
15497
  updateOrCreateProject,
15053
15498
  updateOrCreateUserFormSubmission,