mirror of
https://github.com/xibyte/jsketcher
synced 2025-12-06 16:33:15 +01:00
improve wizard react integration
This commit is contained in:
parent
4d8990c0d9
commit
02efa9a3dc
29 changed files with 595 additions and 519 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
8
modules/lstream/index.d.ts
vendored
8
modules/lstream/index.d.ts
vendored
|
|
@ -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>;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
40
modules/ui/errorBoundary.tsx
Normal file
40
modules/ui/errorBoundary.tsx
Normal 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
15
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
const mods = this.modifications$.value;
|
||||
return mods && mods.pointer !== mods.history.length - 1;
|
||||
}
|
||||
|
||||
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$),
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,4 +93,6 @@ export function unwrapMetadata(fieldMd: SchemaField) {
|
|||
);
|
||||
}
|
||||
return fieldMd;
|
||||
}
|
||||
}
|
||||
|
||||
export const isValueNotProvided = value => value === undefined || value === null || value === '';
|
||||
|
|
|
|||
|
|
@ -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}) => ({
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
122
web/app/cad/craft/wizard/components/Wizard.tsx
Normal file
122
web/app/cad/craft/wizard/components/Wizard.tsx
Normal 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>
|
||||
}
|
||||
|
|
@ -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);
|
||||
22
web/app/cad/craft/wizard/components/WizardManager.tsx
Normal file
22
web/app/cad/craft/wizard/components/WizardManager.tsx
Normal 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>
|
||||
}
|
||||
|
|
@ -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);
|
||||
return <React.Fragment>
|
||||
<Control value={ctx.readParam(name)}
|
||||
onChange={onChange}
|
||||
name={name} {...props}
|
||||
setActive={setActive}
|
||||
active={ctx.activeParam === fullPath} />
|
||||
</React.Fragment>;
|
||||
}
|
||||
}
|
||||
</FormContext.Consumer>;
|
||||
|
||||
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={value}
|
||||
onChange={onChange}
|
||||
name={name} {...props}
|
||||
setActive={setActive}
|
||||
active={formState.activeParam === fullPathFlatten} />
|
||||
</React.Fragment>;
|
||||
};
|
||||
}
|
||||
|
||||
export function SubForm(props: {name: ParamsPathSegment, children: any}) {
|
||||
|
||||
return <FormContext.Consumer>
|
||||
{
|
||||
(ctx: FormContextData) => {
|
||||
return <FormContext.Provider value={ctx.dot(props.name)}>
|
||||
{props.children}
|
||||
</FormContext.Provider>
|
||||
}
|
||||
}
|
||||
</FormContext.Consumer>
|
||||
const formState = useContext(FormStateContext);
|
||||
const formPath = useContext(FormPathContext);
|
||||
|
||||
return <FormParamsContext.Provider value={formState[props.name]}>
|
||||
<FormPathContext.Provider value={[...formPath, props.name]}>
|
||||
{props.children}
|
||||
</FormPathContext.Provider>
|
||||
</FormParamsContext.Provider>;
|
||||
}
|
||||
|
|
@ -1,163 +1,181 @@
|
|||
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 = {};
|
||||
|
||||
streams.wizard.insertOperation = state(EMPTY_OBJECT);
|
||||
const insertOperation$ = state<NewOperationCall>(null);
|
||||
|
||||
streams.wizard.effectiveOperation = state(EMPTY_OBJECT);
|
||||
let REQUEST_COUNTER = 1;
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
const workingRequest$: StateStream<WorkingRequest> = combine<[NewOperationCall, CraftHistory]>(
|
||||
insertOperation$,
|
||||
ctx.craftService.modifications$
|
||||
).map<NewOperationCall|OperationRequest>(([insertOperationReq, mods]) => {
|
||||
|
||||
}
|
||||
|
||||
streams.craft.modifications.attach(mod => {
|
||||
if (streams.wizard.insertOperation.value.type) {
|
||||
return;
|
||||
}
|
||||
gotoEditHistoryModeIfNeeded(mod);
|
||||
});
|
||||
let operation;
|
||||
let params;
|
||||
|
||||
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);
|
||||
}
|
||||
if (insertOperationReq !== null) {
|
||||
operation = ctx.operationService.get(insertOperationReq.type);
|
||||
params = initializeBySchema(operation.schema, ctx);
|
||||
if (insertOperationReq.initialOverrides) {
|
||||
applyOverrides(params, insertOperationReq.initialOverrides);
|
||||
}
|
||||
|
||||
let workingRequest$ = state({
|
||||
type: opRequest.type,
|
||||
params
|
||||
});
|
||||
|
||||
let materializedWorkingRequest$ = workingRequest$.map(req => {
|
||||
let params = {};
|
||||
let errors = [];
|
||||
materializeParams(ctx, req.params, operation.schema, params, errors);
|
||||
if (errors.length !== 0) {
|
||||
return INVALID_REQUEST;
|
||||
}
|
||||
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];
|
||||
return {
|
||||
type: req.type,
|
||||
params
|
||||
type,
|
||||
params: clone(params),
|
||||
hints,
|
||||
requestKey: REQUEST_COUNTER++
|
||||
};
|
||||
}).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));
|
||||
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,
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return wizCtx;
|
||||
|
||||
}).remember(null);
|
||||
|
||||
streams.wizard.wizardContext.pairwise().attach(([oldContext, newContext]) => {
|
||||
if (oldContext) {
|
||||
oldContext.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
services.wizard = {
|
||||
const materializedWorkingRequest$ = workingRequest$.map(req => {
|
||||
if (req == null) {
|
||||
return null;
|
||||
}
|
||||
let params = {};
|
||||
let errors = [];
|
||||
let operation = ctx.services.operation.get(req.type);
|
||||
|
||||
open: (type, initialOverrides) => {
|
||||
streams.wizard.insertOperation.value = {
|
||||
materializeParams(ctx, req.params, operation.schema, params, errors);
|
||||
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,
|
||||
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;
|
||||
|
|
@ -1,37 +1,36 @@
|
|||
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;
|
||||
schemaIndex.entities.forEach(entityRef => {
|
||||
//TODO: move to uiDefinition
|
||||
let color = entityRef.metadata.markColor;
|
||||
const marker = ctx.services.marker;
|
||||
marker.startSession();
|
||||
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));
|
||||
} else {
|
||||
if (val) {
|
||||
marker.mark(val, color);
|
||||
}
|
||||
if (Array.isArray(val)) {
|
||||
val.forEach(id => marker.mark(id, color));
|
||||
} else {
|
||||
if (val) {
|
||||
marker.mark(val, color);
|
||||
}
|
||||
});
|
||||
marker.commit();
|
||||
}
|
||||
});
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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}
|
||||
{props.values.map(value => <ComboBoxOption value={value} key={value}>{value}</ComboBoxOption>)}
|
||||
</ComboBoxField>;
|
||||
}
|
||||
}
|
||||
</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>)}
|
||||
</ComboBoxField>;
|
||||
|
||||
} else {
|
||||
throw 'implement me';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
42
web/app/cad/preview/previewPlugin.ts
Normal file
42
web/app/cad/preview/previewPlugin.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue