From 5e1463e472a269082378d68a9a8ba949cc87a675 Mon Sep 17 00:00:00 2001 From: Paul Farault Date: Thu, 2 Mar 2023 18:06:14 +0100 Subject: [PATCH] feat: raw editor using monaco Add react-hook-form to optimize state. Sync raw and visual modes. Persit user settings accross pages (raw mode, show unused tabs). Fixes #96 Fixes #164 --- package-lock.json | 139 +++++- package.json | 3 + src/components/Services/ComponentsNav.tsx | 50 ++- src/components/Services/ParamsContext.tsx | 35 +- src/components/Services/VariablesDisplay.tsx | 423 ++++++++++++------ src/features/userInput/userInputSlice.ts | 106 ++--- src/hooks/usePutServiceConfig.tsx | 6 +- src/pages/_app.tsx | 12 +- src/pages/services/[serviceId].tsx | 26 +- .../[serviceId]/components/[componentId].tsx | 31 +- src/types/index.ts | 7 + 11 files changed, 567 insertions(+), 271 deletions(-) diff --git a/package-lock.json b/package-lock.json index 30a7bda..d749b5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,15 @@ "version": "0.0.0", "dependencies": { "@heroicons/react": "^2.0.13", + "@monaco-editor/react": "^4.4.6", "@reduxjs/toolkit": "^1.9.1", + "mixme": "^0.5.5", "next": "^13.1.4", "oidc-client-ts": "^2.2.1", "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", "react-dom": "^18.2.0", + "react-hook-form": "^7.43.3", "react-oidc-context": "^2.2.1", "react-redux": "^8.0.5", "react-select": "^5.7.0", @@ -678,12 +681,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@commitlint/config-validator/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "node_modules/@commitlint/ensure": { "version": "17.4.0", "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-17.4.0.tgz", @@ -1288,6 +1285,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/@monaco-editor/loader": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.3.2.tgz", + "integrity": "sha512-BTDbpHl3e47r3AAtpfVFTlAi7WXv4UQ/xZmz8atKl4q7epQV5e7+JbigFDViWF71VBi4IIBdcWP57Hj+OWuc9g==", + "dependencies": { + "state-local": "^1.0.6" + }, + "peerDependencies": { + "monaco-editor": ">= 0.21.0 < 1" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.4.6.tgz", + "integrity": "sha512-Gr3uz3LYf33wlFE3eRnta4RxP5FSNxiIV9ENn2D2/rN8KgGAD8ecvcITRtsbbyuOuNkwbuHYxfeaz2Vr+CtyFA==", + "dependencies": { + "@monaco-editor/loader": "^1.3.2", + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@next/env": { "version": "13.1.4", "resolved": "https://registry.npmjs.org/@next/env/-/env-13.1.4.tgz", @@ -1896,6 +1918,12 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -4528,9 +4556,9 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, "node_modules/json-stable-stringify-without-jsonify": { @@ -5096,6 +5124,20 @@ "node": ">= 6" } }, + "node_modules/mixme": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mixme/-/mixme-0.5.5.tgz", + "integrity": "sha512-/6IupbRx32s7jjEwHcycXikJwFD5UujbVNuJFkeKLYje+92OvtuPniF6JhnFm5JCTDUhS+kYK3W/4BWYQYXz7w==", + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/monaco-editor": { + "version": "0.36.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.36.1.tgz", + "integrity": "sha512-/CaclMHKQ3A6rnzBzOADfwdSJ25BFoFT0Emxsc4zYVyav5SkK9iA6lEtIeuN/oRYbwPgviJT+t3l+sjFa28jYg==", + "peer": true + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -5968,6 +6010,21 @@ "react": "^18.2.0" } }, + "node_modules/react-hook-form": { + "version": "7.43.3", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.43.3.tgz", + "integrity": "sha512-LV6Fixh+hirrl6dXbM78aB6n//82aKbsNbcofF3wc6nx1UJLu3Jj/gsg1E5C9iISnLX+du8VTUyBUz2aCy+H7w==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -6658,6 +6715,11 @@ "readable-stream": "^3.0.0" } }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -7981,12 +8043,6 @@ "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true } } }, @@ -8467,6 +8523,23 @@ } } }, + "@monaco-editor/loader": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.3.2.tgz", + "integrity": "sha512-BTDbpHl3e47r3AAtpfVFTlAi7WXv4UQ/xZmz8atKl4q7epQV5e7+JbigFDViWF71VBi4IIBdcWP57Hj+OWuc9g==", + "requires": { + "state-local": "^1.0.6" + } + }, + "@monaco-editor/react": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.4.6.tgz", + "integrity": "sha512-Gr3uz3LYf33wlFE3eRnta4RxP5FSNxiIV9ENn2D2/rN8KgGAD8ecvcITRtsbbyuOuNkwbuHYxfeaz2Vr+CtyFA==", + "requires": { + "@monaco-editor/loader": "^1.3.2", + "prop-types": "^15.7.2" + } + }, "@next/env": { "version": "13.1.4", "resolved": "https://registry.npmjs.org/@next/env/-/env-13.1.4.tgz", @@ -8856,6 +8929,14 @@ "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" + }, + "dependencies": { + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + } } }, "ansi-escapes": { @@ -10755,9 +10836,9 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, "json-stable-stringify-without-jsonify": { @@ -11196,6 +11277,17 @@ "kind-of": "^6.0.3" } }, + "mixme": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mixme/-/mixme-0.5.5.tgz", + "integrity": "sha512-/6IupbRx32s7jjEwHcycXikJwFD5UujbVNuJFkeKLYje+92OvtuPniF6JhnFm5JCTDUhS+kYK3W/4BWYQYXz7w==" + }, + "monaco-editor": { + "version": "0.36.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.36.1.tgz", + "integrity": "sha512-/CaclMHKQ3A6rnzBzOADfwdSJ25BFoFT0Emxsc4zYVyav5SkK9iA6lEtIeuN/oRYbwPgviJT+t3l+sjFa28jYg==", + "peer": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -11711,6 +11803,12 @@ "scheduler": "^0.23.0" } }, + "react-hook-form": { + "version": "7.43.3", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.43.3.tgz", + "integrity": "sha512-LV6Fixh+hirrl6dXbM78aB6n//82aKbsNbcofF3wc6nx1UJLu3Jj/gsg1E5C9iISnLX+du8VTUyBUz2aCy+H7w==", + "requires": {} + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -12207,6 +12305,11 @@ "readable-stream": "^3.0.0" } }, + "state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==" + }, "string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", diff --git a/package.json b/package.json index e60d771..4464af3 100644 --- a/package.json +++ b/package.json @@ -14,12 +14,15 @@ }, "dependencies": { "@heroicons/react": "^2.0.13", + "@monaco-editor/react": "^4.4.6", "@reduxjs/toolkit": "^1.9.1", + "mixme": "^0.5.5", "next": "^13.1.4", "oidc-client-ts": "^2.2.1", "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", "react-dom": "^18.2.0", + "react-hook-form": "^7.43.3", "react-oidc-context": "^2.2.1", "react-redux": "^8.0.5", "react-select": "^5.7.0", diff --git a/src/components/Services/ComponentsNav.tsx b/src/components/Services/ComponentsNav.tsx index 7e9c92d..ae1495e 100644 --- a/src/components/Services/ComponentsNav.tsx +++ b/src/components/Services/ComponentsNav.tsx @@ -1,10 +1,12 @@ -import { useState } from 'react' import Link from 'next/link' import { useRouter } from 'next/router' import { ComponentAsValue, useSelectService } from 'src/features/variables' import { classNames } from 'src/utils' import { Button } from 'src/components/commons' import { useParamsContext } from './ParamsContext' +import { useSelectUserInput } from 'src/features/userInput/hooks' +import { useAppDispatch } from 'src/store' +import { toogleShowUnusedTabs } from 'src/features/userInput' type ComponentNavItem = { id: string @@ -15,9 +17,10 @@ type ComponentsNav = { usedComponents: ComponentNavItem[] unusedComponents: ComponentNavItem[] currentTabId: string + onChange: () => void } -export function ComponentsNav() { +export function ComponentsNav({ onChange }: { onChange: () => void }) { const { currentServiceId, currentComponentId } = useParamsContext() const { value: { components }, @@ -35,6 +38,7 @@ export function ComponentsNav() { usedComponents={usedComponents} unusedComponents={unusedComponents} currentTabId={currentComponentId || currentServiceId} + onChange={onChange} />
@@ -42,6 +46,7 @@ export function ComponentsNav() { usedComponents={usedComponents} unusedComponents={unusedComponents} currentTabId={currentComponentId || currentServiceId} + onChange={onChange} />
@@ -52,6 +57,7 @@ function ComponentsDropdown({ usedComponents, unusedComponents, currentTabId, + onChange, }: ComponentsNav) { const { push, isReady } = useRouter() function handleChange(e: React.ChangeEvent) { @@ -71,13 +77,13 @@ function ComponentsDropdown({ onChange={handleChange} > {usedComponents.map((tab) => ( - ))} {unusedComponents.map((tab) => ( - ))} @@ -90,8 +96,12 @@ function ComponentsTabs({ usedComponents, unusedComponents, currentTabId, + onChange, }: ComponentsNav) { - const [showUnused, setShowUnused] = useState(false) + const { + settings: { showUnusedTabs }, + } = useSelectUserInput() + const dispatch = useAppDispatch() const isCurrentTab = (tab: string) => { if (currentTabId === tab) return true @@ -99,27 +109,30 @@ function ComponentsTabs({ } function toggleShowUnused() { - setShowUnused(!showUnused) + dispatch(toogleShowUnusedTabs()) } return ( ) @@ -128,14 +141,17 @@ function ComponentsTabs({ function ComponentTab({ tab, isCurrentTab, + onChange, }: { tab: ComponentNavItem isCurrentTab: boolean + onChange: () => void }) { return ( (null) -export function ParamsContextProvider({ - children, - currentServiceId, - currentComponentId, -}: React.PropsWithChildren) { - const dispatch = useAppDispatch() - const { getService } = useTdpClient() +export function ParamsContextProvider({ children }) { + // Needs to be in the context to share it on both services and components pages + const { + isReady, + query: { serviceId: tempServiceId, componentId: tempComponentId }, + } = useRouter() + const currentServiceId = isReady && getFirstElementIfArray(tempServiceId) + const currentComponentId = isReady && getFirstElementIfArray(tempComponentId) - const updateServiceValue = useCallback( - async (serviceId: string) => { - const service = await getService(serviceId) - dispatch(setServiceValue(service)) - }, - [getService, dispatch] - ) + const dispatch = useAppDispatch() useEffect(() => { - updateServiceValue(currentServiceId) dispatch(clearUserInput()) - }, [updateServiceValue, currentServiceId, dispatch]) + dispatch(setServiceId(currentServiceId)) + }, [dispatch, currentServiceId]) return ( diff --git a/src/components/Services/VariablesDisplay.tsx b/src/components/Services/VariablesDisplay.tsx index ac93589..81484b1 100644 --- a/src/components/Services/VariablesDisplay.tsx +++ b/src/components/Services/VariablesDisplay.tsx @@ -1,32 +1,167 @@ -import { useState } from 'react' +import { useRef, useState } from 'react' +import { + useForm, + FormProvider, + useFormContext, + useFormState, + useFieldArray, +} from 'react-hook-form' +import Editor from '@monaco-editor/react' +import { merge } from 'mixme' import { Bars3CenterLeftIcon, EyeIcon } from '@heroicons/react/24/solid' import { Disclosure, Sidebar } from 'src/components/Layout' import { Button, IconButon } from 'src/components/commons' -import { useAppDispatch } from 'src/store' -import { setProperty } from 'src/features/userInput' import { classNames } from 'src/utils' import { useParamsContext } from './ParamsContext' import { usePutServiceConfig } from 'src/hooks' +import { ComponentsNav } from './ComponentsNav' +import { useAppDispatch } from 'src/store' +import { + clearUserInput, + setComponent, + setRawMode, + setServiceVariables, +} from 'src/features/userInput' +import { useSelectUserInput } from 'src/features/userInput/hooks' export function VariablesDisplay({ variables }: { variables: Object }) { - const [isRawMode, setIsRawMode] = useState(false) + const flattenedVariables = flattenObject(variables) + const methods = useForm({ + defaultValues: flattenedVariables, + }) + + return ( + + + + ) +} + +function flattenObject(obj: Object) { + const res = {} + Object.entries(obj).forEach((o) => { + const [k, v] = o + if (typeof v === 'object' && v !== null) { + if (Array.isArray(v)) { + res[k] = v.map((av) => flattenObject(av)) + } else { + Object.entries(flattenObject(v)).forEach((fo) => { + const [fk, fv] = fo + res[k + '.' + fk] = fv + }) + } + } else { + res[k] = v + } + }) + return res +} + +function ServiceVariables({ variables }: { variables: Object }) { + const { currentServiceId, currentComponentId } = useParamsContext() + const dispatch = useAppDispatch() + const { getValues, control, reset } = useFormContext() + const { dirtyFields } = useFormState({ control }) + const editorRef = useRef(null) + const { + settings: { showRawMode }, + } = useSelectUserInput() + + function handleRawEditorDidMount(editor, monaco) { + editorRef.current = editor + } + + function saveVariables() { + let dirtyValues: Object + if (showRawMode) { + const editorValues = JSON.parse(editorRef.current.getValue()) as Object + dirtyValues = getDirtyValues(variables, editorValues) + } else { + dirtyValues = getRHFDirtyValues(dirtyFields, getValues) + } + if (currentServiceId && currentComponentId) { + dispatch( + setComponent({ + componentId: currentComponentId, + variables: dirtyValues, + }) + ) + } + if (currentServiceId && !currentComponentId) { + dispatch(setServiceVariables(dirtyValues)) + } + reset(flattenObject(merge(variables, dirtyValues))) + } + + return ( + <> + + + + ) +} + +function getDirtyValues(baseObject: Object, object: Object) { + const dirtyValues = {} + Object.keys(object).forEach((key) => { + if (baseObject.hasOwnProperty(key)) { + if (baseObject[key] !== object[key]) { + if (typeof baseObject[key] === 'object') { + const dirtySubValues = getDirtyValues(baseObject[key], object[key]) + if (Object.keys(dirtySubValues).length > 0) { + dirtyValues[key] = dirtySubValues + } + } else { + dirtyValues[key] = object[key] + } + } + } else { + dirtyValues[key] = object[key] + } + }) + return dirtyValues +} + +function VariablesEditionZone({ + variables, + saveVariables, + handleRawEditorDidMount, +}: { + variables: Object + saveVariables: () => void + handleRawEditorDidMount: (editor: any, monaco: any) => void +}) { const isVariableEmpty = !Object.entries(variables).length if (isVariableEmpty) return return ( <> - - {isRawMode ? ( - - ) : ( - - )} - + + ) } +function getRHFDirtyValues( + dirtyFields: Object, + getValues: (key: string) => any +) { + const dirtyValues = {} + Object.keys(dirtyFields).forEach((key) => { + dirtyValues[key] = getValues(key) + }) + return dirtyValues +} + function NoVariableMessage() { const { currentServiceId, currentComponentId } = useParamsContext() return ( @@ -36,38 +171,36 @@ function NoVariableMessage() { ) } -function Toolbar({ - isRawMode, - setIsRawMode, -}: { - isRawMode: boolean - setIsRawMode: (isRaw: boolean) => void -}) { +function Toolbar({ saveVariables }: { saveVariables: () => void }) { return (
- +
) } -function RawViewButton({ - isRaw, - setIsRaw, -}: { - isRaw: boolean - setIsRaw: React.Dispatch> -}) { +function RawViewButton({ saveVariables }: { saveVariables: () => void }) { + const dispatch = useAppDispatch() + const { + settings: { showRawMode }, + } = useSelectUserInput() + + function handleSetRawMode(rawMode: boolean) { + saveVariables() + dispatch(setRawMode(rawMode)) + } + return (
setIsRaw(true)} - isActive={isRaw} + onClick={() => handleSetRawMode(true)} + isActive={showRawMode} icon={Bars3CenterLeftIcon} text="Raw" /> setIsRaw(false)} - isActive={!isRaw} + onClick={() => handleSetRawMode(false)} + isActive={!showRawMode} className="-ml-px border-l-gray-400 border-l" icon={EyeIcon} text="View" @@ -76,24 +209,92 @@ function RawViewButton({ ) } -function RawMode({ variables }: { variables: Object }) { +function VariablesForm({ + variables, + saveVariables, + handleRawEditorDidMount, +}: { + variables: Object + saveVariables: () => void + handleRawEditorDidMount: (editor: any, monaco: any) => void +}) { + const { + settings: { showRawMode }, + } = useSelectUserInput() + const [message, setMessage] = useState('') + return ( -
-

TODO: code editor

-
+
+ {showRawMode ? ( + + ) : ( + + )} + + + ) +} + +function Form({ + children, + message, + setMessage, +}: { + children: React.ReactNode + message: string + setMessage: (message: string) => void +}) { + const methods = useFormContext() + const { sendVariables } = usePutServiceConfig() + const dispatch = useAppDispatch() + + function handleSubmit(data: Object) { + sendVariables(message) + setMessage('') + dispatch(clearUserInput()) + } + + return
{children}
+} + +function RawMode({ + variables, + handleRawEditorDidMount, +}: { + variables: Object + handleRawEditorDidMount: (editor: any, monaco: any) => void +}) { + return ( + ) } function ViewMode({ variables }: { variables: Object }) { const { primitiveVariables, objectVariables: dictionaries } = splitObjectVariables(variables) + + const Dictionaries = dictionaries.map((dict) => ( + + )) + return ( -
+
- {dictionaries.map((dict) => ( - - ))} - + {Dictionaries} +
) } @@ -105,7 +306,10 @@ type SplitObjectVariables = { function splitObjectVariables(variables: Object) { return Object.entries(variables).reduce( ({ primitiveVariables, objectVariables }, currentValue) => { - if (typeof currentValue[1] === 'object') { + if ( + typeof currentValue[1] === 'object' && + !Array.isArray(currentValue[1]) + ) { objectVariables.push(currentValue) } else { primitiveVariables.push(currentValue) @@ -119,7 +323,7 @@ function splitObjectVariables(variables: Object) { function Dictionary({ dict }: { dict: [string, Object] }) { const [dictId, dictVariables] = dict return ( - + undefined

if (typeof value === 'string' || typeof value === 'number') { - return + return } if (typeof value === 'boolean') { - return + return } if (typeof value === 'object') { + if (value === null) return

null

if (Array.isArray(value)) { - return + return } - return ( - - ) + console.log(property) + return } return

{`Unknown type (${typeof value})`}

} -function ArrayField({ - property, - value, -}: { - property: string - value: SimpleValue[] -}) { +function ArrayField({ property }: { property: string }) { + const { register } = useFormContext() + const { fields } = useFieldArray({ + name: property, + }) + + if (fields.length === 0) return

[]

+ return ( -
    - {value.map((v, i) => ( -
  1. {getField({ value: v, property })}
  2. +
      + {fields.map((field, index) => ( +
    1. + +
    2. ))}
    ) } -function BooleanField({ - property, - value, -}: { - property: string - value: boolean -}) { - const { currentServiceId, currentComponentId } = useParamsContext() - const dispatch = useAppDispatch() +function BooleanField({ property }: { property: string }) { + const { register } = useFormContext() - function handleChecked(event: React.ChangeEvent) { - const newValue = event.target.checked - dispatch( - setProperty({ - serviceId: currentServiceId, - componentId: currentComponentId, - property, - value: newValue, - }) - ) - } return (
    @@ -244,68 +436,39 @@ function BooleanField({ ) } -function StringNumberField({ - property, - value, -}: { - property: string - value: string | number -}) { - const { currentServiceId, currentComponentId } = useParamsContext() - const dispatch = useAppDispatch() - const [error, setError] = useState(false) +function StringNumberField({ property }: { property: string }) { + const { register } = useFormContext() + //TODO: handle errors + let error - function handleChange(event: React.ChangeEvent) { - setError(false) - try { - const newValue = JSON.parse(event.target.value) - dispatch( - setProperty({ - serviceId: currentServiceId, - componentId: currentComponentId, - property, - value: newValue, - }) - ) - } catch (err) { - setError(true) - } - } + // TODO: handle objects return ( -
    - -
    + ) } -function ValidateBar() { - const { sendVariables } = usePutServiceConfig() - const [validateMessage, setValidateMessage] = useState('') - - function handleSubmit(event: React.MouseEvent) { - event.preventDefault() - sendVariables(validateMessage) - setValidateMessage('') - } - +function ValidateBar({ + onValidate, + message, + setMessage, +}: { + onValidate: () => void + message: string + setMessage: (message: string) => void +}) { return (
    setValidateMessage(e.target.value)} + value={message} + onChange={(e) => setMessage(e.target.value)} placeholder="Commit message" /> -
    diff --git a/src/features/userInput/userInputSlice.ts b/src/features/userInput/userInputSlice.ts index acca5e6..373fa90 100644 --- a/src/features/userInput/userInputSlice.ts +++ b/src/features/userInput/userInputSlice.ts @@ -1,4 +1,5 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import { merge } from 'mixme' type userInput = { id: string @@ -7,97 +8,72 @@ type userInput = { id: string variables: Object }[] + settings: { + showUnusedTabs: boolean + showRawMode: boolean + } } -const initialState = {} as userInput +const initialState = { + components: [], + settings: { showUnusedTabs: false, showRawMode: false }, +} as userInput export const userInputSlice = createSlice({ name: 'userInput', initialState, reducers: { - clearUserInput: () => initialState, - setProperty: ( - state, - action: PayloadAction<{ - serviceId: string - componentId?: string - property: string - value: any - }> - ) => { - if (action.payload.componentId) { - userInputSlice.caseReducers.setComponentProperty(state, { - type: 'setComponentProperty', - payload: { - serviceId: action.payload.serviceId, - componentId: action.payload.componentId, - property: action.payload.property, - value: action.payload.value, - }, - }) - } else { - userInputSlice.caseReducers.setServiceProperty(state, action) - } + clearUserInput: (state) => { + state.components = [] + state.variables = {} + state.id = null }, - setServiceProperty: ( - state, - action: PayloadAction<{ - serviceId: string - property: string - value: any - }> - ) => { - const { serviceId, property, value } = action.payload - if (!state.id) { - state.id = serviceId - state.variables = {} - } - if (state.id !== serviceId) { - throw new Error( - `ServiceId mismatch: ${serviceId} does not match ${state.id}` - ) - } - state.variables[property] = value + setServiceId: (state, action: PayloadAction) => { + state.id = action.payload }, - setComponentProperty: ( + setServiceVariables: (state, action: PayloadAction) => { + const isVariablesEmpty = Object.keys(action.payload).length === 0 + if (isVariablesEmpty) return + state.variables = merge(state.variables, action.payload) + }, + setComponent: ( state, action: PayloadAction<{ - serviceId: string componentId: string - property: string - value: any + variables: Object }> ) => { - const { serviceId, componentId, property, value } = action.payload - if (!state.id) { - state.id = serviceId - state.variables = {} - } - if (!state.components) { - state.components = [] - } - if (state.id !== serviceId) { - throw new Error( - `ServiceId mismatch: ${serviceId} does not match ${state.id}` - ) - } + const { componentId, variables } = action.payload + const isVariablesEmpty = Object.keys(variables).length === 0 + if (isVariablesEmpty) return const component = state.components.find( (component) => component.id === componentId ) if (component) { - component.variables[property] = value + component.variables = merge(component.variables, variables) } else { state.components.push({ id: componentId, - variables: { - [property]: value, - }, + variables, }) } }, + toogleShowUnusedTabs: (state) => { + state.settings.showUnusedTabs = !state.settings.showUnusedTabs + }, + setRawMode: (state, action: PayloadAction) => { + state.settings.showRawMode = action.payload + }, }, }) -export const { setProperty, clearUserInput } = userInputSlice.actions +export const { + clearUserInput, + setServiceId, + setServiceVariables, + setComponent, + toogleShowUnusedTabs, + setRawMode, +} = userInputSlice.actions export default userInputSlice.reducer diff --git a/src/hooks/usePutServiceConfig.tsx b/src/hooks/usePutServiceConfig.tsx index 041da0c..a5092e1 100644 --- a/src/hooks/usePutServiceConfig.tsx +++ b/src/hooks/usePutServiceConfig.tsx @@ -3,7 +3,6 @@ import type { ComponentUpdate, ServiceUpdate } from 'src/clients/tdpClient' import { toast } from 'react-toastify' import { useSelectUserInput } from 'src/features/userInput/hooks' import { useAppDispatch } from 'src/store' -import { clearUserInput } from 'src/features/userInput' import { setServiceValue } from 'src/features/variables' import Link from 'next/link' @@ -27,7 +26,7 @@ export function usePutServiceConfig() { return await patchComponent(componentId, serviceId, componentUpdate) } - const fetchServices = async () => { + const refreshService = async () => { const service = await getService(userInput.id) dispatch(setServiceValue(service)) } @@ -52,8 +51,7 @@ export function usePutServiceConfig() { }) }) } - fetchServices() - dispatch(clearUserInput()) + refreshService() toast.success( Variables successfully updated for {userInput.id} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index f93ff9a..b138c52 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -7,8 +7,16 @@ import { DashboardLayout } from 'src/components/Layout' import '../styles/globals.css' import { LoadVariables } from 'src/features/variables/LoadVariables' import { AppProps } from 'next/app' +import { NextPageWithLayout } from 'src/types' -export default function MyApp({ Component, pageProps }: AppProps) { +type AppPropsWithLayout = AppProps & { + Component: NextPageWithLayout +} + +export default function MyApp({ Component, pageProps }: AppPropsWithLayout) { + // Use the layout defined at the page level, if available + // see https://nextjs.org/docs/basic-features/layouts#with-typescript + const getLayout = Component.getLayout ?? ((page) => page) return ( @@ -16,7 +24,7 @@ export default function MyApp({ Component, pageProps }: AppProps) { - + {getLayout()} diff --git a/src/pages/services/[serviceId].tsx b/src/pages/services/[serviceId].tsx index 248c245..933f187 100644 --- a/src/pages/services/[serviceId].tsx +++ b/src/pages/services/[serviceId].tsx @@ -1,14 +1,17 @@ import { useRouter } from 'next/router' +import { ReactElement } from 'react' +import { merge } from 'mixme' import { PageTitle } from 'src/components/Layout' import { - ComponentsNav, ParamsContextProvider, VariablesDisplay, } from 'src/components/Services' import { useSelectService } from 'src/features/variables' +import { NextPageWithLayout } from 'src/types' import { getFirstElementIfArray } from 'src/utils' +import { useSelectUserInput } from 'src/features/userInput/hooks' -export default function ServicePage() { +const ServicePage: NextPageWithLayout = () => { const { isReady, query: { serviceId: tempServiceId }, @@ -19,13 +22,24 @@ export default function ServicePage() { value: { variables }, } = useSelectService(serviceId) + const { variables: userInput } = useSelectUserInput() + if (!variables) return

    Loading...

    return ( - + <> Variables configuration - - - + {/* key allows to re-render when changing page (as the ParamsContext is shared accross all services/components) */} + + ) } + +ServicePage.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default ServicePage diff --git a/src/pages/services/[serviceId]/components/[componentId].tsx b/src/pages/services/[serviceId]/components/[componentId].tsx index a24d84b..282ad04 100644 --- a/src/pages/services/[serviceId]/components/[componentId].tsx +++ b/src/pages/services/[serviceId]/components/[componentId].tsx @@ -1,14 +1,17 @@ import { useRouter } from 'next/router' +import { ReactElement } from 'react' import { PageTitle } from 'src/components/Layout' import { - ComponentsNav, ParamsContextProvider, VariablesDisplay, } from 'src/components/Services' +import { useSelectUserInput } from 'src/features/userInput/hooks' import { useSelectComponent } from 'src/features/variables' +import { NextPageWithLayout } from 'src/types' import { getFirstElementIfArray } from 'src/utils' +import { merge } from 'mixme' -export default function ServicePage() { +const ComponentPage: NextPageWithLayout = () => { const { isReady, query: { serviceId: tempServiceId, componentId: tempComponentId }, @@ -20,16 +23,26 @@ export default function ServicePage() { value: { variables }, } = useSelectComponent(serviceId, componentId) + const { components } = useSelectUserInput() + const userInput = + components.find((c) => c.id === componentId)?.variables || {} + if (!variables) return

    Loading...

    return ( - + <> Variables configuration - - - + {/* key allows to re-render when changing page (as the ParamsContext is shared accross all services/components) */} + + ) } + +ComponentPage.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default ComponentPage diff --git a/src/types/index.ts b/src/types/index.ts index dff44ce..7ad6c86 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1 +1,8 @@ +import { NextPage } from 'next' +import { ReactElement, ReactNode } from 'react' + export type HeroIcon = (props: React.ComponentProps<'svg'>) => JSX.Element + +export type NextPageWithLayout

    = NextPage & { + getLayout?: (page: ReactElement) => ReactNode +}