Skip to content

Commit

Permalink
fix: Undo / redo behavior on LG resources (#1813)
Browse files Browse the repository at this point in the history
* inline lg editor undo and redo

* add forceUpdate for undo/redo

* support the lg undo

* update the lg to store directly

* fix unit test

* fix some comments

* register api to extension

* add some comments

* update the sync logic

* revert some code

* update the type

Co-authored-by: Dong Lei <donglei@microsoft.com>
Co-authored-by: Chris Whitten <christopher.whitten@microsoft.com>
  • Loading branch information
3 people committed Jan 21, 2020
1 parent 261cfb8 commit 1688eb1
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 42 deletions.
14 changes: 12 additions & 2 deletions Composer/packages/client/__tests__/store/reducer/reducer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,18 @@ describe('test all reducer handlers', () => {
expect(result.dialogs[0]).toBe('test dialogs');
});
it('test updateLgTemplate reducer', () => {
const result = reducer({}, { type: ActionTypes.UPDATE_LG_SUCCESS, payload: { response: mockResponse } });
expect(result.lgFiles[0]).toBe('test lgFiles');
const result = reducer(
{ lgFiles: [{ id: 'common.lg', content: 'test lgFiles' }] },
{
type: ActionTypes.UPDATE_LG_SUCCESS,
payload: {
id: 'common.lg',
content: ` # bfdactivity-003038
- You said '@{turn.activity.text}'`,
},
}
);
expect(result.lgFiles[0].templates.length).toBe(1);
});

it('test getStorageFileSuccess reducer', () => {
Expand Down
59 changes: 41 additions & 18 deletions Composer/packages/client/src/store/action/lg.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,51 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import clonedeep from 'lodash/cloneDeep';
import debounce from 'lodash/debounce';

import { ActionTypes } from '../../constants';
import httpClient from '../../utils/httpUtil';
import * as lgUtil from '../../utils/lgUtil';
import { ActionCreator } from '../types';
import { undoable } from '../middlewares/undo';
import { ActionCreator, State } from '../types';

import { ActionTypes } from './../../constants';
import httpClient from './../../utils/httpUtil';
import { fetchProject } from './project';
import { setError } from './error';

export const updateLgFile: ActionCreator = async ({ dispatch }, { id, content }) => {
//remove editor's debounce and add it to action
export const debouncedUpdateLg = debounce(async (store, id, content) => {
try {
const response = await httpClient.put(`/projects/opened/lgFiles/${id}`, { id, content });
dispatch({
type: ActionTypes.UPDATE_LG_SUCCESS,
payload: { response },
});
await httpClient.put(`/projects/opened/lgFiles/${id}`, { id, content });
} catch (err) {
dispatch({
type: ActionTypes.UPDATE_LG_FAILURE,
payload: null,
error: err,
setError(store, {
message: err.response && err.response.data.message ? err.response.data.message : err,
summary: 'UPDATE LG ERROR',
});
//if update lg error, do a full refresh.
fetchProject(store);
}
}, 500);

export const updateLgFile: ActionCreator = async (store, { id, content }) => {
store.dispatch({ type: ActionTypes.UPDATE_LG_SUCCESS, payload: { id, content } });
debouncedUpdateLg(store, id, content);
};

export const undoableUpdateLgFile = undoable(
updateLgFile,
(state: State, args: any[], isEmpty) => {
if (isEmpty) {
const id = args[0].id;
const content = clonedeep(state.lgFiles.find(lgFile => lgFile.id === id)?.content);
return [{ id, content }];
} else {
return args;
}
},
updateLgFile,
updateLgFile
);

export const createLgFile: ActionCreator = async ({ dispatch }, { id, content }) => {
try {
const response = await httpClient.post(`/projects/opened/lgFiles`, { id, content });
Expand Down Expand Up @@ -57,25 +80,25 @@ export const removeLgFile: ActionCreator = async ({ dispatch }, { id }) => {

export const updateLgTemplate: ActionCreator = async (store, { file, templateName, template }) => {
const newContent = lgUtil.updateTemplate(file.content, templateName, template);
return await updateLgFile(store, { id: file.id, content: newContent });
return await undoableUpdateLgFile(store, { id: file.id, content: newContent });
};

export const createLgTemplate: ActionCreator = async (store, { file, template }) => {
const newContent = lgUtil.addTemplate(file.content, template);
return await updateLgFile(store, { id: file.id, content: newContent });
return await undoableUpdateLgFile(store, { id: file.id, content: newContent });
};

export const removeLgTemplate: ActionCreator = async (store, { file, templateName }) => {
const newContent = lgUtil.removeTemplate(file.content, templateName);
return await updateLgFile(store, { id: file.id, content: newContent });
return await undoableUpdateLgFile(store, { id: file.id, content: newContent });
};

export const removeLgTemplates: ActionCreator = async (store, { file, templateNames }) => {
const newContent = lgUtil.removeTemplates(file.content, templateNames);
return await updateLgFile(store, { id: file.id, content: newContent });
return await undoableUpdateLgFile(store, { id: file.id, content: newContent });
};

export const copyLgTemplate: ActionCreator = async (store, { file, fromTemplateName, toTemplateName }) => {
const newContent = lgUtil.copyTemplate(file.content, fromTemplateName, toTemplateName);
return await updateLgFile(store, { id: file.id, content: newContent });
return await undoableUpdateLgFile(store, { id: file.id, content: newContent });
};
2 changes: 2 additions & 0 deletions Composer/packages/client/src/store/action/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ActionCreator } from './../types';
import { ActionTypes } from './../../constants';
import { updateBreadcrumb, navigateTo, checkUrl, getUrlSearch, BreadcrumbUpdateType } from './../../utils/navigation';
import { debouncedUpdateDialog } from './dialog';
import { debouncedUpdateLg } from './lg';

export const setDesignPageLocation: ActionCreator = (
{ dispatch },
Expand All @@ -25,6 +26,7 @@ export const navTo: ActionCreator = ({ getState }, dialogId, breadcrumb = []) =>
if (checkUrl(currentUri, state.designPageLocation)) return;
//if dialog change we should flush some debounced functions
debouncedUpdateDialog.flush();
debouncedUpdateLg.flush();
navigateTo(currentUri, { state: { breadcrumb } });
};

Expand Down
18 changes: 16 additions & 2 deletions Composer/packages/client/src/store/reducer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import get from 'lodash/get';
import set from 'lodash/set';
import { dialogIndexer } from '@bfc/indexers';
import { SensitiveProperties } from '@bfc/shared';
import { Diagnostic, DiagnosticSeverity, LgTemplate, lgIndexer } from '@bfc/indexers';

import { ActionTypes, FileTypes } from '../../constants';
import { DialogSetting, ReducerFunc } from '../types';
Expand Down Expand Up @@ -105,8 +106,21 @@ const createDialogSuccess: ReducerFunc = (state, { response }) => {
return state;
};

const updateLgTemplate: ReducerFunc = (state, { response }) => {
state.lgFiles = response.data.lgFiles;
const updateLgTemplate: ReducerFunc = (state, { id, content }) => {
state.lgFiles = state.lgFiles.map(lgFile => {
if (lgFile.id === id) {
const { check, parse } = lgIndexer;
const diagnostics = check(content, id);
let templates: LgTemplate[] = [];
try {
templates = parse(content, id);
} catch (err) {
diagnostics.push(new Diagnostic(err.message, id, DiagnosticSeverity.Error));
}
return { ...lgFile, templates, diagnostics };
}
return lgFile;
});
return state;
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import React, { useState, useMemo, useEffect } from 'react';
import React, { useState, useCallback, useEffect, useRef } from 'react';
import { LgEditor } from '@bfc/code-editor';
import { LgMetaData, LgTemplateRef } from '@bfc/shared';
import debounce from 'lodash/debounce';
import { filterTemplateDiagnostics } from '@bfc/indexers';
import debounce from 'lodash/debounce';
import isEqual from 'lodash/isEqual';

import { FormContext } from '../types';

Expand Down Expand Up @@ -48,11 +49,10 @@ export const LgEditorWidget: React.FC<LgEditorWidgetProps> = props => {
const lgFileId = formContext.currentDialog.lgFile || 'common';
const lgFile = formContext.lgFiles && formContext.lgFiles.find(file => file.id === lgFileId);

const updateLgTemplate = useMemo(
() =>
debounce((body: string) => {
formContext.shellApi.updateLgTemplate(lgFileId, lgName, body).catch(() => {});
}, 500),
const updateLgTemplate = useCallback(
(body: string) => {
formContext.shellApi.updateLgTemplate(lgFileId, lgName, body).catch(() => {});
},
[lgName, lgFileId]
);

Expand All @@ -76,6 +76,22 @@ export const LgEditorWidget: React.FC<LgEditorWidgetProps> = props => {
? diagnostic.message.split('error message: ')[diagnostic.message.split('error message: ').length - 1]
: '';
const [localValue, setLocalValue] = useState(template.body);
const sync = useRef(
debounce((shellData: any, localData: any) => {
if (!isEqual(shellData, localData)) {
setLocalValue(shellData);
}
}, 750)
).current;

useEffect(() => {
sync(template.body, localValue);

return () => {
sync.cancel();
};
}, [template.body]);

const lgOption = {
fileId: lgFileId,
templateId: lgName,
Expand All @@ -88,20 +104,12 @@ export const LgEditorWidget: React.FC<LgEditorWidgetProps> = props => {
updateLgTemplate(body);
props.onChange(new LgTemplateRef(lgName).toString());
} else {
updateLgTemplate.flush();
formContext.shellApi.removeLgTemplate(lgFileId, lgName);
props.onChange();
}
}
};

// update the template on mount to get validation
useEffect(() => {
if (localValue) {
updateLgTemplate(localValue);
}
}, []);

return (
<LgEditor
onChange={onChange}
Expand Down
19 changes: 15 additions & 4 deletions Composer/packages/extensions/obiformeditor/src/FormEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@

/** @jsx jsx */
import { Global, jsx } from '@emotion/core';
import React, { useState, useMemo, useEffect } from 'react';
import React, { useState, useMemo, useEffect, useRef } from 'react';
import { Dropdown } from 'office-ui-fabric-react/lib/Dropdown';
import { JSONSchema6Definition, JSONSchema6 } from 'json-schema';
import merge from 'lodash/merge';
import get from 'lodash/get';
import isEqual from 'lodash/isEqual';
import debounce from 'lodash/debounce';
import { appschema, ShellData, ShellApi } from '@bfc/shared';
import { Diagnostic } from '@bfc/indexers';

Expand All @@ -34,10 +35,20 @@ export const FormEditor: React.FunctionComponent<FormEditorProps> = props => {
const [localData, setLocalData] = useState(data);
const type = getType(localData);

const sync = useRef(
debounce((shellData: FormData, localData: FormData) => {
if (!isEqual(shellData, localData)) {
setLocalData(shellData);
}
}, 750)
).current;

useEffect(() => {
if (!isEqual(localData, data)) {
setLocalData(data);
}
sync(data, localData);

return () => {
sync.cancel();
};
}, [data]);

const formErrors = useMemo(() => {
Expand Down
2 changes: 1 addition & 1 deletion Composer/packages/lib/shared/src/types/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface ShellData {
botName: string;
currentDialog: any;
data: {
$type: string;
$type?: string;
[key: string]: any;
};
dialogId: string;
Expand Down

0 comments on commit 1688eb1

Please sign in to comment.