From b287ca7925636e0dcc6be198a2eb77c3d3f7a9ff Mon Sep 17 00:00:00 2001 From: kyusho Date: Fri, 16 Sep 2022 11:32:45 +0800 Subject: [PATCH 1/7] performance(g_walker): Removed repeated initializations in visualSpecStore. --- .../src/store/visualSpecStore.ts | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/packages/graphic-walker/src/store/visualSpecStore.ts b/packages/graphic-walker/src/store/visualSpecStore.ts index df26eaf5..fdaba11a 100644 --- a/packages/graphic-walker/src/store/visualSpecStore.ts +++ b/packages/graphic-walker/src/store/visualSpecStore.ts @@ -104,11 +104,22 @@ function initVisualConfig (): IVisualConfig { } interface IVisSpec { + readonly visId: string; name?: [string, Record?]; - visId: string; encodings: DraggableFieldState; config: IVisualConfig; } + +class RevertibleVisSpec { + + readonly visId: IVisSpec['visId']; + + constructor(initState: IVisSpec) { + this.visId = initState.visId; + } + +} + export class VizSpecStore { // public fields: IViewField[] = []; private commonStore: CommonStore; @@ -124,17 +135,25 @@ export class VizSpecStore { this.visList.push({ name: ['main.tablist.autoTitle', { idx: 1 }], visId: uuidv4(), - config: initVisualConfig(), - encodings: initEncoding() + config: this.visualConfig, + encodings: this.draggableFieldState, }) makeAutoObservable(this, { visList: observable.shallow }); // FIXME!!!!! - this.reactions.push(reaction(() => commonStore.currentDataset, (dataset) => { - this.initState(); - this.initMetaState(dataset); - })) + this.reactions.push( + reaction(() => commonStore.currentDataset, (dataset) => { + this.initState(); + this.initMetaState(dataset); + }), + reaction(() => this.visList[this.visIndex].config, curVisCfg => { + this.visualConfig = curVisCfg; + }), + reaction(() => this.visList[this.visIndex].encodings, curEncodings => { + this.draggableFieldState = curEncodings; + }), + ); } /** * dimension fields in visualization @@ -172,13 +191,9 @@ export class VizSpecStore { encodings: initEncoding() }) this.visIndex = this.visList.length - 1; - this.draggableFieldState = this.visList[this.visIndex].encodings; - this.visualConfig = this.visList[this.visIndex].config; } public selectVisualization (visIndex: number) { this.visIndex = visIndex; - this.draggableFieldState = this.visList[visIndex].encodings; - this.visualConfig = this.visList[visIndex].config } public setVisName (visIndex: number, name: string) { this.visList[visIndex] = { From f8e16faf745a1f71f2154e56f49e96421ae9627a Mon Sep 17 00:00:00 2001 From: kyusho Date: Mon, 19 Sep 2022 15:38:07 +0800 Subject: [PATCH 2/7] feat(g_walker): Undo & Redo. --- packages/graphic-walker/package.json | 1 + packages/graphic-walker/src/App.tsx | 2 + .../graphic-walker/src/locales/en-US.json | 4 + .../graphic-walker/src/locales/zh-CN.json | 4 + .../graphic-walker/src/segments/visNav.tsx | 2 - .../src/store/visualSpecStore.ts | 588 ++++++++++++------ .../src/visualSettings/menubar.tsx | 99 +++ yarn.lock | 37 +- 8 files changed, 549 insertions(+), 188 deletions(-) create mode 100644 packages/graphic-walker/src/visualSettings/menubar.tsx diff --git a/packages/graphic-walker/package.json b/packages/graphic-walker/package.json index 25873440..a3a37df0 100644 --- a/packages/graphic-walker/package.json +++ b/packages/graphic-walker/package.json @@ -31,6 +31,7 @@ "autoprefixer": "^10.3.5", "i18next": "^21.9.1", "i18next-browser-languagedetector": "^6.1.5", + "immer": "^9.0.15", "mobx": "^6.3.3", "mobx-react-lite": "^3.2.1", "postcss": "^8.3.7", diff --git a/packages/graphic-walker/src/App.tsx b/packages/graphic-walker/src/App.tsx index 5caef497..88c12473 100644 --- a/packages/graphic-walker/src/App.tsx +++ b/packages/graphic-walker/src/App.tsx @@ -21,6 +21,7 @@ import { Specification } from 'visual-insights'; import VisNav from './segments/visNav'; import { useTranslation } from 'react-i18next'; import { mergeLocaleRes, setLocaleLanguage } from './locales/i18n'; +import Menubar from './visualSettings/menubar'; export interface EditorProps { @@ -95,6 +96,7 @@ const App: React.FC = props => { {/* {}} /> */} +
diff --git a/packages/graphic-walker/src/locales/en-US.json b/packages/graphic-walker/src/locales/en-US.json index 6c577f08..d72af9d2 100644 --- a/packages/graphic-walker/src/locales/en-US.json +++ b/packages/graphic-walker/src/locales/en-US.json @@ -69,6 +69,10 @@ "autoTitle": "Chart {{idx}}" }, "tabpanel": { + "menubar": { + "undo": "Undo", + "redo": "Redo" + }, "settings": { "toggle": { "aggregation": "Enable Aggregation", diff --git a/packages/graphic-walker/src/locales/zh-CN.json b/packages/graphic-walker/src/locales/zh-CN.json index c55946ef..a473ee19 100644 --- a/packages/graphic-walker/src/locales/zh-CN.json +++ b/packages/graphic-walker/src/locales/zh-CN.json @@ -69,6 +69,10 @@ "autoTitle": "图表 {{idx}}" }, "tabpanel": { + "menubar": { + "undo": "撤销", + "redo": "重做" + }, "settings": { "toggle": { "aggregation": "聚合度量", diff --git a/packages/graphic-walker/src/segments/visNav.tsx b/packages/graphic-walker/src/segments/visNav.tsx index 5c3549f2..1168478a 100644 --- a/packages/graphic-walker/src/segments/visNav.tsx +++ b/packages/graphic-walker/src/segments/visNav.tsx @@ -24,11 +24,9 @@ const VisNav: React.FC = (props) => { const visSelectionHandler = useCallback((tabKey: string, tabIndex: number) => { if (tabKey === ADD_KEY) { - vizStore.saveVisChange(); vizStore.addVisualization(); vizStore.initMetaState(currentDataset) } else { - vizStore.saveVisChange(); vizStore.selectVisualization(tabIndex); } }, [currentDataset, vizStore]) diff --git a/packages/graphic-walker/src/store/visualSpecStore.ts b/packages/graphic-walker/src/store/visualSpecStore.ts index fdaba11a..54942b40 100644 --- a/packages/graphic-walker/src/store/visualSpecStore.ts +++ b/packages/graphic-walker/src/store/visualSpecStore.ts @@ -5,6 +5,8 @@ import { v4 as uuidv4 } from 'uuid'; import { Specification } from "visual-insights"; import { GEMO_TYPES } from "../config"; import { makeBinField, makeLogField } from "../utils/normalization"; +import produce from 'immer'; + interface IVisualConfig { defaultAggregated: boolean; @@ -103,43 +105,191 @@ function initVisualConfig (): IVisualConfig { } } +type DeepReadonly> = { + readonly [K in keyof T]: T[K] extends Record ? DeepReadonly : T[K]; +}; + interface IVisSpec { readonly visId: string; - name?: [string, Record?]; - encodings: DraggableFieldState; - config: IVisualConfig; + readonly name?: [string, Record?]; + readonly encodings: DeepReadonly; + readonly config: DeepReadonly; } -class RevertibleVisSpec { +const MAX_HISTORY_SIZE = 20; + +class IVisSpecWithHistory { readonly visId: IVisSpec['visId']; + private snapshots: Pick[]; + private cursor: number; - constructor(initState: IVisSpec) { - this.visId = initState.visId; + constructor(data: IVisSpec) { + this.visId = data.visId; + this.snapshots = [{ + name: data.name, + encodings: data.encodings, + config: data.config, + }]; + this.cursor = 0; } - + + private get frame(): Readonly { + return { + visId: this.visId, + ...this.snapshots[this.cursor]!, + }; + } + + private batchFlag = false; + + private commit(snapshot: Partial>): void { + if (this.batchFlag) { + // batch this commit + this.snapshots[this.cursor] = toJS({ + ...this.frame, + ...snapshot, + }); + + return; + } + + this.batchFlag = true; + + this.snapshots = [ + ...this.snapshots.slice(0, this.cursor + 1), + toJS({ + ...this.frame, + ...snapshot, + }), + ]; + + if (this.snapshots.length > MAX_HISTORY_SIZE) { + this.snapshots.splice(0, 1); + } + + this.cursor = this.snapshots.length - 1; + + requestAnimationFrame(() => this.batchFlag = false); + } + + public get canUndo() { + return this.cursor > 0; + } + + public undo(): boolean { + if (this.cursor === 0) { + return false; + } + + this.cursor -= 1; + + return true; + } + + public get canRedo() { + return this.cursor < this.snapshots.length - 1; + } + + public redo(): boolean { + if (this.cursor === this.snapshots.length - 1) { + return false; + } + + this.cursor += 1; + + return true; + } + + public rebase() { + this.snapshots = [this.snapshots[this.cursor]]; + this.cursor = 0; + } + + get name() { + return this.frame.name; + } + + set name(name: IVisSpec['name']) { + this.commit({ + name, + }); + } + + get encodings(): DeepReadonly { + return this.frame.encodings; + } + + set encodings(encodings: IVisSpec['encodings']) { + this.commit({ + encodings, + }); + } + + get config(): DeepReadonly { + return this.frame.config; + } + + set config(config: IVisSpec['config']) { + this.commit({ + config, + }); + } + } export class VizSpecStore { // public fields: IViewField[] = []; private commonStore: CommonStore; - public draggableFieldState: DraggableFieldState; + /** + * This segment will always refers to the state of the active tab - + * `this.visList[this.visIndex].encodings`. + * Notice that observing rule of `this.visList` is `"shallow"` + * so mobx will NOT compare every deep value of `this.visList`, + * because the active tab is the only item in the list that may change. + * @readonly + * Assignment or mutable operations applied to ANY members of this segment + * is strictly FORBIDDEN. + * Members of it can only be got as READONLY objects. + * + * If you're trying to change the value of it and let mobx catch the action to trigger an update, + * please use `this.useMutable()` to access to a writable reference + * (an `immer` draft) of `this.visList[this.visIndex]`. + */ + public readonly draggableFieldState: DeepReadonly; private reactions: IReactionDisposer[] = [] - public visualConfig: IVisualConfig; - public visList: IVisSpec[] = []; + /** + * This segment will always refers to the state of the active tab - + * `this.visList[this.visIndex].config`. + * Notice that observing rule of `this.visList` is `"shallow"` + * so mobx will NOT compare every deep value of `this.visList`, + * because the active tab is the only item in the list that may change. + * @readonly + * Assignment or mutable operations applied to ANY members of this segment + * is strictly FORBIDDEN. + * Members of it can only be got as READONLY objects. + * + * If you're trying to change the value of it and let mobx catch the action to trigger an update, + * please use `this.useMutable()` to access to a writable reference + * (an `immer` draft) of `this.visList[this.visIndex]`. + */ + public readonly visualConfig: Readonly; + public visList: IVisSpecWithHistory[] = []; public visIndex: number = 0; + public canUndo = false; + public canRedo = false; constructor (commonStore: CommonStore) { this.commonStore = commonStore; this.draggableFieldState = initEncoding(); this.visualConfig = initVisualConfig(); - this.visList.push({ + this.visList.push(new IVisSpecWithHistory({ name: ['main.tablist.autoTitle', { idx: 1 }], visId: uuidv4(), config: this.visualConfig, encodings: this.draggableFieldState, - }) + })); makeAutoObservable(this, { - visList: observable.shallow + visList: observable.shallow, }); // FIXME!!!!! this.reactions.push( @@ -147,14 +297,67 @@ export class VizSpecStore { this.initState(); this.initMetaState(dataset); }), - reaction(() => this.visList[this.visIndex].config, curVisCfg => { - this.visualConfig = curVisCfg; - }), - reaction(() => this.visList[this.visIndex].encodings, curEncodings => { - this.draggableFieldState = curEncodings; - }), ); } + /** + * Allow to change any deep member of `encodings` or `config` + * in the active tab `this.visList[this.visIndex]`. + * + * - `tab.encodings` + * + * A mutable reference of `this.draggableFieldState` + * + * - `tab.config` + * + * A mutable reference of `this.visualConfig` + */ + private useMutable( + cb: (tab: { + encodings: DraggableFieldState; + config: IVisualConfig; + }) => void, + ) { + const { encodings, config } = produce({ + encodings: this.visList[this.visIndex].encodings, + config: this.visList[this.visIndex].config, + }, draft => { cb(draft) }); // notice that cb() may unexpectedly returns a non-nullable value + + this.visList[this.visIndex].encodings = encodings; + this.visList[this.visIndex].config = config; + + this.canUndo = this.visList[this.visIndex].canUndo; + this.canRedo = this.visList[this.visIndex].canRedo; + + // @ts-ignore Allow assignment here to trigger watch + this.visualConfig = config; + // @ts-ignore Allow assignment here to trigger watch + this.draggableFieldState = encodings; + } + public undo() { + if (this.visList[this.visIndex]?.undo()) { + this.canUndo = this.visList[this.visIndex].canUndo; + this.canRedo = this.visList[this.visIndex].canRedo; + // @ts-ignore Allow assignment here to trigger watch + this.visualConfig = this.visList[this.visIndex].config; + // @ts-ignore Allow assignment here to trigger watch + this.draggableFieldState = this.visList[this.visIndex].encodings; + } + } + public redo() { + if (this.visList[this.visIndex]?.redo()) { + this.canUndo = this.visList[this.visIndex].canUndo; + this.canRedo = this.visList[this.visIndex].canRedo; + // @ts-ignore Allow assignment here to trigger watch + this.visualConfig = this.visList[this.visIndex].config; + // @ts-ignore Allow assignment here to trigger watch + this.draggableFieldState = this.visList[this.visIndex].encodings; + } + } + private freezeHistory() { + this.useMutable(() => { + this.visList[this.visIndex]?.rebase(); + }); + } /** * dimension fields in visualization */ @@ -184,61 +387,62 @@ export class VizSpecStore { return fields; } public addVisualization () { - this.visList.push({ + this.visList.push(new IVisSpecWithHistory({ name: ['main.tablist.autoTitle', { idx: this.visList.length + 1 }], visId: uuidv4(), config: initVisualConfig(), encodings: initEncoding() - }) + })); this.visIndex = this.visList.length - 1; } public selectVisualization (visIndex: number) { - this.visIndex = visIndex; + this.useMutable(() => { + this.visIndex = visIndex; + }); } public setVisName (visIndex: number, name: string) { - this.visList[visIndex] = { - ...this.visList[visIndex], - name: [name], - }; - } - /** - * FIXME: tmp - */ - public saveVisChange () { - this.visList[this.visIndex].config = toJS(this.visualConfig); - this.visList[this.visIndex].encodings = toJS(this.draggableFieldState); + this.useMutable(() => { + this.visList[visIndex].name = [name]; + }); } public initState () { - this.draggableFieldState = initEncoding(); + this.useMutable(tab => { + tab.encodings = initEncoding(); + this.freezeHistory(); + }); } public initMetaState (dataset: DataSet) { - this.draggableFieldState.fields = dataset.rawFields.map((f) => ({ - dragId: uuidv4(), - fid: f.fid, - name: f.name || f.fid, - aggName: f.analyticType === 'measure' ? 'sum' : undefined, - analyticType: f.analyticType, - semanticType: f.semanticType - })) - this.draggableFieldState.dimensions = dataset.rawFields - .filter(f => f.analyticType === 'dimension') - .map((f) => ({ - dragId: uuidv4(), - fid: f.fid, - name: f.name || f.fid, - semanticType: f.semanticType, - analyticType: f.analyticType, - })) - this.draggableFieldState.measures = dataset.rawFields - .filter(f => f.analyticType === 'measure') - .map((f) => ({ + this.useMutable(({ encodings }) => { + encodings.fields = dataset.rawFields.map((f) => ({ dragId: uuidv4(), fid: f.fid, name: f.name || f.fid, + aggName: f.analyticType === 'measure' ? 'sum' : undefined, analyticType: f.analyticType, - semanticType: f.semanticType, - aggName: 'sum' - })) + semanticType: f.semanticType + })); + encodings.dimensions = dataset.rawFields + .filter(f => f.analyticType === 'dimension') + .map((f) => ({ + dragId: uuidv4(), + fid: f.fid, + name: f.name || f.fid, + semanticType: f.semanticType, + analyticType: f.analyticType, + })); + encodings.measures = dataset.rawFields + .filter(f => f.analyticType === 'measure') + .map((f) => ({ + dragId: uuidv4(), + fid: f.fid, + name: f.name || f.fid, + analyticType: f.analyticType, + semanticType: f.semanticType, + aggName: 'sum' + })); + + this.freezeHistory(); + }); // this.draggableFieldState.measures.push({ // dragId: uuidv4(), // fid: COUNT_FIELD_ID, @@ -249,25 +453,31 @@ export class VizSpecStore { // }) } public clearState () { - for (let key in this.draggableFieldState) { - if (!MetaFieldKeys.includes(key as keyof DraggableFieldState)) { - this.draggableFieldState[key] = [] + this.useMutable(({ encodings }) => { + for (let key in encodings) { + if (!MetaFieldKeys.includes(key as keyof DraggableFieldState)) { + encodings[key] = []; + } } - } + }); } - public setVisualConfig (configKey: keyof IVisualConfig, value: any) { - // this.visualConfig[configKey] = //value; - if (configKey === 'defaultAggregated' || configKey === 'defaultStack' || configKey === 'showActions' || configKey === 'interactiveScale') { - this.visualConfig[configKey] = Boolean(value); - } else if (configKey === 'geoms' && value instanceof Array) { - this.visualConfig[configKey] = value; - } else if (configKey === 'size' && value instanceof Object) { - this.visualConfig[configKey] = value; - } else if (configKey === 'sorted') { - this.visualConfig[configKey] = value; - } else { - console.error('unknow key' + configKey) - } + public setVisualConfig(configKey: K, value: IVisualConfig[K]) { + this.useMutable(({ config }) => { + switch (true) { + case ['defaultAggregated', 'defaultStack', 'showActions', 'interactiveScale'].includes(configKey): { + return (config as unknown as {[k: string]: boolean})[configKey] = Boolean(value); + } + case configKey === 'geoms' && Array.isArray(value): + case configKey === 'size' && typeof value === 'object': + case configKey === 'sorted': + { + return config[configKey] = value; + } + default: { + console.error('unknown key' + configKey); + } + } + }); } public transformCoord (coord: 'cartesian' | 'polar') { if (coord === 'polar') { @@ -275,81 +485,103 @@ export class VizSpecStore { } } public setChartLayout(props: {mode: IVisualConfig['size']['mode'], width?: number, height?: number }) { - const { - mode = this.visualConfig.size.mode, - width = this.visualConfig.size.width, - height = this.visualConfig.size.height - } = props - this.visualConfig.size.mode = mode; - this.visualConfig.size.width = width; - this.visualConfig.size.height = height; + this.useMutable(({ config }) => { + const { + mode = config.size.mode, + width = config.size.width, + height = config.size.height + } = props; + + config.size.mode = mode; + config.size.width = width; + config.size.height = height; + }); } public reorderField(stateKey: keyof DraggableFieldState, sourceIndex: number, destinationIndex: number) { if (MetaFieldKeys.includes(stateKey)) return; if (sourceIndex === destinationIndex) return; - const fields = this.draggableFieldState[stateKey]; - const [field] = fields.splice(sourceIndex, 1); - fields.splice(destinationIndex, 0, field); + + this.useMutable(({ encodings }) => { + const fields = encodings[stateKey]; + const [field] = fields.splice(sourceIndex, 1); + fields.splice(destinationIndex, 0, field); + }); } public moveField(sourceKey: keyof DraggableFieldState, sourceIndex: number, destinationKey: keyof DraggableFieldState, destinationIndex: number) { - let movingField: IViewField; - // 来源是不是metafield,是->clone;不是->直接删掉 - if (MetaFieldKeys.includes(sourceKey)) { - // use toJS for cloning - movingField = toJS(this.draggableFieldState[sourceKey][sourceIndex]) - movingField.dragId = uuidv4(); - } else { - [movingField] = this.draggableFieldState[sourceKey].splice(sourceIndex, 1); - } - // 目的地是metafields的情况,只有在来源也是metafields时,会执行字段类型转化操作 - if (MetaFieldKeys.includes(destinationKey)) { - if (!MetaFieldKeys.includes(sourceKey))return; - this.draggableFieldState[sourceKey].splice(sourceIndex, 1); - movingField.analyticType = destinationKey === 'dimensions' ? 'dimension' : 'measure'; - } - const limitSize = getChannelSizeLimit(destinationKey); - const fixedDestinationIndex = Math.min(destinationIndex, limitSize - 1); - const overflowSize = Math.max(0, this.draggableFieldState[destinationKey].length + 1 - limitSize); - this.draggableFieldState[destinationKey].splice(fixedDestinationIndex, overflowSize, movingField) + this.useMutable(({ encodings }) => { + let movingField: IViewField; + // 来源是不是metafield,是->clone;不是->直接删掉 + if (MetaFieldKeys.includes(sourceKey)) { + // use toJS for cloning + movingField = toJS(encodings[sourceKey][sourceIndex]) + movingField.dragId = uuidv4(); + } else { + [movingField] = encodings[sourceKey].splice(sourceIndex, 1); + } + // 目的地是metafields的情况,只有在来源也是metafields时,会执行字段类型转化操作 + if (MetaFieldKeys.includes(destinationKey)) { + if (!MetaFieldKeys.includes(sourceKey))return; + encodings[sourceKey].splice(sourceIndex, 1); + movingField.analyticType = destinationKey === 'dimensions' ? 'dimension' : 'measure'; + } + const limitSize = getChannelSizeLimit(destinationKey); + const fixedDestinationIndex = Math.min(destinationIndex, limitSize - 1); + const overflowSize = Math.max(0, encodings[destinationKey].length + 1 - limitSize); + encodings[destinationKey].splice(fixedDestinationIndex, overflowSize, movingField); + }); } public removeField(sourceKey: keyof DraggableFieldState, sourceIndex: number) { if (MetaFieldKeys.includes(sourceKey))return; - this.draggableFieldState[sourceKey].splice(sourceIndex, 1); + + this.useMutable(({ encodings }) => { + const fields = encodings[sourceKey]; + fields.splice(sourceIndex, 1); + }); } public transpose() { - const fieldsInCup = this.draggableFieldState.columns; - this.draggableFieldState.columns = this.draggableFieldState.rows; - this.draggableFieldState.rows = fieldsInCup; + this.useMutable(({ encodings }) => { + const fieldsInCup = encodings.columns; + + encodings.columns = encodings.rows; + encodings.rows = fieldsInCup as typeof encodings.rows; // assume this as writable + }); } public createBinField(stateKey: keyof DraggableFieldState, index: number) { - const originField = this.draggableFieldState[stateKey][index] - const binField: IViewField = { - fid: uuidv4(), - dragId: uuidv4(), - name: `bin(${originField.name})`, - semanticType: 'ordinal', - analyticType: 'dimension', - }; - this.draggableFieldState.dimensions.push(binField); - this.commonStore.currentDataset.dataSource = makeBinField(this.commonStore.currentDataset.dataSource, originField.fid, binField.fid) + this.useMutable(({ encodings }) => { + const originField = encodings[stateKey][index] + const binField: IViewField = { + fid: uuidv4(), + dragId: uuidv4(), + name: `bin(${originField.name})`, + semanticType: 'ordinal', + analyticType: 'dimension', + }; + encodings.dimensions.push(binField); + this.commonStore.currentDataset.dataSource = makeBinField(this.commonStore.currentDataset.dataSource, originField.fid, binField.fid) + }); } public createLogField(stateKey: keyof DraggableFieldState, index: number) { - const originField = this.draggableFieldState[stateKey][index]; - const logField: IViewField = { - fid: uuidv4(), - dragId: uuidv4(), - name: `log10(${originField.name})`, - semanticType: 'quantitative', - analyticType: originField.analyticType - } - this.draggableFieldState[stateKey].push(logField); - this.commonStore.currentDataset.dataSource = makeLogField(this.commonStore.currentDataset.dataSource, originField.fid, logField.fid) + this.useMutable(({ encodings }) => { + const originField = encodings[stateKey][index]; + const logField: IViewField = { + fid: uuidv4(), + dragId: uuidv4(), + name: `log10(${originField.name})`, + semanticType: 'quantitative', + analyticType: originField.analyticType + }; + encodings[stateKey].push(logField); + this.commonStore.currentDataset.dataSource = makeLogField(this.commonStore.currentDataset.dataSource, originField.fid, logField.fid) + }); } public setFieldAggregator (stateKey: keyof DraggableFieldState, index: number, aggName: string) { - const fields = this.draggableFieldState[stateKey] - if (fields[index]) { - fields[index].aggName = aggName; - } + this.useMutable(({ encodings }) => { + const fields = encodings[stateKey]; + + if (fields[index]) { + encodings[stateKey][index].aggName = aggName; + } + }); } public get sortCondition () { const { rows, columns } = this.draggableFieldState; @@ -364,57 +596,65 @@ export class VizSpecStore { return false; } public setFieldSort (stateKey: keyof DraggableFieldState, index: number, sortType: 'none' | 'ascending' | 'descending') { - this.draggableFieldState[stateKey][index].sort = sortType; + this.useMutable(({ encodings }) => { + encodings[stateKey][index].sort = sortType; + }); } public applyDefaultSort(sortType: 'none' | 'ascending' | 'descending' = 'ascending') { - const { rows, columns } = this.draggableFieldState; - const yField = rows.length > 0 ? rows[rows.length - 1] : null; - const xField = columns.length > 0 ? columns[columns.length - 1] : null; - - if (xField !== null && xField.analyticType === 'dimension' && yField !== null && yField.analyticType === 'measure') { - xField.sort = sortType - return - } - if (xField !== null && xField.analyticType === 'measure' && yField !== null && yField.analyticType === 'dimension') { - yField.sort = sortType - return - } + this.useMutable(({ encodings }) => { + const { rows, columns } = encodings; + const yField = rows.length > 0 ? rows[rows.length - 1] : null; + const xField = columns.length > 0 ? columns[columns.length - 1] : null; + + if (xField !== null && xField.analyticType === 'dimension' && yField !== null && yField.analyticType === 'measure') { + encodings.columns[columns.length - 1].sort = sortType; + return; + } + if (xField !== null && xField.analyticType === 'measure' && yField !== null && yField.analyticType === 'dimension') { + encodings.rows[rows.length - 1].sort = sortType; + return; + } + }); } public appendField (destinationKey: keyof DraggableFieldState, field: IViewField | undefined) { if (MetaFieldKeys.includes(destinationKey)) return; if (typeof field === 'undefined') return; - const cloneField = toJS(field); - cloneField.dragId = uuidv4(); - this.draggableFieldState[destinationKey].push(cloneField); + + this.useMutable(({ encodings }) => { + const cloneField = toJS(field); + cloneField.dragId = uuidv4(); + encodings[destinationKey].push(cloneField); + }); } public renderSpec (spec: Specification) { - const fields = this.draggableFieldState.fields; - // thi - // const [xField, yField, ] = spec.position; - this.clearState(); - if (spec.geomType && spec.geomType.length > 0) { - this.setVisualConfig('geoms', spec.geomType.map(g => geomAdapter(g))); - } - if (spec.facets && spec.facets.length > 0) { - const facets = (spec.facets || []).concat(spec.highFacets || []); - for (let facet of facets) { - this.appendField('rows', fields.find(f => f.fid === facet)); + this.useMutable(tab => { + const fields = tab.encodings.fields; + // thi + // const [xField, yField, ] = spec.position; + this.clearState(); + if (spec.geomType && spec.geomType.length > 0) { + this.setVisualConfig('geoms', spec.geomType.map(g => geomAdapter(g))); } - } - if (spec.position) { - if (spec.position.length > 0) this.appendField('columns', fields.find(f => f.fid === spec.position![0])); - if (spec.position.length > 1) this.appendField('rows', fields.find(f => f.fid === spec.position![1])); - } - if (spec.color && spec.color.length > 0) { - this.appendField('color', fields.find(f => f.fid === spec.color![0])); - } - if (spec.size && spec.size.length > 0) { - this.appendField('size', fields.find(f => f.fid === spec.size![0])); - } - if (spec.opacity && spec.opacity.length > 0) { - this.appendField('opacity', fields.find(f => f.fid === spec.opacity![0])); - } - + if (spec.facets && spec.facets.length > 0) { + const facets = (spec.facets || []).concat(spec.highFacets || []); + for (let facet of facets) { + this.appendField('rows', fields.find(f => f.fid === facet)); + } + } + if (spec.position) { + if (spec.position.length > 0) this.appendField('columns', fields.find(f => f.fid === spec.position![0])); + if (spec.position.length > 1) this.appendField('rows', fields.find(f => f.fid === spec.position![1])); + } + if (spec.color && spec.color.length > 0) { + this.appendField('color', fields.find(f => f.fid === spec.color![0])); + } + if (spec.size && spec.size.length > 0) { + this.appendField('size', fields.find(f => f.fid === spec.size![0])); + } + if (spec.opacity && spec.opacity.length > 0) { + this.appendField('opacity', fields.find(f => f.fid === spec.opacity![0])); + } + }); } public destroy () { this.reactions.forEach(rec => { diff --git a/packages/graphic-walker/src/visualSettings/menubar.tsx b/packages/graphic-walker/src/visualSettings/menubar.tsx new file mode 100644 index 00000000..fb7c0110 --- /dev/null +++ b/packages/graphic-walker/src/visualSettings/menubar.tsx @@ -0,0 +1,99 @@ +import { BarsArrowDownIcon, BarsArrowUpIcon } from '@heroicons/react/24/outline'; +import { observer } from 'mobx-react-lite'; +import React from 'react'; +import { LiteForm } from '../components/liteForm'; +import SizeSetting from '../components/sizeSetting'; +import { CHART_LAYOUT_TYPE, GEMO_TYPES } from '../config'; +import { useGlobalStore } from '../store'; +import styled from 'styled-components' +import { ArrowPathIcon } from '@heroicons/react/24/solid'; +import { useTranslation } from 'react-i18next'; + + +export const MenubarContainer = styled.div({ + marginBlock: '0 0.6em', + marginInline: '0.2em', +}); + +const Button = styled.button(({ disabled = false }) => ({ + '&:hover': disabled ? {} : { + backgroundColor: 'rgba(243, 244, 246, 0.5)', + }, + color: disabled ? 'rgb(156, 163, 175)' : 'rgb(55, 65, 81)', + '& > pre': { + display: 'inline-block', + marginInlineStart: '0.2em', + }, + marginInlineStart: '0.6em', + '&:first-child': { + marginInlineStart: '0', + }, + cursor: disabled ? 'default' : 'pointer', +})); + +interface ButtonWithShortcutProps { + label: string; + shortcut: string; + disabled: boolean; + handler: () => void; +} + +const ButtonWithShortcut: React.FC = ({ label, shortcut, disabled, handler }) => { + const { t } = useTranslation('translation', { keyPrefix: 'main.tabpanel.menubar' }); + + React.useEffect(() => { + const cb = (ev: KeyboardEvent) => { + if (ev.key === shortcut.toLowerCase()) { + handler(); + ev.stopPropagation(); + } + }; + + document.body.addEventListener('keydown', cb); + + return () => document.body.removeEventListener('keydown', cb); + }, [shortcut, handler]); + + return ( + + ); +}; + +const Menubar: React.FC = () => { + const { vizStore } = useGlobalStore(); + const { canUndo, canRedo } = vizStore; + + return ( + + + + + ); +} + +export default observer(Menubar); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 58985257..3944cbcf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1302,6 +1302,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.14.5", "@babel/runtime@^7.17.2", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.9": + version "7.19.0" + resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259" + integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/runtime@^7.14.8", "@babel/runtime@^7.8.7": version "7.15.4" resolved "https://registry.nlark.com/@babel/runtime/download/@babel/runtime-7.15.4.tgz?cache=0&sync_timestamp=1630619277795&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40babel%2Fruntime%2Fdownload%2F%40babel%2Fruntime-7.15.4.tgz#fd17d16bfdf878e6dd02d19753a39fa8a8d9c84a" @@ -1309,13 +1316,6 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.18.3": - version "7.19.0" - resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259" - integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA== - dependencies: - regenerator-runtime "^0.13.4" - "@babel/template@^7.10.4", "@babel/template@^7.14.5", "@babel/template@^7.3.3": version "7.14.5" resolved "https://registry.nlark.com/@babel/template/download/@babel/template-7.14.5.tgz?cache=0&sync_timestamp=1623280386138&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40babel%2Ftemplate%2Fdownload%2F%40babel%2Ftemplate-7.14.5.tgz#a9bc9d8b33354ff6e55a9c60d1109200a68974f4" @@ -7780,6 +7780,11 @@ immer@8.0.1: resolved "https://registry.nlark.com/immer/download/immer-8.0.1.tgz?cache=0&sync_timestamp=1623232631798&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fimmer%2Fdownload%2Fimmer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656" integrity sha1-nHPbaD4rOXXEJPsFcq9YiYd65lY= +immer@^9.0.15: + version "9.0.15" + resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.15.tgz#0b9169e5b1d22137aba7d43f8a81a495dd1b62dc" + integrity sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ== + immer@^9.0.6: version "9.0.6" resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.6.tgz#7a96bf2674d06c8143e327cbf73539388ddf1a73" @@ -11803,6 +11808,14 @@ react-error-overlay@^6.0.9: resolved "https://registry.nlark.com/react-error-overlay/download/react-error-overlay-6.0.9.tgz?cache=0&sync_timestamp=1618847933355&other_urls=https%3A%2F%2Fregistry.nlark.com%2Freact-error-overlay%2Fdownload%2Freact-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a" integrity sha1-PHQwEMk1lgjDdezWvHbzXZOZWwo= +react-i18next@^11.18.6: + version "11.18.6" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.18.6.tgz#e159c2960c718c1314f1e8fcaa282d1c8b167887" + integrity sha512-yHb2F9BiT0lqoQDt8loZ5gWP331GwctHz9tYQ8A2EIEUu+CcEdjBLQWli1USG3RdWQt3W+jqQLg/d4rrQR96LA== + dependencies: + "@babel/runtime" "^7.14.5" + html-parse-stringify "^3.0.1" + react-intl-universal@^2.6.6: version "2.6.6" resolved "https://registry.npmmirror.com/react-intl-universal/-/react-intl-universal-2.6.6.tgz#8212cdd052544455c2d95aa1924f02c31e9097ce" @@ -15290,11 +15303,6 @@ y18n@^5.0.5: resolved "https://registry.nlark.com/y18n/download/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha1-f0k00PfKjFb5UxSTndzS3ZHOHVU= -yallist@*, yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - yallist@^2.1.2: version "2.1.2" resolved "https://registry.nlark.com/yallist/download/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" @@ -15305,6 +15313,11 @@ yallist@^3.0.2: resolved "https://registry.nlark.com/yallist/download/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha1-27fa+b/YusmrRev2ArjLrQ1dCP0= +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2: version "1.10.2" resolved "https://registry.nlark.com/yaml/download/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" From 08c8f9195811d67c380b1bd3589978171031d1a0 Mon Sep 17 00:00:00 2001 From: kyusho Date: Mon, 19 Sep 2022 15:52:13 +0800 Subject: [PATCH 3/7] fix(g_walker): Fixed types of readonly params. --- packages/graphic-walker/src/insightBoard/index.tsx | 1 + packages/graphic-walker/src/insightBoard/mainBoard.tsx | 2 +- packages/graphic-walker/src/insightBoard/std2vegaSpec.ts | 2 +- packages/graphic-walker/src/insightBoard/utils.ts | 2 +- packages/graphic-walker/src/vis/react-vega.tsx | 4 ++-- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/graphic-walker/src/insightBoard/index.tsx b/packages/graphic-walker/src/insightBoard/index.tsx index d173bca2..3147d09a 100644 --- a/packages/graphic-walker/src/insightBoard/index.tsx +++ b/packages/graphic-walker/src/insightBoard/index.tsx @@ -2,6 +2,7 @@ import { toJS } from 'mobx'; import { observer } from 'mobx-react-lite'; import React, { useCallback } from 'react'; import Modal from '../components/modal'; +import type { IField } from '../interfaces'; import { useGlobalStore } from '../store'; import InsightMainBoard from './mainBoard'; diff --git a/packages/graphic-walker/src/insightBoard/mainBoard.tsx b/packages/graphic-walker/src/insightBoard/mainBoard.tsx index 06fa95b8..bb6d84ff 100644 --- a/packages/graphic-walker/src/insightBoard/mainBoard.tsx +++ b/packages/graphic-walker/src/insightBoard/mainBoard.tsx @@ -26,7 +26,7 @@ interface SubSpace { interface InsightMainBoardProps { dataSource: IRow[]; - fields: IField[]; + fields: Readonly; filters?: Filters; viewDs: IField[]; viewMs: IField[]; diff --git a/packages/graphic-walker/src/insightBoard/std2vegaSpec.ts b/packages/graphic-walker/src/insightBoard/std2vegaSpec.ts index 0f63d1f5..061030b0 100644 --- a/packages/graphic-walker/src/insightBoard/std2vegaSpec.ts +++ b/packages/graphic-walker/src/insightBoard/std2vegaSpec.ts @@ -17,7 +17,7 @@ export function baseVis( measures: string[], predicates: IPredicate[] | null, aggregatedMeasures: Array<{ op: string; field: string; as: string }>, - fields: IField[], + fields: Readonly, type: IReasonType, defaultAggregated?: boolean, defaultStack?: boolean diff --git a/packages/graphic-walker/src/insightBoard/utils.ts b/packages/graphic-walker/src/insightBoard/utils.ts index 70230b5a..abd3ad8f 100644 --- a/packages/graphic-walker/src/insightBoard/utils.ts +++ b/packages/graphic-walker/src/insightBoard/utils.ts @@ -26,7 +26,7 @@ export function mergeMeasures(measures1: IMeasure[], measures2: IMeasure[]): IMe return merged; } -export function formatFieldName(fid: string, fields: IField[]) { +export function formatFieldName(fid: string, fields: Readonly) { const target = fields.find(f => f.fid === fid); return target ? target.name : fid; } \ No newline at end of file diff --git a/packages/graphic-walker/src/vis/react-vega.tsx b/packages/graphic-walker/src/vis/react-vega.tsx index 99c547b5..640bd978 100644 --- a/packages/graphic-walker/src/vis/react-vega.tsx +++ b/packages/graphic-walker/src/vis/react-vega.tsx @@ -17,8 +17,8 @@ const CanvaContainer = styled.div<{rowSize: number; colSize: number;}>` const SELECTION_NAME = 'geom'; interface ReactVegaProps { - rows: IViewField[]; - columns: IViewField[]; + rows: Readonly; + columns: Readonly; dataSource: IRow[]; defaultAggregate?: boolean; defaultStack?: boolean; From a22cefd250e404e7d160f3baa46d1100ca4c5efb Mon Sep 17 00:00:00 2001 From: kyusho Date: Mon, 19 Sep 2022 16:21:39 +0800 Subject: [PATCH 4/7] doc(g_walker): Appended description in 'Customize I18n'. --- packages/graphic-walker/README.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/graphic-walker/README.md b/packages/graphic-walker/README.md index b7ded9ea..bafe445d 100644 --- a/packages/graphic-walker/README.md +++ b/packages/graphic-walker/README.md @@ -1,7 +1,7 @@ # Graphic Walker -Graphic Walker is a lite tableau style visual analysis interface. It is used for cases when users have specific analytic target or user want to analysis further result based on the recommanded results by Rath's auto insights. +Graphic Walker is a lite tableau style visual analysis interface. It is used for cases when users have specific analytic target or user want to analysis further result based on the recommended results by Rath's auto insights. -** You can also use Graphic Walker as a lite tableau style analysis app independently. It can be used as an independent app or an embeding module. ** +** You can also use Graphic Walker as a lite tableau style analysis app independently. It can be used as an independent app or an embedding module. ** Main features: @@ -45,7 +45,7 @@ npm run dev ## I18n Support -Graphic Walker now support _English_ (as `"en"` or `"en-US"`) and _Chinese_ (as `"zh"` or `"zh-CN"`) with built-in locale resources. You can simply provide a valid string value (enumerated above) as `props.i18nLang` to set a language or synchronize your global i18n language with the component like the example given as follow. +GraphicWalker now support _English_ (as `"en"` or `"en-US"`) and _Chinese_ (as `"zh"` or `"zh-CN"`) with built-in locale resources. You can simply provide a valid string value (enumerated above) as `props.i18nLang` to set a language or synchronize your global i18n language with the component like the example given as follow. ```typescript const YourApp = props => { @@ -63,14 +63,15 @@ const YourApp = props => { ### Customize I18n -If you need i18n support to cover languages not supported currently, or to totally rewrite the content of any built-in resource(s), you can also provide your resource(s) as `props.i18nResources` to Graphic Walker like this. +If you need i18n support to cover languages not supported currently, or to totally rewrite the content of any built-in resource(s), you can also provide your resource(s) as `props.i18nResources` to GraphicWalker like this. ```typescript const yourResources = { 'de-DE': { + 'key': 'value', ... }, - 'fr-FE': { + 'fr-FR': { ... }, }; @@ -88,3 +89,5 @@ const YourApp = props => { /> } ``` + +GraphicWalker uses `react-i18next` to support i18n, which is based on `i18next`, so your translation resources should follow [this format](https://www.i18next.com/misc/json-format). You can simply fork and edit `/locales/en-US.json` to start your translation. From c3aa7425a9bd3351034eb5500e1540791dbf673c Mon Sep 17 00:00:00 2001 From: kyusho Date: Mon, 19 Sep 2022 16:36:26 +0800 Subject: [PATCH 5/7] chore(g_walker): Removed unused import in insightBoard/index.tsx --- packages/graphic-walker/src/insightBoard/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/graphic-walker/src/insightBoard/index.tsx b/packages/graphic-walker/src/insightBoard/index.tsx index 3147d09a..d173bca2 100644 --- a/packages/graphic-walker/src/insightBoard/index.tsx +++ b/packages/graphic-walker/src/insightBoard/index.tsx @@ -2,7 +2,6 @@ import { toJS } from 'mobx'; import { observer } from 'mobx-react-lite'; import React, { useCallback } from 'react'; import Modal from '../components/modal'; -import type { IField } from '../interfaces'; import { useGlobalStore } from '../store'; import InsightMainBoard from './mainBoard'; From 14b6e28a6278d46e6b2944bcbb2fc67f1d883d5d Mon Sep 17 00:00:00 2001 From: kyusho Date: Mon, 19 Sep 2022 18:21:31 +0800 Subject: [PATCH 6/7] fix(g_walker): Added missing reaction for changing of the active tab. --- .../src/store/visualSpecStore.ts | 24 ++++++--- .../src/visualSettings/menubar.tsx | 51 +++++++++++-------- 2 files changed, 47 insertions(+), 28 deletions(-) diff --git a/packages/graphic-walker/src/store/visualSpecStore.ts b/packages/graphic-walker/src/store/visualSpecStore.ts index 54942b40..87a1da76 100644 --- a/packages/graphic-walker/src/store/visualSpecStore.ts +++ b/packages/graphic-walker/src/store/visualSpecStore.ts @@ -163,6 +163,8 @@ class IVisSpecWithHistory { ...snapshot, }), ]; + console.log('commit', snapshot); + console.trace(); if (this.snapshots.length > MAX_HISTORY_SIZE) { this.snapshots.splice(0, 1); @@ -297,6 +299,14 @@ export class VizSpecStore { this.initState(); this.initMetaState(dataset); }), + reaction(() => this.visList[this.visIndex], frame => { + // @ts-ignore Allow assignment here to trigger watch + this.draggableFieldState = frame.encodings; + // @ts-ignore Allow assignment here to trigger watch + this.visualConfig = frame.config; + this.canUndo = frame.canUndo; + this.canRedo = frame.canRedo; + }), ); } /** @@ -354,9 +364,9 @@ export class VizSpecStore { } } private freezeHistory() { - this.useMutable(() => { - this.visList[this.visIndex]?.rebase(); - }); + this.visList[this.visIndex]?.rebase(); + this.canUndo = this.visList[this.visIndex].canUndo; + this.canRedo = this.visList[this.visIndex].canRedo; } /** * dimension fields in visualization @@ -396,9 +406,7 @@ export class VizSpecStore { this.visIndex = this.visList.length - 1; } public selectVisualization (visIndex: number) { - this.useMutable(() => { - this.visIndex = visIndex; - }); + this.visIndex = visIndex; } public setVisName (visIndex: number, name: string) { this.useMutable(() => { @@ -440,9 +448,9 @@ export class VizSpecStore { semanticType: f.semanticType, aggName: 'sum' })); - - this.freezeHistory(); }); + + this.freezeHistory(); // this.draggableFieldState.measures.push({ // dragId: uuidv4(), // fid: COUNT_FIELD_ID, diff --git a/packages/graphic-walker/src/visualSettings/menubar.tsx b/packages/graphic-walker/src/visualSettings/menubar.tsx index fb7c0110..e778b827 100644 --- a/packages/graphic-walker/src/visualSettings/menubar.tsx +++ b/packages/graphic-walker/src/visualSettings/menubar.tsx @@ -1,12 +1,8 @@ -import { BarsArrowDownIcon, BarsArrowUpIcon } from '@heroicons/react/24/outline'; import { observer } from 'mobx-react-lite'; import React from 'react'; -import { LiteForm } from '../components/liteForm'; -import SizeSetting from '../components/sizeSetting'; -import { CHART_LAYOUT_TYPE, GEMO_TYPES } from '../config'; import { useGlobalStore } from '../store'; import styled from 'styled-components' -import { ArrowPathIcon } from '@heroicons/react/24/solid'; +import { ArrowUturnLeftIcon, ArrowUturnRightIcon } from '@heroicons/react/24/solid'; import { useTranslation } from 'react-i18next'; @@ -19,7 +15,8 @@ const Button = styled.button(({ disabled = false }) => ({ '&:hover': disabled ? {} : { backgroundColor: 'rgba(243, 244, 246, 0.5)', }, - color: disabled ? 'rgb(156, 163, 175)' : 'rgb(55, 65, 81)', + color: disabled ? 'rgba(156, 163, 175, 0.5)' : 'rgb(55, 65, 81)', + boxShadow: disabled ? undefined : '1px 1px 2px #0002, inset 2px 2px 4px #0001', '& > pre': { display: 'inline-block', marginInlineStart: '0.2em', @@ -36,14 +33,33 @@ interface ButtonWithShortcutProps { shortcut: string; disabled: boolean; handler: () => void; + icon?: JSX.Element; } -const ButtonWithShortcut: React.FC = ({ label, shortcut, disabled, handler }) => { +const ButtonWithShortcut: React.FC = ({ label, shortcut, disabled, handler, icon }) => { const { t } = useTranslation('translation', { keyPrefix: 'main.tabpanel.menubar' }); + const rule = React.useMemo(() => { + const keys = shortcut.split('+').map(d => d.trim()); + + return { + key: keys.filter( + d => /^[a-z]$/i.test(d) + )[0], + ctrlKey: keys.includes('Ctrl'), + shiftKey: keys.includes('Shift'), + altKey: keys.includes('Alt'), + }; + }, [shortcut]); + React.useEffect(() => { const cb = (ev: KeyboardEvent) => { - if (ev.key === shortcut.toLowerCase()) { + if ( + ev.ctrlKey === rule.ctrlKey + && ev.shiftKey === rule.shiftKey + && ev.altKey === rule.altKey + && ev.key.toLowerCase() === rule.key.toLowerCase() + ) { handler(); ev.stopPropagation(); } @@ -52,7 +68,7 @@ const ButtonWithShortcut: React.FC = ({ label, shortcut document.body.addEventListener('keydown', cb); return () => document.body.removeEventListener('keydown', cb); - }, [shortcut, handler]); + }, [rule, handler]); return ( ); }; @@ -84,13 +93,15 @@ const Menubar: React.FC = () => { label="undo" disabled={!canUndo} handler={vizStore.undo.bind(vizStore)} - shortcut="U" + shortcut="Ctrl+Z" + icon={} /> } /> ); From 372d57b3658d01e570b433763cec4600b8a17346 Mon Sep 17 00:00:00 2001 From: kyusho Date: Mon, 19 Sep 2022 18:26:36 +0800 Subject: [PATCH 7/7] fix(g_walker): Removed console.log in store. --- packages/graphic-walker/src/store/visualSpecStore.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/graphic-walker/src/store/visualSpecStore.ts b/packages/graphic-walker/src/store/visualSpecStore.ts index 87a1da76..791c8d4d 100644 --- a/packages/graphic-walker/src/store/visualSpecStore.ts +++ b/packages/graphic-walker/src/store/visualSpecStore.ts @@ -163,8 +163,6 @@ class IVisSpecWithHistory { ...snapshot, }), ]; - console.log('commit', snapshot); - console.trace(); if (this.snapshots.length > MAX_HISTORY_SIZE) { this.snapshots.splice(0, 1);