mirror of
https://github.com/stan-smith/FossFLOW.git
synced 2025-12-24 06:58:48 -05:00
feat: implements lastUpdated field on views
This commit is contained in:
@@ -31,7 +31,9 @@ export const DEFAULT_COLOR: Colors[0] = {
|
||||
|
||||
export const DEFAULT_FONT_FAMILY = 'Roboto, Arial, sans-serif';
|
||||
|
||||
export const VIEW_DEFAULTS: Required<Omit<View, 'id' | 'description'>> = {
|
||||
export const VIEW_DEFAULTS: Required<
|
||||
Omit<View, 'id' | 'description' | 'lastUpdated'>
|
||||
> = {
|
||||
name: 'Untitled view',
|
||||
items: [],
|
||||
connectors: [],
|
||||
@@ -88,6 +90,10 @@ export const INITIAL_UI_STATE = {
|
||||
offset: CoordsUtils.zero()
|
||||
}
|
||||
};
|
||||
export const INITIAL_SCENE_STATE = {
|
||||
connectors: {},
|
||||
textBoxes: {}
|
||||
};
|
||||
export const MAIN_MENU_OPTIONS: MainMenuOptions = [
|
||||
'ACTION.OPEN',
|
||||
'EXPORT.JSON',
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useCallback, useState, useRef } from 'react';
|
||||
import { InitialData, IconCollectionState } from 'src/types';
|
||||
import { INITIAL_DATA, VIEW_DEFAULTS } from 'src/config';
|
||||
import { INITIAL_DATA, INITIAL_SCENE_STATE } from 'src/config';
|
||||
import {
|
||||
generateId,
|
||||
getFitToViewParams,
|
||||
CoordsUtils,
|
||||
categoriseIcons
|
||||
categoriseIcons,
|
||||
generateId
|
||||
} from 'src/utils';
|
||||
import { createView } from 'src/stores/reducers';
|
||||
import * as reducers from 'src/stores/reducers';
|
||||
import { useModelStore } from 'src/stores/modelStore';
|
||||
import { useView } from 'src/hooks/useView';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
@@ -43,13 +43,14 @@ export const useInitialDataManager = () => {
|
||||
const initialData = _initialData;
|
||||
|
||||
if (initialData.views.length === 0) {
|
||||
const updates = createView(
|
||||
{
|
||||
...VIEW_DEFAULTS,
|
||||
id: generateId()
|
||||
},
|
||||
initialData
|
||||
);
|
||||
const updates = reducers.view({
|
||||
action: 'CREATE_VIEW',
|
||||
payload: {},
|
||||
ctx: {
|
||||
state: { model: initialData, scene: INITIAL_SCENE_STATE },
|
||||
viewId: generateId()
|
||||
}
|
||||
});
|
||||
|
||||
Object.assign(initialData, updates);
|
||||
}
|
||||
|
||||
@@ -119,11 +119,11 @@ export const useScene = () => {
|
||||
|
||||
const createViewItem = useCallback(
|
||||
(newViewItem: ViewItem) => {
|
||||
const newState = reducers.createViewItem(
|
||||
newViewItem,
|
||||
currentViewId,
|
||||
getState()
|
||||
);
|
||||
const newState = reducers.view({
|
||||
action: 'CREATE_VIEWITEM',
|
||||
payload: newViewItem,
|
||||
ctx: { viewId: currentViewId, state: getState() }
|
||||
});
|
||||
setState(newState);
|
||||
},
|
||||
[getState, setState, currentViewId]
|
||||
@@ -131,12 +131,11 @@ export const useScene = () => {
|
||||
|
||||
const updateViewItem = useCallback(
|
||||
(id: string, updates: Partial<ViewItem>) => {
|
||||
const newState = reducers.updateViewItem(
|
||||
id,
|
||||
updates,
|
||||
currentViewId,
|
||||
getState()
|
||||
);
|
||||
const newState = reducers.view({
|
||||
action: 'UPDATE_VIEWITEM',
|
||||
payload: { id, ...updates },
|
||||
ctx: { viewId: currentViewId, state: getState() }
|
||||
});
|
||||
setState(newState);
|
||||
},
|
||||
[getState, setState, currentViewId]
|
||||
@@ -144,7 +143,11 @@ export const useScene = () => {
|
||||
|
||||
const deleteViewItem = useCallback(
|
||||
(id: string) => {
|
||||
const newState = reducers.deleteViewItem(id, currentViewId, getState());
|
||||
const newState = reducers.view({
|
||||
action: 'DELETE_VIEWITEM',
|
||||
payload: id,
|
||||
ctx: { viewId: currentViewId, state: getState() }
|
||||
});
|
||||
setState(newState);
|
||||
},
|
||||
[getState, setState, currentViewId]
|
||||
@@ -152,11 +155,11 @@ export const useScene = () => {
|
||||
|
||||
const createConnector = useCallback(
|
||||
(newConnector: Connector) => {
|
||||
const newState = reducers.createConnector(
|
||||
newConnector,
|
||||
currentViewId,
|
||||
getState()
|
||||
);
|
||||
const newState = reducers.view({
|
||||
action: 'CREATE_CONNECTOR',
|
||||
payload: newConnector,
|
||||
ctx: { viewId: currentViewId, state: getState() }
|
||||
});
|
||||
setState(newState);
|
||||
},
|
||||
[getState, setState, currentViewId]
|
||||
@@ -164,12 +167,11 @@ export const useScene = () => {
|
||||
|
||||
const updateConnector = useCallback(
|
||||
(id: string, updates: Partial<Connector>) => {
|
||||
const newState = reducers.updateConnector(
|
||||
id,
|
||||
updates,
|
||||
currentViewId,
|
||||
getState()
|
||||
);
|
||||
const newState = reducers.view({
|
||||
action: 'UPDATE_CONNECTOR',
|
||||
payload: { id, ...updates },
|
||||
ctx: { viewId: currentViewId, state: getState() }
|
||||
});
|
||||
setState(newState);
|
||||
},
|
||||
[getState, setState, currentViewId]
|
||||
@@ -177,7 +179,11 @@ export const useScene = () => {
|
||||
|
||||
const deleteConnector = useCallback(
|
||||
(id: string) => {
|
||||
const newState = reducers.deleteConnector(id, currentViewId, getState());
|
||||
const newState = reducers.view({
|
||||
action: 'DELETE_CONNECTOR',
|
||||
payload: id,
|
||||
ctx: { viewId: currentViewId, state: getState() }
|
||||
});
|
||||
setState(newState);
|
||||
},
|
||||
[getState, setState, currentViewId]
|
||||
@@ -185,11 +191,11 @@ export const useScene = () => {
|
||||
|
||||
const createTextBox = useCallback(
|
||||
(newTextBox: TextBox) => {
|
||||
const newState = reducers.createTextBox(
|
||||
newTextBox,
|
||||
currentViewId,
|
||||
getState()
|
||||
);
|
||||
const newState = reducers.view({
|
||||
action: 'CREATE_TEXTBOX',
|
||||
payload: newTextBox,
|
||||
ctx: { viewId: currentViewId, state: getState() }
|
||||
});
|
||||
setState(newState);
|
||||
},
|
||||
[getState, setState, currentViewId]
|
||||
@@ -197,12 +203,11 @@ export const useScene = () => {
|
||||
|
||||
const updateTextBox = useCallback(
|
||||
(id: string, updates: Partial<TextBox>) => {
|
||||
const newState = reducers.updateTextBox(
|
||||
id,
|
||||
updates,
|
||||
currentViewId,
|
||||
getState()
|
||||
);
|
||||
const newState = reducers.view({
|
||||
action: 'UPDATE_TEXTBOX',
|
||||
payload: { id, ...updates },
|
||||
ctx: { viewId: currentViewId, state: getState() }
|
||||
});
|
||||
setState(newState);
|
||||
},
|
||||
[getState, setState, currentViewId]
|
||||
@@ -210,7 +215,11 @@ export const useScene = () => {
|
||||
|
||||
const deleteTextBox = useCallback(
|
||||
(id: string) => {
|
||||
const newState = reducers.deleteTextBox(id, currentViewId, getState());
|
||||
const newState = reducers.view({
|
||||
action: 'DELETE_TEXTBOX',
|
||||
payload: id,
|
||||
ctx: { viewId: currentViewId, state: getState() }
|
||||
});
|
||||
setState(newState);
|
||||
},
|
||||
[getState, setState, currentViewId]
|
||||
@@ -218,12 +227,11 @@ export const useScene = () => {
|
||||
|
||||
const createRectangle = useCallback(
|
||||
(newRectangle: Rectangle) => {
|
||||
const newState = reducers.createRectangle(
|
||||
newRectangle,
|
||||
currentViewId,
|
||||
getState()
|
||||
);
|
||||
|
||||
const newState = reducers.view({
|
||||
action: 'CREATE_RECTANGLE',
|
||||
payload: newRectangle,
|
||||
ctx: { viewId: currentViewId, state: getState() }
|
||||
});
|
||||
setState(newState);
|
||||
},
|
||||
[getState, setState, currentViewId]
|
||||
@@ -231,12 +239,11 @@ export const useScene = () => {
|
||||
|
||||
const updateRectangle = useCallback(
|
||||
(id: string, updates: Partial<Rectangle>) => {
|
||||
const newState = reducers.updateRectangle(
|
||||
id,
|
||||
updates,
|
||||
currentViewId,
|
||||
getState()
|
||||
);
|
||||
const newState = reducers.view({
|
||||
action: 'UPDATE_RECTANGLE',
|
||||
payload: { id, ...updates },
|
||||
ctx: { viewId: currentViewId, state: getState() }
|
||||
});
|
||||
setState(newState);
|
||||
},
|
||||
[getState, setState, currentViewId]
|
||||
@@ -244,7 +251,11 @@ export const useScene = () => {
|
||||
|
||||
const deleteRectangle = useCallback(
|
||||
(id: string) => {
|
||||
const newState = reducers.deleteRectangle(id, currentViewId, getState());
|
||||
const newState = reducers.view({
|
||||
action: 'DELETE_RECTANGLE',
|
||||
payload: id,
|
||||
ctx: { viewId: currentViewId, state: getState() }
|
||||
});
|
||||
setState(newState);
|
||||
},
|
||||
[getState, setState, currentViewId]
|
||||
@@ -252,12 +263,11 @@ export const useScene = () => {
|
||||
|
||||
const changeLayerOrder = useCallback(
|
||||
(action: LayerOrderingAction, item: ItemReference) => {
|
||||
const newState = reducers.changeLayerOrder(
|
||||
action,
|
||||
item,
|
||||
currentViewId,
|
||||
getState()
|
||||
);
|
||||
const newState = reducers.view({
|
||||
action: 'CHANGE_LAYER_ORDER',
|
||||
payload: { action, item },
|
||||
ctx: { viewId: currentViewId, state: getState() }
|
||||
});
|
||||
setState(newState);
|
||||
},
|
||||
[getState, setState, currentViewId]
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { useSceneStore } from 'src/stores/sceneStore';
|
||||
import { syncScene } from 'src/stores/reducers';
|
||||
import * as reducers from 'src/stores/reducers';
|
||||
import { Model } from 'src/types';
|
||||
import { INITIAL_SCENE_STATE } from 'src/config';
|
||||
|
||||
export const useView = () => {
|
||||
const uiStateActions = useUiStateStore((state) => {
|
||||
@@ -15,7 +16,12 @@ export const useView = () => {
|
||||
|
||||
const changeView = useCallback(
|
||||
(viewId: string, model: Model) => {
|
||||
const newState = syncScene(viewId, model);
|
||||
const newState = reducers.view({
|
||||
action: 'SYNC_SCENE',
|
||||
payload: undefined,
|
||||
ctx: { viewId, state: { model, scene: INITIAL_SCENE_STATE } }
|
||||
});
|
||||
|
||||
sceneActions.set(newState.scene);
|
||||
uiStateActions.setView(viewId);
|
||||
},
|
||||
|
||||
@@ -12,6 +12,7 @@ export const viewItemSchema = z.object({
|
||||
|
||||
export const viewSchema = z.object({
|
||||
id,
|
||||
lastUpdated: z.string().datetime().optional(),
|
||||
name: constrainedStrings.name,
|
||||
description: constrainedStrings.description.optional(),
|
||||
items: z.array(viewItemSchema),
|
||||
|
||||
@@ -1,21 +1,8 @@
|
||||
// This file will be exported as it's own bundle (separate to the main bundle). This is because the main
|
||||
// bundle requires `window` to be present and so can't be imported into a Node environment.
|
||||
export const version = PACKAGE_VERSION;
|
||||
export * as reducers from 'src/stores/reducers';
|
||||
export { INITIAL_DATA } from 'src/config';
|
||||
export { modelSchema } from 'src/schemas/model';
|
||||
export const version = PACKAGE_VERSION;
|
||||
export type {
|
||||
IsoflowProps,
|
||||
InitialData,
|
||||
Model,
|
||||
View,
|
||||
Views,
|
||||
ModelItems,
|
||||
ViewItem,
|
||||
Icon,
|
||||
ModelItem,
|
||||
Rectangle,
|
||||
Colors,
|
||||
Icons,
|
||||
Connector
|
||||
} from 'src/types';
|
||||
export type { IsoflowProps, InitialData } from 'src/types';
|
||||
export type * from 'src/types/model';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { produce } from 'immer';
|
||||
import { model as modelFixture } from 'src/fixtures/model';
|
||||
import { ItemReference } from 'src/types';
|
||||
import { changeLayerOrder } from '../layerOrdering';
|
||||
import * as reducers from 'src/stores/reducers';
|
||||
|
||||
const getModel = () => {
|
||||
return produce(modelFixture, (draft) => {
|
||||
@@ -37,9 +37,17 @@ describe('Layer ordering reducers works correctly', () => {
|
||||
type: 'RECTANGLE',
|
||||
id: 'rect3'
|
||||
};
|
||||
const result = changeLayerOrder('BRING_FORWARD', item, 'view1', {
|
||||
model,
|
||||
scene
|
||||
|
||||
const result = reducers.view({
|
||||
action: 'CHANGE_LAYER_ORDER',
|
||||
payload: {
|
||||
action: 'BRING_FORWARD',
|
||||
item
|
||||
},
|
||||
ctx: {
|
||||
viewId: 'view1',
|
||||
state: { model, scene }
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.model.views[0].rectangles?.[1].id).toBe('rect3');
|
||||
@@ -51,9 +59,17 @@ describe('Layer ordering reducers works correctly', () => {
|
||||
type: 'RECTANGLE',
|
||||
id: 'rect3'
|
||||
};
|
||||
const result = changeLayerOrder('BRING_TO_FRONT', item, 'view1', {
|
||||
model,
|
||||
scene
|
||||
|
||||
const result = reducers.view({
|
||||
action: 'CHANGE_LAYER_ORDER',
|
||||
payload: {
|
||||
action: 'BRING_TO_FRONT',
|
||||
item
|
||||
},
|
||||
ctx: {
|
||||
viewId: 'view1',
|
||||
state: { model, scene }
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.model.views[0].rectangles?.[0].id).toBe('rect3');
|
||||
@@ -65,9 +81,17 @@ describe('Layer ordering reducers works correctly', () => {
|
||||
type: 'RECTANGLE',
|
||||
id: 'rect1'
|
||||
};
|
||||
const result = changeLayerOrder('SEND_BACKWARD', item, 'view1', {
|
||||
model,
|
||||
scene
|
||||
|
||||
const result = reducers.view({
|
||||
action: 'CHANGE_LAYER_ORDER',
|
||||
payload: {
|
||||
action: 'SEND_BACKWARD',
|
||||
item
|
||||
},
|
||||
ctx: {
|
||||
viewId: 'view1',
|
||||
state: { model, scene }
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.model.views[0].rectangles?.[1].id).toBe('rect1');
|
||||
@@ -79,9 +103,17 @@ describe('Layer ordering reducers works correctly', () => {
|
||||
type: 'RECTANGLE',
|
||||
id: 'rect1'
|
||||
};
|
||||
const result = changeLayerOrder('SEND_TO_BACK', item, 'view1', {
|
||||
model,
|
||||
scene
|
||||
|
||||
const result = reducers.view({
|
||||
action: 'CHANGE_LAYER_ORDER',
|
||||
payload: {
|
||||
action: 'SEND_TO_BACK',
|
||||
item
|
||||
},
|
||||
ctx: {
|
||||
viewId: 'view1',
|
||||
state: { model, scene }
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.model.views[0].rectangles?.[2].id).toBe('rect1');
|
||||
|
||||
@@ -2,12 +2,11 @@ import { Connector } from 'src/types';
|
||||
import { produce } from 'immer';
|
||||
import { getItemByIdOrThrow, getConnectorPath, getAllAnchors } from 'src/utils';
|
||||
import { validateConnector } from 'src/schemas/validation';
|
||||
import { State } from './types';
|
||||
import { State, ViewReducerContext } from './types';
|
||||
|
||||
export const deleteConnector = (
|
||||
id: string,
|
||||
viewId: string,
|
||||
state: State
|
||||
{ viewId, state }: ViewReducerContext
|
||||
): State => {
|
||||
const view = getItemByIdOrThrow(state.model.views, viewId);
|
||||
const connector = getItemByIdOrThrow(view.value.connectors ?? [], id);
|
||||
@@ -20,7 +19,10 @@ export const deleteConnector = (
|
||||
return newState;
|
||||
};
|
||||
|
||||
export const syncConnector = (id: string, viewId: string, state: State) => {
|
||||
export const syncConnector = (
|
||||
id: string,
|
||||
{ viewId, state }: ViewReducerContext
|
||||
) => {
|
||||
const newState = produce(state, (draft) => {
|
||||
const view = getItemByIdOrThrow(draft.model.views, viewId);
|
||||
const connector = getItemByIdOrThrow(view.value.connectors ?? [], id);
|
||||
@@ -32,7 +34,8 @@ export const syncConnector = (id: string, viewId: string, state: State) => {
|
||||
});
|
||||
|
||||
if (issues.length > 0) {
|
||||
const stateAfterDelete = deleteConnector(id, viewId, draft);
|
||||
const stateAfterDelete = deleteConnector(id, { viewId, state: draft });
|
||||
|
||||
draft.scene = stateAfterDelete.scene;
|
||||
draft.model = stateAfterDelete.model;
|
||||
} else {
|
||||
@@ -49,10 +52,8 @@ export const syncConnector = (id: string, viewId: string, state: State) => {
|
||||
};
|
||||
|
||||
export const updateConnector = (
|
||||
id: string,
|
||||
updates: Partial<Connector>,
|
||||
viewId: string,
|
||||
state: State
|
||||
{ id, ...updates }: { id: string } & Partial<Connector>,
|
||||
{ state, viewId }: ViewReducerContext
|
||||
): State => {
|
||||
const newState = produce(state, (draft) => {
|
||||
const view = getItemByIdOrThrow(draft.model.views, viewId);
|
||||
@@ -65,7 +66,11 @@ export const updateConnector = (
|
||||
connectors[connector.index] = newConnector;
|
||||
|
||||
if (updates.anchors) {
|
||||
const stateAfterSync = syncConnector(id, viewId, draft);
|
||||
const stateAfterSync = syncConnector(newConnector.id, {
|
||||
viewId,
|
||||
state: draft
|
||||
});
|
||||
|
||||
draft.model = stateAfterSync.model;
|
||||
draft.scene = stateAfterSync.scene;
|
||||
}
|
||||
@@ -76,8 +81,7 @@ export const updateConnector = (
|
||||
|
||||
export const createConnector = (
|
||||
newConnector: Connector,
|
||||
viewId: string,
|
||||
state: State
|
||||
{ state, viewId }: ViewReducerContext
|
||||
): State => {
|
||||
const newState = produce(state, (draft) => {
|
||||
const view = getItemByIdOrThrow(draft.model.views, viewId);
|
||||
@@ -89,7 +93,11 @@ export const createConnector = (
|
||||
draft.model.views[view.index].connectors?.unshift(newConnector);
|
||||
}
|
||||
|
||||
const stateAfterSync = syncConnector(newConnector.id, viewId, draft);
|
||||
const stateAfterSync = syncConnector(newConnector.id, {
|
||||
viewId,
|
||||
state: draft
|
||||
});
|
||||
|
||||
draft.model = stateAfterSync.model;
|
||||
draft.scene = stateAfterSync.scene;
|
||||
});
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
export * from './connector';
|
||||
export { view } from './view';
|
||||
export * from './modelItem';
|
||||
export * from './viewItem';
|
||||
export * from './rectangle';
|
||||
export * from './textBox';
|
||||
export * from './view';
|
||||
export * from './layerOrdering';
|
||||
export { syncConnector } from './connector';
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { produce } from 'immer';
|
||||
import { ItemReference, LayerOrderingAction, View } from 'src/types';
|
||||
import { getItemByIdOrThrow } from 'src/utils';
|
||||
import { State } from './types';
|
||||
import { State, ViewReducerContext } from './types';
|
||||
|
||||
export const changeLayerOrder = (
|
||||
action: LayerOrderingAction,
|
||||
item: ItemReference,
|
||||
viewId: string,
|
||||
state: State
|
||||
{ action, item }: { action: LayerOrderingAction; item: ItemReference },
|
||||
{ viewId, state }: ViewReducerContext
|
||||
): State => {
|
||||
const newState = produce(state, (draft) => {
|
||||
const view = getItemByIdOrThrow(draft.model.views, viewId);
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { produce } from 'immer';
|
||||
import { Rectangle } from 'src/types';
|
||||
import { getItemByIdOrThrow } from 'src/utils';
|
||||
import { State } from './types';
|
||||
import { State, ViewReducerContext } from './types';
|
||||
|
||||
export const updateRectangle = (
|
||||
id: string,
|
||||
updates: Partial<Rectangle>,
|
||||
viewId: string,
|
||||
state: State
|
||||
{ id, ...updates }: { id: string } & Partial<Rectangle>,
|
||||
{ viewId, state }: ViewReducerContext
|
||||
): State => {
|
||||
const view = getItemByIdOrThrow(state.model.views, viewId);
|
||||
|
||||
@@ -26,8 +24,7 @@ export const updateRectangle = (
|
||||
|
||||
export const createRectangle = (
|
||||
newRectangle: Rectangle,
|
||||
viewId: string,
|
||||
state: State
|
||||
{ viewId, state }: ViewReducerContext
|
||||
): State => {
|
||||
const view = getItemByIdOrThrow(state.model.views, viewId);
|
||||
|
||||
@@ -41,13 +38,15 @@ export const createRectangle = (
|
||||
}
|
||||
});
|
||||
|
||||
return updateRectangle(newRectangle.id, newRectangle, viewId, newState);
|
||||
return updateRectangle(newRectangle, {
|
||||
viewId,
|
||||
state: newState
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteRectangle = (
|
||||
id: string,
|
||||
viewId: string,
|
||||
state: State
|
||||
{ viewId, state }: ViewReducerContext
|
||||
): State => {
|
||||
const view = getItemByIdOrThrow(state.model.views, viewId);
|
||||
const rectangle = getItemByIdOrThrow(view.value.rectangles ?? [], id);
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { produce } from 'immer';
|
||||
import { TextBox } from 'src/types';
|
||||
import { getItemByIdOrThrow, getTextBoxDimensions } from 'src/utils';
|
||||
|
||||
import { State } from './types';
|
||||
import { State, ViewReducerContext } from './types';
|
||||
|
||||
export const syncTextBox = (
|
||||
id: string,
|
||||
viewId: string,
|
||||
state: State
|
||||
{ viewId, state }: ViewReducerContext
|
||||
): State => {
|
||||
const newState = produce(state, (draft) => {
|
||||
const view = getItemByIdOrThrow(draft.model.views, viewId);
|
||||
@@ -22,10 +20,8 @@ export const syncTextBox = (
|
||||
};
|
||||
|
||||
export const updateTextBox = (
|
||||
id: string,
|
||||
updates: Partial<TextBox>,
|
||||
viewId: string,
|
||||
state: State
|
||||
{ id, ...updates }: { id: string } & Partial<TextBox>,
|
||||
{ viewId, state }: ViewReducerContext
|
||||
): State => {
|
||||
const view = getItemByIdOrThrow(state.model.views, viewId);
|
||||
|
||||
@@ -39,7 +35,11 @@ export const updateTextBox = (
|
||||
textBoxes[textBox.index] = newTextBox;
|
||||
|
||||
if (updates.content !== undefined || updates.fontSize !== undefined) {
|
||||
const stateAfterSync = syncTextBox(newTextBox.id, viewId, draft);
|
||||
const stateAfterSync = syncTextBox(newTextBox.id, {
|
||||
viewId,
|
||||
state: draft
|
||||
});
|
||||
|
||||
draft.model = stateAfterSync.model;
|
||||
draft.scene = stateAfterSync.scene;
|
||||
}
|
||||
@@ -50,8 +50,7 @@ export const updateTextBox = (
|
||||
|
||||
export const createTextBox = (
|
||||
newTextBox: TextBox,
|
||||
viewId: string,
|
||||
state: State
|
||||
{ viewId, state }: ViewReducerContext
|
||||
): State => {
|
||||
const view = getItemByIdOrThrow(state.model.views, viewId);
|
||||
|
||||
@@ -65,13 +64,12 @@ export const createTextBox = (
|
||||
}
|
||||
});
|
||||
|
||||
return updateTextBox(newTextBox.id, newTextBox, viewId, newState);
|
||||
return updateTextBox(newTextBox, { viewId, state: newState });
|
||||
};
|
||||
|
||||
export const deleteTextBox = (
|
||||
id: string,
|
||||
viewId: string,
|
||||
state: State
|
||||
{ viewId, state }: ViewReducerContext
|
||||
): State => {
|
||||
const view = getItemByIdOrThrow(state.model.views, viewId);
|
||||
const textBox = getItemByIdOrThrow(view.value.textBoxes ?? [], id);
|
||||
|
||||
@@ -1,6 +1,89 @@
|
||||
import { Model, Scene } from 'src/types';
|
||||
import type * as viewReducers from './view';
|
||||
import type * as viewItemReducers from './viewItem';
|
||||
import type * as connectorReducers from './connector';
|
||||
import type * as textBoxReducers from './textBox';
|
||||
import type * as rectangleReducers from './rectangle';
|
||||
import type * as layerOrderingReducers from './layerOrdering';
|
||||
|
||||
export interface State {
|
||||
model: Model;
|
||||
scene: Scene;
|
||||
}
|
||||
|
||||
export interface ViewReducerContext {
|
||||
viewId: string;
|
||||
state: State;
|
||||
}
|
||||
|
||||
type ViewReducerAction =
|
||||
| {
|
||||
action: 'SYNC_SCENE';
|
||||
payload: undefined;
|
||||
}
|
||||
| {
|
||||
action: 'CREATE_VIEW';
|
||||
payload: Parameters<typeof viewReducers.createView>[0];
|
||||
}
|
||||
| {
|
||||
action: 'UPDATE_VIEW';
|
||||
payload: Parameters<typeof viewReducers.updateView>[0];
|
||||
}
|
||||
| {
|
||||
action: 'CREATE_VIEWITEM';
|
||||
payload: Parameters<typeof viewItemReducers.createViewItem>[0];
|
||||
}
|
||||
| {
|
||||
action: 'UPDATE_VIEWITEM';
|
||||
payload: Parameters<typeof viewItemReducers.updateViewItem>[0];
|
||||
}
|
||||
| {
|
||||
action: 'DELETE_VIEWITEM';
|
||||
payload: Parameters<typeof viewItemReducers.deleteViewItem>[0];
|
||||
}
|
||||
| {
|
||||
action: 'CREATE_CONNECTOR';
|
||||
payload: Parameters<typeof connectorReducers.createConnector>[0];
|
||||
}
|
||||
| {
|
||||
action: 'UPDATE_CONNECTOR';
|
||||
payload: Parameters<typeof connectorReducers.updateConnector>[0];
|
||||
}
|
||||
| {
|
||||
action: 'DELETE_CONNECTOR';
|
||||
payload: Parameters<typeof connectorReducers.deleteConnector>[0];
|
||||
}
|
||||
| {
|
||||
action: 'SYNC_CONNECTOR';
|
||||
payload: Parameters<typeof connectorReducers.syncConnector>[0];
|
||||
}
|
||||
| {
|
||||
action: 'CREATE_TEXTBOX';
|
||||
payload: Parameters<typeof textBoxReducers.createTextBox>[0];
|
||||
}
|
||||
| {
|
||||
action: 'UPDATE_TEXTBOX';
|
||||
payload: Parameters<typeof textBoxReducers.updateTextBox>[0];
|
||||
}
|
||||
| {
|
||||
action: 'DELETE_TEXTBOX';
|
||||
payload: Parameters<typeof textBoxReducers.deleteTextBox>[0];
|
||||
}
|
||||
| {
|
||||
action: 'CREATE_RECTANGLE';
|
||||
payload: Parameters<typeof rectangleReducers.createRectangle>[0];
|
||||
}
|
||||
| {
|
||||
action: 'UPDATE_RECTANGLE';
|
||||
payload: Parameters<typeof rectangleReducers.updateRectangle>[0];
|
||||
}
|
||||
| {
|
||||
action: 'DELETE_RECTANGLE';
|
||||
payload: Parameters<typeof rectangleReducers.deleteRectangle>[0];
|
||||
}
|
||||
| {
|
||||
action: 'CHANGE_LAYER_ORDER';
|
||||
payload: Parameters<typeof layerOrderingReducers.changeLayerOrder>[0];
|
||||
};
|
||||
|
||||
export type ViewReducerParams = ViewReducerAction & { ctx: ViewReducerContext };
|
||||
|
||||
@@ -1,41 +1,141 @@
|
||||
import { produce } from 'immer';
|
||||
import { View } from 'src/types';
|
||||
import { getItemByIdOrThrow } from 'src/utils';
|
||||
import { State } from './types';
|
||||
import { VIEW_DEFAULTS, INITIAL_SCENE_STATE } from 'src/config';
|
||||
import type { ViewReducerContext, State, ViewReducerParams } from './types';
|
||||
import { syncConnector } from './connector';
|
||||
import { syncTextBox } from './textBox';
|
||||
import * as viewItemReducers from './viewItem';
|
||||
import * as connectorReducers from './connector';
|
||||
import * as textBoxReducers from './textBox';
|
||||
import * as rectangleReducers from './rectangle';
|
||||
import * as layerOrderingReducers from './layerOrdering';
|
||||
|
||||
export const syncScene = (viewId: string, model: State['model']) => {
|
||||
const view = getItemByIdOrThrow(model.views, viewId);
|
||||
export const updateViewTimestamp = (ctx: ViewReducerContext): State => {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const newState = produce(ctx.state, (draft) => {
|
||||
const view = getItemByIdOrThrow(draft.model.views, ctx.viewId);
|
||||
|
||||
view.value.lastUpdated = now;
|
||||
});
|
||||
|
||||
return newState;
|
||||
};
|
||||
|
||||
export const syncScene = ({ viewId, state }: ViewReducerContext): State => {
|
||||
const view = getItemByIdOrThrow(state.model.views, viewId);
|
||||
|
||||
const startingState: State = {
|
||||
model,
|
||||
scene: {
|
||||
connectors: {},
|
||||
textBoxes: {}
|
||||
}
|
||||
model: state.model,
|
||||
scene: INITIAL_SCENE_STATE
|
||||
};
|
||||
|
||||
const stateAfterConnectorsSynced = [
|
||||
...(view.value.connectors ?? [])
|
||||
].reduce<State>((acc, connector) => {
|
||||
return syncConnector(connector.id, viewId, acc);
|
||||
return syncConnector(connector.id, { viewId, state: acc });
|
||||
}, startingState);
|
||||
|
||||
const stateAfterTextBoxesSynced = [
|
||||
...(view.value.textBoxes ?? [])
|
||||
].reduce<State>((acc, textBox) => {
|
||||
return syncTextBox(textBox.id, viewId, acc);
|
||||
return syncTextBox(textBox.id, { viewId, state: acc });
|
||||
}, stateAfterConnectorsSynced);
|
||||
|
||||
return stateAfterTextBoxesSynced;
|
||||
};
|
||||
|
||||
export const createView = (
|
||||
newView: View,
|
||||
model: State['model']
|
||||
): State['model'] => {
|
||||
return produce(model, (draft) => {
|
||||
draft.views.push(newView);
|
||||
export const updateView = (
|
||||
updates: Partial<Pick<View, 'name'>>,
|
||||
ctx: ViewReducerContext
|
||||
): State => {
|
||||
const newState = produce(ctx.state, (draft) => {
|
||||
const view = getItemByIdOrThrow(draft.model.views, ctx.viewId);
|
||||
view.value = { ...view.value, ...updates };
|
||||
});
|
||||
|
||||
return newState;
|
||||
};
|
||||
|
||||
export const createView = (
|
||||
newView: Partial<View>,
|
||||
ctx: ViewReducerContext
|
||||
): State => {
|
||||
const newState = produce(ctx.state, (draft) => {
|
||||
draft.model.views.push({
|
||||
...VIEW_DEFAULTS,
|
||||
id: ctx.viewId,
|
||||
...newView
|
||||
});
|
||||
});
|
||||
|
||||
return newState;
|
||||
};
|
||||
|
||||
export const view = ({ action, payload, ctx }: ViewReducerParams) => {
|
||||
let newState: State;
|
||||
|
||||
switch (action) {
|
||||
case 'SYNC_SCENE':
|
||||
newState = syncScene(ctx);
|
||||
break;
|
||||
case 'CREATE_VIEW':
|
||||
newState = createView(payload, ctx);
|
||||
break;
|
||||
case 'UPDATE_VIEW':
|
||||
newState = updateView(payload, ctx);
|
||||
break;
|
||||
case 'CREATE_VIEWITEM':
|
||||
newState = viewItemReducers.createViewItem(payload, ctx);
|
||||
break;
|
||||
case 'UPDATE_VIEWITEM':
|
||||
newState = viewItemReducers.updateViewItem(payload, ctx);
|
||||
break;
|
||||
case 'DELETE_VIEWITEM':
|
||||
newState = viewItemReducers.deleteViewItem(payload, ctx);
|
||||
break;
|
||||
case 'CREATE_CONNECTOR':
|
||||
newState = connectorReducers.createConnector(payload, ctx);
|
||||
break;
|
||||
case 'UPDATE_CONNECTOR':
|
||||
newState = connectorReducers.updateConnector(payload, ctx);
|
||||
break;
|
||||
case 'SYNC_CONNECTOR':
|
||||
newState = connectorReducers.syncConnector(payload, ctx);
|
||||
break;
|
||||
case 'DELETE_CONNECTOR':
|
||||
newState = connectorReducers.deleteConnector(payload, ctx);
|
||||
break;
|
||||
case 'CREATE_TEXTBOX':
|
||||
newState = textBoxReducers.createTextBox(payload, ctx);
|
||||
break;
|
||||
case 'UPDATE_TEXTBOX':
|
||||
newState = textBoxReducers.updateTextBox(payload, ctx);
|
||||
break;
|
||||
case 'DELETE_TEXTBOX':
|
||||
newState = textBoxReducers.deleteTextBox(payload, ctx);
|
||||
break;
|
||||
case 'CREATE_RECTANGLE':
|
||||
newState = rectangleReducers.createRectangle(payload, ctx);
|
||||
break;
|
||||
case 'UPDATE_RECTANGLE':
|
||||
newState = rectangleReducers.updateRectangle(payload, ctx);
|
||||
break;
|
||||
case 'DELETE_RECTANGLE':
|
||||
newState = rectangleReducers.deleteRectangle(payload, ctx);
|
||||
break;
|
||||
case 'CHANGE_LAYER_ORDER':
|
||||
newState = layerOrderingReducers.changeLayerOrder(payload, ctx);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Invalid action.');
|
||||
}
|
||||
|
||||
const withUpdatedTimeStamp = updateViewTimestamp({
|
||||
state: newState,
|
||||
viewId: ctx.viewId
|
||||
});
|
||||
|
||||
return withUpdatedTimeStamp;
|
||||
};
|
||||
|
||||
@@ -2,14 +2,12 @@ import { produce } from 'immer';
|
||||
import { ViewItem } from 'src/types';
|
||||
import { getItemByIdOrThrow, getConnectorsByViewItem } from 'src/utils';
|
||||
import { validateView } from 'src/schemas/validation';
|
||||
import { State } from './types';
|
||||
import { updateConnector, syncConnector } from './connector';
|
||||
import { State, ViewReducerContext } from './types';
|
||||
import * as reducers from './view';
|
||||
|
||||
export const updateViewItem = (
|
||||
id: string,
|
||||
updates: Partial<ViewItem>,
|
||||
viewId: string,
|
||||
state: State
|
||||
{ id, ...updates }: { id: string } & Partial<ViewItem>,
|
||||
{ viewId, state }: ViewReducerContext
|
||||
): State => {
|
||||
const newState = produce(state, (draft) => {
|
||||
const view = getItemByIdOrThrow(draft.model.views, viewId);
|
||||
@@ -28,7 +26,11 @@ export const updateViewItem = (
|
||||
);
|
||||
|
||||
const updatedConnectors = connectorsToUpdate.reduce((acc, connector) => {
|
||||
return updateConnector(connector.id, connector, viewId, acc);
|
||||
return reducers.view({
|
||||
action: 'UPDATE_CONNECTOR',
|
||||
payload: connector,
|
||||
ctx: { viewId, state: acc }
|
||||
});
|
||||
}, draft);
|
||||
|
||||
draft.model.views[view.index].connectors =
|
||||
@@ -50,9 +52,9 @@ export const updateViewItem = (
|
||||
|
||||
export const createViewItem = (
|
||||
newViewItem: ViewItem,
|
||||
viewId: string,
|
||||
state: State
|
||||
ctx: ViewReducerContext
|
||||
): State => {
|
||||
const { state, viewId } = ctx;
|
||||
const view = getItemByIdOrThrow(state.model.views, viewId);
|
||||
|
||||
const newState = produce(state, (draft) => {
|
||||
@@ -60,13 +62,12 @@ export const createViewItem = (
|
||||
items.unshift(newViewItem);
|
||||
});
|
||||
|
||||
return updateViewItem(newViewItem.id, newViewItem, viewId, newState);
|
||||
return updateViewItem(newViewItem, { viewId, state: newState });
|
||||
};
|
||||
|
||||
export const deleteViewItem = (
|
||||
id: string,
|
||||
viewId: string,
|
||||
state: State
|
||||
{ state, viewId }: ViewReducerContext
|
||||
): State => {
|
||||
const newState = produce(state, (draft) => {
|
||||
const view = getItemByIdOrThrow(draft.model.views, viewId);
|
||||
@@ -80,7 +81,11 @@ export const deleteViewItem = (
|
||||
);
|
||||
|
||||
const updatedConnectors = connectorsToUpdate.reduce((acc, connector) => {
|
||||
return syncConnector(connector.id, viewId, acc);
|
||||
return reducers.view({
|
||||
action: 'SYNC_CONNECTOR',
|
||||
payload: connector.id,
|
||||
ctx: { viewId, state: acc }
|
||||
});
|
||||
}, draft);
|
||||
|
||||
draft.model.views[view.index].connectors =
|
||||
|
||||
@@ -3,6 +3,8 @@ import {
|
||||
iconSchema,
|
||||
modelSchema,
|
||||
modelItemSchema,
|
||||
modelItemsSchema,
|
||||
viewsSchema,
|
||||
viewSchema,
|
||||
viewItemSchema,
|
||||
connectorSchema,
|
||||
@@ -17,12 +19,12 @@ import { StoreApi } from 'zustand';
|
||||
|
||||
export { connectorStyleOptions } from 'src/schemas';
|
||||
export type Model = z.infer<typeof modelSchema>;
|
||||
export type ModelItems = Model['items'];
|
||||
export type Views = Model['views'];
|
||||
export type ModelItems = z.infer<typeof modelItemsSchema>;
|
||||
export type Icon = z.infer<typeof iconSchema>;
|
||||
export type Icons = z.infer<typeof iconsSchema>;
|
||||
export type Colors = z.infer<typeof colorsSchema>;
|
||||
export type ModelItem = z.infer<typeof modelItemSchema>;
|
||||
export type Views = z.infer<typeof viewsSchema>;
|
||||
export type View = z.infer<typeof viewSchema>;
|
||||
export type ViewItem = z.infer<typeof viewItemSchema>;
|
||||
export type ConnectorStyle = keyof typeof connectorStyleOptions;
|
||||
|
||||
Reference in New Issue
Block a user