From e6073ba2e7f7661329315f3fe9dd67cce32c13e6 Mon Sep 17 00:00:00 2001 From: DDurandDTI <45204737+DDurandDTI@users.noreply.github.com> Date: Thu, 20 Dec 2018 14:09:49 -0500 Subject: [PATCH] Feature/modul 582 arbo multinode select (#612) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Checkboxes and recursive node select * Refactoring and completion of logic * Fixed recursivity between elements * Moved tree sub-components, updated broken unit tests * Unit tests for tree-icon * Made interminated state for checkboxes and revised tree prop name * Intégration du partially-checked * Refactored multiple tree nodes code * Added a few states for multiple-node selection * Merged integration branch, adjusted autoselect multinode's behavior * Fine-tuned code, started unit testing * Updated unit tests * Changes prior to yet another small refactor * Finished correcting checkboxes behavior * Prior to new tree being added to sandbox * Prior to selectedParentNodes prop * Button auto-select feature completed * Added more tests and checkbox states * Updated tests and added descriptions to trees * Light refactor * MODUL-582 Added new behaviors (parent-checkbox auto-select) and new tests * MODUL-582 Removed the ability to show selection button when checkboxes are hidden * MODUL-582 Added conditional icon class name rendering to prevent all snapshots to be updated * MODUL-582 Updated behaviors and tests * MODUL-582 Fixed multinode tree's behavior * MODUL-582 Simplified indeterminated checkbox state * MODUL-582 Removed consective blank lines from Checkbox.spec * MODUL-582 Updated sandbox to improve terminology comprehension * MODUL-582 Prior to checkbox display condition refactor * MODUL-582 Prior to dev merge * MODUL-582 Merged develop, updated tree unit tests, refactored, added comments * MODUL-582 Slight var names improvement * MODUL-582 Simplified tree-node code * MODUL-582 TreeNode light refactor * MODUL-582 Removed treeNode and TreeIcon module export * MODUL-582 Fixed tree plugin --- package-lock.json | 3 +- .../__snapshots__/checkbox.spec.ts.snap | 24 +- src/components/checkbox/checkbox.html | 14 +- src/components/checkbox/checkbox.sandbox.html | 16 +- src/components/checkbox/checkbox.scss | 78 +++++- src/components/checkbox/checkbox.spec.ts | 34 +++ src/components/checkbox/checkbox.ts | 11 + src/components/component-names.ts | 2 - src/components/components-plugin.ts | 4 - src/components/icon-file/icon-file.html | 7 +- src/components/icon/icon.html | 11 +- src/components/icon/icon.ts | 7 + src/components/tree-icon/tree-icon.html | 2 - src/components/tree-icon/tree-icon.scss | 5 - src/components/tree-icon/tree-icon.spec.ts | 75 ------ src/components/tree-icon/tree-icon.ts | 45 ---- .../__snapshots__/tree-node.spec.ts.snap | 40 --- src/components/tree-node/tree-node.html | 22 -- .../tree-node/tree-node.lang.en.json | 5 - .../tree-node/tree-node.lang.fr.json | 5 - src/components/tree-node/tree-node.ts | 114 -------- .../tree/__snapshots__/tree.spec.ts.snap | 30 ++- src/components/tree/component-names.ts | 2 + src/components/tree/tree-icon/tree-icon.html | 16 ++ src/components/tree/tree-icon/tree-icon.scss | 10 + .../tree/tree-icon/tree-icon.spec.ts | 96 +++++++ src/components/tree/tree-icon/tree-icon.ts | 37 +++ .../__snapshots__/tree-node.spec.ts.snap | 93 +++++++ src/components/tree/tree-node/tree-node.html | 61 +++++ .../{ => tree}/tree-node/tree-node.scss | 37 ++- .../{ => tree}/tree-node/tree-node.spec.ts | 195 +++++++++++++- src/components/tree/tree-node/tree-node.ts | 251 ++++++++++++++++++ src/components/tree/tree.html | 29 +- src/components/tree/tree.lang.en.json | 3 + src/components/tree/tree.lang.fr.json | 3 + src/components/tree/tree.sandbox.html | 144 +++++++++- src/components/tree/tree.sandbox.ts | 84 +++++- src/components/tree/tree.spec.ts | 169 ++++++++---- src/components/tree/tree.ts | 109 ++++++-- src/lang/fr.ts | 1 - 40 files changed, 1430 insertions(+), 464 deletions(-) delete mode 100644 src/components/tree-icon/tree-icon.html delete mode 100644 src/components/tree-icon/tree-icon.scss delete mode 100644 src/components/tree-icon/tree-icon.spec.ts delete mode 100644 src/components/tree-icon/tree-icon.ts delete mode 100644 src/components/tree-node/__snapshots__/tree-node.spec.ts.snap delete mode 100644 src/components/tree-node/tree-node.html delete mode 100644 src/components/tree-node/tree-node.lang.en.json delete mode 100644 src/components/tree-node/tree-node.lang.fr.json delete mode 100644 src/components/tree-node/tree-node.ts create mode 100644 src/components/tree/component-names.ts create mode 100644 src/components/tree/tree-icon/tree-icon.html create mode 100644 src/components/tree/tree-icon/tree-icon.scss create mode 100644 src/components/tree/tree-icon/tree-icon.spec.ts create mode 100644 src/components/tree/tree-icon/tree-icon.ts create mode 100644 src/components/tree/tree-node/__snapshots__/tree-node.spec.ts.snap create mode 100644 src/components/tree/tree-node/tree-node.html rename src/components/{ => tree}/tree-node/tree-node.scss (72%) rename src/components/{ => tree}/tree-node/tree-node.spec.ts (53%) create mode 100644 src/components/tree/tree-node/tree-node.ts diff --git a/package-lock.json b/package-lock.json index 8fb2a0a4d..146ff580b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2761,8 +2761,7 @@ "es6-promise": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz", - "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==", - "dev": true + "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==" }, "es6-promisify": { "version": "5.0.0", diff --git a/src/components/checkbox/__snapshots__/checkbox.spec.ts.snap b/src/components/checkbox/__snapshots__/checkbox.spec.ts.snap index 7551ecdc7..7c4dc387e 100644 --- a/src/components/checkbox/__snapshots__/checkbox.spec.ts.snap +++ b/src/components/checkbox/__snapshots__/checkbox.spec.ts.snap @@ -32,6 +32,28 @@ exports[`MCheckbox should render correctly when disabled 1`] = ` `; +exports[`MCheckbox should render correctly when disabled and checked 1`] = ` +
+ + +
+ +
+
+`; + +exports[`MCheckbox should render correctly when indeterminated (with correct classes) 1`] = ` +
+ + +
+ +
+
+`; + exports[`MCheckbox should render correctly when position prop is right 1`] = `
@@ -46,7 +68,7 @@ exports[`MCheckbox should render correctly when position prop is right 1`] = ` exports[`MCheckbox should render correctly when readonly 1`] = `
-
diff --git a/src/components/tree/tree.sandbox.ts b/src/components/tree/tree.sandbox.ts index 19c8bf0b3..88896da25 100644 --- a/src/components/tree/tree.sandbox.ts +++ b/src/components/tree/tree.sandbox.ts @@ -1,6 +1,5 @@ import Vue, { PluginObject } from 'vue'; import Component from 'vue-class-component'; - import { TREE_NAME } from '../component-names'; import { TreeNode } from './tree'; import WithRender from './tree.sandbox.html'; @@ -18,6 +17,13 @@ export class MRootTreeSandbox extends Vue { public currentFile: string[] = ['/folder 1/folder 2/index.html']; public currentFile2: string[] = ['/1/2']; public wrongCurrentFile: string[] = ['/3/4']; + public currentNodesBasic: string[] = []; + public currentNodesCheckboxes: string[] = ['/3/11']; + public disabledNodesCheckboxes: string[] = ['/3/11']; + public currentNodesCheckboxesParent: string[] = []; + public currentNodesCheckboxesButtonAutoselect: string[] = []; + public currentNodesCheckboxesNone: string[] = ['/1/11/444/1111']; + public disabledNodesCheckboxesNone: string[] = ['/3', '/1/11/444/2222']; public emptyTree: TreeNode[] = []; @@ -256,8 +262,82 @@ export class MRootTreeSandbox extends Vue { } ]; + public multiNodeTree: TreeNode[] = [ + { + id: '1', + label: 'UL - Université Laval', + hasChildren: true, + children: [ + { + id: '11', + label: 'FSG - Faculté de Sciences et Génie', + children: [ + { + id: '111', + label: 'ACT - Actuariat' + }, + { + id: '222', + label: 'BIO - Biologie' + }, + { + id: '333', + label: 'CHM - Chimie' + }, + { + id: '444', + label: 'GMCN - Génie Mécanique', + children: [ + { + id: '1111', + label: 'PRS - Personne' + }, + { + id: '2222', + label: 'SMT - Something' + } + ] + } + ] + }, + { + id: '22', + label: 'LLI - Lettres et sciences humaines', + children: [ + { + id: '111', + label: 'JPN - Japonais' + }, + { + id: '222', + label: 'EN - Anglais' + } + ] + } + ] + }, + { + id: '2', + label: 'CSF - Cégep Sainte-Foy' + }, + { + id: '3', + label: 'UQAM - Université du Québec à Montréal', + children: [ + { + id: '11', + label: 'CHM - Chimie' + }, + { + id: '22', + label: 'GMCN - Génie Mécanique' + } + ] + } + ]; + public onSelect(): void { - console.error('modUL - New file selected'); + console.error('modUL - New node selected'); } } diff --git a/src/components/tree/tree.spec.ts b/src/components/tree/tree.spec.ts index 65a3bc632..59198e178 100644 --- a/src/components/tree/tree.spec.ts +++ b/src/components/tree/tree.spec.ts @@ -1,15 +1,18 @@ -import { RefSelector, shallow, Wrapper } from '@vue/test-utils'; - +import { mount, RefSelector, shallow, Wrapper } from '@vue/test-utils'; import { renderComponent } from '../../../tests/helpers/render'; -import { MSelectionMode, MTree, TreeNode } from './tree'; +import uuid from '../../utils/uuid/uuid'; +import { MCheckboxes, MSelectionMode, MTree, TreeNode } from './tree'; + +jest.mock('../../utils/uuid/uuid'); +(uuid.generate as jest.Mock).mockReturnValue('uuid'); -const TREE_NODE_REF: RefSelector = { ref: 'tree-node' }; -const EMPTY_TREE_REF: RefSelector = { ref: 'empty-tree-txt' }; const ERROR_TREE_REF: RefSelector = { ref: 'error-tree-txt' }; -const SELECTED_NODES: string[] = ['/medias/Videos']; -const SELECTED_NODES_INVALID: string[] = ['/medias/Videos/video-dog.mov']; +const SELECTED_NODE: string[] = ['/medias/Videos']; +const SELECTED_NODES: string[] = ['/index.html', '/medias/Videos']; const NEW_TREE_NODE_SELECTED: string[] = ['/index.html']; +const NEW_TREE_NODES_SELECTED: string[] = ['/index.html', '/medias/Videos']; +const SELECTED_NODE_CLASS: string = '.m--is-selected'; const EMPTY_TREE: TreeNode[] = []; const TREE_WITH_DATA: TreeNode[] = [ @@ -28,7 +31,7 @@ const TREE_WITH_DATA: TreeNode[] = [ ] } ]; -const TREE_WITH_NIVALID_DATA: TreeNode[] = [ +const TREE_WITH_INVALID_DATA: TreeNode[] = [ { label: 'index.html', id: '' @@ -45,17 +48,12 @@ const TREE_WITH_NIVALID_DATA: TreeNode[] = [ } ]; -let wrapper: Wrapper; - let tree: TreeNode[] = TREE_WITH_DATA; let selectionMode: MSelectionMode = MSelectionMode.Single; -let selectedNodes: string[] = SELECTED_NODES; +let selectedNodes: string[] = SELECTED_NODE; +let checkboxes: MCheckboxes; -afterEach(() => { - tree = []; - selectionMode = MSelectionMode.Single; - selectedNodes = SELECTED_NODES; -}); +let wrapper: Wrapper; const initializeShallowWrapper: any = () => { wrapper = shallow(MTree, { @@ -67,76 +65,133 @@ const initializeShallowWrapper: any = () => { }); }; -describe(`MTree`, () => { - - describe(`Given an empty tree`, () => { - - beforeEach(() => { - tree = EMPTY_TREE; - initializeShallowWrapper(); - }); +const initializeMountWrapper: any = () => { + wrapper = mount(MTree, { + propsData: { + tree, + selectedNodes, + selectionMode, + checkboxes + } + }); +}; - it(`Should render correctly`, () => { - expect(renderComponent(wrapper.vm)).resolves.toMatchSnapshot(); - }); +describe(`MTree`, () => { - it(`Should be empty`, () => { - expect(wrapper.vm.propTreeEmpty).toBeTruthy(); + describe('Given a single node selection', () => { + afterEach(() => { + tree = []; + selectionMode = MSelectionMode.Single; + selectedNodes = SELECTED_NODE; }); - }); - describe(`Given a tree with some data`, () => { + describe(`Given an empty tree`, () => { - beforeEach(() => { - tree = TREE_WITH_DATA; - initializeShallowWrapper(); - }); + beforeEach(() => { + tree = EMPTY_TREE; + initializeShallowWrapper(); + }); - it(`Should render correctly`, () => { - expect(renderComponent(wrapper.vm)).resolves.toMatchSnapshot(); - }); + it(`Should render correctly`, () => { + expect(renderComponent(wrapper.vm)).resolves.toMatchSnapshot(); + }); - it(`Should not be empty`, () => { - expect(wrapper.vm.propTreeEmpty).toBeFalsy(); + it(`Should be empty`, () => { + expect(wrapper.vm.propTreeEmpty).toBeTruthy(); + }); }); - describe(`When a node is selected`, () => { + describe(`Given a tree with some data`, () => { beforeEach(() => { - wrapper.vm.onClick(NEW_TREE_NODE_SELECTED[0]); + tree = TREE_WITH_DATA; + initializeShallowWrapper(); }); - it(`Call onClick`, () => { - wrapper.setMethods({ onClick: jest.fn() }); - wrapper.find(TREE_NODE_REF).trigger('click'); + it(`Should render correctly`, () => { + expect(renderComponent(wrapper.vm)).resolves.toMatchSnapshot(); + }); - expect(wrapper.vm.onClick).toHaveBeenCalled(); + it(`Should not be empty`, () => { + expect(wrapper.vm.propTreeEmpty).toBeFalsy(); }); - it(`Emit select`, () => { - expect(wrapper.emitted('select')).toBeTruthy(); + describe(`When a node is selected`, () => { + + beforeEach(() => { + wrapper.vm.onClick(NEW_TREE_NODE_SELECTED[0]); + }); + + it(`Emit select`, () => { + expect(wrapper.emitted('select')).toBeTruthy(); + }); + + it(`A new node is selected`, () => { + expect(wrapper.vm.propSelectedNodes).toEqual(NEW_TREE_NODE_SELECTED); + }); + }); - it(`A new node is selected`, () => { - expect(wrapper.vm.propSelectedNodes).toEqual(NEW_TREE_NODE_SELECTED); + describe(`When there is an error in the tree`, () => { + + beforeEach(() => { + tree = TREE_WITH_INVALID_DATA; + initializeShallowWrapper(); + }); + + it(`Should generate an error`, () => { + expect(wrapper.vm.errorTree).toBeTruthy(); + }); + + it(`Should show an error message`, () => { + expect(wrapper.find(ERROR_TREE_REF).exists()).toBeTruthy(); + }); + }); }); + }); + + describe(`Given a tree with multiple selection`, () => { + + afterEach(() => { + tree = []; + selectedNodes = []; + checkboxes = MCheckboxes.False; + }); - describe(`When there is an error in the tree`, () => { + describe(`Given a tree with no node selected`, () => { beforeEach(() => { - tree = TREE_WITH_NIVALID_DATA; - initializeShallowWrapper(); + tree = TREE_WITH_DATA; + selectedNodes = []; + selectionMode = MSelectionMode.Multiple; + initializeMountWrapper(); }); - it(`Should generate an error`, () => { - expect(wrapper.vm.errorTree).toBeTruthy(); + it(`Should allow multiple selection`, () => { + wrapper.vm.onClick(NEW_TREE_NODES_SELECTED[0]); + wrapper.vm.onClick(NEW_TREE_NODES_SELECTED[1]); + expect(wrapper.vm.propSelectedNodes).toEqual(NEW_TREE_NODES_SELECTED); }); + }); - it(`Should show an error message`, () => { - expect(wrapper.find(ERROR_TREE_REF).exists()).toBeTruthy(); + describe(`Given a tree with two nodes selected`, () => { + + beforeEach(() => { + tree = TREE_WITH_DATA; + selectedNodes = []; + selectionMode = MSelectionMode.Multiple; + selectedNodes = SELECTED_NODES; + initializeMountWrapper(); }); + it(`Should render correctly`, () => { + expect(renderComponent(wrapper.vm)).resolves.toMatchSnapshot(); + }); + + it(`Should render with two selected nodes`, () => { + expect(wrapper.findAll(SELECTED_NODE_CLASS).length).toBe(2); + }); }); }); }); diff --git a/src/components/tree/tree.ts b/src/components/tree/tree.ts index c843670a7..acb8c3cb2 100644 --- a/src/components/tree/tree.ts +++ b/src/components/tree/tree.ts @@ -1,13 +1,18 @@ import { PluginObject } from 'vue'; import Component from 'vue-class-component'; -import { Prop, Watch } from 'vue-property-decorator'; - +import { Emit, Prop, Watch } from 'vue-property-decorator'; import { ModulVue } from '../../utils/vue/vue'; +import AccordionTransitionPlugin from '../accordion/accordion-transition'; +import CheckboxPlugin from '../checkbox/checkbox'; import { TREE_NAME } from '../component-names'; import I18nPlugin from '../i18n/i18n'; -import TreeNodePlugin from '../tree-node/tree-node'; +import IconFilePlugin from '../icon-file/icon-file'; +import IconPlugin from '../icon/icon'; +import MessagePlugin from '../message/message'; +import PlusPlugin from '../plus/plus'; +import { TREE_NODE_NAME } from './component-names'; +import { MTreeNode } from './tree-node/tree-node'; import WithRender from './tree.html?style=./tree.scss'; - export interface TreeNode { id: string; label?: string; @@ -18,13 +23,24 @@ export interface TreeNode { } export enum MSelectionMode { - None = '0', - Single = '1', - Multiple = '2' + None = 'none', + Single = 'single', + Multiple = 'multiple' } +export enum MCheckboxes { + True = 'true', // Fully independant checkbox selection + False = 'false', // No Checkboxes + WithButtonAutoSelect = 'with-button-auto-select', // Fully independat, but a button can handle mass-selection + WithCheckboxAutoSelect = 'with-checkbox-auto-select', // Selection of parents is 100% related to children + WithParentAutoSelect = 'with-parent-auto-select' // Children can be selected by parent & children can unselect parent +} @WithRender -@Component +@Component({ + components: { + [TREE_NODE_NAME]: MTreeNode + } +}) export class MTree extends ModulVue { @Prop() public tree: TreeNode[]; @@ -38,36 +54,46 @@ export class MTree extends ModulVue { }) public selectionMode: MSelectionMode; + @Prop({ + default: MCheckboxes.False, + validator: value => + value === MCheckboxes.False || + value === MCheckboxes.True || + value === MCheckboxes.WithButtonAutoSelect || + value === MCheckboxes.WithCheckboxAutoSelect || + value === MCheckboxes.WithParentAutoSelect + }) + public checkboxes: MCheckboxes; + @Prop() public selectedNodes: string[]; @Prop() - public icons: boolean; + public useFilesIcons: boolean; + + @Prop() + public disabledNodes: string[]; public propSelectedNodes: string[] = this.selectedNodes || []; + public errorTree: boolean = false; private selectedNodesFound: string[] = []; - public onClick(path: string): void { - if (this.propSelectedNodes.indexOf(path) === -1) { - if (this.selectionMode === MSelectionMode.Multiple) { - this.propSelectedNodes.push(path); - } else { - this.propSelectedNodes = [path]; + @Emit('select') + public onClick(path: string): string { + if (!this.pathIsDisabled(path)) { + if (this.propSelectedNodes.indexOf(path) === -1) { + if (this.selectionMode === MSelectionMode.Multiple) { + this.propSelectedNodes.push(path); + } else { + this.propSelectedNodes = [path]; + } + } else if (this.selectionMode === MSelectionMode.Multiple) { + this.propSelectedNodes.splice(this.propSelectedNodes.indexOf(path), 1); } - } else if (this.selectionMode === MSelectionMode.Multiple) { - this.propSelectedNodes.splice(this.propSelectedNodes.indexOf(path), 1); } - this.$emit('select', path); - } - - public get propTreeEmpty(): boolean { - return !this.tree.length; - } - - public get selectable(): boolean { - return this.selectionMode !== MSelectionMode.None; + return path; } protected created(): void { @@ -87,6 +113,10 @@ export class MTree extends ModulVue { }); } + private pathIsDisabled(path: string): boolean { + return this.propDisabledNodes.indexOf(path) !== -1; + } + private browseNode(node: TreeNode, path: string = ''): void { if (node.id.trim() === '') { this.errorTree = true; @@ -101,13 +131,38 @@ export class MTree extends ModulVue { }); } } + + public get propTreeEmpty(): boolean { + return !this.tree.length; + } + + public get propDisabledNodes(): string[] { + return this.disabledNodes || []; + } + + public get selectable(): boolean { + return this.selectionMode !== MSelectionMode.None; + } + + public get isMultipleSelectWithCheckboxes(): boolean { + return this.selectionMode === MSelectionMode.Multiple && this.withCheckboxes; + } + + public get withCheckboxes(): boolean { + return this.checkboxes !== MCheckboxes.False; + } } const TreePlugin: PluginObject = { install(v, options): void { v.prototype.$log.debug(TREE_NAME, 'plugin.install'); - v.use(TreeNodePlugin); v.use(I18nPlugin); + v.use(CheckboxPlugin); + v.use(IconFilePlugin); + v.use(IconPlugin); + v.use(PlusPlugin); + v.use(MessagePlugin); + v.use(AccordionTransitionPlugin); v.component(TREE_NAME, MTree); } }; diff --git a/src/lang/fr.ts b/src/lang/fr.ts index 2538ebe18..0aef59b66 100644 --- a/src/lang/fr.ts +++ b/src/lang/fr.ts @@ -40,7 +40,6 @@ const FrenchPlugin: PluginObject = { require('../components/rich-text-editor/rich-text-editor.lang.fr.json'), require('../components/table/table.lang.fr.json'), require('../components/toast/toast.lang.fr.json'), - require('../components/tree-node/tree-node.lang.fr.json'), require('../components/tree/tree.lang.fr.json'), require('../components/scroll-top/scroll-top.lang.fr.json'), require('../components/session-expired/session-expired.lang.fr.json'),