@frigear-nu/forms 0.0.6 → 0.0.7
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/README.md +16 -1
- package/dist/module.d.mts +148 -8
- package/dist/module.json +1 -1
- package/dist/module.mjs +64 -35
- package/dist/runtime/app/components/FormBuilder.d.vue.ts +26 -0
- package/dist/runtime/app/components/FormBuilder.vue +220 -0
- package/dist/runtime/app/components/FormBuilder.vue.d.ts +26 -0
- package/dist/runtime/app/components/FormBuilderAccess.d.vue.ts +16 -0
- package/dist/runtime/app/components/FormBuilderAccess.vue +404 -0
- package/dist/runtime/app/components/FormBuilderAccess.vue.d.ts +16 -0
- package/dist/runtime/app/components/FormBuilderCaptchaSettings.d.vue.ts +15 -0
- package/dist/runtime/app/components/FormBuilderCaptchaSettings.vue +126 -0
- package/dist/runtime/app/components/FormBuilderCaptchaSettings.vue.d.ts +15 -0
- package/dist/runtime/app/components/FormBuilderCondition.d.vue.ts +16 -0
- package/dist/runtime/app/components/FormBuilderCondition.vue +62 -0
- package/dist/runtime/app/components/FormBuilderCondition.vue.d.ts +16 -0
- package/dist/runtime/app/components/FormBuilderFieldRow.d.vue.ts +27 -0
- package/dist/runtime/app/components/FormBuilderFieldRow.vue +87 -0
- package/dist/runtime/app/components/FormBuilderFieldRow.vue.d.ts +27 -0
- package/dist/runtime/app/components/FormBuilderFieldSettings.d.vue.ts +25 -0
- package/dist/runtime/app/components/FormBuilderFieldSettings.vue +170 -0
- package/dist/runtime/app/components/FormBuilderFieldSettings.vue.d.ts +25 -0
- package/dist/runtime/app/components/FormBuilderInsertRow.d.vue.ts +20 -0
- package/dist/runtime/app/components/FormBuilderInsertRow.vue +51 -0
- package/dist/runtime/app/components/FormBuilderInsertRow.vue.d.ts +20 -0
- package/dist/runtime/app/components/FormBuilderOptions.d.vue.ts +11 -0
- package/dist/runtime/app/components/FormBuilderOptions.vue +81 -0
- package/dist/runtime/app/components/FormBuilderOptions.vue.d.ts +11 -0
- package/dist/runtime/app/components/FormBuilderPhoneSettings.d.vue.ts +11 -0
- package/dist/runtime/app/components/FormBuilderPhoneSettings.vue +63 -0
- package/dist/runtime/app/components/FormBuilderPhoneSettings.vue.d.ts +11 -0
- package/dist/runtime/app/components/FormBuilderSettings.d.vue.ts +11 -0
- package/dist/runtime/app/components/FormBuilderSettings.vue +233 -0
- package/dist/runtime/app/components/FormBuilderSettings.vue.d.ts +11 -0
- package/dist/runtime/app/components/FormBuilderStepHeader.d.vue.ts +27 -0
- package/dist/runtime/app/components/FormBuilderStepHeader.vue +71 -0
- package/dist/runtime/app/components/FormBuilderStepHeader.vue.d.ts +27 -0
- package/dist/runtime/app/components/FormBuilderStepSettings.d.vue.ts +13 -0
- package/dist/runtime/app/components/FormBuilderStepSettings.vue +33 -0
- package/dist/runtime/app/components/FormBuilderStepSettings.vue.d.ts +13 -0
- package/dist/runtime/app/components/FormBuilderText.d.vue.ts +19 -0
- package/dist/runtime/app/components/FormBuilderText.vue +127 -0
- package/dist/runtime/app/components/FormBuilderText.vue.d.ts +19 -0
- package/dist/runtime/app/components/FormCaptcha.d.vue.ts +19 -0
- package/dist/runtime/app/components/FormCaptcha.vue +95 -0
- package/dist/runtime/app/components/FormCaptcha.vue.d.ts +19 -0
- package/dist/runtime/app/components/FormEmbed.d.vue.ts +17 -0
- package/dist/runtime/app/components/FormEmbed.vue +113 -0
- package/dist/runtime/app/components/FormEmbed.vue.d.ts +17 -0
- package/dist/runtime/app/components/FormField.d.vue.ts +4 -0
- package/dist/runtime/app/components/FormField.vue +35 -3
- package/dist/runtime/app/components/FormField.vue.d.ts +4 -0
- package/dist/runtime/app/components/FormPhoneInput.d.vue.ts +20 -0
- package/dist/runtime/app/components/FormPhoneInput.vue +56 -0
- package/dist/runtime/app/components/FormPhoneInput.vue.d.ts +20 -0
- package/dist/runtime/app/components/FormPreview.d.vue.ts +12 -0
- package/dist/runtime/app/components/FormPreview.vue +244 -0
- package/dist/runtime/app/components/FormPreview.vue.d.ts +12 -0
- package/dist/runtime/app/components/FormRenderer.d.vue.ts +6 -0
- package/dist/runtime/app/components/FormRenderer.vue +16 -7
- package/dist/runtime/app/components/FormRenderer.vue.d.ts +6 -0
- package/dist/runtime/app/components/FormSubmissionView.vue +2 -2
- package/dist/runtime/app/composables/index.d.ts +4 -1
- package/dist/runtime/app/composables/index.js +4 -1
- package/dist/runtime/app/composables/useCaptcha.d.ts +22 -0
- package/dist/runtime/app/composables/useCaptcha.js +39 -0
- package/dist/runtime/app/composables/useCaptchaSecrets.d.ts +7 -0
- package/dist/runtime/app/composables/useCaptchaSecrets.js +69 -0
- package/dist/runtime/app/composables/useFormBuilderEditor.d.ts +49 -0
- package/dist/runtime/app/composables/useFormBuilderEditor.js +210 -0
- package/dist/runtime/app/composables/useFormBuilderLabels.d.ts +8 -0
- package/dist/runtime/app/composables/useFormBuilderLabels.js +41 -0
- package/dist/runtime/app/composables/useForms.d.ts +18 -3
- package/dist/runtime/app/composables/useForms.js +38 -5
- package/dist/runtime/app/pages/admin-form.vue +112 -69
- package/dist/runtime/app/pages/admin-submission.vue +7 -16
- package/dist/runtime/app/pages/admin-submissions.vue +41 -46
- package/dist/runtime/app/pages/admin.vue +128 -44
- package/dist/runtime/app/pages/form.vue +71 -5
- package/dist/runtime/i18n/lang/da.json +231 -3
- package/dist/runtime/i18n/lang/en.json +231 -3
- package/dist/runtime/menus.js +10 -0
- package/dist/runtime/server/db/schema.sqlite.d.ts +283 -0
- package/dist/runtime/server/db/schema.sqlite.js +33 -1
- package/dist/runtime/server/mcp/schemas.d.ts +9 -0
- package/dist/runtime/server/mcp/schemas.js +8 -0
- package/dist/runtime/server/mcp/tools/admin/create-form.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/create-form.js +21 -0
- package/dist/runtime/server/mcp/tools/admin/get-form-definition.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/get-form-definition.js +14 -0
- package/dist/runtime/server/mcp/tools/admin/get-submission.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/get-submission.js +26 -0
- package/dist/runtime/server/mcp/tools/admin/list-all-forms.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/list-all-forms.js +21 -0
- package/dist/runtime/server/mcp/tools/admin/list-submissions.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/list-submissions.js +22 -0
- package/dist/runtime/server/mcp/tools/admin/update-form.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/update-form.js +24 -0
- package/dist/runtime/server/mcp/tools/get-form.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/get-form.js +12 -0
- package/dist/runtime/server/mcp/tools/list-forms.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/list-forms.js +11 -0
- package/dist/runtime/server/mcp/tools/submit-form.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/submit-form.js +17 -0
- package/dist/runtime/server/plugins/email-delivery.js +16 -11
- package/dist/runtime/server/plugins/encryption.d.ts +2 -0
- package/dist/runtime/server/plugins/encryption.js +4 -0
- package/dist/runtime/server/plugins/staff-scopes.d.ts +2 -0
- package/dist/runtime/server/plugins/staff-scopes.js +1 -0
- package/dist/runtime/server/routes/admin/form-clone.post.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-clone.post.js +17 -0
- package/dist/runtime/server/routes/admin/form-password.delete.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-password.delete.js +8 -0
- package/dist/runtime/server/routes/admin/form-password.get.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-password.get.js +8 -0
- package/dist/runtime/server/routes/admin/form-password.put.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-password.put.js +14 -0
- package/dist/runtime/server/routes/admin/form-secret.delete.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-secret.delete.js +8 -0
- package/dist/runtime/server/routes/admin/form-secrets.get.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-secrets.get.js +5 -0
- package/dist/runtime/server/routes/admin/form-secrets.put.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-secrets.put.js +12 -0
- package/dist/runtime/server/routes/admin/form-token.delete.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-token.delete.js +12 -0
- package/dist/runtime/server/routes/admin/form-tokens.get.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-tokens.get.js +8 -0
- package/dist/runtime/server/routes/admin/form-tokens.post.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-tokens.post.js +12 -0
- package/dist/runtime/server/routes/admin/form.get.js +5 -1
- package/dist/runtime/server/routes/admin/form.patch.js +3 -12
- package/dist/runtime/server/routes/admin/forms.post.js +1 -6
- package/dist/runtime/server/routes/public/access.get.d.ts +2 -0
- package/dist/runtime/server/routes/public/access.get.js +3 -0
- package/dist/runtime/server/routes/public/form.get.js +3 -1
- package/dist/runtime/server/routes/public/submit.post.js +2 -2
- package/dist/runtime/server/utils/access.d.ts +27 -0
- package/dist/runtime/server/utils/access.js +63 -0
- package/dist/runtime/server/utils/admin-schemas.d.ts +219 -15
- package/dist/runtime/server/utils/auth.d.ts +7 -7
- package/dist/runtime/server/utils/auth.js +4 -4
- package/dist/runtime/server/utils/captcha.d.ts +10 -0
- package/dist/runtime/server/utils/captcha.js +25 -0
- package/dist/runtime/server/utils/crypto.d.ts +18 -0
- package/dist/runtime/server/utils/crypto.js +48 -0
- package/dist/runtime/server/utils/forms.d.ts +6 -1
- package/dist/runtime/server/utils/forms.js +28 -0
- package/dist/runtime/server/utils/mcp-access.d.ts +9 -0
- package/dist/runtime/server/utils/mcp-access.js +8 -0
- package/dist/runtime/server/utils/rate-limit.d.ts +3 -0
- package/dist/runtime/server/utils/rate-limit.js +19 -0
- package/dist/runtime/server/utils/render.d.ts +2 -11
- package/dist/runtime/server/utils/render.js +1 -26
- package/dist/runtime/server/utils/secrets.d.ts +28 -0
- package/dist/runtime/server/utils/secrets.js +60 -0
- package/dist/runtime/server/utils/submit.d.ts +12 -1
- package/dist/runtime/server/utils/submit.js +18 -4
- package/dist/runtime/utils/builder.d.ts +73 -0
- package/dist/runtime/utils/builder.js +220 -0
- package/dist/runtime/utils/captcha.d.ts +9 -0
- package/dist/runtime/utils/captcha.js +7 -0
- package/dist/runtime/utils/conditions.d.ts +3 -1
- package/dist/runtime/utils/conditions.js +4 -0
- package/dist/runtime/utils/definition.d.ts +116 -6
- package/dist/runtime/utils/definition.js +53 -4
- package/dist/runtime/utils/embed.d.ts +8 -0
- package/dist/runtime/utils/embed.js +7 -0
- package/dist/runtime/utils/index.d.ts +16 -3
- package/dist/runtime/utils/index.js +66 -3
- package/dist/runtime/utils/markdown.d.ts +1 -5
- package/dist/runtime/utils/markdown.js +1 -5
- package/dist/runtime/utils/phone.d.ts +29 -0
- package/dist/runtime/utils/phone.js +294 -0
- package/dist/runtime/utils/preview.d.ts +12 -0
- package/dist/runtime/utils/preview.js +17 -0
- package/dist/runtime/utils/schedule.d.ts +1 -0
- package/dist/runtime/utils/schedule.js +1 -0
- package/dist/runtime/utils/schema.d.ts +2 -0
- package/dist/runtime/utils/schema.js +12 -4
- package/dist/runtime/utils/template.d.ts +57 -0
- package/dist/runtime/utils/template.js +147 -0
- package/dist/runtime/utils/text.d.ts +0 -3
- package/dist/runtime/utils/text.js +0 -5
- package/dist/types.d.mts +5 -1
- package/package.json +13 -5
|
@@ -502,3 +502,286 @@ export declare const formSubmissionFiles: import("drizzle-orm/sqlite-core").SQLi
|
|
|
502
502
|
};
|
|
503
503
|
dialect: "sqlite";
|
|
504
504
|
}>;
|
|
505
|
+
export declare const formAccessTokens: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
|
|
506
|
+
name: "form_access_tokens";
|
|
507
|
+
schema: undefined;
|
|
508
|
+
columns: {
|
|
509
|
+
id: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
510
|
+
name: "id";
|
|
511
|
+
tableName: "form_access_tokens";
|
|
512
|
+
dataType: "string";
|
|
513
|
+
columnType: "SQLiteText";
|
|
514
|
+
data: string;
|
|
515
|
+
driverParam: string;
|
|
516
|
+
notNull: true;
|
|
517
|
+
hasDefault: false;
|
|
518
|
+
isPrimaryKey: true;
|
|
519
|
+
isAutoincrement: false;
|
|
520
|
+
hasRuntimeDefault: false;
|
|
521
|
+
enumValues: [string, ...string[]];
|
|
522
|
+
baseColumn: never;
|
|
523
|
+
identity: undefined;
|
|
524
|
+
generated: undefined;
|
|
525
|
+
}, {}, {
|
|
526
|
+
length: undefined;
|
|
527
|
+
}>;
|
|
528
|
+
formId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
529
|
+
name: "formId";
|
|
530
|
+
tableName: "form_access_tokens";
|
|
531
|
+
dataType: "string";
|
|
532
|
+
columnType: "SQLiteText";
|
|
533
|
+
data: string;
|
|
534
|
+
driverParam: string;
|
|
535
|
+
notNull: true;
|
|
536
|
+
hasDefault: false;
|
|
537
|
+
isPrimaryKey: false;
|
|
538
|
+
isAutoincrement: false;
|
|
539
|
+
hasRuntimeDefault: false;
|
|
540
|
+
enumValues: [string, ...string[]];
|
|
541
|
+
baseColumn: never;
|
|
542
|
+
identity: undefined;
|
|
543
|
+
generated: undefined;
|
|
544
|
+
}, {}, {
|
|
545
|
+
length: undefined;
|
|
546
|
+
}>;
|
|
547
|
+
name: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
548
|
+
name: "name";
|
|
549
|
+
tableName: "form_access_tokens";
|
|
550
|
+
dataType: "string";
|
|
551
|
+
columnType: "SQLiteText";
|
|
552
|
+
data: string;
|
|
553
|
+
driverParam: string;
|
|
554
|
+
notNull: true;
|
|
555
|
+
hasDefault: false;
|
|
556
|
+
isPrimaryKey: false;
|
|
557
|
+
isAutoincrement: false;
|
|
558
|
+
hasRuntimeDefault: false;
|
|
559
|
+
enumValues: [string, ...string[]];
|
|
560
|
+
baseColumn: never;
|
|
561
|
+
identity: undefined;
|
|
562
|
+
generated: undefined;
|
|
563
|
+
}, {}, {
|
|
564
|
+
length: undefined;
|
|
565
|
+
}>;
|
|
566
|
+
tokenHash: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
567
|
+
name: "tokenHash";
|
|
568
|
+
tableName: "form_access_tokens";
|
|
569
|
+
dataType: "string";
|
|
570
|
+
columnType: "SQLiteText";
|
|
571
|
+
data: string;
|
|
572
|
+
driverParam: string;
|
|
573
|
+
notNull: true;
|
|
574
|
+
hasDefault: false;
|
|
575
|
+
isPrimaryKey: false;
|
|
576
|
+
isAutoincrement: false;
|
|
577
|
+
hasRuntimeDefault: false;
|
|
578
|
+
enumValues: [string, ...string[]];
|
|
579
|
+
baseColumn: never;
|
|
580
|
+
identity: undefined;
|
|
581
|
+
generated: undefined;
|
|
582
|
+
}, {}, {
|
|
583
|
+
length: undefined;
|
|
584
|
+
}>;
|
|
585
|
+
prefix: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
586
|
+
name: "prefix";
|
|
587
|
+
tableName: "form_access_tokens";
|
|
588
|
+
dataType: "string";
|
|
589
|
+
columnType: "SQLiteText";
|
|
590
|
+
data: string;
|
|
591
|
+
driverParam: string;
|
|
592
|
+
notNull: true;
|
|
593
|
+
hasDefault: false;
|
|
594
|
+
isPrimaryKey: false;
|
|
595
|
+
isAutoincrement: false;
|
|
596
|
+
hasRuntimeDefault: false;
|
|
597
|
+
enumValues: [string, ...string[]];
|
|
598
|
+
baseColumn: never;
|
|
599
|
+
identity: undefined;
|
|
600
|
+
generated: undefined;
|
|
601
|
+
}, {}, {
|
|
602
|
+
length: undefined;
|
|
603
|
+
}>;
|
|
604
|
+
createdAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
605
|
+
name: "createdAt";
|
|
606
|
+
tableName: "form_access_tokens";
|
|
607
|
+
dataType: "date";
|
|
608
|
+
columnType: "SQLiteTimestamp";
|
|
609
|
+
data: Date;
|
|
610
|
+
driverParam: number;
|
|
611
|
+
notNull: true;
|
|
612
|
+
hasDefault: true;
|
|
613
|
+
isPrimaryKey: false;
|
|
614
|
+
isAutoincrement: false;
|
|
615
|
+
hasRuntimeDefault: true;
|
|
616
|
+
enumValues: undefined;
|
|
617
|
+
baseColumn: never;
|
|
618
|
+
identity: undefined;
|
|
619
|
+
generated: undefined;
|
|
620
|
+
}, {}, {}>;
|
|
621
|
+
lastUsedAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
622
|
+
name: "lastUsedAt";
|
|
623
|
+
tableName: "form_access_tokens";
|
|
624
|
+
dataType: "date";
|
|
625
|
+
columnType: "SQLiteTimestamp";
|
|
626
|
+
data: Date;
|
|
627
|
+
driverParam: number;
|
|
628
|
+
notNull: false;
|
|
629
|
+
hasDefault: false;
|
|
630
|
+
isPrimaryKey: false;
|
|
631
|
+
isAutoincrement: false;
|
|
632
|
+
hasRuntimeDefault: false;
|
|
633
|
+
enumValues: undefined;
|
|
634
|
+
baseColumn: never;
|
|
635
|
+
identity: undefined;
|
|
636
|
+
generated: undefined;
|
|
637
|
+
}, {}, {}>;
|
|
638
|
+
revokedAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
639
|
+
name: "revokedAt";
|
|
640
|
+
tableName: "form_access_tokens";
|
|
641
|
+
dataType: "date";
|
|
642
|
+
columnType: "SQLiteTimestamp";
|
|
643
|
+
data: Date;
|
|
644
|
+
driverParam: number;
|
|
645
|
+
notNull: false;
|
|
646
|
+
hasDefault: false;
|
|
647
|
+
isPrimaryKey: false;
|
|
648
|
+
isAutoincrement: false;
|
|
649
|
+
hasRuntimeDefault: false;
|
|
650
|
+
enumValues: undefined;
|
|
651
|
+
baseColumn: never;
|
|
652
|
+
identity: undefined;
|
|
653
|
+
generated: undefined;
|
|
654
|
+
}, {}, {}>;
|
|
655
|
+
};
|
|
656
|
+
dialect: "sqlite";
|
|
657
|
+
}>;
|
|
658
|
+
/** Shared password of a form with `access.auth = 'password'`. Kept out of the (versioned) definition. */
|
|
659
|
+
export declare const formAccess: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
|
|
660
|
+
name: "form_access";
|
|
661
|
+
schema: undefined;
|
|
662
|
+
columns: {
|
|
663
|
+
formId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
664
|
+
name: "formId";
|
|
665
|
+
tableName: "form_access";
|
|
666
|
+
dataType: "string";
|
|
667
|
+
columnType: "SQLiteText";
|
|
668
|
+
data: string;
|
|
669
|
+
driverParam: string;
|
|
670
|
+
notNull: true;
|
|
671
|
+
hasDefault: false;
|
|
672
|
+
isPrimaryKey: true;
|
|
673
|
+
isAutoincrement: false;
|
|
674
|
+
hasRuntimeDefault: false;
|
|
675
|
+
enumValues: [string, ...string[]];
|
|
676
|
+
baseColumn: never;
|
|
677
|
+
identity: undefined;
|
|
678
|
+
generated: undefined;
|
|
679
|
+
}, {}, {
|
|
680
|
+
length: undefined;
|
|
681
|
+
}>;
|
|
682
|
+
passwordHash: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
683
|
+
name: "passwordHash";
|
|
684
|
+
tableName: "form_access";
|
|
685
|
+
dataType: "string";
|
|
686
|
+
columnType: "SQLiteText";
|
|
687
|
+
data: string;
|
|
688
|
+
driverParam: string;
|
|
689
|
+
notNull: true;
|
|
690
|
+
hasDefault: false;
|
|
691
|
+
isPrimaryKey: false;
|
|
692
|
+
isAutoincrement: false;
|
|
693
|
+
hasRuntimeDefault: false;
|
|
694
|
+
enumValues: [string, ...string[]];
|
|
695
|
+
baseColumn: never;
|
|
696
|
+
identity: undefined;
|
|
697
|
+
generated: undefined;
|
|
698
|
+
}, {}, {
|
|
699
|
+
length: undefined;
|
|
700
|
+
}>;
|
|
701
|
+
updatedAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
702
|
+
name: "updatedAt";
|
|
703
|
+
tableName: "form_access";
|
|
704
|
+
dataType: "date";
|
|
705
|
+
columnType: "SQLiteTimestamp";
|
|
706
|
+
data: Date;
|
|
707
|
+
driverParam: number;
|
|
708
|
+
notNull: true;
|
|
709
|
+
hasDefault: true;
|
|
710
|
+
isPrimaryKey: false;
|
|
711
|
+
isAutoincrement: false;
|
|
712
|
+
hasRuntimeDefault: true;
|
|
713
|
+
enumValues: undefined;
|
|
714
|
+
baseColumn: never;
|
|
715
|
+
identity: undefined;
|
|
716
|
+
generated: undefined;
|
|
717
|
+
}, {}, {}>;
|
|
718
|
+
};
|
|
719
|
+
dialect: "sqlite";
|
|
720
|
+
}>;
|
|
721
|
+
/**
|
|
722
|
+
* Per-form secrets, encrypted at rest. Keys are namespaced strings (`captcha.turnstile.secretKey`, ...) from an
|
|
723
|
+
* allowlist in `utils/secrets.ts`; future integrations (webhook signing secrets, ...) add their keys there.
|
|
724
|
+
* Kept out of the (versioned) definition, submission snapshots, deliveries and exports.
|
|
725
|
+
*/
|
|
726
|
+
export declare const formSecrets: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
|
|
727
|
+
name: "form_secrets";
|
|
728
|
+
schema: undefined;
|
|
729
|
+
columns: {
|
|
730
|
+
formId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
731
|
+
name: "formId";
|
|
732
|
+
tableName: "form_secrets";
|
|
733
|
+
dataType: "string";
|
|
734
|
+
columnType: "SQLiteText";
|
|
735
|
+
data: string;
|
|
736
|
+
driverParam: string;
|
|
737
|
+
notNull: true;
|
|
738
|
+
hasDefault: false;
|
|
739
|
+
isPrimaryKey: true;
|
|
740
|
+
isAutoincrement: false;
|
|
741
|
+
hasRuntimeDefault: false;
|
|
742
|
+
enumValues: [string, ...string[]];
|
|
743
|
+
baseColumn: never;
|
|
744
|
+
identity: undefined;
|
|
745
|
+
generated: undefined;
|
|
746
|
+
}, {}, {
|
|
747
|
+
length: undefined;
|
|
748
|
+
}>;
|
|
749
|
+
secrets: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
750
|
+
name: "secrets";
|
|
751
|
+
tableName: "form_secrets";
|
|
752
|
+
dataType: "custom";
|
|
753
|
+
columnType: "SQLiteCustomColumn";
|
|
754
|
+
data: Record<string, string>;
|
|
755
|
+
driverParam: string;
|
|
756
|
+
notNull: true;
|
|
757
|
+
hasDefault: false;
|
|
758
|
+
isPrimaryKey: false;
|
|
759
|
+
isAutoincrement: false;
|
|
760
|
+
hasRuntimeDefault: false;
|
|
761
|
+
enumValues: undefined;
|
|
762
|
+
baseColumn: never;
|
|
763
|
+
identity: undefined;
|
|
764
|
+
generated: undefined;
|
|
765
|
+
}, {}, {
|
|
766
|
+
sqliteColumnBuilderBrand: "SQLiteCustomColumnBuilderBrand";
|
|
767
|
+
}>;
|
|
768
|
+
updatedAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
769
|
+
name: "updatedAt";
|
|
770
|
+
tableName: "form_secrets";
|
|
771
|
+
dataType: "date";
|
|
772
|
+
columnType: "SQLiteTimestamp";
|
|
773
|
+
data: Date;
|
|
774
|
+
driverParam: number;
|
|
775
|
+
notNull: true;
|
|
776
|
+
hasDefault: true;
|
|
777
|
+
isPrimaryKey: false;
|
|
778
|
+
isAutoincrement: false;
|
|
779
|
+
hasRuntimeDefault: true;
|
|
780
|
+
enumValues: undefined;
|
|
781
|
+
baseColumn: never;
|
|
782
|
+
identity: undefined;
|
|
783
|
+
generated: undefined;
|
|
784
|
+
}, {}, {}>;
|
|
785
|
+
};
|
|
786
|
+
dialect: "sqlite";
|
|
787
|
+
}>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
|
|
1
|
+
import { customType, sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
|
|
2
2
|
export const forms = sqliteTable("forms", {
|
|
3
3
|
/** `frm_` + random id. */
|
|
4
4
|
id: text().primaryKey(),
|
|
@@ -44,3 +44,35 @@ export const formSubmissionFiles = sqliteTable("form_submission_files", {
|
|
|
44
44
|
}, (t) => [
|
|
45
45
|
index("form_submission_file_submission_idx").on(t.submissionId)
|
|
46
46
|
]);
|
|
47
|
+
export const formAccessTokens = sqliteTable("form_access_tokens", {
|
|
48
|
+
/** `fat_` + random id. */
|
|
49
|
+
id: text().primaryKey(),
|
|
50
|
+
formId: text().notNull().references(() => forms.id, { onDelete: "cascade" }),
|
|
51
|
+
name: text().notNull(),
|
|
52
|
+
/** SHA-256 (hex) of the token; the token itself is shown once on creation and never stored. */
|
|
53
|
+
tokenHash: text().notNull().unique(),
|
|
54
|
+
/** First characters of the token, for display. */
|
|
55
|
+
prefix: text().notNull(),
|
|
56
|
+
createdAt: integer({ mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
|
|
57
|
+
lastUsedAt: integer({ mode: "timestamp" }),
|
|
58
|
+
revokedAt: integer({ mode: "timestamp" })
|
|
59
|
+
}, (t) => [
|
|
60
|
+
index("form_access_token_form_idx").on(t.formId)
|
|
61
|
+
]);
|
|
62
|
+
export const formAccess = sqliteTable("form_access", {
|
|
63
|
+
formId: text().primaryKey().references(() => forms.id, { onDelete: "cascade" }),
|
|
64
|
+
/** Scrypt hash (nuxt-auth-utils `hashPassword`). */
|
|
65
|
+
passwordHash: text().notNull(),
|
|
66
|
+
updatedAt: integer({ mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
|
|
67
|
+
});
|
|
68
|
+
export const formSecrets = sqliteTable("form_secrets", {
|
|
69
|
+
formId: text().primaryKey().references(() => forms.id, { onDelete: "cascade" }),
|
|
70
|
+
// Inline on purpose: the module kit copies this property (and only the imports above) into a generated schema
|
|
71
|
+
// template, so it cannot call a local helper. `utils/crypto.ts` registers the cipher on `globalThis`.
|
|
72
|
+
secrets: customType({
|
|
73
|
+
dataType: () => "text",
|
|
74
|
+
toDriver: (value) => globalThis.__frigearFormsCrypto.encrypt(value),
|
|
75
|
+
fromDriver: (value) => globalThis.__frigearFormsCrypto.decrypt(value)
|
|
76
|
+
})("secrets").notNull(),
|
|
77
|
+
updatedAt: integer({ mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
|
|
78
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Shown to the agent in tool descriptions and errors, so it can explain the limit to the user
|
|
4
|
+
* instead of retrying or looking for a way around it.
|
|
5
|
+
*/
|
|
6
|
+
export declare const DELIVERY_NOTICE = "Delivery settings (email recipients, webhooks) are intentionally not available through MCP: letting an agent change where answers are sent could be used to exfiltrate submissions. A person must change them in the admin UI. Do not try to work around this; tell the user to do it there.";
|
|
7
|
+
export declare const formDefinitionInput: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
8
|
+
export declare const formSlug: z.ZodString;
|
|
9
|
+
export declare const deliveryRefusal: () => any;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const DELIVERY_NOTICE = "Delivery settings (email recipients, webhooks) are intentionally not available through MCP: letting an agent change where answers are sent could be used to exfiltrate submissions. A person must change them in the admin UI. Do not try to work around this; tell the user to do it there.";
|
|
3
|
+
export const formDefinitionInput = z.record(z.string(), z.unknown()).describe(`The form definition (fields, steps, storage, access). Use get-form-definition on an existing form to see the shape. Do not include "delivery". ${DELIVERY_NOTICE}`);
|
|
4
|
+
export const formSlug = z.string().min(1).max(100).describe("Lowercase letters, digits and dashes.");
|
|
5
|
+
export const deliveryRefusal = () => createError({
|
|
6
|
+
statusCode: 400,
|
|
7
|
+
statusMessage: `Delivery settings cannot be set through MCP. ${DELIVERY_NOTICE}`
|
|
8
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { formInput } from "../../../utils/admin-schemas.js";
|
|
3
|
+
import { DELIVERY_NOTICE, deliveryRefusal, formDefinitionInput, formSlug } from "../../schemas.js";
|
|
4
|
+
export default defineScopedMcpTool({
|
|
5
|
+
description: `Create a form (staff). It starts as a draft unless a status is given. New forms have no delivery settings; ${DELIVERY_NOTICE}`,
|
|
6
|
+
scopes: ["forms:manage"],
|
|
7
|
+
gate: canManageForms,
|
|
8
|
+
annotations: { destructiveHint: false },
|
|
9
|
+
inputSchema: {
|
|
10
|
+
slug: formSlug,
|
|
11
|
+
name: z.string().min(1).max(200),
|
|
12
|
+
status: z.enum(["draft", "published", "archived"]).optional(),
|
|
13
|
+
definition: formDefinitionInput
|
|
14
|
+
},
|
|
15
|
+
handler: async (args) => {
|
|
16
|
+
if ("delivery" in args.definition) throw deliveryRefusal();
|
|
17
|
+
const row = await createForm(formInput.parse(args));
|
|
18
|
+
const { delivery: _delivery, ...definition } = row.definition;
|
|
19
|
+
return { id: row.id, slug: row.slug, name: row.name, status: row.status, version: row.version, definition };
|
|
20
|
+
}
|
|
21
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { DELIVERY_NOTICE } from "../../schemas.js";
|
|
3
|
+
export default defineScopedMcpTool({
|
|
4
|
+
description: `Get a form including its draft/archived status (staff). Delivery settings are never returned. ${DELIVERY_NOTICE}`,
|
|
5
|
+
scopes: ["forms:manage"],
|
|
6
|
+
gate: canManageForms,
|
|
7
|
+
annotations: { readOnlyHint: true },
|
|
8
|
+
inputSchema: { id: z.string() },
|
|
9
|
+
handler: async ({ id }) => {
|
|
10
|
+
const row = await findFormById(id);
|
|
11
|
+
const { delivery: _delivery, ...definition } = row.definition;
|
|
12
|
+
return { id: row.id, slug: row.slug, name: row.name, status: row.status, version: row.version, definition };
|
|
13
|
+
}
|
|
14
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { eq } from "drizzle-orm";
|
|
3
|
+
import { db, schema } from "@nuxthub/db";
|
|
4
|
+
export default defineScopedMcpTool({
|
|
5
|
+
description: "Get one submission (staff). Answers, submitter and file names are only included with the forms:pii scope.",
|
|
6
|
+
scopes: ["forms:manage"],
|
|
7
|
+
gate: canManageForms,
|
|
8
|
+
annotations: { readOnlyHint: true },
|
|
9
|
+
inputSchema: { id: z.string() },
|
|
10
|
+
handler: async ({ id }, ctx) => {
|
|
11
|
+
const [row] = await db.select().from(schema.formSubmissions).where(eq(schema.formSubmissions.id, id)).limit(1);
|
|
12
|
+
if (!row) throw createError({ statusCode: 404, statusMessage: "Submission not found." });
|
|
13
|
+
const files = await db.select().from(schema.formSubmissionFiles).where(eq(schema.formSubmissionFiles.submissionId, row.id));
|
|
14
|
+
const allow = ctx.hasScope("forms:pii");
|
|
15
|
+
const submission = redactPii({
|
|
16
|
+
...pick(row, ["id", "formId", "formSlug", "formVersion", "locale", "deliveryStatus"]),
|
|
17
|
+
createdAt: row.createdAt.toISOString(),
|
|
18
|
+
answeredFields: Object.keys(row.answers),
|
|
19
|
+
fileCount: files.length,
|
|
20
|
+
answers: row.answers,
|
|
21
|
+
userId: row.userId,
|
|
22
|
+
files: files.map(({ id: id2, fieldKey, filename, contentType, size }) => ({ id: id2, fieldKey, filename, contentType, size }))
|
|
23
|
+
}, { allow, fields: ["answers", "userId", "files"] });
|
|
24
|
+
return { notice: piiNotice(allow, "forms:pii", "Answers, the submitter and file names"), submission };
|
|
25
|
+
}
|
|
26
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { count, desc } from "drizzle-orm";
|
|
2
|
+
import { db, schema } from "@nuxthub/db";
|
|
3
|
+
export default defineScopedMcpTool({
|
|
4
|
+
description: "List all forms including drafts, with submission counts (staff).",
|
|
5
|
+
scopes: ["forms:manage"],
|
|
6
|
+
gate: canManageForms,
|
|
7
|
+
annotations: { readOnlyHint: true },
|
|
8
|
+
handler: async () => {
|
|
9
|
+
const rows = await db.select().from(schema.forms).orderBy(desc(schema.forms.updatedAt));
|
|
10
|
+
const counts = await db.select({ formId: schema.formSubmissions.formId, total: count() }).from(schema.formSubmissions).groupBy(schema.formSubmissions.formId);
|
|
11
|
+
const totals = new Map(counts.map((row) => [row.formId, row.total]));
|
|
12
|
+
return rows.map((row) => ({
|
|
13
|
+
id: row.id,
|
|
14
|
+
slug: row.slug,
|
|
15
|
+
name: row.name,
|
|
16
|
+
status: row.status,
|
|
17
|
+
version: row.version,
|
|
18
|
+
submissionCount: totals.get(row.id) ?? 0
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { desc, eq } from "drizzle-orm";
|
|
3
|
+
import { db, schema } from "@nuxthub/db";
|
|
4
|
+
import { renderAnswers } from "../../../utils/render.js";
|
|
5
|
+
export default defineScopedMcpTool({
|
|
6
|
+
description: "List the latest submissions of a form (staff). Answer previews are only included with the forms:pii scope.",
|
|
7
|
+
scopes: ["forms:manage"],
|
|
8
|
+
gate: canManageForms,
|
|
9
|
+
annotations: { readOnlyHint: true },
|
|
10
|
+
inputSchema: { formId: z.string(), limit: z.number().int().min(1).max(200).optional() },
|
|
11
|
+
handler: async ({ formId, limit }, ctx) => {
|
|
12
|
+
const form = await findFormById(formId);
|
|
13
|
+
const rows = await db.select().from(schema.formSubmissions).where(eq(schema.formSubmissions.formId, form.id)).orderBy(desc(schema.formSubmissions.createdAt)).limit(limit ?? 50);
|
|
14
|
+
const allow = ctx.hasScope("forms:pii");
|
|
15
|
+
const submissions = rows.map((row) => redactPii({
|
|
16
|
+
...pick(row, ["id", "locale", "deliveryStatus"]),
|
|
17
|
+
createdAt: row.createdAt.toISOString(),
|
|
18
|
+
preview: renderAnswers(row.definitionSnapshot, row.answers, row.locale).filter((answer) => answer.value).slice(0, 3).map(({ label, value }) => ({ label, value: value.slice(0, 120) }))
|
|
19
|
+
}, { allow, fields: ["preview"] }));
|
|
20
|
+
return { notice: piiNotice(allow, "forms:pii", "Answer previews"), submissions };
|
|
21
|
+
}
|
|
22
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { formPatch } from "../../../utils/admin-schemas.js";
|
|
3
|
+
import { DELIVERY_NOTICE, deliveryRefusal, formDefinitionInput, formSlug } from "../../schemas.js";
|
|
4
|
+
export default defineScopedMcpTool({
|
|
5
|
+
description: `Update a form (staff). A changed definition creates a new version; earlier submissions keep theirs. Existing delivery settings are kept unchanged. ${DELIVERY_NOTICE}`,
|
|
6
|
+
scopes: ["forms:manage"],
|
|
7
|
+
gate: canManageForms,
|
|
8
|
+
annotations: { idempotentHint: true },
|
|
9
|
+
inputSchema: {
|
|
10
|
+
id: z.string(),
|
|
11
|
+
slug: formSlug.optional(),
|
|
12
|
+
name: z.string().min(1).max(200).optional(),
|
|
13
|
+
status: z.enum(["draft", "published", "archived"]).optional(),
|
|
14
|
+
definition: formDefinitionInput.optional()
|
|
15
|
+
},
|
|
16
|
+
handler: async ({ id, ...patch }) => {
|
|
17
|
+
const current = await findFormById(id);
|
|
18
|
+
if (patch.definition && "delivery" in patch.definition) throw deliveryRefusal();
|
|
19
|
+
const definition = patch.definition ? { ...patch.definition, ...current.definition.delivery ? { delivery: current.definition.delivery } : {} } : void 0;
|
|
20
|
+
const row = await patchForm(id, formPatch.parse({ ...patch, definition }));
|
|
21
|
+
const { delivery: _delivery, ...visible } = row.definition;
|
|
22
|
+
return { id: row.id, slug: row.slug, name: row.name, status: row.status, version: row.version, definition: visible };
|
|
23
|
+
}
|
|
24
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export default defineScopedMcpTool({
|
|
3
|
+
description: "Get the definition (fields) of a published form so you can fill it in.",
|
|
4
|
+
scopes: ["forms:submit"],
|
|
5
|
+
annotations: { readOnlyHint: true },
|
|
6
|
+
inputSchema: { slug: z.string() },
|
|
7
|
+
handler: async ({ slug }, { event }) => {
|
|
8
|
+
const form = await findPublishedForm(slug);
|
|
9
|
+
await assertMcpFormAccess(event, form);
|
|
10
|
+
return toPublicForm(form);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { desc, eq } from "drizzle-orm";
|
|
2
|
+
import { db, schema } from "@nuxthub/db";
|
|
3
|
+
export default defineScopedMcpTool({
|
|
4
|
+
description: "List the published forms you can fill in.",
|
|
5
|
+
scopes: ["forms:submit"],
|
|
6
|
+
annotations: { readOnlyHint: true },
|
|
7
|
+
handler: async () => {
|
|
8
|
+
const rows = await db.select().from(schema.forms).where(eq(schema.forms.status, "published")).orderBy(desc(schema.forms.updatedAt));
|
|
9
|
+
return rows.map((row) => ({ slug: row.slug, name: row.name, mode: row.definition.mode }));
|
|
10
|
+
}
|
|
11
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export default defineScopedMcpTool({
|
|
3
|
+
description: "Submit a published form as yourself. Use get-form first to see the fields.",
|
|
4
|
+
scopes: ["forms:submit"],
|
|
5
|
+
annotations: { destructiveHint: false, idempotentHint: false },
|
|
6
|
+
inputSchema: {
|
|
7
|
+
slug: z.string(),
|
|
8
|
+
answers: z.record(z.string(), z.unknown()).describe("Answers keyed by field key."),
|
|
9
|
+
locale: z.string().max(20).optional()
|
|
10
|
+
},
|
|
11
|
+
handler: async ({ slug, answers, locale }, { event, user }) => {
|
|
12
|
+
await enforceRateLimit(event, slug);
|
|
13
|
+
const form = await findPublishedForm(slug);
|
|
14
|
+
await assertMcpFormAccess(event, form);
|
|
15
|
+
return processSubmission(event, form, user.id, { answers, locale: locale ?? "en", uploads: /* @__PURE__ */ new Map() });
|
|
16
|
+
}
|
|
17
|
+
});
|
|
@@ -1,19 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { composeEmail, renderAnswers } from "../../utils/template.js";
|
|
2
|
+
import { resolveEmailDelivery } from "../../utils/index.js";
|
|
3
3
|
export default defineNitroPlugin((nitro) => {
|
|
4
4
|
nitro.hooks.hook("forms:submitted", async (ctx) => {
|
|
5
5
|
const email = ctx.form.definition.delivery?.email;
|
|
6
|
-
if (!email
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
const
|
|
6
|
+
if (!email) return;
|
|
7
|
+
const { to, replyTo } = resolveEmailDelivery(email, ctx.answers);
|
|
8
|
+
if (!to.length) return;
|
|
9
|
+
const { subject, body, text } = composeEmail(email, {
|
|
10
|
+
form: { name: ctx.form.name, slug: ctx.form.slug, version: ctx.form.version },
|
|
11
|
+
submission: { id: ctx.submissionId, createdAt: (/* @__PURE__ */ new Date()).toISOString(), locale: ctx.locale },
|
|
12
|
+
answers: renderAnswers(ctx.form.definition, ctx.answers, ctx.locale),
|
|
13
|
+
files: ctx.files.map((file) => `${file.filename} (${file.fieldKey})`),
|
|
14
|
+
replyTo
|
|
15
|
+
});
|
|
10
16
|
await dispatchEmailNotification({
|
|
11
|
-
to
|
|
17
|
+
to,
|
|
18
|
+
...replyTo ? { replyTo } : {},
|
|
12
19
|
subject,
|
|
13
|
-
body
|
|
14
|
-
text
|
|
15
|
-
|
|
16
|
-
Files stored with the submission: ${fileNames.join(", ")}` : ""),
|
|
20
|
+
body,
|
|
21
|
+
text,
|
|
17
22
|
meta: { formSlug: ctx.form.slug, submissionId: ctx.submissionId }
|
|
18
23
|
});
|
|
19
24
|
ctx.delivered = true;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default defineStaffScopes(["forms:manage", "forms:pii"], (event, userId) => canManageForms(event, { user: { id: userId } }));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { db, schema } from "@nuxthub/db";
|
|
2
|
+
import { newId } from "../../../utils/index.js";
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
await requireFormsManager(event);
|
|
5
|
+
const source = await findFormById(getRouterParam(event, "id"));
|
|
6
|
+
let slug = `${source.slug}-copy`;
|
|
7
|
+
for (let n = 2; await findFormBySlug(slug); n++) slug = `${source.slug}-copy-${n}`;
|
|
8
|
+
const [row] = await db.insert(schema.forms).values({
|
|
9
|
+
id: newId("frm"),
|
|
10
|
+
slug,
|
|
11
|
+
name: `${source.name} (copy)`,
|
|
12
|
+
status: "draft",
|
|
13
|
+
definition: source.definition
|
|
14
|
+
}).returning();
|
|
15
|
+
setResponseStatus(event, 201);
|
|
16
|
+
return toAdminForm(row);
|
|
17
|
+
});
|