improve wizard react integration

This commit is contained in:
Val Erastov 2022-03-02 00:01:14 -08:00
parent 4d8990c0d9
commit 02efa9a3dc
29 changed files with 595 additions and 519 deletions

View file

@ -14,8 +14,8 @@ export class StreamBase {
return new PairwiseStream(this, first); return new PairwiseStream(this, first);
} }
scan(initAccumulator) { scan(initAccumulator, scanFunc) {
return new ScanStream(this, initAccumulator); return new ScanStream(this, initAccumulator, scanFunc);
} }
remember(initialValue, usingStream) { remember(initialValue, usingStream) {

View file

@ -9,11 +9,11 @@ interface Stream<T> extends Observable<T> {
filter(predicate: (T) => boolean): Stream<T>; filter(predicate: (T) => boolean): Stream<T>;
pairwise(first: T): Stream<[T, T]>; pairwise(first?: T): Stream<[T, T]>;
scan(initAccumulator: any): Stream<any>; scan<T>(seed: T, scanFn: (accum: T, current: T) => T): Stream<T>;
remember(initialValue: T, usingStream: any): StateStream<T> remember(initialValue: T, usingStream?: any): StateStream<T>
distinct(): Stream<T>; distinct(): Stream<T>;
@ -42,7 +42,7 @@ export function stream<T>(): Emitter<T>;
export function eventStream<T>(): Emitter<T>; export function eventStream<T>(): Emitter<T>;
export function combine(...streams: Stream<any>[]): Stream<any[]>; export function combine<T>(...streams: Stream<any>[]): Stream<T>;
export function merge(...streams: Stream<any>[]): Stream<any>; export function merge(...streams: Stream<any>[]): Stream<any>;

View file

@ -2,13 +2,17 @@ import {StreamBase} from './base';
export class ScanStream extends StreamBase { export class ScanStream extends StreamBase {
constructor(stream, initAccumulator) { constructor(stream, seed, scanFunc) {
super(); super();
this.stream = stream; this.stream = stream;
this.acc = initAccumulator; this.value = seed;
this.scanFunc = scanFunc;
} }
attach(observer) { attach(observer) {
return this.stream.attach(v => this.acc = observer(this.acc, v)); return this.stream.attach(v => {
this.value = this.scanFunc(this.value, v);
observer(this.value);
});
} }
} }

View file

@ -13,7 +13,8 @@ export function GenericWizard({topicId, title, left, className, children, onCanc
left?: number, left?: number,
onCancel: () => any, onCancel: () => any,
onOK: () => any, onOK: () => any,
infoText: string onKeyDown: (e) => any,
infoText: any
} & WindowProps ) { } & WindowProps ) {
return <Window initWidth={250} return <Window initWidth={250}

View file

@ -3,8 +3,22 @@ import React from 'react';
export default class ComboBoxControl extends React.Component { export default class ComboBoxControl extends React.Component {
render() { render() {
let {onChange, value, children} = this.props; let {onChange, value, includeNonExistent, children} = this.props;
let nonExistent = null;
if (includeNonExistent) {
let needsInclusion = false;
React.Children.forEach(children, opt => {
if (opt.value === value) {
needsInclusion = false;
}
});
if (needsInclusion) {
nonExistent = <ComboBoxOption value={value}>{value ? value+'' : '<empty>'}</ComboBoxOption>;
}
}
return <select value={value} onChange={e => onChange(e.target.value)}> return <select value={value} onChange={e => onChange(e.target.value)}>
{nonExistent}
{children} {children}
</select> </select>
} }

View file

@ -1,50 +0,0 @@
import React from 'react';
import context from 'context';
export default function errorBoundary(message, fix, resetOn) {
return function(Comp) {
class ErrorBoundary extends React.Component {
state = {
hasError: false,
fixAttempt: false
};
componentDidCatch() {
this.setState({hasError: true});
if (!this.state.fixAttempt) {
if (fix) {
fix(this.props);
this.setState({hasError: false, fixAttempt: true});
}
}
if (resetOn) {
let stream = resetOn(context.streams);
if (stream) {
this.attcahing = true;
this.detacher = stream.attach(this.reset);
this.attcahing = false;
}
}
}
reset = () => {
if (this.attcahing) {
return;
}
this.setState({hasError: false, fixAttempt: false});
if (this.detacher) {
this.detacher();
}
};
render() {
if (this.state.hasError) {
return message || null;
}
return <Comp {...this.props} />;
}
}
return ErrorBoundary;
}
}

View file

@ -0,0 +1,40 @@
import React from 'react';
import context from 'context';
import {Stream} from "lstream";
import {useStream} from "ui/effects";
type ErrorBoundaryProps = {
message: any;
children: any;
};
export function HealingErrorBoundary({resetOn, ...props}: ErrorBoundaryProps & {
resetOn: (ctx) => Stream<any>,
}) {
const key = useStream(ctx => resetOn(ctx).scan(0, (acc, curr) => acc + curr));
return <ErrorBoundary key={key} {...props} />;
}
export class ErrorBoundary extends React.Component<ErrorBoundaryProps, {
hasError: boolean
}> {
state = {
hasError: false,
};
componentDidCatch(error:Error, errorInfo:React.ErrorInfo) {
console.error(error);
this.setState({hasError: true});
}
render() {
if (this.state.hasError) {
return this.props.message || null;
}
return this.props.children;
}
}

15
package-lock.json generated
View file

@ -12,6 +12,7 @@
"classnames": "2.2.5", "classnames": "2.2.5",
"clipper-lib": "6.2.1", "clipper-lib": "6.2.1",
"earcut": "2.1.1", "earcut": "2.1.1",
"immer": "^9.0.12",
"less": "^3.11.1", "less": "^3.11.1",
"libtess": "1.2.2", "libtess": "1.2.2",
"lodash": "^4.17.15", "lodash": "^4.17.15",
@ -7512,6 +7513,15 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/immer": {
"version": "9.0.12",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.12.tgz",
"integrity": "sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/import-fresh": { "node_modules/import-fresh": {
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz",
@ -21428,6 +21438,11 @@
"integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=",
"optional": true "optional": true
}, },
"immer": {
"version": "9.0.12",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.12.tgz",
"integrity": "sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA=="
},
"import-fresh": { "import-fresh": {
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz",

View file

@ -68,6 +68,7 @@
"classnames": "2.2.5", "classnames": "2.2.5",
"clipper-lib": "6.2.1", "clipper-lib": "6.2.1",
"earcut": "2.1.1", "earcut": "2.1.1",
"immer": "^9.0.12",
"less": "^3.11.1", "less": "^3.11.1",
"libtess": "1.2.2", "libtess": "1.2.2",
"lodash": "^4.17.15", "lodash": "^4.17.15",

View file

@ -49,7 +49,7 @@ export function activate(ctx: ApplicationContext) {
} }
function solveAssembly(): void { function solveAssembly(): void {
if (ctx.craftService.isEditingHistory()) { if (ctx.craftService.isEditingHistory) {
console.log('skipping assembly resolve request in the history mode'); console.log('skipping assembly resolve request in the history mode');
return; return;
} }

View file

@ -1,4 +1,4 @@
import {addModification, stepOverriding} from './craftHistoryUtils'; import {addModification, finishHistoryEditing, stepOverriding} from './craftHistoryUtils';
import {Emitter, state, StateStream, stream} from 'lstream'; import {Emitter, state, StateStream, stream} from 'lstream';
import materializeParams from './schema/materializeParams'; import materializeParams from './schema/materializeParams';
import CadError from '../../utils/errors'; import CadError from '../../utils/errors';
@ -103,15 +103,16 @@ export function activate(ctx: CoreContext) {
} }
} }
function isEditingHistory() {
const mods = this.modifications$.value;
return mods && mods.pointer !== mods.history.length - 1;
}
ctx.craftService = { ctx.craftService = {
modify, modifyInHistoryAndStep, reset, rebuild, runRequest, runPipeline,
get isEditingHistory() {
const mods = this.modifications$.value;
return mods && mods.pointer !== mods.history.length - 1;
},
modify, modifyInHistoryAndStep, reset, rebuild, runRequest, runPipeline,
historyTravel: historyTravel(modifications$), historyTravel: historyTravel(modifications$),
modifications$, models$, update$, isEditingHistory, pipelineFailure$ modifications$, models$, update$, pipelineFailure$
}; };
// @ts-ignore // @ts-ignore
@ -244,9 +245,16 @@ export interface OperationResult {
} }
interface CraftHistory { export interface CraftHistory {
history: OperationRequest[]; history: OperationRequest[];
pointer: number; pointer: number;
hints?: CraftHints;
}
export interface CraftHints {
noWizardFocus?: boolean;
} }
interface CraftService { interface CraftService {
@ -256,9 +264,9 @@ interface CraftService {
update$: Emitter<void>; update$: Emitter<void>;
pipelineFailure$: StateStream<any> pipelineFailure$: StateStream<any>
modify(request: OperationRequest, onAccepted: () => void, onError: () => Error); modify(request: OperationRequest, onAccepted: () => void, onError: (error) => void);
modifyInHistoryAndStep(request: OperationRequest, onAccepted: () => void, onError: () => Error); modifyInHistoryAndStep(request: OperationRequest, onAccepted: () => void, onError: (error) => void);
reset(modifications: OperationRequest[]); reset(modifications: OperationRequest[]);
@ -270,13 +278,13 @@ interface CraftService {
runPipeline(history: OperationRequest[], beginIndex: number, endIndex: number): Promise<void>; runPipeline(history: OperationRequest[], beginIndex: number, endIndex: number): Promise<void>;
isEditingHistory(): boolean; isEditingHistory: boolean;
} }
interface HistoryTravel { interface HistoryTravel {
setPointer(pointer, hints:any); setPointer(pointer, hints:any);
begin(hints: any); begin(hints?: any);
end(hints: any); end(hints?: any);
forward(hints: any); forward(hints: any);
backward(hints: any); backward(hints: any);
} }

View file

@ -8,6 +8,9 @@ const pushedModels = new Set();
export const OCI: OCCCommandInterface = new Proxy({}, { export const OCI: OCCCommandInterface = new Proxy({}, {
get: function (target, prop: string, receiver) { get: function (target, prop: string, receiver) {
return prop in target ? target[prop] : function() { return prop in target ? target[prop] : function() {
if (typeof prop !== 'string') {
return undefined;
}
prop = prop.replace(/^_/, ''); prop = prop.replace(/^_/, '');
const args = Array.from(arguments).map(arg => { const args = Array.from(arguments).map(arg => {
const type = typeof arg; const type = typeof arg;

View file

@ -94,7 +94,7 @@ export interface Operation<R> extends OperationDescriptor<R>{
icon: string|IconType; icon: string|IconType;
}; };
schemaIndex: SchemaIndex; schemaIndex: SchemaIndex;
form: () => React.ReactNode; form: React.FunctionComponent;
schema: OperationSchema; schema: OperationSchema;
} }
@ -107,7 +107,8 @@ export interface OperationDescriptor<R> {
run: (request: R, opContext: CoreContext) => OperationResult | Promise<OperationResult>; run: (request: R, opContext: CoreContext) => OperationResult | Promise<OperationResult>;
paramsInfo: (params: R) => string, paramsInfo: (params: R) => string,
previewGeomProvider?: (params: R) => OperationGeometryProvider, previewGeomProvider?: (params: R) => OperationGeometryProvider,
form: FormDefinition | (() => React.ReactNode), previewer?: any,
form: FormDefinition | React.FunctionComponent,
schema?: OperationSchema, schema?: OperationSchema,
onParamsUpdate?: (params, name, value) => void, onParamsUpdate?: (params, name, value) => void,
} }

View file

@ -1,8 +1,8 @@
import {TypeRegistry, Types} from "cad/craft/schema/types"; import {Types} from "cad/craft/schema/types";
import {OperationSchema, SchemaField} from "cad/craft/schema/schema"; import {isValueNotProvided, OperationSchema, SchemaField} from "cad/craft/schema/schema";
import {ApplicationContext} from "context"; import {CoreContext} from "context";
export default function initializeBySchema(schema: OperationSchema, context: ApplicationContext) { export default function initializeBySchema(schema: OperationSchema, context: CoreContext) {
let fields = Object.keys(schema); let fields = Object.keys(schema);
let obj = {}; let obj = {};
for (let field of fields) { for (let field of fields) {
@ -42,3 +42,33 @@ export default function initializeBySchema(schema: OperationSchema, context: App
} }
return obj; return obj;
} }
export function fillUpMissingFields(params: any, schema: OperationSchema, context: CoreContext) {
let fields = Object.keys(schema);
for (let field of fields) {
const md = schema[field] as SchemaField;
if (md.optional) {
continue;
}
let val = params[field];
const isPrimitive =
md.type !== Types.array
&& md.type !== Types.object
&& md.type !== Types.entity;
if (isPrimitive && isValueNotProvided(val)) {
params[field] = md.defaultValue;
} else if (md.type === Types.object) {
if (!val) {
val = {};
params[field] = val;
}
fillUpMissingFields(val, md.schema, context);
}
}
}

View file

@ -1,6 +1,7 @@
import {TypeRegistry} from "cad/craft/schema/types"; import {TypeRegistry} from "cad/craft/schema/types";
import {CoreContext} from "context"; import {CoreContext} from "context";
import { import {
isValueNotProvided,
OperationParams, OperationParams,
OperationParamsError, OperationParamsError,
OperationParamsErrorReporter, OperationParamsErrorReporter,
@ -33,14 +34,13 @@ function materializeParamsImpl(ctx: CoreContext,
result: any, result: any,
parentReportError: OperationParamsErrorReporter) { parentReportError: OperationParamsErrorReporter) {
const notProvided = value => value === undefined || value === null || value === '';
for (let field of Object.keys(schema)) { for (let field of Object.keys(schema)) {
const reportError = parentReportError.dot(field); const reportError = parentReportError.dot(field);
const md = schema[field]; const md = schema[field];
let value = params[field]; let value = params[field];
if (notProvided(value)) { if (isValueNotProvided(value)) {
if (!md.optional) { if (!md.optional) {
reportError('required'); reportError('required');
} }
@ -52,7 +52,7 @@ function materializeParamsImpl(ctx: CoreContext,
value = md.resolve( value = md.resolve(
ctx, value, md as any, reportError ctx, value, md as any, reportError
) )
if (notProvided(value) && !md.optional) { if (isValueNotProvided(value) && !md.optional) {
reportError('required'); reportError('required');
} }
} }

View file

@ -93,4 +93,6 @@ export function unwrapMetadata(fieldMd: SchemaField) {
); );
} }
return fieldMd; return fieldMd;
} }
export const isValueNotProvided = value => value === undefined || value === null || value === '';

View file

@ -16,7 +16,7 @@ import {aboveElement} from 'ui/positionUtils';
.map(([modifications, operationRegistry, insertOperationReq]) => ({ .map(([modifications, operationRegistry, insertOperationReq]) => ({
...modifications, ...modifications,
operationRegistry, operationRegistry,
inProgressOperation: insertOperationReq.type, inProgressOperation: !!insertOperationReq,
getOperation: type => operationRegistry[type]||EMPTY_OBJECT getOperation: type => operationRegistry[type]||EMPTY_OBJECT
}))) })))
@mapContext(({streams, services}) => ({ @mapContext(({streams, services}) => ({

View file

@ -1,119 +0,0 @@
import React from 'react';
import Stack from 'ui/components/Stack';
import Button from 'ui/components/controls/Button';
import ButtonGroup from 'ui/components/controls/ButtonGroup';
import ls from './Wizard.less';
import CadError from '../../../../utils/errors';
import {FormContext, FormContextData} from './form/Form';
import connect from 'ui/connect';
import {combine} from 'lstream';
import {GenericWizard} from "ui/components/GenericWizard";
import * as PropTypes from "prop-types";
import {useStream} from "ui/effects";
@connect((streams, props) => combine(props.context.workingRequest$, props.context.state$)
.map(([workingRequest, state]) => ({
...workingRequest,
activeParam: state.activeParam,
error: state.error
})))
export default class Wizard extends React.Component {
state = {
hasInternalError: false,
};
updateParam = (name, value) => {
this.props.context.updateParam(name, value);
};
componentDidCatch() {
this.setState({hasInternalError: true});
}
render() {
if (this.state.hasInternalError) {
return <span>operation error</span>;
}
let {left, type, params, state, context} = this.props;
let operation = context.operation;
let title = (operation.label || type).toUpperCase();
let Form = operation.form;
const error = this.props.error;
return <GenericWizard
left={left}
title={title}
onClose={this.cancel}
onKeyDown={this.onKeyDown}
setFocus={this.focusFirstInput}
className='Wizard'
data-operation-id={operation.id}
topicId={operation.id}
onCancel={this.cancel}
onOK={this.onOK}
infoText={<>
{error && <ErrorPrinter error={error}/>}
<PipelineError />
</>}
>
<FormContext.Provider value={new FormContextData(this.props.context, [])}>
<Form/>
</FormContext.Provider>
</GenericWizard>;
}
onKeyDown = e => {
switch (e.keyCode) {
case 27 :
this.cancel();
break;
case 13 :
this.onOK();
break;
}
};
focusFirstInput = el => {
if (this.props.noFocus) {
return;
}
let toFocus = el.querySelector('input, select');
if (!toFocus) {
toFocus = el;
}
toFocus.focus();
};
cancel = () => {
this.props.onCancel();
};
onOK = () => {
this.props.onOK();
};
}
function PipelineError() {
const pipelineFailure = useStream(ctx => ctx.craftService.pipelineFailure$);
if (!pipelineFailure) {
return null;
}
return <ErrorPrinter error={pipelineFailure}/>
}
function ErrorPrinter({error}) {
return <div className={ls.errorMessage}>
{CadError.ALGORITHM_ERROR_KINDS.includes(error.kind) && <span>
performing operation with current parameters leads to an invalid object
(self-intersecting / zero-thickness / complete degeneration or unsupported cases)
</span>}
{error.code && <div className={ls.errorCode}>{error.code}</div>}
{error.userMessage && <div className={ls.userErrorMessage}>{error.userMessage}</div>}
{!error.userMessage && <div>internal error processing operation, check the log</div>}
</div>
}

View file

@ -0,0 +1,122 @@
import React, {useContext} from 'react';
import ls from './Wizard.less';
import CadError from '../../../../utils/errors';
import {FormParamsContext, FormPathContext, FormStateContext} from './form/Form';
import {GenericWizard} from "ui/components/GenericWizard";
import {useStream} from "ui/effects";
import {AppContext} from "cad/dom/components/AppContext";
interface WizardProps {
noFocus: boolean;
left?: number;
onCancel(): void;
onOK(): void;
}
export default function Wizard(props: WizardProps) {
const ctx = useContext(AppContext);
const state = useStream(ctx => ctx.wizardService.state$);
const workingRequest = useStream(ctx => ctx.wizardService.workingRequest$);
if (!workingRequest) {
return;
}
const operation = ctx.operationService.get(workingRequest.type);
if (!operation) {
return;
}
const error = state.error;
const onKeyDown = e => {
switch (e.keyCode) {
case 27 :
cancel();
break;
case 13 :
onOK();
break;
}
};
const focusFirstInput = el => {
if (props.noFocus) {
return;
}
let toFocus = el.querySelector('input, select');
if (!toFocus) {
toFocus = el;
}
toFocus.focus();
};
const cancel = () => {
props.onCancel();
};
const onOK = () => {
props.onOK();
};
let {left} = props;
let wizardService = ctx.wizardService;
let title = (operation.label || operation.id).toUpperCase();
let Form = operation.form;
return <GenericWizard
left={left}
title={title}
onClose={cancel}
onKeyDown={onKeyDown}
setFocus={focusFirstInput}
className='Wizard'
data-operation-id={operation.id}
topicId={operation.id}
onCancel={cancel}
onOK={onOK}
infoText={<>
{error && <ErrorPrinter error={error}/>}
<PipelineError />
</>}
>
<FormParamsContext.Provider value={workingRequest.params}>
<FormPathContext.Provider value={[]}>
<FormStateContext.Provider value={state}>
<Form/>
</FormStateContext.Provider>
</FormPathContext.Provider>
</FormParamsContext.Provider>
</GenericWizard>;
}
function PipelineError() {
const pipelineFailure = useStream(ctx => ctx.craftService.pipelineFailure$);
if (!pipelineFailure) {
return null;
}
return <ErrorPrinter error={pipelineFailure}/>
}
function ErrorPrinter({error}) {
return <div className={ls.errorMessage}>
{CadError.ALGORITHM_ERROR_KINDS.includes(error.kind) && <span>
performing operation with current parameters leads to an invalid object
(self-intersecting / zero-thickness / complete degeneration or unsupported cases)
</span>}
{error.code && <div className={ls.errorCode}>{error.code}</div>}
{error.userMessage && <div className={ls.userErrorMessage}>{error.userMessage}</div>}
{!error.userMessage && <div>internal error processing operation, check the log</div>}
</div>
}

View file

@ -1,27 +0,0 @@
import React from 'react';
import Wizard from './Wizard';
import connect from 'ui/connect';
import decoratorChain from 'ui/decoratorChain';
import mapContext from 'ui/mapContext';
import {finishHistoryEditing} from '../../craftHistoryUtils';
function WizardManager({wizardContext, type, cancel, cancelHistoryEdit, applyWorkingRequest}) {
if (!wizardContext) {
return null;
}
return <Wizard key={wizardContext.ID}
context={wizardContext}
noFocus={wizardContext.noWizardFocus}
onCancel={wizardContext.changingHistory ? cancelHistoryEdit : cancel}
onOK={applyWorkingRequest} />
}
export default decoratorChain(
connect(streams => streams.wizard.wizardContext.map(wizardContext => ({wizardContext}))),
mapContext(ctx => ({
cancel: ctx.services.wizard.cancel,
cancelHistoryEdit: () => ctx.streams.craft.modifications.update(modifications => finishHistoryEditing(modifications)),
applyWorkingRequest: ctx.services.wizard.applyWorkingRequest
}))
)
(WizardManager);

View file

@ -0,0 +1,22 @@
import React, {useContext} from 'react';
import Wizard from './Wizard';
import {useStream} from "ui/effects";
import {AppContext} from "cad/dom/components/AppContext";
import {ErrorBoundary} from "ui/errorBoundary";
export default function WizardManager() {
const ctx = useContext(AppContext);
const workingRequest = useStream(ctx => ctx.wizardService.workingRequest$);
if (!workingRequest) {
return null;
}
return <ErrorBoundary key={workingRequest.requestKey}
message={<span>operation error</span>}>
<Wizard noFocus={workingRequest.hints?.noWizardFocus}
onCancel={ctx.craftService.isEditingHistory ? ctx.craftService.historyTravel.end : ctx.wizardService.cancel}
onOK={ctx.wizardService.applyWorkingRequest} />
</ErrorBoundary>
}

View file

@ -1,43 +1,17 @@
import React from 'react'; import React, {useContext} from 'react';
import Label from 'ui/components/controls/Label'; import Label from 'ui/components/controls/Label';
import Field from 'ui/components/controls/Field'; import Field from 'ui/components/controls/Field';
import Stack from 'ui/components/Stack'; import Stack from 'ui/components/Stack';
import {camelCaseSplitToStr} from 'gems/camelCaseSplit'; import {camelCaseSplitToStr} from 'gems/camelCaseSplit';
import {FlattenPath, ParamsPath, ParamsPathSegment, WizardContext} from "cad/craft/wizard/wizardTypes"; import {ParamsPath, ParamsPathSegment, WizardState} from "cad/craft/wizard/wizardTypes";
import {flattenPath, OperationParamValue} from "cad/craft/schema/schema"; import {OperationParams, OperationParamValue} from "cad/craft/schema/schema";
import {AppContext} from "cad/dom/components/AppContext";
import _ from "lodash";
export const FormContext: React.Context<FormContextData> = React.createContext(null); export const FormStateContext: React.Context<WizardState> = React.createContext(null);
export const FormParamsContext: React.Context<OperationParams> = React.createContext(null);
export const FormPathContext: React.Context<ParamsPath> = React.createContext([]);
export class FormContextData {
wizardContext: WizardContext;
prefix: ParamsPath;
constructor(wizardContext: WizardContext, prefix: ParamsPath) {
this.wizardContext = wizardContext;
this.prefix = prefix;
}
updateParam(segment: ParamsPathSegment, value: OperationParamValue): void {
this.wizardContext.updateParam([...this.prefix, segment], value);
}
readParam(segment: ParamsPathSegment): OperationParamValue {
return this.wizardContext.readParam([...this.prefix, segment]);
}
dot(segment: ParamsPathSegment): FormContextData {
return new FormContextData(this.wizardContext, [...this.prefix, segment]);
}
setActiveParam = (path: FlattenPath) => {
this.wizardContext.updateState(state => state.activeParam = path);
}
get activeParam(): FlattenPath {
return this.wizardContext.state$.value.activeParam;
}
}
export function Group({children}) { export function Group({children}) {
return <Stack> return <Stack>
@ -61,36 +35,42 @@ interface FormFieldProps {
children?: any children?: any
} }
export function attachToForm(Control) { export function attachToForm(Control): any {
return function FormField({name, ...props}: FormFieldProps) { return function FormField({name, ...props}: FormFieldProps) {
return <FormContext.Consumer>
{ const ctx = useContext(AppContext);
(ctx: FormContextData) => { const formPath = useContext(FormPathContext);
const fullPath = flattenPath([...ctx.prefix, name]); const formState = useContext(FormStateContext);
const onChange = val => ctx.updateParam(name, val); const params = useContext(FormParamsContext);
const setActive = val => ctx.setActiveParam(val ? fullPath : undefined);
return <React.Fragment> const fullPath = [...formPath, name];
<Control value={ctx.readParam(name)} const fullPathFlatten = fullPath.join('.');
onChange={onChange} const onChange = value => ctx.wizardService.updateParam(fullPath, value);
name={name} {...props} const setActive = (isActive) => ctx.wizardService.updateState(state => {
setActive={setActive} state.activeParam = isActive ? fullPathFlatten : null;
active={ctx.activeParam === fullPath} /> });
</React.Fragment>;
} const value = _.get(params, fullPath);
}
</FormContext.Consumer>; return <React.Fragment>
<Control value={value}
onChange={onChange}
name={name} {...props}
setActive={setActive}
active={formState.activeParam === fullPathFlatten} />
</React.Fragment>;
}; };
} }
export function SubForm(props: {name: ParamsPathSegment, children: any}) { export function SubForm(props: {name: ParamsPathSegment, children: any}) {
return <FormContext.Consumer> const formState = useContext(FormStateContext);
{ const formPath = useContext(FormPathContext);
(ctx: FormContextData) => {
return <FormContext.Provider value={ctx.dot(props.name)}> return <FormParamsContext.Provider value={formState[props.name]}>
{props.children} <FormPathContext.Provider value={[...formPath, props.name]}>
</FormContext.Provider> {props.children}
} </FormPathContext.Provider>
} </FormParamsContext.Provider>;
</FormContext.Consumer>
} }

View file

@ -1,163 +1,181 @@
import {state} from 'lstream'; import {combine, state, StateStream} from 'lstream';
import initializeBySchema from '../schema/initializeBySchema'; import initializeBySchema, {fillUpMissingFields} from '../schema/initializeBySchema';
import {clone, EMPTY_OBJECT} from 'gems/objects'; import {clone} from 'gems/objects';
import materializeParams from '../schema/materializeParams'; import materializeParams from '../schema/materializeParams';
import {createFunctionList} from 'gems/func'; import {createFunctionList} from 'gems/func';
import {OperationRequest} from "cad/craft/craftPlugin"; import {CraftHints, CraftHistory, OperationRequest} from "cad/craft/craftPlugin";
import {ParamsPath, WizardContext, WizardState} from "cad/craft/wizard/wizardTypes"; import {NewOperationCall, ParamsPath, WizardService, WizardState} from "cad/craft/wizard/wizardTypes";
import _ from "lodash"; import _ from "lodash";
import {OperationParamValue} from "cad/craft/schema/schema"; import {OperationParamValue} from "cad/craft/schema/schema";
import {ApplicationContext} from "context";
import {Operation} from "cad/craft/operationPlugin";
import produce from "immer"
export function activate(ctx) { type WorkingRequest = OperationRequest & {
hints?: CraftHints,
requestKey: number
}
export function activate(ctx: ApplicationContext) {
let {streams, services} = ctx; let {streams, services} = ctx;
streams.wizard = {}; const insertOperation$ = state<NewOperationCall>(null);
streams.wizard.insertOperation = state(EMPTY_OBJECT);
streams.wizard.effectiveOperation = state(EMPTY_OBJECT); let REQUEST_COUNTER = 1;
streams.wizard.insertOperation.attach(insertOperationReq => { const workingRequest$: StateStream<WorkingRequest> = combine<[NewOperationCall, CraftHistory]>(
if (insertOperationReq.type) { insertOperation$,
let type = insertOperationReq.type; ctx.craftService.modifications$
let operation = ctx.services.operation.get(type); ).map<NewOperationCall|OperationRequest>(([insertOperationReq, mods]) => {
streams.wizard.effectiveOperation.value = {
type: operation.id,
initialOverrides: insertOperationReq.initialOverrides,
changingHistory: false
};
}
});
function gotoEditHistoryModeIfNeeded({pointer, history, hints}) {
if (pointer !== history.length - 1) {
let {type, params} = history[pointer + 1];
streams.wizard.effectiveOperation.value = {
type,
params,
noWizardFocus: hints && hints.noWizardFocus,
changingHistory: true
};
} else {
streams.wizard.effectiveOperation.value = EMPTY_OBJECT;
}
} let operation;
let params;
streams.craft.modifications.attach(mod => {
if (streams.wizard.insertOperation.value.type) {
return;
}
gotoEditHistoryModeIfNeeded(mod);
});
streams.wizard.wizardContext = streams.wizard.effectiveOperation.map((opRequest: OperationRequest) => { if (insertOperationReq !== null) {
let wizCtx: WizardContext = null; operation = ctx.operationService.get(insertOperationReq.type);
if (opRequest.type) { params = initializeBySchema(operation.schema, ctx);
if (insertOperationReq.initialOverrides) {
let operation = ctx.services.operation.get(opRequest.type); applyOverrides(params, insertOperationReq.initialOverrides);
let params;
let {changingHistory, noWizardFocus} = opRequest;
if (changingHistory) {
params = clone(opRequest.params)
} else {
params = initializeBySchema(operation.schema, ctx);
if (opRequest.initialOverrides) {
applyOverrides(params, opRequest.initialOverrides);
}
} }
let workingRequest$ = state({ return {
type: opRequest.type, type: operation.id,
params params,
}); requestKey: REQUEST_COUNTER++
}
let materializedWorkingRequest$ = workingRequest$.map(req => { } else {
let params = {}; const {pointer, history, hints} = mods
let errors = []; if (pointer !== history.length - 1) {
materializeParams(ctx, req.params, operation.schema, params, errors); let {type, params} = history[pointer + 1];
if (errors.length !== 0) {
return INVALID_REQUEST;
}
return { return {
type: req.type, type,
params params: clone(params),
hints,
requestKey: REQUEST_COUNTER++
}; };
}).remember(INVALID_REQUEST).filter(r => r !== INVALID_REQUEST).throttle(500); } else {
const state$ = state<WizardState>({ return null;
activeParam: null }
});
const updateParams = mutator => workingRequest$.mutate(data => mutator(data.params));
const updateState = mutator => state$.mutate(state => mutator(state));
const updateParam = (path: ParamsPath, value: OperationParamValue) => {
updateParams(params => {
// if (operation.onParamsUpdate) {
// operation.onParamsUpdate(params, name, value, params[name]);
// }
if (!Array.isArray(path)) {
path = [path]
}
_.set(params, path, value);
});
};
const readParam = (path: ParamsPath) => {
return _.get(params, path);
};
const disposerList = createFunctionList();
wizCtx = {
workingRequest$, materializedWorkingRequest$, state$,
updateParams, updateParam, readParam, updateState,
operation, changingHistory, noWizardFocus,
addDisposer: disposerList.add,
dispose: disposerList.call,
ID: ++REQUEST_COUNTER,
};
} }
return wizCtx;
}).remember(null); }).remember(null);
streams.wizard.wizardContext.pairwise().attach(([oldContext, newContext]) => { const materializedWorkingRequest$ = workingRequest$.map(req => {
if (oldContext) { if (req == null) {
oldContext.dispose(); return null;
} }
}); let params = {};
let errors = [];
services.wizard = { let operation = ctx.services.operation.get(req.type);
open: (type, initialOverrides) => { materializeParams(ctx, req.params, operation.schema, params, errors);
streams.wizard.insertOperation.value = { if (errors.length !== 0) {
return null;
}
return {
type: req.type,
params
};
}).remember(null).filter(r => r !== null).throttle(500);
const state$ = state<WizardState>({});
let disposerList = createFunctionList();
// reset effect
workingRequest$.pairwise().attach(([old, curr]) => {
if (old !== null && old.requestKey !== curr?.requestKey) {
console.log("=========> DISPOSE")
disposerList.call();
disposerList = createFunctionList();
state$.next({});
}
})
const updateParams = mutator => workingRequest$.update((req: WorkingRequest) => produce(req, draft => mutator(draft.params)));
const updateState = mutator => state$.update((state: WizardState) => produce(state, mutator));
const updateParam = (path: ParamsPath, value: OperationParamValue) => {
updateParams(params => {
// if (operation.onParamsUpdate) {
// operation.onParamsUpdate(params, name, value, params[name]);
// }
if (!Array.isArray(path)) {
path = [path]
}
_.set(params, path, value);
});
};
const readParam = (path: ParamsPath) => {
return _.get(workingRequest$.value.params, path);
};
const getWorkingRequest = () => workingRequest$.value;
//legacy
streams.wizard = {
insertOperation: insertOperation$,
};
const cancel = () => {
insertOperation$.next(null);
};
const wizardService: WizardService = {
open: (type: string, initialOverrides: NewOperationCall) => {
streams.wizard.insertOperation.next({
type, type,
initialOverrides initialOverrides
}; });
}, },
cancel: () => { cancel,
streams.wizard.insertOperation.value = EMPTY_OBJECT;
gotoEditHistoryModeIfNeeded(streams.craft.modifications.value);
},
applyWorkingRequest: () => { applyWorkingRequest: () => {
let {type, params} = streams.wizard.wizardContext.value.workingRequest$.value; let {type, params} = getWorkingRequest();
let request = clone({type, params}); let request = clone({type, params});
const setError = error => streams.wizard.wizardContext.mutate(ctx => ctx.state$.mutate(state => state.error = error)); const setError = error => state$.mutate(state => state.error = error);
if (streams.wizard.insertOperation.value.type) { if (insertOperation$.value) {
ctx.services.craft.modify(request, () => streams.wizard.insertOperation.value = EMPTY_OBJECT, setError ); ctx.craftService.modify(request, cancel, setError);
} else { } else {
ctx.services.craft.modifyInHistoryAndStep(request, () => streams.wizard.effectiveOperation.value = EMPTY_OBJECT, setError); ctx.craftService.modifyInHistoryAndStep(request, () => {}, setError);
} }
}, },
isInProgress: () => streams.wizard.wizardContext.value !== null isInProgress: () => getWorkingRequest() !== null,
get workingRequest() {
return getWorkingRequest();
},
get materializedWorkingRequest() {
return materializedWorkingRequest$.value;
},
get operation(): Operation<any> {
const req = getWorkingRequest();
if (!req) {
return null;
}
return ctx.operationService.get(req.type);
},
workingRequest$, materializedWorkingRequest$, state$,
updateParams, updateParam, readParam, updateState,
addDisposer: disposerList.add
}; };
ctx.wizardService = services.wizard = wizardService;
} }
declare module 'context' {
interface ApplicationContext {
wizardService: WizardService
}
}
function applyOverrides(params, initialOverrides) { function applyOverrides(params, initialOverrides) {
Object.assign(params, initialOverrides); Object.assign(params, initialOverrides);
} }
const INVALID_REQUEST = {};
let REQUEST_COUNTER = 0;

View file

@ -1,37 +1,36 @@
import {FACE, SHELL} from '../../model/entities'; import {FACE, SHELL} from 'cad/model/entities';
import {memoize} from "lodash/function"; import {memoize} from "lodash/function";
import {Types} from "cad/craft/schema/types";
import {OperationRequest} from "cad/craft/craftPlugin"; import {OperationRequest} from "cad/craft/craftPlugin";
import {FlattenPath, ParamsPath, WizardContext} from "cad/craft/wizard/wizardTypes"; import {FlattenPath, ParamsPath, WizardService} from "cad/craft/wizard/wizardTypes";
import {OperationParamValue, SchemaField} from "cad/craft/schema/schema"; import {OperationParamValue} from "cad/craft/schema/schema";
import {EntityReference, SchemaIndexField} from "cad/craft/operationPlugin"; import {EntityReference} from "cad/craft/operationPlugin";
import {ApplicationContext} from "context";
export function activate(ctx) { export function activate(ctx: ApplicationContext) {
ctx.streams.wizard.wizardContext.attach((wizCtx: WizardContext) => { const wizardService = ctx.wizardService;
wizardService.workingRequest$.attach((opRequest: OperationRequest) => {
ctx.services.marker.clear(); ctx.services.marker.clear();
if (wizCtx) { if (opRequest) {
const wizardPickHandler = createPickHandlerFromSchema(wizCtx); const wizardPickHandler = createPickHandlerFromSchema(wizardService);
ctx.services.pickControl.setPickHandler(wizardPickHandler); ctx.services.pickControl.setPickHandler(wizardPickHandler);
wizCtx.workingRequest$.attach(({type, params}: OperationRequest) => { const marker = ctx.services.marker;
const marker = ctx.services.marker; marker.startSession();
marker.startSession(); let {schemaIndex} = wizardService.operation;
let {schemaIndex} = wizCtx.operation; schemaIndex.entities.forEach(entityRef => {
schemaIndex.entities.forEach(entityRef => { //TODO: move to uiDefinition
//TODO: move to uiDefinition let color = entityRef.metadata.markColor;
let color = entityRef.metadata.markColor;
let val = wizCtx.readParam(entityRef.field.path); let val = wizardService.readParam(entityRef.field.path);
if (Array.isArray(val)) { if (Array.isArray(val)) {
val.forEach(id => marker.mark(id, color)); val.forEach(id => marker.mark(id, color));
} else { } else {
if (val) { if (val) {
marker.mark(val, color); marker.mark(val, color);
}
} }
}); }
marker.commit();
}); });
marker.commit();
} else { } else {
ctx.services.pickControl.setPickHandler(null); ctx.services.pickControl.setPickHandler(null);
@ -52,20 +51,20 @@ const arrayValue = (id, arr) => {
const getEntityParams = memoize(schema => Object.keys(schema).filter(key => schema[key].type === 'entity')); const getEntityParams = memoize(schema => Object.keys(schema).filter(key => schema[key].type === 'entity'));
function createPickHandlerFromSchema(wizCtx: WizardContext) { function createPickHandlerFromSchema(wizardService: WizardService) {
function update(param: ParamsPath, value: OperationParamValue, paramToMakeActive: FlattenPath) { function update(param: ParamsPath, value: OperationParamValue, paramToMakeActive: FlattenPath) {
wizCtx.updateParam(param, value); wizardService.updateParam(param, value);
wizCtx.updateState(state => { wizardService.updateState(state => {
state.activeParam = paramToMakeActive; state.activeParam = paramToMakeActive;
}); });
} }
return model => { return model => {
const modelType = model.TYPE; const modelType = model.TYPE;
let {schemaIndex} = wizCtx.operation; let {schemaIndex} = wizardService.operation;
let activeEntityRef = () => { let activeEntityRef = () => {
const state = wizCtx.state$.value; const state = wizardService.state$.value;
return schemaIndex.entitiesByFlattenedPaths[state.activeParam]; return schemaIndex.entitiesByFlattenedPaths[state.activeParam];
} }
@ -83,7 +82,7 @@ function createPickHandlerFromSchema(wizCtx: WizardContext) {
const param = entityRef.field; const param = entityRef.field;
const valueGetter = entityRef.isArray ? arrayValue : singleValue; const valueGetter = entityRef.isArray ? arrayValue : singleValue;
let paramToMakeActive = getNextActiveParam(entityRef); let paramToMakeActive = getNextActiveParam(entityRef);
const currentValue = wizCtx.readParam(param.path); const currentValue = wizardService.readParam(param.path);
update(param.path, valueGetter(id, currentValue), paramToMakeActive.field.flattenedPath); update(param.path, valueGetter(id, currentValue), paramToMakeActive.field.flattenedPath);
} }
@ -111,7 +110,7 @@ function createPickHandlerFromSchema(wizCtx: WizardContext) {
function deselectIfNeeded(id) { function deselectIfNeeded(id) {
for (let entityRef of schemaIndex.entities) { for (let entityRef of schemaIndex.entities) {
let val = wizCtx.readParam(entityRef.field.path); let val = wizardService.readParam(entityRef.field.path);
if (val === id) { if (val === id) {
update(entityRef.field.path, undefined, entityRef.field.flattenedPath); update(entityRef.field.path, undefined, entityRef.field.flattenedPath);
return true; return true;

View file

@ -1,5 +1,5 @@
import {StateStream} from "lstream"; import {StateStream} from "lstream";
import {OperationRequest} from "cad/craft/craftPlugin"; import {CraftHints, OperationRequest} from "cad/craft/craftPlugin";
import {MaterializedOperationParams, OperationParamValue, OperationParams} from "cad/craft/schema/schema"; import {MaterializedOperationParams, OperationParamValue, OperationParams} from "cad/craft/schema/schema";
import {Operation} from "cad/craft/operationPlugin"; import {Operation} from "cad/craft/operationPlugin";
@ -10,17 +10,30 @@ export type ParamsPath = ParamsPathSegment[];
export type FlattenPath = string; export type FlattenPath = string;
export type WizardState = { export type WizardState = {
activeParam: FlattenPath activeParam?: FlattenPath
error?: any
}; };
export interface WizardContext { export interface WizardService {
workingRequest$: StateStream<OperationRequest>; workingRequest$: StateStream<OperationRequest&{hints?: CraftHints}>;
materializedWorkingRequest$: StateStream<MaterializedOperationParams>; materializedWorkingRequest$: StateStream<MaterializedOperationParams>;
state$: StateStream<WizardState>; state$: StateStream<WizardState>;
open(type: string, initialOverrides: NewOperationCall);
cancel();
applyWorkingRequest();
isInProgress(): boolean;
workingRequest: OperationRequest;
materializedWorkingRequest: any;
updateParams: (mutator: (params: OperationParams) => void) => void; updateParams: (mutator: (params: OperationParams) => void) => void;
updateParam: (path: ParamsPath, value: OperationParamValue) => void; updateParam: (path: ParamsPath, value: OperationParamValue) => void;
@ -31,13 +44,11 @@ export interface WizardContext {
operation: Operation<any>; operation: Operation<any>;
changingHistory: boolean;
noWizardFocus: boolean;
addDisposer: (disposer: () => any|void) => void; addDisposer: (disposer: () => any|void) => void;
dispose: () => void; }
ID: number; export interface NewOperationCall {
type: string;
initialOverrides: OperationParams;
} }

View file

@ -1,10 +1,8 @@
import {ComboBoxField, NumberField} from "cad/craft/wizard/components/form/Fields"; import {ComboBoxField} from "cad/craft/wizard/components/form/Fields";
import React from "react"; import React from "react";
import {flattenPath, OperationSchema} from "cad/craft/schema/schema";
import {FieldBasicProps, fieldToSchemaGeneric} from "cad/mdf/ui/field"; import {FieldBasicProps, fieldToSchemaGeneric} from "cad/mdf/ui/field";
import {Types} from "cad/craft/schema/types"; import {Types} from "cad/craft/schema/types";
import {ComboBoxOption} from "ui/components/controls/ComboBoxControl"; import {ComboBoxOption} from "ui/components/controls/ComboBoxControl";
import {FormContext, FormContextData} from "cad/craft/wizard/components/form/Form";
export interface ChoiceWidgetProps extends FieldBasicProps { export interface ChoiceWidgetProps extends FieldBasicProps {
@ -19,21 +17,10 @@ export interface ChoiceWidgetProps extends FieldBasicProps {
export function ChoiceWidget(props: ChoiceWidgetProps) { export function ChoiceWidget(props: ChoiceWidgetProps) {
if (!props.style || props.style === 'dropdown') { if (!props.style || props.style === 'dropdown') {
return <FormContext.Consumer> return <ComboBoxField name={props.name} defaultValue={props.defaultValue} label={props.label} includeNonExistent>
{ {props.values.map(value => <ComboBoxOption value={value} key={value}>{value}</ComboBoxOption>)}
(ctx: FormContextData) => { </ComboBoxField>;
const value = ctx.readParam(props.name);
let nonExistent = null;
if (!props.values.includes(value as any)) {
nonExistent = <ComboBoxOption value={value}>{value ? value+'' : '<empty>'}</ComboBoxOption>
}
return <ComboBoxField name={props.name} defaultValue={props.defaultValue} label={props.label} >
{nonExistent}
{props.values.map(value => <ComboBoxOption value={value} key={value}>{value}</ComboBoxOption>)}
</ComboBoxField>;
}
}
</FormContext.Consumer>
} else { } else {
throw 'implement me'; throw 'implement me';
} }

View file

@ -1,40 +0,0 @@
import {createPreviewer} from './scenePreviewer';
export function activate(ctx) {
let {streams, services} = ctx;
streams.wizard.wizardContext.attach(wizCtx => {
if (!wizCtx) {
return;
}
let {operation, materializedWorkingRequest$} = wizCtx;
if (operation.previewGeomProvider || operation.previewer) {
let previewer = null;
materializedWorkingRequest$.attach(({type, params}) => {
if (previewer === null) {
try {
if (operation.previewGeomProvider) {
previewer = createPreviewer(operation.previewGeomProvider, services, params);
} else if (operation.previewer) {
previewer = operation.previewer(ctx, params, wizCtx.updateParams);
}
} catch (e) {
console.error(e);
return;
}
wizCtx.addDisposer(() => {
previewer.dispose();
ctx.services.viewer.requestRender();
});
} else {
try {
previewer.update(params);
} catch (e) {
console.error(e);
}
}
ctx.services.viewer.requestRender();
});
}
});
}

View file

@ -0,0 +1,42 @@
import {createPreviewer} from './scenePreviewer';
import {ApplicationContext} from "context";
export function activate(ctx: ApplicationContext) {
let previewer = null;
ctx.wizardService.materializedWorkingRequest$.attach(materializedWorkingRequest => {
if (!materializedWorkingRequest) {
previewer = null;
return;
}
const {type, params} = materializedWorkingRequest;
const operation = ctx.wizardService.operation;
if (operation.previewGeomProvider || operation.previewer) {
if (previewer === null) {
let newPreviewer;
try {
if (operation.previewGeomProvider) {
newPreviewer = createPreviewer(operation.previewGeomProvider, ctx.services, params);
} else if (operation.previewer) {
newPreviewer = operation.previewer(ctx, params, ctx.wizardService.updateParams);
}
previewer = newPreviewer;
} catch (e) {
console.error(e);
return;
}
ctx.wizardService.addDisposer(() => {
newPreviewer.dispose();
previewer = null;
ctx.services.viewer.requestRender();
});
} else {
try {
previewer.update(params);
} catch (e) {
console.error(e);
}
}
ctx.services.viewer.requestRender();
}
});
}

View file

@ -6,6 +6,7 @@ import {SketchFormat_V3} from "../sketcher/io";
import {OperationRequest} from "./craft/craftPlugin"; import {OperationRequest} from "./craft/craftPlugin";
import {ProjectModel} from "./projectManager/projectManagerPlugin"; import {ProjectModel} from "./projectManager/projectManagerPlugin";
import {DebugMode$} from "debugger/Debugger"; import {DebugMode$} from "debugger/Debugger";
import {fillUpMissingFields} from "cad/craft/schema/initializeBySchema";
export const STORAGE_GLOBAL_PREFIX = 'TCAD'; export const STORAGE_GLOBAL_PREFIX = 'TCAD';
export const PROJECTS_PREFIX = `${STORAGE_GLOBAL_PREFIX}.projects.`; export const PROJECTS_PREFIX = `${STORAGE_GLOBAL_PREFIX}.projects.`;
@ -54,6 +55,7 @@ export function initProjectService(ctx: CoreContext, id: string, hints: any) {
let dataStr = ctx.storageService.get(ctx.projectService.projectStorageKey()); let dataStr = ctx.storageService.get(ctx.projectService.projectStorageKey());
if (dataStr) { if (dataStr) {
let data = JSON.parse(dataStr); let data = JSON.parse(dataStr);
upgradeIfNeeded(data);
loadData(data); loadData(data);
} }
} catch (e) { } catch (e) {
@ -61,6 +63,16 @@ export function initProjectService(ctx: CoreContext, id: string, hints: any) {
} }
} }
function upgradeIfNeeded(data: ProjectModel) {
if (data.history) {
data.history.forEach(req => {
const operation = ctx.operationService.get(req.type);
if (operation) {
fillUpMissingFields(req.params, operation.schema, ctx);
}
});
}
}
function loadData(data: ProjectModel) { function loadData(data: ProjectModel) {
if (data.expressions) { if (data.expressions) {