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);
}
scan(initAccumulator) {
return new ScanStream(this, initAccumulator);
scan(initAccumulator, scanFunc) {
return new ScanStream(this, initAccumulator, scanFunc);
}
remember(initialValue, usingStream) {

View file

@ -9,11 +9,11 @@ interface Stream<T> extends Observable<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>;
@ -42,7 +42,7 @@ export function stream<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>;

View file

@ -2,13 +2,17 @@ import {StreamBase} from './base';
export class ScanStream extends StreamBase {
constructor(stream, initAccumulator) {
constructor(stream, seed, scanFunc) {
super();
this.stream = stream;
this.acc = initAccumulator;
this.value = seed;
this.scanFunc = scanFunc;
}
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,
onCancel: () => any,
onOK: () => any,
infoText: string
onKeyDown: (e) => any,
infoText: any
} & WindowProps ) {
return <Window initWidth={250}

View file

@ -3,8 +3,22 @@ import React from 'react';
export default class ComboBoxControl extends React.Component {
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)}>
{nonExistent}
{children}
</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",
"clipper-lib": "6.2.1",
"earcut": "2.1.1",
"immer": "^9.0.12",
"less": "^3.11.1",
"libtess": "1.2.2",
"lodash": "^4.17.15",
@ -7512,6 +7513,15 @@
"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": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz",
@ -21428,6 +21438,11 @@
"integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=",
"optional": true
},
"immer": {
"version": "9.0.12",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.12.tgz",
"integrity": "sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA=="
},
"import-fresh": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz",

View file

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

View file

@ -49,7 +49,7 @@ export function activate(ctx: ApplicationContext) {
}
function solveAssembly(): void {
if (ctx.craftService.isEditingHistory()) {
if (ctx.craftService.isEditingHistory) {
console.log('skipping assembly resolve request in the history mode');
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 materializeParams from './schema/materializeParams';
import CadError from '../../utils/errors';
@ -103,15 +103,16 @@ export function activate(ctx: CoreContext) {
}
}
function isEditingHistory() {
ctx.craftService = {
get isEditingHistory() {
const mods = this.modifications$.value;
return mods && mods.pointer !== mods.history.length - 1;
}
},
ctx.craftService = {
modify, modifyInHistoryAndStep, reset, rebuild, runRequest, runPipeline,
historyTravel: historyTravel(modifications$),
modifications$, models$, update$, isEditingHistory, pipelineFailure$
modifications$, models$, update$, pipelineFailure$
};
// @ts-ignore
@ -244,9 +245,16 @@ export interface OperationResult {
}
interface CraftHistory {
export interface CraftHistory {
history: OperationRequest[];
pointer: number;
hints?: CraftHints;
}
export interface CraftHints {
noWizardFocus?: boolean;
}
interface CraftService {
@ -256,9 +264,9 @@ interface CraftService {
update$: Emitter<void>;
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[]);
@ -270,13 +278,13 @@ interface CraftService {
runPipeline(history: OperationRequest[], beginIndex: number, endIndex: number): Promise<void>;
isEditingHistory(): boolean;
isEditingHistory: boolean;
}
interface HistoryTravel {
setPointer(pointer, hints:any);
begin(hints: any);
end(hints: any);
begin(hints?: any);
end(hints?: any);
forward(hints: any);
backward(hints: any);
}

View file

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

View file

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

View file

@ -1,8 +1,8 @@
import {TypeRegistry, Types} from "cad/craft/schema/types";
import {OperationSchema, SchemaField} from "cad/craft/schema/schema";
import {ApplicationContext} from "context";
import {Types} from "cad/craft/schema/types";
import {isValueNotProvided, OperationSchema, SchemaField} from "cad/craft/schema/schema";
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 obj = {};
for (let field of fields) {
@ -42,3 +42,33 @@ export default function initializeBySchema(schema: OperationSchema, context: App
}
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 {CoreContext} from "context";
import {
isValueNotProvided,
OperationParams,
OperationParamsError,
OperationParamsErrorReporter,
@ -33,14 +34,13 @@ function materializeParamsImpl(ctx: CoreContext,
result: any,
parentReportError: OperationParamsErrorReporter) {
const notProvided = value => value === undefined || value === null || value === '';
for (let field of Object.keys(schema)) {
const reportError = parentReportError.dot(field);
const md = schema[field];
let value = params[field];
if (notProvided(value)) {
if (isValueNotProvided(value)) {
if (!md.optional) {
reportError('required');
}
@ -52,7 +52,7 @@ function materializeParamsImpl(ctx: CoreContext,
value = md.resolve(
ctx, value, md as any, reportError
)
if (notProvided(value) && !md.optional) {
if (isValueNotProvided(value) && !md.optional) {
reportError('required');
}
}

View file

@ -94,3 +94,5 @@ export function unwrapMetadata(fieldMd: SchemaField) {
}
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]) => ({
...modifications,
operationRegistry,
inProgressOperation: insertOperationReq.type,
inProgressOperation: !!insertOperationReq,
getOperation: type => operationRegistry[type]||EMPTY_OBJECT
})))
@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 Field from 'ui/components/controls/Field';
import Stack from 'ui/components/Stack';
import {camelCaseSplitToStr} from 'gems/camelCaseSplit';
import {FlattenPath, ParamsPath, ParamsPathSegment, WizardContext} from "cad/craft/wizard/wizardTypes";
import {flattenPath, OperationParamValue} from "cad/craft/schema/schema";
import {ParamsPath, ParamsPathSegment, WizardState} from "cad/craft/wizard/wizardTypes";
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}) {
return <Stack>
@ -61,36 +35,42 @@ interface FormFieldProps {
children?: any
}
export function attachToForm(Control) {
export function attachToForm(Control): any {
return function FormField({name, ...props}: FormFieldProps) {
return <FormContext.Consumer>
{
(ctx: FormContextData) => {
const fullPath = flattenPath([...ctx.prefix, name]);
const onChange = val => ctx.updateParam(name, val);
const setActive = val => ctx.setActiveParam(val ? fullPath : undefined);
const ctx = useContext(AppContext);
const formPath = useContext(FormPathContext);
const formState = useContext(FormStateContext);
const params = useContext(FormParamsContext);
const fullPath = [...formPath, name];
const fullPathFlatten = fullPath.join('.');
const onChange = value => ctx.wizardService.updateParam(fullPath, value);
const setActive = (isActive) => ctx.wizardService.updateState(state => {
state.activeParam = isActive ? fullPathFlatten : null;
});
const value = _.get(params, fullPath);
return <React.Fragment>
<Control value={ctx.readParam(name)}
<Control value={value}
onChange={onChange}
name={name} {...props}
setActive={setActive}
active={ctx.activeParam === fullPath} />
active={formState.activeParam === fullPathFlatten} />
</React.Fragment>;
}
}
</FormContext.Consumer>;
};
}
export function SubForm(props: {name: ParamsPathSegment, children: any}) {
return <FormContext.Consumer>
{
(ctx: FormContextData) => {
return <FormContext.Provider value={ctx.dot(props.name)}>
const formState = useContext(FormStateContext);
const formPath = useContext(FormPathContext);
return <FormParamsContext.Provider value={formState[props.name]}>
<FormPathContext.Provider value={[...formPath, props.name]}>
{props.children}
</FormContext.Provider>
}
}
</FormContext.Consumer>
</FormPathContext.Provider>
</FormParamsContext.Provider>;
}

View file

@ -1,96 +1,99 @@
import {state} from 'lstream';
import initializeBySchema from '../schema/initializeBySchema';
import {clone, EMPTY_OBJECT} from 'gems/objects';
import {combine, state, StateStream} from 'lstream';
import initializeBySchema, {fillUpMissingFields} from '../schema/initializeBySchema';
import {clone} from 'gems/objects';
import materializeParams from '../schema/materializeParams';
import {createFunctionList} from 'gems/func';
import {OperationRequest} from "cad/craft/craftPlugin";
import {ParamsPath, WizardContext, WizardState} from "cad/craft/wizard/wizardTypes";
import {CraftHints, CraftHistory, OperationRequest} from "cad/craft/craftPlugin";
import {NewOperationCall, ParamsPath, WizardService, WizardState} from "cad/craft/wizard/wizardTypes";
import _ from "lodash";
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;
streams.wizard = {};
const insertOperation$ = state<NewOperationCall>(null);
streams.wizard.insertOperation = state(EMPTY_OBJECT);
let REQUEST_COUNTER = 1;
streams.wizard.effectiveOperation = state(EMPTY_OBJECT);
const workingRequest$: StateStream<WorkingRequest> = combine<[NewOperationCall, CraftHistory]>(
insertOperation$,
ctx.craftService.modifications$
).map<NewOperationCall|OperationRequest>(([insertOperationReq, mods]) => {
streams.wizard.insertOperation.attach(insertOperationReq => {
if (insertOperationReq.type) {
let type = insertOperationReq.type;
let operation = ctx.services.operation.get(type);
streams.wizard.effectiveOperation.value = {
type: operation.id,
initialOverrides: insertOperationReq.initialOverrides,
changingHistory: false
};
let operation;
let params;
if (insertOperationReq !== null) {
operation = ctx.operationService.get(insertOperationReq.type);
params = initializeBySchema(operation.schema, ctx);
if (insertOperationReq.initialOverrides) {
applyOverrides(params, insertOperationReq.initialOverrides);
}
});
function gotoEditHistoryModeIfNeeded({pointer, history, hints}) {
return {
type: operation.id,
params,
requestKey: REQUEST_COUNTER++
}
} else {
const {pointer, history, hints} = mods
if (pointer !== history.length - 1) {
let {type, params} = history[pointer + 1];
streams.wizard.effectiveOperation.value = {
return {
type,
params,
noWizardFocus: hints && hints.noWizardFocus,
changingHistory: true
params: clone(params),
hints,
requestKey: REQUEST_COUNTER++
};
} else {
streams.wizard.effectiveOperation.value = EMPTY_OBJECT;
}
}
streams.craft.modifications.attach(mod => {
if (streams.wizard.insertOperation.value.type) {
return;
}
gotoEditHistoryModeIfNeeded(mod);
});
streams.wizard.wizardContext = streams.wizard.effectiveOperation.map((opRequest: OperationRequest) => {
let wizCtx: WizardContext = null;
if (opRequest.type) {
let operation = ctx.services.operation.get(opRequest.type);
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);
return null;
}
}
let workingRequest$ = state({
type: opRequest.type,
params
});
}).remember(null);
let materializedWorkingRequest$ = workingRequest$.map(req => {
const materializedWorkingRequest$ = workingRequest$.map(req => {
if (req == null) {
return null;
}
let params = {};
let errors = [];
let operation = ctx.services.operation.get(req.type);
materializeParams(ctx, req.params, operation.schema, params, errors);
if (errors.length !== 0) {
return INVALID_REQUEST;
return null;
}
return {
type: req.type,
params
};
}).remember(INVALID_REQUEST).filter(r => r !== INVALID_REQUEST).throttle(500);
const state$ = state<WizardState>({
activeParam: null
});
const updateParams = mutator => workingRequest$.mutate(data => mutator(data.params));
const updateState = mutator => state$.mutate(state => mutator(state));
}).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) {
@ -104,60 +107,75 @@ export function activate(ctx) {
};
const readParam = (path: ParamsPath) => {
return _.get(params, path);
return _.get(workingRequest$.value.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,
const getWorkingRequest = () => workingRequest$.value;
//legacy
streams.wizard = {
insertOperation: insertOperation$,
};
}
return wizCtx;
}).remember(null);
streams.wizard.wizardContext.pairwise().attach(([oldContext, newContext]) => {
if (oldContext) {
oldContext.dispose();
}
});
const cancel = () => {
insertOperation$.next(null);
};
services.wizard = {
const wizardService: WizardService = {
open: (type, initialOverrides) => {
streams.wizard.insertOperation.value = {
open: (type: string, initialOverrides: NewOperationCall) => {
streams.wizard.insertOperation.next({
type,
initialOverrides
};
});
},
cancel: () => {
streams.wizard.insertOperation.value = EMPTY_OBJECT;
gotoEditHistoryModeIfNeeded(streams.craft.modifications.value);
},
cancel,
applyWorkingRequest: () => {
let {type, params} = streams.wizard.wizardContext.value.workingRequest$.value;
let {type, params} = getWorkingRequest();
let request = clone({type, params});
const setError = error => streams.wizard.wizardContext.mutate(ctx => ctx.state$.mutate(state => state.error = error));
if (streams.wizard.insertOperation.value.type) {
ctx.services.craft.modify(request, () => streams.wizard.insertOperation.value = EMPTY_OBJECT, setError );
const setError = error => state$.mutate(state => state.error = error);
if (insertOperation$.value) {
ctx.craftService.modify(request, cancel, setError);
} 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) {
Object.assign(params, initialOverrides);
}
const INVALID_REQUEST = {};
let REQUEST_COUNTER = 0;

View file

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

View file

@ -1,5 +1,5 @@
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 {Operation} from "cad/craft/operationPlugin";
@ -10,17 +10,30 @@ export type ParamsPath = ParamsPathSegment[];
export type FlattenPath = string;
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>;
state$: StateStream<WizardState>;
open(type: string, initialOverrides: NewOperationCall);
cancel();
applyWorkingRequest();
isInProgress(): boolean;
workingRequest: OperationRequest;
materializedWorkingRequest: any;
updateParams: (mutator: (params: OperationParams) => void) => void;
updateParam: (path: ParamsPath, value: OperationParamValue) => void;
@ -31,13 +44,11 @@ export interface WizardContext {
operation: Operation<any>;
changingHistory: boolean;
noWizardFocus: boolean;
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 {flattenPath, OperationSchema} from "cad/craft/schema/schema";
import {FieldBasicProps, fieldToSchemaGeneric} from "cad/mdf/ui/field";
import {Types} from "cad/craft/schema/types";
import {ComboBoxOption} from "ui/components/controls/ComboBoxControl";
import {FormContext, FormContextData} from "cad/craft/wizard/components/form/Form";
export interface ChoiceWidgetProps extends FieldBasicProps {
@ -19,21 +17,10 @@ export interface ChoiceWidgetProps extends FieldBasicProps {
export function ChoiceWidget(props: ChoiceWidgetProps) {
if (!props.style || props.style === 'dropdown') {
return <FormContext.Consumer>
{
(ctx: FormContextData) => {
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}
return <ComboBoxField name={props.name} defaultValue={props.defaultValue} label={props.label} includeNonExistent>
{props.values.map(value => <ComboBoxOption value={value} key={value}>{value}</ComboBoxOption>)}
</ComboBoxField>;
}
}
</FormContext.Consumer>
} else {
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 {ProjectModel} from "./projectManager/projectManagerPlugin";
import {DebugMode$} from "debugger/Debugger";
import {fillUpMissingFields} from "cad/craft/schema/initializeBySchema";
export const STORAGE_GLOBAL_PREFIX = 'TCAD';
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());
if (dataStr) {
let data = JSON.parse(dataStr);
upgradeIfNeeded(data);
loadData(data);
}
} 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) {
if (data.expressions) {