From 6202193a77d424a2ca678821a3c279541fa213c7 Mon Sep 17 00:00:00 2001 From: Universe Date: Mon, 2 Sep 2024 00:15:58 +0900 Subject: [PATCH 01/32] rename type lang --- apps/forms/app/(api)/v1/[id]/route.ts | 4 +- apps/forms/database.types.ts | 43 ++++++++------- apps/forms/i18n/resources.ts | 4 +- apps/forms/k/supported_field_types.ts | 1 + apps/forms/k/supported_languages.ts | 8 +-- apps/forms/lib/forms/renderer.ts | 4 +- apps/forms/lib/formstate/core/agent.tsx | 3 +- apps/forms/scaffolds/editor/action.ts | 4 +- apps/forms/scaffolds/editor/state.ts | 52 +++++++++++-------- .../scaffolds/playground/preview/index.tsx | 4 +- .../custom-ending-page-preferences.tsx | 6 +-- .../sidecontrol/sidecontrol-global.tsx | 4 +- apps/forms/types/schema.ts | 4 +- apps/forms/types/types.ts | 6 +-- 14 files changed, 79 insertions(+), 68 deletions(-) diff --git a/apps/forms/app/(api)/v1/[id]/route.ts b/apps/forms/app/(api)/v1/[id]/route.ts index cbe9fbd451..18c21b1905 100644 --- a/apps/forms/app/(api)/v1/[id]/route.ts +++ b/apps/forms/app/(api)/v1/[id]/route.ts @@ -38,7 +38,7 @@ import type { FormMethod, FormDocument, Option, - FormsPageLanguage, + LanguageCode, } from "@/types"; import { Features } from "@/lib/features/scheduling"; import { requesterurl, resolverurl } from "@/services/form/session-storage"; @@ -201,7 +201,7 @@ export async function GET( store_connection, } = data; - const lang: FormsPageLanguage = + const lang: LanguageCode = (default_page as unknown as FormDocument | null)?.lang ?? "en"; const is_powered_by_branding_enabled = (default_page as unknown as FormDocument | null) diff --git a/apps/forms/database.types.ts b/apps/forms/database.types.ts index 23142d2bcf..0754bb3893 100644 --- a/apps/forms/database.types.ts +++ b/apps/forms/database.types.ts @@ -799,6 +799,7 @@ export type Database = { is_scheduling_enabled: boolean max_form_responses_by_customer: number | null max_form_responses_in_total: number | null + name: string project_id: number scheduling_close_at: string | null scheduling_open_at: string | null @@ -819,6 +820,7 @@ export type Database = { is_scheduling_enabled?: boolean max_form_responses_by_customer?: number | null max_form_responses_in_total?: number | null + name?: string project_id: number scheduling_close_at?: string | null scheduling_open_at?: string | null @@ -839,6 +841,7 @@ export type Database = { is_scheduling_enabled?: boolean max_form_responses_by_customer?: number | null max_form_responses_in_total?: number | null + name?: string project_id?: number scheduling_close_at?: string | null scheduling_open_at?: string | null @@ -957,6 +960,7 @@ export type Database = { } form_document: { Row: { + __name: string background: Json | null created_at: string ending_page_i18n_overrides: Json | null @@ -966,14 +970,14 @@ export type Database = { is_ending_page_enabled: boolean is_powered_by_branding_enabled: boolean is_redirect_after_response_uri_enabled: boolean - lang: Database["grida_forms"]["Enums"]["form_page_language"] + lang: Database["public"]["Enums"]["language_code"] method: Database["grida_forms"]["Enums"]["form_method"] - name: string project_id: number redirect_after_response_uri: string | null stylesheet: Json | null } Insert: { + __name?: string background?: Json | null created_at?: string ending_page_i18n_overrides?: Json | null @@ -983,14 +987,14 @@ export type Database = { is_ending_page_enabled?: boolean is_powered_by_branding_enabled?: boolean is_redirect_after_response_uri_enabled?: boolean - lang?: Database["grida_forms"]["Enums"]["form_page_language"] + lang?: Database["public"]["Enums"]["language_code"] method?: Database["grida_forms"]["Enums"]["form_method"] - name?: string project_id: number redirect_after_response_uri?: string | null stylesheet?: Json | null } Update: { + __name?: string background?: Json | null created_at?: string ending_page_i18n_overrides?: Json | null @@ -1000,9 +1004,8 @@ export type Database = { is_ending_page_enabled?: boolean is_powered_by_branding_enabled?: boolean is_redirect_after_response_uri_enabled?: boolean - lang?: Database["grida_forms"]["Enums"]["form_page_language"] + lang?: Database["public"]["Enums"]["language_code"] method?: Database["grida_forms"]["Enums"]["form_method"] - name?: string project_id?: number redirect_after_response_uri?: string | null stylesheet?: Json | null @@ -1767,20 +1770,6 @@ export type Database = { | "video" | "json" form_method: "post" | "get" | "dialog" - form_page_language: - | "en" - | "ko" - | "es" - | "de" - | "ja" - | "fr" - | "pt" - | "it" - | "ru" - | "zh" - | "ar" - | "hi" - | "nl" form_response_unknown_field_handling_strategy_type: | "ignore" | "accept" @@ -2432,6 +2421,20 @@ export type Database = { } Enums: { doctype: "v0_form" | "v0_site" | "v0_schema" + language_code: + | "en" + | "ko" + | "es" + | "de" + | "ja" + | "fr" + | "pt" + | "it" + | "ru" + | "zh" + | "ar" + | "hi" + | "nl" } CompositeTypes: { [_ in never]: never diff --git a/apps/forms/i18n/resources.ts b/apps/forms/i18n/resources.ts index 22bb46f431..0ba6712185 100644 --- a/apps/forms/i18n/resources.ts +++ b/apps/forms/i18n/resources.ts @@ -1,6 +1,6 @@ import { TemplateVariables } from "@/lib/templating"; import type { ObjectPath } from "@/lib/templating/@types"; -import type { FormsPageLanguage } from "@/types"; +import type { LanguageCode } from "@/types"; type T = ObjectPath< TemplateVariables.FormResponseContext & { @@ -79,7 +79,7 @@ export interface Translation { } const resources: Record< - FormsPageLanguage, + LanguageCode, { translation: Translation; } diff --git a/apps/forms/k/supported_field_types.ts b/apps/forms/k/supported_field_types.ts index b6784f9849..bb92c4ac43 100644 --- a/apps/forms/k/supported_field_types.ts +++ b/apps/forms/k/supported_field_types.ts @@ -206,6 +206,7 @@ const html5_checkbox_alias_field_types: FormInputType[] = [ const html5_placeholder_not_supported_field_types: FormInputType[] = [ ...html5_file_alias_field_types, ...html5_checkbox_alias_field_types, + "hidden", "toggle", "toggle-group", "radio", diff --git a/apps/forms/k/supported_languages.ts b/apps/forms/k/supported_languages.ts index d5a261c462..4cd53285ee 100644 --- a/apps/forms/k/supported_languages.ts +++ b/apps/forms/k/supported_languages.ts @@ -1,11 +1,11 @@ -import { FormsPageLanguage } from "@/types"; +import { LanguageCode } from "@/types"; import resources from "@/i18n"; -export const supported_form_page_languages: FormsPageLanguage[] = Object.keys( +export const supported_form_page_languages: LanguageCode[] = Object.keys( resources -) as FormsPageLanguage[]; +) as LanguageCode[]; -export const language_label_map: Record = { +export const language_label_map: Record = { en: "English", es: "Spanish / Español", de: "German / Deutsch", diff --git a/apps/forms/lib/forms/renderer.ts b/apps/forms/lib/forms/renderer.ts index e989042e5c..caf6f4cf40 100644 --- a/apps/forms/lib/forms/renderer.ts +++ b/apps/forms/lib/forms/renderer.ts @@ -5,7 +5,7 @@ import type { FormFieldDefinition, FormBlock, Option, - FormsPageLanguage, + LanguageCode, } from "@/types"; import { blockstree } from "./tree"; import { FormBlockTree } from "./types"; @@ -187,7 +187,7 @@ export class FormRenderTree { readonly id: string, readonly title: string | null | undefined, readonly description: string | null | undefined, - readonly lang: FormsPageLanguage | null | undefined, + readonly lang: LanguageCode | null | undefined, private readonly _m_fields: FormFieldDefinition[] = [], private readonly _m_blocks?: FormBlock[] | null, private readonly config?: RenderTreeConfig, diff --git a/apps/forms/lib/formstate/core/agent.tsx b/apps/forms/lib/formstate/core/agent.tsx index caecbf68bc..c0f42e41d0 100644 --- a/apps/forms/lib/formstate/core/agent.tsx +++ b/apps/forms/lib/formstate/core/agent.tsx @@ -9,10 +9,9 @@ export function FormAgentProvider({ }: React.PropsWithChildren<{ initial: FormAgentState }>) { const [state, dispatch] = React.useReducer(reducer, initial); - // console.log("FormAgentProvider", state.tree, initial.tree); - useEffect(() => { dispatch({ type: "refresh", state: initial }); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [initial.tree]); return ( diff --git a/apps/forms/scaffolds/editor/action.ts b/apps/forms/scaffolds/editor/action.ts index 76d1641054..130b98000e 100644 --- a/apps/forms/scaffolds/editor/action.ts +++ b/apps/forms/scaffolds/editor/action.ts @@ -9,7 +9,7 @@ import type { FormResponse, FormResponseWithFields, FormStyleSheetV1Schema, - FormsPageLanguage, + LanguageCode, GridaXSupabase, } from "@/types"; import type { @@ -336,7 +336,7 @@ export interface FeedXSupabaseMainTableRowsAction { export interface EditorThemeLangAction { type: "editor/theme/lang"; - lang: FormsPageLanguage; + lang: LanguageCode; } export interface EditorThemePoweredByBrandingAction { diff --git a/apps/forms/scaffolds/editor/state.ts b/apps/forms/scaffolds/editor/state.ts index ca62459246..b25492da83 100644 --- a/apps/forms/scaffolds/editor/state.ts +++ b/apps/forms/scaffolds/editor/state.ts @@ -16,7 +16,7 @@ import type { FormResponseSession, FormResponseUnknownFieldHandlingStrategyType, FormStyleSheetV1Schema, - FormsPageLanguage, + LanguageCode, GDocumentType, GridaXSupabase, OrderBy, @@ -317,26 +317,7 @@ interface IInsertionMenuState { insertmenu: TGlobalEditorDialogState; } -export interface BaseDocumentEditorState - extends IEditorGlobalSavingState, - IEditorDateContextState, - IEditorAssetsState, - IInsertionMenuState, - IFieldEditorState, - ICustomerEditorState, - IRowEditorState { - basepath: string; - organization: { - name: string; - id: number; - }; - project: { - name: string; - id: number; - }; - document_id: string; - document_title: string; - doctype: GDocumentType; +interface IEditorDocumentState { document: { pages: MenuItem[]; selected_page_id?: string; @@ -362,9 +343,12 @@ export interface BaseDocumentEditorState selected_node_default_text?: Tokens.StringValueExpression; selected_node_context?: Record; }; +} + +interface IEditorDocumentThemeState { theme: { is_powered_by_branding_enabled: boolean; - lang: FormsPageLanguage; + lang: LanguageCode; appearance: Appearance; palette?: FormStyleSheetV1Schema["palette"]; fontFamily: FontFamily; @@ -374,6 +358,30 @@ export interface BaseDocumentEditorState }; } +export interface BaseDocumentEditorState + extends IEditorGlobalSavingState, + IEditorDateContextState, + IEditorAssetsState, + IInsertionMenuState, + IFieldEditorState, + IEditorDocumentState, + ICustomerEditorState, + IEditorDocumentThemeState, + IRowEditorState { + basepath: string; + organization: { + name: string; + id: number; + }; + project: { + name: string; + id: number; + }; + document_id: string; + document_title: string; + doctype: GDocumentType; +} + interface IFieldEditorState { field_editor: TGlobalEditorDialogState<{ draft: Partial | null; diff --git a/apps/forms/scaffolds/playground/preview/index.tsx b/apps/forms/scaffolds/playground/preview/index.tsx index e5d74c6433..1c59eb21f0 100644 --- a/apps/forms/scaffolds/playground/preview/index.tsx +++ b/apps/forms/scaffolds/playground/preview/index.tsx @@ -8,7 +8,7 @@ import resources from "@/i18n"; import { nanoid } from "nanoid"; import { ExclamationTriangleIcon } from "@radix-ui/react-icons"; import useVariablesCSS from "../use-variables-css"; -import { FormsPageLanguage } from "@/types"; +import { LanguageCode } from "@/types"; import { useTheme } from "next-themes"; import type { FormEventMessage, @@ -170,7 +170,7 @@ export function PlaygroundPreviewSlave() { }, []); const [renderer, invalid] = useRenderer(schema); - const lang: FormsPageLanguage = (renderer?.lang ?? "en") as FormsPageLanguage; + const lang: LanguageCode = (renderer?.lang ?? "en") as LanguageCode; return ( <> diff --git a/apps/forms/scaffolds/settings/customize/custom-ending-page-preferences.tsx b/apps/forms/scaffolds/settings/customize/custom-ending-page-preferences.tsx index 576c9f599c..ba9b7c66d6 100644 --- a/apps/forms/scaffolds/settings/customize/custom-ending-page-preferences.tsx +++ b/apps/forms/scaffolds/settings/customize/custom-ending-page-preferences.tsx @@ -29,7 +29,7 @@ import toast from "react-hot-toast"; import type { EndingPageI18nOverrides, EndingPageTemplateID, - FormsPageLanguage, + LanguageCode, } from "@/types"; import { render, @@ -220,7 +220,7 @@ function Preview({ }: { template: EndingPageTemplateID; title: string; - lang: FormsPageLanguage; + lang: LanguageCode; overrides?: Record; }) { const { t } = useTranslation(); @@ -288,7 +288,7 @@ function CustomizeTemplate({ template_id?: EndingPageTemplateID; i18n_overrides?: Record; }; - lang: FormsPageLanguage; + lang: LanguageCode; title: string; form_id: string; onSave?: (template_id: string, data: Record) => void; diff --git a/apps/forms/scaffolds/sidecontrol/sidecontrol-global.tsx b/apps/forms/scaffolds/sidecontrol/sidecontrol-global.tsx index 8e9c84a9d8..c14f21e503 100644 --- a/apps/forms/scaffolds/sidecontrol/sidecontrol-global.tsx +++ b/apps/forms/scaffolds/sidecontrol/sidecontrol-global.tsx @@ -26,7 +26,7 @@ import { Appearance, FontFamily, FormStyleSheetV1Schema, - FormsPageLanguage, + LanguageCode, } from "@/types"; import * as _variants from "@/theme/palettes"; import { PaletteColorChip } from "@/components/design/palette-color-chip"; @@ -516,7 +516,7 @@ function Settings() { } = state; const onLangChange = useCallback( - (lang: FormsPageLanguage) => { + (lang: LanguageCode) => { dispatch({ type: "editor/theme/lang", lang, diff --git a/apps/forms/types/schema.ts b/apps/forms/types/schema.ts index ae172f174d..992eb9b88b 100644 --- a/apps/forms/types/schema.ts +++ b/apps/forms/types/schema.ts @@ -3,7 +3,7 @@ import type { FormFieldAutocompleteType, FormFieldDefinition, FormInputType, - FormsPageLanguage, + LanguageCode, Option, } from "./types"; import { toArrayOf, MaybeArray } from "./utility"; @@ -44,7 +44,7 @@ interface _JSONForm { title?: string; name: string; description?: string; - lang?: FormsPageLanguage; + lang?: LanguageCode; action?: string; enctype?: | "application/x-www-form-urlencoded" diff --git a/apps/forms/types/types.ts b/apps/forms/types/types.ts index 23059b2db8..d0286993bd 100644 --- a/apps/forms/types/types.ts +++ b/apps/forms/types/types.ts @@ -46,9 +46,9 @@ export interface Customer { } /** - * user facing page language + * Grida supported languages */ -export type FormsPageLanguage = +export type LanguageCode = | "en" | "es" | "de" @@ -233,7 +233,7 @@ export interface FormDocument { stylesheet?: FormStyleSheetV1Schema; is_redirect_after_response_uri_enabled: boolean; redirect_after_response_uri: string | null; - lang: FormsPageLanguage; + lang: LanguageCode; is_powered_by_branding_enabled: boolean; is_ending_page_enabled: boolean; ending_page_template_id: string | null; From ac6414603eb5bbe288dbff04fa57eedfd40355f9 Mon Sep 17 00:00:00 2001 From: Universe Date: Mon, 2 Sep 2024 00:53:10 +0900 Subject: [PATCH 02/32] footer preview --- apps/forms/i18n/resources.common.ts | 99 +++++++++++++++++++ apps/forms/i18n/resources.ts | 79 +++------------ apps/forms/k/supported_languages.ts | 26 ++--- .../scaffolds/blocks-editor/blocks-editor.tsx | 35 +++++++ 4 files changed, 161 insertions(+), 78 deletions(-) create mode 100644 apps/forms/i18n/resources.common.ts diff --git a/apps/forms/i18n/resources.common.ts b/apps/forms/i18n/resources.common.ts new file mode 100644 index 0000000000..125a407ed6 --- /dev/null +++ b/apps/forms/i18n/resources.common.ts @@ -0,0 +1,99 @@ +/// +/// use for .min usage. +/// + +const common = { + en: { + next: "Next", + back: "Previous", + submit: "Submit", + pay: "Pay", + home: "Home", + }, + es: { + next: "Siguiente", + back: "Anterior", + submit: "Enviar", + pay: "Pagar", + home: "Inicio", + }, + ko: { + next: "다음", + back: "이전", + submit: "제출", + pay: "결제", + home: "홈", + }, + ja: { + next: "次へ", + back: "戻る", + submit: "提出する", + pay: "支払う", + home: "ホーム", + }, + zh: { + next: "下一步", + back: "上一步", + submit: "提交", + pay: "支付", + home: "首页", + }, + fr: { + next: "Suivant", + back: "Précédent", + submit: "Soumettre", + pay: "Payer", + home: "Accueil", + }, + pt: { + next: "Próximo", + back: "Anterior", + submit: "Enviar", + pay: "Pagar", + home: "Início", + }, + it: { + next: "Avanti", + back: "Indietro", + submit: "Invia", + pay: "Paga", + home: "Home", + }, + de: { + next: "Weiter", + back: "Zurück", + submit: "Einreichen", + pay: "Bezahlen", + home: "Startseite", + }, + ru: { + next: "Далее", + back: "Назад", + submit: "Отправить", + pay: "Оплатить", + home: "Главная", + }, + ar: { + next: "التالي", + back: "السابق", + submit: "إرسال", + pay: "دفع", + home: "الرئيسية", + }, + hi: { + next: "अगला", + back: "पिछला", + submit: "जमा करें", + pay: "भुगतान करें", + home: "होम", + }, + nl: { + next: "Volgende", + back: "Vorige", + submit: "Indienen", + pay: "Betalen", + home: "Home", + }, +}; + +export default common; diff --git a/apps/forms/i18n/resources.ts b/apps/forms/i18n/resources.ts index 0ba6712185..eb7ecce539 100644 --- a/apps/forms/i18n/resources.ts +++ b/apps/forms/i18n/resources.ts @@ -1,6 +1,7 @@ import { TemplateVariables } from "@/lib/templating"; import type { ObjectPath } from "@/lib/templating/@types"; import type { LanguageCode } from "@/types"; +import common from "./resources.common"; type T = ObjectPath< TemplateVariables.FormResponseContext & { @@ -86,11 +87,7 @@ const resources: Record< > = { en: { translation: { - next: "Next", - back: "Previous", - submit: "Submit", - pay: "Pay", - home: "Home", + ...common.en, left_in_stock: `${use("available")} left`, sold_out: "Sold Out", support_metadata: `Support Metadata`, @@ -152,11 +149,7 @@ const resources: Record< }, es: { translation: { - next: "Siguiente", - back: "Anterior", - submit: "Enviar", - pay: "Pagar", - home: "Inicio", + ...common.es, left_in_stock: `${use("available")} restantes`, sold_out: "Agotado", support_metadata: `Metadatos de soporte`, @@ -218,11 +211,7 @@ const resources: Record< }, ko: { translation: { - next: "다음", - back: "이전", - submit: "제출", - pay: "결제", - home: "홈", + ...common.ko, left_in_stock: `${use("available")}개 남음`, sold_out: "매진됨", support_metadata: `서포트 메타데이터`, @@ -283,11 +272,7 @@ const resources: Record< }, ja: { translation: { - next: "次へ", - back: "戻る", - submit: "提出する", - pay: "支払う", - home: "ホーム", + ...common.ja, left_in_stock: `残り${use("available")}個`, sold_out: "完売", support_metadata: `サポートメタデータ`, @@ -348,11 +333,7 @@ const resources: Record< }, zh: { translation: { - next: "下一步", - back: "上一步", - submit: "提交", - pay: "支付", - home: "首页", + ...common.zh, left_in_stock: `剩余${use("available")}件`, sold_out: "售罄", support_metadata: `支持元数据`, @@ -410,11 +391,7 @@ const resources: Record< }, fr: { translation: { - next: "Suivant", - back: "Précédent", - submit: "Soumettre", - pay: "Payer", - home: "Accueil", + ...common.fr, left_in_stock: `${use("available")} restants`, sold_out: "Épuisé", support_metadata: `Métadonnées de support`, @@ -476,11 +453,7 @@ const resources: Record< }, pt: { translation: { - next: "Próximo", - back: "Anterior", - submit: "Enviar", - pay: "Pagar", - home: "Início", + ...common.pt, left_in_stock: `${use("available")} restantes`, sold_out: "Esgotado", support_metadata: `Metadados de suporte`, @@ -542,11 +515,7 @@ const resources: Record< }, it: { translation: { - next: "Avanti", - back: "Indietro", - submit: "Invia", - pay: "Paga", - home: "Home", + ...common.it, left_in_stock: `${use("available")} rimasti`, sold_out: "Esaurito", support_metadata: `Metadati di supporto`, @@ -608,11 +577,7 @@ const resources: Record< }, de: { translation: { - next: "Weiter", - back: "Zurück", - submit: "Einreichen", - pay: "Bezahlen", - home: "Startseite", + ...common.de, left_in_stock: `${use("available")} übrig`, sold_out: "Ausverkauft", support_metadata: `Support-Metadaten`, @@ -674,11 +639,7 @@ const resources: Record< }, ru: { translation: { - next: "Далее", - back: "Назад", - submit: "Отправить", - pay: "Оплатить", - home: "Главная", + ...common.ru, left_in_stock: `Осталось ${use("available")} шт.`, sold_out: "Распродано", support_metadata: `Метаданные поддержки`, @@ -738,11 +699,7 @@ const resources: Record< }, ar: { translation: { - next: "التالي", - back: "السابق", - submit: "إرسال", - pay: "دفع", - home: "الرئيسية", + ...common.ar, left_in_stock: `${use("available")} متبقية`, sold_out: "نفذت الكمية", support_metadata: `بيانات الدعم الوصفية`, @@ -802,11 +759,7 @@ const resources: Record< }, hi: { translation: { - next: "अगला", - back: "पिछला", - submit: "जमा करें", - pay: "भुगतान करें", - home: "होम", + ...common.hi, left_in_stock: `${use("available")} बचे हैं`, sold_out: "बिक गया", support_metadata: `सहायता मेटाडाटा`, @@ -867,11 +820,7 @@ const resources: Record< }, nl: { translation: { - next: "Volgende", - back: "Vorige", - submit: "Indienen", - pay: "Betalen", - home: "Home", + ...common.nl, left_in_stock: `Nog ${use("available")} beschikbaar`, sold_out: "Uitverkocht", support_metadata: `Ondersteuningsmetadata`, diff --git a/apps/forms/k/supported_languages.ts b/apps/forms/k/supported_languages.ts index 4cd53285ee..00244ed924 100644 --- a/apps/forms/k/supported_languages.ts +++ b/apps/forms/k/supported_languages.ts @@ -6,17 +6,17 @@ export const supported_form_page_languages: LanguageCode[] = Object.keys( ) as LanguageCode[]; export const language_label_map: Record = { - en: "English", - es: "Spanish / Español", - de: "German / Deutsch", - ja: "Japanese / 日本語", - fr: "French / Français", - pt: "Portuguese / Português", - it: "Italian / Italiano", - ko: "Korean / 한국어", - ru: "Russian / Русский", - zh: "Chinese / 中文", - ar: "Arabic / العربية", - hi: "Hindi / हिन्दी", - nl: "Dutch / Nederlands", + en: "🇺🇸 English", + es: "🇪🇸 Spanish / Español", + de: "🇩🇪 German / Deutsch", + ja: "🇯🇵 Japanese / 日本語", + fr: "🇫🇷 French / Français", + pt: "🇵🇹 Portuguese / Português", + it: "🇮🇹 Italian / Italiano", + ko: "🇰🇷 Korean / 한국어", + ru: "🇷🇺 Russian / Русский", + zh: "🇨🇳 Chinese / 中文", + ar: "🇸🇦 Arabic / العربية", + hi: "🇮🇳 Hindi / हिन्दी", + nl: "🇳🇱 Dutch / Nederlands", }; diff --git a/apps/forms/scaffolds/blocks-editor/blocks-editor.tsx b/apps/forms/scaffolds/blocks-editor/blocks-editor.tsx index ad35d14349..6822053206 100644 --- a/apps/forms/scaffolds/blocks-editor/blocks-editor.tsx +++ b/apps/forms/scaffolds/blocks-editor/blocks-editor.tsx @@ -24,6 +24,10 @@ import { usePrevious } from "@uidotdev/usehooks"; import equal from "deep-equal"; import { FormPageBackgroundSchema, FormStyleSheetV1Schema } from "@/types"; import { FormAgentProvider, initdummy } from "@/lib/formstate"; +import { Button } from "@/components/ui/button"; +import { PoweredByGridaFooter } from "@/scaffolds/e/form/powered-by-brand-footer"; +import clsx from "clsx"; +import common from "@/i18n/resources.common"; export default function BlocksEditorRoot() { return ( @@ -311,11 +315,42 @@ function BlocksEditor() { + ); } +function FooterPreview() { + const [state] = useEditorState(); + + const { + theme: { is_powered_by_branding_enabled, lang }, + } = state; + + return ( + <> +
+ +
+ {is_powered_by_branding_enabled && ( +
+ +
+ )} + + ); +} + function shallowEqual(obj1: any, obj2: any) { if (obj1 === obj2) return true; From 6b2c6aaaf5620f49e916e13a181bf108532a71bd Mon Sep 17 00:00:00 2001 From: Universe Date: Mon, 2 Sep 2024 03:12:06 +0900 Subject: [PATCH 03/32] add stepperize --- apps/forms/package.json | 1 + pnpm-lock.yaml | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/apps/forms/package.json b/apps/forms/package.json index 6920b98869..65746092f4 100644 --- a/apps/forms/package.json +++ b/apps/forms/package.json @@ -52,6 +52,7 @@ "@radix-ui/react-toolbar": "^1.0.3", "@radix-ui/react-tooltip": "^1.0.7", "@react-email/components": "^0.0.18", + "@stepperize/react": "^3.0.1", "@supabase/auth-helpers-nextjs": "^0.9.0", "@supabase/postgrest-js": "^1.15.5", "@supabase/ssr": "^0.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c51513cbc1..f209ac72fe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -383,6 +383,9 @@ importers: '@react-email/components': specifier: ^0.0.18 version: 0.0.18(@types/react@18.2.58)(react@18.3.1) + '@stepperize/react': + specifier: ^3.0.1 + version: 3.0.1(react@18.3.1) '@supabase/auth-helpers-nextjs': specifier: ^0.9.0 version: 0.9.0(@supabase/supabase-js@2.43.5) @@ -18629,6 +18632,14 @@ packages: '@stdlib/utils-global': 0.0.7 dev: false + /@stepperize/react@3.0.1(react@18.3.1): + resolution: {integrity: sha512-1U9oN3hVghVvGpQSo7ZTPYunY+9YcqTk3qL+T6f4yQ6gi8Tv+5mpqovuOlsN6Ao3Zu7e60FUz7WtidZt/ll6SQ==} + peerDependencies: + react: ^18.2.0 + dependencies: + react: 18.3.1 + dev: false + /@stitches/react@1.2.8(react@18.3.1): resolution: {integrity: sha512-9g9dWI4gsSVe8bNLlb+lMkBYsnIKCZTmvqvDG+Avnn69XfmHZKiaMrx7cgTaddq7aTPPmXiTsbFcUy0xgI4+wA==} peerDependencies: From df517ad530b11de3d732561375f0316b4a1848f6 Mon Sep 17 00:00:00 2001 From: Universe Date: Mon, 2 Sep 2024 03:12:18 +0900 Subject: [PATCH 04/32] sidebar styles --- apps/forms/components/sidebar/index.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/forms/components/sidebar/index.tsx b/apps/forms/components/sidebar/index.tsx index 15292ce075..ba20adcb89 100644 --- a/apps/forms/components/sidebar/index.tsx +++ b/apps/forms/components/sidebar/index.tsx @@ -104,7 +104,10 @@ export function SidebarMenuLink({ return ( {/* override selected prop */} - {React.cloneElement(children as any, { selected })} + {React.cloneElement(children as any, { + selected, + className: "cursor-pointer", + })} ); } @@ -149,7 +152,7 @@ export const SidebarMenuItem = React.forwardRef(function SidebarMenuItem( "relative group", "w-full px-2 py-1 rounded text-sm font-medium text-foreground", "text-ellipsis whitespace-nowrap overflow-hidden", - "hover:bg-accent hover:text-accent-foreground", + "hover:bg-accent hover:text-accent-foreground cursor-default", "data-[muted='true']:text-muted-foreground", "data-[disabled='true']:cursor-not-allowed data-[disabled='true']:opacity-40 data-[disabled='true']:bg-background", "data-[selected='true']:bg-accent data-[selected='true']:text-accent-foreground", From 5d04c9e43a503c2658554abfc16420075e348e68 Mon Sep 17 00:00:00 2001 From: Universe Date: Mon, 2 Sep 2024 03:13:30 +0900 Subject: [PATCH 05/32] ui --- .../scaffolds/sidecontrol/controls/hidden.tsx | 7 +++++- .../sidecontrol/controls/plaintext.tsx | 22 +++++++++++++++++++ apps/forms/scaffolds/sidecontrol/ui/index.tsx | 10 +++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 apps/forms/scaffolds/sidecontrol/controls/plaintext.tsx diff --git a/apps/forms/scaffolds/sidecontrol/controls/hidden.tsx b/apps/forms/scaffolds/sidecontrol/controls/hidden.tsx index 2ab2ecbea5..e3cb224747 100644 --- a/apps/forms/scaffolds/sidecontrol/controls/hidden.tsx +++ b/apps/forms/scaffolds/sidecontrol/controls/hidden.tsx @@ -1,4 +1,5 @@ import { Switch } from "@/components/ui/switch"; +import { PropertyLineControlRoot } from "../ui"; export function HiddenControl({ value, @@ -7,5 +8,9 @@ export function HiddenControl({ value?: boolean; onValueChange?: (value: boolean) => void; }) { - return ; + return ( + + + + ); } diff --git a/apps/forms/scaffolds/sidecontrol/controls/plaintext.tsx b/apps/forms/scaffolds/sidecontrol/controls/plaintext.tsx new file mode 100644 index 0000000000..7d21c9bc39 --- /dev/null +++ b/apps/forms/scaffolds/sidecontrol/controls/plaintext.tsx @@ -0,0 +1,22 @@ +import { Input } from "@/components/ui/input"; +import { inputVariants } from "./utils/input-variants"; + +export function PlainTextControl({ + value, + onValueChange, +}: { + value?: string; + onValueChange?: (value?: string) => void; +}) { + return ( + { + onValueChange?.(e.target.value || undefined); + }} + /> + ); +} diff --git a/apps/forms/scaffolds/sidecontrol/ui/index.tsx b/apps/forms/scaffolds/sidecontrol/ui/index.tsx index a64928f09b..a9a763cbae 100644 --- a/apps/forms/scaffolds/sidecontrol/ui/index.tsx +++ b/apps/forms/scaffolds/sidecontrol/ui/index.tsx @@ -15,3 +15,13 @@ export function PropertyLineLabel({ children }: React.PropsWithChildren<{}>) { ); } + +/** + * use for matching the height with the label - useful with `` + * @returns + */ +export function PropertyLineControlRoot({ + children, +}: React.PropsWithChildren<{}>) { + return
{children}
; +} From d2e20781730010a08cddc83419f1bbda196fdf89 Mon Sep 17 00:00:00 2001 From: Universe Date: Mon, 2 Sep 2024 03:15:07 +0900 Subject: [PATCH 06/32] wip - language control --- .../scaffolds/sidebar/sidebar-mode-blocks.tsx | 40 ++- .../sidecontrol/sidecontrol-doctype-form.tsx | 241 +++++++++++------ .../sidecontrol/sidecontrol-global.tsx | 242 ++++++++++++++---- 3 files changed, 390 insertions(+), 133 deletions(-) diff --git a/apps/forms/scaffolds/sidebar/sidebar-mode-blocks.tsx b/apps/forms/scaffolds/sidebar/sidebar-mode-blocks.tsx index a16d574de4..baac7aeff8 100644 --- a/apps/forms/scaffolds/sidebar/sidebar-mode-blocks.tsx +++ b/apps/forms/scaffolds/sidebar/sidebar-mode-blocks.tsx @@ -10,6 +10,7 @@ import { SidebarMenuItemAction, SidebarSectionHeaderItem, SidebarSectionHeaderLabel, + SidebarMenuItemLabel, } from "@/components/sidebar"; import { EditorFlatFormBlock, MenuItem } from "../editor/state"; import { FormBlockType, FormFieldDefinition, FormInputType } from "@/types"; @@ -20,7 +21,7 @@ import { CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; -import { DotsHorizontalIcon } from "@radix-ui/react-icons"; +import { DotsHorizontalIcon, GlobeIcon } from "@radix-ui/react-icons"; import { DropdownMenu, DropdownMenuContent, @@ -57,10 +58,47 @@ export function ModeDesign() { )} + + {/* WIP */} + {process.env.NODE_ENV === "development" && } ); } +function LocalizationView() { + return ( + + {/* + Localization + */} + + + + + Translations + + + + + + 🇺🇸 + + en + + + + + + 🇰🇷 + + ko + + + + + ); +} + function FormBlockHierarchyList() { const [state, dispatch] = useEditorState(); const fields = useFormFields(); diff --git a/apps/forms/scaffolds/sidecontrol/sidecontrol-doctype-form.tsx b/apps/forms/scaffolds/sidecontrol/sidecontrol-doctype-form.tsx index 8955ad375d..0e240d59a6 100644 --- a/apps/forms/scaffolds/sidecontrol/sidecontrol-doctype-form.tsx +++ b/apps/forms/scaffolds/sidecontrol/sidecontrol-doctype-form.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from "react"; +import React, { useCallback, useMemo, useState } from "react"; import { SidebarMenuSectionContent, SidebarSection, @@ -16,12 +16,13 @@ import { import { useEditorState, useFormFields } from "@/scaffolds/editor"; import { MixIcon } from "@radix-ui/react-icons"; import { Tokens } from "@/ast"; -import { KeyIcon } from "lucide-react"; -import toast from "react-hot-toast"; import { FormExpression } from "@/lib/forms/expression"; import { PropertyLine, PropertyLineLabel } from "./ui"; import { EditBinaryExpression } from "../panels/extensions/v-edit"; import { PopoverClose } from "@radix-ui/react-popover"; +import { PlainTextControl } from "./controls/plaintext"; +import { FieldSupports } from "@/k/supported_field_types"; +import toast from "react-hot-toast"; export function SideControlDoctypeForm() { const [state, dispatch] = useEditorState(); @@ -34,26 +35,76 @@ export function SideControlDoctypeForm() { } /** - * NOTE - the type string represents a id, not a scalar for this component, atm. - * TODO: support scalar types + * use within the context where focus_block_id is set + * @returns */ -type ConditionExpression = Tokens.ShorthandBooleanBinaryExpression; - -function SelectedFormBlockProperties() { - // +function useFocusedFormBlock() { const [state, dispatch] = useEditorState(); - const fields = useFormFields(); + + if (!state.focus_block_id) { + throw new Error("No block focused"); + } const block = useMemo( - () => state.blocks.find((b) => b.id === state.focus_block_id), + () => state.blocks.find((b) => b.id === state.focus_block_id)!, [state.blocks, state.focus_block_id] ); + const set_v_hidden = useCallback( + (exp: Tokens.ShorthandBooleanBinaryExpression) => { + dispatch({ + type: "blocks/hidden", + block_id: block.id, + v_hidden: exp, + }); + }, + [block] + ); + + return [block, { set_v_hidden }] as const; +} + +function useFormField(id: string) { + const fields = useFormFields(); + return useMemo(() => fields.find((f) => f.id === id), [fields, id]); +} + +function SelectedFormBlockProperties() { + const [block] = useFocusedFormBlock(); + + return ( +
+ + + Block + + + + Hidden + + + + + {block.type === "field" && } + {block.type === "header" && } +
+ ); +} + +/** + * NOTE - the type string represents a id, not a scalar for this component, atm. + * TODO: support scalar types + */ +type ConditionExpression = Tokens.ShorthandBooleanBinaryExpression; + +function PropertyV_Hidden() { + const [block, { set_v_hidden }] = useFocusedFormBlock(); + + const fields = useFormFields(); + const [condition_v_hidden, set_condition_v_hidden] = useState(); - // console.log("block?.v_hidden", block?.v_hidden); - const _v_hidden_set = !!block?.v_hidden; const onSave = (e: any) => { @@ -71,73 +122,53 @@ function SelectedFormBlockProperties() { r, ]; - // - dispatch({ - type: "blocks/hidden", - block_id: block!.id, - v_hidden: exp, - }); + set_v_hidden(exp); }; return ( -
- - - Block - - - - Hidden - - -
- -
-
- -
- - ({ - type: "form_field_value", - identifier: f.id, - label: f.name, - }))} - rightOptions={(left) => - fields - .find((f) => f.id === left) - ?.options?.map((o) => ({ - type: "option_value_reference", - identifier: o.id, - label: o.label || o.value, - })) - } - defaultValue={condition_v_hidden} - onValueChange={(value) => { - set_condition_v_hidden(value); - }} - /> - - - -
-
-
-
-
-
- + + +
+ +
+
+ +
+ + ({ + type: "form_field_value", + identifier: f.id, + label: f.name, + }))} + rightOptions={(left) => + fields + .find((f) => f.id === left) + ?.options?.map((o) => ({ + type: "option_value_reference", + identifier: o.id, + label: o.label || o.value, + })) + } + defaultValue={condition_v_hidden} + onValueChange={(value) => { + set_condition_v_hidden(value); + }} + /> + + + +
+
{/*
{_v_hidden_set ? ( @@ -147,6 +178,60 @@ function SelectedFormBlockProperties() { "No condition set" )}
*/} -
+ + ); +} + +function BlockTypeField() { + const [block] = useFocusedFormBlock(); + const field = useFormField(block.form_field_id!)!; + const is_hidden_field = field.type === "hidden"; + + if (is_hidden_field) return <>; + + return ( + + + Customize + + + + Label + + + {FieldSupports.placeholder(field.type) && ( + + Placeholder + + + )} + + Help Text + + + + + ); +} + +function BlockTypeHeader() { + const [block] = useFocusedFormBlock(); + + return ( + + + Customize + + + + Title + + + + Description + + + + ); } diff --git a/apps/forms/scaffolds/sidecontrol/sidecontrol-global.tsx b/apps/forms/scaffolds/sidecontrol/sidecontrol-global.tsx index c14f21e503..0338662335 100644 --- a/apps/forms/scaffolds/sidecontrol/sidecontrol-global.tsx +++ b/apps/forms/scaffolds/sidecontrol/sidecontrol-global.tsx @@ -70,6 +70,10 @@ import { } from "@/k/supported_languages"; import { Switch } from "@/components/ui/switch"; import { PoweredByGridaWaterMark } from "@/components/powered-by-branding"; +import { PropertyLine, PropertyLineControlRoot, PropertyLineLabel } from "./ui"; +import { inputVariants } from "./controls/utils/input-variants"; +import { useDialogState } from "@/components/hooks/use-dialog-state"; +import { defineStepper } from "@stepperize/react"; const { default: all, ...variants } = _variants; @@ -108,6 +112,14 @@ export function SideControlGlobal() { + + + Language + + + + + Custom CSS @@ -128,6 +140,181 @@ export function SideControlGlobal() { ); } +function Language() { + const [state, dispatch] = useEditorState(); + const localizationSetupDialog = useDialogState("localization-setup", { + refreshkey: true, + }); + const { + theme: { lang }, + } = state; + + const onLangChange = useCallback( + (lang: LanguageCode) => { + dispatch({ + type: "editor/theme/lang", + lang, + }); + }, + [dispatch] + ); + + return ( + <> + + + Default + + + + Multiple + + { + if (checked) { + localizationSetupDialog.openDialog(); + } + }} + /> + + + + ); +} + +function LanguageSelect({ + name, + required, + value, + defaultValue, + onValueChange, + options = supported_form_page_languages, +}: { + name?: string; + required?: boolean; + value?: LanguageCode; + defaultValue?: LanguageCode; + onValueChange?: (value: LanguageCode) => void; + options?: LanguageCode[]; +}) { + return ( + + ); +} + +const { useStepper } = defineStepper( + { id: "fallbacklang" }, + { id: "firstlang" } +); + +function EnableMultiLanguageDialog({ + ...props +}: React.ComponentProps) { + const stepper = useStepper(); + + const [fallbackLang, setFallbackLang] = useState("en"); + + return ( + + + Setup Localization + + Ensure your content feels local everywhere, making every user feel + right at home. + +
+
{ + e.preventDefault(); + if (stepper.isLast) { + // do something + alert("done"); + } else { + stepper.next(); + } + }} + > + {stepper.when("fallbacklang", () => ( +
+ + +

+ + Your current contents will be used as the default language. + +
+ Choose a default language that will be used when a user's + preferred language isn't available. This ensures your content is + always accessible and understandable. +

+
+ ))} + {stepper.when("firstlang", () => ( +
+ + l !== fallbackLang + )} + /> +

+ Select the first language for localization. You can add or + delete languages later, so don't worry if you're unsure. +

+
+ ))} +
+
+ + + + + + +
+
+ ); +} + function FontFamilyControl() { const [state, dispatch] = useEditorState(); @@ -512,19 +699,9 @@ function CustomCSS() { function Settings() { const [state, dispatch] = useEditorState(); const { - theme: { lang, is_powered_by_branding_enabled }, + theme: { is_powered_by_branding_enabled }, } = state; - const onLangChange = useCallback( - (lang: LanguageCode) => { - dispatch({ - type: "editor/theme/lang", - lang, - }); - }, - [dispatch] - ); - const onPoweredByBrandingEnabledChange = useCallback( (enabled: boolean) => { dispatch({ @@ -550,51 +727,8 @@ function Settings() {
- Language Branding - - - Page Language} - description={ - <>Choose the language that your customers will be seeing. - } - /> - -
-
-
- - - The form page will be displayed in{" "} - - {language_label_map[lang]} - - -
-
-
-
-
-
Date: Mon, 2 Sep 2024 18:57:31 +0900 Subject: [PATCH 07/32] mv --- apps/forms/package.json | 2 +- .../{sidebar-mode-blocks.tsx => sidebar-mode-design.tsx} | 0 apps/forms/scaffolds/sidebar/sidebar.tsx | 2 +- apps/forms/scaffolds/sidecontrol/sidecontrol-global.tsx | 9 +++++---- 4 files changed, 7 insertions(+), 6 deletions(-) rename apps/forms/scaffolds/sidebar/{sidebar-mode-blocks.tsx => sidebar-mode-design.tsx} (100%) diff --git a/apps/forms/package.json b/apps/forms/package.json index 65746092f4..821cd8a288 100644 --- a/apps/forms/package.json +++ b/apps/forms/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "next dev --turbo", "build": "next build", "start": "next start", "lint": "next lint", diff --git a/apps/forms/scaffolds/sidebar/sidebar-mode-blocks.tsx b/apps/forms/scaffolds/sidebar/sidebar-mode-design.tsx similarity index 100% rename from apps/forms/scaffolds/sidebar/sidebar-mode-blocks.tsx rename to apps/forms/scaffolds/sidebar/sidebar-mode-design.tsx diff --git a/apps/forms/scaffolds/sidebar/sidebar.tsx b/apps/forms/scaffolds/sidebar/sidebar.tsx index ee54d3caa9..8aca2abcb5 100644 --- a/apps/forms/scaffolds/sidebar/sidebar.tsx +++ b/apps/forms/scaffolds/sidebar/sidebar.tsx @@ -16,7 +16,7 @@ import { SidebarSectionHeaderItem, SidebarSectionHeaderLabel, } from "@/components/sidebar"; -import { ModeDesign } from "./sidebar-mode-blocks"; +import { ModeDesign } from "./sidebar-mode-design"; import { editorlink } from "@/lib/forms/url"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useWorkspace } from "../workspace"; diff --git a/apps/forms/scaffolds/sidecontrol/sidecontrol-global.tsx b/apps/forms/scaffolds/sidecontrol/sidecontrol-global.tsx index 0338662335..6c34174c87 100644 --- a/apps/forms/scaffolds/sidecontrol/sidecontrol-global.tsx +++ b/apps/forms/scaffolds/sidecontrol/sidecontrol-global.tsx @@ -278,9 +278,9 @@ function EnableMultiLanguageDialog({ Your current contents will be used as the default language.
- Choose a default language that will be used when a user's - preferred language isn't available. This ensures your content is - always accessible and understandable. + Choose a default language that will be used when a user's + preferred language isn't available. This ensures your + content is always accessible and understandable.

))} @@ -296,7 +296,8 @@ function EnableMultiLanguageDialog({ />

Select the first language for localization. You can add or - delete languages later, so don't worry if you're unsure. + delete languages later, so don't worry if you're + unsure.

))} From 826b63b98545c6d22341168c60438a5558e8cd78 Mon Sep 17 00:00:00 2001 From: Universe Date: Mon, 2 Sep 2024 20:49:38 +0900 Subject: [PATCH 08/32] lang state --- .../[org]/[proj]/[id]/design/page.tsx | 1 - .../[org]/[proj]/[id]/form/edit/page.tsx | 1 - .../[org]/[proj]/[id]/form/page.tsx | 6 +--- .../(workbench)/[org]/[proj]/[id]/layout.tsx | 14 ++++++++-- .../(workbench)/[org]/[proj]/[id]/router.tsx | 28 +++++++++++++++++++ .../scaffolds/blocks-editor/blocks-editor.tsx | 9 +++--- apps/forms/scaffolds/editor/action.ts | 20 +++++++++---- apps/forms/scaffolds/editor/init.ts | 9 ++++++ apps/forms/scaffolds/editor/reducer.ts | 26 ++++++++++++----- apps/forms/scaffolds/editor/state.ts | 17 ++++++++++- .../custom-ending-page-preferences.tsx | 2 +- .../sidecontrol/sidecontrol-global.tsx | 4 +-- 12 files changed, 106 insertions(+), 31 deletions(-) create mode 100644 apps/forms/app/(workbench)/[org]/[proj]/[id]/router.tsx diff --git a/apps/forms/app/(workbench)/[org]/[proj]/[id]/design/page.tsx b/apps/forms/app/(workbench)/[org]/[proj]/[id]/design/page.tsx index d275f44497..551349005c 100644 --- a/apps/forms/app/(workbench)/[org]/[proj]/[id]/design/page.tsx +++ b/apps/forms/app/(workbench)/[org]/[proj]/[id]/design/page.tsx @@ -55,7 +55,6 @@ function CurrentPageCanvas() { const [state, dispatch] = useEditorState(); const { - theme: { lang }, document: { selected_page_id }, } = state; diff --git a/apps/forms/app/(workbench)/[org]/[proj]/[id]/form/edit/page.tsx b/apps/forms/app/(workbench)/[org]/[proj]/[id]/form/edit/page.tsx index 87f4eb79e8..17a94a8f80 100644 --- a/apps/forms/app/(workbench)/[org]/[proj]/[id]/form/edit/page.tsx +++ b/apps/forms/app/(workbench)/[org]/[proj]/[id]/form/edit/page.tsx @@ -53,7 +53,6 @@ function CurrentPageCanvas() { const [state, dispatch] = useEditorState(); const { - theme: { lang }, document: { selected_page_id }, } = state; diff --git a/apps/forms/app/(workbench)/[org]/[proj]/[id]/form/page.tsx b/apps/forms/app/(workbench)/[org]/[proj]/[id]/form/page.tsx index c6cd647a0e..c31404ede2 100644 --- a/apps/forms/app/(workbench)/[org]/[proj]/[id]/form/page.tsx +++ b/apps/forms/app/(workbench)/[org]/[proj]/[id]/form/page.tsx @@ -24,11 +24,7 @@ import { useEditorState } from "@/scaffolds/editor"; export default function FormDashboard() { const [state, dispatch] = useEditorState(); - const { - form, - theme: { lang }, - document: { selected_page_id }, - } = state; + const { form } = state; return (
diff --git a/apps/forms/app/(workbench)/[org]/[proj]/[id]/layout.tsx b/apps/forms/app/(workbench)/[org]/[proj]/[id]/layout.tsx index 2227947c93..777ae0acd9 100644 --- a/apps/forms/app/(workbench)/[org]/[proj]/[id]/layout.tsx +++ b/apps/forms/app/(workbench)/[org]/[proj]/[id]/layout.tsx @@ -39,6 +39,7 @@ import React from "react"; import { PlayActions } from "@/scaffolds/workbench/play-actions"; import { DontCastJsonProperties } from "@/types/supabase-ext"; import { SupabasePostgRESTOpenApi } from "@/lib/supabase-postgrest"; +import EditorRouterProvider from "./router"; export const revalidate = 0; @@ -174,7 +175,6 @@ export default async function Layout({ supabase: supabase_connection_state || undefined, }, theme: { - lang: data.lang, is_powered_by_branding_enabled: data.is_powered_by_branding_enabled, appearance: appearance, @@ -193,6 +193,9 @@ export default async function Layout({ form_id: form.id, // TODO: form_title: form.title, + document: { + lang: data.lang, + }, campaign: { is_scheduling_enabled: form.is_scheduling_enabled, is_force_closed: form.is_force_closed, @@ -258,9 +261,11 @@ export default async function Layout({ theme: { appearance: "system", fontFamily: "inter", - lang: "en", is_powered_by_branding_enabled: true, }, + document: { + lang: "en", + }, }} > {children} + diff --git a/apps/forms/app/(workbench)/[org]/[proj]/[id]/router.tsx b/apps/forms/app/(workbench)/[org]/[proj]/[id]/router.tsx new file mode 100644 index 0000000000..f156b5a5b9 --- /dev/null +++ b/apps/forms/app/(workbench)/[org]/[proj]/[id]/router.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { useEditorState } from "@/scaffolds/editor"; +import { usePathname } from "next/navigation"; +import { useEffect, useMemo } from "react"; + +export default function EditorRouterProvider() { + const [state, dispatch] = useEditorState(); + const pathname = usePathname(); + + const { params, workbenchpath } = useMemo(() => { + const [org, proj, docid, ...params] = pathname.split("/").filter(Boolean); + const workbenchpath = params.join("/"); + return { + org, + proj, + docid, + params, + workbenchpath, + }; + }, [pathname]); + + useEffect(() => { + dispatch({ type: "workbench/path", path: workbenchpath }); + }, [workbenchpath, dispatch]); + + return <>; +} diff --git a/apps/forms/scaffolds/blocks-editor/blocks-editor.tsx b/apps/forms/scaffolds/blocks-editor/blocks-editor.tsx index 6822053206..e92f7ec488 100644 --- a/apps/forms/scaffolds/blocks-editor/blocks-editor.tsx +++ b/apps/forms/scaffolds/blocks-editor/blocks-editor.tsx @@ -227,7 +227,7 @@ function OptimisticBlocksSyncProvider({ function AgentThemeSyncProvider({ children }: React.PropsWithChildren<{}>) { const [state] = useEditorState(); - const { document_id, theme } = state; + const { document_id, document, theme } = state; const prev = usePrevious(state.theme); const supabase = createClientFormsClient(); @@ -242,7 +242,7 @@ function AgentThemeSyncProvider({ children }: React.PropsWithChildren<{}>) { supabase .from("form_document") .update({ - lang: theme.lang, + lang: document.lang, is_powered_by_branding_enabled: theme.is_powered_by_branding_enabled, stylesheet: { appearance: theme.appearance, @@ -266,8 +266,8 @@ function AgentThemeSyncProvider({ children }: React.PropsWithChildren<{}>) { prev, supabase, document_id, + document.lang, theme.is_powered_by_branding_enabled, - theme.lang, theme.appearance, theme.customCSS, theme.fontFamily, @@ -325,7 +325,8 @@ function FooterPreview() { const [state] = useEditorState(); const { - theme: { is_powered_by_branding_enabled, lang }, + theme: { is_powered_by_branding_enabled }, + document: { lang }, } = state; return ( diff --git a/apps/forms/scaffolds/editor/action.ts b/apps/forms/scaffolds/editor/action.ts index 130b98000e..0f15d52efb 100644 --- a/apps/forms/scaffolds/editor/action.ts +++ b/apps/forms/scaffolds/editor/action.ts @@ -24,6 +24,7 @@ import { ZodObject } from "zod"; export type BlocksEditorAction = | GlobalSavingAction + | GlobalWorkbenchPathAction | EditorSidebarModeAction | CreateNewPendingBlockAction | ResolvePendingBlockAction @@ -64,7 +65,7 @@ export type BlocksEditorAction = | DataTableLoadingAction | DataGridCellChangeAction | FeedXSupabaseMainTableRowsAction - | EditorThemeLangAction + | EditorDocumentLangAction | EditorThemePoweredByBrandingAction | EditorThemePaletteAction | EditorThemeAppearanceAction @@ -90,6 +91,14 @@ export type GlobalSavingAction = { saving: boolean; }; +/** + * /[org]/[proj]/[docid]/[...workbenchpath] + */ +export type GlobalWorkbenchPathAction = { + type: "workbench/path"; + path: string; +}; + export interface EditorSidebarModeAction { type: "editor/sidebar/mode"; mode: "project" | "build" | "data" | "connect"; @@ -334,11 +343,6 @@ export interface FeedXSupabaseMainTableRowsAction { data: GridaXSupabase.XDataRow[]; } -export interface EditorThemeLangAction { - type: "editor/theme/lang"; - lang: LanguageCode; -} - export interface EditorThemePoweredByBrandingAction { type: "editor/theme/powered_by_branding"; enabled: boolean; @@ -383,6 +387,10 @@ export interface FormEndingPreferencesAction extends Partial { type: "editor/form/ending/preferences"; } +export interface EditorDocumentLangAction { + type: "editor/document/lang"; + lang: LanguageCode; +} export interface DocumentSelectPageAction { type: "editor/document/select-page"; diff --git a/apps/forms/scaffolds/editor/init.ts b/apps/forms/scaffolds/editor/init.ts index ebc90238e5..fdd5ed7d21 100644 --- a/apps/forms/scaffolds/editor/init.ts +++ b/apps/forms/scaffolds/editor/init.ts @@ -163,6 +163,9 @@ function initialDatabaseEditorState( supabase_project: init.supabase_project, connections: {}, document: { + lang: init.document.lang, + lang_default: init.document.lang, + langs: [init.document.lang], pages: [], nodes: [], templatedata: {}, @@ -239,6 +242,9 @@ function initialSiteEditorState(init: SiteDocumentEditorInit): EditorState { return { ...base, document: { + lang: init.document.lang, + lang_default: init.document.lang, + langs: [init.document.lang], pages: sitedocumentpagesinit({ basepath: base.basepath, document_id: init.document_id, @@ -443,6 +449,9 @@ function initialFormEditorState(init: FormDocumentEditorInit): EditorState { blocks: blockstreeflat(init.blocks), document: { + lang: init.document.lang, + lang_default: init.document.lang, + langs: [init.document.lang], pages: formdocumentpagesinit({ basepath: base.basepath, document_id: init.document_id, diff --git a/apps/forms/scaffolds/editor/reducer.ts b/apps/forms/scaffolds/editor/reducer.ts index bcf78ef0b6..04c2551272 100644 --- a/apps/forms/scaffolds/editor/reducer.ts +++ b/apps/forms/scaffolds/editor/reducer.ts @@ -14,6 +14,7 @@ import type { } from "./state"; import type { GlobalSavingAction, + GlobalWorkbenchPathAction, EditorSidebarModeAction, BlockDescriptionAction, BlockTitleAction, @@ -50,7 +51,7 @@ import type { FeedXSupabaseMainTableRowsAction, DataTableRefreshAction, DataTableLoadingAction, - EditorThemeLangAction, + EditorDocumentLangAction, EditorThemePaletteAction, EditorThemeFontFamilyAction, EditorThemeBackgroundAction, @@ -110,6 +111,17 @@ export function reducer( draft.saving = saving; }); } + case "workbench/path": { + // TODO: experiemntal + const { path } = action; + return produce(state, (draft) => { + if (path === "form/edit") { + draft.document.selected_page_id = "form"; + } else { + draft.document.selected_page_id = ""; + } + }); + } case "editor/sidebar/mode": { const { mode } = action; return produce(state, (draft) => { @@ -983,12 +995,6 @@ export function reducer( }); // } - case "editor/theme/lang": { - const { lang } = action; - return produce(state, (draft) => { - draft.theme.lang = lang; - }); - } case "editor/theme/powered_by_branding": { const { enabled } = action; return produce(state, (draft) => { @@ -1050,6 +1056,12 @@ export function reducer( }); } // + case "editor/document/lang": { + const { lang } = action; + return produce(state, (draft) => { + draft.document.lang = lang; + }); + } case "editor/document/select-page": { const { page_id } = action; diff --git a/apps/forms/scaffolds/editor/state.ts b/apps/forms/scaffolds/editor/state.ts index b25492da83..4ee7dea6cd 100644 --- a/apps/forms/scaffolds/editor/state.ts +++ b/apps/forms/scaffolds/editor/state.ts @@ -52,6 +52,9 @@ export interface BaseDocumentEditorInit { }; document_id: string; document_title: string; + document: { + lang: LanguageCode; + }; doctype: GDocumentType; theme: EditorState["theme"]; } @@ -319,6 +322,19 @@ interface IInsertionMenuState { interface IEditorDocumentState { document: { + /** + * view document in... + */ + lang: LanguageCode; + /** + * default language + */ + lang_default: LanguageCode; + /** + * available languages provided by user + */ + langs: LanguageCode[]; + pages: MenuItem[]; selected_page_id?: string; nodes: any[]; @@ -348,7 +364,6 @@ interface IEditorDocumentState { interface IEditorDocumentThemeState { theme: { is_powered_by_branding_enabled: boolean; - lang: LanguageCode; appearance: Appearance; palette?: FormStyleSheetV1Schema["palette"]; fontFamily: FontFamily; diff --git a/apps/forms/scaffolds/settings/customize/custom-ending-page-preferences.tsx b/apps/forms/scaffolds/settings/customize/custom-ending-page-preferences.tsx index ba9b7c66d6..04a1a76f9b 100644 --- a/apps/forms/scaffolds/settings/customize/custom-ending-page-preferences.tsx +++ b/apps/forms/scaffolds/settings/customize/custom-ending-page-preferences.tsx @@ -51,7 +51,7 @@ export function EndingPagePreferences() { const { form, - theme: { lang }, + document: { lang }, form: { ending }, } = state; diff --git a/apps/forms/scaffolds/sidecontrol/sidecontrol-global.tsx b/apps/forms/scaffolds/sidecontrol/sidecontrol-global.tsx index 6c34174c87..120dcdf556 100644 --- a/apps/forms/scaffolds/sidecontrol/sidecontrol-global.tsx +++ b/apps/forms/scaffolds/sidecontrol/sidecontrol-global.tsx @@ -146,13 +146,13 @@ function Language() { refreshkey: true, }); const { - theme: { lang }, + document: { lang }, } = state; const onLangChange = useCallback( (lang: LanguageCode) => { dispatch({ - type: "editor/theme/lang", + type: "editor/document/lang", lang, }); }, From 20a0d69bcd087699ea5d0fc33adb8664f4998bca Mon Sep 17 00:00:00 2001 From: Universe Date: Mon, 2 Sep 2024 20:49:53 +0900 Subject: [PATCH 09/32] langs add actions --- .../components/language-select/index.tsx | 77 +++++++++ .../scaffolds/dialogs/langs-add-dialog.tsx | 76 +++++++++ apps/forms/scaffolds/editor/action.ts | 17 ++ apps/forms/scaffolds/editor/reducer.ts | 41 +++++ .../scaffolds/sidebar/sidebar-mode-design.tsx | 157 ++++++++++++++---- .../sidecontrol/sidecontrol-global.tsx | 125 ++++++++------ 6 files changed, 408 insertions(+), 85 deletions(-) create mode 100644 apps/forms/components/language-select/index.tsx create mode 100644 apps/forms/scaffolds/dialogs/langs-add-dialog.tsx diff --git a/apps/forms/components/language-select/index.tsx b/apps/forms/components/language-select/index.tsx new file mode 100644 index 0000000000..eacfed5437 --- /dev/null +++ b/apps/forms/components/language-select/index.tsx @@ -0,0 +1,77 @@ +"use client"; +import React from "react"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + language_label_map, + supported_form_page_languages, +} from "@/k/supported_languages"; +import { LanguageCode } from "@/types"; +import { cn } from "@/utils"; + +/** + * easily provide override for language options + */ +export type LanguageSelectOptionMap = Partial< + Record< + LanguageCode, + { + disabled?: boolean; + } + > +>; + +export function LanguageSelect({ + name, + required, + value, + defaultValue, + onValueChange, + options = supported_form_page_languages, + optionsmap, + className, + placeholder = "Select language", +}: { + name?: string; + required?: boolean; + value?: LanguageCode; + defaultValue?: LanguageCode; + onValueChange?: (value: LanguageCode) => void; + options?: LanguageCode[]; + optionsmap?: LanguageSelectOptionMap; + className?: string; + placeholder?: string; +}) { + return ( + + ); +} diff --git a/apps/forms/scaffolds/dialogs/langs-add-dialog.tsx b/apps/forms/scaffolds/dialogs/langs-add-dialog.tsx new file mode 100644 index 0000000000..a322b77a02 --- /dev/null +++ b/apps/forms/scaffolds/dialogs/langs-add-dialog.tsx @@ -0,0 +1,76 @@ +"use client"; + +import React, { useCallback } from "react"; +import { useEditorState } from "../editor"; +import { LanguageCode } from "@/types"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogTitle, +} from "@/components/ui/dialog"; +import { + LanguageSelect, + LanguageSelectOptionMap, +} from "@/components/language-select"; +import { Button } from "@/components/ui/button"; + +export function AddNewLanguageDialog({ + ...props +}: React.ComponentProps) { + const [state, dispatch] = useEditorState(); + + const { langs } = state.document; + + const [newlang, setNewLang] = React.useState(); + + const onAddLang = useCallback( + (lang: LanguageCode) => { + dispatch({ + type: "editor/document/langs/add", + lang, + }); + }, + [dispatch] + ); + + return ( + + + Add New Language + Select a language to add. +
{ + e.preventDefault(); + // + onAddLang(newlang!); + props?.onOpenChange?.(false); + }} + > +
+ { + acc[l] = { disabled: true }; + return acc; + }, {})} + value={newlang} + onValueChange={setNewLang} + /> +
+
+ + + + + + +
+
+ ); +} diff --git a/apps/forms/scaffolds/editor/action.ts b/apps/forms/scaffolds/editor/action.ts index 0f15d52efb..edecdfe088 100644 --- a/apps/forms/scaffolds/editor/action.ts +++ b/apps/forms/scaffolds/editor/action.ts @@ -66,6 +66,9 @@ export type BlocksEditorAction = | DataGridCellChangeAction | FeedXSupabaseMainTableRowsAction | EditorDocumentLangAction + | EditorDocumentLangSetDefaultAction + | EditorDocumentLangAddAction + | EditorDocumentLangDeleteAction | EditorThemePoweredByBrandingAction | EditorThemePaletteAction | EditorThemeAppearanceAction @@ -392,6 +395,20 @@ export interface EditorDocumentLangAction { lang: LanguageCode; } +export interface EditorDocumentLangSetDefaultAction { + type: "editor/document/langs/set-default"; + lang: LanguageCode; +} + +export interface EditorDocumentLangAddAction { + type: "editor/document/langs/add"; + lang: LanguageCode; +} +export interface EditorDocumentLangDeleteAction { + type: "editor/document/langs/delete"; + lang: LanguageCode; +} + export interface DocumentSelectPageAction { type: "editor/document/select-page"; page_id: string; diff --git a/apps/forms/scaffolds/editor/reducer.ts b/apps/forms/scaffolds/editor/reducer.ts index 04c2551272..cb1dbc0c39 100644 --- a/apps/forms/scaffolds/editor/reducer.ts +++ b/apps/forms/scaffolds/editor/reducer.ts @@ -52,6 +52,9 @@ import type { DataTableRefreshAction, DataTableLoadingAction, EditorDocumentLangAction, + EditorDocumentLangSetDefaultAction, + EditorDocumentLangAddAction, + EditorDocumentLangDeleteAction, EditorThemePaletteAction, EditorThemeFontFamilyAction, EditorThemeBackgroundAction, @@ -99,6 +102,7 @@ import { table_to_sidebar_table_menu, } from "./init"; import assert from "assert"; +import toast from "react-hot-toast"; export function reducer( state: EditorState, @@ -1062,6 +1066,43 @@ export function reducer( draft.document.lang = lang; }); } + case "editor/document/langs/set-default": { + const { lang } = action; + return produce(state, (draft) => { + if (draft.document.langs.length === 1) { + draft.document.langs = [lang]; + draft.document.lang_default = lang; + draft.document.lang = lang; + } else { + assert(draft.document.langs.includes(lang), "Language not found"); + draft.document.lang_default = lang; + draft.document.lang = lang; + } + }); + } + case "editor/document/langs/add": { + const { lang } = action; + return produce(state, (draft) => { + const langs = new Set(draft.document.langs); + langs.add(lang); + draft.document.langs = Array.from(langs); + draft.document.lang = lang; + }); + } + case "editor/document/langs/delete": { + const { lang } = action; + return produce(state, (draft) => { + if (draft.document.langs.length === 1) { + toast.error("At least one language is required"); + return; + } + const langs = new Set(draft.document.langs); + langs.delete(lang); + draft.document.langs = Array.from(langs); + draft.document.lang = draft.document.langs[0]; + }); + } + // case "editor/document/select-page": { const { page_id } = action; diff --git a/apps/forms/scaffolds/sidebar/sidebar-mode-design.tsx b/apps/forms/scaffolds/sidebar/sidebar-mode-design.tsx index baac7aeff8..60da1c978e 100644 --- a/apps/forms/scaffolds/sidebar/sidebar-mode-design.tsx +++ b/apps/forms/scaffolds/sidebar/sidebar-mode-design.tsx @@ -1,6 +1,6 @@ "use client"; -import React from "react"; +import React, { useCallback } from "react"; import { useEditorState, useFormFields } from "../editor"; import { SidebarMenuItem, @@ -13,7 +13,12 @@ import { SidebarMenuItemLabel, } from "@/components/sidebar"; import { EditorFlatFormBlock, MenuItem } from "../editor/state"; -import { FormBlockType, FormFieldDefinition, FormInputType } from "@/types"; +import { + FormBlockType, + FormFieldDefinition, + FormInputType, + LanguageCode, +} from "@/types"; import { BlockTypeIcon } from "@/components/form-blcok-type-icon"; import { FormFieldTypeIcon } from "@/components/form-field-type-icon"; import { @@ -21,14 +26,34 @@ import { CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; -import { DotsHorizontalIcon, GlobeIcon } from "@radix-ui/react-icons"; +import { + DotsHorizontalIcon, + GlobeIcon, + TrashIcon, +} from "@radix-ui/react-icons"; import { DropdownMenu, DropdownMenuContent, + DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { FormFieldBlockMenuItems } from "../blocks-editor/blocks/field-block"; import { renderMenuItems } from "./render"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogTitle, +} from "@/components/ui/dialog"; +import { useDialogState } from "@/components/hooks/use-dialog-state"; +import { + LanguageSelect, + LanguageSelectOptionMap, +} from "@/components/language-select"; +import { Button } from "@/components/ui/button"; +import { AddNewLanguageDialog } from "../dialogs/langs-add-dialog"; export function ModeDesign() { const [state, dispatch] = useEditorState(); @@ -37,7 +62,7 @@ export function ModeDesign() { document: { pages }, } = state; - const show_hierarchy = + const show_tools = state.document.selected_page_id && ["form", "collection"].includes(state.document.selected_page_id); @@ -52,50 +77,112 @@ export function ModeDesign() { }, })} - {show_hierarchy && ( + {/* WIP */} + {process.env.NODE_ENV === "development" && show_tools && ( + + )} + + {show_tools && ( <>
)} - - {/* WIP */} - {process.env.NODE_ENV === "development" && } ); } function LocalizationView() { + const [state, dispatch] = useEditorState(); + const addnewlangDialog = useDialogState("addnewlang", { refreshkey: true }); + + const { lang, lang_default, langs } = state.document; + + const switchLang = useCallback( + (lang: LanguageCode) => { + dispatch({ + type: "editor/document/lang", + lang: lang, + }); + }, + [dispatch] + ); + return ( - - {/* + <> + + + {/* Localization */} - - - - - Translations - - - - - - 🇺🇸 - - en - - - - - - 🇰🇷 - - ko - - - - + + + + + Languages + + + + + + + + + + + Add Language + + + + + + + {langs.map((l) => { + const isdefault = l === lang_default; + const isselected = l === lang; + return ( + switchLang(l)} + > + + {/* + 🇺🇸 + */} + {l} + + + + + + + + + + + Delete + + + + Set as Default + + + + + + + ); + })} + + + ); } diff --git a/apps/forms/scaffolds/sidecontrol/sidecontrol-global.tsx b/apps/forms/scaffolds/sidecontrol/sidecontrol-global.tsx index 120dcdf556..67c2bc784f 100644 --- a/apps/forms/scaffolds/sidecontrol/sidecontrol-global.tsx +++ b/apps/forms/scaffolds/sidecontrol/sidecontrol-global.tsx @@ -38,6 +38,7 @@ import { MoonIcon, OpenInNewWindowIcon, Pencil2Icon, + PlusIcon, SunIcon, } from "@radix-ui/react-icons"; import { @@ -74,6 +75,9 @@ import { PropertyLine, PropertyLineControlRoot, PropertyLineLabel } from "./ui"; import { inputVariants } from "./controls/utils/input-variants"; import { useDialogState } from "@/components/hooks/use-dialog-state"; import { defineStepper } from "@stepperize/react"; +import { LanguageSelect } from "@/components/language-select"; +import { Badge } from "@/components/ui/badge"; +import { AddNewLanguageDialog } from "../dialogs/langs-add-dialog"; const { default: all, ...variants } = _variants; @@ -145,8 +149,9 @@ function Language() { const localizationSetupDialog = useDialogState("localization-setup", { refreshkey: true, }); + const addnewlangDialog = useDialogState("addnewlang", { refreshkey: true }); const { - document: { lang }, + document: { lang, langs, lang_default }, } = state; const onLangChange = useCallback( @@ -159,6 +164,50 @@ function Language() { [dispatch] ); + const ismultilangs = langs.length > 1; + + if (ismultilangs) { + return ( + <> + + +
+ {langs.map((l) => { + const isdefault = l === lang_default; + const iscurrent = l === lang; + return ( + onLangChange(l)} + variant={iscurrent ? "default" : "outline"} + key={l} + className="cursor-pointer" + > + {l}{" "} + {isdefault && ( + + (default) + + )} + + ); + })} + + + New + +
+
+ + ); + } + return ( <> Default - + Multiple @@ -186,50 +239,6 @@ function Language() { ); } -function LanguageSelect({ - name, - required, - value, - defaultValue, - onValueChange, - options = supported_form_page_languages, -}: { - name?: string; - required?: boolean; - value?: LanguageCode; - defaultValue?: LanguageCode; - onValueChange?: (value: LanguageCode) => void; - options?: LanguageCode[]; -}) { - return ( - - ); -} - const { useStepper } = defineStepper( { id: "fallbacklang" }, { id: "firstlang" } @@ -238,9 +247,24 @@ const { useStepper } = defineStepper( function EnableMultiLanguageDialog({ ...props }: React.ComponentProps) { + const [state, dispatch] = useEditorState(); const stepper = useStepper(); - const [fallbackLang, setFallbackLang] = useState("en"); + const [fallbackLang, setFallbackLang] = useState( + state.document.lang_default + ); + + const [firstLang, setFirstLang] = useState(); + + const addLang = useCallback( + (lang: LanguageCode) => { + dispatch({ + type: "editor/document/langs/add", + lang, + }); + }, + [dispatch] + ); return ( @@ -256,8 +280,8 @@ function EnableMultiLanguageDialog({ onSubmit={(e) => { e.preventDefault(); if (stepper.isLast) { - // do something - alert("done"); + addLang(firstLang!); + props?.onOpenChange?.(false); } else { stepper.next(); } @@ -269,7 +293,6 @@ function EnableMultiLanguageDialog({ @@ -290,6 +313,8 @@ function EnableMultiLanguageDialog({ l !== fallbackLang )} From 193026140ff2c89fad917449ef31f1ffd102fc2f Mon Sep 17 00:00:00 2001 From: Universe Date: Mon, 2 Sep 2024 21:22:05 +0900 Subject: [PATCH 10/32] delete confirmation --- .../delete-confirmation-dialog/index.tsx | 42 +++++++++++-- .../scaffolds/sidebar/sidebar-mode-design.tsx | 62 ++++++++++++++----- 2 files changed, 85 insertions(+), 19 deletions(-) diff --git a/apps/forms/components/delete-confirmation-dialog/index.tsx b/apps/forms/components/delete-confirmation-dialog/index.tsx index d728eae9e3..075ad42578 100644 --- a/apps/forms/components/delete-confirmation-dialog/index.tsx +++ b/apps/forms/components/delete-confirmation-dialog/index.tsx @@ -11,8 +11,42 @@ import { import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Spinner } from "../spinner"; +import { useDialogState } from "../hooks/use-dialog-state"; -export function DeleteConfirmationAlertDialog({ +type UseDeleteConfirmationAlertDialogStateProps = { + id: ID; + match: string; + title: string; + description?: string; +}; + +export function useDeleteConfirmationAlertDialogState< + ID extends string = string, +>(name = "alertdialog", config?: { refreshkey?: boolean }) { + const { open, setOpen, openDialog, closeDialog, data, setData, refreshkey } = + useDialogState>( + name, + config + ); + + return { + refreshkey, + open, + setOpen, + onOpenChange: setOpen, + openDialog, + closeDialog, + data: { + id: data?.id as ID, + }, + setData, + title: data?.title, + description: data?.description, + match: data?.match, + }; +} + +export function DeleteConfirmationAlertDialog({ title, description, placeholder, @@ -26,7 +60,7 @@ export function DeleteConfirmationAlertDialog({ placeholder?: string; match?: string; data?: { - id: string; + id: ID; }; /** * trigger when the delete button is clicked @@ -34,7 +68,7 @@ export function DeleteConfirmationAlertDialog({ * if the promise resolves to true, the dialog will be closed */ onDelete?: ( - data: { id: string }, + data: { id: ID }, user_confirmation_txt: string ) => Promise; }) { @@ -71,7 +105,7 @@ export function DeleteConfirmationAlertDialog({ spellCheck="false" type="text" name="comfirmation" - placeholder={placeholder} + placeholder={placeholder ?? match} value={confirmation} onChange={(e) => { setConfirmation(e.target.value); diff --git a/apps/forms/scaffolds/sidebar/sidebar-mode-design.tsx b/apps/forms/scaffolds/sidebar/sidebar-mode-design.tsx index 60da1c978e..422e3be9c9 100644 --- a/apps/forms/scaffolds/sidebar/sidebar-mode-design.tsx +++ b/apps/forms/scaffolds/sidebar/sidebar-mode-design.tsx @@ -39,21 +39,13 @@ import { } from "@/components/ui/dropdown-menu"; import { FormFieldBlockMenuItems } from "../blocks-editor/blocks/field-block"; import { renderMenuItems } from "./render"; -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogTitle, -} from "@/components/ui/dialog"; import { useDialogState } from "@/components/hooks/use-dialog-state"; -import { - LanguageSelect, - LanguageSelectOptionMap, -} from "@/components/language-select"; -import { Button } from "@/components/ui/button"; import { AddNewLanguageDialog } from "../dialogs/langs-add-dialog"; +import { Badge } from "@/components/ui/badge"; +import { + DeleteConfirmationAlertDialog, + useDeleteConfirmationAlertDialogState, +} from "@/components/delete-confirmation-dialog"; export function ModeDesign() { const [state, dispatch] = useEditorState(); @@ -95,6 +87,10 @@ export function ModeDesign() { function LocalizationView() { const [state, dispatch] = useEditorState(); const addnewlangDialog = useDialogState("addnewlang", { refreshkey: true }); + const deleteConfirmationDialog = + useDeleteConfirmationAlertDialogState("deleteconfirmation", { + refreshkey: true, + }); const { lang, lang_default, langs } = state.document; @@ -108,12 +104,30 @@ function LocalizationView() { [dispatch] ); + const deleteLang = useCallback( + (lang: LanguageCode) => { + dispatch({ + type: "editor/document/langs/delete", + lang, + }); + }, + [dispatch] + ); + return ( <> + + {...deleteConfirmationDialog} + key={deleteConfirmationDialog.refreshkey} + onDelete={async ({ id }) => { + deleteLang(id); + return true; + }} + /> {/* Localization @@ -156,7 +170,15 @@ function LocalizationView() { {/* 🇺🇸 */} - {l} + {l}{" "} + {isdefault && ( + + default + + )} @@ -165,7 +187,17 @@ function LocalizationView() { - + + deleteConfirmationDialog.openDialog({ + id: l, + title: "DELETE " + l, + description: `Are you sure you want to delete the language "${l}"? This action cannot be undone.`, + match: "DELETE " + l, + }) + } + > Delete From 426279086a1753d4de64c2c80738dd91ed72fcf8 Mon Sep 17 00:00:00 2001 From: Universe Date: Mon, 2 Sep 2024 23:28:08 +0900 Subject: [PATCH 11/32] switch default --- apps/forms/scaffolds/editor/reducer.ts | 5 +++ .../scaffolds/sidebar/sidebar-mode-design.tsx | 40 +++++++++++++++++-- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/apps/forms/scaffolds/editor/reducer.ts b/apps/forms/scaffolds/editor/reducer.ts index cb1dbc0c39..7eae2a22dd 100644 --- a/apps/forms/scaffolds/editor/reducer.ts +++ b/apps/forms/scaffolds/editor/reducer.ts @@ -1077,6 +1077,11 @@ export function reducer( assert(draft.document.langs.includes(lang), "Language not found"); draft.document.lang_default = lang; draft.document.lang = lang; + draft.document.langs = draft.document.langs.slice().sort((a, b) => { + if (a === lang) return -1; + if (b === lang) return 1; + return 0; + }); } }); } diff --git a/apps/forms/scaffolds/sidebar/sidebar-mode-design.tsx b/apps/forms/scaffolds/sidebar/sidebar-mode-design.tsx index 422e3be9c9..68e89db230 100644 --- a/apps/forms/scaffolds/sidebar/sidebar-mode-design.tsx +++ b/apps/forms/scaffolds/sidebar/sidebar-mode-design.tsx @@ -35,6 +35,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { FormFieldBlockMenuItems } from "../blocks-editor/blocks/field-block"; @@ -46,6 +47,7 @@ import { DeleteConfirmationAlertDialog, useDeleteConfirmationAlertDialogState, } from "@/components/delete-confirmation-dialog"; +import { LanguagesIcon } from "lucide-react"; export function ModeDesign() { const [state, dispatch] = useEditorState(); @@ -94,6 +96,8 @@ function LocalizationView() { const { lang, lang_default, langs } = state.document; + const ismultilang = langs.length > 1; + const switchLang = useCallback( (lang: LanguageCode) => { dispatch({ @@ -104,6 +108,16 @@ function LocalizationView() { [dispatch] ); + const switchDefaultLang = useCallback( + (lang: LanguageCode) => { + dispatch({ + type: "editor/document/langs/set-default", + lang: lang, + }); + }, + [dispatch] + ); + const deleteLang = useCallback( (lang: LanguageCode) => { dispatch({ @@ -114,6 +128,10 @@ function LocalizationView() { [dispatch] ); + if (!ismultilang) { + return <>; + } + return ( <> + { + // TODO: + alert("open in translate"); + }} + > + + Open in Translate + + + switchDefaultLang(l)} + > + + Set as Default + + @@ -201,10 +237,6 @@ function LocalizationView() { Delete - - - Set as Default - From dc10234c2c641ac8d32f6c3ec00f67211e9e4f71 Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 3 Sep 2024 16:46:44 +0900 Subject: [PATCH 12/32] split reducer --- apps/forms/scaffolds/editor/action.ts | 2 +- apps/forms/scaffolds/editor/dispatch.ts | 8 +- apps/forms/scaffolds/editor/init.ts | 26 ++++--- apps/forms/scaffolds/editor/reducer.ts | 66 +++------------- .../scaffolds/editor/reducers/lang.reducer.ts | 68 ++++++++++++++++ apps/forms/scaffolds/editor/state.ts | 78 ++++++++++--------- 6 files changed, 138 insertions(+), 110 deletions(-) create mode 100644 apps/forms/scaffolds/editor/reducers/lang.reducer.ts diff --git a/apps/forms/scaffolds/editor/action.ts b/apps/forms/scaffolds/editor/action.ts index edecdfe088..50baff806c 100644 --- a/apps/forms/scaffolds/editor/action.ts +++ b/apps/forms/scaffolds/editor/action.ts @@ -22,7 +22,7 @@ import type { Tokens } from "@/ast"; import { SYM_LOCALTZ } from "./symbols"; import { ZodObject } from "zod"; -export type BlocksEditorAction = +export type EditorAction = | GlobalSavingAction | GlobalWorkbenchPathAction | EditorSidebarModeAction diff --git a/apps/forms/scaffolds/editor/dispatch.ts b/apps/forms/scaffolds/editor/dispatch.ts index 0d8ca6b72b..b8af50f562 100644 --- a/apps/forms/scaffolds/editor/dispatch.ts +++ b/apps/forms/scaffolds/editor/dispatch.ts @@ -1,9 +1,9 @@ -import { BlocksEditorAction } from "./action"; +import { EditorAction } from "./action"; import { createContext, useCallback, useContext } from "react"; -export type Dispatcher = (action: BlocksEditorAction) => void; +export type Dispatcher = (action: EditorAction) => void; -export type FlatDispatcher = (action: BlocksEditorAction) => void; +export type FlatDispatcher = (action: EditorAction) => void; const __noop = () => {}; @@ -12,7 +12,7 @@ export const DispatchContext = createContext(__noop); export const useDispatch = (): FlatDispatcher => { const dispatch = useContext(DispatchContext); return useCallback( - (action: BlocksEditorAction) => { + (action: EditorAction) => { dispatch(action); }, [dispatch] diff --git a/apps/forms/scaffolds/editor/init.ts b/apps/forms/scaffolds/editor/init.ts index fdd5ed7d21..bf2a6f8ad6 100644 --- a/apps/forms/scaffolds/editor/init.ts +++ b/apps/forms/scaffolds/editor/init.ts @@ -15,10 +15,11 @@ import type { TableXSBMainTableConnection, GDocSchemaTable, TableMenuItem, + IDocumentLangState, } from "./state"; import { blockstreeflat } from "@/lib/forms/tree"; import { SYM_LOCALTZ, EditorSymbols } from "./symbols"; -import { FormFieldDefinition, GridaXSupabase } from "@/types"; +import { FormFieldDefinition, GridaXSupabase, LanguageCode } from "@/types"; import { SupabasePostgRESTOpenApi } from "@/lib/supabase-postgrest"; export function initialEditorState(init: EditorInit): EditorState { @@ -163,9 +164,7 @@ function initialDatabaseEditorState( supabase_project: init.supabase_project, connections: {}, document: { - lang: init.document.lang, - lang_default: init.document.lang, - langs: [init.document.lang], + ...langinit(init.document.lang), pages: [], nodes: [], templatedata: {}, @@ -242,9 +241,7 @@ function initialSiteEditorState(init: SiteDocumentEditorInit): EditorState { return { ...base, document: { - lang: init.document.lang, - lang_default: init.document.lang, - langs: [init.document.lang], + ...langinit(init.document.lang), pages: sitedocumentpagesinit({ basepath: base.basepath, document_id: init.document_id, @@ -449,9 +446,7 @@ function initialFormEditorState(init: FormDocumentEditorInit): EditorState { blocks: blockstreeflat(init.blocks), document: { - lang: init.document.lang, - lang_default: init.document.lang, - langs: [init.document.lang], + ...langinit(init.document.lang), pages: formdocumentpagesinit({ basepath: base.basepath, document_id: init.document_id, @@ -524,6 +519,17 @@ function sitedocumentpagesinit({ ]; } +function langinit(lang: LanguageCode): IDocumentLangState { + return { + lang: lang, + lang_default: lang, + langs: [lang], + messages: { + [lang]: {}, + }, + }; +} + function formdocumentpagesinit({ basepath, document_id, diff --git a/apps/forms/scaffolds/editor/reducer.ts b/apps/forms/scaffolds/editor/reducer.ts index 7eae2a22dd..892b4e0a4b 100644 --- a/apps/forms/scaffolds/editor/reducer.ts +++ b/apps/forms/scaffolds/editor/reducer.ts @@ -19,7 +19,7 @@ import type { BlockDescriptionAction, BlockTitleAction, BlockVHiddenAction, - BlocksEditorAction, + EditorAction, ChangeBlockFieldAction, CreateFielFromBlockdAction, CreateNewPendingBlockAction, @@ -51,10 +51,6 @@ import type { FeedXSupabaseMainTableRowsAction, DataTableRefreshAction, DataTableLoadingAction, - EditorDocumentLangAction, - EditorDocumentLangSetDefaultAction, - EditorDocumentLangAddAction, - EditorDocumentLangDeleteAction, EditorThemePaletteAction, EditorThemeFontFamilyAction, EditorThemeBackgroundAction, @@ -102,12 +98,9 @@ import { table_to_sidebar_table_menu, } from "./init"; import assert from "assert"; -import toast from "react-hot-toast"; +import langReducer from "./reducers/lang.reducer"; -export function reducer( - state: EditorState, - action: BlocksEditorAction -): EditorState { +export function reducer(state: EditorState, action: EditorAction): EditorState { switch (action.type) { case "saving": { const { saving } = action; @@ -1059,55 +1052,14 @@ export function reducer( draft.theme.customCSS = custom; }); } - // - case "editor/document/lang": { - const { lang } = action; - return produce(state, (draft) => { - draft.document.lang = lang; - }); - } - case "editor/document/langs/set-default": { - const { lang } = action; - return produce(state, (draft) => { - if (draft.document.langs.length === 1) { - draft.document.langs = [lang]; - draft.document.lang_default = lang; - draft.document.lang = lang; - } else { - assert(draft.document.langs.includes(lang), "Language not found"); - draft.document.lang_default = lang; - draft.document.lang = lang; - draft.document.langs = draft.document.langs.slice().sort((a, b) => { - if (a === lang) return -1; - if (b === lang) return 1; - return 0; - }); - } - }); - } - case "editor/document/langs/add": { - const { lang } = action; - return produce(state, (draft) => { - const langs = new Set(draft.document.langs); - langs.add(lang); - draft.document.langs = Array.from(langs); - draft.document.lang = lang; - }); - } + // #region lang + case "editor/document/lang": + case "editor/document/langs/set-default": + case "editor/document/langs/add": case "editor/document/langs/delete": { - const { lang } = action; - return produce(state, (draft) => { - if (draft.document.langs.length === 1) { - toast.error("At least one language is required"); - return; - } - const langs = new Set(draft.document.langs); - langs.delete(lang); - draft.document.langs = Array.from(langs); - draft.document.lang = draft.document.langs[0]; - }); + return langReducer(state, action); } - // + // #endregion lang case "editor/document/select-page": { const { page_id } = action; diff --git a/apps/forms/scaffolds/editor/reducers/lang.reducer.ts b/apps/forms/scaffolds/editor/reducers/lang.reducer.ts new file mode 100644 index 0000000000..ec0743933b --- /dev/null +++ b/apps/forms/scaffolds/editor/reducers/lang.reducer.ts @@ -0,0 +1,68 @@ +import { produce, type Draft } from "immer"; +import type { EditorState } from "../state"; +import type { + EditorAction, + EditorDocumentLangAction, + EditorDocumentLangSetDefaultAction, + EditorDocumentLangAddAction, + EditorDocumentLangDeleteAction, +} from "../action"; +import assert from "assert"; +import toast from "react-hot-toast"; + +export default function langReducer( + state: EditorState, + action: EditorAction +): EditorState { + switch (action.type) { + case "editor/document/lang": { + const { lang } = action; + return produce(state, (draft) => { + draft.document.lang = lang; + }); + } + case "editor/document/langs/set-default": { + const { lang } = action; + return produce(state, (draft) => { + if (draft.document.langs.length === 1) { + draft.document.langs = [lang]; + draft.document.lang_default = lang; + draft.document.lang = lang; + } else { + assert(draft.document.langs.includes(lang), "Language not found"); + draft.document.lang_default = lang; + draft.document.lang = lang; + draft.document.langs = draft.document.langs.slice().sort((a, b) => { + if (a === lang) return -1; + if (b === lang) return 1; + return 0; + }); + } + }); + } + case "editor/document/langs/add": { + const { lang } = action; + return produce(state, (draft) => { + const langs = new Set(draft.document.langs); + langs.add(lang); + draft.document.langs = Array.from(langs); + draft.document.lang = lang; + }); + } + case "editor/document/langs/delete": { + const { lang } = action; + return produce(state, (draft) => { + if (draft.document.langs.length === 1) { + toast.error("At least one language is required"); + return; + } + const langs = new Set(draft.document.langs); + langs.delete(lang); + draft.document.langs = Array.from(langs); + draft.document.lang = draft.document.langs[0]; + }); + } + } + + return state; +} diff --git a/apps/forms/scaffolds/editor/state.ts b/apps/forms/scaffolds/editor/state.ts index 4ee7dea6cd..de890fcc95 100644 --- a/apps/forms/scaffolds/editor/state.ts +++ b/apps/forms/scaffolds/editor/state.ts @@ -320,45 +320,47 @@ interface IInsertionMenuState { insertmenu: TGlobalEditorDialogState; } -interface IEditorDocumentState { - document: { - /** - * view document in... - */ - lang: LanguageCode; - /** - * default language - */ - lang_default: LanguageCode; - /** - * available languages provided by user - */ - langs: LanguageCode[]; - - pages: MenuItem[]; - selected_page_id?: string; - nodes: any[]; - templatesample?: string; - templatedata: { - [key: string]: { - text?: Tokens.StringValueExpression; - template_id: string; - attributes?: Omit< - React.HtmlHTMLAttributes, - "style" | "className" - >; - properties?: { [key: string]: Tokens.StringValueExpression }; - style?: React.CSSProperties; - }; +export interface IDocumentLangState { + /** + * view document in... + */ + lang: LanguageCode; + /** + * default language + */ + lang_default: LanguageCode; + /** + * available languages provided by user + */ + langs: LanguageCode[]; + + messages: Partial>>; +} + +interface IDocumentState extends IDocumentLangState { + pages: MenuItem[]; + selected_page_id?: string; + nodes: any[]; + templatesample?: string; + templatedata: { + [key: string]: { + text?: Tokens.StringValueExpression; + template_id: string; + attributes?: Omit< + React.HtmlHTMLAttributes, + "style" | "className" + >; + properties?: { [key: string]: Tokens.StringValueExpression }; + style?: React.CSSProperties; }; - selected_node_id?: string; - selected_node_type?: string; - selected_node_schema?: ZodObject | null; - selected_node_default_properties?: Record; - selected_node_default_style?: React.CSSProperties; - selected_node_default_text?: Tokens.StringValueExpression; - selected_node_context?: Record; }; + selected_node_id?: string; + selected_node_type?: string; + selected_node_schema?: ZodObject | null; + selected_node_default_properties?: Record; + selected_node_default_style?: React.CSSProperties; + selected_node_default_text?: Tokens.StringValueExpression; + selected_node_context?: Record; } interface IEditorDocumentThemeState { @@ -379,7 +381,6 @@ export interface BaseDocumentEditorState IEditorAssetsState, IInsertionMenuState, IFieldEditorState, - IEditorDocumentState, ICustomerEditorState, IEditorDocumentThemeState, IRowEditorState { @@ -395,6 +396,7 @@ export interface BaseDocumentEditorState document_id: string; document_title: string; doctype: GDocumentType; + document: IDocumentState; } interface IFieldEditorState { From 2bc96ca40e08d6df26aeab6453e22c99b7bbbc1d Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 3 Sep 2024 16:50:21 +0900 Subject: [PATCH 13/32] rename --- apps/forms/scaffolds/editor/action.ts | 18 ++++++++++++------ apps/forms/scaffolds/editor/reducer.ts | 2 +- .../scaffolds/editor/reducers/lang.reducer.ts | 10 +++++----- .../scaffolds/sidebar/sidebar-mode-design.tsx | 2 +- .../sidecontrol/sidecontrol-global.tsx | 2 +- 5 files changed, 20 insertions(+), 14 deletions(-) diff --git a/apps/forms/scaffolds/editor/action.ts b/apps/forms/scaffolds/editor/action.ts index 50baff806c..f5400319d2 100644 --- a/apps/forms/scaffolds/editor/action.ts +++ b/apps/forms/scaffolds/editor/action.ts @@ -65,10 +65,7 @@ export type EditorAction = | DataTableLoadingAction | DataGridCellChangeAction | FeedXSupabaseMainTableRowsAction - | EditorDocumentLangAction - | EditorDocumentLangSetDefaultAction - | EditorDocumentLangAddAction - | EditorDocumentLangDeleteAction + | NSEditorDocumentLangAction | EditorThemePoweredByBrandingAction | EditorThemePaletteAction | EditorThemeAppearanceAction @@ -390,8 +387,16 @@ export interface FormEndingPreferencesAction extends Partial { type: "editor/form/ending/preferences"; } -export interface EditorDocumentLangAction { - type: "editor/document/lang"; + +// #region lang +export type NSEditorDocumentLangAction = + | EditorDocumentLangSetCurrentAction + | EditorDocumentLangSetDefaultAction + | EditorDocumentLangAddAction + | EditorDocumentLangDeleteAction; + +export interface EditorDocumentLangSetCurrentAction { + type: "editor/document/langs/set-current"; lang: LanguageCode; } @@ -408,6 +413,7 @@ export interface EditorDocumentLangDeleteAction { type: "editor/document/langs/delete"; lang: LanguageCode; } +// #endregion lang export interface DocumentSelectPageAction { type: "editor/document/select-page"; diff --git a/apps/forms/scaffolds/editor/reducer.ts b/apps/forms/scaffolds/editor/reducer.ts index 892b4e0a4b..58f444d667 100644 --- a/apps/forms/scaffolds/editor/reducer.ts +++ b/apps/forms/scaffolds/editor/reducer.ts @@ -1053,7 +1053,7 @@ export function reducer(state: EditorState, action: EditorAction): EditorState { }); } // #region lang - case "editor/document/lang": + case "editor/document/langs/set-current": case "editor/document/langs/set-default": case "editor/document/langs/add": case "editor/document/langs/delete": { diff --git a/apps/forms/scaffolds/editor/reducers/lang.reducer.ts b/apps/forms/scaffolds/editor/reducers/lang.reducer.ts index ec0743933b..72f7c47e24 100644 --- a/apps/forms/scaffolds/editor/reducers/lang.reducer.ts +++ b/apps/forms/scaffolds/editor/reducers/lang.reducer.ts @@ -1,22 +1,22 @@ import { produce, type Draft } from "immer"; import type { EditorState } from "../state"; import type { - EditorAction, - EditorDocumentLangAction, + EditorDocumentLangSetCurrentAction, EditorDocumentLangSetDefaultAction, EditorDocumentLangAddAction, EditorDocumentLangDeleteAction, + NSEditorDocumentLangAction, } from "../action"; import assert from "assert"; import toast from "react-hot-toast"; export default function langReducer( state: EditorState, - action: EditorAction + action: NSEditorDocumentLangAction ): EditorState { switch (action.type) { - case "editor/document/lang": { - const { lang } = action; + case "editor/document/langs/set-current": { + const { lang } = action; return produce(state, (draft) => { draft.document.lang = lang; }); diff --git a/apps/forms/scaffolds/sidebar/sidebar-mode-design.tsx b/apps/forms/scaffolds/sidebar/sidebar-mode-design.tsx index 68e89db230..7b170ea261 100644 --- a/apps/forms/scaffolds/sidebar/sidebar-mode-design.tsx +++ b/apps/forms/scaffolds/sidebar/sidebar-mode-design.tsx @@ -101,7 +101,7 @@ function LocalizationView() { const switchLang = useCallback( (lang: LanguageCode) => { dispatch({ - type: "editor/document/lang", + type: "editor/document/langs/set-current", lang: lang, }); }, diff --git a/apps/forms/scaffolds/sidecontrol/sidecontrol-global.tsx b/apps/forms/scaffolds/sidecontrol/sidecontrol-global.tsx index 67c2bc784f..203437f34d 100644 --- a/apps/forms/scaffolds/sidecontrol/sidecontrol-global.tsx +++ b/apps/forms/scaffolds/sidecontrol/sidecontrol-global.tsx @@ -157,7 +157,7 @@ function Language() { const onLangChange = useCallback( (lang: LanguageCode) => { dispatch({ - type: "editor/document/lang", + type: "editor/document/langs/set-current", lang, }); }, From 30e0efe7e0359f3c06d58e63f94d237b04e555af Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 3 Sep 2024 20:09:16 +0900 Subject: [PATCH 14/32] grid header style --- apps/forms/scaffolds/grid-editor/components/layout.tsx | 2 +- apps/forms/scaffolds/grid/grid.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/forms/scaffolds/grid-editor/components/layout.tsx b/apps/forms/scaffolds/grid-editor/components/layout.tsx index 0162ae3164..cbc0381b4e 100644 --- a/apps/forms/scaffolds/grid-editor/components/layout.tsx +++ b/apps/forms/scaffolds/grid-editor/components/layout.tsx @@ -8,7 +8,7 @@ export function Root({ children }: React.PropsWithChildren<{}>) { export function Header({ children }: React.PropsWithChildren<{}>) { return ( -
+
{children}
); diff --git a/apps/forms/scaffolds/grid/grid.css b/apps/forms/scaffolds/grid/grid.css index 3f460810d4..6e9672c3cf 100644 --- a/apps/forms/scaffolds/grid/grid.css +++ b/apps/forms/scaffolds/grid/grid.css @@ -32,7 +32,7 @@ } .rdg-header-row > .rdg-cell { - border-top: 1px solid hsl(var(--border)); + border-top: none; border-bottom: 1px solid hsl(var(--border)); } From e0203b92482d0363f6239c505e6fc3a39fa24448 Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 3 Sep 2024 20:09:31 +0900 Subject: [PATCH 15/32] issues/new routing --- apps/forms/components/invalid.tsx | 7 +++++-- apps/forms/next.config.mjs | 5 +++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/forms/components/invalid.tsx b/apps/forms/components/invalid.tsx index cf203daeb6..abc9fddef9 100644 --- a/apps/forms/components/invalid.tsx +++ b/apps/forms/components/invalid.tsx @@ -9,7 +9,7 @@ export default function Invalid() { const { basepath } = state; return ( -
+

@@ -17,7 +17,10 @@ export default function Invalid() {

The page you're looking for doesn't exist or has been moved. - Don't worry, we're here to help. + If you think this is a mistake,{" "} + + please file an issue. +

Date: Tue, 3 Sep 2024 20:28:41 +0900 Subject: [PATCH 16/32] message action --- apps/forms/scaffolds/editor/action.ts | 10 +++++++++- apps/forms/scaffolds/editor/reducer.ts | 3 ++- apps/forms/scaffolds/editor/reducers/lang.reducer.ts | 11 +++++++++++ apps/forms/scaffolds/editor/state.ts | 2 +- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/apps/forms/scaffolds/editor/action.ts b/apps/forms/scaffolds/editor/action.ts index f5400319d2..817c220f58 100644 --- a/apps/forms/scaffolds/editor/action.ts +++ b/apps/forms/scaffolds/editor/action.ts @@ -393,7 +393,8 @@ export type NSEditorDocumentLangAction = | EditorDocumentLangSetCurrentAction | EditorDocumentLangSetDefaultAction | EditorDocumentLangAddAction - | EditorDocumentLangDeleteAction; + | EditorDocumentLangDeleteAction + | EditorDocumentLangMessageAction; export interface EditorDocumentLangSetCurrentAction { type: "editor/document/langs/set-current"; @@ -413,6 +414,13 @@ export interface EditorDocumentLangDeleteAction { type: "editor/document/langs/delete"; lang: LanguageCode; } + +export interface EditorDocumentLangMessageAction { + type: "editor/document/langs/messages/update"; + lang: LanguageCode; + key: string; + message: string | undefined; +} // #endregion lang export interface DocumentSelectPageAction { diff --git a/apps/forms/scaffolds/editor/reducer.ts b/apps/forms/scaffolds/editor/reducer.ts index 58f444d667..73520a2cbd 100644 --- a/apps/forms/scaffolds/editor/reducer.ts +++ b/apps/forms/scaffolds/editor/reducer.ts @@ -1056,7 +1056,8 @@ export function reducer(state: EditorState, action: EditorAction): EditorState { case "editor/document/langs/set-current": case "editor/document/langs/set-default": case "editor/document/langs/add": - case "editor/document/langs/delete": { + case "editor/document/langs/delete": + case "editor/document/langs/messages/update": { return langReducer(state, action); } // #endregion lang diff --git a/apps/forms/scaffolds/editor/reducers/lang.reducer.ts b/apps/forms/scaffolds/editor/reducers/lang.reducer.ts index 72f7c47e24..e9917985d0 100644 --- a/apps/forms/scaffolds/editor/reducers/lang.reducer.ts +++ b/apps/forms/scaffolds/editor/reducers/lang.reducer.ts @@ -6,6 +6,7 @@ import type { EditorDocumentLangAddAction, EditorDocumentLangDeleteAction, NSEditorDocumentLangAction, + EditorDocumentLangMessageAction, } from "../action"; import assert from "assert"; import toast from "react-hot-toast"; @@ -47,6 +48,7 @@ export default function langReducer( langs.add(lang); draft.document.langs = Array.from(langs); draft.document.lang = lang; + draft.document.messages[lang] = {}; }); } case "editor/document/langs/delete": { @@ -60,6 +62,15 @@ export default function langReducer( langs.delete(lang); draft.document.langs = Array.from(langs); draft.document.lang = draft.document.langs[0]; + delete draft.document.messages[lang]; + }); + } + case "editor/document/langs/messages/update": { + const { lang, key, message } = action; + + return produce(state, (draft) => { + assert(draft.document.messages[lang], "Language not found"); + draft.document.messages[lang][key] = message; }); } } diff --git a/apps/forms/scaffolds/editor/state.ts b/apps/forms/scaffolds/editor/state.ts index de890fcc95..a1933e03a0 100644 --- a/apps/forms/scaffolds/editor/state.ts +++ b/apps/forms/scaffolds/editor/state.ts @@ -334,7 +334,7 @@ export interface IDocumentLangState { */ langs: LanguageCode[]; - messages: Partial>>; + messages: Partial>>; } interface IDocumentState extends IDocumentLangState { From 077d70998552eca0fe1d67361553a2fe2fcc7594 Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 3 Sep 2024 20:28:59 +0900 Subject: [PATCH 17/32] wip translate in panel --- .../sidecontrol/controls/plaintext.tsx | 4 +- .../sidecontrol/sidecontrol-doctype-form.tsx | 63 ++++++++++++++++++- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/apps/forms/scaffolds/sidecontrol/controls/plaintext.tsx b/apps/forms/scaffolds/sidecontrol/controls/plaintext.tsx index 7d21c9bc39..66b5f9a8b5 100644 --- a/apps/forms/scaffolds/sidecontrol/controls/plaintext.tsx +++ b/apps/forms/scaffolds/sidecontrol/controls/plaintext.tsx @@ -4,15 +4,17 @@ import { inputVariants } from "./utils/input-variants"; export function PlainTextControl({ value, onValueChange, + placeholder = "Enter", }: { value?: string; onValueChange?: (value?: string) => void; + placeholder?: string; }) { return ( { onValueChange?.(e.target.value || undefined); diff --git a/apps/forms/scaffolds/sidecontrol/sidecontrol-doctype-form.tsx b/apps/forms/scaffolds/sidecontrol/sidecontrol-doctype-form.tsx index 0e240d59a6..955f0c6d35 100644 --- a/apps/forms/scaffolds/sidecontrol/sidecontrol-doctype-form.tsx +++ b/apps/forms/scaffolds/sidecontrol/sidecontrol-doctype-form.tsx @@ -23,6 +23,7 @@ import { PopoverClose } from "@radix-ui/react-popover"; import { PlainTextControl } from "./controls/plaintext"; import { FieldSupports } from "@/k/supported_field_types"; import toast from "react-hot-toast"; +import { Badge } from "@/components/ui/badge"; export function SideControlDoctypeForm() { const [state, dispatch] = useEditorState(); @@ -214,22 +215,78 @@ function BlockTypeField() { ); } +/** + * @example + * ```ts + * const t = useDocumentTranslations() + * ``` + */ +function useDocumentTranslations() { + const [state, dispatch] = useEditorState(); + const { lang, lang_default, messages } = state.document; + // + + return useCallback( + (key: string) => { + return messages[lang]?.[key]; + }, + [lang] + ); +} + function BlockTypeHeader() { + const [state, dispatch] = useEditorState(); const [block] = useFocusedFormBlock(); + const t = useDocumentTranslations(); + const { lang, lang_default } = state.document; + const istranslationmode = lang !== lang_default; + + const updateMessage = useCallback( + (key: string, message: string | undefined) => { + dispatch({ + type: "editor/document/langs/messages/update", + lang: lang, + key, + message, + }); + }, + [dispatch, lang] + ); return ( - Customize + + {istranslationmode ? ( + <> + Translate {lang} + + ) : ( + <>Customize + )} + Title - + { + updateMessage(block.id + "/title_html", value); + }} + placeholder={block.title_html ?? ""} + value={ + istranslationmode + ? t(block.id + "/title_html") + : block.title_html ?? "" + } + /> Description - + From 767311ed91e154e81c6a670e4d9174880e78a7b1 Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 3 Sep 2024 20:29:20 +0900 Subject: [PATCH 18/32] translator view --- .../[org]/[proj]/[id]/form/i18n/page.tsx | 20 +++ apps/forms/lib/forms/url.ts | 3 + apps/forms/scaffolds/i18n-editor/index.tsx | 134 ++++++++++++++++++ .../scaffolds/sidebar/sidebar-mode-design.tsx | 11 +- 4 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 apps/forms/app/(workbench)/[org]/[proj]/[id]/form/i18n/page.tsx create mode 100644 apps/forms/scaffolds/i18n-editor/index.tsx diff --git a/apps/forms/app/(workbench)/[org]/[proj]/[id]/form/i18n/page.tsx b/apps/forms/app/(workbench)/[org]/[proj]/[id]/form/i18n/page.tsx new file mode 100644 index 0000000000..8d1fdc21f9 --- /dev/null +++ b/apps/forms/app/(workbench)/[org]/[proj]/[id]/form/i18n/page.tsx @@ -0,0 +1,20 @@ +"use client"; + +import Invalid from "@/components/invalid"; +import { useEditorState } from "@/scaffolds/editor"; +import { I18nEditor } from "@/scaffolds/i18n-editor"; + +export default function FormI18nPage() { + const [state] = useEditorState(); + const { lang, lang_default, langs, messages } = state.document; + + if (langs.length <= 1) { + return ; + } + + return ( +
+ +
+ ); +} diff --git a/apps/forms/lib/forms/url.ts b/apps/forms/lib/forms/url.ts index efffd907fe..4a29078be9 100644 --- a/apps/forms/lib/forms/url.ts +++ b/apps/forms/lib/forms/url.ts @@ -20,6 +20,7 @@ type EditorPageParamsMap = { ".": {}; form: {}; "form/edit": {}; + "form/i18n": {}; settings: {}; design: {}; data: {}; @@ -58,6 +59,8 @@ export function editorlink

( return `${origin}/${basepath}/${id}/form`; case "form/edit": return `${origin}/${basepath}/${id}/form/edit`; + case "form/i18n": + return `${origin}/${basepath}/${id}/form/i18n`; case "settings": return `${origin}/${basepath}/${id}/settings`; // case "settings/customize": diff --git a/apps/forms/scaffolds/i18n-editor/index.tsx b/apps/forms/scaffolds/i18n-editor/index.tsx new file mode 100644 index 0000000000..41ba12a5c6 --- /dev/null +++ b/apps/forms/scaffolds/i18n-editor/index.tsx @@ -0,0 +1,134 @@ +"use client"; + +import React from "react"; +import { useEditorState } from "../editor"; +import { DotsHorizontalIcon, DownloadIcon } from "@radix-ui/react-icons"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; +import { SparkleIcon } from "lucide-react"; +import { cn } from "@/utils"; +import * as GridLayout from "@/scaffolds/grid-editor/components/layout"; + +export function I18nEditor() { + const [state] = useEditorState(); + const { lang, lang_default, langs, messages } = state.document; + + const keys = [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + ]; + + return ( + + Header + +

+ + {keys.map((key) => { + return ; + })} + + {keys.map((key) => { + return ; + })} +
+ + + + + + ); +} + +function DuoRow({ keyname }: { keyname: string }) { + const value = "text" + keyname; + return ( +
+
+ + {value} + +
+
+ +
+
+ + +
+
+ ); +} + +function GroupHeaderRow() { + return ( +
+ Namespace +
+ ); +} + +function AnchorLangCell({ + children, + className, + ...props +}: React.LabelHTMLAttributes) { + return ( + + ); +} + +function TargetLangCell({ + className, + ...props +}: React.InputHTMLAttributes) { + return ( + + ); +} diff --git a/apps/forms/scaffolds/sidebar/sidebar-mode-design.tsx b/apps/forms/scaffolds/sidebar/sidebar-mode-design.tsx index 7b170ea261..193ccbb695 100644 --- a/apps/forms/scaffolds/sidebar/sidebar-mode-design.tsx +++ b/apps/forms/scaffolds/sidebar/sidebar-mode-design.tsx @@ -48,6 +48,8 @@ import { useDeleteConfirmationAlertDialogState, } from "@/components/delete-confirmation-dialog"; import { LanguagesIcon } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { editorlink } from "@/lib/forms/url"; export function ModeDesign() { const [state, dispatch] = useEditorState(); @@ -87,6 +89,7 @@ export function ModeDesign() { } function LocalizationView() { + const router = useRouter(); const [state, dispatch] = useEditorState(); const addnewlangDialog = useDialogState("addnewlang", { refreshkey: true }); const deleteConfirmationDialog = @@ -207,8 +210,12 @@ function LocalizationView() { { - // TODO: - alert("open in translate"); + router.push( + editorlink("form/i18n", { + basepath: state.basepath, + document_id: state.document_id, + }) + ); }} > From d21ef8106d6537fc6a2b25d473a6a862136f239c Mon Sep 17 00:00:00 2001 From: Universe Date: Sat, 7 Sep 2024 18:20:37 +0900 Subject: [PATCH 19/32] clean block --- .../blocks-editor/blocks/field-block.tsx | 403 ++++++++++-------- 1 file changed, 217 insertions(+), 186 deletions(-) diff --git a/apps/forms/scaffolds/blocks-editor/blocks/field-block.tsx b/apps/forms/scaffolds/blocks-editor/blocks/field-block.tsx index d0eb9d1298..550877f5dd 100644 --- a/apps/forms/scaffolds/blocks-editor/blocks/field-block.tsx +++ b/apps/forms/scaffolds/blocks-editor/blocks/field-block.tsx @@ -46,6 +46,7 @@ import { Button } from "@/components/ui/button"; import clsx from "clsx"; import { editorlink } from "@/lib/forms/url"; import { SYSTEM_GF_KEY_STARTS_WITH } from "@/k/system"; +import { useDialogState } from "@/components/hooks/use-dialog-state"; export function FieldBlock({ id, @@ -58,8 +59,6 @@ export function FieldBlock({ const fields = useFormFields(); - const { document_id, basepath } = state; - const form_field: FormFieldDefinition | undefined = fields.find( (f) => f.id === form_field_id ); @@ -69,7 +68,7 @@ export function FieldBlock({ const { form: { available_field_ids }, } = state; - const [advanced, setAdvanced] = useState(false); + const advancedModeDialog = useDialogState(); const can_advanced_mode = fields.length > 0; @@ -96,44 +95,42 @@ export function FieldBlock({ }); }, [dispatch, id]); - const onFieldEditClick = useCallback(() => { - dispatch({ - type: "editor/field/edit", - field_id: form_field_id!, - }); - }, [dispatch, form_field_id]); - return ( - - -
- - - { - if (!open) { - setAdvanced(false); - } - }} - > - - Advanced Mode - - In advanced mode, you can re-use already referenced field. - This is useful when there are multiple blocks that should be - visible optionally. (Use with caution, only one value will be - accepted if there are multiple rendered blocks with the same - field) - -
+ <> + + + +
+ + + {fields.length === 0 ? ( + <> + + + ) : ( + <> -
- - - - - - -
- {fields.length === 0 ? ( - <> - - - ) : ( - <> - - - )} -
-
-
- - - - - - - - -
-
-
- {is_hidden_field ? ( -
-

- Hidden fields are not displayed in the form. -
- Configure how this field is populated with{" "} - - URL Parameters - {" "} - {form_field.required ? "(required)" : ""}{" "} - {!( - form_field.name.startsWith(SYSTEM_GF_KEY_STARTS_WITH) || - form_field.required - ) && ( - <> - or{" "} - + {can_advanced_mode && ( + +

+ + Advanced +
+ + )} + + )} -

+ +
+
+ + + + + + + +
- ) : ( - + +
+ {is_hidden_field ? ( + + ) : ( + + )} +
+ + + ); +} + +function HiddenFieldInfo({ data }: { data: FormFieldDefinition }) { + const [state, dispatch] = useEditorState(); + const { document_id, basepath } = state; + + const onFieldEditClick = useCallback(() => { + dispatch({ + type: "editor/field/edit", + field_id: data.id, + }); + }, [dispatch, data.id]); + + return ( +
+

+ Hidden fields are not displayed in the form. +
+ Configure how this field is populated with{" "} + + URL Parameters + {" "} + {data.required ? "(required)" : ""}{" "} + {!( + data.name.startsWith(SYSTEM_GF_KEY_STARTS_WITH) || data.required + ) && ( + <> + or{" "} + + )} -

- +

+
); } @@ -336,3 +293,77 @@ export function FormFieldBlockMenuItems({ ); } + +function AdvancedModeDialog({ + block_id, + form_field_id, + ...props +}: React.ComponentProps & { + block_id: string; + form_field_id: string; +}) { + const [state, dispatch] = useEditorState(); + + const fields = useFormFields(); + + const onFieldChange = useCallback( + (field_id: string) => { + dispatch({ + type: "blocks/field/change", + field_id, + block_id: block_id, + }); + }, + [dispatch, block_id] + ); + + return ( + + + Advanced Mode + + In advanced mode, you can re-use already referenced field. This is + useful when there are multiple blocks that should be visible + optionally. (Use with caution, only one value will be accepted if + there are multiple rendered blocks with the same field) + +
+ +
+ + + + + +
+
+ ); +} From 86caa2dc8c551c0e3ab37668cfd5e69048b40332 Mon Sep 17 00:00:00 2001 From: Universe Date: Sat, 7 Sep 2024 20:11:44 +0900 Subject: [PATCH 20/32] update state --- README.md | 2 + .../[org]/[proj]/[id]/form/i18n/page.tsx | 2 +- .../components/language-select/index.tsx | 2 +- apps/forms/k/supported_languages.ts | 31 +-- apps/forms/k/video_block_defaults.ts | 2 +- .../scaffolds/blocks-editor/blocks-editor.tsx | 8 +- .../blocks-editor/blocks/field-block.tsx | 35 ++- .../blocks-editor/blocks/header-block.tsx | 41 +--- .../blocks-editor/blocks/pdf-block.tsx | 5 +- .../blocks-editor/blocks/video-block.tsx | 22 +- .../scaffolds/dialogs/langs-add-dialog.tsx | 2 +- apps/forms/scaffolds/editor/action.ts | 17 +- apps/forms/scaffolds/editor/init.ts | 17 +- apps/forms/scaffolds/editor/reducer.ts | 2 +- .../scaffolds/editor/reducers/lang.reducer.ts | 56 ++--- apps/forms/scaffolds/editor/state.ts | 40 ++-- apps/forms/scaffolds/editor/use.ts | 106 ++++++++- apps/forms/scaffolds/i18n-editor/index.tsx | 2 +- .../custom-ending-page-preferences.tsx | 8 +- .../scaffolds/sidebar/sidebar-mode-design.tsx | 11 +- .../controls/{plaintext.tsx => input.tsx} | 21 +- .../sidecontrol/sidecontrol-doctype-form.tsx | 208 +++++++++++++----- .../sidecontrol/sidecontrol-global.tsx | 32 +-- apps/forms/scaffolds/sidecontrol/ui/index.tsx | 15 +- 24 files changed, 482 insertions(+), 205 deletions(-) rename apps/forms/scaffolds/sidecontrol/controls/{plaintext.tsx => input.tsx} (50%) diff --git a/README.md b/README.md index f835db1961..fbfa0c9487 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,8 @@ If you wish to contribute or run this project locally, see [CONTRIBUTING.md](./C - [ ] Localization - [ ] Joins & Relational Queries - [ ] Gallary & List View +- [ ] Charts & Analytics +- [ ] Actions & Workflows # Grida Localization (Comming this Fall) diff --git a/apps/forms/app/(workbench)/[org]/[proj]/[id]/form/i18n/page.tsx b/apps/forms/app/(workbench)/[org]/[proj]/[id]/form/i18n/page.tsx index 8d1fdc21f9..5e615c33c2 100644 --- a/apps/forms/app/(workbench)/[org]/[proj]/[id]/form/i18n/page.tsx +++ b/apps/forms/app/(workbench)/[org]/[proj]/[id]/form/i18n/page.tsx @@ -6,7 +6,7 @@ import { I18nEditor } from "@/scaffolds/i18n-editor"; export default function FormI18nPage() { const [state] = useEditorState(); - const { lang, lang_default, langs, messages } = state.document; + const { lang, lang_default, langs } = state.document.g11n; if (langs.length <= 1) { return ; diff --git a/apps/forms/components/language-select/index.tsx b/apps/forms/components/language-select/index.tsx index eacfed5437..0629d2ea81 100644 --- a/apps/forms/components/language-select/index.tsx +++ b/apps/forms/components/language-select/index.tsx @@ -67,7 +67,7 @@ export function LanguageSelect({ const disabled = optionsmap?.[lang]?.disabled; return ( - {language_label_map[lang]} + {language_label_map[lang].flag} {language_label_map[lang].label} ); })} diff --git a/apps/forms/k/supported_languages.ts b/apps/forms/k/supported_languages.ts index 00244ed924..37b667aa8f 100644 --- a/apps/forms/k/supported_languages.ts +++ b/apps/forms/k/supported_languages.ts @@ -5,18 +5,21 @@ export const supported_form_page_languages: LanguageCode[] = Object.keys( resources ) as LanguageCode[]; -export const language_label_map: Record = { - en: "🇺🇸 English", - es: "🇪🇸 Spanish / Español", - de: "🇩🇪 German / Deutsch", - ja: "🇯🇵 Japanese / 日本語", - fr: "🇫🇷 French / Français", - pt: "🇵🇹 Portuguese / Português", - it: "🇮🇹 Italian / Italiano", - ko: "🇰🇷 Korean / 한국어", - ru: "🇷🇺 Russian / Русский", - zh: "🇨🇳 Chinese / 中文", - ar: "🇸🇦 Arabic / العربية", - hi: "🇮🇳 Hindi / हिन्दी", - nl: "🇳🇱 Dutch / Nederlands", +export const language_label_map: Record< + LanguageCode, + { flag: string; label: string } +> = { + en: { flag: "🇺🇸", label: "English" }, + es: { flag: "🇪🇸", label: "Spanish / Español" }, + de: { flag: "🇩🇪", label: "German / Deutsch" }, + ja: { flag: "🇯🇵", label: "Japanese / 日本語" }, + fr: { flag: "🇫🇷", label: "French / Français" }, + pt: { flag: "🇵🇹", label: "Portuguese / Português" }, + it: { flag: "🇮🇹", label: "Italian / Italiano" }, + ko: { flag: "🇰🇷", label: "Korean / 한국어" }, + ru: { flag: "🇷🇺", label: "Russian / Русский" }, + zh: { flag: "🇨🇳", label: "Chinese / 中文" }, + ar: { flag: "🇸🇦", label: "Arabic / العربية" }, + hi: { flag: "🇮🇳", label: "Hindi / हिन्दी" }, + nl: { flag: "🇳🇱", label: "Dutch / Nederlands" }, }; diff --git a/apps/forms/k/video_block_defaults.ts b/apps/forms/k/video_block_defaults.ts index efe54825dd..7f3b234b01 100644 --- a/apps/forms/k/video_block_defaults.ts +++ b/apps/forms/k/video_block_defaults.ts @@ -1,2 +1,2 @@ export const VIDEO_BLOCK_SRC_DEFAULT_VALUE = - "https://www.youtube.com/watch?v=BFhp7Y0iLSA&ab_channel=AbstractMotion"; + "https://www.youtube.com/watch?v=jNQXAC9IVRw&ab_channel=jawed"; diff --git a/apps/forms/scaffolds/blocks-editor/blocks-editor.tsx b/apps/forms/scaffolds/blocks-editor/blocks-editor.tsx index e92f7ec488..7219f9dc12 100644 --- a/apps/forms/scaffolds/blocks-editor/blocks-editor.tsx +++ b/apps/forms/scaffolds/blocks-editor/blocks-editor.tsx @@ -242,7 +242,7 @@ function AgentThemeSyncProvider({ children }: React.PropsWithChildren<{}>) { supabase .from("form_document") .update({ - lang: document.lang, + lang: document.g11n.lang, is_powered_by_branding_enabled: theme.is_powered_by_branding_enabled, stylesheet: { appearance: theme.appearance, @@ -266,7 +266,7 @@ function AgentThemeSyncProvider({ children }: React.PropsWithChildren<{}>) { prev, supabase, document_id, - document.lang, + document.g11n.lang, theme.is_powered_by_branding_enabled, theme.appearance, theme.customCSS, @@ -326,7 +326,7 @@ function FooterPreview() { const { theme: { is_powered_by_branding_enabled }, - document: { lang }, + document: { g11n }, } = state; return ( @@ -341,7 +341,7 @@ function FooterPreview() { "w-full md:w-auto" )} > - + {is_powered_by_branding_enabled && (
diff --git a/apps/forms/scaffolds/blocks-editor/blocks/field-block.tsx b/apps/forms/scaffolds/blocks-editor/blocks/field-block.tsx index 550877f5dd..3562d93a6d 100644 --- a/apps/forms/scaffolds/blocks-editor/blocks/field-block.tsx +++ b/apps/forms/scaffolds/blocks-editor/blocks/field-block.tsx @@ -47,6 +47,7 @@ import clsx from "clsx"; import { editorlink } from "@/lib/forms/url"; import { SYSTEM_GF_KEY_STARTS_WITH } from "@/k/system"; import { useDialogState } from "@/components/hooks/use-dialog-state"; +import { g11nkey, useG11nResource } from "@/scaffolds/editor/use"; export function FieldBlock({ id, @@ -57,6 +58,12 @@ export function FieldBlock({ const [state, dispatch] = useEditorState(); const [focused, setFocus] = useBlockFocus(id); + const { + form: { available_field_ids }, + } = state; + + const advancedModeDialog = useDialogState(); + const fields = useFormFields(); const form_field: FormFieldDefinition | undefined = fields.find( @@ -65,13 +72,20 @@ export function FieldBlock({ const is_hidden_field = form_field?.type === "hidden"; - const { - form: { available_field_ids }, - } = state; - const advancedModeDialog = useDialogState(); - const can_advanced_mode = fields.length > 0; + const label = useG11nResource( + g11nkey("field", { id: form_field_id!, property: "label" }) + ); + + const placeholder = useG11nResource( + g11nkey("field", { id: form_field_id!, property: "placeholder" }) + ); + + const helptext = useG11nResource( + g11nkey("field", { id: form_field_id!, property: "help_text" }) + ); + const onFieldChange = useCallback( (field_id: string) => { dispatch({ @@ -192,7 +206,14 @@ export function FieldBlock({ preview disabled={!!!form_field} name={form_field?.name ?? ""} - label={form_field?.label ?? ""} + // + // label={form_field?.label ?? ""} + // helpText={form_field?.help_text ?? ""} + // placeholder={form_field?.placeholder ?? ""} + label={label.value} + helpText={helptext.value} + placeholder={placeholder.value} + // type={form_field?.type ?? "text"} required={form_field?.required ?? false} requiredAsterisk @@ -200,8 +221,6 @@ export function FieldBlock({ step={form_field?.step ?? undefined} min={form_field?.min ?? undefined} max={form_field?.max ?? undefined} - helpText={form_field?.help_text ?? ""} - placeholder={form_field?.placeholder ?? ""} options={form_field?.options} optgroups={form_field?.optgroups} multiple={form_field?.multiple ?? false} diff --git a/apps/forms/scaffolds/blocks-editor/blocks/header-block.tsx b/apps/forms/scaffolds/blocks-editor/blocks/header-block.tsx index 927c4cb114..8e57599747 100644 --- a/apps/forms/scaffolds/blocks-editor/blocks/header-block.tsx +++ b/apps/forms/scaffolds/blocks-editor/blocks/header-block.tsx @@ -1,6 +1,5 @@ "use client"; -import { useCallback } from "react"; import { DotsHorizontalIcon, HeadingIcon, @@ -12,8 +11,6 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { EditorFlatFormBlock } from "@/scaffolds/editor/state"; -import { useEditorState } from "@/scaffolds/editor"; import { BlockHeader, FlatBlockBase, @@ -22,36 +19,18 @@ import { } from "./base-block"; import TextareaAutosize from "react-textarea-autosize"; import { Button } from "@/components/ui/button"; +import { g11nkey, useG11nResource } from "@/scaffolds/editor/use"; -export function HeaderBlock({ - id, - title_html, - description_html, -}: EditorFlatFormBlock) { - const [state, dispatch] = useEditorState(); +export function HeaderBlock({ id }: { id: string }) { const deleteBlock = useDeleteBlock(); const [focused, setFocus] = useBlockFocus(id); - const onEditTitle = useCallback( - (title: string) => { - dispatch({ - type: "blocks/title", - block_id: id, - title_html: title, - }); - }, - [dispatch, id] + const title = useG11nResource( + g11nkey("block", { id: id, property: "title_html" }) ); - const onEditDescription = useCallback( - (description: string) => { - dispatch({ - type: "blocks/description", - block_id: id, - description_html: description, - }); - }, - [dispatch, id] + const description = useG11nResource( + g11nkey("block", { id: id, property: "description_html" }) ); return ( @@ -87,15 +66,15 @@ export function HeaderBlock({ type="text" className="bg-background w-full p-4 text-2xl font-bold outline-none" placeholder="Heading" - value={title_html ?? ""} - onChange={(e) => onEditTitle(e.target.value)} + value={title.value} + onChange={(e) => title.change(e.target.value)} /> onEditDescription(e.target.value)} + value={description.value} + onChange={(e) => description.change(e.target.value)} />
diff --git a/apps/forms/scaffolds/blocks-editor/blocks/pdf-block.tsx b/apps/forms/scaffolds/blocks-editor/blocks/pdf-block.tsx index 948847ecef..244a4df9bb 100644 --- a/apps/forms/scaffolds/blocks-editor/blocks/pdf-block.tsx +++ b/apps/forms/scaffolds/blocks-editor/blocks/pdf-block.tsx @@ -21,6 +21,7 @@ import { } from "./base-block"; import { useEditorState } from "@/scaffolds/editor"; import { PDFViewer } from "@/components/pdf-viewer"; +import { Input } from "@/components/ui/input"; export function PdfBlock({ id, @@ -64,17 +65,17 @@ export function PdfBlock({
- { + // TODO: dispatch({ type: "blocks/video/src", block_id: id, src: e.target.value, }); }} - className="bg-neutral-50 border border-neutral-300 text-neutral-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-neutral-700 dark:border-neutral-600 dark:placeholder-neutral-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Video URL" />
diff --git a/apps/forms/scaffolds/blocks-editor/blocks/video-block.tsx b/apps/forms/scaffolds/blocks-editor/blocks/video-block.tsx index a474eade64..8613892269 100644 --- a/apps/forms/scaffolds/blocks-editor/blocks/video-block.tsx +++ b/apps/forms/scaffolds/blocks-editor/blocks/video-block.tsx @@ -23,6 +23,8 @@ import { } from "./base-block"; import { useEditorState } from "@/scaffolds/editor"; import dynamic from "next/dynamic"; +import { Input } from "@/components/ui/input"; +import { g11nkey, useG11nResource } from "@/scaffolds/editor/use"; const ReactPlayer = dynamic(() => import("react-player/lazy"), { ssr: false }); @@ -30,13 +32,15 @@ export function VideoBlock({ id, type, form_field_id, - src, + // src, data, }: EditorFlatFormBlock) { const [state, dispatch] = useEditorState(); const [focused, setFocus] = useBlockFocus(id); const deleteBlock = useDeleteBlock(); + const src = useG11nResource(g11nkey("block", { id: id, property: "src" })); + return ( @@ -77,22 +81,18 @@ export function VideoBlock({
- { - dispatch({ - type: "blocks/video/src", - block_id: id, - src: e.target.value, - }); + src.change(e.target.value || undefined); }} - className="bg-neutral-50 border border-neutral-300 text-neutral-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-neutral-700 dark:border-neutral-600 dark:placeholder-neutral-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Video URL" />
- +
diff --git a/apps/forms/scaffolds/dialogs/langs-add-dialog.tsx b/apps/forms/scaffolds/dialogs/langs-add-dialog.tsx index a322b77a02..46f3a37316 100644 --- a/apps/forms/scaffolds/dialogs/langs-add-dialog.tsx +++ b/apps/forms/scaffolds/dialogs/langs-add-dialog.tsx @@ -22,7 +22,7 @@ export function AddNewLanguageDialog({ }: React.ComponentProps) { const [state, dispatch] = useEditorState(); - const { langs } = state.document; + const { langs } = state.document.g11n; const [newlang, setNewLang] = React.useState(); diff --git a/apps/forms/scaffolds/editor/action.ts b/apps/forms/scaffolds/editor/action.ts index 817c220f58..22d2132743 100644 --- a/apps/forms/scaffolds/editor/action.ts +++ b/apps/forms/scaffolds/editor/action.ts @@ -151,30 +151,45 @@ export interface BlockVHiddenAction { v_hidden: Tokens.ShorthandBooleanBinaryExpression; } +/** + * @deprecated - remove me - use translation module + */ export interface HtmlBlockBodyAction { type: "blocks/html/body"; block_id: string; html: string; } +/** + * @deprecated - remove me - use translation module + */ export interface ImageBlockSrcAction { type: "blocks/image/src"; block_id: string; src: string; } +/** + * @deprecated - remove me - use translation module + */ export interface VideoBlockSrcAction { type: "blocks/video/src"; block_id: string; src: string; } +/** + * @deprecated - remove me - use translation module + */ export interface BlockTitleAction { type: "blocks/title"; block_id: string; title_html: string; } +/** + * @deprecated - remove me - use translation module + */ export interface BlockDescriptionAction { type: "blocks/description"; block_id: string; @@ -416,7 +431,7 @@ export interface EditorDocumentLangDeleteAction { } export interface EditorDocumentLangMessageAction { - type: "editor/document/langs/messages/update"; + type: "editor/document/langs/messages/change"; lang: LanguageCode; key: string; message: string | undefined; diff --git a/apps/forms/scaffolds/editor/init.ts b/apps/forms/scaffolds/editor/init.ts index bf2a6f8ad6..bdd67b71fc 100644 --- a/apps/forms/scaffolds/editor/init.ts +++ b/apps/forms/scaffolds/editor/init.ts @@ -15,7 +15,7 @@ import type { TableXSBMainTableConnection, GDocSchemaTable, TableMenuItem, - IDocumentLangState, + IG11nState, } from "./state"; import { blockstreeflat } from "@/lib/forms/tree"; import { SYM_LOCALTZ, EditorSymbols } from "./symbols"; @@ -519,13 +519,16 @@ function sitedocumentpagesinit({ ]; } -function langinit(lang: LanguageCode): IDocumentLangState { +function langinit(lang: LanguageCode): IG11nState { return { - lang: lang, - lang_default: lang, - langs: [lang], - messages: { - [lang]: {}, + g11n: { + lang: lang, + lang_default: lang, + langs: [lang], + keys: [], + resources: { + [lang]: {}, + }, }, }; } diff --git a/apps/forms/scaffolds/editor/reducer.ts b/apps/forms/scaffolds/editor/reducer.ts index 73520a2cbd..265d3bffd4 100644 --- a/apps/forms/scaffolds/editor/reducer.ts +++ b/apps/forms/scaffolds/editor/reducer.ts @@ -1057,7 +1057,7 @@ export function reducer(state: EditorState, action: EditorAction): EditorState { case "editor/document/langs/set-default": case "editor/document/langs/add": case "editor/document/langs/delete": - case "editor/document/langs/messages/update": { + case "editor/document/langs/messages/change": { return langReducer(state, action); } // #endregion lang diff --git a/apps/forms/scaffolds/editor/reducers/lang.reducer.ts b/apps/forms/scaffolds/editor/reducers/lang.reducer.ts index e9917985d0..eb91a72422 100644 --- a/apps/forms/scaffolds/editor/reducers/lang.reducer.ts +++ b/apps/forms/scaffolds/editor/reducers/lang.reducer.ts @@ -19,58 +19,64 @@ export default function langReducer( case "editor/document/langs/set-current": { const { lang } = action; return produce(state, (draft) => { - draft.document.lang = lang; + assert(draft.document.g11n.langs.includes(lang), "Language not found"); + draft.document.g11n.lang = lang; }); } case "editor/document/langs/set-default": { const { lang } = action; return produce(state, (draft) => { - if (draft.document.langs.length === 1) { - draft.document.langs = [lang]; - draft.document.lang_default = lang; - draft.document.lang = lang; + if (draft.document.g11n.langs.length === 1) { + draft.document.g11n.langs = [lang]; + draft.document.g11n.lang_default = lang; + draft.document.g11n.lang = lang; } else { - assert(draft.document.langs.includes(lang), "Language not found"); - draft.document.lang_default = lang; - draft.document.lang = lang; - draft.document.langs = draft.document.langs.slice().sort((a, b) => { - if (a === lang) return -1; - if (b === lang) return 1; - return 0; - }); + assert( + draft.document.g11n.langs.includes(lang), + "Language not found" + ); + draft.document.g11n.lang_default = lang; + draft.document.g11n.lang = lang; + draft.document.g11n.langs = draft.document.g11n.langs + .slice() + .sort((a, b) => { + if (a === lang) return -1; + if (b === lang) return 1; + return 0; + }); } }); } case "editor/document/langs/add": { const { lang } = action; return produce(state, (draft) => { - const langs = new Set(draft.document.langs); + const langs = new Set(draft.document.g11n.langs); langs.add(lang); - draft.document.langs = Array.from(langs); - draft.document.lang = lang; - draft.document.messages[lang] = {}; + draft.document.g11n.langs = Array.from(langs); + draft.document.g11n.lang = lang; + draft.document.g11n.resources[lang] = {}; }); } case "editor/document/langs/delete": { const { lang } = action; return produce(state, (draft) => { - if (draft.document.langs.length === 1) { + if (draft.document.g11n.langs.length === 1) { toast.error("At least one language is required"); return; } - const langs = new Set(draft.document.langs); + const langs = new Set(draft.document.g11n.langs); langs.delete(lang); - draft.document.langs = Array.from(langs); - draft.document.lang = draft.document.langs[0]; - delete draft.document.messages[lang]; + draft.document.g11n.langs = Array.from(langs); + draft.document.g11n.lang = draft.document.g11n.langs[0]; + delete draft.document.g11n.resources[lang]; }); } - case "editor/document/langs/messages/update": { + case "editor/document/langs/messages/change": { const { lang, key, message } = action; return produce(state, (draft) => { - assert(draft.document.messages[lang], "Language not found"); - draft.document.messages[lang][key] = message; + assert(draft.document.g11n.resources[lang], "Language not found"); + draft.document.g11n.resources[lang][key] = message; }); } } diff --git a/apps/forms/scaffolds/editor/state.ts b/apps/forms/scaffolds/editor/state.ts index a1933e03a0..151cc7f039 100644 --- a/apps/forms/scaffolds/editor/state.ts +++ b/apps/forms/scaffolds/editor/state.ts @@ -320,24 +320,32 @@ interface IInsertionMenuState { insertmenu: TGlobalEditorDialogState; } -export interface IDocumentLangState { - /** - * view document in... - */ - lang: LanguageCode; - /** - * default language - */ - lang_default: LanguageCode; - /** - * available languages provided by user - */ - langs: LanguageCode[]; - - messages: Partial>>; +export interface IG11nState { + g11n: { + /** + * view document in... + */ + lang: LanguageCode; + /** + * default language + */ + lang_default: LanguageCode; + /** + * available languages provided by user + */ + langs: LanguageCode[]; + /** + * @deprecated - not supported + */ + keys: Array; + + resources: Partial< + Record> + >; + }; } -interface IDocumentState extends IDocumentLangState { +interface IDocumentState extends IG11nState { pages: MenuItem[]; selected_page_id?: string; nodes: any[]; diff --git a/apps/forms/scaffolds/editor/use.ts b/apps/forms/scaffolds/editor/use.ts index 03ea26ee96..eb266d358e 100644 --- a/apps/forms/scaffolds/editor/use.ts +++ b/apps/forms/scaffolds/editor/use.ts @@ -1,10 +1,11 @@ "use client"; -import { useMemo, useContext } from "react"; +import { useMemo, useContext, useCallback } from "react"; import type { EditorState, GDocTable, GDocTableID } from "./state"; import { useDispatch, type FlatDispatcher } from "./dispatch"; import { Context } from "./provider"; +import { IFormBlock, IFormField } from "@/types"; export const useEditorState = (): [EditorState, FlatDispatcher] => { const state = useContext(Context); @@ -95,3 +96,106 @@ export function useDatabaseTableId(): string | null { return table_id; } + +/** + * @example + * ```ts + * const t = useDocumentTranslations() + * ``` + */ +export function useDocumentTranslations() { + const [state, dispatch] = useEditorState(); + const { lang, lang_default, resources: messages } = state.document.g11n; + // + + return useCallback( + (key: string) => { + return messages[lang]?.[key]; + }, + [lang, messages] + ); +} + +type G11nFormBlockResourceQuery = [ + type: "block", + { + id: string; + property: keyof Pick< + IFormBlock, + "title_html" | "description_html" | "body_html" | "src" + >; + }, +]; + +type G11nFormFieldResourceQuery = [ + type: "field", + { + id: string; + property: keyof Pick; + }, +]; + +type ResourceKeyQuery = G11nFormBlockResourceQuery | G11nFormFieldResourceQuery; + +export function g11nkey(...q: ResourceKeyQuery) { + const [type, { id, property }] = q; + return [type, id, property].join("."); +} + +export function useG11nResource(key: string) { + // const onEditTitle = useCallback( + // (title: string) => { + // dispatch({ + // type: "blocks/title", + // block_id: id, + // title_html: title, + // }); + // }, + // [dispatch, id] + // ); + + // const onEditDescription = useCallback( + // (description: string) => { + // dispatch({ + // type: "blocks/description", + // block_id: id, + // description_html: description, + // }); + // }, + // [dispatch, id] + // ); + + const [state, dispatch] = useEditorState(); + const { lang, lang_default, resources: messages } = state.document.g11n; + + // + + const fallback = useMemo(() => { + return messages[lang_default]?.[key]; + }, [lang_default, messages, key]); + + const value = useMemo(() => { + return messages[lang]?.[key]; + }, [lang, messages, key]); + + const change = useCallback( + (message?: string) => { + dispatch({ + type: "editor/document/langs/messages/change", + key: key, + message: message, + lang: lang, + }); + }, + [key, lang, dispatch] + ); + + return { + fallback, + value, + change, + lang, + lang_default, + isTranslationMode: lang !== lang_default, + }; +} diff --git a/apps/forms/scaffolds/i18n-editor/index.tsx b/apps/forms/scaffolds/i18n-editor/index.tsx index 41ba12a5c6..9a83024476 100644 --- a/apps/forms/scaffolds/i18n-editor/index.tsx +++ b/apps/forms/scaffolds/i18n-editor/index.tsx @@ -12,7 +12,7 @@ import * as GridLayout from "@/scaffolds/grid-editor/components/layout"; export function I18nEditor() { const [state] = useEditorState(); - const { lang, lang_default, langs, messages } = state.document; + const { lang, lang_default, langs } = state.document.g11n; const keys = [ "1", diff --git a/apps/forms/scaffolds/settings/customize/custom-ending-page-preferences.tsx b/apps/forms/scaffolds/settings/customize/custom-ending-page-preferences.tsx index 04a1a76f9b..748b47de97 100644 --- a/apps/forms/scaffolds/settings/customize/custom-ending-page-preferences.tsx +++ b/apps/forms/scaffolds/settings/customize/custom-ending-page-preferences.tsx @@ -51,7 +51,7 @@ export function EndingPagePreferences() { const { form, - document: { lang }, + document: { g11n }, form: { ending }, } = state; @@ -158,12 +158,12 @@ export function EndingPagePreferences() { Enabling ending page will disable redirection - + {template && (
@@ -175,7 +175,7 @@ export function EndingPagePreferences() { key={template} form_id={form.form_id} title={form.form_title} - lang={lang} + lang={g11n.lang} init={{ template_id: template ?? "default", i18n_overrides: overrides, diff --git a/apps/forms/scaffolds/sidebar/sidebar-mode-design.tsx b/apps/forms/scaffolds/sidebar/sidebar-mode-design.tsx index 193ccbb695..1f5e7b7df4 100644 --- a/apps/forms/scaffolds/sidebar/sidebar-mode-design.tsx +++ b/apps/forms/scaffolds/sidebar/sidebar-mode-design.tsx @@ -50,6 +50,10 @@ import { import { LanguagesIcon } from "lucide-react"; import { useRouter } from "next/navigation"; import { editorlink } from "@/lib/forms/url"; +import { + language_label_map, + supported_form_page_languages, +} from "@/k/supported_languages"; export function ModeDesign() { const [state, dispatch] = useEditorState(); @@ -97,7 +101,7 @@ function LocalizationView() { refreshkey: true, }); - const { lang, lang_default, langs } = state.document; + const { lang, lang_default, langs } = state.document.g11n; const ismultilang = langs.length > 1; @@ -188,10 +192,7 @@ function LocalizationView() { onSelect={() => switchLang(l)} > - {/* - 🇺🇸 - */} - {l}{" "} + {language_label_map[l].flag} {l}{" "} {isdefault && ( , "onChange"> & { onValueChange?: (value?: string) => void; - placeholder?: string; }) { return ( { onValueChange?.(e.target.value || undefined); }} + {...props} /> ); } diff --git a/apps/forms/scaffolds/sidecontrol/sidecontrol-doctype-form.tsx b/apps/forms/scaffolds/sidecontrol/sidecontrol-doctype-form.tsx index 955f0c6d35..4d8c61ca7c 100644 --- a/apps/forms/scaffolds/sidecontrol/sidecontrol-doctype-form.tsx +++ b/apps/forms/scaffolds/sidecontrol/sidecontrol-doctype-form.tsx @@ -20,10 +20,12 @@ import { FormExpression } from "@/lib/forms/expression"; import { PropertyLine, PropertyLineLabel } from "./ui"; import { EditBinaryExpression } from "../panels/extensions/v-edit"; import { PopoverClose } from "@radix-ui/react-popover"; -import { PlainTextControl } from "./controls/plaintext"; +import { InputControl } from "./controls/input"; import { FieldSupports } from "@/k/supported_field_types"; import toast from "react-hot-toast"; import { Badge } from "@/components/ui/badge"; +import { g11nkey, useG11nResource } from "../editor/use"; +import { language_label_map } from "@/k/supported_languages"; export function SideControlDoctypeForm() { const [state, dispatch] = useEditorState(); @@ -88,6 +90,10 @@ function SelectedFormBlockProperties() { {block.type === "field" && } {block.type === "header" && } + {block.type === "video" && } + {/* NOT SUPPORTED */} + {/* {block.type === "image" && } */} + {/* {block.type === "html" && } */}
); } @@ -184,73 +190,87 @@ function PropertyV_Hidden() { } function BlockTypeField() { + const [state, dispatch] = useEditorState(); const [block] = useFocusedFormBlock(); const field = useFormField(block.form_field_id!)!; const is_hidden_field = field.type === "hidden"; + const { lang, lang_default } = state.document.g11n; + const istranslationmode = lang !== lang_default; + + const label = useG11nResource( + g11nkey("field", { id: block.form_field_id!, property: "label" }) + ); + + const placeholder = useG11nResource( + g11nkey("field", { id: block.form_field_id!, property: "placeholder" }) + ); + + const helptext = useG11nResource( + g11nkey("field", { id: block.form_field_id!, property: "help_text" }) + ); if (is_hidden_field) return <>; return ( - Customize + + {istranslationmode ? ( + <> + Field{" "} + + {language_label_map[lang].flag} {lang} + + + ) : ( + <>Field + )} + Label - + {FieldSupports.placeholder(field.type) && ( Placeholder - + )} Help Text - + ); } -/** - * @example - * ```ts - * const t = useDocumentTranslations() - * ``` - */ -function useDocumentTranslations() { - const [state, dispatch] = useEditorState(); - const { lang, lang_default, messages } = state.document; - // - - return useCallback( - (key: string) => { - return messages[lang]?.[key]; - }, - [lang] - ); -} - function BlockTypeHeader() { const [state, dispatch] = useEditorState(); const [block] = useFocusedFormBlock(); - const t = useDocumentTranslations(); - const { lang, lang_default } = state.document; + const { lang, lang_default } = state.document.g11n; const istranslationmode = lang !== lang_default; - const updateMessage = useCallback( - (key: string, message: string | undefined) => { - dispatch({ - type: "editor/document/langs/messages/update", - lang: lang, - key, - message, - }); - }, - [dispatch, lang] + const title = useG11nResource( + g11nkey("block", { id: block.id, property: "title_html" }) + ); + + const description = useG11nResource( + g11nkey("block", { id: block.id, property: "description_html" }) ); return ( @@ -259,33 +279,123 @@ function BlockTypeHeader() { {istranslationmode ? ( <> - Translate {lang} + Header{" "} + + {language_label_map[lang].flag} {lang} + ) : ( - <>Customize + <>Header )} Title - { - updateMessage(block.id + "/title_html", value); - }} - placeholder={block.title_html ?? ""} + Description - + + + + ); +} + +function BlockTypeVideo() { + const [state, dispatch] = useEditorState(); + const [block] = useFocusedFormBlock(); + const { lang, lang_default } = state.document.g11n; + const istranslationmode = lang !== lang_default; + + const src = useG11nResource( + g11nkey("block", { id: block.id, property: "src" }) + ); + + return ( + + + + {istranslationmode ? ( + <> + Video{" "} + + {language_label_map[lang].flag} {lang} + + + ) : ( + <>Video + )} + + + + + URL + + + + + ); +} + +function BlockTypeImage() { + const [state, dispatch] = useEditorState(); + const [block] = useFocusedFormBlock(); + const { lang, lang_default } = state.document.g11n; + const istranslationmode = lang !== lang_default; + + const src = useG11nResource( + g11nkey("block", { id: block.id, property: "src" }) + ); + + return ( + + + + {istranslationmode ? ( + <> + Image{" "} + + {language_label_map[lang].flag} {lang} + + + ) : ( + <>Image + )} + + + + + URL + diff --git a/apps/forms/scaffolds/sidecontrol/sidecontrol-global.tsx b/apps/forms/scaffolds/sidecontrol/sidecontrol-global.tsx index 203437f34d..3284703221 100644 --- a/apps/forms/scaffolds/sidecontrol/sidecontrol-global.tsx +++ b/apps/forms/scaffolds/sidecontrol/sidecontrol-global.tsx @@ -63,12 +63,8 @@ import { PreferenceBody, PreferenceBox, PreferenceBoxHeader, - PreferenceDescription, } from "@/components/preferences"; -import { - language_label_map, - supported_form_page_languages, -} from "@/k/supported_languages"; +import { supported_form_page_languages } from "@/k/supported_languages"; import { Switch } from "@/components/ui/switch"; import { PoweredByGridaWaterMark } from "@/components/powered-by-branding"; import { PropertyLine, PropertyLineControlRoot, PropertyLineLabel } from "./ui"; @@ -151,7 +147,7 @@ function Language() { }); const addnewlangDialog = useDialogState("addnewlang", { refreshkey: true }); const { - document: { lang, langs, lang_default }, + document: { g11n }, } = state; const onLangChange = useCallback( @@ -164,7 +160,17 @@ function Language() { [dispatch] ); - const ismultilangs = langs.length > 1; + const onDefaultLangChange = useCallback( + (lang: LanguageCode) => { + dispatch({ + type: "editor/document/langs/set-default", + lang, + }); + }, + [dispatch] + ); + + const ismultilangs = g11n.langs.length > 1; if (ismultilangs) { return ( @@ -175,9 +181,9 @@ function Language() { />
- {langs.map((l) => { - const isdefault = l === lang_default; - const iscurrent = l === lang; + {g11n.langs.map((l) => { + const isdefault = l === g11n.lang_default; + const iscurrent = l === g11n.lang; return ( onLangChange(l)} @@ -217,8 +223,8 @@ function Language() { Default @@ -251,7 +257,7 @@ function EnableMultiLanguageDialog({ const stepper = useStepper(); const [fallbackLang, setFallbackLang] = useState( - state.document.lang_default + state.document.g11n.lang_default ); const [firstLang, setFirstLang] = useState(); diff --git a/apps/forms/scaffolds/sidecontrol/ui/index.tsx b/apps/forms/scaffolds/sidecontrol/ui/index.tsx index a9a763cbae..9d4d898813 100644 --- a/apps/forms/scaffolds/sidecontrol/ui/index.tsx +++ b/apps/forms/scaffolds/sidecontrol/ui/index.tsx @@ -1,4 +1,5 @@ import { Label } from "@/components/ui/label"; +import { cn } from "@/utils"; export function PropertyLine({ children }: React.PropsWithChildren<{}>) { return ( @@ -8,9 +9,19 @@ export function PropertyLine({ children }: React.PropsWithChildren<{}>) { ); } -export function PropertyLineLabel({ children }: React.PropsWithChildren<{}>) { +export function PropertyLineLabel({ + children, + className, + ...props +}: React.ComponentProps) { return ( -