diff --git a/.env b/.env index d359dd3..fba9e7f 100644 --- a/.env +++ b/.env @@ -1,2 +1,3 @@ -GRAPH_URL=cg.optimizely.com -GRAPH_SINGLE_KEY= \ No newline at end of file +GRAPH_URL=staging.cg.optimizely.com +GRAPH_SINGLE_KEY=sWolKR11UkBvhjCu5phHocFiIsHFQfOzUCPIeCRDAPRm5uEF +CMS_URL=app-sacttestrel9m87qp001.cmstest.optimizely.com \ No newline at end of file diff --git a/components.json b/components.json new file mode 100644 index 0000000..90e9bf9 --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/styles/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/package.json b/package.json index 792406a..17cfacc 100644 --- a/package.json +++ b/package.json @@ -11,10 +11,20 @@ }, "dependencies": { "@apollo/client": "^3.10.4", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-select": "^2.2.5", + "@radix-ui/react-slot": "^1.2.3", + "axios": "^1.10.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "graphql": "^16.8.1", + "lucide-react": "^0.525.0", "next": "14.2.3", "react": "^18", - "react-dom": "^18" + "react-dom": "^18", + "shadcn": "^2.7.0", + "tailwind-merge": "^3.3.1", + "tailwindcss-animate": "^1.0.7" }, "devDependencies": { "@graphql-codegen/cli": "^5.0.2", @@ -26,5 +36,6 @@ "postcss": "^8", "tailwindcss": "^3.4.1", "typescript": "^5" - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/src/apolloClient.tsx b/src/apolloClient.tsx index c487ee1..a6110f7 100644 --- a/src/apolloClient.tsx +++ b/src/apolloClient.tsx @@ -7,6 +7,15 @@ const graphUrl = process.env.GRAPH_URL; const cmsUrl = process.env.CMS_URL; const preview_token = getPreviewToken(); +const possibleTypes = { + _IComponent: [ + 'OptiFormsContainerData', + '_Component', + '_Section' + ], +}; + + // In Preview Mode if (preview_token) { const httpLink = createHttpLink({ @@ -24,7 +33,7 @@ if (preview_token) { client = new ApolloClient({ link: authLink.concat(httpLink), - cache: new InMemoryCache() + cache: new InMemoryCache({ possibleTypes }), }); const communicationScript = document.createElement('script'); @@ -50,7 +59,7 @@ if (client === undefined) { client = new ApolloClient({ link: authLink.concat(httpLink), - cache: new InMemoryCache() + cache: new InMemoryCache({ possibleTypes }), }); } diff --git a/src/components/base/CompositionNodeComponent.tsx b/src/components/base/CompositionNodeComponent.tsx index 96c4078..2df416e 100644 --- a/src/components/base/CompositionNodeComponent.tsx +++ b/src/components/base/CompositionNodeComponent.tsx @@ -1,28 +1,47 @@ import { FragmentType, useFragment } from '../../graphql/fragment-masking' import { graphql } from '@/graphql' +import TextboxElementComponent from '../elements/TextboxElementComponent' +import TextareaElementComponent from '../elements/TextareaElementComponent' +import SelectionElementComponent from '../elements/SelectionElementComponent' +import SubmitElementComponent from '../elements/SubmitElementComponent' import ParagraphElementComponent from '../elements/ParagraphElementComponent' export const CompositionComponentNodeFragment = graphql(/* GraphQL */ ` - fragment compositionComponentNode on CompositionComponentNode { - key - component { - _metadata { - types - } - ...paragraphElement +fragment compositionComponentNode on CompositionComponentNode { + key + component { + _metadata { + types } + ...textboxElement + ...textareaElement + ...selectionElement + ...submitElement + ...paragraphElement } +} `) const CompositionComponentNodeComponent = (props: { - compositionComponentNode: FragmentType + compositionComponentNode: FragmentType, + formState?: any }) => { const compositionComponentNode = useFragment(CompositionComponentNodeFragment, props.compositionComponentNode) const component = compositionComponentNode.component + switch (component?.__typename) { + case "OptiFormsTextboxElement": + return + case "OptiFormsTextareaElement": + return + case "OptiFormsSelectionElement": + return + case "OptiFormsSubmitElement": + return case "ParagraphElement": - return + return default: + console.log(`Unknown component type: ${component?.__typename}`); return <>NotImplementedException } } diff --git a/src/components/base/FormsComponent.tsx b/src/components/base/FormsComponent.tsx new file mode 100644 index 0000000..df68ff7 --- /dev/null +++ b/src/components/base/FormsComponent.tsx @@ -0,0 +1,126 @@ +import React, { FC, useEffect } from 'react' +import { useQuery } from '@apollo/client' + +import { graphql } from '@/graphql' +import { onContentSaved } from "@/helpers/onContentSaved"; +import { RenderCompositionNode } from './VisualBuilderComponent'; + +export const Forms = graphql(/* GraphQL */ ` +query Forms($key: String, $version: String) { + OptiFormsContainerData(where: { + _metadata: { key: { eq: $key } } + _or: { _metadata: { version: { eq: $version } } } + }) { + items { + composition { + grids: nodes { + ... on CompositionStructureNode { + key + __typename + displayName + nodeType + layoutType + component { + ..._IComponent + } + nodes: nodes { + ... on CompositionStructureNode { + key + __typename + displayName + nodeType + layoutType + nodes: nodes { + ... on CompositionStructureNode { + key + __typename + displayName + nodeType + layoutType + nodes: nodes { + ...compositionComponentNode + ... on CompositionStructureNode { + key + __typename + displayName + nodeType + layoutType + nodes: nodes { + ...compositionComponentNode + } + } + } + } + } + } + } + } + } + } + _metadata { + key + version, + } + } + } +} + +`) + +interface FormsProps { + contentKey?: string; + version?: string; +} + +const FormsComponent: FC = ({ version, contentKey }) => { + const formState: any = {}; + const variables: Record = {}; + if (version) { + variables.version = version; + } + + if (contentKey) { + variables.key = contentKey; + } + + const { data, refetch } = useQuery(Forms, { + variables: variables, + notifyOnNetworkStatusChange: true, + }); + + useEffect(() => { + onContentSaved(_ => { + const contentIdArray = _.contentLink.split('_') + if (contentIdArray.length > 1) { + version = contentIdArray[contentIdArray.length - 1] + variables.version = version; + } + refetch(variables); + }) + }, []); + + const forms = data?.OptiFormsContainerData?.items; + if (!forms) { + return null; + } + + const form: any = forms[forms.length - 1]; + if (!form) { + return null; + } + + return ( +
+
+ {form?.composition?.grids?.map((grid: any) => +
+ {RenderCompositionNode(grid, formState)} +
+ )} +
+
+ ) +} + +export default FormsComponent \ No newline at end of file diff --git a/src/components/base/VisualBuilderComponent.tsx b/src/components/base/VisualBuilderComponent.tsx index 3ed752d..2a712ec 100644 --- a/src/components/base/VisualBuilderComponent.tsx +++ b/src/components/base/VisualBuilderComponent.tsx @@ -1,9 +1,10 @@ -import React, { FC, useEffect } from 'react' +import React, { FC, useEffect, useState } from 'react' import { useQuery } from '@apollo/client' import { graphql } from '@/graphql' import CompositionNodeComponent from './CompositionNodeComponent' import { onContentSaved } from "@/helpers/onContentSaved"; +import FormsComponent from './FormsComponent'; export const VisualBuilder = graphql(/* GraphQL */ ` query VisualBuilder($key: String, $version: String) { @@ -13,16 +14,39 @@ query VisualBuilder($key: String, $version: String) { }) { items { composition { - grids: nodes { + grids: nodes { + ... on CompositionStructureNode { + key + __typename + displayName + nodeType + layoutType + component { + ..._IComponent + } + nodes: nodes { ... on CompositionStructureNode { key - rows: nodes { + __typename + displayName + nodeType + layoutType + nodes: nodes { ... on CompositionStructureNode { key - columns: nodes { + __typename + displayName + nodeType + layoutType + nodes: nodes { + ...compositionComponentNode ... on CompositionStructureNode { key - elements: nodes { + __typename + displayName + nodeType + layoutType + nodes: nodes { ...compositionComponentNode } } @@ -32,6 +56,8 @@ query VisualBuilder($key: String, $version: String) { } } } + } + } _metadata { key version, @@ -39,14 +65,36 @@ query VisualBuilder($key: String, $version: String) { } } } + +fragment _IComponent on _IComponent { + __typename + ...FormContainerData +} + +fragment FormContainerData on OptiFormsContainerData { + SubmitConfirmationMessage + ResetConfirmationMessage + SubmitUrl { + type + default + hierarchical + internal + graph + base + } + Title + Description + ShowSummaryMessageAfterSubmission +} `) interface VisualBuilderProps { - contentKey?: string; - version?: string; + contentKey?: string; + version?: string; } const VisualBuilderComponent: FC = ({ version, contentKey }) => { + const formState: any = {}; const variables: Record = {}; if (version) { variables.version = version; @@ -56,18 +104,19 @@ const VisualBuilderComponent: FC = ({ version, contentKey }) variables.key = contentKey; } - const { data, refetch } = useQuery(VisualBuilder, { - variables: variables, - notifyOnNetworkStatusChange: true, }); + const { data, refetch } = useQuery(VisualBuilder, { + variables: variables, + notifyOnNetworkStatusChange: true, + }); useEffect(() => { onContentSaved(_ => { - const contentIdArray = _.contentLink.split('_') - if (contentIdArray.length > 1) { - version = contentIdArray[contentIdArray.length - 1] - variables.version = version; - } - refetch(variables); + const contentIdArray = _.contentLink.split('_') + if (contentIdArray.length > 1) { + version = contentIdArray[contentIdArray.length - 1] + variables.version = version; + } + refetch(variables); }) }, []); @@ -76,6 +125,10 @@ const VisualBuilderComponent: FC = ({ version, contentKey }) return null; } + if (data?._Experience?.items?.length === 0) { + return ; + } + const experience: any = experiences[experiences.length - 1]; if (!experience) { @@ -83,23 +136,12 @@ const VisualBuilderComponent: FC = ({ version, contentKey }) } return ( -
-
+
+
{experience?.composition?.grids?.map((grid: any) => -
- {grid.rows?.map((row: any) => -
- {row.columns?.map((column: any) => ( -
- {column.elements?.map((element: any) => -
- -
- )} -
- ))} -
)} +
+ {RenderCompositionNode(grid, formState)}
)}
@@ -107,4 +149,71 @@ const VisualBuilderComponent: FC = ({ version, contentKey }) ) } -export default VisualBuilderComponent \ No newline at end of file +export default VisualBuilderComponent + +export const RenderCompositionNode = (node: any, formState?: any): JSX.Element | null => { + if (!node || !node.__typename) { + return null; + } + const { layoutType } = node; + if (layoutType === "form") { + const w = window as any; + w.submitUrl = node.component.SubmitUrl.default; + } + + // Handle CompositionStructureNode with different nodeTypes + if (node.__typename === "CompositionStructureNode") { + const { key, nodeType, nodes } = node; + + // Switch based on nodeType + switch (nodeType) { + case "section": + return ( +
+ {nodes?.map((childNode: any) => RenderCompositionNode(childNode, formState))} +
+ ); + + case "step": + return ( +
+ {nodes?.map((childNode: any) => RenderCompositionNode(childNode, formState))} +
+ ); + + case "row": + return ( +
+ {nodes?.map((childNode: any) => RenderCompositionNode(childNode, formState))} +
+ ); + + case "column": + return ( +
+ {nodes?.map((childNode: any) => RenderCompositionNode(childNode, formState))} +
+ ); + + default: + // Handle any other nodeType or fallback to generic structure + return ( +
+ {nodes?.map((childNode: any) => RenderCompositionNode(childNode, formState))} +
+ ); + } + } + + // Handle CompositionComponentNode (leaf elements) + if (node.__typename === "CompositionComponentNode" || node.component) { + return ( +
+ +
+ ); + } + + // Fallback for unknown node types + return null; +}; diff --git a/src/components/elements/SelectionElementComponent.tsx b/src/components/elements/SelectionElementComponent.tsx new file mode 100644 index 0000000..dc8a248 --- /dev/null +++ b/src/components/elements/SelectionElementComponent.tsx @@ -0,0 +1,49 @@ +import { FragmentType, useFragment } from '../../graphql/fragment-masking' +import { graphql } from '@/graphql' +import { Label } from '../ui/label' + +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { isRequiredValidator } from '@/helpers/validatorHelper' + +export const SelectionElementComponentNodeFragment = graphql(/* GraphQL */ ` +fragment selectionElement on OptiFormsSelectionElement { + Label + Tooltip + AutoComplete + Validators + Options +} +`) + +const SelectionElementComponent = (props: { + selectionElement: FragmentType, + formState?: any +}) => { + const node = useFragment(SelectionElementComponentNodeFragment, props.selectionElement) + const Options = node.Options || [] + return (<> + + + + ) +} + +export default SelectionElementComponent \ No newline at end of file diff --git a/src/components/elements/SubmitElementComponent.tsx b/src/components/elements/SubmitElementComponent.tsx new file mode 100644 index 0000000..2f602e4 --- /dev/null +++ b/src/components/elements/SubmitElementComponent.tsx @@ -0,0 +1,39 @@ +import { FragmentType, useFragment } from '../../graphql/fragment-masking' +import { graphql } from '@/graphql' +import { Input } from '../ui/input' +import { Button } from '../ui/button' +import axios from 'axios' + +export const SubmitElementComponentNodeFragment = graphql(/* GraphQL */ ` +fragment submitElement on OptiFormsSubmitElement { + Label + Tooltip +} +`) + +const SubmitElementComponent = (props: { + submitElement: FragmentType, + formState?: any +}) => { + const node = useFragment(SubmitElementComponentNodeFragment, props.submitElement) + + return ( + <> +

+ + + ) +} + +export default SubmitElementComponent \ No newline at end of file diff --git a/src/components/elements/TextareaElementComponent.tsx b/src/components/elements/TextareaElementComponent.tsx new file mode 100644 index 0000000..1df66c9 --- /dev/null +++ b/src/components/elements/TextareaElementComponent.tsx @@ -0,0 +1,36 @@ +import { FragmentType, useFragment } from '../../graphql/fragment-masking' +import { graphql } from '@/graphql' +import { Textarea } from '../ui/textarea' +import { Label } from '../ui/label' +import { isRequiredValidator } from '@/helpers/validatorHelper' + +export const TextareaComponentNodeFragment = graphql(/* GraphQL */ ` +fragment textareaElement on OptiFormsTextareaElement { + Label + Tooltip + Placeholder + AutoComplete + PredefinedValue + Validators +} +`) + +const TextareaElementComponent = (props: { + textareElement: FragmentType, + formState?: any +}) => { + const node = useFragment(TextareaComponentNodeFragment, props.textareElement) + + return ( + <> + +