diff --git a/package-lock.json b/package-lock.json index 52ada7f1d..8f0ad8e2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,20 @@ { "name": "@processmaker/screen-builder", - "version": "2.84.2", + "version": "2.85.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@processmaker/screen-builder", - "version": "2.84.2", + "version": "2.85.1", "dependencies": { "@chantouchsek/validatorjs": "1.2.3", "@storybook/addon-docs": "^7.6.13", "axios-extensions": "^3.1.6", "lodash": "^4.17.21", "lru-cache": "^10.0.1", - "moment": "^2.29.1", - "moment-timezone": "^0.5.27", + "moment": "^2.30.1", + "moment-timezone": "^0.5.45", "monaco-editor": "^0.34.0", "scrollparent": "^2.0.1", "vue-loader": "^15.9.2", @@ -33,7 +33,7 @@ "@fortawesome/fontawesome-free": "^5.6.1", "@originjs/vite-plugin-commonjs": "^1.0.3", "@panter/vue-i18next": "^0.15.2", - "@processmaker/vue-form-elements": "0.51.0", + "@processmaker/vue-form-elements": "0.52.0", "@processmaker/vue-multiselect": "2.3.0", "@storybook/addon-essentials": "^7.6.13", "@storybook/addon-interactions": "^7.6.13", @@ -96,7 +96,7 @@ }, "peerDependencies": { "@panter/vue-i18next": "^0.15.0", - "@processmaker/vue-form-elements": "0.51.0", + "@processmaker/vue-form-elements": "0.52.0", "i18next": "^15.0.8", "vue": "^2.6.12", "vuex": "^3.1.1" @@ -4005,9 +4005,9 @@ } }, "node_modules/@processmaker/vue-form-elements": { - "version": "0.51.0", - "resolved": "https://registry.npmjs.org/@processmaker/vue-form-elements/-/vue-form-elements-0.51.0.tgz", - "integrity": "sha512-genxpQ5JpXv2Zhsm/DC/3B43Nj4euG+50YX75J6FzQ0KxtrQL8ID7fCkaoCxDm54yzrEEFclw9hVyjru3KPV2w==", + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/@processmaker/vue-form-elements/-/vue-form-elements-0.52.0.tgz", + "integrity": "sha512-atorz/UKXgYl9arTnpNYysJBGanWWoJo6WnSJqN5iqUVk6hECzNNShIpMoA1xTs6BOyu79QP+hl3dutW2I0BSA==", "dev": true, "dependencies": { "@chantouchsek/validatorjs": "1.2.3", @@ -4015,10 +4015,10 @@ "bootstrap": "^4.5.3", "bootstrap-vue": "^2.23.1", "core-js": "^3.8.3", - "jquery": "^3.5.1", + "jquery": "^3.7.1", "lodash": "^4.17.21", - "moment": "^2.29.1", - "moment-timezone": "^0.5.26", + "moment": "^2.30.1", + "moment-timezone": "^0.5.45", "popper.js": "^1.14.4", "tinymce": "5.10.0", "vue": "^2.6.12", @@ -19138,9 +19138,9 @@ } }, "node_modules/moment-timezone": { - "version": "0.5.44", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.44.tgz", - "integrity": "sha512-nv3YpzI/8lkQn0U6RkLd+f0W/zy/JnoR5/EyPz/dNkPTBjA2jNLCVxaiQ8QpeLymhSZvX0wCL5s27NQWdOPwAw==", + "version": "0.5.45", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.45.tgz", + "integrity": "sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==", "dependencies": { "moment": "^2.29.4" }, diff --git a/package.json b/package.json index 7ae36be40..6a5e8da49 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@processmaker/screen-builder", - "version": "2.84.2", + "version": "2.85.1", "scripts": { "dev": "VITE_COVERAGE=true vite", "build": "vite build", @@ -35,8 +35,8 @@ "axios-extensions": "^3.1.6", "lodash": "^4.17.21", "lru-cache": "^10.0.1", - "moment": "^2.29.1", - "moment-timezone": "^0.5.27", + "moment": "^2.30.1", + "moment-timezone": "^0.5.45", "monaco-editor": "^0.34.0", "scrollparent": "^2.0.1", "vue-loader": "^15.9.2", @@ -55,7 +55,7 @@ "@fortawesome/fontawesome-free": "^5.6.1", "@originjs/vite-plugin-commonjs": "^1.0.3", "@panter/vue-i18next": "^0.15.2", - "@processmaker/vue-form-elements": "0.51.0", + "@processmaker/vue-form-elements": "0.52.0", "@processmaker/vue-multiselect": "2.3.0", "@storybook/addon-essentials": "^7.6.13", "@storybook/addon-interactions": "^7.6.13", @@ -114,7 +114,7 @@ }, "peerDependencies": { "@panter/vue-i18next": "^0.15.0", - "@processmaker/vue-form-elements": "0.51.0", + "@processmaker/vue-form-elements": "0.52.0", "i18next": "^15.0.8", "vue": "^2.6.12", "vuex": "^3.1.1" diff --git a/sonar-project.properties b/sonar-project.properties index 37fec5778..34446f8a6 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -3,3 +3,5 @@ sonar.javascript.lcov.reportPaths=./coverage/lcov.info sonar.exclusions=node_modules/**, dist/**, public/**, home/**, storybook-static/** sonar.sources=src sonar.tests=tests +sonar.cpd.exclusions=src/stories/** +sonar.coverage.exclusions=src/stories/** diff --git a/src/assets/css/custom.css b/src/assets/css/custom.css index 38ec225ce..09fecf462 100644 --- a/src/assets/css/custom.css +++ b/src/assets/css/custom.css @@ -8,4 +8,7 @@ } .page-dropdown-menu { min-width: 333px; + max-height: 26rem; + overflow-y: auto; + scrollbar-width: thin; } \ No newline at end of file diff --git a/src/assets/css/tabs.css b/src/assets/css/tabs.css index 3c15ade72..52908505f 100644 --- a/src/assets/css/tabs.css +++ b/src/assets/css/tabs.css @@ -10,10 +10,12 @@ /* Override Bootstrap default tab styles */ .nav-tabs { border-bottom: 1px solid var(--tabs-border) !important; + } + .nav-tabs-nowrap { flex-wrap: nowrap !important; overflow: hidden !important; } - + .nav-tabs .nav-item .nav-link { display: flex; align-items: center; diff --git a/src/components/TabsBar.vue b/src/components/TabsBar.vue index bfbd9cd51..4678ecc21 100644 --- a/src/components/TabsBar.vue +++ b/src/components/TabsBar.vue @@ -4,6 +4,7 @@ v-model="activeTab" class="h-100 w-100 flat-tabs" content-class="h-tab" + nav-class="nav-tabs-nowrap" lazy @changed="tabsUpdated" @input="tabOpened" @@ -196,9 +197,10 @@ export default { }, visualThreshold); }); }, - closeTab(pageId) { - this.localOpenedPages.splice(this.localOpenedPages.indexOf(pageId), 1); - this.$emit("tab-closed", this.pages[pageId], this.localOpenedPages); + closeTab(tabIndex) { + const pageIndex = this.localOpenedPages[tabIndex]; + this.localOpenedPages.splice(tabIndex, 1); + this.$emit("tab-closed", this.pages[pageIndex], this.localOpenedPages); }, updateTabsReferences(pageDelete) { this.localOpenedPages = this.localOpenedPages.map((page) => diff --git a/src/components/sortable/sortableList/SortableList.vue b/src/components/sortable/sortableList/SortableList.vue index e712ab005..18e044df7 100644 --- a/src/components/sortable/sortableList/SortableList.vue +++ b/src/components/sortable/sortableList/SortableList.vue @@ -24,15 +24,16 @@
{{ item.name }}
@@ -63,7 +64,7 @@ export default { }, data() { return { - originalName: '', + newName: '', draggedItem: 0, draggedOverItem: 0, editRowIndex: null, @@ -79,14 +80,14 @@ export default { }, methods: { validateState(name, item) { - const isEmpty = !name; + const isEmpty = !name?.trim(); const isDuplicated = this.items .filter((i) => i !== item) .find((i) => i.name === name); return isEmpty || isDuplicated ? false : null; }, validateError(name, item) { - const isEmpty = !name; + const isEmpty = !name?.trim(); if (!isEmpty) { return this.$t("The Page Name field is required."); } @@ -98,19 +99,25 @@ export default { } return ''; }, - onFocus(name, item) { - this.originalName = name; + onFocus(item) { + this.newName = item.name; }, async onBlur(name, item) { if (this.validateState(name, item) === false) { + this.newName = item.name; + } else { // eslint-disable-next-line no-param-reassign - item.name = this.originalName; + item.name = name; } await this.$nextTick(); setTimeout(() => { this.editRowIndex = null; }, 250); }, + async onCancel(item) { + this.newName = item.name; + this.editRowIndex = null; + }, onClick(item, index) { this.editRowIndex = index; this.$emit("item-edit", item); @@ -162,6 +169,14 @@ export default { itemsSortedClone[draggedItemIndex].order = tempOrder; } + + // Update order of the items + const clone = [...itemsSortedClone]; + clone.sort((a, b) => a.order - b.order); + clone.forEach((item, index) => { + // eslint-disable-next-line no-param-reassign + item.order = index + 1; + }); } this.$emit('ordered', itemsSortedClone); diff --git a/src/components/vue-form-builder.vue b/src/components/vue-form-builder.vue index 964efb35c..d8926f559 100644 --- a/src/components/vue-form-builder.vue +++ b/src/components/vue-form-builder.vue @@ -881,6 +881,7 @@ export default { config.forEach((page) => this.replaceFormText(page.items)); config.forEach((page) => this.migrateFormSubmit(page.items)); config.forEach((page) => this.updateFieldNameValidation(page.items)); + this.updatePageOrder(config); config.forEach((page) => this.removeDataVariableFromNestedScreens(page.items) ); @@ -889,6 +890,18 @@ export default { page.order = page.order || index + 1; }); }, + updatePageOrder(pages) { + const clone = [...pages]; + clone.sort((a, b) => { + const aOrder = a.order || pages.indexOf(a) + 1; + const bOrder = b.order || pages.indexOf(b) + 1; + return aOrder - bOrder; + }); + clone.forEach((item, index) => { + // eslint-disable-next-line no-param-reassign + item.order = index + 1; + }); + }, updateFieldNameValidation(items) { items.forEach((item) => { if (item.inspector) { @@ -1148,9 +1161,9 @@ export default { this.updateState(); }, addPage(e) { - this.showAddPageValidations = true; const error = this.checkPageName(this.addPageName, true); if (error) { + this.showAddPageValidations = true; e.preventDefault(); return; } @@ -1227,6 +1240,7 @@ export default { globalObject.ProcessMaker.alert(error.message, "danger"); return; } + this.updatePageOrder(this.config); this.$store.dispatch("undoRedoModule/pushState", { config: JSON.stringify(this.config), currentPage: this.currentPage, diff --git a/src/stories/DropdownAndPages.stories.js b/src/stories/DropdownAndPages.stories.js index 5db2901b6..8de866df5 100644 --- a/src/stories/DropdownAndPages.stories.js +++ b/src/stories/DropdownAndPages.stories.js @@ -80,7 +80,6 @@ export const OpenPageUsingDropdown = { "[data-test=page-dropdown] button" ); let selectorAddPage = canvasElement.querySelector("[data-test=page-Page3]"); - console.log(selectorAddPage); await selector.click(selector); await selectorAddPage.click(selectorAddPage); // Open Page 3 (index=2) @@ -111,3 +110,37 @@ export const OpenPageUsingDropdown = { }); } }; + +// Open a page using the PageDropdown(index) +export const ScrollWithMoreThanTenPages = { + args: { + pages: [ + { name: "Page1" }, + { name: "Page2" }, + { name: "Page3" }, + { name: "Page4" }, + { name: "Page5" }, + { name: "Page6" }, + { name: "Page7" }, + { name: "Page8" }, + { name: "Page9" }, + { name: "Page10" }, + { name: "Page11" }, + { name: "Page12" } + ], + initialOpenedPages: [0] + }, + play: async ({ canvasElement }) => { + const selector = canvasElement.querySelector( + "[data-test=page-dropdown] button" + ); + await selector.click(selector); + // Test .page-dropdown-menu has scroll (scrollHeight > clientHeight) + await waitFor(() => { + const dropdownMenu = canvasElement.querySelector(".page-dropdown-menu"); + expect(dropdownMenu.scrollHeight).toBeGreaterThan( + dropdownMenu.clientHeight + ); + }); + } +}; diff --git a/src/stories/PageTabs.stories.js b/src/stories/PageTabs.stories.js index 49c9b2449..15833dd82 100644 --- a/src/stories/PageTabs.stories.js +++ b/src/stories/PageTabs.stories.js @@ -336,3 +336,60 @@ export const WithoutAnyPageOpened = { ); } }; + +// User can close tabs +export const UserCanCloseTabs = { + args: { + pages: [ + { name: "Page 1" }, + { name: "Page 2" }, + { name: "Page 3" }, + { name: "Page 4" }, + { name: "Page 5" } + ], + initialOpenedPages: [0, 1, 2, 3, 4] + }, + play: async ({ canvasElement, step }) => { + const canvas = within(canvasElement); + + // Close Tab #1 = Page 1 (tab index=0) + await step("Close Page 1 (tab index=0)", async () => { + canvas.getByTestId("close-tab-0").click(); + await waitFor( + () => { + expect(canvas.getByTestId("tab-content")).toContainHTML( + "Here comes content of Page 2 (#1)" + ); + }, + { timeout: 1000 } + ); + }); + + // Close Tab #1 = Page 2 (tab index=0) + await step("Close Page 2 (tab index=0)", async () => { + canvas.getByTestId("close-tab-0").click(); + await waitFor( + () => { + expect(canvas.getByTestId("tab-content")).toContainHTML( + "Here comes content of Page 3 (#2)" + ); + }, + { timeout: 1000 } + ); + }); + + // Close Tab #2 = Page 4 (tab index=1) + await step("Close Page 4 (tab index=1)", async () => { + canvas.getByTestId("close-tab-1").click(); + await waitFor( + () => { + // keep focus in the tab #1 + expect(canvas.getByTestId("tab-content")).toContainHTML( + "Here comes content of Page 3 (#2)" + ); + }, + { timeout: 1000 } + ); + }); + } +};