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

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.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { A as RadioFormField, B as isEmailFormField, C as FormNotificationItem, D as HiddenFormField, E as FormSubmission, F as TextareaFormField, G as isSelectFormField, H as isFormField, I as TimeFormField, K as isTextFormField, L as UrlFormField, M as SelectFormField, N as SubmissionDocument, O as NumberFormField, P as TextFormField, R as ValidationRules, S as FormNotification, T as FormSettings, U as isFormFieldType, V as isFileFormField, W as isKnownFormField, _ as FormField, a as ConditionalLogic, b as FormHookContext, c as DateFormField, d as FieldBlockConfig, f as FileFormField, g as FormDocument, h as FormConfig, i as CheckboxFormField, j as ResolvedFormBuilderConfig, k as PhoneFormField, l as EmailConfig, m as FormBuilderPluginOptions, n as BUILT_IN_FORM_FIELD_TYPES, o as ConditionalLogicCondition, p as FormAccess, r as BaseFormField, s as CustomFormField, t as AnyFormField, u as EmailFormField, v as FormFieldType, w as FormNotifications, x as FormHooks, y as FormFieldTypeId, z as isCheckboxFormField } from "./types-BHReIA0j.js";
1
+ import { A as HiddenFormField, B as UrlFormField, C as FormHookContext, D as FormNotifications, E as FormNotificationItem, F as SelectFormField, G as isFormField, H as isCheckboxFormField, I as SubmissionDocument, J as isSelectFormField, K as isFormFieldType, L as TextFormField, M as PhoneFormField, N as RadioFormField, O as FormSettings, P as ResolvedFormBuilderConfig, R as TextareaFormField, S as FormFieldTypeId, T as FormNotification, U as isEmailFormField, V as ValidationRules, W as isFileFormField, Y as isTextFormField, _ as FormBuilderPluginOptions, a as BUILT_IN_FORM_FIELD_TYPES, b as FormField, c as ConditionalLogic, d as DateFormField, f as EmailConfig, g as FormAccess, h as FileFormField, i as AnyFormField, j as NumberFormField, k as FormSubmission, l as ConditionalLogicCondition, m as FieldBlockConfig, n as NewSubmissionRow, o as BaseFormField, p as EmailFormField, q as isKnownFormField, r as NewSubmissionsData, s as CheckboxFormField, t as NEW_SUBMISSIONS_WIDGET_ID, u as CustomFormField, v as FormConfig, w as FormHooks, x as FormFieldType, y as FormDocument, z as TimeFormField } from "./new-submissions-CzlhVGhH.js";
2
2
  import { PluginDefinition } from "@nextlyhq/plugin-sdk";
3
3
  import { CollectionConfig, PluginContext } from "nextly";
4
4
  import { z } from "zod";
@@ -1385,5 +1385,5 @@ interface ExtendedCollectionConfig extends Omit<CollectionConfig, "admin"> {
1385
1385
  */
1386
1386
  export declare function submissionsCollection(pluginConfig: ResolvedFormBuilderConfig): ExtendedCollectionConfig;
1387
1387
  export declare const formBuilderPlugin: ReturnType<typeof formBuilder>["plugin"];
1388
- export { type AnyFormField, BUILT_IN_FORM_FIELD_TYPES, type BaseFormField, type CSVExportOptions, type CheckboxFormField, type ComparisonOperator, type ConditionalLogic, type ConditionalLogicCondition, type CustomFormField, type DateFormField, type EmailConfig, type EmailFormField, type ExportedJSON, type FieldBlockConfig, type FileFormField, type FormAccess, type FormBuilderPluginOptions, type FormBuilderPluginResult, type FormConfig, type FormDocument, type FormField, type FormFieldOption, type FormFieldType, type FormFieldTypeId, type FormHookContext, type FormHooks, type FormNotification, type FormNotificationItem, type FormNotifications, type FormSettings, type FormSubmission, type FormValidationError, type FormValidationErrorCode, type FormValidationResult, type HiddenFormField, type JSONExportOptions, type NextlyPlugin, type NumberFormField, type PhoneFormField, type RadioFormField, type ResolvedFormBuilderConfig, type SelectFormField, type SpamCheckConfig, type SpamCheckOptions, type SpamCheckResult, type SubmissionDocument, type SubmitFormContext, type SubmitFormOptions, type SubmitFormResult, type TextFormField, type TextareaFormField, type TimeFormField, type UrlFormField, type ValidationRules, isCheckboxFormField, isEmailFormField, isFileFormField, isFormField, isFormFieldType, isKnownFormField, isSelectFormField, isTextFormField };
1388
+ export { type AnyFormField, BUILT_IN_FORM_FIELD_TYPES, type BaseFormField, type CSVExportOptions, type CheckboxFormField, type ComparisonOperator, type ConditionalLogic, type ConditionalLogicCondition, type CustomFormField, type DateFormField, type EmailConfig, type EmailFormField, type ExportedJSON, type FieldBlockConfig, type FileFormField, type FormAccess, type FormBuilderPluginOptions, type FormBuilderPluginResult, type FormConfig, type FormDocument, type FormField, type FormFieldOption, type FormFieldType, type FormFieldTypeId, type FormHookContext, type FormHooks, type FormNotification, type FormNotificationItem, type FormNotifications, type FormSettings, type FormSubmission, type FormValidationError, type FormValidationErrorCode, type FormValidationResult, type HiddenFormField, type JSONExportOptions, NEW_SUBMISSIONS_WIDGET_ID, type NewSubmissionRow, type NewSubmissionsData, type NextlyPlugin, type NumberFormField, type PhoneFormField, type RadioFormField, type ResolvedFormBuilderConfig, type SelectFormField, type SpamCheckConfig, type SpamCheckOptions, type SpamCheckResult, type SubmissionDocument, type SubmitFormContext, type SubmitFormOptions, type SubmitFormResult, type TextFormField, type TextareaFormField, type TimeFormField, type UrlFormField, type ValidationRules, isCheckboxFormField, isEmailFormField, isFileFormField, isFormField, isFormFieldType, isKnownFormField, isSelectFormField, isTextFormField };
1389
1389
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { A as isKnownFormField, D as isFileFormField, E as isEmailFormField, F as pickedDocumentField, M as isTextFormField, N as documentReachability, O as isFormField, P as parseRedirectReference, S as ENFORCED_VALIDATION_RULES, T as isCheckboxFormField, _ as applyFormDefaults, a as exportToCSV, b as toTitleCase, c as generateExportFilename, d as getSupportedComparisonOperators, f as isValidComparisonOperator, g as applyFieldDefaults, h as DEFAULT_NOTIFICATION_SETTINGS, i as exportAndDownload, j as isSelectFormField, k as isFormFieldType, l as evaluateConditions, m as DEFAULT_FORM_SETTINGS, n as parseFieldRef, o as exportToJSON, p as normalizeFormSettings, r as downloadFile, s as formatExportValue, u as evaluateSingleCondition, v as createFormConfig, w as BUILT_IN_FORM_FIELD_TYPES, x as warnIfWebhooksConfigured, y as pluralize } from "./field-references-BshlgMcn.js";
1
+ import { A as isKnownFormField, D as isFileFormField, E as isEmailFormField, F as SCHEMA_ONLY_CONTEXT, I as SCHEMA_ONLY_READ, L as documentReachability, M as isTextFormField, N as NEW_SUBMISSIONS_WIDGET_ID, O as isFormField, P as readNewSubmissions, R as parseRedirectReference, S as ENFORCED_VALIDATION_RULES, T as isCheckboxFormField, _ as applyFormDefaults, a as exportToCSV, b as toTitleCase, c as generateExportFilename, d as getSupportedComparisonOperators, f as isValidComparisonOperator, g as applyFieldDefaults, h as DEFAULT_NOTIFICATION_SETTINGS, i as exportAndDownload, j as isSelectFormField, k as isFormFieldType, l as evaluateConditions, m as DEFAULT_FORM_SETTINGS, n as parseFieldRef, o as exportToJSON, p as normalizeFormSettings, r as downloadFile, s as formatExportValue, u as evaluateSingleCondition, v as createFormConfig, w as BUILT_IN_FORM_FIELD_TYPES, x as warnIfWebhooksConfigured, y as pluralize, z as pickedDocumentField } from "./field-references-CtzGg51W.js";
2
2
  import { createRequire } from "node:module";
3
- import { NextlyError, definePlugin, isLocaleSelector } from "@nextlyhq/plugin-sdk";
3
+ import { NextlyError, definePlugin, defineWidget, isLocaleSelector, rowsThatFit } from "@nextlyhq/plugin-sdk";
4
4
  import { NO_SUCH_FORM, NextlyError as NextlyError$1, RateLimiter, buildPaginatedResponse, checkbox as checkbox$1, clampLimit, date as date$1, formAvailability, group, isPluginFieldTypeOnSurface, json, relationship, resolveRateLimitStore, respondAction, respondDoc, respondList, select as select$1, stripHtmlTags, text as text$1, textarea as textarea$1, trustedClientIp } from "nextly";
5
5
  import { previewUrlFromTemplate } from "nextly/config";
6
6
  import { z } from "zod";
@@ -69,9 +69,9 @@ function applyRedirectPattern(pattern, document) {
69
69
  * the role screen grants it: `read-form-submissions` to read submissions,
70
70
  * `update-forms` to edit a form.
71
71
  *
72
- * A collection's rule is the whole answer for a signed-in user, so "signed in"
73
- * here would admit every staff member whatever their role holds, and a role
74
- * name would refuse a custom role that holds the permission. The rule is given
72
+ * The grant is checked before a collection's rule, and the rule only narrows
73
+ * it, so this check repeats the gate's and keeps the default plain; a role
74
+ * name here would refuse a custom role that holds the permission. The rule is given
75
75
  * the caller's permissions as the role screen spells them, `<action>-<slug>`,
76
76
  * for a session and an API key alike, and a visitor holds none. `collection` is the slug the host installed
77
77
  * the collection under, so a renamed collection is judged by its own
@@ -686,6 +686,19 @@ function formsCollection(pluginConfig) {
686
686
 
687
687
  // src/collections/submissions.ts
688
688
  /**
689
+ * Whether a create keeps the `submittedAt` it names: only one made on the
690
+ * server with no request behind it (`req.http` absent), such as an import
691
+ * restoring old submissions with their dates, and only where it names one.
692
+ *
693
+ * A create a request produced, a visitor's through the public grant above
694
+ * all, is given the server's time whatever it says: the dashboard lists new
695
+ * submissions newest first by this field, and a date a visitor chose (one far
696
+ * in the future) would keep theirs ahead of every real submission.
697
+ */
698
+ function trustedSubmittedAt(context) {
699
+ return context.req?.http === void 0 && Boolean(context.data?.submittedAt);
700
+ }
701
+ /**
689
702
  * Generate the Form Submissions collection configuration.
690
703
  *
691
704
  * Creates a collection for storing form submissions with:
@@ -750,6 +763,7 @@ function submissionsCollection(pluginConfig) {
750
763
  value: "spam"
751
764
  }
752
765
  ],
766
+ index: true,
753
767
  admin: { description: "Track whether this submission has been reviewed" }
754
768
  }),
755
769
  text$1({
@@ -785,6 +799,7 @@ function submissionsCollection(pluginConfig) {
785
799
  name: "submittedAt",
786
800
  label: "Submitted At",
787
801
  required: true,
802
+ index: true,
788
803
  admin: {
789
804
  readOnly: true,
790
805
  description: "When the form was submitted"
@@ -841,7 +856,7 @@ function submissionsCollection(pluginConfig) {
841
856
  hooks: {
842
857
  beforeValidate: [(context) => {
843
858
  const { data, operation } = context;
844
- if (operation === "create" && data && !data.submittedAt) data.submittedAt = /* @__PURE__ */ new Date();
859
+ if (operation === "create" && data && !trustedSubmittedAt(context)) data.submittedAt = /* @__PURE__ */ new Date();
845
860
  return data;
846
861
  }],
847
862
  afterChange: [(context) => {
@@ -853,6 +868,106 @@ function submissionsCollection(pluginConfig) {
853
868
  };
854
869
  }
855
870
 
871
+ // src/dashboard/widget.ts
872
+ /**
873
+ * New form submissions: the form-builder plugin's dashboard widget, declared
874
+ * once with `defineWidget` and contributed under `contributes.dashboard`.
875
+ *
876
+ * Offered to a reader of both the forms and the submissions, and shown while
877
+ * at least one form is theirs to read; its card lists the new submissions of
878
+ * those forms, newest first, and stays on the dashboard with "No new
879
+ * submissions" while there are none. Its summary puts the card's own count in
880
+ * the dashboard's header. Core's default dashboard names it by its id
881
+ * (`NEW_SUBMISSIONS_WIDGET_ID`), so a site without this plugin has no such
882
+ * card and no such part.
883
+ *
884
+ * @module dashboard/widget
885
+ */
886
+ /** The component the card is drawn by, as the admin's component map imports it. */
887
+ const CARD_COMPONENT = "@nextlyhq/plugin-form-builder/admin#NewSubmissionsCard";
888
+ /** A case for the preview page and the add panel's tile. */
889
+ const PREVIEW = {
890
+ items: [{
891
+ id: "preview-1",
892
+ title: "Ana Ruiz",
893
+ form: "Contact",
894
+ excerpt: "Could you send me your price list?",
895
+ at: "2026-01-05T09:30:00.000Z",
896
+ href: "/admin/collections/form-submissions/preview-1"
897
+ }, {
898
+ id: "preview-2",
899
+ title: "sam@example.com",
900
+ form: "Newsletter",
901
+ at: "2026-01-05T08:10:00.000Z",
902
+ href: "/admin/collections/form-submissions/preview-2"
903
+ }],
904
+ total: 2,
905
+ allHref: "/admin/collections/form-submissions",
906
+ collection: "form-submissions"
907
+ };
908
+ /**
909
+ * The widget, over the plugin's collections as it declares them and the
910
+ * submissions slug the admin knows them by once installed.
911
+ *
912
+ * `submissionsPath` is read when a card loads rather than when the widget is
913
+ * declared: the plugin learns the slug a site renamed its collection to only
914
+ * at `init`, after this declaration was made. Every link the card draws, its
915
+ * empty state's among them, is made from it, so none names a slug the site
916
+ * renamed away.
917
+ */
918
+ function newSubmissionsWidget(declared, submissionsPath) {
919
+ return defineWidget({
920
+ id: NEW_SUBMISSIONS_WIDGET_ID,
921
+ title: "New form submissions",
922
+ description: "Form submissions nobody has opened yet, newest first.",
923
+ icon: "Inbox",
924
+ category: "Forms",
925
+ size: {
926
+ default: {
927
+ w: 6,
928
+ h: 3
929
+ },
930
+ max: {
931
+ w: 12,
932
+ h: 8
933
+ }
934
+ },
935
+ access: { collections: [declared.forms, declared.submissions] },
936
+ shows: async ({ services, signal }) => await services.count(declared.forms, {
937
+ status: "all",
938
+ signal
939
+ }) > 0,
940
+ skeleton: "list",
941
+ load: async ({ services, size, signal, scope }) => readNewSubmissions(services, declared, {
942
+ limit: rowsThatFit(size, { density: "default" }),
943
+ signal,
944
+ scope,
945
+ submissionsPath: submissionsPath()
946
+ }),
947
+ view: { component: CARD_COMPONENT },
948
+ isEmpty: () => false,
949
+ preview: [{
950
+ name: "Two new",
951
+ data: PREVIEW
952
+ }],
953
+ summary: {
954
+ label: "new submissions",
955
+ count: async ({ services, signal, scope }) => {
956
+ const { total, atLeast } = await readNewSubmissions(services, declared, {
957
+ limit: 0,
958
+ signal,
959
+ scope,
960
+ submissionsPath: submissionsPath()
961
+ });
962
+ return {
963
+ count: total,
964
+ atLeast: atLeast === true
965
+ };
966
+ }
967
+ }
968
+ });
969
+ }
970
+
856
971
  // src/document-shapes.ts
857
972
  /**
858
973
  * Read a stored row as a submission.
@@ -2376,6 +2491,11 @@ function formBuilder(options = {}) {
2376
2491
  const resolvedConfig = resolveConfig(options);
2377
2492
  const formsCol = formsCollection(resolvedConfig);
2378
2493
  const submissionsCol = submissionsCollection(resolvedConfig);
2494
+ let installedSubmissionsSlug = resolvedConfig.formSubmissionOverrides.slug;
2495
+ const newSubmissions = newSubmissionsWidget({
2496
+ forms: resolvedConfig.formOverrides.slug,
2497
+ submissions: resolvedConfig.formSubmissionOverrides.slug
2498
+ }, () => installedSubmissionsSlug);
2379
2499
  return {
2380
2500
  plugin: definePlugin({
2381
2501
  name: "@nextlyhq/plugin-form-builder",
@@ -2393,6 +2513,11 @@ function formBuilder(options = {}) {
2393
2513
  ],
2394
2514
  contributes: {
2395
2515
  collections: [formsCol, submissionsCol],
2516
+ dashboard: { widgets: [newSubmissions] },
2517
+ publicGrants: [{
2518
+ collection: resolvedConfig.formSubmissionOverrides.slug,
2519
+ action: "create"
2520
+ }],
2396
2521
  permissions: [{
2397
2522
  action: "export",
2398
2523
  resource: "submissions",
@@ -2503,6 +2628,7 @@ function formBuilder(options = {}) {
2503
2628
  async init(nextly) {
2504
2629
  const declaredSubmissionsSlug = resolvedConfig.formSubmissionOverrides.slug;
2505
2630
  const submissionSlug = nextly.self.collections[declaredSubmissionsSlug] ?? declaredSubmissionsSlug;
2631
+ installedSubmissionsSlug = submissionSlug;
2506
2632
  nextly.logger.info("Form Builder plugin initialized", {
2507
2633
  formsCollection: resolvedConfig.formOverrides.slug,
2508
2634
  submissionsCollection: submissionSlug,
@@ -2797,7 +2923,8 @@ async function prepareSubmissionForWrite(context, formsSlug, nextly) {
2797
2923
  })) });
2798
2924
  ctx.data = {
2799
2925
  ...submission,
2800
- data: prepared.data
2926
+ data: prepared.data,
2927
+ ...startingStatus(ctx, marks)
2801
2928
  };
2802
2929
  if (spam) {
2803
2930
  ctx.data.status = "spam";
@@ -2807,6 +2934,21 @@ async function prepareSubmissionForWrite(context, formsSlug, nextly) {
2807
2934
  return ctx.data;
2808
2935
  }
2809
2936
  /**
2937
+ * The status a submission a request created starts with: `new`, whatever the
2938
+ * request named, before the spam verdict may make it `spam`. The collection
2939
+ * grants public create and nothing restricts `status`, so a visitor posting
2940
+ * `read` or `archived` would otherwise file their submission where nobody
2941
+ * looks for one that was never opened.
2942
+ *
2943
+ * A create no request produced (an import, a seed, a job) keeps the status it
2944
+ * names, and so does this plugin's own handler storing content spam as
2945
+ * evidence (`keepAsEvidence`), which chose that status itself. An update is
2946
+ * left as it is.
2947
+ */
2948
+ function startingStatus(ctx, marks) {
2949
+ return ctx.operation === "create" && ctx.req?.http && marks?.keepAsEvidence !== true ? { status: "new" } : {};
2950
+ }
2951
+ /**
2810
2952
  * The fields a submission has to satisfy.
2811
2953
  *
2812
2954
  * The handler that already read this form hands it over rather than have the
@@ -3002,7 +3144,7 @@ async function sendNotificationEmails(input) {
3002
3144
  */
3003
3145
  async function injectSubmissionCount(context, submissionSlug, nextly) {
3004
3146
  const hook = context;
3005
- if (hook.context?.[SCHEMA_ONLY_READ] === true) return;
3147
+ if (hook.context?.["formBuilder.schemaOnlyRead"] === true) return;
3006
3148
  const records = Array.isArray(hook.data) ? hook.data : hook.data ? [hook.data] : [];
3007
3149
  await Promise.all(records.map(async (record) => {
3008
3150
  const form = record;
@@ -3017,16 +3159,11 @@ async function injectSubmissionCount(context, submissionSlug, nextly) {
3017
3159
  }
3018
3160
  }));
3019
3161
  }
3020
- /**
3021
- * Tells the form's `afterRead` hook that this read wants the schema and nothing
3022
- * else, so it can skip work whose only purpose is to be displayed.
3023
- */
3024
- const SCHEMA_ONLY_READ = "formBuilder.schemaOnlyRead";
3025
3162
  async function fetchParentForm(formsSlug, formId, nextly) {
3026
3163
  try {
3027
3164
  return await nextly.services.collections.findEntryById(formsSlug, formId, {
3028
3165
  as: "system",
3029
- context: { [SCHEMA_ONLY_READ]: true }
3166
+ context: { ...SCHEMA_ONLY_CONTEXT }
3030
3167
  });
3031
3168
  } catch (err) {
3032
3169
  if (!NextlyError.isNotFound(err)) throw err;
@@ -3953,5 +4090,5 @@ const hidden = (config) => ({
3953
4090
  */
3954
4091
  const formBuilderPlugin = formBuilder().plugin;
3955
4092
 
3956
- export { BUILT_IN_FORM_FIELD_TYPES, DEFAULT_FORM_SETTINGS, DEFAULT_NOTIFICATION_SETTINGS, RESERVED_FORM_SLUGS, applyFieldDefaults, applyFormDefaults, assertValidFormConfig, checkSpam, checkbox, createFormConfig, date, downloadFile, email, evaluateConditions, exportAndDownload, exportToCSV, exportToJSON, file, formBuilder, formBuilderPlugin, formatExportValue, formsCollection, generateExportFilename, generateZodSchema, getFormBuilderConfig, getFormSubmissionStats, getSupportedComparisonOperators, getValidationErrors, hidden, isCheckboxFormField, isEmailFormField, isFileFormField, isFormAcceptingSubmissions, isFormField, isFormFieldType, isKnownFormField, isSelectFormField, isTextFormField, isValidComparisonOperator, number, option, phone, pluralize, radio, select, submissionsCollection, submitForm, text, textarea, time, toTitleCase, transformFormData, url, validateFormConfig, validateFormData, validateSubmission };
4093
+ export { BUILT_IN_FORM_FIELD_TYPES, DEFAULT_FORM_SETTINGS, DEFAULT_NOTIFICATION_SETTINGS, NEW_SUBMISSIONS_WIDGET_ID, RESERVED_FORM_SLUGS, applyFieldDefaults, applyFormDefaults, assertValidFormConfig, checkSpam, checkbox, createFormConfig, date, downloadFile, email, evaluateConditions, exportAndDownload, exportToCSV, exportToJSON, file, formBuilder, formBuilderPlugin, formatExportValue, formsCollection, generateExportFilename, generateZodSchema, getFormBuilderConfig, getFormSubmissionStats, getSupportedComparisonOperators, getValidationErrors, hidden, isCheckboxFormField, isEmailFormField, isFileFormField, isFormAcceptingSubmissions, isFormField, isFormFieldType, isKnownFormField, isSelectFormField, isTextFormField, isValidComparisonOperator, number, option, phone, pluralize, radio, select, submissionsCollection, submitForm, text, textarea, time, toTitleCase, transformFormData, url, validateFormConfig, validateFormData, validateSubmission };
3957
4094
  //# sourceMappingURL=index.js.map