From 6f41b9e7f3704efe71d7ad647173685d6e0a9d32 Mon Sep 17 00:00:00 2001 From: Universe Date: Fri, 10 May 2024 13:25:07 +0900 Subject: [PATCH 01/31] form props --- apps/forms/scaffolds/e/form/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/forms/scaffolds/e/form/index.tsx b/apps/forms/scaffolds/e/form/index.tsx index 33367792b..b4b70dd74 100644 --- a/apps/forms/scaffolds/e/form/index.tsx +++ b/apps/forms/scaffolds/e/form/index.tsx @@ -178,7 +178,6 @@ interface FormTranslation { export function FormView({ form_id, - action, title, blocks, fields, @@ -187,9 +186,9 @@ export function FormView({ translation, options, stylesheet, + ...formattributes }: { form_id: string; - action?: string; title: string; defaultValues?: { [key: string]: string }; fields: FormFieldDefinition[]; @@ -201,7 +200,7 @@ export function FormView({ optimize_for_cjk?: boolean; }; stylesheet?: any; -}) { +} & React.FormHTMLAttributes) { const [checkoutSession, setCheckoutSession] = useState(null); const [is_submitting, set_is_submitting] = useState(false); @@ -444,8 +443,9 @@ export function FormView({ >
{ if (submit_hidden) { e.preventDefault(); From 04b6788e6a93c549ad11cb4cdbe490ef3058d9c8 Mon Sep 17 00:00:00 2001 From: Universe Date: Fri, 10 May 2024 14:11:17 +0900 Subject: [PATCH 02/31] mv --- apps/forms/types/index.ts | 281 +------------------------------------- apps/forms/types/types.ts | 279 +++++++++++++++++++++++++++++++++++++ 2 files changed, 280 insertions(+), 280 deletions(-) create mode 100644 apps/forms/types/types.ts diff --git a/apps/forms/types/index.ts b/apps/forms/types/index.ts index d15f32fba..d0bb0ac74 100644 --- a/apps/forms/types/index.ts +++ b/apps/forms/types/index.ts @@ -1,281 +1,2 @@ +export * from "./types"; export * from "./schema"; - -type UUID = string; -export interface Form { - created_at: string; - custom_preview_url_path: string | null; - custom_publish_url_path: string | null; - default_form_page_id: string | null; - default_form_page_language: FormsPageLanguage; - description: string | null; - id: string; - is_edit_after_submission_allowed: boolean; - is_max_form_responses_by_customer_enabled: boolean; - is_max_form_responses_in_total_enabled: boolean; - is_multiple_response_allowed: boolean; - is_powered_by_branding_enabled: boolean; - is_redirect_after_response_uri_enabled: boolean; - max_form_responses_by_customer: number | null; - max_form_responses_in_total: number | null; - project_id: number; - redirect_after_response_uri: string | null; - title: string; - unknown_field_handling_strategy: FormResponseUnknownFieldHandlingStrategyType; - updated_at: string; -} - -export interface Customer { - uid: string; - created_at: string; - last_seen_at: string; - email: string | null; - uuid: string | null; - phone: string | null; -} - -/** - * user facing page language - */ -export type FormsPageLanguage = - | "en" - | "es" - | "de" - | "ja" - | "fr" - | "pt" - | "it" - | "ko" - | "ru" - | "zh" - | "ar" - | "hi" - | "nl"; - -export type FormResponseUnknownFieldHandlingStrategyType = - | "accept" - | "ignore" - | "reject"; - -export type FormFieldType = - | "text" - | "textarea" - | "tel" - | "url" - | "checkbox" - | "checkboxes" - | "switch" - | "number" - | "date" - | "datetime-local" - | "month" - | "week" - | "time" - | "email" - | "file" - | "image" - | "select" - | "latlng" - | "password" - | "color" - | "radio" - | "country" - | "payment" - | "hidden" - | "signature" - | "range"; - -export type FormFieldAutocompleteType = - | "off" - | "on" - | "name" - | "honorific-prefix" - | "given-name" - | "additional-name" - | "family-name" - | "honorific-suffix" - | "nickname" - | "email" - | "username" - | "new-password" - | "current-password" - | "one-time-code" - | "organization-title" - | "organization" - | "street-address" - | "shipping" - | "billing" - | "address-line1" - | "address-line2" - | "address-line3" - | "address-level4" - | "address-level3" - | "address-level2" - | "address-level1" - | "country" - | "country-name" - | "postal-code" - | "cc-name" - | "cc-given-name" - | "cc-additional-name" - | "cc-family-name" - | "cc-number" - | "cc-exp" - | "cc-exp-month" - | "cc-exp-year" - | "cc-csc" - | "cc-type" - | "transaction-currency" - | "transaction-amount" - | "language" - | "bday" - | "bday-day" - | "bday-month" - | "bday-year" - | "sex" - | "tel" - | "tel-country-code" - | "tel-national" - | "tel-area-code" - | "tel-local" - | "tel-extension" - | "impp" - | "url" - | "photo" - | "webauthn"; - -export type PlatformPoweredBy = "api" | "grida_forms" | "web_client"; - -export type FormFieldInit = { - id?: string; - name: string; - label: string; - type: FormFieldType; - placeholder: string; - required: boolean; - help_text: string; - pattern?: string; - options?: Option[]; - autocomplete?: FormFieldAutocompleteType[] | null; - data?: FormFieldDataSchema | null; - accept?: string | null; - multiple?: boolean; - // options_inventory?: { [option_id: string]: MutableInventoryStock }; -}; - -export interface IFormField { - name: string; - label?: string | null; - type: FormFieldType; - placeholder?: string | null; - required: boolean; - help_text?: string | null; - pattern?: any | null; - options?: Option[]; - autocomplete?: FormFieldAutocompleteType[] | null; - data?: FormFieldDataSchema | null; - accept?: string | null; - multiple?: boolean | null; -} - -export interface FormFieldDefinition extends IFormField { - id: UUID; - local_index: number; -} - -export interface FormPage { - id: string; - form_id: string; - name: string; - blocks: FormBlock[]; - background?: FormPageBackgroundSchema; - stylesheet?: any; -} - -export interface FormBlock { - id: string; - form_id: string; - form_field_id?: string | null; - form_page_id: string | null; - type: T; - title_html?: string | null; - description_html?: string | null; - body_html?: string | null; - src?: string | null; - data: any; - created_at: string; - parent_id?: string | null; - local_index: number; -} - -export type Option = { - id: string; - label?: string; - value: string; - src?: string | null; - disabled?: boolean | null; - index?: number; -}; - -export type FormBlockType = - | "section" - | "field" - | "image" - | "video" - | "html" - | "divider" - | "header" - | "pdf" - // not supported yet - | "group"; -// not supported yet -// | "layout" - -export interface FormResponse { - id: string; - local_id: number; - browser: string | null; - created_at: string; - customer_id: string | null; - form_id: string | null; - ip: string | null; - platform_powered_by: PlatformPoweredBy | null; - raw: any; - updated_at: string; - x_referer: string | null; - x_useragent: string | null; - fields?: FormResponseField[]; -} - -export interface FormResponseField { - id: string; - created_at: string; - form_field_id: string; - response_id: string; - type: FormFieldType; - updated_at: string; - value: any; -} - -export type FormFieldDataSchema = PaymentFieldData | {}; - -export type PaymentsServiceProviders = "stripe" | "tosspayments"; - -export interface PaymentFieldData { - type: "payment"; - service_provider: PaymentsServiceProviders; -} - -export type FormPageBackgroundSchema = FormPageThemeEmbeddedBackgroundData; - -interface FormPageThemeEmbeddedBackgroundData { - type: "background"; - element: "iframe" | "img" | "div"; - /** - * allowed for iframe, img - */ - src?: string; - /** - * allowed for all - */ - "background-color"?: string; -} diff --git a/apps/forms/types/types.ts b/apps/forms/types/types.ts new file mode 100644 index 000000000..8ab00b189 --- /dev/null +++ b/apps/forms/types/types.ts @@ -0,0 +1,279 @@ +type UUID = string; +export interface Form { + created_at: string; + custom_preview_url_path: string | null; + custom_publish_url_path: string | null; + default_form_page_id: string | null; + default_form_page_language: FormsPageLanguage; + description: string | null; + id: string; + is_edit_after_submission_allowed: boolean; + is_max_form_responses_by_customer_enabled: boolean; + is_max_form_responses_in_total_enabled: boolean; + is_multiple_response_allowed: boolean; + is_powered_by_branding_enabled: boolean; + is_redirect_after_response_uri_enabled: boolean; + max_form_responses_by_customer: number | null; + max_form_responses_in_total: number | null; + project_id: number; + redirect_after_response_uri: string | null; + title: string; + unknown_field_handling_strategy: FormResponseUnknownFieldHandlingStrategyType; + updated_at: string; +} + +export interface Customer { + uid: string; + created_at: string; + last_seen_at: string; + email: string | null; + uuid: string | null; + phone: string | null; +} + +/** + * user facing page language + */ +export type FormsPageLanguage = + | "en" + | "es" + | "de" + | "ja" + | "fr" + | "pt" + | "it" + | "ko" + | "ru" + | "zh" + | "ar" + | "hi" + | "nl"; + +export type FormResponseUnknownFieldHandlingStrategyType = + | "accept" + | "ignore" + | "reject"; + +export type FormFieldType = + | "text" + | "textarea" + | "tel" + | "url" + | "checkbox" + | "checkboxes" + | "switch" + | "number" + | "date" + | "datetime-local" + | "month" + | "week" + | "time" + | "email" + | "file" + | "image" + | "select" + | "latlng" + | "password" + | "color" + | "radio" + | "country" + | "payment" + | "hidden" + | "signature" + | "range"; + +export type FormFieldAutocompleteType = + | "off" + | "on" + | "name" + | "honorific-prefix" + | "given-name" + | "additional-name" + | "family-name" + | "honorific-suffix" + | "nickname" + | "email" + | "username" + | "new-password" + | "current-password" + | "one-time-code" + | "organization-title" + | "organization" + | "street-address" + | "shipping" + | "billing" + | "address-line1" + | "address-line2" + | "address-line3" + | "address-level4" + | "address-level3" + | "address-level2" + | "address-level1" + | "country" + | "country-name" + | "postal-code" + | "cc-name" + | "cc-given-name" + | "cc-additional-name" + | "cc-family-name" + | "cc-number" + | "cc-exp" + | "cc-exp-month" + | "cc-exp-year" + | "cc-csc" + | "cc-type" + | "transaction-currency" + | "transaction-amount" + | "language" + | "bday" + | "bday-day" + | "bday-month" + | "bday-year" + | "sex" + | "tel" + | "tel-country-code" + | "tel-national" + | "tel-area-code" + | "tel-local" + | "tel-extension" + | "impp" + | "url" + | "photo" + | "webauthn"; + +export type PlatformPoweredBy = "api" | "grida_forms" | "web_client"; + +export type FormFieldInit = { + id?: string; + name: string; + label: string; + type: FormFieldType; + placeholder: string; + required: boolean; + help_text: string; + pattern?: string; + options?: Option[]; + autocomplete?: FormFieldAutocompleteType[] | null; + data?: FormFieldDataSchema | null; + accept?: string | null; + multiple?: boolean; + // options_inventory?: { [option_id: string]: MutableInventoryStock }; +}; + +export interface IFormField { + name: string; + label?: string | null; + type: FormFieldType; + placeholder?: string | null; + required: boolean; + help_text?: string | null; + pattern?: any | null; + options?: Option[]; + autocomplete?: FormFieldAutocompleteType[] | null; + data?: FormFieldDataSchema | null; + accept?: string | null; + multiple?: boolean | null; +} + +export interface FormFieldDefinition extends IFormField { + id: UUID; + local_index: number; +} + +export interface FormPage { + id: string; + form_id: string; + name: string; + blocks: FormBlock[]; + background?: FormPageBackgroundSchema; + stylesheet?: any; +} + +export interface FormBlock { + id: string; + form_id: string; + form_field_id?: string | null; + form_page_id: string | null; + type: T; + title_html?: string | null; + description_html?: string | null; + body_html?: string | null; + src?: string | null; + data: any; + created_at: string; + parent_id?: string | null; + local_index: number; +} + +export type Option = { + id: string; + label?: string; + value: string; + src?: string | null; + disabled?: boolean | null; + index?: number; +}; + +export type FormBlockType = + | "section" + | "field" + | "image" + | "video" + | "html" + | "divider" + | "header" + | "pdf" + // not supported yet + | "group"; +// not supported yet +// | "layout" + +export interface FormResponse { + id: string; + local_id: number; + browser: string | null; + created_at: string; + customer_id: string | null; + form_id: string | null; + ip: string | null; + platform_powered_by: PlatformPoweredBy | null; + raw: any; + updated_at: string; + x_referer: string | null; + x_useragent: string | null; + fields?: FormResponseField[]; +} + +export interface FormResponseField { + id: string; + created_at: string; + form_field_id: string; + response_id: string; + type: FormFieldType; + updated_at: string; + value: any; +} + +export type FormFieldDataSchema = PaymentFieldData | {}; + +export type PaymentsServiceProviders = "stripe" | "tosspayments"; + +export interface PaymentFieldData { + type: "payment"; + service_provider: PaymentsServiceProviders; +} + +export type FormPageBackgroundSchema = FormPageThemeEmbeddedBackgroundData; + +interface FormPageThemeEmbeddedBackgroundData { + type: "background"; + element: "iframe" | "img" | "div"; + /** + * allowed for iframe, img + */ + src?: string; + /** + * allowed for all + */ + "background-color"?: string; +} From 53d9a654d34d363e0b98d51bdb194ba3f1caebda Mon Sep 17 00:00:00 2001 From: Universe Date: Fri, 10 May 2024 16:20:04 +0900 Subject: [PATCH 03/31] toggle type --- apps/forms/app/(dev)/playground/page.tsx | 9 +++------ apps/forms/public/schema/v1/.gitkeep | 0 apps/forms/types/types.ts | 3 ++- 3 files changed, 5 insertions(+), 7 deletions(-) create mode 100644 apps/forms/public/schema/v1/.gitkeep diff --git a/apps/forms/app/(dev)/playground/page.tsx b/apps/forms/app/(dev)/playground/page.tsx index 805abf1a8..d21021de0 100644 --- a/apps/forms/app/(dev)/playground/page.tsx +++ b/apps/forms/app/(dev)/playground/page.tsx @@ -153,8 +153,8 @@ export default function FormsPlayground() { -
-
+
+
-
+
{renderer ? ( )}
- - {/* */} -
diff --git a/apps/forms/public/schema/v1/.gitkeep b/apps/forms/public/schema/v1/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/apps/forms/types/types.ts b/apps/forms/types/types.ts index 8ab00b189..d1caf8fd1 100644 --- a/apps/forms/types/types.ts +++ b/apps/forms/types/types.ts @@ -62,6 +62,8 @@ export type FormFieldType = | "checkbox" | "checkboxes" | "switch" + | "toggle" + | "radio" | "number" | "date" | "datetime-local" @@ -75,7 +77,6 @@ export type FormFieldType = | "latlng" | "password" | "color" - | "radio" | "country" | "payment" | "hidden" From 01a7c2d2ce4991b32bc5b3d39d977709eb92cd26 Mon Sep 17 00:00:00 2001 From: Universe Date: Fri, 10 May 2024 16:42:54 +0900 Subject: [PATCH 04/31] sync types --- apps/forms/types/supabase.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/forms/types/supabase.ts b/apps/forms/types/supabase.ts index c2e68ffbe..f47661852 100644 --- a/apps/forms/types/supabase.ts +++ b/apps/forms/types/supabase.ts @@ -1378,6 +1378,7 @@ export type Database = { | "checkbox" | "checkboxes" | "switch" + | "toggle" | "date" | "month" | "week" From b7a94127c3b983303cfb4c935149bff452afc541 Mon Sep 17 00:00:00 2001 From: Universe Date: Fri, 10 May 2024 18:08:34 +0900 Subject: [PATCH 05/31] add toggle --- .../app/(api)/private/editor/fields/route.ts | 4 +- apps/forms/app/(dev)/playground/page.tsx | 104 ++++++++---------- .../forms/components/form-field-type-icon.tsx | 4 +- .../formfield/form-field-preview.tsx | 12 +- apps/forms/components/toggle/toggle.tsx | 3 + apps/forms/k/supported_field_types.ts | 6 +- apps/forms/lib/forms/renderer.ts | 4 +- apps/forms/lib/forms/unwrap.ts | 4 +- .../schema/examples/003-fields/form.json | 66 +++++++++++ apps/forms/public/schema/field.schema.json | 7 +- apps/forms/scaffolds/e/form/index.tsx | 7 +- apps/forms/scaffolds/grid/grid.tsx | 12 +- .../scaffolds/panels/field-edit-panel.tsx | 20 ++-- .../scaffolds/panels/response-edit-panel.tsx | 2 +- apps/forms/types/schema.ts | 6 +- apps/forms/types/types.ts | 8 +- 16 files changed, 170 insertions(+), 99 deletions(-) create mode 100644 apps/forms/public/schema/examples/003-fields/form.json diff --git a/apps/forms/app/(api)/private/editor/fields/route.ts b/apps/forms/app/(api)/private/editor/fields/route.ts index 20fda3400..07f0cd7ba 100644 --- a/apps/forms/app/(api)/private/editor/fields/route.ts +++ b/apps/forms/app/(api)/private/editor/fields/route.ts @@ -3,7 +3,7 @@ import { createRouteHandlerClient, } from "@/lib/supabase/server"; import { GridaCommerceClient } from "@/services/commerce"; -import { FormFieldDataSchema, FormFieldType, PaymentFieldData } from "@/types"; +import { FormFieldDataSchema, FormInputType, PaymentFieldData } from "@/types"; import { FormFieldUpsert } from "@/types/private/api"; import assert from "assert"; import { cookies } from "next/headers"; @@ -194,7 +194,7 @@ function safe_data_field({ type, data, }: { - type: FormFieldType; + type: FormInputType; data?: FormFieldDataSchema; }): FormFieldDataSchema | undefined | null { switch (type) { diff --git a/apps/forms/app/(dev)/playground/page.tsx b/apps/forms/app/(dev)/playground/page.tsx index d21021de0..7f905ba9a 100644 --- a/apps/forms/app/(dev)/playground/page.tsx +++ b/apps/forms/app/(dev)/playground/page.tsx @@ -42,6 +42,15 @@ const examples = [ }, }, }, + { + id: "003-fields", + name: "Fields", + template: { + schema: { + src: `${HOST}/schema/examples/003-fields/form.json`, + }, + }, + }, ] as const; type MaybeArray = T | T[]; @@ -77,10 +86,24 @@ function compile(txt?: string) { required: f.required || false, local_index: i, options: - f.options?.map((o) => ({ - ...o, - id: o.value, - })) || [], + f.options?.map((o) => { + switch (typeof o) { + case "string": + case "number": { + return { + id: String(o), + value: String(o), + label: String(o), + }; + } + case "object": { + return { + ...o, + id: o.value, + }; + } + } + }) || [], })) || [], [] ); @@ -139,8 +162,8 @@ export default function FormsPlayground() { -
-
+
+
@@ -153,59 +176,24 @@ export default function FormsPlayground() {
-
-
-
-
- - - -
-
-
- {renderer ? ( - - ) : ( -
- Invalid schema -
- )} +
+ {renderer ? ( + + ) : ( +
+ Invalid schema
-
+ )}
diff --git a/apps/forms/components/form-field-type-icon.tsx b/apps/forms/components/form-field-type-icon.tsx index 6bd362e7b..c48248c59 100644 --- a/apps/forms/components/form-field-type-icon.tsx +++ b/apps/forms/components/form-field-type-icon.tsx @@ -1,4 +1,4 @@ -import { FormFieldType } from "@/types"; +import { FormInputType } from "@/types"; import { EnvelopeClosedIcon, TextIcon, @@ -14,7 +14,7 @@ import { SliderIcon, } from "@radix-ui/react-icons"; -export function FormFieldTypeIcon({ type }: { type: FormFieldType }) { +export function FormFieldTypeIcon({ type }: { type: FormInputType }) { switch (type) { case "text": return ; diff --git a/apps/forms/components/formfield/form-field-preview.tsx b/apps/forms/components/formfield/form-field-preview.tsx index 0da8af512..085da3db6 100644 --- a/apps/forms/components/formfield/form-field-preview.tsx +++ b/apps/forms/components/formfield/form-field-preview.tsx @@ -1,6 +1,6 @@ import { FormFieldDataSchema, - FormFieldType, + FormInputType, Option, PaymentFieldData, } from "@/types"; @@ -22,6 +22,7 @@ import { Checkbox } from "@/components/ui/checkbox"; import useSafeSelectValue from "./use-safe-select-value"; import { Switch } from "../ui/switch"; import { Slider } from "../ui/slider"; +import { Toggle } from "../ui/toggle"; /** * this disables the auto zoom in input text tag safari on iphone by setting font-size to 16px @@ -54,7 +55,7 @@ export function FormFieldPreview({ }: { name: string; label?: string; - type: FormFieldType; + type: FormInputType; placeholder?: string; helpText?: string; required?: boolean; @@ -227,6 +228,13 @@ export function FormFieldPreview({ ); } + case "toggle": { + return ( +
+ {label || name} +
+ ); + } case "switch": { return ( // @ts-ignore diff --git a/apps/forms/components/toggle/toggle.tsx b/apps/forms/components/toggle/toggle.tsx index e077681c4..44818f5dd 100644 --- a/apps/forms/components/toggle/toggle.tsx +++ b/apps/forms/components/toggle/toggle.tsx @@ -1,5 +1,8 @@ "use client"; +/** + * @deprecated + */ export function Toggle({ label, disabled, diff --git a/apps/forms/k/supported_field_types.ts b/apps/forms/k/supported_field_types.ts index 6e01d29ec..6af2a952d 100644 --- a/apps/forms/k/supported_field_types.ts +++ b/apps/forms/k/supported_field_types.ts @@ -1,6 +1,6 @@ -import { FormFieldAutocompleteType, FormFieldType } from "@/types"; +import { FormFieldAutocompleteType, FormInputType } from "@/types"; -export const supported_field_types: FormFieldType[] = [ +export const supported_field_types: FormInputType[] = [ "text", "textarea", "select", @@ -86,7 +86,7 @@ export const supported_field_autocomplete_types: FormFieldAutocompleteType[] = [ "webauthn", ]; -export const html5_multiple_supported_field_types: FormFieldType[] = [ +export const html5_multiple_supported_field_types: FormInputType[] = [ "file", "email", "select", diff --git a/apps/forms/lib/forms/renderer.ts b/apps/forms/lib/forms/renderer.ts index 4c0a42faa..49fa1805e 100644 --- a/apps/forms/lib/forms/renderer.ts +++ b/apps/forms/lib/forms/renderer.ts @@ -2,7 +2,7 @@ import type { JSONField, FormBlockType, FormFieldDataSchema, - FormFieldType, + FormInputType, FormFieldDefinition, FormBlock, Option, @@ -31,7 +31,7 @@ export interface ClientFieldRenderBlock extends BaseRenderBlock { type: "field"; field: { id: string; - type: FormFieldType; + type: FormInputType; name: string; label?: string; help_text?: string; diff --git a/apps/forms/lib/forms/unwrap.ts b/apps/forms/lib/forms/unwrap.ts index 3849b4830..e2403c04a 100644 --- a/apps/forms/lib/forms/unwrap.ts +++ b/apps/forms/lib/forms/unwrap.ts @@ -1,8 +1,8 @@ -import type { FormFieldType } from "@/types"; +import type { FormInputType } from "@/types"; export function unwrapFeildValue( value: any, - type: FormFieldType, + type: FormInputType, options?: { obscure?: boolean; } diff --git a/apps/forms/public/schema/examples/003-fields/form.json b/apps/forms/public/schema/examples/003-fields/form.json new file mode 100644 index 000000000..cc97030d9 --- /dev/null +++ b/apps/forms/public/schema/examples/003-fields/form.json @@ -0,0 +1,66 @@ +{ + "$schema": "https://forms.grida.co/schema/form.schema.json", + "title": "iPhone 15 - Pre-order", + "name": "iphone-15-pre-order", + "fields": [ + { + "name": "text", + "type": "text" + }, + { + "name": "number", + "type": "number" + }, + { + "name": "email", + "type": "email" + }, + { + "name": "password", + "type": "password" + }, + { + "name": "textarea", + "type": "textarea" + }, + { + "name": "select", + "type": "select", + "placeholder": "Select Option", + "options": ["A", "B", "C", "D", "E"] + }, + { + "name": "radio", + "type": "radio", + "options": ["yes", "no"] + }, + { + "name": "checkbox", + "type": "checkbox" + }, + { + "name": "checkbox []", + "type": "array", + "items": { + "type": "checkbox", + "options": ["A", "B", "C", "D", "E"] + } + }, + { + "name": "toggle", + "type": "toggle" + }, + { + "name": "date", + "type": "date" + }, + { + "name": "color", + "type": "color" + }, + { + "name": "signature", + "type": "signature" + } + ] +} diff --git a/apps/forms/public/schema/field.schema.json b/apps/forms/public/schema/field.schema.json index 84d792d1e..29236a7af 100644 --- a/apps/forms/public/schema/field.schema.json +++ b/apps/forms/public/schema/field.schema.json @@ -56,7 +56,12 @@ "options": { "type": "array", "items": { - "$ref": "https://forms.grida.co/schema/option.schema.json" + "anyOf": [ + { "$ref": "https://forms.grida.co/schema/option.schema.json" }, + { "type": "string" }, + { "type": "number" } + ], + "uniqueItems": true } }, "autocomplete": { diff --git a/apps/forms/scaffolds/e/form/index.tsx b/apps/forms/scaffolds/e/form/index.tsx index b4b70dd74..1100f9c18 100644 --- a/apps/forms/scaffolds/e/form/index.tsx +++ b/apps/forms/scaffolds/e/form/index.tsx @@ -432,7 +432,8 @@ export function FormView({ }; return ( -
{tree.children.map((b) => renderBlock(b))} @@ -538,7 +539,7 @@ export function FormView({
)} - + ); } diff --git a/apps/forms/scaffolds/grid/grid.tsx b/apps/forms/scaffolds/grid/grid.tsx index 94c096cec..6b6389dce 100644 --- a/apps/forms/scaffolds/grid/grid.tsx +++ b/apps/forms/scaffolds/grid/grid.tsx @@ -26,7 +26,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@editor-ui/dropdown-menu"; -import { FormFieldType } from "@/types"; +import { FormInputType } from "@/types"; import { JsonEditCell } from "./json-cell"; import { useEditorState } from "../editor"; import { GFRow } from "./types"; @@ -125,7 +125,7 @@ export function Grid({ renderHeaderCell: (props) => ( { onEditFieldClick?.(col.key); }} @@ -199,7 +199,7 @@ function FieldHeaderCell({ onEditClick, onDeleteClick, }: RenderHeaderCellProps & { - type: FormFieldType; + type: FormInputType; onEditClick?: () => void; onDeleteClick?: () => void; }) { @@ -293,11 +293,11 @@ function FieldCell({ column, row }: RenderCellProps) { const { type, value } = data; - const unwrapped = unwrapFeildValue(value, type as FormFieldType, { + const unwrapped = unwrapFeildValue(value, type as FormInputType, { obscure: true, }); - switch (type as FormFieldType) { + switch (type as FormInputType) { case "checkbox": { return ; } @@ -327,7 +327,7 @@ function FieldEditCell(props: RenderEditCellProps) { const unwrapped = JSON.parse(value); - switch (type as FormFieldType) { + switch (type as FormInputType) { case "email": case "password": case "tel": diff --git a/apps/forms/scaffolds/panels/field-edit-panel.tsx b/apps/forms/scaffolds/panels/field-edit-panel.tsx index 4c731e5ab..d73467025 100644 --- a/apps/forms/scaffolds/panels/field-edit-panel.tsx +++ b/apps/forms/scaffolds/panels/field-edit-panel.tsx @@ -17,7 +17,7 @@ import { FormFieldPreview } from "@/components/formfield"; import { FormFieldAutocompleteType, FormFieldDataSchema, - FormFieldType, + FormInputType, FormFieldInit, PaymentFieldData, Option, @@ -81,7 +81,7 @@ import { FormFieldTypeIcon } from "@/components/form-field-type-icon"; // @ts-ignore const default_field_init: { - [key in FormFieldType]: Partial; + [key in FormInputType]: Partial; } = { text: {}, textarea: { type: "textarea" }, @@ -147,13 +147,13 @@ const default_field_init: { }, }; -const input_can_have_options: FormFieldType[] = [ +const input_can_have_options: FormInputType[] = [ "select", "radio", "checkboxes", ]; -const html5_input_like_checkbox_field_types: FormFieldType[] = [ +const html5_input_like_checkbox_field_types: FormInputType[] = [ "checkbox", "switch", ]; @@ -162,7 +162,7 @@ const html5_input_like_checkbox_field_types: FormFieldType[] = [ * html5 pattern allowed input types * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/pattern */ -const input_can_have_pattern: FormFieldType[] = [ +const input_can_have_pattern: FormInputType[] = [ "text", "tel", // `date` uses pattern on fallback - https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date#handling_browser_support @@ -173,7 +173,7 @@ const input_can_have_pattern: FormFieldType[] = [ // "search", // not supported ]; -const input_can_have_autocomplete: FormFieldType[] = +const input_can_have_autocomplete: FormInputType[] = supported_field_types.filter( (type) => ![ @@ -313,8 +313,8 @@ export function TypeSelect({ value, onValueChange, }: { - value: FormFieldType; - onValueChange: (value: FormFieldType) => void; + value: FormInputType; + onValueChange: (value: FormInputType) => void; }) { return ( + ); +} diff --git a/apps/forms/scaffolds/e/form/index.tsx b/apps/forms/scaffolds/e/form/index.tsx index 1100f9c18..0c83773ff 100644 --- a/apps/forms/scaffolds/e/form/index.tsx +++ b/apps/forms/scaffolds/e/form/index.tsx @@ -1,46 +1,23 @@ "use client"; -import type { FormPageBackgroundSchema } from "@/types"; -import type { - FormClientFetchResponseData, - FormClientFetchResponseError, -} from "@/app/(api)/v1/[id]/route"; -import { EditorApiResponse } from "@/types/private/api"; +import React from "react"; +import useSWR from "swr"; import { notFound, redirect } from "next/navigation"; import { FormPageDeveloperErrorDialog } from "@/scaffolds/e/form/error"; -import useSWR from "swr"; import { Skeleton } from "@/components/ui/skeleton"; -import { FormFieldPreview } from "@/components/formfield"; -import { PoweredByGridaFooter } from "./powered-by-brand-footer"; -import React, { useEffect, useMemo, useState } from "react"; -import { FormBlockTree } from "@/lib/forms/types"; -import { FormFieldDefinition, PaymentFieldData } from "@/types"; -import dynamic from "next/dynamic"; -import clsx from "clsx"; -import { TossPaymentsCheckoutSessionResponseData } from "@/types/integrations/api"; -import { request_toss_payments_checkout_session } from "@/lib/agent/integrations/payments/tosspayments/api"; -import { - TossPaymentsCheckout, - TossPaymentsCheckoutProvider, - TossPaymentsPayButton, -} from "@/components/tosspayments"; -import { StripePaymentFormFieldPreview } from "@/components/formfield/form-field-preview-payment-stripe"; import { useFingerprint } from "@/scaffolds/fingerprint"; +import { formlink } from "@/lib/forms/url"; +import { FormView, FormViewTranslation } from "./formview"; import { SYSTEM_GF_CUSTOMER_UUID_KEY, SYSTEM_GF_FINGERPRINT_VISITORID_KEY, } from "@/k/system"; -import { formlink } from "@/lib/forms/url"; -import { ClientRenderBlock, ClientSectionRenderBlock } from "@/lib/forms"; - -const ReactPlayer = dynamic(() => import("react-player/lazy"), { ssr: false }); - -const cls_button_submit = - "text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"; -const cls_button_nuetral = - "py-2.5 px-5 me-2 mb-2 text-sm font-medium text-neutral-900 focus:outline-none bg-white rounded-lg border border-neutral-200 hover:bg-neutral-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-neutral-100 dark:focus:ring-neutral-700 dark:bg-neutral-800 dark:text-neutral-200 dark:border-neutral-700 dark:hover:text-white dark:hover:bg-neutral-800"; - -type PaymentCheckoutSession = TossPaymentsCheckoutSessionResponseData | any; +import type { EditorApiResponse } from "@/types/private/api"; +import type { FormPageBackgroundSchema } from "@/types"; +import type { + FormClientFetchResponseData, + FormClientFetchResponseError, +} from "@/app/(api)/v1/[id]/route"; const HOST_NAME = process.env.NEXT_PUBLIC_HOST_NAME || "http://localhost:3000"; @@ -51,7 +28,7 @@ export function Form({ }: { form_id: string; params: { [key: string]: string }; - translation: FormTranslation; + translation: FormViewTranslation; }) { const { result: fingerprint } = useFingerprint(); @@ -169,397 +146,6 @@ export function Form({ ); } -interface FormTranslation { - next: string; - back: string; - submit: string; - pay: string; -} - -export function FormView({ - form_id, - title, - blocks, - fields, - defaultValues, - tree, - translation, - options, - stylesheet, - ...formattributes -}: { - form_id: string; - title: string; - defaultValues?: { [key: string]: string }; - fields: FormFieldDefinition[]; - blocks: ClientRenderBlock[]; - tree: FormBlockTree; - translation: FormTranslation; - options: { - is_powered_by_branding_enabled: boolean; - optimize_for_cjk?: boolean; - }; - stylesheet?: any; -} & React.FormHTMLAttributes) { - const [checkoutSession, setCheckoutSession] = - useState(null); - const [is_submitting, set_is_submitting] = useState(false); - - const sections = tree.children.filter((block) => block.type === "section"); - - const has_sections = sections.length > 0; - - const last_section_id = has_sections - ? sections[sections.length - 1].id - : null; - - const [current_section_id, set_current_section_id] = useState( - has_sections ? sections[0].id : null - ); - - const current_section = useMemo(() => { - return sections.find((section) => section.id === current_section_id) as - | ClientSectionRenderBlock - | undefined; - }, [current_section_id]); - - const primary_action_override_by_payment = - current_section?.attributes?.contains_payment; - - const submit_hidden = has_sections - ? primary_action_override_by_payment || - last_section_id !== current_section_id - : false; - - const pay_hidden = !primary_action_override_by_payment; - - const previous_section_button_hidden = has_sections - ? current_section_id === sections[0].id - : true; - - const next_section_button_hidden = - (has_sections ? current_section_id === last_section_id : true) || - primary_action_override_by_payment; - - useEffect(() => { - request_toss_payments_checkout_session({ - form_id: form_id, - testmode: true, - redirect: true, - }).then(setCheckoutSession); - }, []); - - const onPrevious = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - - if (current_section_id === sections[0].id) { - return; - } - - const index = sections.findIndex( - (section) => section.id === current_section_id - ); - set_current_section_id(sections[index - 1].id); - - // scroll to top - window.scrollTo({ top: 0, behavior: "smooth" }); - }; - - const onNext = (e?: React.MouseEvent) => { - // validate current section - // e.preventDefault(); - // e.stopPropagation(); - - if (current_section_id === last_section_id) { - return; - } - - const index = sections.findIndex( - (section) => section.id === current_section_id - ); - set_current_section_id(sections[index + 1].id); - - // scroll to top - window.scrollTo({ top: 0, behavior: "smooth" }); - }; - - const renderBlock = ( - block: ClientRenderBlock, - context?: { - is_in_current_section: boolean; - } - ): any => { - switch (block.type) { - case "section": { - const is_current_section = current_section_id === block.id; - return ( -
- - {block.children?.map((b) => - renderBlock(b, { - is_in_current_section: is_current_section, - }) - )} - -
- ); - } - case "field": { - const { field } = block; - const { type } = field; - - switch (type) { - case "payment": { - switch ((field.data as PaymentFieldData)?.service_provider) { - case "tosspayments": { - return ; - } - case "stripe": { - return ; - } - default: { - return <>; - } - } - } - default: { - const defaultValue = defaultValues?.[field.name]; - const is_not_in_current_section = !context?.is_in_current_section; - - return ( - - ); - } - } - } - case "html": { - return ( -
- ); - } - case "header": { - return ( -
- {block.title_html && ( -

- )} - {block.description_html && ( -

- )} -

- ); - } - case "image": { - return ( - // eslint-disable-next-line @next/next/no-img-element - - ); - } - case "video": { - return ( -
- -
- ); - } - case "pdf": { - return ( - - - {block.data} - - - ); - } - case "divider": - return
; - default: - return
; - } - }; - - return ( -
- -
{ - if (submit_hidden) { - e.preventDefault(); - e.stopPropagation(); - const valid = (e.target as HTMLFormElement).checkValidity(); - if (valid) { - onNext(); - } else { - // show error - alert("Please fill out the form correctly."); - } - } else { - // submit - // disable submit button - set_is_submitting(true); - } - }} - className="p-4 pt-10 md:pt-16 h-full overflow-scroll flex-1" - > - - {tree.children.map((b) => renderBlock(b))} - {options.is_powered_by_branding_enabled && ( - // desktop branding -
- -
- )} - -
- - - - - {translation.pay} - -
-
- {options.is_powered_by_branding_enabled && ( - // desktop branding -
- -
- )} -
- ); -} - -function GroupLayout({ children }: React.PropsWithChildren<{}>) { - return
{children}
; -} - -function FingerprintField() { - const { result } = useFingerprint(); - - /* hidden client fingerprint field */ - return ( - - ); -} - function FormPageBackground({ element, src }: FormPageBackgroundSchema) { const renderBackground = () => { switch (element) { @@ -609,3 +195,5 @@ function SkeletonCard() { ); } + +export { FormView } from "./formview"; From 1d8e84fc24134355ea805bb304d2cb7cec631991 Mon Sep 17 00:00:00 2001 From: Universe Date: Fri, 10 May 2024 19:15:24 +0900 Subject: [PATCH 07/31] add toggle group --- apps/forms/app/(dev)/playground/page.tsx | 50 +++++----- .../formfield/form-field-preview.tsx | 95 +++++++++++-------- apps/forms/lib/forms/renderer.ts | 72 +++++++------- .../schema/examples/003-fields/form.json | 14 +-- apps/forms/public/schema/field.schema.json | 6 ++ apps/forms/types/schema.ts | 5 +- apps/forms/types/supabase.ts | 1 + apps/forms/types/types.ts | 1 + 8 files changed, 137 insertions(+), 107 deletions(-) diff --git a/apps/forms/app/(dev)/playground/page.tsx b/apps/forms/app/(dev)/playground/page.tsx index 7f905ba9a..d16e1b091 100644 --- a/apps/forms/app/(dev)/playground/page.tsx +++ b/apps/forms/app/(dev)/playground/page.tsx @@ -11,15 +11,12 @@ import { FormView } from "@/scaffolds/e/form"; import { Editor as MonacoEditor, useMonaco } from "@monaco-editor/react"; import { useEffect, useMemo, useState } from "react"; import { nanoid } from "nanoid"; -import { JSONForm } from "@/types/schema"; +import { JSONField, JSONForm, JSONOptionLike } from "@/types/schema"; import resources from "@/k/i18n"; -import Ajv from "ajv"; import { FormRenderer } from "@/lib/forms"; -import { Label } from "@/components/ui/label"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; import { GridaLogo } from "@/components/grida-logo"; -import { FormFieldAutocompleteType } from "@/types"; +import { FormFieldAutocompleteType, Option } from "@/types"; +import Ajv from "ajv"; const HOST = process.env.NEXT_PUBLIC_HOST_NAME || "http://localhost:3000"; @@ -75,9 +72,28 @@ function compile(txt?: string) { return; } + const map_option = (o: JSONOptionLike): Option => { + switch (typeof o) { + case "string": + case "number": { + return { + id: String(o), + value: String(o), + label: String(o), + }; + } + case "object": { + return { + ...o, + id: o.value, + }; + } + } + }; + const renderer = new FormRenderer( nanoid(), - schema.fields?.map((f, i) => ({ + schema.fields?.map((f: JSONField, i) => ({ ...f, id: f.name, autocomplete: toArrayOf( @@ -85,25 +101,7 @@ function compile(txt?: string) { ), required: f.required || false, local_index: i, - options: - f.options?.map((o) => { - switch (typeof o) { - case "string": - case "number": { - return { - id: String(o), - value: String(o), - label: String(o), - }; - } - case "object": { - return { - ...o, - id: o.value, - }; - } - } - }) || [], + options: f.options?.map(map_option) || [], })) || [], [] ); diff --git a/apps/forms/components/formfield/form-field-preview.tsx b/apps/forms/components/formfield/form-field-preview.tsx index 085da3db6..869260405 100644 --- a/apps/forms/components/formfield/form-field-preview.tsx +++ b/apps/forms/components/formfield/form-field-preview.tsx @@ -23,6 +23,7 @@ import useSafeSelectValue from "./use-safe-select-value"; import { Switch } from "../ui/switch"; import { Slider } from "../ui/slider"; import { Toggle } from "../ui/toggle"; +import { ToggleGroup, ToggleGroupItem } from "../ui/toggle-group"; /** * this disables the auto zoom in input text tag safari on iphone by setting font-size to 16px @@ -189,45 +190,6 @@ export function FormFieldPreview({ )} /> ); } - case "checkboxes": { - return ( -
-
    - {options?.map((option) => ( -
  • -
    - )} - className="w-4 h-4 text-blue-600 bg-neutral-100 border-neutral-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-neutral-700 dark:focus:ring-offset-neutral-700 focus:ring-2 dark:bg-neutral-600 dark:border-neutral-500" - /> - -
    -
  • - ))} -
-
- ); - } case "toggle": { return (
@@ -351,6 +313,35 @@ export function FormFieldPreview({ ); } case "checkboxes": { + const renderItem = (item: Option) => { + return ( +
+ )} + className="w-4 h-4 text-blue-600 bg-neutral-100 border-neutral-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-neutral-700 dark:focus:ring-offset-neutral-700 focus:ring-2 dark:bg-neutral-600 dark:border-neutral-500" + /> + +
+ ); + }; + return ( ); } + case "toggle-group": { + if (options) { + return ( + + {options.map((option) => ( + + {option.label} + + ))} + + ); + } + } } return ( diff --git a/apps/forms/lib/forms/renderer.ts b/apps/forms/lib/forms/renderer.ts index 49fa1805e..65783a833 100644 --- a/apps/forms/lib/forms/renderer.ts +++ b/apps/forms/lib/forms/renderer.ts @@ -27,6 +27,14 @@ export interface BaseRenderBlock { parent_id: string | null; } +type ClientRenderOption = { + id: string; + value: string; + label?: string; + disabled?: boolean | null; + index?: number; +}; + export interface ClientFieldRenderBlock extends BaseRenderBlock { type: "field"; field: { @@ -42,13 +50,7 @@ export interface ClientFieldRenderBlock extends BaseRenderBlock { minlength?: number; maxlength?: number; placeholder?: string; - options?: { - id: string; - label?: string; - value: string; - disabled?: boolean; - index: number; - }[]; + options?: ClientRenderOption[]; autocomplete?: string; data?: FormFieldDataSchema | null; accept?: string; @@ -103,7 +105,7 @@ export class FormRenderer { readonly id: string, private readonly _m_fields: FormFieldDefinition[], private readonly _m_blocks?: FormBlock[], - plugins?: { + private readonly plugins?: { option_renderer: (option: Option) => Option; } ) { @@ -123,19 +125,7 @@ export class FormRenderer { return { id: block.id, type: "field", - field: { - ...field, - options: field.options - ?.sort((a, b) => (a?.index || 0) - (b?.index || 0)) - .map( - plugins?.option_renderer - ? plugins.option_renderer - : (option) => option - ), - required: field.required ?? undefined, - multiple: field.multiple ?? undefined, - autocomplete: field.autocomplete?.join(" ") ?? null, - }, + field: this._field_block_field_definition(field), local_index: block.local_index, parent_id: block.parent_id, }; @@ -234,19 +224,7 @@ export class FormRenderer { return { id: field.id, type: "field", - field: { - ...field, - options: field.options - ?.sort((a: any, b: any) => (a?.index || 0) - (b?.index || 0)) - .map( - plugins?.option_renderer - ? plugins.option_renderer - : (option: Option) => option - ), - required: field.required ?? undefined, - multiple: field.multiple ?? undefined, - autocomplete: field.autocomplete?.join(" ") ?? null, - }, + field: this._field_block_field_definition(field), local_index: i, parent_id: null, }; @@ -276,4 +254,30 @@ export class FormRenderer { public validate() { throw new Error("Not implemented"); } + + private _field_block_field_definition( + field: FormFieldDefinition + ): ClientFieldRenderBlock["field"] { + const mkoption = (options?: Option[]) => + options + ?.sort((a, b) => (a?.index || 0) - (b?.index || 0)) + .map((o, i) => ({ ...o, index: i })) + .map( + this.plugins?.option_renderer + ? this.plugins.option_renderer + : (option) => option + ); + + return { + ...field, + options: mkoption(field.options), + label: field.label || undefined, + help_text: field.help_text || undefined, + placeholder: field.placeholder || undefined, + accept: field.accept || undefined, + required: field.required ?? undefined, + multiple: field.multiple ?? undefined, + autocomplete: field.autocomplete?.join(" ") ?? undefined, + }; + } } diff --git a/apps/forms/public/schema/examples/003-fields/form.json b/apps/forms/public/schema/examples/003-fields/form.json index cc97030d9..b09b41cd4 100644 --- a/apps/forms/public/schema/examples/003-fields/form.json +++ b/apps/forms/public/schema/examples/003-fields/form.json @@ -39,17 +39,19 @@ "type": "checkbox" }, { - "name": "checkbox []", - "type": "array", - "items": { - "type": "checkbox", - "options": ["A", "B", "C", "D", "E"] - } + "name": "checkboxes", + "type": "checkboxes", + "options": ["A", "B", "C", "D", "E"] }, { "name": "toggle", "type": "toggle" }, + { + "name": "toggle-group", + "type": "toggle-group", + "options": ["A", "B", "C", "D", "E"] + }, { "name": "date", "type": "date" diff --git a/apps/forms/public/schema/field.schema.json b/apps/forms/public/schema/field.schema.json index 29236a7af..70277ce15 100644 --- a/apps/forms/public/schema/field.schema.json +++ b/apps/forms/public/schema/field.schema.json @@ -22,6 +22,10 @@ "type": "string", "format": "regex" }, + "multiple": { + "type": "boolean", + "default": false + }, "type": { "type": "string", "enum": [ @@ -31,6 +35,8 @@ "url", "checkbox", "checkboxes", + "toggle", + "toggle-group", "switch", "number", "date", diff --git a/apps/forms/types/schema.ts b/apps/forms/types/schema.ts index 941d56a42..1a19dc9d9 100644 --- a/apps/forms/types/schema.ts +++ b/apps/forms/types/schema.ts @@ -22,10 +22,13 @@ export interface JSONField { required?: boolean; pattern?: string; type: FormInputType; - options?: (JSONOption | string | number)[]; + options?: JSONOptionLike[]; + multiple?: boolean; autocomplete?: FormFieldAutocompleteType | FormFieldAutocompleteType[]; } +export type JSONOptionLike = JSONOption | string | number; + export interface JSONOption { value: string; label?: string; diff --git a/apps/forms/types/supabase.ts b/apps/forms/types/supabase.ts index f47661852..a1ddb4637 100644 --- a/apps/forms/types/supabase.ts +++ b/apps/forms/types/supabase.ts @@ -1379,6 +1379,7 @@ export type Database = { | "checkboxes" | "switch" | "toggle" + | "toggle-group" | "date" | "month" | "week" diff --git a/apps/forms/types/types.ts b/apps/forms/types/types.ts index 68d1ee8ab..9cb9c1be8 100644 --- a/apps/forms/types/types.ts +++ b/apps/forms/types/types.ts @@ -63,6 +63,7 @@ export type FormInputType = | "checkboxes" | "switch" | "toggle" + | "toggle-group" | "radio" | "number" | "date" From b227a2f30d05f1a477c25121e09981fc4b758416 Mon Sep 17 00:00:00 2001 From: Universe Date: Fri, 10 May 2024 19:33:32 +0900 Subject: [PATCH 08/31] add shadcn overrides --- .../formfield/form-field-preview.tsx | 124 ++++++++++++++---- .../schema/examples/003-fields/form.json | 2 +- 2 files changed, 101 insertions(+), 25 deletions(-) diff --git a/apps/forms/components/formfield/form-field-preview.tsx b/apps/forms/components/formfield/form-field-preview.tsx index 869260405..e8eddcf08 100644 --- a/apps/forms/components/formfield/form-field-preview.tsx +++ b/apps/forms/components/formfield/form-field-preview.tsx @@ -24,6 +24,9 @@ import { Switch } from "../ui/switch"; import { Slider } from "../ui/slider"; import { Toggle } from "../ui/toggle"; import { ToggleGroup, ToggleGroupItem } from "../ui/toggle-group"; +import { RadioGroup, RadioGroupItem } from "../ui/radio-group"; +import { Textarea } from "../ui/textarea"; +import { Input } from "../ui/input"; /** * this disables the auto zoom in input text tag safari on iphone by setting font-size to 16px @@ -109,11 +112,67 @@ export function FormFieldPreview({ // step: novalidate ? undefined : data?.step, }; + function renderChildren({ + name, + label, + src, + }: { + name: string; + label?: string; + src?: string | null; + }) { + return ( + <> + {label || name} + {src && ( + // eslint-disable-next-line @next/next/no-img-element + {label + )} + + ); + } + function renderInput() { switch (type) { + case "text": + case "tel": + case "email": + case "number": + case "url": + case "password": { + if (vanilla) { + return ( + )} + /> + ); + } + + return ( + // @ts-ignore + )} + /> + ); + } case "textarea": { + if (vanilla) { + return ( + )} + /> + ); + } + return ( - )} /> ); @@ -154,34 +213,51 @@ export function FormFieldPreview({ } } case "radio": { + if (vanilla) { + return ( +
+ {options?.map((option) => ( +
+ )} + /> + +
+ ))} +
+ ); + } + + console.log(options); + return ( -
+ {options?.map((option) => ( -
- )} - /> -
+ ); } case "checkbox": { diff --git a/apps/forms/public/schema/examples/003-fields/form.json b/apps/forms/public/schema/examples/003-fields/form.json index b09b41cd4..cacccfb82 100644 --- a/apps/forms/public/schema/examples/003-fields/form.json +++ b/apps/forms/public/schema/examples/003-fields/form.json @@ -44,7 +44,7 @@ "options": ["A", "B", "C", "D", "E"] }, { - "name": "toggle", + "name": "âś”", "type": "toggle" }, { From 2c76f1f90822d9026b0ec39f1b13e62a1c4a2a7b Mon Sep 17 00:00:00 2001 From: Universe Date: Fri, 10 May 2024 20:31:17 +0900 Subject: [PATCH 09/31] add array support in shcema --- apps/forms/app/(dev)/playground/page.tsx | 41 ++++----- .../schema/examples/003-fields/form.json | 4 + apps/forms/public/schema/field.schema.json | 82 +++++++++++------- apps/forms/public/schema/form.schema.json | 2 +- apps/forms/types/schema.ts | 84 +++++++++++++++++-- apps/forms/types/types.ts | 1 + 6 files changed, 161 insertions(+), 53 deletions(-) diff --git a/apps/forms/app/(dev)/playground/page.tsx b/apps/forms/app/(dev)/playground/page.tsx index d16e1b091..26ccd328b 100644 --- a/apps/forms/app/(dev)/playground/page.tsx +++ b/apps/forms/app/(dev)/playground/page.tsx @@ -11,7 +11,13 @@ import { FormView } from "@/scaffolds/e/form"; import { Editor as MonacoEditor, useMonaco } from "@monaco-editor/react"; import { useEffect, useMemo, useState } from "react"; import { nanoid } from "nanoid"; -import { JSONField, JSONForm, JSONOptionLike } from "@/types/schema"; +import { + JSONField, + JSONForm, + JSONOptionLike, + parse, + parse_jsonfield_type, +} from "@/types/schema"; import resources from "@/k/i18n"; import { FormRenderer } from "@/lib/forms"; import { GridaLogo } from "@/components/grida-logo"; @@ -58,14 +64,6 @@ function toArrayOf(value: MaybeArray, nofalsy = true): NonNullable[] { ) as NonNullable[]; } -function parse(txt?: string): JSONForm | null { - try { - return txt ? JSON.parse(txt) : null; - } catch (error) { - return null; - } -} - function compile(txt?: string) { const schema = parse(txt); if (!schema) { @@ -93,16 +91,21 @@ function compile(txt?: string) { const renderer = new FormRenderer( nanoid(), - schema.fields?.map((f: JSONField, i) => ({ - ...f, - id: f.name, - autocomplete: toArrayOf( - f.autocomplete - ), - required: f.required || false, - local_index: i, - options: f.options?.map(map_option) || [], - })) || [], + schema.fields?.map((f: JSONField, i) => { + const { type, is_array } = parse_jsonfield_type(f.type); + return { + ...f, + id: f.name, + type: type, + is_array, + autocomplete: toArrayOf( + f.autocomplete + ), + required: f.required || false, + local_index: i, + options: f.options?.map(map_option) || [], + }; + }) || [], [] ); diff --git a/apps/forms/public/schema/examples/003-fields/form.json b/apps/forms/public/schema/examples/003-fields/form.json index cacccfb82..a39f1497c 100644 --- a/apps/forms/public/schema/examples/003-fields/form.json +++ b/apps/forms/public/schema/examples/003-fields/form.json @@ -15,6 +15,10 @@ "name": "email", "type": "email" }, + { + "name": "email[]", + "type": ["email"] + }, { "name": "password", "type": "password" diff --git a/apps/forms/public/schema/field.schema.json b/apps/forms/public/schema/field.schema.json index 70277ce15..5dde8deb3 100644 --- a/apps/forms/public/schema/field.schema.json +++ b/apps/forms/public/schema/field.schema.json @@ -26,6 +26,60 @@ "type": "boolean", "default": false }, + "type": { + "anyOf": [ + { "$ref": "#/definitions/type" }, + { + "type": "object", + "properties": { + "type": { "const": "array" }, + "items": { + "type": "object", + "properties": { + "type": { "$ref": "#/definitions/type" } + } + } + }, + "required": ["type"] + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/type" + }, + "minItems": 1, + "maxItems": 1 + } + ] + }, + "options": { + "type": "array", + "items": { + "anyOf": [ + { "$ref": "https://forms.grida.co/schema/option.schema.json" }, + { "type": "string" }, + { "type": "number" } + ], + "uniqueItems": true + } + }, + "autocomplete": { + "anyOf": [ + { + "$ref": "#/definitions/autocomplete" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/autocomplete" + }, + "uniqueItems": true + } + ] + } + }, + "required": ["name", "type"], + "definitions": { "type": { "type": "string", "enum": [ @@ -59,34 +113,6 @@ "range" ] }, - "options": { - "type": "array", - "items": { - "anyOf": [ - { "$ref": "https://forms.grida.co/schema/option.schema.json" }, - { "type": "string" }, - { "type": "number" } - ], - "uniqueItems": true - } - }, - "autocomplete": { - "anyOf": [ - { - "$ref": "#/definitions/autocomplete" - }, - { - "type": "array", - "items": { - "$ref": "#/definitions/autocomplete" - }, - "uniqueItems": true - } - ] - } - }, - "required": ["name", "type"], - "definitions": { "autocomplete": { "type": "array", "enum": [ diff --git a/apps/forms/public/schema/form.schema.json b/apps/forms/public/schema/form.schema.json index 8231d5644..c706d4f33 100644 --- a/apps/forms/public/schema/form.schema.json +++ b/apps/forms/public/schema/form.schema.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://forms.grida.co/schema/form.schema.json", "title": "Form", - "description": "A form schema", + "description": "Form JSON spec for Grida Forms - https://forms.grida.co", "type": "object", "properties": { "title": { diff --git a/apps/forms/types/schema.ts b/apps/forms/types/schema.ts index 1a19dc9d9..97f758787 100644 --- a/apps/forms/types/schema.ts +++ b/apps/forms/types/schema.ts @@ -1,6 +1,23 @@ import type { FormFieldAutocompleteType, FormInputType } from "."; -export interface JSONForm { +/** + * used when representing a type in json following the schema. + * + * `"enum" | ["enum"] | { type: "array", type: "enum" }` + */ +type JSONSchemaOptionalArrayDescriptor = + | T + | [T] + | { + type: "array"; + items: { + type: T; + }; + }; + +type JSONOptionalArrayAnnotation = T | [T]; + +interface _JSONForm { title?: string; name: string; description?: string; @@ -12,20 +29,30 @@ export interface JSONForm { method?: "get" | "post" | "dialog"; novalidate?: boolean; target?: "_blank" | "_self" | "_parent" | "_top"; - fields?: JSONField[]; + fields?: _JSONField[]; } -export interface JSONField { +export type JSONForm = _JSONForm>; +export type JSONFormRaw = _JSONForm< + JSONSchemaOptionalArrayDescriptor +>; +type _JSONField = { name: string; label?: string; placeholder?: string; required?: boolean; pattern?: string; - type: FormInputType; + type: T; options?: JSONOptionLike[]; multiple?: boolean; autocomplete?: FormFieldAutocompleteType | FormFieldAutocompleteType[]; -} +}; + +export type JSONFieldRaw = _JSONField< + JSONSchemaOptionalArrayDescriptor +>; + +export type JSONField = _JSONField>; export type JSONOptionLike = JSONOption | string | number; @@ -35,3 +62,50 @@ export interface JSONOption { src?: string; disabled?: boolean; } + +function json_optional_array_descriptor_to_annotation< + T extends string = string, +>( + descriptor: JSONSchemaOptionalArrayDescriptor +): JSONOptionalArrayAnnotation { + switch (typeof descriptor) { + case "string": + return descriptor; + case "object": + if (Array.isArray(descriptor)) { + return [descriptor[0]]; + } + return [descriptor.items.type]; + } +} + +export function parse_jsonfield_type( + descriptor: JSONSchemaOptionalArrayDescriptor +): { type: FormInputType; is_array: boolean } { + const annotation = json_optional_array_descriptor_to_annotation(descriptor); + if (Array.isArray(annotation)) { + return { type: annotation[0], is_array: true }; + } + return { type: annotation, is_array: false }; +} + +export function parse_jsonfield(raw: JSONFieldRaw): JSONField { + return { + ...raw, + type: json_optional_array_descriptor_to_annotation(raw.type), + }; +} + +export function parse(txt?: string): JSONForm | null | undefined { + try { + const shema_raw: JSONFormRaw = txt ? JSON.parse(txt) : null; + if (shema_raw) { + return { + ...shema_raw, + fields: shema_raw.fields?.map(parse_jsonfield), + }; + } + } catch (error) { + return null; + } +} diff --git a/apps/forms/types/types.ts b/apps/forms/types/types.ts index 9cb9c1be8..0351793ba 100644 --- a/apps/forms/types/types.ts +++ b/apps/forms/types/types.ts @@ -166,6 +166,7 @@ export interface IFormField { name: string; label?: string | null; type: FormInputType; + is_array?: boolean; placeholder?: string | null; required: boolean; help_text?: string | null; From c6732da576bbf6c34f1d8ace6b44d5691c41c16c Mon Sep 17 00:00:00 2001 From: Universe Date: Fri, 10 May 2024 20:56:00 +0900 Subject: [PATCH 10/31] poc - array field rendering --- .../formfield/form-field-preview.tsx | 104 +++++++++++++----- apps/forms/components/formfield/index.ts | 1 + apps/forms/lib/forms/renderer.ts | 1 + .../schema/examples/003-fields/form.json | 2 +- .../blocks-editor/blocks/field-block.tsx | 2 +- apps/forms/scaffolds/e/form/formview.tsx | 5 +- .../scaffolds/panels/field-edit-panel.tsx | 2 +- 7 files changed, 83 insertions(+), 34 deletions(-) diff --git a/apps/forms/components/formfield/form-field-preview.tsx b/apps/forms/components/formfield/form-field-preview.tsx index e8eddcf08..2f54fb199 100644 --- a/apps/forms/components/formfield/form-field-preview.tsx +++ b/apps/forms/components/formfield/form-field-preview.tsx @@ -17,7 +17,7 @@ import { SignatureCanvas } from "../signature-canvas"; import { StripePaymentFormFieldPreview } from "./form-field-preview-payment-stripe"; import { TossPaymentsPaymentFormFieldPreview } from "./form-field-preview-payment-tosspayments"; import clsx from "clsx"; -import { ClockIcon } from "@radix-ui/react-icons"; +import { ClockIcon, PlusIcon } from "@radix-ui/react-icons"; import { Checkbox } from "@/components/ui/checkbox"; import useSafeSelectValue from "./use-safe-select-value"; import { Switch } from "../ui/switch"; @@ -27,6 +27,7 @@ import { ToggleGroup, ToggleGroupItem } from "../ui/toggle-group"; import { RadioGroup, RadioGroupItem } from "../ui/radio-group"; import { Textarea } from "../ui/textarea"; import { Input } from "../ui/input"; +import { Button } from "../ui/button"; /** * this disables the auto zoom in input text tag safari on iphone by setting font-size to 16px @@ -34,29 +35,7 @@ import { Input } from "../ui/input"; */ const cls_input_ios_zoom_disable = "!text-base sm:!text-sm"; -export function FormFieldPreview({ - name, - label, - labelCapitalize, - type, - placeholder, - required, - requiredAsterisk, - defaultValue, - options, - helpText, - readonly, - disabled, - autoComplete, - accept, - multiple, - pattern, - data, - novalidate, - vanilla, - locked, - preview, -}: { +interface IInputField { name: string; label?: string; type: FormInputType; @@ -74,21 +53,86 @@ export function FormFieldPreview({ multiple?: boolean; labelCapitalize?: boolean; data?: FormFieldDataSchema | null; +} + +interface IFormField extends IInputField { novalidate?: boolean; - /** - * use vanilla html5 input element only - */ - vanilla?: boolean; /** * disable auto mutation of value when locked. * by default, the input values are only modified by user input, thus, there is a exception for select input for extra validation (e.g. useSafeSelectValue) */ locked?: boolean; +} + +interface IMonoFormFieldRenderingProps extends IFormField { + /** + * use vanilla html5 input element only + */ + vanilla?: boolean; /** * force render invisible field if true */ preview?: boolean; -}) { +} + +interface IFormFieldRenderingProps extends IMonoFormFieldRenderingProps { + is_array?: boolean; +} + +/** + * @beta is_array=true is experimental and only works on playground + * @returns + */ +function FormField({ is_array, ...props }: IFormFieldRenderingProps) { + const [n, setN] = useState(1); + if (is_array) { + return ( + <> +
+ + {Array.from({ length: n }).map((_, i) => ( + + ))} +
+ + ); + } + return ; +} + +function MonoFormField({ + name, + label, + labelCapitalize, + type, + placeholder, + required, + requiredAsterisk, + defaultValue, + options, + helpText, + readonly, + disabled, + autoComplete, + accept, + multiple, + pattern, + data, + novalidate, + vanilla, + locked, + preview, +}: IMonoFormFieldRenderingProps) { const sharedInputProps: | React.ComponentProps<"input"> | React.ComponentProps<"textarea"> = { @@ -667,3 +711,5 @@ function PaymentField({ return ; } } + +export default FormField; diff --git a/apps/forms/components/formfield/index.ts b/apps/forms/components/formfield/index.ts index d6d3706fb..766200715 100644 --- a/apps/forms/components/formfield/index.ts +++ b/apps/forms/components/formfield/index.ts @@ -1 +1,2 @@ export * from "./form-field-preview"; +export { default as default } from "./form-field-preview"; diff --git a/apps/forms/lib/forms/renderer.ts b/apps/forms/lib/forms/renderer.ts index 65783a833..9e868ae5d 100644 --- a/apps/forms/lib/forms/renderer.ts +++ b/apps/forms/lib/forms/renderer.ts @@ -40,6 +40,7 @@ export interface ClientFieldRenderBlock extends BaseRenderBlock { field: { id: string; type: FormInputType; + is_array?: boolean; name: string; label?: string; help_text?: string; diff --git a/apps/forms/public/schema/examples/003-fields/form.json b/apps/forms/public/schema/examples/003-fields/form.json index a39f1497c..49bf002a9 100644 --- a/apps/forms/public/schema/examples/003-fields/form.json +++ b/apps/forms/public/schema/examples/003-fields/form.json @@ -16,7 +16,7 @@ "type": "email" }, { - "name": "email[]", + "name": "emails", "type": ["email"] }, { diff --git a/apps/forms/scaffolds/blocks-editor/blocks/field-block.tsx b/apps/forms/scaffolds/blocks-editor/blocks/field-block.tsx index 0e6e45aee..dce6df981 100644 --- a/apps/forms/scaffolds/blocks-editor/blocks/field-block.tsx +++ b/apps/forms/scaffolds/blocks-editor/blocks/field-block.tsx @@ -23,7 +23,7 @@ import { import { useEditorState } from "@/scaffolds/editor"; import { FormFieldDefinition } from "@/types"; import Link from "next/link"; -import { FormFieldPreview } from "@/components/formfield"; +import FormFieldPreview from "@/components/formfield"; import { Select, SelectContent, diff --git a/apps/forms/scaffolds/e/form/formview.tsx b/apps/forms/scaffolds/e/form/formview.tsx index 6b4787f41..5311346d6 100644 --- a/apps/forms/scaffolds/e/form/formview.tsx +++ b/apps/forms/scaffolds/e/form/formview.tsx @@ -1,6 +1,6 @@ "use client"; -import { FormFieldPreview } from "@/components/formfield"; +import FormField from "@/components/formfield"; import { PoweredByGridaFooter } from "./powered-by-brand-footer"; import React, { useEffect, useMemo, useState } from "react"; import { FormBlockTree } from "@/lib/forms/types"; @@ -200,12 +200,13 @@ export function FormView({ const is_not_in_current_section = !context?.is_in_current_section; return ( - Date: Fri, 10 May 2024 21:00:45 +0900 Subject: [PATCH 11/31] mv --- .../formfield/{form-field-preview.tsx => form-field.tsx} | 0 apps/forms/components/formfield/index.ts | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename apps/forms/components/formfield/{form-field-preview.tsx => form-field.tsx} (100%) diff --git a/apps/forms/components/formfield/form-field-preview.tsx b/apps/forms/components/formfield/form-field.tsx similarity index 100% rename from apps/forms/components/formfield/form-field-preview.tsx rename to apps/forms/components/formfield/form-field.tsx diff --git a/apps/forms/components/formfield/index.ts b/apps/forms/components/formfield/index.ts index 766200715..9868aa77d 100644 --- a/apps/forms/components/formfield/index.ts +++ b/apps/forms/components/formfield/index.ts @@ -1,2 +1,2 @@ -export * from "./form-field-preview"; -export { default as default } from "./form-field-preview"; +export * from "./form-field"; +export { default as default } from "./form-field"; From 3edb496ca48211c4494fbfff0562c4c672c33ad3 Mon Sep 17 00:00:00 2001 From: Universe Date: Sat, 11 May 2024 21:39:18 +0900 Subject: [PATCH 12/31] schema --- apps/forms/app/(dev)/playground/page.tsx | 24 ++++++++++++++++--- .../schema/examples/003-fields/form.json | 15 ++++++++++++ apps/forms/public/schema/field.schema.json | 4 ++++ 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/apps/forms/app/(dev)/playground/page.tsx b/apps/forms/app/(dev)/playground/page.tsx index 26ccd328b..d1a1ff5c3 100644 --- a/apps/forms/app/(dev)/playground/page.tsx +++ b/apps/forms/app/(dev)/playground/page.tsx @@ -23,6 +23,13 @@ import { FormRenderer } from "@/lib/forms"; import { GridaLogo } from "@/components/grida-logo"; import { FormFieldAutocompleteType, Option } from "@/types"; import Ajv from "ajv"; +import { Button } from "@/components/ui/button"; +import { + Link1Icon, + Link2Icon, + RocketIcon, + SlashIcon, +} from "@radix-ui/react-icons"; const HOST = process.env.NEXT_PUBLIC_HOST_NAME || "http://localhost:3000"; @@ -135,8 +142,8 @@ export default function FormsPlayground() { return (
-
-
+
+

Forms @@ -144,7 +151,8 @@ export default function FormsPlayground() { Playground

-
+ +
+
+ + +
diff --git a/apps/forms/public/schema/examples/003-fields/form.json b/apps/forms/public/schema/examples/003-fields/form.json index 49bf002a9..85b28e00e 100644 --- a/apps/forms/public/schema/examples/003-fields/form.json +++ b/apps/forms/public/schema/examples/003-fields/form.json @@ -19,6 +19,21 @@ "name": "emails", "type": ["email"] }, + { + "name": "todos", + "type": "object", + "schema": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "done": { + "type": "boolean" + } + } + } + }, { "name": "password", "type": "password" diff --git a/apps/forms/public/schema/field.schema.json b/apps/forms/public/schema/field.schema.json index 5dde8deb3..db2c0d00c 100644 --- a/apps/forms/public/schema/field.schema.json +++ b/apps/forms/public/schema/field.schema.json @@ -29,6 +29,7 @@ "type": { "anyOf": [ { "$ref": "#/definitions/type" }, + { "const": "object" }, { "type": "object", "properties": { @@ -52,6 +53,9 @@ } ] }, + "schema": { + "$ref": "https://json-schema.org/draft-07/schema#" + }, "options": { "type": "array", "items": { From df840309eda3acc74eda0c7267c41f3fbc729ffe Mon Sep 17 00:00:00 2001 From: Universe Date: Sun, 12 May 2024 02:39:43 +0900 Subject: [PATCH 13/31] playground prep --- apps/forms/app/(dev)/playground/page.tsx | 44 ++++++++++++++---------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/apps/forms/app/(dev)/playground/page.tsx b/apps/forms/app/(dev)/playground/page.tsx index d1a1ff5c3..83e33ca6e 100644 --- a/apps/forms/app/(dev)/playground/page.tsx +++ b/apps/forms/app/(dev)/playground/page.tsx @@ -22,14 +22,9 @@ import resources from "@/k/i18n"; import { FormRenderer } from "@/lib/forms"; import { GridaLogo } from "@/components/grida-logo"; import { FormFieldAutocompleteType, Option } from "@/types"; -import Ajv from "ajv"; import { Button } from "@/components/ui/button"; -import { - Link1Icon, - Link2Icon, - RocketIcon, - SlashIcon, -} from "@radix-ui/react-icons"; +import { Link2Icon, RocketIcon, SlashIcon } from "@radix-ui/react-icons"; +import Ajv from "ajv"; const HOST = process.env.NEXT_PUBLIC_HOST_NAME || "http://localhost:3000"; @@ -120,8 +115,6 @@ function compile(txt?: string) { } export default function FormsPlayground() { - const [action, setAction] = useState(""); - const [method, setMethod] = useState("get"); const [exampleId, setExampleId] = useState(examples[0].id); const [__schema_txt, __set_schema_txt] = useState(); @@ -181,18 +174,31 @@ export default function FormsPlayground() {
-
+
-
-
+
+
-
- Renderer JSON -
-                {JSON.stringify(renderer, null, 2)}
-              
-
+
+
+ Data + +
+
@@ -252,12 +258,14 @@ function Editor({ onChange={onChange} value={value} options={{ + automaticLayout: true, padding: { top: 16, }, minimap: { enabled: false, }, + scrollBeyondLastLine: false, }} />
From 97361a97b704a771cdfa49cc4146509bf9224e97 Mon Sep 17 00:00:00 2001 From: Universe Date: Sun, 12 May 2024 03:38:03 +0900 Subject: [PATCH 14/31] playground sharing --- apps/forms/app/(d)/d/[id]/layout.tsx | 1 - apps/forms/app/(dev)/layout.tsx | 3 + .../app/(dev)/playground/[slug]/page.tsx | 34 ++ apps/forms/app/(dev)/playground/page.tsx | 270 +-------------- .../forms/app/(dev)/playground/share/route.ts | 22 ++ apps/forms/scaffolds/playground/index.ts | 1 + .../forms/scaffolds/playground/playground.tsx | 313 ++++++++++++++++++ apps/forms/types/supabase.ts | 24 ++ 8 files changed, 400 insertions(+), 268 deletions(-) create mode 100644 apps/forms/app/(dev)/playground/[slug]/page.tsx create mode 100644 apps/forms/app/(dev)/playground/share/route.ts create mode 100644 apps/forms/scaffolds/playground/index.ts create mode 100644 apps/forms/scaffolds/playground/playground.tsx diff --git a/apps/forms/app/(d)/d/[id]/layout.tsx b/apps/forms/app/(d)/d/[id]/layout.tsx index 8ec5a68da..985c44834 100644 --- a/apps/forms/app/(d)/d/[id]/layout.tsx +++ b/apps/forms/app/(d)/d/[id]/layout.tsx @@ -5,7 +5,6 @@ import { cookies } from "next/headers"; import { createServerComponentClient } from "@/lib/supabase/server"; import { GridaLogo } from "@/components/grida-logo"; import { EyeOpenIcon, SlashIcon } from "@radix-ui/react-icons"; -import { Toaster } from "react-hot-toast"; import { Tabs } from "@/scaffolds/d/tabs"; import { FormEditorProvider } from "@/scaffolds/editor"; import type { Metadata } from "next"; diff --git a/apps/forms/app/(dev)/layout.tsx b/apps/forms/app/(dev)/layout.tsx index 94edff814..7a86bb027 100644 --- a/apps/forms/app/(dev)/layout.tsx +++ b/apps/forms/app/(dev)/layout.tsx @@ -1,6 +1,8 @@ import type { Metadata } from "next"; import { Inter } from "next/font/google"; import { ThemeProvider } from "@/components/theme-provider"; +import { Toaster } from "react-hot-toast"; + import "../editor.css"; const inter = Inter({ subsets: ["latin"] }); @@ -17,6 +19,7 @@ export default function RootLayout({ return ( + + +
+ ); +} diff --git a/apps/forms/app/(dev)/playground/page.tsx b/apps/forms/app/(dev)/playground/page.tsx index 83e33ca6e..3e2380ba1 100644 --- a/apps/forms/app/(dev)/playground/page.tsx +++ b/apps/forms/app/(dev)/playground/page.tsx @@ -1,273 +1,9 @@ -"use client"; - -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { FormView } from "@/scaffolds/e/form"; -import { Editor as MonacoEditor, useMonaco } from "@monaco-editor/react"; -import { useEffect, useMemo, useState } from "react"; -import { nanoid } from "nanoid"; -import { - JSONField, - JSONForm, - JSONOptionLike, - parse, - parse_jsonfield_type, -} from "@/types/schema"; -import resources from "@/k/i18n"; -import { FormRenderer } from "@/lib/forms"; -import { GridaLogo } from "@/components/grida-logo"; -import { FormFieldAutocompleteType, Option } from "@/types"; -import { Button } from "@/components/ui/button"; -import { Link2Icon, RocketIcon, SlashIcon } from "@radix-ui/react-icons"; -import Ajv from "ajv"; - -const HOST = process.env.NEXT_PUBLIC_HOST_NAME || "http://localhost:3000"; - -const examples = [ - { - id: "001-hello-world", - name: "Hello World", - template: { - schema: { - src: `${HOST}/schema/examples/001-hello-world/form.json`, - }, - }, - }, - { - id: "002-iphone-pre-order", - name: "iPhone Pre-Order", - template: { - schema: { - src: `${HOST}/schema/examples/002-iphone-pre-order/form.json`, - }, - }, - }, - { - id: "003-fields", - name: "Fields", - template: { - schema: { - src: `${HOST}/schema/examples/003-fields/form.json`, - }, - }, - }, -] as const; - -type MaybeArray = T | T[]; - -function toArrayOf(value: MaybeArray, nofalsy = true): NonNullable[] { - return ( - Array.isArray(value) ? value : nofalsy && value ? [value] : [] - ) as NonNullable[]; -} - -function compile(txt?: string) { - const schema = parse(txt); - if (!schema) { - return; - } - - const map_option = (o: JSONOptionLike): Option => { - switch (typeof o) { - case "string": - case "number": { - return { - id: String(o), - value: String(o), - label: String(o), - }; - } - case "object": { - return { - ...o, - id: o.value, - }; - } - } - }; - - const renderer = new FormRenderer( - nanoid(), - schema.fields?.map((f: JSONField, i) => { - const { type, is_array } = parse_jsonfield_type(f.type); - return { - ...f, - id: f.name, - type: type, - is_array, - autocomplete: toArrayOf( - f.autocomplete - ), - required: f.required || false, - local_index: i, - options: f.options?.map(map_option) || [], - }; - }) || [], - [] - ); - - return renderer; -} +import Playground from "@/scaffolds/playground"; export default function FormsPlayground() { - const [exampleId, setExampleId] = useState(examples[0].id); - const [__schema_txt, __set_schema_txt] = useState(); - - const renderer: FormRenderer | undefined = useMemo( - () => compile(__schema_txt), - [__schema_txt] - ); - - useEffect(() => { - if (exampleId) { - fetch(examples.find((e) => e.id === exampleId)!.template.schema.src) - .then((res) => res.text()) - .then((schema) => { - __set_schema_txt(schema); - }); - } - }, [exampleId]); - return ( -
-
-
-

- - Forms - - Playground - -

- -
- -
-
-
- - -
-
-
-
-
-
- -
-
-
- Data - -
-
-
-
-
- {renderer ? ( - - ) : ( -
- Invalid schema -
- )} -
-
+
+
); } - -const schema = { - uri: "https://forms.grida.co/schema/form.schema.json", - fileMatch: ["*"], // Associate with all JSON files -}; - -function Editor({ - value, - onChange, -}: { - value?: string; - onChange?: (value?: string) => void; -}) { - const monaco = useMonaco(); - - useEffect(() => { - monaco?.languages.json.jsonDefaults.setDiagnosticsOptions({ - validate: true, - enableSchemaRequest: true, - schemas: [schema], - }); - }, [monaco]); - - return ( -
-
-

form.json

-
- -
- ); -} diff --git a/apps/forms/app/(dev)/playground/share/route.ts b/apps/forms/app/(dev)/playground/share/route.ts new file mode 100644 index 000000000..786d11932 --- /dev/null +++ b/apps/forms/app/(dev)/playground/share/route.ts @@ -0,0 +1,22 @@ +import { createRouteHandlerClient } from "@/lib/supabase/server"; +import assert from "assert"; +import { cookies } from "next/headers"; +import { NextRequest, NextResponse } from "next/server"; + +export async function POST(req: NextRequest) { + const body = await req.json(); + const cookieStore = cookies(); + const supabase = createRouteHandlerClient(cookieStore); + + const { data } = await supabase + .from("playground_gist") + .insert({ + gist: body, + }) + .select() + .single(); + + assert(data, "Failed to create a new playground gist"); + + return NextResponse.json(data); +} diff --git a/apps/forms/scaffolds/playground/index.ts b/apps/forms/scaffolds/playground/index.ts new file mode 100644 index 000000000..081bda032 --- /dev/null +++ b/apps/forms/scaffolds/playground/index.ts @@ -0,0 +1 @@ +export { Playground as default } from "./playground"; diff --git a/apps/forms/scaffolds/playground/playground.tsx b/apps/forms/scaffolds/playground/playground.tsx new file mode 100644 index 000000000..32631e086 --- /dev/null +++ b/apps/forms/scaffolds/playground/playground.tsx @@ -0,0 +1,313 @@ +"use client"; + +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { FormView } from "@/scaffolds/e/form"; +import { Editor as MonacoEditor, useMonaco } from "@monaco-editor/react"; +import { useEffect, useMemo, useState } from "react"; +import { nanoid } from "nanoid"; +import { + JSONField, + JSONForm, + JSONOptionLike, + parse, + parse_jsonfield_type, +} from "@/types/schema"; +import resources from "@/k/i18n"; +import { FormRenderer } from "@/lib/forms"; +import { GridaLogo } from "@/components/grida-logo"; +import { FormFieldAutocompleteType, Option } from "@/types"; +import { Button } from "@/components/ui/button"; +import { Link2Icon, RocketIcon, SlashIcon } from "@radix-ui/react-icons"; +import { createClientFormsClient } from "@/lib/supabase/client"; +import toast from "react-hot-toast"; +import Ajv from "ajv"; +import { useRouter } from "next/navigation"; + +const HOST = process.env.NEXT_PUBLIC_HOST_NAME || "http://localhost:3000"; + +const examples = [ + { + id: "001-hello-world", + name: "Hello World", + template: { + schema: { + src: `${HOST}/schema/examples/001-hello-world/form.json`, + }, + }, + }, + { + id: "002-iphone-pre-order", + name: "iPhone Pre-Order", + template: { + schema: { + src: `${HOST}/schema/examples/002-iphone-pre-order/form.json`, + }, + }, + }, + { + id: "003-fields", + name: "Fields", + template: { + schema: { + src: `${HOST}/schema/examples/003-fields/form.json`, + }, + }, + }, +] as const; + +type MaybeArray = T | T[]; + +function toArrayOf(value: MaybeArray, nofalsy = true): NonNullable[] { + return ( + Array.isArray(value) ? value : nofalsy && value ? [value] : [] + ) as NonNullable[]; +} + +function compile(txt?: string) { + const schema = parse(txt); + if (!schema) { + return; + } + + const map_option = (o: JSONOptionLike): Option => { + switch (typeof o) { + case "string": + case "number": { + return { + id: String(o), + value: String(o), + label: String(o), + }; + } + case "object": { + return { + ...o, + id: o.value, + }; + } + } + }; + + const renderer = new FormRenderer( + nanoid(), + schema.fields?.map((f: JSONField, i) => { + const { type, is_array } = parse_jsonfield_type(f.type); + return { + ...f, + id: f.name, + type: type, + is_array, + autocomplete: toArrayOf( + f.autocomplete + ), + required: f.required || false, + local_index: i, + options: f.options?.map(map_option) || [], + }; + }) || [], + [] + ); + + return renderer; +} + +export function Playground({ + initial, + slug, +}: { + initial?: string; + slug?: string; +}) { + const router = useRouter(); + const [is_modified, set_is_modified] = useState(false); + const [exampleId, setExampleId] = useState( + initial ? undefined : examples[0].id + ); + const [__schema_txt, __set_schema_txt] = useState( + initial + ); + + const renderer: FormRenderer | undefined = useMemo( + () => compile(__schema_txt), + [__schema_txt] + ); + + useEffect(() => { + if (exampleId) { + fetch(examples.find((e) => e.id === exampleId)!.template.schema.src) + .then((res) => res.text()) + .then((schema) => { + __set_schema_txt(schema); + }); + } + }, [exampleId]); + + const onShare = async () => { + const supabase = createClientFormsClient(); + const { data, error } = await supabase + .from("playground_gist") + .insert({ + gist: JSON.parse(__schema_txt!), + }) + .select("slug") + .single(); + + if (error) { + toast.error("Failed"); + } + + // update the route + router.replace(`/playground/${data!.slug}`); + }; + + return ( +
+
+
+

+ + Forms + + Playground + +

+ +
+ +
+ {slug && !is_modified && } +
+
+ + +
+
+
+
+
+
+ { + __set_schema_txt(v); + set_is_modified(true); + }} + /> +
+
+
+ Data + +
+
+
+
+
+ {renderer ? ( + + ) : ( +
+ Invalid schema +
+ )} +
+
+
+ ); +} + +const schema = { + uri: "https://forms.grida.co/schema/form.schema.json", + fileMatch: ["*"], // Associate with all JSON files +}; + +function Editor({ + value, + onChange, +}: { + value?: string; + onChange?: (value?: string) => void; +}) { + const monaco = useMonaco(); + + useEffect(() => { + monaco?.languages.json.jsonDefaults.setDiagnosticsOptions({ + validate: true, + enableSchemaRequest: true, + schemas: [schema], + }); + }, [monaco]); + + return ( +
+
+

form.json

+
+ +
+ ); +} diff --git a/apps/forms/types/supabase.ts b/apps/forms/types/supabase.ts index a1ddb4637..76db90076 100644 --- a/apps/forms/types/supabase.ts +++ b/apps/forms/types/supabase.ts @@ -1001,6 +1001,7 @@ export type Database = { form_id: string help_text: string | null id: string + is_array: boolean label: string | null local_index: number max: Json | null @@ -1027,6 +1028,7 @@ export type Database = { form_id: string help_text?: string | null id?: string + is_array?: boolean label?: string | null local_index?: number max?: Json | null @@ -1053,6 +1055,7 @@ export type Database = { form_id?: string help_text?: string | null id?: string + is_array?: boolean label?: string | null local_index?: number max?: Json | null @@ -1163,6 +1166,27 @@ export type Database = { }, ] } + playground_gist: { + Row: { + created_at: string + gist: Json + id: number + slug: string | null + } + Insert: { + created_at?: string + gist: Json + id?: number + slug?: string | null + } + Update: { + created_at?: string + gist?: Json + id?: number + slug?: string | null + } + Relationships: [] + } response: { Row: { browser: string | null From c8a09033c775b41f669d7c125f024b69dd02a4e2 Mon Sep 17 00:00:00 2001 From: Universe Date: Sun, 12 May 2024 03:38:15 +0900 Subject: [PATCH 15/31] form default styles --- apps/forms/components/formfield/form-field.tsx | 4 ++-- apps/forms/scaffolds/e/form/formview.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/forms/components/formfield/form-field.tsx b/apps/forms/components/formfield/form-field.tsx index 2f54fb199..1c81132ca 100644 --- a/apps/forms/components/formfield/form-field.tsx +++ b/apps/forms/components/formfield/form-field.tsx @@ -657,7 +657,7 @@ function HtmlTextarea({ ...props }: React.ComponentProps<"textarea">) { return (