Skip to content

Commit

Permalink
feat: Selector to get all lg and lu files imported by a form dialog (m…
Browse files Browse the repository at this point in the history
…icrosoft#4515)

* selector to get all lg and lu files imported by a form dialog

* export as selector

* rename type

* ProjectTree: env variable flag -> feature flag

* adds test and rename

* fix

Co-authored-by: Soroush <[email protected]>
  • Loading branch information
hatpick and sorgh authored Oct 26, 2020
1 parent 67bc817 commit 250e8f7
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
} from '../../recoilModel';
import { getFriendlyName } from '../../utils/dialogUtil';
import { triggerNotSupported } from '../../utils/dialogValidator';
import { useFeatureFlag } from '../../utils/hooks';

import { TreeItem } from './treeItem';
import { ExpandableNode } from './ExpandableNode';
Expand Down Expand Up @@ -133,6 +134,7 @@ export const ProjectTree: React.FC<Props> = ({
const { onboardingAddCoachMarkRef, selectTo, navTo, navigateToFormDialogSchema } = useRecoilValue(dispatcherState);

const [filter, setFilter] = useState('');
const formDialogComposerFeatureEnabled = useFeatureFlag('FORM_DIALOG');
const [selectedLink, setSelectedLink] = useState<TreeLink | undefined>();
const delayedSetFilter = debounce((newValue) => setFilter(newValue), 1000);
const addMainDialogRef = useCallback((mainDialog) => onboardingAddCoachMarkRef({ mainDialog }), []);
Expand Down Expand Up @@ -164,7 +166,7 @@ export const ProjectTree: React.FC<Props> = ({
};

const dialogIsFormDialog = (dialog: DialogInfo) => {
return process.env.COMPOSER_ENABLE_FORMS && dialog.content?.schema !== undefined;
return formDialogComposerFeatureEnabled && dialog.content?.schema !== undefined;
};

const formDialogSchemaExists = (projectId: string, dialog: DialogInfo) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { getBaseName } from '../../../utils/fileUtil';
import { getLanguageFileImports } from '../dialogImports';

const files = [
{
id: 'name1.lg',
content: '[name2.lg](../files/name2.lg)\n[name3.lg](../files/name3.lg)\n',
},
{
id: 'id.lg',
content: '',
},
{
id: 'gender.lg',
content: '',
},
{
id: 'name2.lg',
content: '[name4.lg](../files/name4.lg)\n[name5-entity.lg](../files/name5-entity.lg)\n',
},
{
id: 'name3.lg',
content: '- Enter a value for name3',
},
{
id: 'name4.lg',
content: '[name5-entity.lg](../files/name5-entity.lg)',
},
{
id: 'name5-entity.lg',
content: '- Enter a value for name5',
},
];

describe('dialogImports selectors', () => {
it('should follow all imports and list all unique imports', () => {
const getFile = (id) => files.find((f) => getBaseName(f.id) === id) as { id: string; content: string };

const fileImports = getLanguageFileImports('name1', getFile);
expect(fileImports).toEqual([
{
id: 'name2.lg',
content: '[name4.lg](../files/name4.lg)\n[name5-entity.lg](../files/name5-entity.lg)\n',
},
{
id: 'name3.lg',
content: '- Enter a value for name3',
},
{
id: 'name4.lg',
content: '[name5-entity.lg](../files/name5-entity.lg)',
},
{
id: 'name5-entity.lg',
content: '- Enter a value for name5',
},
]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { LanguageFileImport, LgFile, LuFile } from '@bfc/shared';
import uniqBy from 'lodash/uniqBy';
import { selectorFamily } from 'recoil';

import { getBaseName } from '../../utils/fileUtil';
import { localeState, lgFilesState, luFilesState } from '../atoms';

// eslint-disable-next-line security/detect-unsafe-regex
const importRegex = /\[(?<id>.*?)]\((?<importPath>.*?)(?="|\))(?<optionalpart>".*")?\)/g;

const getImportsHelper = (content: string): LanguageFileImport[] => {
const lines = content.split(/\r?\n/g).filter((l) => !!l) ?? [];

return (lines
.map((l) => {
importRegex.lastIndex = 0;
return importRegex.exec(l) as RegExpExecArray;
})
.filter(Boolean) as RegExpExecArray[]).map((regExecArr) => ({
id: getBaseName(regExecArr.groups?.id ?? ''),
importPath: regExecArr.groups?.importPath ?? '',
}));
};

// Finds all the file imports starting from a given dialog file.
export const getLanguageFileImports = <T extends { id: string; content: string }>(
rootDialogId: string,
getFile: (fileId: string) => T
): T[] => {
const imports: LanguageFileImport[] = [];

const visitedIds: string[] = [];
const fileIds = [rootDialogId];

while (fileIds.length) {
const currentId = fileIds.pop() as string;
// If this file is already visited, then continue.
if (visitedIds.includes(currentId)) {
continue;
}
const file = getFile(currentId);
// If file is not found or file content is empty, then continue.
if (!file || !file.content) {
continue;
}
const currentImports = getImportsHelper(file.content);
visitedIds.push(currentId);
imports.push(...currentImports);
const newIds = currentImports.map((ci) => getBaseName(ci.id));
fileIds.push(...newIds);
}

return uniqBy(imports, 'id').map((impExpr) => getFile(impExpr.id));
};

// Returns all the lg files referenced by a dialog file and its referenced lg files.
export const lgImportsSelectorFamily = selectorFamily<LgFile[], { projectId: string; dialogId: string }>({
key: 'lgImports',
get: ({ projectId, dialogId }) => ({ get }) => {
const locale = get(localeState(projectId));

const getFile = (fileId: string) =>
get(lgFilesState(projectId)).find((f) => f.id === fileId || f.id === `${fileId}.${locale}`) as LgFile;

return getLanguageFileImports(dialogId, getFile);
},
});

// Returns all the lu files referenced by a dialog file and its referenced lu files.
export const luImportsSelectorFamily = selectorFamily<LuFile[], { projectId: string; dialogId: string }>({
key: 'luImports',
get: ({ projectId, dialogId }) => ({ get }) => {
const locale = get(localeState(projectId));

const getFile = (fileId: string) =>
get(luFilesState(projectId)).find((f) => f.id === fileId || f.id === `${fileId}.${locale}`) as LuFile;

return getLanguageFileImports(dialogId, getFile);
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export * from './eject';
export * from './extensions';
export * from './validatedDialogs';
export * from './dialogs';
export * from './dialogImports';
export * from './projectTemplates';
5 changes: 5 additions & 0 deletions Composer/packages/types/src/indexers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@ export type LgParsed = {
templates: LgTemplate[];
};

export type LanguageFileImport = {
id: string;
importPath: string;
};

export type LgFile = {
id: string;
content: string;
Expand Down

0 comments on commit 250e8f7

Please sign in to comment.