From 9860472089bebd8faca016c58156e04677d13c64 Mon Sep 17 00:00:00 2001 From: pling-scottlogic <79100986+pling-scottlogic@users.noreply.github.com> Date: Fri, 22 Sep 2023 12:01:44 +0100 Subject: [PATCH] Manage layout persistence via interface (#55) --- vuu-ui/package-lock.json | 3 + vuu-ui/packages/vuu-layout/src/index.ts | 1 + .../LayoutPersistenceManager.ts | 46 +++++++++++ .../LocalLayoutPersistenceManager.ts | 79 +++++++++++++++++++ .../src/layout-persistence/index.ts | 2 + .../src/layout-management/LayoutList.tsx | 6 +- .../src/layout-management/layoutTypes.ts | 4 +- .../layout-management/useLayoutManager.tsx | 57 +++++++------ .../src/examples/Apps/NewTheme.examples.tsx | 4 - 9 files changed, 163 insertions(+), 39 deletions(-) create mode 100644 vuu-ui/packages/vuu-layout/src/layout-persistence/LayoutPersistenceManager.ts create mode 100644 vuu-ui/packages/vuu-layout/src/layout-persistence/LocalLayoutPersistenceManager.ts create mode 100644 vuu-ui/packages/vuu-layout/src/layout-persistence/index.ts diff --git a/vuu-ui/package-lock.json b/vuu-ui/package-lock.json index 328c20480..7bf7e1674 100644 --- a/vuu-ui/package-lock.json +++ b/vuu-ui/package-lock.json @@ -11721,6 +11721,7 @@ } }, "sample-apps/feature-basket-trading": { + "name": "feature-vuu-basket-trading", "version": "0.0.26", "license": "Apache-2.0", "dependencies": { @@ -11782,6 +11783,7 @@ } }, "sample-apps/feature-instrument-tiles": { + "name": "feature-vuu-instrument-tiles", "version": "0.0.26", "license": "Apache-2.0", "dependencies": { @@ -11812,6 +11814,7 @@ } }, "sample-apps/feature-template": { + "name": "feature-vuu-template", "version": "0.0.26", "license": "Apache-2.0", "dependencies": { diff --git a/vuu-ui/packages/vuu-layout/src/index.ts b/vuu-ui/packages/vuu-layout/src/index.ts index f67818af5..18342616a 100644 --- a/vuu-ui/packages/vuu-layout/src/index.ts +++ b/vuu-ui/packages/vuu-layout/src/index.ts @@ -6,6 +6,7 @@ export * from "./DraggableLayout"; export * from "./flexbox"; export { Action } from "./layout-action"; export * from "./layout-header"; +export * from "./layout-persistence"; export * from "./layout-provider"; export * from "./layout-reducer"; export * from "./layout-view"; diff --git a/vuu-ui/packages/vuu-layout/src/layout-persistence/LayoutPersistenceManager.ts b/vuu-ui/packages/vuu-layout/src/layout-persistence/LayoutPersistenceManager.ts new file mode 100644 index 000000000..4dc086207 --- /dev/null +++ b/vuu-ui/packages/vuu-layout/src/layout-persistence/LayoutPersistenceManager.ts @@ -0,0 +1,46 @@ +import { LayoutJSON } from "@finos/vuu-layout"; +import { LayoutMetadata } from "@finos/vuu-shell"; + +export interface LayoutPersistenceManager { + /** + * Saves a new layout and its corresponding metadata + * + * @param metadata - Metadata about the layout to be saved + * @param layout - Full JSON representation of the layout to be saved + * + * @returns Unique identifier assigned to the saved layout + */ + createLayout: (metadata: Omit, layout: LayoutJSON) => string; + + /** + * Overwrites an existing layout and its corresponding metadata with the provided infromation + * + * @param id - Unique identifier of the existing layout to be updated + * @param metadata - Metadata describing the new layout to overwrite with + * @param layout - Full JSON representation of the new layout to overwrite with + */ + updateLayout: (id: string, metadata: Omit, layout: LayoutJSON) => void; + + /** + * Deletes an existing layout and its corresponding metadata + * + * @param id - Unique identifier of the existing layout to be deleted + */ + deleteLayout: (id: string) => void; + + /** + * Retrieves an existing layout + * + * @param id - Unique identifier of the existing layout to be retrieved + * + * @returns Full JSON representation of the layout corresponding to the provided ID + */ + loadLayout: (id: string) => LayoutJSON; + + /** + * Retrieves metadata for all existing layouts + * + * @returns an array of all persisted layout metadata + */ + loadMetadata: () => LayoutMetadata[]; +} diff --git a/vuu-ui/packages/vuu-layout/src/layout-persistence/LocalLayoutPersistenceManager.ts b/vuu-ui/packages/vuu-layout/src/layout-persistence/LocalLayoutPersistenceManager.ts new file mode 100644 index 000000000..19ddbb59e --- /dev/null +++ b/vuu-ui/packages/vuu-layout/src/layout-persistence/LocalLayoutPersistenceManager.ts @@ -0,0 +1,79 @@ +import { Layout, LayoutMetadata } from "@finos/vuu-shell"; +import { LayoutJSON, LayoutPersistenceManager } from "@finos/vuu-layout"; + +import { getLocalEntity, saveLocalEntity } from "@finos/vuu-filters"; +import { getUniqueId } from "@finos/vuu-utils"; + +const metadataSaveLocation = "layouts/metadata"; +const layoutsSaveLocation = "layouts/layouts"; + +export class LocalLayoutPersistenceManager implements LayoutPersistenceManager { + createLayout(metadata: Omit, layout: LayoutJSON): string { + console.log(`Saving layout as ${metadata.name} to group ${metadata.group}...`); + + const existingLayouts = this.loadLayouts(); + const existingMetadata = this.loadMetadata(); + + const id = getUniqueId(); + + this.appendAndPersist(id, metadata, layout, existingLayouts, existingMetadata); + + return id; + } + + updateLayout(id: string, metadata: Omit, newLayoutJson: LayoutJSON): void { + const existingLayouts = this.loadLayouts().filter(layout => layout.id !== id); + const existingMetadata = this.loadMetadata().filter(metadata => metadata.id !== id); + + this.appendAndPersist(id, metadata, newLayoutJson, existingLayouts, existingMetadata); + } + + deleteLayout(id: string): void { + const layouts = this.loadLayouts().filter(layout => layout.id !== id); + const metadata = this.loadMetadata().filter(metadata => metadata.id !== id); + + this.saveLayoutsWithMetadata(layouts, metadata); + } + + loadLayout(id: string): LayoutJSON { + const layout = this.loadLayouts().filter(layout => layout.id === id); + + switch (layout.length) { + case 1: { + return layout[0].json; + } + case 0: { + console.log(`WARNING: no layout exists for ID "${id}"; returning empty layout`); + return {} as LayoutJSON; + } + default: { + console.log(`WARNING: multiple layouts exist for ID "${id}"; returning first instance`) + return layout[0].json; + } + } + } + + loadMetadata(): LayoutMetadata[] { + return getLocalEntity(metadataSaveLocation) || []; + } + + private loadLayouts(): Layout[] { + return getLocalEntity(layoutsSaveLocation) || []; + } + + private appendAndPersist(newId: string, + newMetadata: Omit, + newLayout: LayoutJSON, + existingLayouts: Layout[], + existingMetadata: LayoutMetadata[]) { + existingLayouts.push({id: newId, json: newLayout}); + existingMetadata.push({id: newId, ...newMetadata}); + + this.saveLayoutsWithMetadata(existingLayouts, existingMetadata); + } + + private saveLayoutsWithMetadata(layouts: Layout[], metadata: LayoutMetadata[]): void { + saveLocalEntity(layoutsSaveLocation, layouts); + saveLocalEntity(metadataSaveLocation, metadata); + } +} diff --git a/vuu-ui/packages/vuu-layout/src/layout-persistence/index.ts b/vuu-ui/packages/vuu-layout/src/layout-persistence/index.ts new file mode 100644 index 000000000..602a44308 --- /dev/null +++ b/vuu-ui/packages/vuu-layout/src/layout-persistence/index.ts @@ -0,0 +1,2 @@ +export * from './LayoutPersistenceManager'; +export * from './LocalLayoutPersistenceManager'; diff --git a/vuu-ui/packages/vuu-shell/src/layout-management/LayoutList.tsx b/vuu-ui/packages/vuu-shell/src/layout-management/LayoutList.tsx index 2827530d9..01877f98c 100644 --- a/vuu-ui/packages/vuu-shell/src/layout-management/LayoutList.tsx +++ b/vuu-ui/packages/vuu-shell/src/layout-management/LayoutList.tsx @@ -12,14 +12,12 @@ type LayoutGroups = { const classBase = "vuuLayoutList"; export const LayoutsList = (props: HTMLAttributes) => { - const { layouts } = useLayoutManager(); - - const layoutMetadata = layouts.map(layout => layout.metadata) + const { layoutMetadata } = useLayoutManager(); const handleLoadLayout = (layoutId?: string) => { // TODO load layout console.log("loading layout with id", layoutId) - console.log("json:", layouts.find(layout => layout.metadata.id === layoutId)) + console.log("json:", layoutMetadata.find(metadata => metadata.id === layoutId)) } const layoutsByGroup = layoutMetadata.reduce((acc: LayoutGroups, cur) => { diff --git a/vuu-ui/packages/vuu-shell/src/layout-management/layoutTypes.ts b/vuu-ui/packages/vuu-shell/src/layout-management/layoutTypes.ts index a59d42635..756146751 100644 --- a/vuu-ui/packages/vuu-shell/src/layout-management/layoutTypes.ts +++ b/vuu-ui/packages/vuu-shell/src/layout-management/layoutTypes.ts @@ -1,15 +1,15 @@ import { LayoutJSON } from "@finos/vuu-layout"; export type LayoutMetadata = { + id: string; name: string; group: string; screenshot: string; user: string; date: string; - id: string; }; export type Layout = { + id: string, json: LayoutJSON; - metadata: LayoutMetadata; }; diff --git a/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx b/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx index 3ae6fc8b3..71f03e245 100644 --- a/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx +++ b/vuu-ui/packages/vuu-shell/src/layout-management/useLayoutManager.tsx @@ -1,47 +1,46 @@ import React, { useState, useCallback, useContext, useEffect } from "react"; -import { getLocalEntity, saveLocalEntity } from "@finos/vuu-filters"; -import { LayoutJSON } from "@finos/vuu-layout"; -import { getUniqueId } from "@finos/vuu-utils"; -import { LayoutMetadata, Layout } from "./layoutTypes"; +import { getLocalEntity } from "@finos/vuu-filters"; +import { LayoutJSON, LocalLayoutPersistenceManager } from "@finos/vuu-layout"; +import { LayoutMetadata } from "./layoutTypes"; + +const persistenceManager = new LocalLayoutPersistenceManager(); export const LayoutManagementContext = React.createContext<{ - layouts: Layout[], + layoutMetadata: LayoutMetadata[], saveLayout: (n: Omit) => void -}>({ layouts: [], saveLayout: () => { } }) - -export const LayoutManagementProvider = (props: { children: JSX.Element | JSX.Element[] }) => { +}>({ layoutMetadata: [], saveLayout: () => { } }) - const [layouts, setLayouts] = useState([]) +export const LayoutManagementProvider = (props: { + children: JSX.Element | JSX.Element[] + }) => { + const [layoutMetadata, setLayoutMetadata] = useState([]); useEffect(() => { - const layouts = getLocalEntity("layouts") - setLayouts(layouts || []) + const loadedMetadata = persistenceManager.loadMetadata(); + setLayoutMetadata(loadedMetadata || []) }, []) - useEffect(() => { - saveLocalEntity("layouts", layouts) - }, [layouts]) - const saveLayout = useCallback((metadata: Omit) => { - const json = getLocalEntity("api/vui") + const json = getLocalEntity("api/vui"); + if (json) { - setLayouts(prev => - [ - ...prev, - { - metadata: { - ...metadata, - id: getUniqueId() - }, - json - } - ] - ) + // Persist layouts + const generatedId = persistenceManager.createLayout(metadata, json); + + // Update state + const newMetadata: LayoutMetadata = { + ...metadata, + id: generatedId + }; + + setLayoutMetadata(prev => [...prev, newMetadata]); } }, []) + // TODO: add loadLayout function + return ( - + {props.children} ) diff --git a/vuu-ui/showcase/src/examples/Apps/NewTheme.examples.tsx b/vuu-ui/showcase/src/examples/Apps/NewTheme.examples.tsx index 8f4c9bc6b..c4a635a64 100644 --- a/vuu-ui/showcase/src/examples/Apps/NewTheme.examples.tsx +++ b/vuu-ui/showcase/src/examples/Apps/NewTheme.examples.tsx @@ -85,9 +85,6 @@ const ShellWithNewTheme = () => { const handleSave = useCallback( (layoutMetadata: Omit) => { - console.log( - `Save layout as ${layoutMetadata.name} to group ${layoutMetadata.group}` - ); saveLayout(layoutMetadata); setDialogContent(undefined); }, @@ -229,7 +226,6 @@ const ShellWithNewTheme = () => { style={{ maxHeight: 500, borderColor: "#6d188b" }} title={"Save Layout"} hideCloseButton - headerProps={{ className: "dialogHeader" }} > {dialogContent}