@nextlyhq/plugin-form-builder 0.0.2-alpha.73 → 0.0.2-alpha.74

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.
@@ -18,6 +18,11 @@ interface FormBuilderViewProps {
18
18
  fields?: unknown[];
19
19
  settings?: Record<string, unknown>;
20
20
  notifications?: unknown[];
21
+ /**
22
+ * The form's version as read, which a save sends back so one made from an
23
+ * older version is refused rather than replacing what someone saved since.
24
+ */
25
+ _rev?: string;
21
26
  };
22
27
  onSave?: (data: unknown) => void;
23
28
  onSuccess?: (entry?: Record<string, unknown>) => void;
@@ -49,6 +54,8 @@ interface SubmissionRow {
49
54
  submittedAt?: string | null;
50
55
  editedAt?: string | null;
51
56
  editedBy?: string | null;
57
+ /** The submission's version as the list read it; see `submissionPatch`. */
58
+ _rev?: string;
52
59
  }
53
60
  interface FormOption {
54
61
  id: string;
@@ -388,6 +388,19 @@ function SubmissionSheet({ submission, form, canUpdate, onClose, onPrev, onNext,
388
388
  *
389
389
  * @module admin/components/submissions/SubmissionsView
390
390
  */
391
+ /**
392
+ * What a change to a submission sends: the change, with the version its row
393
+ * had when the list read it, so a change made from an older copy is refused
394
+ * rather than replacing what someone saved since.
395
+ */
396
+ function submissionPatch(row, changes) {
397
+ return typeof row?._rev === "string" ? {
398
+ ...changes,
399
+ _rev: row._rev
400
+ } : changes;
401
+ }
402
+ /** What a refused change says when someone saved the submission since. */
403
+ const SUBMISSION_CHANGED_SINCE = "Someone changed this submission since the list loaded. The list now shows their change; make yours again.";
391
404
  const STATUS_TABS = [
392
405
  {
393
406
  value: "all",
@@ -469,7 +482,7 @@ function SubmissionsView({ collectionSlug }) {
469
482
  const allForms = [];
470
483
  const formsPageSize = 200;
471
484
  for (let formsPage = 1;; formsPage += 1) {
472
- const formsRes = await fetch(`/admin/api/collections/${resolved.forms}/entries?pageSize=${formsPageSize}&page=${formsPage}`, { credentials: "include" });
485
+ const formsRes = await fetch(`/admin/api/collections/${resolved.forms}/entries?limit=${formsPageSize}&page=${formsPage}`, { credentials: "include" });
473
486
  if (!formsRes.ok) break;
474
487
  const items = (await formsRes.json()).items ?? [];
475
488
  allForms.push(...items.map((item) => ({
@@ -513,7 +526,7 @@ function SubmissionsView({ collectionSlug }) {
513
526
  else where.status = { equals: statusTab };
514
527
  const params = new URLSearchParams({
515
528
  page: String(page),
516
- pageSize: String(PAGE_SIZE$1),
529
+ limit: String(PAGE_SIZE$1),
517
530
  sortBy: "submittedAt",
518
531
  sortOrder: "desc",
519
532
  where: JSON.stringify(where)
@@ -592,14 +605,22 @@ function SubmissionsView({ collectionSlug }) {
592
605
  formNameById,
593
606
  hiddenFieldColumns
594
607
  ]);
608
+ const rowsNow = useRef(rows);
609
+ rowsNow.current = rows;
595
610
  const patchSubmission = useCallback(async (id, body) => {
596
611
  const res = await fetch(`/admin/api/collections/${submissionsSlug}/entries/${id}`, {
597
612
  method: "PATCH",
598
613
  headers: { "Content-Type": "application/json" },
599
614
  credentials: "include",
600
- body: JSON.stringify(body)
615
+ body: JSON.stringify(submissionPatch(rowsNow.current.find((row) => row.id === id), body))
601
616
  });
602
- if (!res.ok) throw new Error(`Update failed (${res.status})`);
617
+ if (!res.ok) {
618
+ if ((await res.json().catch(() => null))?.error?.code === "STALE_VERSION") {
619
+ await fetchRows();
620
+ throw new Error(SUBMISSION_CHANGED_SINCE);
621
+ }
622
+ throw new Error(`Update failed (${res.status})`);
623
+ }
603
624
  await fetchRows();
604
625
  }, [submissionsSlug, fetchRows]);
605
626
  const deleteSubmission = useCallback(async (id) => {
@@ -619,7 +640,7 @@ function SubmissionsView({ collectionSlug }) {
619
640
  onSelect: (target) => void patchSubmission(target.id, {
620
641
  status: "new",
621
642
  spamReason: null
622
- })
643
+ }).catch((reason) => setError(reason instanceof Error ? reason.message : "Update failed."))
623
644
  });
624
645
  if (canDelete) actions.push({
625
646
  id: "delete",
@@ -4407,10 +4428,32 @@ function FormPreview({ fields, formData }) {
4407
4428
  * @module admin/FormBuilderView
4408
4429
  * @since 0.1.0
4409
4430
  */
4431
+ /**
4432
+ * The key a form's version travels under, as the admin reads and sends it.
4433
+ * Spelled here because the plugin reaches the admin only through the SDK,
4434
+ * which does not publish it.
4435
+ */
4436
+ const DOCUMENT_VERSION_KEY = "_rev";
4437
+ /**
4438
+ * What a save sends: an existing form's save names the version the editor's
4439
+ * copy started from, where it has one, so a save from an older copy is refused.
4440
+ */
4441
+ function saveBody(saveData, isCreating, startedFrom) {
4442
+ return isCreating || startedFrom === void 0 ? saveData : {
4443
+ ...saveData,
4444
+ [DOCUMENT_VERSION_KEY]: startedFrom
4445
+ };
4446
+ }
4447
+ /** The version a save answered, or the copy's own where it names none. */
4448
+ function versionAfterSave(saved, previous) {
4449
+ const answered = saved[DOCUMENT_VERSION_KEY];
4450
+ return typeof answered === "string" ? answered : previous;
4451
+ }
4410
4452
  function slugify(text) {
4411
4453
  return text.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
4412
4454
  }
4413
- function FormBuilderViewInner({ isCreating = false, collectionSlug = "forms", entryId, onSave, onSuccess, onCancel, documentLock }) {
4455
+ function FormBuilderViewInner({ isCreating = false, collectionSlug = "forms", entryId, onSave, onSuccess, onCancel, documentLock, version }) {
4456
+ const startedFrom = useRef(version);
4414
4457
  const { fields, activeTab, isDirty, formData, settings, notifications, setActiveTab, addField, selectField, seedNotifications, updateFormData, markAsSaved } = useFormBuilder();
4415
4458
  const queryClient = useQueryClient();
4416
4459
  const [isSaving, setIsSaving] = useState(false);
@@ -4497,11 +4540,12 @@ function FormBuilderViewInner({ isCreating = false, collectionSlug = "forms", en
4497
4540
  method: isCreating ? "POST" : "PATCH",
4498
4541
  headers: { "Content-Type": "application/json" },
4499
4542
  credentials: "include",
4500
- body: JSON.stringify(saveData)
4543
+ body: JSON.stringify(saveBody(saveData, isCreating, startedFrom.current))
4501
4544
  });
4502
4545
  if (!response.ok) throw new Error(saveErrorMessage(await response.json().catch(() => ({})), response.statusText));
4503
4546
  const result = await response.json();
4504
4547
  const savedEntry = result.item ?? result;
4548
+ startedFrom.current = versionAfterSave(savedEntry, startedFrom.current);
4505
4549
  queryClient.invalidateQueries({ queryKey: [
4506
4550
  "entries",
4507
4551
  "list",
@@ -4744,6 +4788,10 @@ function FormBuilderViewInner({ isCreating = false, collectionSlug = "forms", en
4744
4788
  */
4745
4789
  function saveErrorMessage(body, statusText) {
4746
4790
  const error = body?.error;
4791
+ if (error?.code === "STALE_VERSION") {
4792
+ const name = error.data?.changedBy?.name;
4793
+ return `${typeof name === "string" && name.trim() !== "" ? name.trim() : "Someone"} changed this form since you opened it. Reload to see their change; your edits are still here, not saved.`;
4794
+ }
4747
4795
  return (error?.data?.errors ?? []).map((issue) => issue.message).filter((message) => Boolean(message)).join(" ") || error?.message || `Failed to save form: ${statusText}`;
4748
4796
  }
4749
4797
  function FormBuilderView({ id, entryId, collection, collectionSlug, isCreating, initialData, onSave, onSuccess, onCancel, documentLock }) {
@@ -4771,7 +4819,8 @@ function FormBuilderView({ id, entryId, collection, collectionSlug, isCreating,
4771
4819
  onSave,
4772
4820
  onSuccess,
4773
4821
  onCancel,
4774
- documentLock
4822
+ documentLock,
4823
+ version: initialData?._rev
4775
4824
  })
4776
4825
  }, providerKey);
4777
4826
  }
@@ -4808,7 +4857,10 @@ function markReadRequest(collection, id) {
4808
4857
  id: { equals: id },
4809
4858
  status: { equals: "new" }
4810
4859
  },
4811
- data: { status: READ_STATUS }
4860
+ data: {
4861
+ status: READ_STATUS,
4862
+ _overwrite: true
4863
+ }
4812
4864
  })
4813
4865
  }
4814
4866
  };