Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VUU54: Add validation for layout/metadata IDs #60

Merged
merged 18 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,23 @@ export interface LayoutPersistenceManager {
*
* @returns Unique identifier assigned to the saved layout
*/
createLayout: (metadata: Omit<LayoutMetadata, "id">, layout: LayoutJSON) => string;
createLayout: (metadata: Omit<LayoutMetadata, "id">, layout: LayoutJSON) => Promise<string>;

/**
* Overwrites an existing layout and its corresponding metadata with the provided infromation
* Overwrites an existing layout and its corresponding metadata with the provided information
*
* @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<LayoutMetadata, "id">, layout: LayoutJSON) => void;
updateLayout: (id: string, metadata: Omit<LayoutMetadata, "id">, layout: LayoutJSON) => Promise<void>;

/**
* Deletes an existing layout and its corresponding metadata
*
* @param id - Unique identifier of the existing layout to be deleted
*/
deleteLayout: (id: string) => void;
deleteLayout: (id: string) => Promise<void>;

/**
* Retrieves an existing layout
Expand All @@ -35,12 +35,12 @@ export interface LayoutPersistenceManager {
*
* @returns Full JSON representation of the layout corresponding to the provided ID
*/
loadLayout: (id: string) => LayoutJSON;
loadLayout: (id: string) => Promise<LayoutJSON>;

/**
* Retrieves metadata for all existing layouts
*
* @returns an array of all persisted layout metadata
*/
loadMetadata: () => LayoutMetadata[];
loadMetadata: () => Promise<LayoutMetadata[]>;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Layout, LayoutMetadata } from "@finos/vuu-shell";
import { Layout, LayoutMetadata, WithId } from "@finos/vuu-shell";
import { LayoutJSON, LayoutPersistenceManager } from "@finos/vuu-layout";

import { getLocalEntity, saveLocalEntity } from "@finos/vuu-filters";
Expand All @@ -8,72 +8,144 @@ const metadataSaveLocation = "layouts/metadata";
const layoutsSaveLocation = "layouts/layouts";

export class LocalLayoutPersistenceManager implements LayoutPersistenceManager {
createLayout(metadata: Omit<LayoutMetadata, "id">, layout: LayoutJSON): string {
console.log(`Saving layout as ${metadata.name} to group ${metadata.group}...`);

const existingLayouts = this.loadLayouts();
const existingMetadata = this.loadMetadata();
createLayout(metadata: Omit<LayoutMetadata, "id">, layout: LayoutJSON): Promise<string> {
return new Promise(resolve => {
console.log(`Saving layout as ${metadata.name} to group ${metadata.group}...`);

Promise.all([this.loadLayouts(), this.loadMetadata()])
.then(([existingLayouts, existingMetadata]) => {
const id = getUniqueId();
this.appendAndPersist(
id,
metadata,
layout,
existingLayouts,
existingMetadata
);
resolve(id);
});
})
}

const id = getUniqueId();
updateLayout(id: string, newMetadata: Omit<LayoutMetadata, "id">, newLayout: LayoutJSON): Promise<void> {
return new Promise((resolve, reject) => {
this.loadAndFilter(id)
.then(([layouts, metadata]) => {
this.appendAndPersist(id, newMetadata, newLayout, layouts, metadata);
resolve();
})
.catch(e => reject(e));
});
pling-scottlogic marked this conversation as resolved.
Show resolved Hide resolved
}

this.appendAndPersist(id, metadata, layout, existingLayouts, existingMetadata);
deleteLayout(id: string): Promise<void> {
return new Promise((resolve, reject) => {
this.loadAndFilter(id)
.then(([layouts, metadata]) => {
this.saveLayoutsWithMetadata(layouts, metadata);
resolve();
})
.catch(e => reject(e));
});
}

return id;
loadLayout(id: string): Promise<LayoutJSON> {
return new Promise((resolve, reject) => {
this.validateId(id, "layout")
.then(() => this.loadLayouts())
.then(existingLayouts => {
const layouts = existingLayouts.find(layout => layout.id === id) as Layout;
resolve(layouts.json);
})
.catch(e => reject(e));
});
}

updateLayout(id: string, metadata: Omit<LayoutMetadata, "id">, newLayoutJson: LayoutJSON): void {
const existingLayouts = this.loadLayouts().filter(layout => layout.id !== id);
const existingMetadata = this.loadMetadata().filter(metadata => metadata.id !== id);
loadMetadata(): Promise<LayoutMetadata[]> {
return new Promise(resolve => {
const metadata = getLocalEntity<LayoutMetadata[]>(metadataSaveLocation);
resolve(metadata || []);
})
}

this.appendAndPersist(id, metadata, newLayoutJson, existingLayouts, existingMetadata);
private loadLayouts(): Promise<Layout[]> {
return new Promise(resolve => {
const layouts = getLocalEntity<Layout[]>(layoutsSaveLocation);
resolve(layouts || []);
});
}

deleteLayout(id: string): void {
const layouts = this.loadLayouts().filter(layout => layout.id !== id);
const metadata = this.loadMetadata().filter(metadata => metadata.id !== id);
private async loadAndFilter(idToRemove: string): Promise<[Layout[], LayoutMetadata[]]> {
await this.validateIds(idToRemove);

this.saveLayoutsWithMetadata(layouts, metadata);
}
const [existingLayouts, existingMetadata] =
await Promise.all([this.loadLayouts(), this.loadMetadata()]);

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;
}
}
}
const layouts = this.removeId<Layout>(existingLayouts, idToRemove);
const metadata = this.removeId<LayoutMetadata>(existingMetadata, idToRemove);

loadMetadata(): LayoutMetadata[] {
return getLocalEntity<LayoutMetadata[]>(metadataSaveLocation) || [];
return [layouts, metadata];
}

private loadLayouts(): Layout[] {
return getLocalEntity<Layout[]>(layoutsSaveLocation) || [];
private removeId<T extends WithId>(array: T[], idToRemove: string): T[] {
return array.filter(element => element.id !== idToRemove);
}

private appendAndPersist(newId: string,
newMetadata: Omit<LayoutMetadata, "id">,
newLayout: LayoutJSON,
existingLayouts: Layout[],
existingMetadata: LayoutMetadata[]) {
existingLayouts.push({id: newId, json: newLayout});
existingMetadata.push({id: newId, ...newMetadata});
private appendAndPersist(
newId: string,
newMetadata: Omit<LayoutMetadata, "id">,
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 {
private saveLayoutsWithMetadata(
layouts: Layout[],
metadata: LayoutMetadata[]
): void {
saveLocalEntity<Layout[]>(layoutsSaveLocation, layouts);
saveLocalEntity<LayoutMetadata[]>(metadataSaveLocation, metadata);
}

private async validateIds(id: string): Promise<void> {
return Promise
vferraro-scottlogic marked this conversation as resolved.
Show resolved Hide resolved
.all([
this.validateId(id, "metadata").catch(error => error.message),
vferraro-scottlogic marked this conversation as resolved.
Show resolved Hide resolved
this.validateId(id, "layout").catch(error => error.message)
])
.then((errorMessages: string[]) => {
// filter(Boolean) is used to remove any blank messages before joining.
// Avoids orphaned delimiters in combined messages, e.g. "; " or "; error 2"
const combinedMessage = errorMessages.filter(Boolean).join("; ");
if (combinedMessage) {
vferraro-scottlogic marked this conversation as resolved.
Show resolved Hide resolved
throw new Error(combinedMessage);
}
});
}

private validateId(id: string, dataType: "metadata" | "layout"): Promise<void> {
return new Promise((resolve, reject) => {
const loadFunc = dataType === "metadata" ? this.loadMetadata : this.loadLayouts;

loadFunc().then((array: WithId[]) => {
const count = array.filter(element => element.id === id).length;
switch (count) {
case 1: {
resolve();
break;
}
case 0: {
reject(new Error(`No ${dataType} with ID ${id}`));
break;
}
default: reject(new Error(`Non-unique ${dataType} with ID ${id}`));
}
});
})
}
}
14 changes: 14 additions & 0 deletions vuu-ui/packages/vuu-layout/src/layout-persistence/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const warningLayout = {
type: "View",
props: {
style: { height: "calc(100% - 6px)" },
},
children: [
{
props: {
className: "vuuShell-warningPlaceholder",
},
type: "Placeholder",
},
],
};
Loading
Loading