From 8fa95d26c2a1aed0c5717f057bda9d5a2ba8533c Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Mon, 2 Dec 2024 18:47:46 -0500 Subject: [PATCH 01/36] feat: add new Slickgrid-Vue to add VueJS support --- .github/workflows/vue-cypress.yml | 97 + demos/vue/index.html | 13 + demos/vue/package.json | 54 + demos/vue/public/vue.svg | 1 + demos/vue/src/App.vue | 61 + demos/vue/src/Home.vue | 40 + .../assets/data/collection_100_numbers.json | 12 + .../assets/data/collection_500_numbers.json | 52 + demos/vue/src/assets/data/countries.json | 245 +++ demos/vue/src/assets/data/country_names.json | 245 +++ demos/vue/src/assets/data/customers_100.json | 102 + demos/vue/src/assets/data/example-data.js | 16 + .../src/assets/locales/en/translation.json | 108 ++ .../src/assets/locales/fr/translation.json | 109 ++ demos/vue/src/assets/vue.svg | 1 + demos/vue/src/components/CustomFooter.vue | 13 + demos/vue/src/components/CustomPager.vue | 225 +++ demos/vue/src/components/Example01.vue | 166 ++ demos/vue/src/components/Example02.vue | 284 +++ demos/vue/src/components/Example03.vue | 764 ++++++++ demos/vue/src/components/Example04.vue | 443 +++++ demos/vue/src/components/Example05.vue | 640 +++++++ demos/vue/src/components/Example06.vue | 605 ++++++ demos/vue/src/components/Example07.vue | 313 ++++ demos/vue/src/components/Example08.vue | 279 +++ demos/vue/src/components/Example09.vue | 381 ++++ demos/vue/src/components/Example10.vue | 433 +++++ demos/vue/src/components/Example11.vue | 361 ++++ demos/vue/src/components/Example12.vue | 403 ++++ demos/vue/src/components/Example13.vue | 468 +++++ demos/vue/src/components/Example14.vue | 229 +++ demos/vue/src/components/Example15.vue | 323 ++++ demos/vue/src/components/Example16.vue | 367 ++++ demos/vue/src/components/Example18.vue | 542 ++++++ demos/vue/src/components/Example19.vue | 383 ++++ demos/vue/src/components/Example19Detail.vue | 96 + demos/vue/src/components/Example19Preload.vue | 8 + demos/vue/src/components/Example20.vue | 425 +++++ demos/vue/src/components/Example21.vue | 254 +++ demos/vue/src/components/Example22.vue | 222 +++ demos/vue/src/components/Example23.vue | 405 ++++ demos/vue/src/components/Example24.vue | 819 +++++++++ demos/vue/src/components/Example25.vue | 359 ++++ demos/vue/src/components/Example27.vue | 515 ++++++ demos/vue/src/components/Example28.vue | 627 +++++++ demos/vue/src/components/Example29.vue | 101 + demos/vue/src/components/Example30.vue | 1184 ++++++++++++ demos/vue/src/components/Example31.vue | 537 ++++++ demos/vue/src/components/Example32.vue | 921 +++++++++ demos/vue/src/components/Example33.vue | 597 ++++++ demos/vue/src/components/Example34.vue | 598 ++++++ demos/vue/src/components/Example35.vue | 346 ++++ demos/vue/src/components/Example36.vue | 607 ++++++ demos/vue/src/components/Example37.vue | 143 ++ demos/vue/src/components/Example38.vue | 532 ++++++ demos/vue/src/components/Example39.vue | 481 +++++ demos/vue/src/components/Example40.vue | 266 +++ demos/vue/src/components/Example41.vue | 312 ++++ demos/vue/src/components/Example42.vue | 245 +++ .../vue/src/components/custom-inputEditor.ts | 106 ++ .../vue/src/components/custom-inputFilter.ts | 143 ++ .../src/components/custom-title-formatter.ts | 9 + .../data/collection_100_numbers.json | 12 + .../data/collection_500_numbers.json | 52 + demos/vue/src/components/data/countries.json | 245 +++ .../src/components/data/country_names.json | 245 +++ .../src/components/data/customers_100.json | 102 + demos/vue/src/components/data/example-data.js | 16 + demos/vue/src/components/utilities.ts | 4 + demos/vue/src/main.ts | 34 + demos/vue/src/router/index.ts | 94 + demos/vue/src/styles.scss | 234 +++ demos/vue/src/vite-env.d.ts | 1 + demos/vue/test/cypress.config.mjs | 32 + demos/vue/test/cypress/e2e/example01.cy.ts | 456 +++++ demos/vue/test/cypress/e2e/example02.cy.ts | 25 + demos/vue/test/cypress/e2e/example03.cy.ts | 312 ++++ demos/vue/test/cypress/e2e/example04.cy.ts | 280 +++ demos/vue/test/cypress/e2e/example05.cy.ts | 805 ++++++++ demos/vue/test/cypress/e2e/example06.cy.ts | 962 ++++++++++ demos/vue/test/cypress/e2e/example07.cy.ts | 418 +++++ demos/vue/test/cypress/e2e/example08.cy.ts | 223 +++ demos/vue/test/cypress/e2e/example09.cy.ts | 483 +++++ demos/vue/test/cypress/e2e/example10.cy.ts | 594 ++++++ demos/vue/test/cypress/e2e/example11.cy.ts | 101 + demos/vue/test/cypress/e2e/example12.cy.ts | 334 ++++ demos/vue/test/cypress/e2e/example13.cy.ts | 223 +++ demos/vue/test/cypress/e2e/example14.cy.ts | 119 ++ demos/vue/test/cypress/e2e/example15.cy.ts | 721 ++++++++ demos/vue/test/cypress/e2e/example16.cy.ts | 431 +++++ demos/vue/test/cypress/e2e/example18.cy.ts | 381 ++++ demos/vue/test/cypress/e2e/example19.cy.ts | 344 ++++ demos/vue/test/cypress/e2e/example20.cy.ts | 222 +++ demos/vue/test/cypress/e2e/example21.cy.ts | 98 + demos/vue/test/cypress/e2e/example22.cy.ts | 49 + demos/vue/test/cypress/e2e/example23.cy.ts | 274 +++ demos/vue/test/cypress/e2e/example24.cy.ts | 917 +++++++++ demos/vue/test/cypress/e2e/example25.cy.ts | 200 ++ demos/vue/test/cypress/e2e/example27.cy.ts | 315 ++++ demos/vue/test/cypress/e2e/example28.cy.ts | 509 +++++ demos/vue/test/cypress/e2e/example29.cy.ts | 14 + demos/vue/test/cypress/e2e/example30.cy.ts | 753 ++++++++ demos/vue/test/cypress/e2e/example31.cy.ts | 825 +++++++++ demos/vue/test/cypress/e2e/example32.cy.ts | 325 ++++ demos/vue/test/cypress/e2e/example33.cy.ts | 257 +++ demos/vue/test/cypress/e2e/example34.cy.ts | 63 + demos/vue/test/cypress/e2e/example35.cy.ts | 169 ++ demos/vue/test/cypress/e2e/example36.cy.ts | 193 ++ demos/vue/test/cypress/e2e/example37.cy.ts | 57 + demos/vue/test/cypress/e2e/example38.cy.ts | 206 +++ demos/vue/test/cypress/e2e/example39.cy.ts | 172 ++ demos/vue/test/cypress/e2e/example40.cy.ts | 110 ++ demos/vue/test/cypress/e2e/example41.cy.ts | 94 + demos/vue/test/cypress/e2e/example42.cy.ts | 80 + demos/vue/test/cypress/e2e/home.cy.ts | 11 + demos/vue/test/cypress/fixtures/example.json | 5 + demos/vue/test/cypress/plugins/index.ts | 18 + demos/vue/test/cypress/plugins/utilities.ts | 26 + demos/vue/test/cypress/support/commands.ts | 74 + demos/vue/test/cypress/support/common.ts | 47 + demos/vue/test/cypress/support/drag.ts | 82 + demos/vue/test/cypress/support/index.ts | 25 + demos/vue/test/cypress/tsconfig.json | 10 + demos/vue/test/tsconfig.json | 16 + demos/vue/tsconfig.app.json | 26 + demos/vue/tsconfig.json | 4 + demos/vue/tsconfig.node.json | 24 + demos/vue/vite.config.ts | 35 + frameworks/slickgrid-vue/index.html | 14 + frameworks/slickgrid-vue/package.json | 68 + frameworks/slickgrid-vue/public/vue.svg | 1 + frameworks/slickgrid-vue/src/.npmignore | 2 + frameworks/slickgrid-vue/src/assets/vue.svg | 1 + .../src/components/SlickgridVue.vue | 1638 +++++++++++++++++ .../components/slickgridVueProps.interface.ts | 150 ++ frameworks/slickgrid-vue/src/constants.ts | 95 + .../src/extensions/slickRowDetailView.ts | 416 +++++ .../slickgrid-vue/src/global-grid-options.ts | 288 +++ frameworks/slickgrid-vue/src/index.ts | 23 + .../src/models/gridOption.interface.ts | 16 + frameworks/slickgrid-vue/src/models/index.ts | 5 + .../src/models/rowDetailView.interface.ts | 16 + .../models/viewModelBindableData.interface.ts | 10 + .../viewModelBindableInputData.interface.ts | 9 + .../src/models/vueGridInstance.interface.ts | 77 + .../src/services/container.service.ts | 13 + .../slickgrid-vue/src/services/index.ts | 3 + .../src/services/translater.service.ts | 41 + .../slickgrid-vue/src/services/utilities.ts | 18 + .../slickgrid-vue/src/services/vueUtils.ts | 26 + .../slickgrid-vue/src/slickgrid-config.ts | 10 + frameworks/slickgrid-vue/src/vite-env.d.ts | 1 + frameworks/slickgrid-vue/tsconfig.app.json | 24 + frameworks/slickgrid-vue/tsconfig.json | 7 + frameworks/slickgrid-vue/tsconfig.node.json | 24 + frameworks/slickgrid-vue/vite.config.ts | 40 + package.json | 6 +- pnpm-lock.yaml | 913 ++++++++- pnpm-workspace.yaml | 2 + test/cypress/e2e/example12.cy.ts | 3 +- 160 files changed, 38179 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/vue-cypress.yml create mode 100644 demos/vue/index.html create mode 100644 demos/vue/package.json create mode 100644 demos/vue/public/vue.svg create mode 100644 demos/vue/src/App.vue create mode 100644 demos/vue/src/Home.vue create mode 100644 demos/vue/src/assets/data/collection_100_numbers.json create mode 100644 demos/vue/src/assets/data/collection_500_numbers.json create mode 100644 demos/vue/src/assets/data/countries.json create mode 100644 demos/vue/src/assets/data/country_names.json create mode 100644 demos/vue/src/assets/data/customers_100.json create mode 100644 demos/vue/src/assets/data/example-data.js create mode 100644 demos/vue/src/assets/locales/en/translation.json create mode 100644 demos/vue/src/assets/locales/fr/translation.json create mode 100644 demos/vue/src/assets/vue.svg create mode 100644 demos/vue/src/components/CustomFooter.vue create mode 100644 demos/vue/src/components/CustomPager.vue create mode 100644 demos/vue/src/components/Example01.vue create mode 100644 demos/vue/src/components/Example02.vue create mode 100644 demos/vue/src/components/Example03.vue create mode 100644 demos/vue/src/components/Example04.vue create mode 100644 demos/vue/src/components/Example05.vue create mode 100644 demos/vue/src/components/Example06.vue create mode 100644 demos/vue/src/components/Example07.vue create mode 100644 demos/vue/src/components/Example08.vue create mode 100644 demos/vue/src/components/Example09.vue create mode 100644 demos/vue/src/components/Example10.vue create mode 100644 demos/vue/src/components/Example11.vue create mode 100644 demos/vue/src/components/Example12.vue create mode 100644 demos/vue/src/components/Example13.vue create mode 100644 demos/vue/src/components/Example14.vue create mode 100644 demos/vue/src/components/Example15.vue create mode 100644 demos/vue/src/components/Example16.vue create mode 100644 demos/vue/src/components/Example18.vue create mode 100644 demos/vue/src/components/Example19.vue create mode 100644 demos/vue/src/components/Example19Detail.vue create mode 100644 demos/vue/src/components/Example19Preload.vue create mode 100644 demos/vue/src/components/Example20.vue create mode 100644 demos/vue/src/components/Example21.vue create mode 100644 demos/vue/src/components/Example22.vue create mode 100644 demos/vue/src/components/Example23.vue create mode 100644 demos/vue/src/components/Example24.vue create mode 100644 demos/vue/src/components/Example25.vue create mode 100644 demos/vue/src/components/Example27.vue create mode 100644 demos/vue/src/components/Example28.vue create mode 100644 demos/vue/src/components/Example29.vue create mode 100644 demos/vue/src/components/Example30.vue create mode 100644 demos/vue/src/components/Example31.vue create mode 100644 demos/vue/src/components/Example32.vue create mode 100644 demos/vue/src/components/Example33.vue create mode 100644 demos/vue/src/components/Example34.vue create mode 100644 demos/vue/src/components/Example35.vue create mode 100644 demos/vue/src/components/Example36.vue create mode 100644 demos/vue/src/components/Example37.vue create mode 100644 demos/vue/src/components/Example38.vue create mode 100644 demos/vue/src/components/Example39.vue create mode 100644 demos/vue/src/components/Example40.vue create mode 100644 demos/vue/src/components/Example41.vue create mode 100644 demos/vue/src/components/Example42.vue create mode 100644 demos/vue/src/components/custom-inputEditor.ts create mode 100644 demos/vue/src/components/custom-inputFilter.ts create mode 100644 demos/vue/src/components/custom-title-formatter.ts create mode 100644 demos/vue/src/components/data/collection_100_numbers.json create mode 100644 demos/vue/src/components/data/collection_500_numbers.json create mode 100644 demos/vue/src/components/data/countries.json create mode 100644 demos/vue/src/components/data/country_names.json create mode 100644 demos/vue/src/components/data/customers_100.json create mode 100644 demos/vue/src/components/data/example-data.js create mode 100644 demos/vue/src/components/utilities.ts create mode 100644 demos/vue/src/main.ts create mode 100644 demos/vue/src/router/index.ts create mode 100644 demos/vue/src/styles.scss create mode 100644 demos/vue/src/vite-env.d.ts create mode 100644 demos/vue/test/cypress.config.mjs create mode 100644 demos/vue/test/cypress/e2e/example01.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example02.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example03.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example04.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example05.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example06.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example07.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example08.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example09.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example10.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example11.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example12.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example13.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example14.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example15.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example16.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example18.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example19.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example20.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example21.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example22.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example23.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example24.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example25.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example27.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example28.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example29.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example30.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example31.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example32.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example33.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example34.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example35.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example36.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example37.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example38.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example39.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example40.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example41.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example42.cy.ts create mode 100644 demos/vue/test/cypress/e2e/home.cy.ts create mode 100644 demos/vue/test/cypress/fixtures/example.json create mode 100644 demos/vue/test/cypress/plugins/index.ts create mode 100644 demos/vue/test/cypress/plugins/utilities.ts create mode 100644 demos/vue/test/cypress/support/commands.ts create mode 100644 demos/vue/test/cypress/support/common.ts create mode 100644 demos/vue/test/cypress/support/drag.ts create mode 100644 demos/vue/test/cypress/support/index.ts create mode 100644 demos/vue/test/cypress/tsconfig.json create mode 100644 demos/vue/test/tsconfig.json create mode 100644 demos/vue/tsconfig.app.json create mode 100644 demos/vue/tsconfig.json create mode 100644 demos/vue/tsconfig.node.json create mode 100644 demos/vue/vite.config.ts create mode 100644 frameworks/slickgrid-vue/index.html create mode 100644 frameworks/slickgrid-vue/package.json create mode 100644 frameworks/slickgrid-vue/public/vue.svg create mode 100644 frameworks/slickgrid-vue/src/.npmignore create mode 100644 frameworks/slickgrid-vue/src/assets/vue.svg create mode 100644 frameworks/slickgrid-vue/src/components/SlickgridVue.vue create mode 100644 frameworks/slickgrid-vue/src/components/slickgridVueProps.interface.ts create mode 100644 frameworks/slickgrid-vue/src/constants.ts create mode 100644 frameworks/slickgrid-vue/src/extensions/slickRowDetailView.ts create mode 100644 frameworks/slickgrid-vue/src/global-grid-options.ts create mode 100644 frameworks/slickgrid-vue/src/index.ts create mode 100644 frameworks/slickgrid-vue/src/models/gridOption.interface.ts create mode 100644 frameworks/slickgrid-vue/src/models/index.ts create mode 100644 frameworks/slickgrid-vue/src/models/rowDetailView.interface.ts create mode 100644 frameworks/slickgrid-vue/src/models/viewModelBindableData.interface.ts create mode 100644 frameworks/slickgrid-vue/src/models/viewModelBindableInputData.interface.ts create mode 100644 frameworks/slickgrid-vue/src/models/vueGridInstance.interface.ts create mode 100644 frameworks/slickgrid-vue/src/services/container.service.ts create mode 100644 frameworks/slickgrid-vue/src/services/index.ts create mode 100644 frameworks/slickgrid-vue/src/services/translater.service.ts create mode 100644 frameworks/slickgrid-vue/src/services/utilities.ts create mode 100644 frameworks/slickgrid-vue/src/services/vueUtils.ts create mode 100644 frameworks/slickgrid-vue/src/slickgrid-config.ts create mode 100644 frameworks/slickgrid-vue/src/vite-env.d.ts create mode 100644 frameworks/slickgrid-vue/tsconfig.app.json create mode 100644 frameworks/slickgrid-vue/tsconfig.json create mode 100644 frameworks/slickgrid-vue/tsconfig.node.json create mode 100644 frameworks/slickgrid-vue/vite.config.ts diff --git a/.github/workflows/vue-cypress.yml b/.github/workflows/vue-cypress.yml new file mode 100644 index 000000000..e18da9d20 --- /dev/null +++ b/.github/workflows/vue-cypress.yml @@ -0,0 +1,97 @@ +name: Vue E2E Tests (Cypress) + +on: + push: + branches: + - master + - next + pull_request: + branches: + - '**' + paths-ignore: + - "**.md" + - "!.github/workflows/ci.yml" +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + run: + strategy: + fail-fast: false + matrix: + node: [20] + platform: + - ubuntu-latest + + name: '${{matrix.platform}} / Node ${{ matrix.node }}' + runs-on: ${{matrix.platform}} + if: ${{ !startsWith(github.event.head_commit.message, 'docs:') }} + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set NodeJS + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + + - name: Install pnpm + uses: pnpm/action-setup@v3 + with: + version: 9 + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - uses: pnpm/action-setup@v3 + with: + version: 9 + run_install: true + + - run: pnpm --version + + - name: TSC Build (esm) + run: pnpm predev + + - name: Website Dev Build (served for Cypress) + run: pnpm vue:build + + - name: Start HTTP Server + run: pnpm vue:serve & + + - name: Run Cypress E2E tests + uses: cypress-io/github-action@v6 + with: + install: false + # working-directory: packages/dnd + start: pnpm vue:serve + # start: pnpm serve:vite + wait-on: 'http://localhost:7000' + config-file: test/cypress.config.ts + browser: chrome + record: true + env: + # pass the Dashboard record key as an environment variable + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + # pass GitHub token to allow accurately detecting a build vs a re-run build + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + Cypress_extended: true + + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-screenshots + path: test/cypress/screenshots diff --git a/demos/vue/index.html b/demos/vue/index.html new file mode 100644 index 000000000..8a7e903fd --- /dev/null +++ b/demos/vue/index.html @@ -0,0 +1,13 @@ + + + + + + + Slickgrid-Vue + + +
+ + + diff --git a/demos/vue/package.json b/demos/vue/package.json new file mode 100644 index 000000000..815b3df91 --- /dev/null +++ b/demos/vue/package.json @@ -0,0 +1,54 @@ +{ + "name": "slickgrid-vue-demo", + "private": true, + "version": "0.1.0", + "type": "module", + "author": { + "name": "Ghislain B." + }, + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "cypress": "cypress open --config-file test/cypress.config.mjs", + "cypress:ci": "cypress run --config-file test/cypress.config.mjs", + "serve:demo": "servor ./dist index.html 7000" + }, + "dependencies": { + "@faker-js/faker": "^9.2.0", + "@fnando/sparkline": "^0.3.10", + "@formkit/tempo": "^0.1.2", + "@popperjs/core": "^2.11.8", + "@slickgrid-universal/common": "workspace:*", + "@slickgrid-universal/composite-editor-component": "workspace:*", + "@slickgrid-universal/custom-tooltip-plugin": "workspace:*", + "@slickgrid-universal/event-pub-sub": "~5.10.2", + "@slickgrid-universal/excel-export": "workspace:*", + "@slickgrid-universal/graphql": "workspace:*", + "@slickgrid-universal/odata": "workspace:*", + "@slickgrid-universal/row-detail-view-plugin": "workspace:*", + "@slickgrid-universal/rxjs-observable": "workspace:*", + "@slickgrid-universal/text-export": "workspace:*", + "bootstrap": "^5.3.3", + "dompurify": "^3.2.2", + "i18next": "^24.0.2", + "i18next-http-backend": "^3.0.1", + "i18next-vue": "^5.0.0", + "rxjs": "^7.8.1", + "slickgrid-vue": "workspace:*", + "vue": "^3.5.13", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@4tw/cypress-drag-drop": "^2.2.5", + "@types/fnando__sparkline": "^0.3.7", + "@vitejs/plugin-vue": "^5.2.1", + "cypress": "^13.16.0", + "cypress-real-events": "^1.13.0", + "fetch-jsonp": "^1.3.0", + "sass": "^1.81.0", + "servor": "^4.0.2", + "typescript": "~5.6.2", + "vite": "^6.0.1" + } +} \ No newline at end of file diff --git a/demos/vue/public/vue.svg b/demos/vue/public/vue.svg new file mode 100644 index 000000000..d0edf9b9a --- /dev/null +++ b/demos/vue/public/vue.svg @@ -0,0 +1 @@ + diff --git a/demos/vue/src/App.vue b/demos/vue/src/App.vue new file mode 100644 index 000000000..7d59433f9 --- /dev/null +++ b/demos/vue/src/App.vue @@ -0,0 +1,61 @@ + + + diff --git a/demos/vue/src/Home.vue b/demos/vue/src/Home.vue new file mode 100644 index 000000000..9b6f2a02b --- /dev/null +++ b/demos/vue/src/Home.vue @@ -0,0 +1,40 @@ + + diff --git a/demos/vue/src/assets/data/collection_100_numbers.json b/demos/vue/src/assets/data/collection_100_numbers.json new file mode 100644 index 000000000..fd1cbc1c5 --- /dev/null +++ b/demos/vue/src/assets/data/collection_100_numbers.json @@ -0,0 +1,12 @@ +[ + { "value": 0, "label": 0, "prefix": "Task", "suffix": "day" }, { "value": 1, "label": 1, "prefix": "Task", "suffix": "day" }, { "value": 2, "label": 2, "prefix": "Task", "suffix": "days" }, { "value": 3, "label": 3, "prefix": "Task", "suffix": "days" }, { "value": 4, "label": 4, "prefix": "Task", "suffix": "days" }, { "value": 5, "label": 5, "prefix": "Task", "suffix": "days" }, { "value": 6, "label": 6, "prefix": "Task", "suffix": "days" }, { "value": 7, "label": 7, "prefix": "Task", "suffix": "days" }, { "value": 8, "label": 8, "prefix": "Task", "suffix": "days" }, { "value": 9, "label": 9, "prefix": "Task", "suffix": "days" }, + { "value": 10, "label": 10, "prefix": "Task", "suffix": "days" }, { "value": 11, "label": 11, "prefix": "Task", "suffix": "days" }, { "value": 12, "label": 12, "prefix": "Task", "suffix": "days" }, { "value": 13, "label": 13, "prefix": "Task", "suffix": "days" }, { "value": 14, "label": 14, "prefix": "Task", "suffix": "days" }, { "value": 15, "label": 15, "prefix": "Task", "suffix": "days" }, { "value": 16, "label": 16, "prefix": "Task", "suffix": "days" }, { "value": 17, "label": 17, "prefix": "Task", "suffix": "days" }, { "value": 18, "label": 18, "prefix": "Task", "suffix": "days" }, { "value": 19, "label": 19, "prefix": "Task", "suffix": "days" }, + { "value": 20, "label": 20, "prefix": "Task", "suffix": "days" }, { "value": 21, "label": 21, "prefix": "Task", "suffix": "days" }, { "value": 22, "label": 22, "prefix": "Task", "suffix": "days" }, { "value": 23, "label": 23, "prefix": "Task", "suffix": "days" }, { "value": 24, "label": 24, "prefix": "Task", "suffix": "days" }, { "value": 25, "label": 25, "prefix": "Task", "suffix": "days" }, { "value": 26, "label": 26, "prefix": "Task", "suffix": "days" }, { "value": 27, "label": 27, "prefix": "Task", "suffix": "days" }, { "value": 28, "label": 28, "prefix": "Task", "suffix": "days" }, { "value": 29, "label": 29, "prefix": "Task", "suffix": "days" }, + { "value": 30, "label": 30, "prefix": "Task", "suffix": "days" }, { "value": 31, "label": 31, "prefix": "Task", "suffix": "days" }, { "value": 32, "label": 32, "prefix": "Task", "suffix": "days" }, { "value": 33, "label": 33, "prefix": "Task", "suffix": "days" }, { "value": 34, "label": 34, "prefix": "Task", "suffix": "days" }, { "value": 35, "label": 35, "prefix": "Task", "suffix": "days" }, { "value": 36, "label": 36, "prefix": "Task", "suffix": "days" }, { "value": 37, "label": 37, "prefix": "Task", "suffix": "days" }, { "value": 38, "label": 38, "prefix": "Task", "suffix": "days" }, { "value": 39, "label": 39, "prefix": "Task", "suffix": "days" }, + { "value": 40, "label": 40, "prefix": "Task", "suffix": "days" }, { "value": 41, "label": 41, "prefix": "Task", "suffix": "days" }, { "value": 42, "label": 42, "prefix": "Task", "suffix": "days" }, { "value": 43, "label": 43, "prefix": "Task", "suffix": "days" }, { "value": 44, "label": 44, "prefix": "Task", "suffix": "days" }, { "value": 45, "label": 45, "prefix": "Task", "suffix": "days" }, { "value": 46, "label": 46, "prefix": "Task", "suffix": "days" }, { "value": 47, "label": 47, "prefix": "Task", "suffix": "days" }, { "value": 48, "label": 48, "prefix": "Task", "suffix": "days" }, { "value": 49, "label": 49, "prefix": "Task", "suffix": "days" }, + { "value": 50, "label": 50, "prefix": "Task", "suffix": "days" }, { "value": 51, "label": 51, "prefix": "Task", "suffix": "days" }, { "value": 52, "label": 52, "prefix": "Task", "suffix": "days" }, { "value": 53, "label": 53, "prefix": "Task", "suffix": "days" }, { "value": 54, "label": 54, "prefix": "Task", "suffix": "days" }, { "value": 55, "label": 55, "prefix": "Task", "suffix": "days" }, { "value": 56, "label": 56, "prefix": "Task", "suffix": "days" }, { "value": 57, "label": 57, "prefix": "Task", "suffix": "days" }, { "value": 58, "label": 58, "prefix": "Task", "suffix": "days" }, { "value": 59, "label": 59, "prefix": "Task", "suffix": "days" }, + { "value": 60, "label": 60, "prefix": "Task", "suffix": "days" }, { "value": 61, "label": 61, "prefix": "Task", "suffix": "days" }, { "value": 62, "label": 62, "prefix": "Task", "suffix": "days" }, { "value": 63, "label": 63, "prefix": "Task", "suffix": "days" }, { "value": 64, "label": 64, "prefix": "Task", "suffix": "days" }, { "value": 65, "label": 65, "prefix": "Task", "suffix": "days" }, { "value": 66, "label": 66, "prefix": "Task", "suffix": "days" }, { "value": 67, "label": 67, "prefix": "Task", "suffix": "days" }, { "value": 68, "label": 68, "prefix": "Task", "suffix": "days" }, { "value": 69, "label": 69, "prefix": "Task", "suffix": "days" }, + { "value": 70, "label": 70, "prefix": "Task", "suffix": "days" }, { "value": 71, "label": 71, "prefix": "Task", "suffix": "days" }, { "value": 72, "label": 72, "prefix": "Task", "suffix": "days" }, { "value": 73, "label": 73, "prefix": "Task", "suffix": "days" }, { "value": 74, "label": 74, "prefix": "Task", "suffix": "days" }, { "value": 75, "label": 75, "prefix": "Task", "suffix": "days" }, { "value": 76, "label": 76, "prefix": "Task", "suffix": "days" }, { "value": 77, "label": 77, "prefix": "Task", "suffix": "days" }, { "value": 78, "label": 78, "prefix": "Task", "suffix": "days" }, { "value": 79, "label": 79, "prefix": "Task", "suffix": "days" }, + { "value": 80, "label": 80, "prefix": "Task", "suffix": "days" }, { "value": 81, "label": 81, "prefix": "Task", "suffix": "days" }, { "value": 82, "label": 82, "prefix": "Task", "suffix": "days" }, { "value": 83, "label": 83, "prefix": "Task", "suffix": "days" }, { "value": 84, "label": 84, "prefix": "Task", "suffix": "days" }, { "value": 85, "label": 85, "prefix": "Task", "suffix": "days" }, { "value": 86, "label": 86, "prefix": "Task", "suffix": "days" }, { "value": 87, "label": 87, "prefix": "Task", "suffix": "days" }, { "value": 88, "label": 88, "prefix": "Task", "suffix": "days" }, { "value": 89, "label": 89, "prefix": "Task", "suffix": "days" }, + { "value": 90, "label": 90, "prefix": "Task", "suffix": "days" }, { "value": 91, "label": 91, "prefix": "Task", "suffix": "days" }, { "value": 92, "label": 92, "prefix": "Task", "suffix": "days" }, { "value": 93, "label": 93, "prefix": "Task", "suffix": "days" }, { "value": 94, "label": 94, "prefix": "Task", "suffix": "days" }, { "value": 95, "label": 95, "prefix": "Task", "suffix": "days" }, { "value": 96, "label": 96, "prefix": "Task", "suffix": "days" }, { "value": 97, "label": 97, "prefix": "Task", "suffix": "days" }, { "value": 98, "label": 98, "prefix": "Task", "suffix": "days" }, { "value": 99, "label": 99, "prefix": "Task", "suffix": "days" } +] diff --git a/demos/vue/src/assets/data/collection_500_numbers.json b/demos/vue/src/assets/data/collection_500_numbers.json new file mode 100644 index 000000000..1c6b4321d --- /dev/null +++ b/demos/vue/src/assets/data/collection_500_numbers.json @@ -0,0 +1,52 @@ +[ + { "value": 0, "label": 0, "text": "day" }, { "value": 1, "label": 1, "text": "day" }, { "value": 2, "label": 2, "text": "days" }, { "value": 3, "label": 3, "text": "days" }, { "value": 4, "label": 4, "text": "days" }, { "value": 5, "label": 5, "text": "days" }, { "value": 6, "label": 6, "text": "days" }, { "value": 7, "label": 7, "text": "days" }, { "value": 8, "label": 8, "text": "days" }, { "value": 9, "label": 9, "text": "days" }, + { "value": 10, "label": 10, "text": "days" }, { "value": 11, "label": 11, "text": "days" }, { "value": 12, "label": 12, "text": "days" }, { "value": 13, "label": 13, "text": "days" }, { "value": 14, "label": 14, "text": "days" }, { "value": 15, "label": 15, "text": "days" }, { "value": 16, "label": 16, "text": "days" }, { "value": 17, "label": 17, "text": "days" }, { "value": 18, "label": 18, "text": "days" }, { "value": 19, "label": 19, "text": "days" }, + { "value": 20, "label": 20, "text": "days" }, { "value": 21, "label": 21, "text": "days" }, { "value": 22, "label": 22, "text": "days" }, { "value": 23, "label": 23, "text": "days" }, { "value": 24, "label": 24, "text": "days" }, { "value": 25, "label": 25, "text": "days" }, { "value": 26, "label": 26, "text": "days" }, { "value": 27, "label": 27, "text": "days" }, { "value": 28, "label": 28, "text": "days" }, { "value": 29, "label": 29, "text": "days" }, + { "value": 30, "label": 30, "text": "days" }, { "value": 31, "label": 31, "text": "days" }, { "value": 32, "label": 32, "text": "days" }, { "value": 33, "label": 33, "text": "days" }, { "value": 34, "label": 34, "text": "days" }, { "value": 35, "label": 35, "text": "days" }, { "value": 36, "label": 36, "text": "days" }, { "value": 37, "label": 37, "text": "days" }, { "value": 38, "label": 38, "text": "days" }, { "value": 39, "label": 39, "text": "days" }, + { "value": 40, "label": 40, "text": "days" }, { "value": 41, "label": 41, "text": "days" }, { "value": 42, "label": 42, "text": "days" }, { "value": 43, "label": 43, "text": "days" }, { "value": 44, "label": 44, "text": "days" }, { "value": 45, "label": 45, "text": "days" }, { "value": 46, "label": 46, "text": "days" }, { "value": 47, "label": 47, "text": "days" }, { "value": 48, "label": 48, "text": "days" }, { "value": 49, "label": 49, "text": "days" }, + { "value": 50, "label": 50, "text": "days" }, { "value": 51, "label": 51, "text": "days" }, { "value": 52, "label": 52, "text": "days" }, { "value": 53, "label": 53, "text": "days" }, { "value": 54, "label": 54, "text": "days" }, { "value": 55, "label": 55, "text": "days" }, { "value": 56, "label": 56, "text": "days" }, { "value": 57, "label": 57, "text": "days" }, { "value": 58, "label": 58, "text": "days" }, { "value": 59, "label": 59, "text": "days" }, + { "value": 60, "label": 60, "text": "days" }, { "value": 61, "label": 61, "text": "days" }, { "value": 62, "label": 62, "text": "days" }, { "value": 63, "label": 63, "text": "days" }, { "value": 64, "label": 64, "text": "days" }, { "value": 65, "label": 65, "text": "days" }, { "value": 66, "label": 66, "text": "days" }, { "value": 67, "label": 67, "text": "days" }, { "value": 68, "label": 68, "text": "days" }, { "value": 69, "label": 69, "text": "days" }, + { "value": 70, "label": 70, "text": "days" }, { "value": 71, "label": 71, "text": "days" }, { "value": 72, "label": 72, "text": "days" }, { "value": 73, "label": 73, "text": "days" }, { "value": 74, "label": 74, "text": "days" }, { "value": 75, "label": 75, "text": "days" }, { "value": 76, "label": 76, "text": "days" }, { "value": 77, "label": 77, "text": "days" }, { "value": 78, "label": 78, "text": "days" }, { "value": 79, "label": 79, "text": "days" }, + { "value": 80, "label": 80, "text": "days" }, { "value": 81, "label": 81, "text": "days" }, { "value": 82, "label": 82, "text": "days" }, { "value": 83, "label": 83, "text": "days" }, { "value": 84, "label": 84, "text": "days" }, { "value": 85, "label": 85, "text": "days" }, { "value": 86, "label": 86, "text": "days" }, { "value": 87, "label": 87, "text": "days" }, { "value": 88, "label": 88, "text": "days" }, { "value": 89, "label": 89, "text": "days" }, + { "value": 90, "label": 90, "text": "days" }, { "value": 91, "label": 91, "text": "days" }, { "value": 92, "label": 92, "text": "days" }, { "value": 93, "label": 93, "text": "days" }, { "value": 94, "label": 94, "text": "days" }, { "value": 95, "label": 95, "text": "days" }, { "value": 96, "label": 96, "text": "days" }, { "value": 97, "label": 97, "text": "days" }, { "value": 98, "label": 98, "text": "days" }, { "value": 99, "label": 99, "text": "days" }, + { "value": 100, "label": 100, "text": "days" }, { "value": 101, "label": 101, "text": "days" }, { "value": 102, "label": 102, "text": "days" }, { "value": 103, "label": 103, "text": "days" }, { "value": 104, "label": 104, "text": "days" }, { "value": 105, "label": 105, "text": "days" }, { "value": 106, "label": 106, "text": "days" }, { "value": 107, "label": 107, "text": "days" }, { "value": 108, "label": 108, "text": "days" }, { "value": 109, "label": 109, "text": "days" }, + { "value": 110, "label": 110, "text": "days" }, { "value": 111, "label": 111, "text": "days" }, { "value": 112, "label": 112, "text": "days" }, { "value": 113, "label": 113, "text": "days" }, { "value": 114, "label": 114, "text": "days" }, { "value": 115, "label": 115, "text": "days" }, { "value": 116, "label": 116, "text": "days" }, { "value": 117, "label": 117, "text": "days" }, { "value": 118, "label": 118, "text": "days" }, { "value": 119, "label": 119, "text": "days" }, + { "value": 120, "label": 120, "text": "days" }, { "value": 121, "label": 121, "text": "days" }, { "value": 122, "label": 122, "text": "days" }, { "value": 123, "label": 123, "text": "days" }, { "value": 124, "label": 124, "text": "days" }, { "value": 125, "label": 125, "text": "days" }, { "value": 126, "label": 126, "text": "days" }, { "value": 127, "label": 127, "text": "days" }, { "value": 128, "label": 128, "text": "days" }, { "value": 129, "label": 129, "text": "days" }, + { "value": 130, "label": 130, "text": "days" }, { "value": 131, "label": 131, "text": "days" }, { "value": 132, "label": 132, "text": "days" }, { "value": 133, "label": 133, "text": "days" }, { "value": 134, "label": 134, "text": "days" }, { "value": 135, "label": 135, "text": "days" }, { "value": 136, "label": 136, "text": "days" }, { "value": 137, "label": 137, "text": "days" }, { "value": 138, "label": 138, "text": "days" }, { "value": 139, "label": 139, "text": "days" }, + { "value": 140, "label": 140, "text": "days" }, { "value": 141, "label": 141, "text": "days" }, { "value": 142, "label": 142, "text": "days" }, { "value": 143, "label": 143, "text": "days" }, { "value": 144, "label": 144, "text": "days" }, { "value": 145, "label": 145, "text": "days" }, { "value": 146, "label": 146, "text": "days" }, { "value": 147, "label": 147, "text": "days" }, { "value": 148, "label": 148, "text": "days" }, { "value": 149, "label": 149, "text": "days" }, + { "value": 150, "label": 150, "text": "days" }, { "value": 151, "label": 151, "text": "days" }, { "value": 152, "label": 152, "text": "days" }, { "value": 153, "label": 153, "text": "days" }, { "value": 154, "label": 154, "text": "days" }, { "value": 155, "label": 155, "text": "days" }, { "value": 156, "label": 156, "text": "days" }, { "value": 157, "label": 157, "text": "days" }, { "value": 158, "label": 158, "text": "days" }, { "value": 159, "label": 159, "text": "days" }, + { "value": 160, "label": 160, "text": "days" }, { "value": 161, "label": 161, "text": "days" }, { "value": 162, "label": 162, "text": "days" }, { "value": 163, "label": 163, "text": "days" }, { "value": 164, "label": 164, "text": "days" }, { "value": 165, "label": 165, "text": "days" }, { "value": 166, "label": 166, "text": "days" }, { "value": 167, "label": 167, "text": "days" }, { "value": 168, "label": 168, "text": "days" }, { "value": 169, "label": 169, "text": "days" }, + { "value": 170, "label": 170, "text": "days" }, { "value": 171, "label": 171, "text": "days" }, { "value": 172, "label": 172, "text": "days" }, { "value": 173, "label": 173, "text": "days" }, { "value": 174, "label": 174, "text": "days" }, { "value": 175, "label": 175, "text": "days" }, { "value": 176, "label": 176, "text": "days" }, { "value": 177, "label": 177, "text": "days" }, { "value": 178, "label": 178, "text": "days" }, { "value": 179, "label": 179, "text": "days" }, + { "value": 180, "label": 180, "text": "days" }, { "value": 181, "label": 181, "text": "days" }, { "value": 182, "label": 182, "text": "days" }, { "value": 183, "label": 183, "text": "days" }, { "value": 184, "label": 184, "text": "days" }, { "value": 185, "label": 185, "text": "days" }, { "value": 186, "label": 186, "text": "days" }, { "value": 187, "label": 187, "text": "days" }, { "value": 188, "label": 188, "text": "days" }, { "value": 189, "label": 189, "text": "days" }, + { "value": 190, "label": 190, "text": "days" }, { "value": 191, "label": 191, "text": "days" }, { "value": 192, "label": 192, "text": "days" }, { "value": 193, "label": 193, "text": "days" }, { "value": 194, "label": 194, "text": "days" }, { "value": 195, "label": 195, "text": "days" }, { "value": 196, "label": 196, "text": "days" }, { "value": 197, "label": 197, "text": "days" }, { "value": 198, "label": 198, "text": "days" }, { "value": 199, "label": 199, "text": "days" }, + { "value": 200, "label": 200, "text": "days" }, { "value": 201, "label": 201, "text": "days" }, { "value": 202, "label": 202, "text": "days" }, { "value": 203, "label": 203, "text": "days" }, { "value": 204, "label": 204, "text": "days" }, { "value": 205, "label": 205, "text": "days" }, { "value": 206, "label": 206, "text": "days" }, { "value": 207, "label": 207, "text": "days" }, { "value": 208, "label": 208, "text": "days" }, { "value": 209, "label": 209, "text": "days" }, + { "value": 210, "label": 210, "text": "days" }, { "value": 211, "label": 211, "text": "days" }, { "value": 212, "label": 212, "text": "days" }, { "value": 213, "label": 213, "text": "days" }, { "value": 214, "label": 214, "text": "days" }, { "value": 215, "label": 215, "text": "days" }, { "value": 216, "label": 216, "text": "days" }, { "value": 217, "label": 217, "text": "days" }, { "value": 218, "label": 218, "text": "days" }, { "value": 219, "label": 219, "text": "days" }, + { "value": 220, "label": 220, "text": "days" }, { "value": 221, "label": 221, "text": "days" }, { "value": 222, "label": 222, "text": "days" }, { "value": 223, "label": 223, "text": "days" }, { "value": 224, "label": 224, "text": "days" }, { "value": 225, "label": 225, "text": "days" }, { "value": 226, "label": 226, "text": "days" }, { "value": 227, "label": 227, "text": "days" }, { "value": 228, "label": 228, "text": "days" }, { "value": 229, "label": 229, "text": "days" }, + { "value": 230, "label": 230, "text": "days" }, { "value": 231, "label": 231, "text": "days" }, { "value": 232, "label": 232, "text": "days" }, { "value": 233, "label": 233, "text": "days" }, { "value": 234, "label": 234, "text": "days" }, { "value": 235, "label": 235, "text": "days" }, { "value": 236, "label": 236, "text": "days" }, { "value": 237, "label": 237, "text": "days" }, { "value": 238, "label": 238, "text": "days" }, { "value": 239, "label": 239, "text": "days" }, + { "value": 240, "label": 240, "text": "days" }, { "value": 241, "label": 241, "text": "days" }, { "value": 242, "label": 242, "text": "days" }, { "value": 243, "label": 243, "text": "days" }, { "value": 244, "label": 244, "text": "days" }, { "value": 245, "label": 245, "text": "days" }, { "value": 246, "label": 246, "text": "days" }, { "value": 247, "label": 247, "text": "days" }, { "value": 248, "label": 248, "text": "days" }, { "value": 249, "label": 249, "text": "days" }, + { "value": 250, "label": 250, "text": "days" }, { "value": 251, "label": 251, "text": "days" }, { "value": 252, "label": 252, "text": "days" }, { "value": 253, "label": 253, "text": "days" }, { "value": 254, "label": 254, "text": "days" }, { "value": 255, "label": 255, "text": "days" }, { "value": 256, "label": 256, "text": "days" }, { "value": 257, "label": 257, "text": "days" }, { "value": 258, "label": 258, "text": "days" }, { "value": 259, "label": 259, "text": "days" }, + { "value": 260, "label": 260, "text": "days" }, { "value": 261, "label": 261, "text": "days" }, { "value": 262, "label": 262, "text": "days" }, { "value": 263, "label": 263, "text": "days" }, { "value": 264, "label": 264, "text": "days" }, { "value": 265, "label": 265, "text": "days" }, { "value": 266, "label": 266, "text": "days" }, { "value": 267, "label": 267, "text": "days" }, { "value": 268, "label": 268, "text": "days" }, { "value": 269, "label": 269, "text": "days" }, + { "value": 270, "label": 270, "text": "days" }, { "value": 271, "label": 271, "text": "days" }, { "value": 272, "label": 272, "text": "days" }, { "value": 273, "label": 273, "text": "days" }, { "value": 274, "label": 274, "text": "days" }, { "value": 275, "label": 275, "text": "days" }, { "value": 276, "label": 276, "text": "days" }, { "value": 277, "label": 277, "text": "days" }, { "value": 278, "label": 278, "text": "days" }, { "value": 279, "label": 279, "text": "days" }, + { "value": 280, "label": 280, "text": "days" }, { "value": 281, "label": 281, "text": "days" }, { "value": 282, "label": 282, "text": "days" }, { "value": 283, "label": 283, "text": "days" }, { "value": 284, "label": 284, "text": "days" }, { "value": 285, "label": 285, "text": "days" }, { "value": 286, "label": 286, "text": "days" }, { "value": 287, "label": 287, "text": "days" }, { "value": 288, "label": 288, "text": "days" }, { "value": 289, "label": 289, "text": "days" }, + { "value": 290, "label": 290, "text": "days" }, { "value": 291, "label": 291, "text": "days" }, { "value": 292, "label": 292, "text": "days" }, { "value": 293, "label": 293, "text": "days" }, { "value": 294, "label": 294, "text": "days" }, { "value": 295, "label": 295, "text": "days" }, { "value": 296, "label": 296, "text": "days" }, { "value": 297, "label": 297, "text": "days" }, { "value": 298, "label": 298, "text": "days" }, { "value": 299, "label": 299, "text": "days" }, + { "value": 300, "label": 300, "text": "days" }, { "value": 301, "label": 301, "text": "days" }, { "value": 302, "label": 302, "text": "days" }, { "value": 303, "label": 303, "text": "days" }, { "value": 304, "label": 304, "text": "days" }, { "value": 305, "label": 305, "text": "days" }, { "value": 306, "label": 306, "text": "days" }, { "value": 307, "label": 307, "text": "days" }, { "value": 308, "label": 308, "text": "days" }, { "value": 309, "label": 309, "text": "days" }, + { "value": 310, "label": 310, "text": "days" }, { "value": 311, "label": 311, "text": "days" }, { "value": 312, "label": 312, "text": "days" }, { "value": 313, "label": 313, "text": "days" }, { "value": 314, "label": 314, "text": "days" }, { "value": 315, "label": 315, "text": "days" }, { "value": 316, "label": 316, "text": "days" }, { "value": 317, "label": 317, "text": "days" }, { "value": 318, "label": 318, "text": "days" }, { "value": 319, "label": 319, "text": "days" }, + { "value": 320, "label": 320, "text": "days" }, { "value": 321, "label": 321, "text": "days" }, { "value": 322, "label": 322, "text": "days" }, { "value": 323, "label": 323, "text": "days" }, { "value": 324, "label": 324, "text": "days" }, { "value": 325, "label": 325, "text": "days" }, { "value": 326, "label": 326, "text": "days" }, { "value": 327, "label": 327, "text": "days" }, { "value": 328, "label": 328, "text": "days" }, { "value": 329, "label": 329, "text": "days" }, + { "value": 330, "label": 330, "text": "days" }, { "value": 331, "label": 331, "text": "days" }, { "value": 332, "label": 332, "text": "days" }, { "value": 333, "label": 333, "text": "days" }, { "value": 334, "label": 334, "text": "days" }, { "value": 335, "label": 335, "text": "days" }, { "value": 336, "label": 336, "text": "days" }, { "value": 337, "label": 337, "text": "days" }, { "value": 338, "label": 338, "text": "days" }, { "value": 339, "label": 339, "text": "days" }, + { "value": 340, "label": 340, "text": "days" }, { "value": 341, "label": 341, "text": "days" }, { "value": 342, "label": 342, "text": "days" }, { "value": 343, "label": 343, "text": "days" }, { "value": 344, "label": 344, "text": "days" }, { "value": 345, "label": 345, "text": "days" }, { "value": 346, "label": 346, "text": "days" }, { "value": 347, "label": 347, "text": "days" }, { "value": 348, "label": 348, "text": "days" }, { "value": 349, "label": 349, "text": "days" }, + { "value": 350, "label": 350, "text": "days" }, { "value": 351, "label": 351, "text": "days" }, { "value": 352, "label": 352, "text": "days" }, { "value": 353, "label": 353, "text": "days" }, { "value": 354, "label": 354, "text": "days" }, { "value": 355, "label": 355, "text": "days" }, { "value": 356, "label": 356, "text": "days" }, { "value": 357, "label": 357, "text": "days" }, { "value": 358, "label": 358, "text": "days" }, { "value": 359, "label": 359, "text": "days" }, + { "value": 360, "label": 360, "text": "days" }, { "value": 361, "label": 361, "text": "days" }, { "value": 362, "label": 362, "text": "days" }, { "value": 363, "label": 363, "text": "days" }, { "value": 364, "label": 364, "text": "days" }, { "value": 365, "label": 365, "text": "days" }, { "value": 366, "label": 366, "text": "days" }, { "value": 367, "label": 367, "text": "days" }, { "value": 368, "label": 368, "text": "days" }, { "value": 369, "label": 369, "text": "days" }, + { "value": 370, "label": 370, "text": "days" }, { "value": 371, "label": 371, "text": "days" }, { "value": 372, "label": 372, "text": "days" }, { "value": 373, "label": 373, "text": "days" }, { "value": 374, "label": 374, "text": "days" }, { "value": 375, "label": 375, "text": "days" }, { "value": 376, "label": 376, "text": "days" }, { "value": 377, "label": 377, "text": "days" }, { "value": 378, "label": 378, "text": "days" }, { "value": 379, "label": 379, "text": "days" }, + { "value": 380, "label": 380, "text": "days" }, { "value": 381, "label": 381, "text": "days" }, { "value": 382, "label": 382, "text": "days" }, { "value": 383, "label": 383, "text": "days" }, { "value": 384, "label": 384, "text": "days" }, { "value": 385, "label": 385, "text": "days" }, { "value": 386, "label": 386, "text": "days" }, { "value": 387, "label": 387, "text": "days" }, { "value": 388, "label": 388, "text": "days" }, { "value": 389, "label": 389, "text": "days" }, + { "value": 390, "label": 390, "text": "days" }, { "value": 391, "label": 391, "text": "days" }, { "value": 392, "label": 392, "text": "days" }, { "value": 393, "label": 393, "text": "days" }, { "value": 394, "label": 394, "text": "days" }, { "value": 395, "label": 395, "text": "days" }, { "value": 396, "label": 396, "text": "days" }, { "value": 397, "label": 397, "text": "days" }, { "value": 398, "label": 398, "text": "days" }, { "value": 399, "label": 399, "text": "days" }, + { "value": 400, "label": 400, "text": "days" }, { "value": 401, "label": 401, "text": "days" }, { "value": 402, "label": 402, "text": "days" }, { "value": 403, "label": 403, "text": "days" }, { "value": 404, "label": 404, "text": "days" }, { "value": 405, "label": 405, "text": "days" }, { "value": 406, "label": 406, "text": "days" }, { "value": 407, "label": 407, "text": "days" }, { "value": 408, "label": 408, "text": "days" }, { "value": 409, "label": 409, "text": "days" }, + { "value": 410, "label": 410, "text": "days" }, { "value": 411, "label": 411, "text": "days" }, { "value": 412, "label": 412, "text": "days" }, { "value": 413, "label": 413, "text": "days" }, { "value": 414, "label": 414, "text": "days" }, { "value": 415, "label": 415, "text": "days" }, { "value": 416, "label": 416, "text": "days" }, { "value": 417, "label": 417, "text": "days" }, { "value": 418, "label": 418, "text": "days" }, { "value": 419, "label": 419, "text": "days" }, + { "value": 420, "label": 420, "text": "days" }, { "value": 421, "label": 421, "text": "days" }, { "value": 422, "label": 422, "text": "days" }, { "value": 423, "label": 423, "text": "days" }, { "value": 424, "label": 424, "text": "days" }, { "value": 425, "label": 425, "text": "days" }, { "value": 426, "label": 426, "text": "days" }, { "value": 427, "label": 427, "text": "days" }, { "value": 428, "label": 428, "text": "days" }, { "value": 429, "label": 429, "text": "days" }, + { "value": 430, "label": 430, "text": "days" }, { "value": 431, "label": 431, "text": "days" }, { "value": 432, "label": 432, "text": "days" }, { "value": 433, "label": 433, "text": "days" }, { "value": 434, "label": 434, "text": "days" }, { "value": 435, "label": 435, "text": "days" }, { "value": 436, "label": 436, "text": "days" }, { "value": 437, "label": 437, "text": "days" }, { "value": 438, "label": 438, "text": "days" }, { "value": 439, "label": 439, "text": "days" }, + { "value": 440, "label": 440, "text": "days" }, { "value": 441, "label": 441, "text": "days" }, { "value": 442, "label": 442, "text": "days" }, { "value": 443, "label": 443, "text": "days" }, { "value": 444, "label": 444, "text": "days" }, { "value": 445, "label": 445, "text": "days" }, { "value": 446, "label": 446, "text": "days" }, { "value": 447, "label": 447, "text": "days" }, { "value": 448, "label": 448, "text": "days" }, { "value": 449, "label": 449, "text": "days" }, + { "value": 450, "label": 450, "text": "days" }, { "value": 451, "label": 451, "text": "days" }, { "value": 452, "label": 452, "text": "days" }, { "value": 453, "label": 453, "text": "days" }, { "value": 454, "label": 454, "text": "days" }, { "value": 455, "label": 455, "text": "days" }, { "value": 456, "label": 456, "text": "days" }, { "value": 457, "label": 457, "text": "days" }, { "value": 458, "label": 458, "text": "days" }, { "value": 459, "label": 459, "text": "days" }, + { "value": 460, "label": 460, "text": "days" }, { "value": 461, "label": 461, "text": "days" }, { "value": 462, "label": 462, "text": "days" }, { "value": 463, "label": 463, "text": "days" }, { "value": 464, "label": 464, "text": "days" }, { "value": 465, "label": 465, "text": "days" }, { "value": 466, "label": 466, "text": "days" }, { "value": 467, "label": 467, "text": "days" }, { "value": 468, "label": 468, "text": "days" }, { "value": 469, "label": 469, "text": "days" }, + { "value": 470, "label": 470, "text": "days" }, { "value": 471, "label": 471, "text": "days" }, { "value": 472, "label": 472, "text": "days" }, { "value": 473, "label": 473, "text": "days" }, { "value": 474, "label": 474, "text": "days" }, { "value": 475, "label": 475, "text": "days" }, { "value": 476, "label": 476, "text": "days" }, { "value": 477, "label": 477, "text": "days" }, { "value": 478, "label": 478, "text": "days" }, { "value": 479, "label": 479, "text": "days" }, + { "value": 480, "label": 480, "text": "days" }, { "value": 481, "label": 481, "text": "days" }, { "value": 482, "label": 482, "text": "days" }, { "value": 483, "label": 483, "text": "days" }, { "value": 484, "label": 484, "text": "days" }, { "value": 485, "label": 485, "text": "days" }, { "value": 486, "label": 486, "text": "days" }, { "value": 487, "label": 487, "text": "days" }, { "value": 488, "label": 488, "text": "days" }, { "value": 489, "label": 489, "text": "days" }, + { "value": 490, "label": 490, "text": "days" }, { "value": 491, "label": 491, "text": "days" }, { "value": 492, "label": 492, "text": "days" }, { "value": 493, "label": 493, "text": "days" }, { "value": 494, "label": 494, "text": "days" }, { "value": 495, "label": 495, "text": "days" }, { "value": 496, "label": 496, "text": "days" }, { "value": 497, "label": 497, "text": "days" }, { "value": 498, "label": 498, "text": "days" }, { "value": 499, "label": 499, "text": "days" } +] diff --git a/demos/vue/src/assets/data/countries.json b/demos/vue/src/assets/data/countries.json new file mode 100644 index 000000000..9e0fa9dd7 --- /dev/null +++ b/demos/vue/src/assets/data/countries.json @@ -0,0 +1,245 @@ +[ + {"name": "Afghanistan", "code": "AF"}, + {"name": "ร…land Islands", "code": "AX"}, + {"name": "Albania", "code": "AL"}, + {"name": "Algeria", "code": "DZ"}, + {"name": "American Samoa", "code": "AS"}, + {"name": "AndorrA", "code": "AD"}, + {"name": "Angola", "code": "AO"}, + {"name": "Anguilla", "code": "AI"}, + {"name": "Antarctica", "code": "AQ"}, + {"name": "Antigua and Barbuda", "code": "AG"}, + {"name": "Argentina", "code": "AR"}, + {"name": "Armenia", "code": "AM"}, + {"name": "Aruba", "code": "AW"}, + {"name": "Australia", "code": "AU"}, + {"name": "Austria", "code": "AT"}, + {"name": "Azerbaijan", "code": "AZ"}, + {"name": "Bahamas", "code": "BS"}, + {"name": "Bahrain", "code": "BH"}, + {"name": "Bangladesh", "code": "BD"}, + {"name": "Barbados", "code": "BB"}, + {"name": "Belarus", "code": "BY"}, + {"name": "Belgium", "code": "BE"}, + {"name": "Belize", "code": "BZ"}, + {"name": "Benin", "code": "BJ"}, + {"name": "Bermuda", "code": "BM"}, + {"name": "Bhutan", "code": "BT"}, + {"name": "Bolivia", "code": "BO"}, + {"name": "Bosnia and Herzegovina", "code": "BA"}, + {"name": "Botswana", "code": "BW"}, + {"name": "Bouvet Island", "code": "BV"}, + {"name": "Brazil", "code": "BR"}, + {"name": "British Indian Ocean Territory", "code": "IO"}, + {"name": "Brunei Darussalam", "code": "BN"}, + {"name": "Bulgaria", "code": "BG"}, + {"name": "Burkina Faso", "code": "BF"}, + {"name": "Burundi", "code": "BI"}, + {"name": "Cambodia", "code": "KH"}, + {"name": "Cameroon", "code": "CM"}, + {"name": "Canada", "code": "CA"}, + {"name": "Cape Verde", "code": "CV"}, + {"name": "Cayman Islands", "code": "KY"}, + {"name": "Central African Republic", "code": "CF"}, + {"name": "Chad", "code": "TD"}, + {"name": "Chile", "code": "CL"}, + {"name": "China", "code": "CN"}, + {"name": "Christmas Island", "code": "CX"}, + {"name": "Cocos (Keeling) Islands", "code": "CC"}, + {"name": "Colombia", "code": "CO"}, + {"name": "Comoros", "code": "KM"}, + {"name": "Congo", "code": "CG"}, + {"name": "Congo, The Democratic Republic of the", "code": "CD"}, + {"name": "Cook Islands", "code": "CK"}, + {"name": "Costa Rica", "code": "CR"}, + {"name": "Cote D'Ivoire", "code": "CI"}, + {"name": "Croatia", "code": "HR"}, + {"name": "Cuba", "code": "CU"}, + {"name": "Cyprus", "code": "CY"}, + {"name": "Czech Republic", "code": "CZ"}, + {"name": "Denmark", "code": "DK"}, + {"name": "Djibouti", "code": "DJ"}, + {"name": "Dominica", "code": "DM"}, + {"name": "Dominican Republic", "code": "DO"}, + {"name": "Ecuador", "code": "EC"}, + {"name": "Egypt", "code": "EG"}, + {"name": "El Salvador", "code": "SV"}, + {"name": "Equatorial Guinea", "code": "GQ"}, + {"name": "Eritrea", "code": "ER"}, + {"name": "Estonia", "code": "EE"}, + {"name": "Ethiopia", "code": "ET"}, + {"name": "Falkland Islands (Malvinas)", "code": "FK"}, + {"name": "Faroe Islands", "code": "FO"}, + {"name": "Fiji", "code": "FJ"}, + {"name": "Finland", "code": "FI"}, + {"name": "France", "code": "FR"}, + {"name": "French Guiana", "code": "GF"}, + {"name": "French Polynesia", "code": "PF"}, + {"name": "French Southern Territories", "code": "TF"}, + {"name": "Gabon", "code": "GA"}, + {"name": "Gambia", "code": "GM"}, + {"name": "Georgia", "code": "GE"}, + {"name": "Germany", "code": "DE"}, + {"name": "Ghana", "code": "GH"}, + {"name": "Gibraltar", "code": "GI"}, + {"name": "Greece", "code": "GR"}, + {"name": "Greenland", "code": "GL"}, + {"name": "Grenada", "code": "GD"}, + {"name": "Guadeloupe", "code": "GP"}, + {"name": "Guam", "code": "GU"}, + {"name": "Guatemala", "code": "GT"}, + {"name": "Guernsey", "code": "GG"}, + {"name": "Guinea", "code": "GN"}, + {"name": "Guinea-Bissau", "code": "GW"}, + {"name": "Guyana", "code": "GY"}, + {"name": "Haiti", "code": "HT"}, + {"name": "Heard Island and Mcdonald Islands", "code": "HM"}, + {"name": "Holy See (Vatican City State)", "code": "VA"}, + {"name": "Honduras", "code": "HN"}, + {"name": "Hong Kong", "code": "HK"}, + {"name": "Hungary", "code": "HU"}, + {"name": "Iceland", "code": "IS"}, + {"name": "India", "code": "IN"}, + {"name": "Indonesia", "code": "ID"}, + {"name": "Iran, Islamic Republic Of", "code": "IR"}, + {"name": "Iraq", "code": "IQ"}, + {"name": "Ireland", "code": "IE"}, + {"name": "Isle of Man", "code": "IM"}, + {"name": "Israel", "code": "IL"}, + {"name": "Italy", "code": "IT"}, + {"name": "Jamaica", "code": "JM"}, + {"name": "Japan", "code": "JP"}, + {"name": "Jersey", "code": "JE"}, + {"name": "Jordan", "code": "JO"}, + {"name": "Kazakhstan", "code": "KZ"}, + {"name": "Kenya", "code": "KE"}, + {"name": "Kiribati", "code": "KI"}, + {"name": "Korea, Democratic People's Republic of", "code": "KP"}, + {"name": "Korea, Republic of", "code": "KR"}, + {"name": "Kuwait", "code": "KW"}, + {"name": "Kyrgyzstan", "code": "KG"}, + {"name": "Lao People's Democratic Republic", "code": "LA"}, + {"name": "Latvia", "code": "LV"}, + {"name": "Lebanon", "code": "LB"}, + {"name": "Lesotho", "code": "LS"}, + {"name": "Liberia", "code": "LR"}, + {"name": "Libyan Arab Jamahiriya", "code": "LY"}, + {"name": "Liechtenstein", "code": "LI"}, + {"name": "Lithuania", "code": "LT"}, + {"name": "Luxembourg", "code": "LU"}, + {"name": "Macao", "code": "MO"}, + {"name": "Macedonia, The Former Yugoslav Republic of", "code": "MK"}, + {"name": "Madagascar", "code": "MG"}, + {"name": "Malawi", "code": "MW"}, + {"name": "Malaysia", "code": "MY"}, + {"name": "Maldives", "code": "MV"}, + {"name": "Mali", "code": "ML"}, + {"name": "Malta", "code": "MT"}, + {"name": "Marshall Islands", "code": "MH"}, + {"name": "Martinique", "code": "MQ"}, + {"name": "Mauritania", "code": "MR"}, + {"name": "Mauritius", "code": "MU"}, + {"name": "Mayotte", "code": "YT"}, + {"name": "Mexico", "code": "MX"}, + {"name": "Micronesia, Federated States of", "code": "FM"}, + {"name": "Moldova, Republic of", "code": "MD"}, + {"name": "Monaco", "code": "MC"}, + {"name": "Mongolia", "code": "MN"}, + {"name": "Montserrat", "code": "MS"}, + {"name": "Morocco", "code": "MA"}, + {"name": "Mozambique", "code": "MZ"}, + {"name": "Myanmar", "code": "MM"}, + {"name": "Namibia", "code": "NA"}, + {"name": "Nauru", "code": "NR"}, + {"name": "Nepal", "code": "NP"}, + {"name": "Netherlands", "code": "NL"}, + {"name": "Netherlands Antilles", "code": "AN"}, + {"name": "New Caledonia", "code": "NC"}, + {"name": "New Zealand", "code": "NZ"}, + {"name": "Nicaragua", "code": "NI"}, + {"name": "Niger", "code": "NE"}, + {"name": "Nigeria", "code": "NG"}, + {"name": "Niue", "code": "NU"}, + {"name": "Norfolk Island", "code": "NF"}, + {"name": "Northern Mariana Islands", "code": "MP"}, + {"name": "Norway", "code": "NO"}, + {"name": "Oman", "code": "OM"}, + {"name": "Pakistan", "code": "PK"}, + {"name": "Palau", "code": "PW"}, + {"name": "Palestinian Territory, Occupied", "code": "PS"}, + {"name": "Panama", "code": "PA"}, + {"name": "Papua New Guinea", "code": "PG"}, + {"name": "Paraguay", "code": "PY"}, + {"name": "Peru", "code": "PE"}, + {"name": "Philippines", "code": "PH"}, + {"name": "Pitcairn", "code": "PN"}, + {"name": "Poland", "code": "PL"}, + {"name": "Portugal", "code": "PT"}, + {"name": "Puerto Rico", "code": "PR"}, + {"name": "Qatar", "code": "QA"}, + {"name": "Reunion", "code": "RE"}, + {"name": "Romania", "code": "RO"}, + {"name": "Russian Federation", "code": "RU"}, + {"name": "RWANDA", "code": "RW"}, + {"name": "Saint Helena", "code": "SH"}, + {"name": "Saint Kitts and Nevis", "code": "KN"}, + {"name": "Saint Lucia", "code": "LC"}, + {"name": "Saint Pierre and Miquelon", "code": "PM"}, + {"name": "Saint Vincent and the Grenadines", "code": "VC"}, + {"name": "Samoa", "code": "WS"}, + {"name": "San Marino", "code": "SM"}, + {"name": "Sao Tome and Principe", "code": "ST"}, + {"name": "Saudi Arabia", "code": "SA"}, + {"name": "Senegal", "code": "SN"}, + {"name": "Serbia and Montenegro", "code": "CS"}, + {"name": "Seychelles", "code": "SC"}, + {"name": "Sierra Leone", "code": "SL"}, + {"name": "Singapore", "code": "SG"}, + {"name": "Slovakia", "code": "SK"}, + {"name": "Slovenia", "code": "SI"}, + {"name": "Solomon Islands", "code": "SB"}, + {"name": "Somalia", "code": "SO"}, + {"name": "South Africa", "code": "ZA"}, + {"name": "South Georgia and the South Sandwich Islands", "code": "GS"}, + {"name": "Spain", "code": "ES"}, + {"name": "Sri Lanka", "code": "LK"}, + {"name": "Sudan", "code": "SD"}, + {"name": "Suriname", "code": "SR"}, + {"name": "Svalbard and Jan Mayen", "code": "SJ"}, + {"name": "Swaziland", "code": "SZ"}, + {"name": "Sweden", "code": "SE"}, + {"name": "Switzerland", "code": "CH"}, + {"name": "Syrian Arab Republic", "code": "SY"}, + {"name": "Taiwan, Province of China", "code": "TW"}, + {"name": "Tajikistan", "code": "TJ"}, + {"name": "Tanzania, United Republic of", "code": "TZ"}, + {"name": "Thailand", "code": "TH"}, + {"name": "Timor-Leste", "code": "TL"}, + {"name": "Togo", "code": "TG"}, + {"name": "Tokelau", "code": "TK"}, + {"name": "Tonga", "code": "TO"}, + {"name": "Trinidad and Tobago", "code": "TT"}, + {"name": "Tunisia", "code": "TN"}, + {"name": "Turkey", "code": "TR"}, + {"name": "Turkmenistan", "code": "TM"}, + {"name": "Turks and Caicos Islands", "code": "TC"}, + {"name": "Tuvalu", "code": "TV"}, + {"name": "Uganda", "code": "UG"}, + {"name": "Ukraine", "code": "UA"}, + {"name": "United Arab Emirates", "code": "AE"}, + {"name": "United Kingdom", "code": "GB"}, + {"name": "United States", "code": "US"}, + {"name": "United States Minor Outlying Islands", "code": "UM"}, + {"name": "Uruguay", "code": "UY"}, + {"name": "Uzbekistan", "code": "UZ"}, + {"name": "Vanuatu", "code": "VU"}, + {"name": "Venezuela", "code": "VE"}, + {"name": "Viet Nam", "code": "VN"}, + {"name": "Virgin Islands, British", "code": "VG"}, + {"name": "Virgin Islands, U.S.", "code": "VI"}, + {"name": "Wallis and Futuna", "code": "WF"}, + {"name": "Western Sahara", "code": "EH"}, + {"name": "Yemen", "code": "YE"}, + {"name": "Zambia", "code": "ZM"}, + {"name": "Zimbabwe", "code": "ZW"} +] diff --git a/demos/vue/src/assets/data/country_names.json b/demos/vue/src/assets/data/country_names.json new file mode 100644 index 000000000..1e1e3b363 --- /dev/null +++ b/demos/vue/src/assets/data/country_names.json @@ -0,0 +1,245 @@ +[ + "Afghanistan", + "ร…land Islands", + "Albania", + "Algeria", + "American Samoa", + "AndorrA", + "Angola", + "Anguilla", + "Antarctica", + "Antigua and Barbuda", + "Argentina", + "Armenia", + "Aruba", + "Australia", + "Austria", + "Azerbaijan", + "Bahamas", + "Bahrain", + "Bangladesh", + "Barbados", + "Belarus", + "Belgium", + "Belize", + "Benin", + "Bermuda", + "Bhutan", + "Bolivia", + "Bosnia and Herzegovina", + "Botswana", + "Bouvet Island", + "Brazil", + "British Indian Ocean Territory", + "Brunei Darussalam", + "Bulgaria", + "Burkina Faso", + "Burundi", + "Cambodia", + "Cameroon", + "Canada", + "Cape Verde", + "Cayman Islands", + "Central African Republic", + "Chad", + "Chile", + "China", + "Christmas Island", + "Cocos (Keeling) Islands", + "Colombia", + "Comoros", + "Congo", + "Congo, The Democratic Republic of the", + "Cook Islands", + "Costa Rica", + "Cote D'Ivoire", + "Croatia", + "Cuba", + "Cyprus", + "Czech Republic", + "Denmark", + "Djibouti", + "Dominica", + "Dominican Republic", + "Ecuador", + "Egypt", + "El Salvador", + "Equatorial Guinea", + "Eritrea", + "Estonia", + "Ethiopia", + "Falkland Islands (Malvinas)", + "Faroe Islands", + "Fiji", + "Finland", + "France", + "French Guiana", + "French Polynesia", + "French Southern Territories", + "Gabon", + "Gambia", + "Georgia", + "Germany", + "Ghana", + "Gibraltar", + "Greece", + "Greenland", + "Grenada", + "Guadeloupe", + "Guam", + "Guatemala", + "Guernsey", + "Guinea", + "Guinea-Bissau", + "Guyana", + "Haiti", + "Heard Island and Mcdonald Islands", + "Holy See (Vatican City State)", + "Honduras", + "Hong Kong", + "Hungary", + "Iceland", + "India", + "Indonesia", + "Iran, Islamic Republic Of", + "Iraq", + "Ireland", + "Isle of Man", + "Israel", + "Italy", + "Jamaica", + "Japan", + "Jersey", + "Jordan", + "Kazakhstan", + "Kenya", + "Kiribati", + "Korea, Democratic People'S Republic of", + "Korea, Republic of", + "Kuwait", + "Kyrgyzstan", + "Lao People'S Democratic Republic", + "Latvia", + "Lebanon", + "Lesotho", + "Liberia", + "Libyan Arab Jamahiriya", + "Liechtenstein", + "Lithuania", + "Luxembourg", + "Macao", + "Macedonia, The Former Yugoslav Republic of", + "Madagascar", + "Malawi", + "Malaysia", + "Maldives", + "Mali", + "Malta", + "Marshall Islands", + "Martinique", + "Mauritania", + "Mauritius", + "Mayotte", + "Mexico", + "Micronesia, Federated States of", + "Moldova, Republic of", + "Monaco", + "Mongolia", + "Montserrat", + "Morocco", + "Mozambique", + "Myanmar", + "Namibia", + "Nauru", + "Nepal", + "Netherlands", + "Netherlands Antilles", + "New Caledonia", + "New Zealand", + "Nicaragua", + "Niger", + "Nigeria", + "Niue", + "Norfolk Island", + "Northern Mariana Islands", + "Norway", + "Oman", + "Pakistan", + "Palau", + "Palestinian Territory, Occupied", + "Panama", + "Papua New Guinea", + "Paraguay", + "Peru", + "Philippines", + "Pitcairn", + "Poland", + "Portugal", + "Puerto Rico", + "Qatar", + "Reunion", + "Romania", + "Russian Federation", + "RWANDA", + "Saint Helena", + "Saint Kitts and Nevis", + "Saint Lucia", + "Saint Pierre and Miquelon", + "Saint Vincent and the Grenadines", + "Samoa", + "San Marino", + "Sao Tome and Principe", + "Saudi Arabia", + "Senegal", + "Serbia and Montenegro", + "Seychelles", + "Sierra Leone", + "Singapore", + "Slovakia", + "Slovenia", + "Solomon Islands", + "Somalia", + "South Africa", + "South Georgia and the South Sandwich Islands", + "Spain", + "Sri Lanka", + "Sudan", + "Suriname", + "Svalbard and Jan Mayen", + "Swaziland", + "Sweden", + "Switzerland", + "Syrian Arab Republic", + "Taiwan, Province of China", + "Tajikistan", + "Tanzania, United Republic of", + "Thailand", + "Timor-Leste", + "Togo", + "Tokelau", + "Tonga", + "Trinidad and Tobago", + "Tunisia", + "Turkey", + "Turkmenistan", + "Turks and Caicos Islands", + "Tuvalu", + "Uganda", + "Ukraine", + "United Arab Emirates", + "United Kingdom", + "United States", + "United States Minor Outlying Islands", + "Uruguay", + "Uzbekistan", + "Vanuatu", + "Venezuela", + "Viet Nam", + "Virgin Islands, British", + "Virgin Islands, U.S.", + "Wallis and Futuna", + "Western Sahara", + "Yemen", + "Zambia", + "Zimbabwe" +] diff --git a/demos/vue/src/assets/data/customers_100.json b/demos/vue/src/assets/data/customers_100.json new file mode 100644 index 000000000..bee8511b9 --- /dev/null +++ b/demos/vue/src/assets/data/customers_100.json @@ -0,0 +1,102 @@ +[ + {"name":"Ethel Price","gender":"female","company":"Enersol","id":1,"category":{"id":1,"name":"Gold"}}, + {"name":"Claudine Neal","gender":"female","company":"Sealoud","id":2,"category":{"id":1,"name":"Gold"}}, + {"name":"Beryl Rice","gender":"female","company":"Velity","id":3,"category":{"id":1,"name":"Gold"}}, + {"name":"Wilder Gonzales","gender":"male","company":"Geekko","id":4,"category":{"id":1,"name":"Gold"}}, + {"name":"Georgina Schultz","gender":"female","company":"Suretech","id":5,"category":{"id":1,"name":"Gold"}}, + {"name":"Carroll Buchanan","gender":"male","company":"Ecosys","id":6,"category":{"id":1,"name":"Gold"}}, + {"name":"Valarie Atkinson","gender":"female","company":"Hopeli","id":7,"category":{"id":1,"name":"Gold"}}, + {"name":"Schroeder Mathews","gender":"male","company":"Polarium","id":8,"category":{"id":1,"name":"Gold"}}, + {"name":"Lynda Mendoza","gender":"female","company":"Dogspa","id":9,"category":{"id":1,"name":"Gold"}}, + {"name":"Sarah Massey","gender":"female","company":"Bisba","id":10,"category":{"id":1,"name":"Gold"}}, + {"name":"Robles Boyle","gender":"male","company":"Comtract","id":11,"category":{"id":1,"name":"Gold"}}, + {"name":"Evans Hickman","gender":"male","company":"Parleynet","id":12,"category":{"id":1,"name":"Gold"}}, + {"name":"Dawson Barber","gender":"male","company":"Dymi","id":13,"category":{"id":1,"name":"Gold"}}, + {"name":"Bruce Strong","gender":"male","company":"Xyqag","id":14,"category":{"id":1,"name":"Gold"}}, + {"name":"Nellie Whitfield","gender":"female","company":"Exospace","id":15,"category":{"id":1,"name":"Gold"}}, + {"name":"Jackson Macias","gender":"male","company":"Aquamate","id":16,"category":{"id":1,"name":"Gold"}}, + {"name":"Pena Pena","gender":"male","company":"Quarx","id":17,"category":{"id":1,"name":"Gold"}}, + {"name":"Lelia Gates","gender":"female","company":"Proxsoft","id":18,"category":{"id":1,"name":"Gold"}}, + {"name":"Letitia Vasquez","gender":"female","company":"Slumberia","id":19,"category":{"id":1,"name":"Gold"}}, + {"name":"Trevino Moreno","gender":"male","company":"Conjurica","id":20,"category":{"id":1,"name":"Gold"}}, + {"name":"Barr Page","gender":"male","company":"Apex","id":21,"category":{"id":1,"name":"Gold"}}, + {"name":"Kirkland Merrill","gender":"male","company":"Utara","id":22,"category":{"id":1,"name":"Gold"}}, + {"name":"Blanche Conley","gender":"female","company":"Imkan","id":23,"category":{"id":1,"name":"Gold"}}, + {"name":"Atkins Dunlap","gender":"male","company":"Comveyor","id":24,"category":{"id":1,"name":"Gold"}}, + {"name":"Everett Foreman","gender":"male","company":"Maineland","id":25,"category":{"id":1,"name":"Gold"}}, + {"name":"Gould Randolph","gender":"male","company":"Intergeek","id":26,"category":{"id":1,"name":"Gold"}}, + {"name":"Kelli Leon","gender":"female","company":"Verbus","id":27,"category":{"id":1,"name":"Gold"}}, + {"name":"Freda Mason","gender":"female","company":"Accidency","id":28,"category":{"id":1,"name":"Gold"}}, + {"name":"Tucker Maxwell","gender":"male","company":"Lumbrex","id":29,"category":{"id":1,"name":"Gold"}}, + {"name":"Yvonne Parsons","gender":"female","company":"Zolar","id":30,"category":{"id":1,"name":"Gold"}}, + {"name":"Woods Key","gender":"male","company":"Bedder","id":31,"category":{"id":1,"name":"Gold"}}, + {"name":"Stephens Reilly","gender":"male","company":"Acusage","id":32,"category":{"id":1,"name":"Gold"}}, + {"name":"Mcfarland Sparks","gender":"male","company":"Comvey","id":33,"category":{"id":2,"name":"Silver"}}, + {"name":"Jocelyn Sawyer","gender":"female","company":"Fortean","id":34,"category":{"id":2,"name":"Silver"}}, + {"name":"Renee Barr","gender":"female","company":"Kiggle","id":35,"category":{"id":2,"name":"Silver"}}, + {"name":"Gaines Beck","gender":"male","company":"Sequitur","id":36,"category":{"id":2,"name":"Silver"}}, + {"name":"Luisa Farrell","gender":"female","company":"Cinesanct","id":37,"category":{"id":2,"name":"Silver"}}, + {"name":"Robyn Strickland","gender":"female","company":"Obones","id":38,"category":{"id":2,"name":"Silver"}}, + {"name":"Roseann Jarvis","gender":"female","company":"Aquazure","id":39,"category":{"id":2,"name":"Silver"}}, + {"name":"Johnston Park","gender":"male","company":"Netur","id":40,"category":{"id":2,"name":"Silver"}}, + {"name":"Wong Craft","gender":"male","company":"Opticall","id":41,"category":{"id":2,"name":"Silver"}}, + {"name":"Merritt Cole","gender":"male","company":"Techtrix","id":42,"category":{"id":2,"name":"Silver"}}, + {"name":"Dale Byrd","gender":"female","company":"Kneedles","id":43,"category":{"id":2,"name":"Silver"}}, + {"name":"Sara Delgado","gender":"female","company":"Netagy","id":44,"category":{"id":2,"name":"Silver"}}, + {"name":"Alisha Myers","gender":"female","company":"Intradisk","id":45,"category":{"id":2,"name":"Silver"}}, + {"name":"Felecia Smith","gender":"female","company":"Futurity","id":46,"category":{"id":2,"name":"Silver"}}, + {"name":"Neal Harvey","gender":"male","company":"Pyramax","id":47,"category":{"id":2,"name":"Silver"}}, + {"name":"Nola Miles","gender":"female","company":"Sonique","id":48,"category":{"id":2,"name":"Silver"}}, + {"name":"Herring Pierce","gender":"male","company":"Geeketron","id":49,"category":{"id":2,"name":"Silver"}}, + {"name":"Shelley Rodriquez","gender":"female","company":"Bostonic","id":50,"category":{"id":2,"name":"Silver"}}, + {"name":"Cora Chase","gender":"female","company":"Isonus","id":51,"category":{"id":2,"name":"Silver"}}, + {"name":"Mckay Santos","gender":"male","company":"Amtas","id":52,"category":{"id":2,"name":"Silver"}}, + {"name":"Hilda Crane","gender":"female","company":"Jumpstack","id":53,"category":{"id":2,"name":"Silver"}}, + {"name":"Jeanne Lindsay","gender":"female","company":"Genesynk","id":54,"category":{"id":2,"name":"Silver"}}, + {"name":"Frye Sharpe","gender":"male","company":"Eplode","id":55,"category":{"id":2,"name":"Silver"}}, + {"name":"Velma Fry","gender":"female","company":"Ronelon","id":56,"category":{"id":2,"name":"Silver"}}, + {"name":"Reyna Espinoza","gender":"female","company":"Prismatic","id":57,"category":{"id":2,"name":"Silver"}}, + {"name":"Spencer Sloan","gender":"male","company":"Comverges","id":58,"category":{"id":2,"name":"Silver"}}, + {"name":"Graham Marsh","gender":"male","company":"Medifax","id":59,"category":{"id":2,"name":"Silver"}}, + {"name":"Hale Boone","gender":"male","company":"Digial","id":60,"category":{"id":2,"name":"Silver"}}, + {"name":"Wiley Hubbard","gender":"male","company":"Zensus","id":61,"category":{"id":2,"name":"Silver"}}, + {"name":"Blackburn Drake","gender":"male","company":"Frenex","id":62,"category":{"id":2,"name":"Silver"}}, + {"name":"Franco Hunter","gender":"male","company":"Rockabye","id":63,"category":{"id":2,"name":"Silver"}}, + {"name":"Barnett Case","gender":"male","company":"Norali","id":64,"category":{"id":2,"name":"Silver"}}, + {"name":"Alexander Foley","gender":"male","company":"Geekosis","id":65,"category":{"id":3,"name":"Bronze"}}, + {"name":"Lynette Stein","gender":"female","company":"Macronaut","id":66,"category":{"id":3,"name":"Bronze"}}, + {"name":"Anthony Joyner","gender":"male","company":"Senmei","id":67,"category":{"id":3,"name":"Bronze"}}, + {"name":"Garrett Brennan","gender":"male","company":"Bluegrain","id":68,"category":{"id":3,"name":"Bronze"}}, + {"name":"Betsy Horton","gender":"female","company":"Zilla","id":69,"category":{"id":3,"name":"Bronze"}}, + {"name":"Patton Small","gender":"male","company":"Genmex","id":70,"category":{"id":3,"name":"Bronze"}}, + {"name":"Lakisha Huber","gender":"female","company":"Insource","id":71,"category":{"id":3,"name":"Bronze"}}, + {"name":"Lindsay Avery","gender":"female","company":"Unq","id":72,"category":{"id":3,"name":"Bronze"}}, + {"name":"Ayers Hood","gender":"male","company":"Accuprint","id":73,"category":{"id":3,"name":"Bronze"}}, + {"name":"Torres Durham","gender":"male","company":"Uplinx","id":74,"category":{"id":3,"name":"Bronze"}}, + {"name":"Vincent Hernandez","gender":"male","company":"Talendula","id":75,"category":{"id":3,"name":"Bronze"}}, + {"name":"Baird Ryan","gender":"male","company":"Aquasseur","id":76,"category":{"id":3,"name":"Bronze"}}, + {"name":"Georgia Mercer","gender":"female","company":"Skyplex","id":77,"category":{"id":3,"name":"Bronze"}}, + {"name":"Francesca Elliott","gender":"female","company":"Nspire","id":78,"category":{"id":3,"name":"Bronze"}}, + {"name":"Lyons Peters","gender":"male","company":"Quinex","id":79,"category":{"id":3,"name":"Bronze"}}, + {"name":"Kristi Brewer","gender":"female","company":"Oronoko","id":80,"category":{"id":3,"name":"Bronze"}}, + {"name":"Tonya Bray","gender":"female","company":"Insuron","id":81,"category":{"id":3,"name":"Bronze"}}, + {"name":"Valenzuela Huff","gender":"male","company":"Applideck","id":82,"category":{"id":3,"name":"Bronze"}}, + {"name":"Tiffany Anderson","gender":"female","company":"Zanymax","id":83,"category":{"id":3,"name":"Bronze"}}, + {"name":"Jerri King","gender":"female","company":"Eventex","id":84,"category":{"id":3,"name":"Bronze"}}, + {"name":"Rocha Meadows","gender":"male","company":"Goko","id":85,"category":{"id":3,"name":"Bronze"}}, + {"name":"Marcy Green","gender":"female","company":"Pharmex","id":86,"category":{"id":3,"name":"Bronze"}}, + {"name":"Kirk Cross","gender":"male","company":"Portico","id":87,"category":{"id":3,"name":"Bronze"}}, + {"name":"Hattie Mullen","gender":"female","company":"Zilencio","id":88,"category":{"id":3,"name":"Bronze"}}, + {"name":"Deann Bridges","gender":"female","company":"Equitox","id":89,"category":{"id":3,"name":"Bronze"}}, + {"name":"Chaney Roach","gender":"male","company":"Qualitern","id":90,"category":{"id":3,"name":"Bronze"}}, + {"name":"Consuelo Dickson","gender":"female","company":"Poshome","id":91,"category":{"id":3,"name":"Bronze"}}, + {"name":"Billie Rowe","gender":"female","company":"Cemention","id":92,"category":{"id":3,"name":"Bronze"}}, + {"name":"Bean Donovan","gender":"male","company":"Mantro","id":93,"category":{"id":3,"name":"Bronze"}}, + {"name":"Lancaster Patel","gender":"male","company":"Krog","id":94,"category":{"id":3,"name":"Bronze"}}, + {"name":"Rosa Dyer","gender":"female","company":"Netility","id":95,"category":{"id":3,"name":"Bronze"}}, + {"name":"Christine Compton","gender":"female","company":"Bleeko","id":96,"category":{"id":3,"name":"Bronze"}}, + {"name":"Milagros Finch","gender":"female","company":"Handshake","id":97,"category":{"id":3,"name":"Bronze"}}, + {"name":"Ericka Alvarado","gender":"female","company":"Lyrichord","id":98,"category":{"id":3,"name":"Bronze"}}, + {"name":"Sylvia Sosa","gender":"female","company":"Circum","id":99,"category":{"id":3,"name":"Bronze"}}, + {"name":"Humphrey Curtis","gender":"male","company":"Corepan","id":100,"category":{"id":3,"name":"Bronze"}} + ] \ No newline at end of file diff --git a/demos/vue/src/assets/data/example-data.js b/demos/vue/src/assets/data/example-data.js new file mode 100644 index 000000000..79252ef21 --- /dev/null +++ b/demos/vue/src/assets/data/example-data.js @@ -0,0 +1,16 @@ +const data = []; + +for (let i = 0; i < 500; i++) { + const d = (data[i] = {}); + + d.id = i; + d['title'] = 'Task ' + i; + d['description'] = 'This is a sample task description.\n It can be multiline'; + d['duration'] = '5 days'; + d['percentComplete'] = Math.round(Math.random() * 100); + d['start'] = '01/01/2009'; + d['finish'] = '01/05/2009'; + d['effortDriven'] = i % 5 === 0; +} + +export default data; diff --git a/demos/vue/src/assets/locales/en/translation.json b/demos/vue/src/assets/locales/en/translation.json new file mode 100644 index 000000000..989da0ae4 --- /dev/null +++ b/demos/vue/src/assets/locales/en/translation.json @@ -0,0 +1,108 @@ +{ + "ALL_SELECTED": "All Selected", + "ALL_X_RECORDS_SELECTED": "All {{x}} records selected", + "APPLY_MASS_UPDATE": "Apply Mass Update", + "APPLY_TO_SELECTION": "Update Selection", + "CANCEL": "Cancel", + "CLEAR_ALL_FILTERS": "Clear all Filters", + "CLEAR_ALL_GROUPING": "Clear all Grouping", + "CLEAR_ALL_SORTING": "Clear all Sorting", + "CLEAR_PINNING": "Unfreeze Columns/Rows", + "CLONE": "Clone", + "COLLAPSE_ALL_GROUPS": "Collapse all Groups", + "COLUMNS": "Columns", + "COLUMN_RESIZE_BY_CONTENT": "Resize by Content", + "COMMANDS": "Commands", + "CONTAINS": "Contains", + "COPY": "Copy", + "EMPTY_DATA_WARNING_MESSAGE": "No data to display.", + "ENDS_WITH": "Ends With", + "EQUALS": "Equals", + "EQUAL_TO": "Equal to", + "EXPAND_ALL_GROUPS": "Expand all Groups", + "EXPORT_TO_CSV": "Export in CSV format", + "EXPORT_TO_EXCEL": "Export to Excel", + "EXPORT_TO_TAB_DELIMITED": "Export in Text format (Tab delimited)", + "EXPORT_TO_TEXT_FORMAT": "Export in Text format", + "FILTER_SHORTCUTS": "Filter Shortcuts", + "FROM_TO_OF_TOTAL_ITEMS": "{{from}}-{{to}} of {{totalItems}} items", + "FORCE_FIT_COLUMNS": "Force fit columns", + "FREEZE_COLUMNS": "Freeze Columns", + "INVALID_FLOAT": "The number must be valid and have a maximum of {{maxDecimal}} decimals.", + "GREATER_THAN": "Greater than", + "GREATER_THAN_OR_EQUAL_TO": "Greater than or equal to", + "GROUP_BY": "Group by", + "HIDE_COLUMN": "Hide Column", + "IN_COLLECTION_SEPERATED_BY_COMMA": "Search items in a collection, must be separated by a comma (a,b)", + "ITEMS": "items", + "ITEMS_PER_PAGE": "items per page", + "ITEMS_SELECTED": "items selected", + "NO_ELEMENTS_FOUND": "No elements found", + "LAST_UPDATE": "Last Update", + "LESS_THAN": "Less than", + "LESS_THAN_OR_EQUAL_TO": "Less than or equal to", + "NOT_CONTAINS": "Not contains", + "NOT_EQUAL_TO": "Not equal to", + "NOT_IN_COLLECTION_SEPERATED_BY_COMMA": "Search items not in a collection, must be separated by a comma (a,b)", + "OF": "of", + "OK": "OK", + "PAGE": "Page", + "PAGE_X_OF_Y": "page {{x}} of {{y}}", + "REFRESH_DATASET": "Refresh Dataset", + "REMOVE_FILTER": "Remove Filter", + "REMOVE_SORT": "Remove Sort", + "RESET_INPUT_VALUE": "Reset Input Value", + "RESET_FORM": "Reset Form", + "SAVE": "Save", + "SELECT_ALL": "Select All", + "SORT_ASCENDING": "Sort Ascending", + "SORT_DESCENDING": "Sort Descending", + "STARTS_WITH": "Starts With", + "SYNCHRONOUS_RESIZE": "Synchronous resize", + "TOGGLE_FILTER_ROW": "Toggle Filter Row", + "TOGGLE_PRE_HEADER_ROW": "Toggle Pre-Header Row", + "X_OF_Y_SELECTED": "# of % selected", + "X_OF_Y_MASS_SELECTED": "{{x}} of {{y}} selected", + "BILLING": { + "ADDRESS": { + "STREET": "Billing Address Street", + "ZIP": "Billing Address Zip" + }, + "INFORMATION": "Billing Information" + }, + "BLANK_VALUES": "Blank Values", + "NON_BLANK_VALUES": "Non-Blank Values", + "CUSTOM_COMMANDS": "Custom Commands", + "DURATION": "Duration", + "COMPANY": "Company", + "COMPLETED": "Completed", + "CHANGE_COMPLETED_FLAG": "Change Completed Flag", + "CHANGE_PRIORITY": "Change Priority", + "CUSTOMER_INFORMATION": "Customer Information", + "DELETE_ROW": "Delete Row", + "DISABLED_COMMAND": "Disabled Command", + "FALSE": "False", + "FEMALE": "Female", + "FINISH": "Finish", + "FUTURE": "Future", + "GENDER": "Gender", + "HELP": "Help", + "HIGH": "High", + "LOW": "Low", + "MEDIUM": "Medium", + "MALE": "Male", + "NAME": "Name", + "NEXT_20_DAYS": "Next 20 days", + "NONE": "None", + "PAST": "Past", + "PERCENT_COMPLETE": "% Complete", + "PRIORITY": "Priority", + "START": "Start", + "TASK_X": "Task {{x}}", + "TITLE": "Title", + "TODAY": "Today", + "TRUE": "True", + "X_DAY_PLURAL": "{{x}} day{{plural}}", + "RBE_BTN_UPDATE": "Update the current row", + "RBE_BTN_CANCEL": "Cancel changes of the current row" +} \ No newline at end of file diff --git a/demos/vue/src/assets/locales/fr/translation.json b/demos/vue/src/assets/locales/fr/translation.json new file mode 100644 index 000000000..321f23843 --- /dev/null +++ b/demos/vue/src/assets/locales/fr/translation.json @@ -0,0 +1,109 @@ +{ + "ALL_SELECTED": "Tout sรฉlectionnรฉs", + "ALL_X_RECORDS_SELECTED": "Sur tous les {{x}} รฉlรฉments", + "APPLY_MASS_UPDATE": "Mettre ร  jour en masse", + "APPLY_TO_SELECTION": "Mettre ร  jour la sรฉlection", + "CANCEL": "Annuler", + "CLEAR_ALL_FILTERS": "Supprimer tous les filtres", + "CLEAR_ALL_GROUPING": "Supprimer tous les groupes", + "CLEAR_ALL_SORTING": "Supprimer tous les tris", + "CLEAR_PINNING": "Dรฉgeler les colonnes/rangรฉes", + "CLONE": "Cloner", + "COLLAPSE_ALL_GROUPS": "Rรฉduire tous les groupes", + "COLUMNS": "Colonnes", + "COLUMN_RESIZE_BY_CONTENT": "Redimensionner par contenu", + "COMMANDS": "Commandes", + "CONTAINS": "Contient", + "COPY": "Copier", + "EMPTY_DATA_WARNING_MESSAGE": "Aucune donnรฉe ร  afficher.", + "ENDS_WITH": "Se termine par", + "EQUALS": "ร‰gale", + "EQUAL_TO": "ร‰gal ร ", + "EXPAND_ALL_GROUPS": "ร‰tendre tous les groupes", + "EXPORT_TO_CSV": "Exporter en format CSV", + "EXPORT_TO_EXCEL": "Exporter vers Excel", + "EXPORT_TO_TAB_DELIMITED": "Exporter en format texte (dรฉlimitรฉ par tabulation)", + "EXPORT_TO_TEXT_FORMAT": "Exporter en format texte", + "FILTER_SHORTCUTS": "Raccourcis de filtre", + "FROM_TO_OF_TOTAL_ITEMS": "{{from}}-{{to}} de {{totalItems}} รฉlรฉments", + "FORCE_FIT_COLUMNS": "Ajustement forcรฉ des colonnes", + "FREEZE_COLUMNS": "Geler les colonnes", + "GREATER_THAN": "Plus grand que", + "GREATER_THAN_OR_EQUAL_TO": "Plus grand ou รฉgal ร ", + "GROUP_BY": "Grouper par", + "HIDE_COLUMN": "Cacher la colonne", + "IN_COLLECTION_SEPERATED_BY_COMMA": "Recherche incluant certain รฉlรฉments d'une collection, doit รชtre sรฉparรฉ par une virgule (a,b)", + "INVALID_FLOAT": "Le nombre doit รชtre valide et avoir un maximum de {{maxDecimal}} dรฉcimales.", + "ITEMS": "รฉlรฉments", + "ITEMS_PER_PAGE": "รฉlรฉments par page", + "ITEMS_SELECTED": "รฉlรฉments sรฉlectionnรฉs", + "LAST_UPDATE": "Derniรจre mise ร  jour", + "LESS_THAN": "Plus petit que", + "LESS_THAN_OR_EQUAL_TO": "Plus petit ou รฉgal ร ", + "NO_ELEMENTS_FOUND": "Aucun รฉlรฉment trouvรฉ", + "NOT_CONTAINS": "Ne contient pas", + "NOT_EQUAL_TO": "Non รฉgal ร ", + "NOT_IN_COLLECTION_SEPERATED_BY_COMMA": "Recherche excluant certain รฉlรฉments d'une collection, doit รชtre sรฉparรฉ par une virgule (a,b)", + "OF": "de", + "OK": "Terminรฉ", + "PAGE": "Page", + "PAGE_X_OF_Y": "page {{x}} de {{y}}", + "REFRESH_DATASET": "Rafraรฎchir les donnรฉes", + "REMOVE_FILTER": "Supprimer le filtre", + "REMOVE_SORT": "Supprimer le tri", + "RESET_INPUT_VALUE": "Rรฉinitialiser la valeur", + "RESET_FORM": "Rรฉinitialiser le formulaire", + "SAVE": "Sauvegarder", + "SELECT_ALL": "Sรฉlectionner tout", + "SORT_ASCENDING": "Trier par ordre croissant", + "SORT_DESCENDING": "Trier par ordre dรฉcroissant", + "STARTS_WITH": "Commence par", + "SYNCHRONOUS_RESIZE": "Redimension synchrone", + "TOGGLE_FILTER_ROW": "Basculer la ligne des filtres", + "TOGGLE_PRE_HEADER_ROW": "Basculer la ligne de prรฉ-en-tรชte", + "X_OF_Y_SELECTED": "# de % sรฉlectionnรฉs", + "X_OF_Y_MASS_SELECTED": "{{x}} de {{y}} sรฉlectionnรฉs", + "BILLING": { + "ADDRESS": { + "STREET": "Adresse de facturation", + "ZIP": "Code zip de facturation" + }, + "INFORMATION": "Information de Facturation" + }, + "BLANK_VALUES": "Valeurs nulles", + "NON_BLANK_VALUES": "Valeurs non-nulles", + "DURATION": "Durรฉe", + "COMPANY": "Compagnie", + "COMPLETED": "Terminรฉ", + "CHANGE_COMPLETED_FLAG": "Changer l'indicateur terminรฉ", + "CHANGE_PRIORITY": "Changer la prioritรฉ", + "CUSTOM_COMMANDS": "Commandes Personnalisรฉes", + "CUSTOMER_INFORMATION": "Information Client", + "DELETE_ROW": "Supprimer la ligne", + "DISABLED_COMMAND": "Commande dรฉsactivรฉe", + "FALSE": "Faux", + "FEMALE": "Fรฉminin", + "FINISH": "Fin", + "FUTURE": "Future", + "GENDER": "Sexe", + "HELP": "Aide", + "HIGH": "Haut", + "LOW": "Bas", + "MEDIUM": "Moyen", + "MALE": "Masculin", + "NAME": "Nom", + "NEXT_20_DAYS": "20 prochain jours", + "NONE": "Aucun", + "PAST": "Passรฉ", + "PERCENT_COMPLETE": "% Achevรฉe", + "PRIORITY": "Prioritรฉ", + "START": "Dรฉbut", + "TASK_X": "Tรขche {{x}}", + "TITLE": "Titre", + "TITLE.NAME": "Nom du Titre", + "TODAY": "Aujourd'hui", + "TRUE": "Vrai", + "X_DAY_PLURAL": "{{x}} journรฉe{{plural}}", + "RBE_BTN_UPDATE": "Mettre ร  jour la ligne actuelle", + "RBE_BTN_CANCEL": "Annuler la ligne actuelle" +} \ No newline at end of file diff --git a/demos/vue/src/assets/vue.svg b/demos/vue/src/assets/vue.svg new file mode 100644 index 000000000..770e9d333 --- /dev/null +++ b/demos/vue/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/demos/vue/src/components/CustomFooter.vue b/demos/vue/src/components/CustomFooter.vue new file mode 100644 index 000000000..e7a095bc5 --- /dev/null +++ b/demos/vue/src/components/CustomFooter.vue @@ -0,0 +1,13 @@ + + diff --git a/demos/vue/src/components/CustomPager.vue b/demos/vue/src/components/CustomPager.vue new file mode 100644 index 000000000..beed379a7 --- /dev/null +++ b/demos/vue/src/components/CustomPager.vue @@ -0,0 +1,225 @@ + + + diff --git a/demos/vue/src/components/Example01.vue b/demos/vue/src/components/Example01.vue new file mode 100644 index 000000000..73ba5dc52 --- /dev/null +++ b/demos/vue/src/components/Example01.vue @@ -0,0 +1,166 @@ + + + diff --git a/demos/vue/src/components/Example02.vue b/demos/vue/src/components/Example02.vue new file mode 100644 index 000000000..eaae4e551 --- /dev/null +++ b/demos/vue/src/components/Example02.vue @@ -0,0 +1,284 @@ + + + diff --git a/demos/vue/src/components/Example03.vue b/demos/vue/src/components/Example03.vue new file mode 100644 index 000000000..d48c634ed --- /dev/null +++ b/demos/vue/src/components/Example03.vue @@ -0,0 +1,764 @@ + + + diff --git a/demos/vue/src/components/Example04.vue b/demos/vue/src/components/Example04.vue new file mode 100644 index 000000000..eaaf7a50f --- /dev/null +++ b/demos/vue/src/components/Example04.vue @@ -0,0 +1,443 @@ + + + diff --git a/demos/vue/src/components/Example05.vue b/demos/vue/src/components/Example05.vue new file mode 100644 index 000000000..624ba06c0 --- /dev/null +++ b/demos/vue/src/components/Example05.vue @@ -0,0 +1,640 @@ + + + diff --git a/demos/vue/src/components/Example06.vue b/demos/vue/src/components/Example06.vue new file mode 100644 index 000000000..a42f04b93 --- /dev/null +++ b/demos/vue/src/components/Example06.vue @@ -0,0 +1,605 @@ + + + diff --git a/demos/vue/src/components/Example07.vue b/demos/vue/src/components/Example07.vue new file mode 100644 index 000000000..b37f66d90 --- /dev/null +++ b/demos/vue/src/components/Example07.vue @@ -0,0 +1,313 @@ + + + + + diff --git a/demos/vue/src/components/Example08.vue b/demos/vue/src/components/Example08.vue new file mode 100644 index 000000000..62b69ee2f --- /dev/null +++ b/demos/vue/src/components/Example08.vue @@ -0,0 +1,279 @@ + + + + + diff --git a/demos/vue/src/components/Example09.vue b/demos/vue/src/components/Example09.vue new file mode 100644 index 000000000..dba077004 --- /dev/null +++ b/demos/vue/src/components/Example09.vue @@ -0,0 +1,381 @@ + + + + + diff --git a/demos/vue/src/components/Example10.vue b/demos/vue/src/components/Example10.vue new file mode 100644 index 000000000..5f5be47ad --- /dev/null +++ b/demos/vue/src/components/Example10.vue @@ -0,0 +1,433 @@ + + + + + diff --git a/demos/vue/src/components/Example11.vue b/demos/vue/src/components/Example11.vue new file mode 100644 index 000000000..084bd5249 --- /dev/null +++ b/demos/vue/src/components/Example11.vue @@ -0,0 +1,361 @@ + + + + + diff --git a/demos/vue/src/components/Example12.vue b/demos/vue/src/components/Example12.vue new file mode 100644 index 000000000..33c59b26f --- /dev/null +++ b/demos/vue/src/components/Example12.vue @@ -0,0 +1,403 @@ + + + diff --git a/demos/vue/src/components/Example13.vue b/demos/vue/src/components/Example13.vue new file mode 100644 index 000000000..71d72623c --- /dev/null +++ b/demos/vue/src/components/Example13.vue @@ -0,0 +1,468 @@ + + + diff --git a/demos/vue/src/components/Example14.vue b/demos/vue/src/components/Example14.vue new file mode 100644 index 000000000..249b2a6e0 --- /dev/null +++ b/demos/vue/src/components/Example14.vue @@ -0,0 +1,229 @@ + + + + + diff --git a/demos/vue/src/components/Example15.vue b/demos/vue/src/components/Example15.vue new file mode 100644 index 000000000..9c409c4c4 --- /dev/null +++ b/demos/vue/src/components/Example15.vue @@ -0,0 +1,323 @@ + + + diff --git a/demos/vue/src/components/Example16.vue b/demos/vue/src/components/Example16.vue new file mode 100644 index 000000000..b5492db23 --- /dev/null +++ b/demos/vue/src/components/Example16.vue @@ -0,0 +1,367 @@ + + + diff --git a/demos/vue/src/components/Example18.vue b/demos/vue/src/components/Example18.vue new file mode 100644 index 000000000..4103a9999 --- /dev/null +++ b/demos/vue/src/components/Example18.vue @@ -0,0 +1,542 @@ + + + diff --git a/demos/vue/src/components/Example19.vue b/demos/vue/src/components/Example19.vue new file mode 100644 index 000000000..7a950c5d3 --- /dev/null +++ b/demos/vue/src/components/Example19.vue @@ -0,0 +1,383 @@ + + + diff --git a/demos/vue/src/components/Example19Detail.vue b/demos/vue/src/components/Example19Detail.vue new file mode 100644 index 000000000..70436cac1 --- /dev/null +++ b/demos/vue/src/components/Example19Detail.vue @@ -0,0 +1,96 @@ + + + diff --git a/demos/vue/src/components/Example19Preload.vue b/demos/vue/src/components/Example19Preload.vue new file mode 100644 index 000000000..cf328aa37 --- /dev/null +++ b/demos/vue/src/components/Example19Preload.vue @@ -0,0 +1,8 @@ + diff --git a/demos/vue/src/components/Example20.vue b/demos/vue/src/components/Example20.vue new file mode 100644 index 000000000..6264c62f6 --- /dev/null +++ b/demos/vue/src/components/Example20.vue @@ -0,0 +1,425 @@ + + + + + diff --git a/demos/vue/src/components/Example21.vue b/demos/vue/src/components/Example21.vue new file mode 100644 index 000000000..828ded2f3 --- /dev/null +++ b/demos/vue/src/components/Example21.vue @@ -0,0 +1,254 @@ + + + + + diff --git a/demos/vue/src/components/Example22.vue b/demos/vue/src/components/Example22.vue new file mode 100644 index 000000000..7c9fb9956 --- /dev/null +++ b/demos/vue/src/components/Example22.vue @@ -0,0 +1,222 @@ + + + + diff --git a/demos/vue/src/components/Example23.vue b/demos/vue/src/components/Example23.vue new file mode 100644 index 000000000..6f86ee7e1 --- /dev/null +++ b/demos/vue/src/components/Example23.vue @@ -0,0 +1,405 @@ + + + diff --git a/demos/vue/src/components/Example24.vue b/demos/vue/src/components/Example24.vue new file mode 100644 index 000000000..a41581abe --- /dev/null +++ b/demos/vue/src/components/Example24.vue @@ -0,0 +1,819 @@ + + + + diff --git a/demos/vue/src/components/Example25.vue b/demos/vue/src/components/Example25.vue new file mode 100644 index 000000000..86de778cf --- /dev/null +++ b/demos/vue/src/components/Example25.vue @@ -0,0 +1,359 @@ + + + + diff --git a/demos/vue/src/components/Example27.vue b/demos/vue/src/components/Example27.vue new file mode 100644 index 000000000..11f4dbcb6 --- /dev/null +++ b/demos/vue/src/components/Example27.vue @@ -0,0 +1,515 @@ + + + + diff --git a/demos/vue/src/components/Example28.vue b/demos/vue/src/components/Example28.vue new file mode 100644 index 000000000..dbc89d42b --- /dev/null +++ b/demos/vue/src/components/Example28.vue @@ -0,0 +1,627 @@ + + + + diff --git a/demos/vue/src/components/Example29.vue b/demos/vue/src/components/Example29.vue new file mode 100644 index 000000000..92a473fb7 --- /dev/null +++ b/demos/vue/src/components/Example29.vue @@ -0,0 +1,101 @@ + + + diff --git a/demos/vue/src/components/Example30.vue b/demos/vue/src/components/Example30.vue new file mode 100644 index 000000000..f8a896d74 --- /dev/null +++ b/demos/vue/src/components/Example30.vue @@ -0,0 +1,1184 @@ + + + + diff --git a/demos/vue/src/components/Example31.vue b/demos/vue/src/components/Example31.vue new file mode 100644 index 000000000..685e24fc9 --- /dev/null +++ b/demos/vue/src/components/Example31.vue @@ -0,0 +1,537 @@ + + + diff --git a/demos/vue/src/components/Example32.vue b/demos/vue/src/components/Example32.vue new file mode 100644 index 000000000..c6c58fff5 --- /dev/null +++ b/demos/vue/src/components/Example32.vue @@ -0,0 +1,921 @@ + + + + diff --git a/demos/vue/src/components/Example33.vue b/demos/vue/src/components/Example33.vue new file mode 100644 index 000000000..07b44ec45 --- /dev/null +++ b/demos/vue/src/components/Example33.vue @@ -0,0 +1,597 @@ + + + diff --git a/demos/vue/src/components/Example34.vue b/demos/vue/src/components/Example34.vue new file mode 100644 index 000000000..ff3749641 --- /dev/null +++ b/demos/vue/src/components/Example34.vue @@ -0,0 +1,598 @@ + + + + diff --git a/demos/vue/src/components/Example35.vue b/demos/vue/src/components/Example35.vue new file mode 100644 index 000000000..0d13f2076 --- /dev/null +++ b/demos/vue/src/components/Example35.vue @@ -0,0 +1,346 @@ + + + diff --git a/demos/vue/src/components/Example36.vue b/demos/vue/src/components/Example36.vue new file mode 100644 index 000000000..562762c85 --- /dev/null +++ b/demos/vue/src/components/Example36.vue @@ -0,0 +1,607 @@ + + + + diff --git a/demos/vue/src/components/Example37.vue b/demos/vue/src/components/Example37.vue new file mode 100644 index 000000000..fb6b98f36 --- /dev/null +++ b/demos/vue/src/components/Example37.vue @@ -0,0 +1,143 @@ + + + diff --git a/demos/vue/src/components/Example38.vue b/demos/vue/src/components/Example38.vue new file mode 100644 index 000000000..9798d3015 --- /dev/null +++ b/demos/vue/src/components/Example38.vue @@ -0,0 +1,532 @@ + + + + diff --git a/demos/vue/src/components/Example39.vue b/demos/vue/src/components/Example39.vue new file mode 100644 index 000000000..c628268c8 --- /dev/null +++ b/demos/vue/src/components/Example39.vue @@ -0,0 +1,481 @@ + + + + diff --git a/demos/vue/src/components/Example40.vue b/demos/vue/src/components/Example40.vue new file mode 100644 index 000000000..1cff3314a --- /dev/null +++ b/demos/vue/src/components/Example40.vue @@ -0,0 +1,266 @@ + + + diff --git a/demos/vue/src/components/Example41.vue b/demos/vue/src/components/Example41.vue new file mode 100644 index 000000000..76c3ca33d --- /dev/null +++ b/demos/vue/src/components/Example41.vue @@ -0,0 +1,312 @@ + + + + diff --git a/demos/vue/src/components/Example42.vue b/demos/vue/src/components/Example42.vue new file mode 100644 index 000000000..7272aee3a --- /dev/null +++ b/demos/vue/src/components/Example42.vue @@ -0,0 +1,245 @@ + + + diff --git a/demos/vue/src/components/custom-inputEditor.ts b/demos/vue/src/components/custom-inputEditor.ts new file mode 100644 index 000000000..4d0283802 --- /dev/null +++ b/demos/vue/src/components/custom-inputEditor.ts @@ -0,0 +1,106 @@ +import type { Column, ColumnEditor, Editor, EditorValidationResult, EditorValidator } from 'slickgrid-vue'; + +/* + * An example of a 'detaching' editor. + * KeyDown events are also handled to provide handling for Tab, Shift-Tab, Esc and Ctrl-Enter. + */ +export class CustomInputEditor implements Editor { + private _lastInputEvent?: KeyboardEvent; + inputElm!: HTMLInputElement; + defaultValue: any; + + constructor(private args: any) { + this.init(); + } + + /** Get Column Definition object */ + get columnDef(): Column { + return this.args?.column ?? {}; + } + + /** Get Column Editor object */ + get columnEditor(): ColumnEditor { + return this.columnDef?.editor ?? {}; + } + + /** Get the Validator function, can be passed in Editor property or Column Definition */ + get validator(): EditorValidator | undefined { + return this.columnEditor?.validator || this.columnDef?.validator; + } + + init(): void { + const placeholder = this.columnEditor?.placeholder || ''; + + this.inputElm = document.createElement('input'); + this.inputElm.className = 'editor-text'; + this.inputElm.placeholder = placeholder; + this.args.container.appendChild(this.inputElm); + + this.inputElm.addEventListener('keydown', this.handleKeydown.bind(this)); + + window.setTimeout(() => { + this.inputElm.focus(); + this.inputElm.select(); + }, 50); + } + + handleKeydown(event: KeyboardEvent) { + this._lastInputEvent = event; + if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') { + event.stopImmediatePropagation(); + } + } + + destroy() { + this.inputElm.removeEventListener('keydown', this.handleKeydown.bind(this)); + this.inputElm.remove(); + } + + focus() { + this.inputElm.focus(); + } + + getValue() { + return this.inputElm.value; + } + + setValue(val: string) { + this.inputElm.value = val; + } + + loadValue(item: any) { + this.defaultValue = item[this.args.column.field] || ''; + this.inputElm.value = this.defaultValue; + this.inputElm.defaultValue = this.defaultValue; + this.inputElm.select(); + } + + serializeValue() { + return this.inputElm.value; + } + + applyValue(item: any, state: any) { + const validation = this.validate(state); + item[this.args.column.field] = validation && validation.valid ? state : ''; + } + + isValueChanged(): boolean { + const lastKeyEvent = this._lastInputEvent?.key; + if (this.columnEditor?.alwaysSaveOnEnterKey && lastKeyEvent === 'Enter') { + return true; + } + return !(this.inputElm.value === '' && this.defaultValue === null) && this.inputElm.value !== this.defaultValue; + } + + validate(inputValue?: any): EditorValidationResult { + if (this.validator) { + const value = inputValue !== undefined ? inputValue : this.inputElm?.value; + return this.validator(value, this.args); + } + + return { + valid: true, + msg: null, + }; + } +} diff --git a/demos/vue/src/components/custom-inputFilter.ts b/demos/vue/src/components/custom-inputFilter.ts new file mode 100644 index 000000000..b9137167a --- /dev/null +++ b/demos/vue/src/components/custom-inputFilter.ts @@ -0,0 +1,143 @@ +import { + type Column, + type ColumnFilter, + emptyElement, + type Filter, + type FilterArguments, + type FilterCallback, + type GridOption, + type OperatorString, + OperatorType, + type SearchTerm, + type SlickGrid, +} from 'slickgrid-vue'; + +export class CustomInputFilter implements Filter { + private _clearFilterTriggered = false; + private _shouldTriggerQuery = true; + private filterElm!: HTMLInputElement; + grid!: SlickGrid; + searchTerms: SearchTerm[] = []; + columnDef!: Column; + callback!: FilterCallback; + operator: OperatorType | OperatorString = OperatorType.equal; + + /** Getter for the Filter Operator */ + get columnFilter(): ColumnFilter { + return this.columnDef?.filter ?? {}; + } + + /** Getter for the Grid Options pulled through the Grid Object */ + get gridOptions(): GridOption { + return (this.grid?.getOptions() ?? {}) as GridOption; + } + + /** + * Initialize the Filter + */ + init(args: FilterArguments) { + this.grid = args.grid as SlickGrid; + this.callback = args.callback; + this.columnDef = args.columnDef; + this.searchTerms = ('searchTerms' in args ? args.searchTerms : []) || []; + + // filter input can only have 1 search term, so we will use the 1st array index if it exist + const searchTerm = Array.isArray(this.searchTerms) && this.searchTerms.length > 0 ? this.searchTerms[0] : ''; + + // step 1, create HTML string template + + // step 2, create the DOM Element of the filter & initialize it if searchTerm is filled + this.filterElm = this.createDomElement(searchTerm); + + // step 3, subscribe to the keyup event and run the callback when that happens + // also add/remove "filled" class for styling purposes + this.filterElm.addEventListener('keyup', this.handleKeyup.bind(this)); + } + + handleKeyup(event: any) { + let value = event.target?.value ?? ''; + const enableWhiteSpaceTrim = this.gridOptions.enableFilterTrimWhiteSpace || this.columnFilter.enableTrimWhiteSpace; + if (typeof value === 'string' && enableWhiteSpaceTrim) { + value = value.trim(); + } + + if (this._clearFilterTriggered) { + this.callback(event, { + columnDef: this.columnDef, + clearFilterTriggered: this._clearFilterTriggered, + shouldTriggerQuery: this._shouldTriggerQuery, + }); + this.filterElm.classList.remove('filled'); + } else { + value === '' ? this.filterElm.classList.remove('filled') : this.filterElm.classList.add('filled'); + this.callback(event, { columnDef: this.columnDef, searchTerms: [value], shouldTriggerQuery: this._shouldTriggerQuery }); + } + // reset both flags for next use + this._clearFilterTriggered = false; + this._shouldTriggerQuery = true; + } + + /** + * Clear the filter value + */ + clear(shouldTriggerQuery = true) { + if (this.filterElm) { + this._clearFilterTriggered = true; + this._shouldTriggerQuery = shouldTriggerQuery; + this.filterElm.value = ''; + this.filterElm.dispatchEvent(new Event('keyup')); + } + } + + /** + * destroy the filter + */ + destroy() { + if (this.filterElm) { + this.filterElm.removeEventListener('keyup', this.handleKeyup); + this.filterElm.remove(); + } + } + + /** Set value(s) on the DOM element */ + setValues(values: any) { + if (values) { + this.filterElm.value = values; + } + } + + // + // private functions + // ------------------ + + /** + * From the html template string, create a DOM element + * @param filterTemplate + */ + private createDomElement(searchTerm?: SearchTerm): HTMLInputElement { + const headerElm = this.grid.getHeaderRowColumn(this.columnDef.id); + emptyElement(headerElm); + + let placeholder = this.gridOptions?.defaultFilterPlaceholder ?? ''; + if (this.columnFilter?.placeholder) { + placeholder = this.columnFilter.placeholder; + } + + // create the DOM element & add an ID and filter class + const filterElm = document.createElement('input'); + filterElm.className = 'form-control search-filter'; + filterElm.placeholder = placeholder; + + const searchTermInput = searchTerm as string; + + filterElm.value = searchTermInput; + filterElm.dataset.columnid = `${this.columnDef.id}`; + + // append the new DOM element to the header row + if (headerElm) { + headerElm.appendChild(filterElm); + } + + return filterElm; + } +} diff --git a/demos/vue/src/components/custom-title-formatter.ts b/demos/vue/src/components/custom-title-formatter.ts new file mode 100644 index 000000000..47a7115a9 --- /dev/null +++ b/demos/vue/src/components/custom-title-formatter.ts @@ -0,0 +1,9 @@ +import { bindable, customElement } from 'aurelia'; + +@customElement({ + name: 'custom-title-formatter', + template: '', +}) +export class CustomTitleFormatter { + @bindable() model: any; +} diff --git a/demos/vue/src/components/data/collection_100_numbers.json b/demos/vue/src/components/data/collection_100_numbers.json new file mode 100644 index 000000000..fd1cbc1c5 --- /dev/null +++ b/demos/vue/src/components/data/collection_100_numbers.json @@ -0,0 +1,12 @@ +[ + { "value": 0, "label": 0, "prefix": "Task", "suffix": "day" }, { "value": 1, "label": 1, "prefix": "Task", "suffix": "day" }, { "value": 2, "label": 2, "prefix": "Task", "suffix": "days" }, { "value": 3, "label": 3, "prefix": "Task", "suffix": "days" }, { "value": 4, "label": 4, "prefix": "Task", "suffix": "days" }, { "value": 5, "label": 5, "prefix": "Task", "suffix": "days" }, { "value": 6, "label": 6, "prefix": "Task", "suffix": "days" }, { "value": 7, "label": 7, "prefix": "Task", "suffix": "days" }, { "value": 8, "label": 8, "prefix": "Task", "suffix": "days" }, { "value": 9, "label": 9, "prefix": "Task", "suffix": "days" }, + { "value": 10, "label": 10, "prefix": "Task", "suffix": "days" }, { "value": 11, "label": 11, "prefix": "Task", "suffix": "days" }, { "value": 12, "label": 12, "prefix": "Task", "suffix": "days" }, { "value": 13, "label": 13, "prefix": "Task", "suffix": "days" }, { "value": 14, "label": 14, "prefix": "Task", "suffix": "days" }, { "value": 15, "label": 15, "prefix": "Task", "suffix": "days" }, { "value": 16, "label": 16, "prefix": "Task", "suffix": "days" }, { "value": 17, "label": 17, "prefix": "Task", "suffix": "days" }, { "value": 18, "label": 18, "prefix": "Task", "suffix": "days" }, { "value": 19, "label": 19, "prefix": "Task", "suffix": "days" }, + { "value": 20, "label": 20, "prefix": "Task", "suffix": "days" }, { "value": 21, "label": 21, "prefix": "Task", "suffix": "days" }, { "value": 22, "label": 22, "prefix": "Task", "suffix": "days" }, { "value": 23, "label": 23, "prefix": "Task", "suffix": "days" }, { "value": 24, "label": 24, "prefix": "Task", "suffix": "days" }, { "value": 25, "label": 25, "prefix": "Task", "suffix": "days" }, { "value": 26, "label": 26, "prefix": "Task", "suffix": "days" }, { "value": 27, "label": 27, "prefix": "Task", "suffix": "days" }, { "value": 28, "label": 28, "prefix": "Task", "suffix": "days" }, { "value": 29, "label": 29, "prefix": "Task", "suffix": "days" }, + { "value": 30, "label": 30, "prefix": "Task", "suffix": "days" }, { "value": 31, "label": 31, "prefix": "Task", "suffix": "days" }, { "value": 32, "label": 32, "prefix": "Task", "suffix": "days" }, { "value": 33, "label": 33, "prefix": "Task", "suffix": "days" }, { "value": 34, "label": 34, "prefix": "Task", "suffix": "days" }, { "value": 35, "label": 35, "prefix": "Task", "suffix": "days" }, { "value": 36, "label": 36, "prefix": "Task", "suffix": "days" }, { "value": 37, "label": 37, "prefix": "Task", "suffix": "days" }, { "value": 38, "label": 38, "prefix": "Task", "suffix": "days" }, { "value": 39, "label": 39, "prefix": "Task", "suffix": "days" }, + { "value": 40, "label": 40, "prefix": "Task", "suffix": "days" }, { "value": 41, "label": 41, "prefix": "Task", "suffix": "days" }, { "value": 42, "label": 42, "prefix": "Task", "suffix": "days" }, { "value": 43, "label": 43, "prefix": "Task", "suffix": "days" }, { "value": 44, "label": 44, "prefix": "Task", "suffix": "days" }, { "value": 45, "label": 45, "prefix": "Task", "suffix": "days" }, { "value": 46, "label": 46, "prefix": "Task", "suffix": "days" }, { "value": 47, "label": 47, "prefix": "Task", "suffix": "days" }, { "value": 48, "label": 48, "prefix": "Task", "suffix": "days" }, { "value": 49, "label": 49, "prefix": "Task", "suffix": "days" }, + { "value": 50, "label": 50, "prefix": "Task", "suffix": "days" }, { "value": 51, "label": 51, "prefix": "Task", "suffix": "days" }, { "value": 52, "label": 52, "prefix": "Task", "suffix": "days" }, { "value": 53, "label": 53, "prefix": "Task", "suffix": "days" }, { "value": 54, "label": 54, "prefix": "Task", "suffix": "days" }, { "value": 55, "label": 55, "prefix": "Task", "suffix": "days" }, { "value": 56, "label": 56, "prefix": "Task", "suffix": "days" }, { "value": 57, "label": 57, "prefix": "Task", "suffix": "days" }, { "value": 58, "label": 58, "prefix": "Task", "suffix": "days" }, { "value": 59, "label": 59, "prefix": "Task", "suffix": "days" }, + { "value": 60, "label": 60, "prefix": "Task", "suffix": "days" }, { "value": 61, "label": 61, "prefix": "Task", "suffix": "days" }, { "value": 62, "label": 62, "prefix": "Task", "suffix": "days" }, { "value": 63, "label": 63, "prefix": "Task", "suffix": "days" }, { "value": 64, "label": 64, "prefix": "Task", "suffix": "days" }, { "value": 65, "label": 65, "prefix": "Task", "suffix": "days" }, { "value": 66, "label": 66, "prefix": "Task", "suffix": "days" }, { "value": 67, "label": 67, "prefix": "Task", "suffix": "days" }, { "value": 68, "label": 68, "prefix": "Task", "suffix": "days" }, { "value": 69, "label": 69, "prefix": "Task", "suffix": "days" }, + { "value": 70, "label": 70, "prefix": "Task", "suffix": "days" }, { "value": 71, "label": 71, "prefix": "Task", "suffix": "days" }, { "value": 72, "label": 72, "prefix": "Task", "suffix": "days" }, { "value": 73, "label": 73, "prefix": "Task", "suffix": "days" }, { "value": 74, "label": 74, "prefix": "Task", "suffix": "days" }, { "value": 75, "label": 75, "prefix": "Task", "suffix": "days" }, { "value": 76, "label": 76, "prefix": "Task", "suffix": "days" }, { "value": 77, "label": 77, "prefix": "Task", "suffix": "days" }, { "value": 78, "label": 78, "prefix": "Task", "suffix": "days" }, { "value": 79, "label": 79, "prefix": "Task", "suffix": "days" }, + { "value": 80, "label": 80, "prefix": "Task", "suffix": "days" }, { "value": 81, "label": 81, "prefix": "Task", "suffix": "days" }, { "value": 82, "label": 82, "prefix": "Task", "suffix": "days" }, { "value": 83, "label": 83, "prefix": "Task", "suffix": "days" }, { "value": 84, "label": 84, "prefix": "Task", "suffix": "days" }, { "value": 85, "label": 85, "prefix": "Task", "suffix": "days" }, { "value": 86, "label": 86, "prefix": "Task", "suffix": "days" }, { "value": 87, "label": 87, "prefix": "Task", "suffix": "days" }, { "value": 88, "label": 88, "prefix": "Task", "suffix": "days" }, { "value": 89, "label": 89, "prefix": "Task", "suffix": "days" }, + { "value": 90, "label": 90, "prefix": "Task", "suffix": "days" }, { "value": 91, "label": 91, "prefix": "Task", "suffix": "days" }, { "value": 92, "label": 92, "prefix": "Task", "suffix": "days" }, { "value": 93, "label": 93, "prefix": "Task", "suffix": "days" }, { "value": 94, "label": 94, "prefix": "Task", "suffix": "days" }, { "value": 95, "label": 95, "prefix": "Task", "suffix": "days" }, { "value": 96, "label": 96, "prefix": "Task", "suffix": "days" }, { "value": 97, "label": 97, "prefix": "Task", "suffix": "days" }, { "value": 98, "label": 98, "prefix": "Task", "suffix": "days" }, { "value": 99, "label": 99, "prefix": "Task", "suffix": "days" } +] diff --git a/demos/vue/src/components/data/collection_500_numbers.json b/demos/vue/src/components/data/collection_500_numbers.json new file mode 100644 index 000000000..1c6b4321d --- /dev/null +++ b/demos/vue/src/components/data/collection_500_numbers.json @@ -0,0 +1,52 @@ +[ + { "value": 0, "label": 0, "text": "day" }, { "value": 1, "label": 1, "text": "day" }, { "value": 2, "label": 2, "text": "days" }, { "value": 3, "label": 3, "text": "days" }, { "value": 4, "label": 4, "text": "days" }, { "value": 5, "label": 5, "text": "days" }, { "value": 6, "label": 6, "text": "days" }, { "value": 7, "label": 7, "text": "days" }, { "value": 8, "label": 8, "text": "days" }, { "value": 9, "label": 9, "text": "days" }, + { "value": 10, "label": 10, "text": "days" }, { "value": 11, "label": 11, "text": "days" }, { "value": 12, "label": 12, "text": "days" }, { "value": 13, "label": 13, "text": "days" }, { "value": 14, "label": 14, "text": "days" }, { "value": 15, "label": 15, "text": "days" }, { "value": 16, "label": 16, "text": "days" }, { "value": 17, "label": 17, "text": "days" }, { "value": 18, "label": 18, "text": "days" }, { "value": 19, "label": 19, "text": "days" }, + { "value": 20, "label": 20, "text": "days" }, { "value": 21, "label": 21, "text": "days" }, { "value": 22, "label": 22, "text": "days" }, { "value": 23, "label": 23, "text": "days" }, { "value": 24, "label": 24, "text": "days" }, { "value": 25, "label": 25, "text": "days" }, { "value": 26, "label": 26, "text": "days" }, { "value": 27, "label": 27, "text": "days" }, { "value": 28, "label": 28, "text": "days" }, { "value": 29, "label": 29, "text": "days" }, + { "value": 30, "label": 30, "text": "days" }, { "value": 31, "label": 31, "text": "days" }, { "value": 32, "label": 32, "text": "days" }, { "value": 33, "label": 33, "text": "days" }, { "value": 34, "label": 34, "text": "days" }, { "value": 35, "label": 35, "text": "days" }, { "value": 36, "label": 36, "text": "days" }, { "value": 37, "label": 37, "text": "days" }, { "value": 38, "label": 38, "text": "days" }, { "value": 39, "label": 39, "text": "days" }, + { "value": 40, "label": 40, "text": "days" }, { "value": 41, "label": 41, "text": "days" }, { "value": 42, "label": 42, "text": "days" }, { "value": 43, "label": 43, "text": "days" }, { "value": 44, "label": 44, "text": "days" }, { "value": 45, "label": 45, "text": "days" }, { "value": 46, "label": 46, "text": "days" }, { "value": 47, "label": 47, "text": "days" }, { "value": 48, "label": 48, "text": "days" }, { "value": 49, "label": 49, "text": "days" }, + { "value": 50, "label": 50, "text": "days" }, { "value": 51, "label": 51, "text": "days" }, { "value": 52, "label": 52, "text": "days" }, { "value": 53, "label": 53, "text": "days" }, { "value": 54, "label": 54, "text": "days" }, { "value": 55, "label": 55, "text": "days" }, { "value": 56, "label": 56, "text": "days" }, { "value": 57, "label": 57, "text": "days" }, { "value": 58, "label": 58, "text": "days" }, { "value": 59, "label": 59, "text": "days" }, + { "value": 60, "label": 60, "text": "days" }, { "value": 61, "label": 61, "text": "days" }, { "value": 62, "label": 62, "text": "days" }, { "value": 63, "label": 63, "text": "days" }, { "value": 64, "label": 64, "text": "days" }, { "value": 65, "label": 65, "text": "days" }, { "value": 66, "label": 66, "text": "days" }, { "value": 67, "label": 67, "text": "days" }, { "value": 68, "label": 68, "text": "days" }, { "value": 69, "label": 69, "text": "days" }, + { "value": 70, "label": 70, "text": "days" }, { "value": 71, "label": 71, "text": "days" }, { "value": 72, "label": 72, "text": "days" }, { "value": 73, "label": 73, "text": "days" }, { "value": 74, "label": 74, "text": "days" }, { "value": 75, "label": 75, "text": "days" }, { "value": 76, "label": 76, "text": "days" }, { "value": 77, "label": 77, "text": "days" }, { "value": 78, "label": 78, "text": "days" }, { "value": 79, "label": 79, "text": "days" }, + { "value": 80, "label": 80, "text": "days" }, { "value": 81, "label": 81, "text": "days" }, { "value": 82, "label": 82, "text": "days" }, { "value": 83, "label": 83, "text": "days" }, { "value": 84, "label": 84, "text": "days" }, { "value": 85, "label": 85, "text": "days" }, { "value": 86, "label": 86, "text": "days" }, { "value": 87, "label": 87, "text": "days" }, { "value": 88, "label": 88, "text": "days" }, { "value": 89, "label": 89, "text": "days" }, + { "value": 90, "label": 90, "text": "days" }, { "value": 91, "label": 91, "text": "days" }, { "value": 92, "label": 92, "text": "days" }, { "value": 93, "label": 93, "text": "days" }, { "value": 94, "label": 94, "text": "days" }, { "value": 95, "label": 95, "text": "days" }, { "value": 96, "label": 96, "text": "days" }, { "value": 97, "label": 97, "text": "days" }, { "value": 98, "label": 98, "text": "days" }, { "value": 99, "label": 99, "text": "days" }, + { "value": 100, "label": 100, "text": "days" }, { "value": 101, "label": 101, "text": "days" }, { "value": 102, "label": 102, "text": "days" }, { "value": 103, "label": 103, "text": "days" }, { "value": 104, "label": 104, "text": "days" }, { "value": 105, "label": 105, "text": "days" }, { "value": 106, "label": 106, "text": "days" }, { "value": 107, "label": 107, "text": "days" }, { "value": 108, "label": 108, "text": "days" }, { "value": 109, "label": 109, "text": "days" }, + { "value": 110, "label": 110, "text": "days" }, { "value": 111, "label": 111, "text": "days" }, { "value": 112, "label": 112, "text": "days" }, { "value": 113, "label": 113, "text": "days" }, { "value": 114, "label": 114, "text": "days" }, { "value": 115, "label": 115, "text": "days" }, { "value": 116, "label": 116, "text": "days" }, { "value": 117, "label": 117, "text": "days" }, { "value": 118, "label": 118, "text": "days" }, { "value": 119, "label": 119, "text": "days" }, + { "value": 120, "label": 120, "text": "days" }, { "value": 121, "label": 121, "text": "days" }, { "value": 122, "label": 122, "text": "days" }, { "value": 123, "label": 123, "text": "days" }, { "value": 124, "label": 124, "text": "days" }, { "value": 125, "label": 125, "text": "days" }, { "value": 126, "label": 126, "text": "days" }, { "value": 127, "label": 127, "text": "days" }, { "value": 128, "label": 128, "text": "days" }, { "value": 129, "label": 129, "text": "days" }, + { "value": 130, "label": 130, "text": "days" }, { "value": 131, "label": 131, "text": "days" }, { "value": 132, "label": 132, "text": "days" }, { "value": 133, "label": 133, "text": "days" }, { "value": 134, "label": 134, "text": "days" }, { "value": 135, "label": 135, "text": "days" }, { "value": 136, "label": 136, "text": "days" }, { "value": 137, "label": 137, "text": "days" }, { "value": 138, "label": 138, "text": "days" }, { "value": 139, "label": 139, "text": "days" }, + { "value": 140, "label": 140, "text": "days" }, { "value": 141, "label": 141, "text": "days" }, { "value": 142, "label": 142, "text": "days" }, { "value": 143, "label": 143, "text": "days" }, { "value": 144, "label": 144, "text": "days" }, { "value": 145, "label": 145, "text": "days" }, { "value": 146, "label": 146, "text": "days" }, { "value": 147, "label": 147, "text": "days" }, { "value": 148, "label": 148, "text": "days" }, { "value": 149, "label": 149, "text": "days" }, + { "value": 150, "label": 150, "text": "days" }, { "value": 151, "label": 151, "text": "days" }, { "value": 152, "label": 152, "text": "days" }, { "value": 153, "label": 153, "text": "days" }, { "value": 154, "label": 154, "text": "days" }, { "value": 155, "label": 155, "text": "days" }, { "value": 156, "label": 156, "text": "days" }, { "value": 157, "label": 157, "text": "days" }, { "value": 158, "label": 158, "text": "days" }, { "value": 159, "label": 159, "text": "days" }, + { "value": 160, "label": 160, "text": "days" }, { "value": 161, "label": 161, "text": "days" }, { "value": 162, "label": 162, "text": "days" }, { "value": 163, "label": 163, "text": "days" }, { "value": 164, "label": 164, "text": "days" }, { "value": 165, "label": 165, "text": "days" }, { "value": 166, "label": 166, "text": "days" }, { "value": 167, "label": 167, "text": "days" }, { "value": 168, "label": 168, "text": "days" }, { "value": 169, "label": 169, "text": "days" }, + { "value": 170, "label": 170, "text": "days" }, { "value": 171, "label": 171, "text": "days" }, { "value": 172, "label": 172, "text": "days" }, { "value": 173, "label": 173, "text": "days" }, { "value": 174, "label": 174, "text": "days" }, { "value": 175, "label": 175, "text": "days" }, { "value": 176, "label": 176, "text": "days" }, { "value": 177, "label": 177, "text": "days" }, { "value": 178, "label": 178, "text": "days" }, { "value": 179, "label": 179, "text": "days" }, + { "value": 180, "label": 180, "text": "days" }, { "value": 181, "label": 181, "text": "days" }, { "value": 182, "label": 182, "text": "days" }, { "value": 183, "label": 183, "text": "days" }, { "value": 184, "label": 184, "text": "days" }, { "value": 185, "label": 185, "text": "days" }, { "value": 186, "label": 186, "text": "days" }, { "value": 187, "label": 187, "text": "days" }, { "value": 188, "label": 188, "text": "days" }, { "value": 189, "label": 189, "text": "days" }, + { "value": 190, "label": 190, "text": "days" }, { "value": 191, "label": 191, "text": "days" }, { "value": 192, "label": 192, "text": "days" }, { "value": 193, "label": 193, "text": "days" }, { "value": 194, "label": 194, "text": "days" }, { "value": 195, "label": 195, "text": "days" }, { "value": 196, "label": 196, "text": "days" }, { "value": 197, "label": 197, "text": "days" }, { "value": 198, "label": 198, "text": "days" }, { "value": 199, "label": 199, "text": "days" }, + { "value": 200, "label": 200, "text": "days" }, { "value": 201, "label": 201, "text": "days" }, { "value": 202, "label": 202, "text": "days" }, { "value": 203, "label": 203, "text": "days" }, { "value": 204, "label": 204, "text": "days" }, { "value": 205, "label": 205, "text": "days" }, { "value": 206, "label": 206, "text": "days" }, { "value": 207, "label": 207, "text": "days" }, { "value": 208, "label": 208, "text": "days" }, { "value": 209, "label": 209, "text": "days" }, + { "value": 210, "label": 210, "text": "days" }, { "value": 211, "label": 211, "text": "days" }, { "value": 212, "label": 212, "text": "days" }, { "value": 213, "label": 213, "text": "days" }, { "value": 214, "label": 214, "text": "days" }, { "value": 215, "label": 215, "text": "days" }, { "value": 216, "label": 216, "text": "days" }, { "value": 217, "label": 217, "text": "days" }, { "value": 218, "label": 218, "text": "days" }, { "value": 219, "label": 219, "text": "days" }, + { "value": 220, "label": 220, "text": "days" }, { "value": 221, "label": 221, "text": "days" }, { "value": 222, "label": 222, "text": "days" }, { "value": 223, "label": 223, "text": "days" }, { "value": 224, "label": 224, "text": "days" }, { "value": 225, "label": 225, "text": "days" }, { "value": 226, "label": 226, "text": "days" }, { "value": 227, "label": 227, "text": "days" }, { "value": 228, "label": 228, "text": "days" }, { "value": 229, "label": 229, "text": "days" }, + { "value": 230, "label": 230, "text": "days" }, { "value": 231, "label": 231, "text": "days" }, { "value": 232, "label": 232, "text": "days" }, { "value": 233, "label": 233, "text": "days" }, { "value": 234, "label": 234, "text": "days" }, { "value": 235, "label": 235, "text": "days" }, { "value": 236, "label": 236, "text": "days" }, { "value": 237, "label": 237, "text": "days" }, { "value": 238, "label": 238, "text": "days" }, { "value": 239, "label": 239, "text": "days" }, + { "value": 240, "label": 240, "text": "days" }, { "value": 241, "label": 241, "text": "days" }, { "value": 242, "label": 242, "text": "days" }, { "value": 243, "label": 243, "text": "days" }, { "value": 244, "label": 244, "text": "days" }, { "value": 245, "label": 245, "text": "days" }, { "value": 246, "label": 246, "text": "days" }, { "value": 247, "label": 247, "text": "days" }, { "value": 248, "label": 248, "text": "days" }, { "value": 249, "label": 249, "text": "days" }, + { "value": 250, "label": 250, "text": "days" }, { "value": 251, "label": 251, "text": "days" }, { "value": 252, "label": 252, "text": "days" }, { "value": 253, "label": 253, "text": "days" }, { "value": 254, "label": 254, "text": "days" }, { "value": 255, "label": 255, "text": "days" }, { "value": 256, "label": 256, "text": "days" }, { "value": 257, "label": 257, "text": "days" }, { "value": 258, "label": 258, "text": "days" }, { "value": 259, "label": 259, "text": "days" }, + { "value": 260, "label": 260, "text": "days" }, { "value": 261, "label": 261, "text": "days" }, { "value": 262, "label": 262, "text": "days" }, { "value": 263, "label": 263, "text": "days" }, { "value": 264, "label": 264, "text": "days" }, { "value": 265, "label": 265, "text": "days" }, { "value": 266, "label": 266, "text": "days" }, { "value": 267, "label": 267, "text": "days" }, { "value": 268, "label": 268, "text": "days" }, { "value": 269, "label": 269, "text": "days" }, + { "value": 270, "label": 270, "text": "days" }, { "value": 271, "label": 271, "text": "days" }, { "value": 272, "label": 272, "text": "days" }, { "value": 273, "label": 273, "text": "days" }, { "value": 274, "label": 274, "text": "days" }, { "value": 275, "label": 275, "text": "days" }, { "value": 276, "label": 276, "text": "days" }, { "value": 277, "label": 277, "text": "days" }, { "value": 278, "label": 278, "text": "days" }, { "value": 279, "label": 279, "text": "days" }, + { "value": 280, "label": 280, "text": "days" }, { "value": 281, "label": 281, "text": "days" }, { "value": 282, "label": 282, "text": "days" }, { "value": 283, "label": 283, "text": "days" }, { "value": 284, "label": 284, "text": "days" }, { "value": 285, "label": 285, "text": "days" }, { "value": 286, "label": 286, "text": "days" }, { "value": 287, "label": 287, "text": "days" }, { "value": 288, "label": 288, "text": "days" }, { "value": 289, "label": 289, "text": "days" }, + { "value": 290, "label": 290, "text": "days" }, { "value": 291, "label": 291, "text": "days" }, { "value": 292, "label": 292, "text": "days" }, { "value": 293, "label": 293, "text": "days" }, { "value": 294, "label": 294, "text": "days" }, { "value": 295, "label": 295, "text": "days" }, { "value": 296, "label": 296, "text": "days" }, { "value": 297, "label": 297, "text": "days" }, { "value": 298, "label": 298, "text": "days" }, { "value": 299, "label": 299, "text": "days" }, + { "value": 300, "label": 300, "text": "days" }, { "value": 301, "label": 301, "text": "days" }, { "value": 302, "label": 302, "text": "days" }, { "value": 303, "label": 303, "text": "days" }, { "value": 304, "label": 304, "text": "days" }, { "value": 305, "label": 305, "text": "days" }, { "value": 306, "label": 306, "text": "days" }, { "value": 307, "label": 307, "text": "days" }, { "value": 308, "label": 308, "text": "days" }, { "value": 309, "label": 309, "text": "days" }, + { "value": 310, "label": 310, "text": "days" }, { "value": 311, "label": 311, "text": "days" }, { "value": 312, "label": 312, "text": "days" }, { "value": 313, "label": 313, "text": "days" }, { "value": 314, "label": 314, "text": "days" }, { "value": 315, "label": 315, "text": "days" }, { "value": 316, "label": 316, "text": "days" }, { "value": 317, "label": 317, "text": "days" }, { "value": 318, "label": 318, "text": "days" }, { "value": 319, "label": 319, "text": "days" }, + { "value": 320, "label": 320, "text": "days" }, { "value": 321, "label": 321, "text": "days" }, { "value": 322, "label": 322, "text": "days" }, { "value": 323, "label": 323, "text": "days" }, { "value": 324, "label": 324, "text": "days" }, { "value": 325, "label": 325, "text": "days" }, { "value": 326, "label": 326, "text": "days" }, { "value": 327, "label": 327, "text": "days" }, { "value": 328, "label": 328, "text": "days" }, { "value": 329, "label": 329, "text": "days" }, + { "value": 330, "label": 330, "text": "days" }, { "value": 331, "label": 331, "text": "days" }, { "value": 332, "label": 332, "text": "days" }, { "value": 333, "label": 333, "text": "days" }, { "value": 334, "label": 334, "text": "days" }, { "value": 335, "label": 335, "text": "days" }, { "value": 336, "label": 336, "text": "days" }, { "value": 337, "label": 337, "text": "days" }, { "value": 338, "label": 338, "text": "days" }, { "value": 339, "label": 339, "text": "days" }, + { "value": 340, "label": 340, "text": "days" }, { "value": 341, "label": 341, "text": "days" }, { "value": 342, "label": 342, "text": "days" }, { "value": 343, "label": 343, "text": "days" }, { "value": 344, "label": 344, "text": "days" }, { "value": 345, "label": 345, "text": "days" }, { "value": 346, "label": 346, "text": "days" }, { "value": 347, "label": 347, "text": "days" }, { "value": 348, "label": 348, "text": "days" }, { "value": 349, "label": 349, "text": "days" }, + { "value": 350, "label": 350, "text": "days" }, { "value": 351, "label": 351, "text": "days" }, { "value": 352, "label": 352, "text": "days" }, { "value": 353, "label": 353, "text": "days" }, { "value": 354, "label": 354, "text": "days" }, { "value": 355, "label": 355, "text": "days" }, { "value": 356, "label": 356, "text": "days" }, { "value": 357, "label": 357, "text": "days" }, { "value": 358, "label": 358, "text": "days" }, { "value": 359, "label": 359, "text": "days" }, + { "value": 360, "label": 360, "text": "days" }, { "value": 361, "label": 361, "text": "days" }, { "value": 362, "label": 362, "text": "days" }, { "value": 363, "label": 363, "text": "days" }, { "value": 364, "label": 364, "text": "days" }, { "value": 365, "label": 365, "text": "days" }, { "value": 366, "label": 366, "text": "days" }, { "value": 367, "label": 367, "text": "days" }, { "value": 368, "label": 368, "text": "days" }, { "value": 369, "label": 369, "text": "days" }, + { "value": 370, "label": 370, "text": "days" }, { "value": 371, "label": 371, "text": "days" }, { "value": 372, "label": 372, "text": "days" }, { "value": 373, "label": 373, "text": "days" }, { "value": 374, "label": 374, "text": "days" }, { "value": 375, "label": 375, "text": "days" }, { "value": 376, "label": 376, "text": "days" }, { "value": 377, "label": 377, "text": "days" }, { "value": 378, "label": 378, "text": "days" }, { "value": 379, "label": 379, "text": "days" }, + { "value": 380, "label": 380, "text": "days" }, { "value": 381, "label": 381, "text": "days" }, { "value": 382, "label": 382, "text": "days" }, { "value": 383, "label": 383, "text": "days" }, { "value": 384, "label": 384, "text": "days" }, { "value": 385, "label": 385, "text": "days" }, { "value": 386, "label": 386, "text": "days" }, { "value": 387, "label": 387, "text": "days" }, { "value": 388, "label": 388, "text": "days" }, { "value": 389, "label": 389, "text": "days" }, + { "value": 390, "label": 390, "text": "days" }, { "value": 391, "label": 391, "text": "days" }, { "value": 392, "label": 392, "text": "days" }, { "value": 393, "label": 393, "text": "days" }, { "value": 394, "label": 394, "text": "days" }, { "value": 395, "label": 395, "text": "days" }, { "value": 396, "label": 396, "text": "days" }, { "value": 397, "label": 397, "text": "days" }, { "value": 398, "label": 398, "text": "days" }, { "value": 399, "label": 399, "text": "days" }, + { "value": 400, "label": 400, "text": "days" }, { "value": 401, "label": 401, "text": "days" }, { "value": 402, "label": 402, "text": "days" }, { "value": 403, "label": 403, "text": "days" }, { "value": 404, "label": 404, "text": "days" }, { "value": 405, "label": 405, "text": "days" }, { "value": 406, "label": 406, "text": "days" }, { "value": 407, "label": 407, "text": "days" }, { "value": 408, "label": 408, "text": "days" }, { "value": 409, "label": 409, "text": "days" }, + { "value": 410, "label": 410, "text": "days" }, { "value": 411, "label": 411, "text": "days" }, { "value": 412, "label": 412, "text": "days" }, { "value": 413, "label": 413, "text": "days" }, { "value": 414, "label": 414, "text": "days" }, { "value": 415, "label": 415, "text": "days" }, { "value": 416, "label": 416, "text": "days" }, { "value": 417, "label": 417, "text": "days" }, { "value": 418, "label": 418, "text": "days" }, { "value": 419, "label": 419, "text": "days" }, + { "value": 420, "label": 420, "text": "days" }, { "value": 421, "label": 421, "text": "days" }, { "value": 422, "label": 422, "text": "days" }, { "value": 423, "label": 423, "text": "days" }, { "value": 424, "label": 424, "text": "days" }, { "value": 425, "label": 425, "text": "days" }, { "value": 426, "label": 426, "text": "days" }, { "value": 427, "label": 427, "text": "days" }, { "value": 428, "label": 428, "text": "days" }, { "value": 429, "label": 429, "text": "days" }, + { "value": 430, "label": 430, "text": "days" }, { "value": 431, "label": 431, "text": "days" }, { "value": 432, "label": 432, "text": "days" }, { "value": 433, "label": 433, "text": "days" }, { "value": 434, "label": 434, "text": "days" }, { "value": 435, "label": 435, "text": "days" }, { "value": 436, "label": 436, "text": "days" }, { "value": 437, "label": 437, "text": "days" }, { "value": 438, "label": 438, "text": "days" }, { "value": 439, "label": 439, "text": "days" }, + { "value": 440, "label": 440, "text": "days" }, { "value": 441, "label": 441, "text": "days" }, { "value": 442, "label": 442, "text": "days" }, { "value": 443, "label": 443, "text": "days" }, { "value": 444, "label": 444, "text": "days" }, { "value": 445, "label": 445, "text": "days" }, { "value": 446, "label": 446, "text": "days" }, { "value": 447, "label": 447, "text": "days" }, { "value": 448, "label": 448, "text": "days" }, { "value": 449, "label": 449, "text": "days" }, + { "value": 450, "label": 450, "text": "days" }, { "value": 451, "label": 451, "text": "days" }, { "value": 452, "label": 452, "text": "days" }, { "value": 453, "label": 453, "text": "days" }, { "value": 454, "label": 454, "text": "days" }, { "value": 455, "label": 455, "text": "days" }, { "value": 456, "label": 456, "text": "days" }, { "value": 457, "label": 457, "text": "days" }, { "value": 458, "label": 458, "text": "days" }, { "value": 459, "label": 459, "text": "days" }, + { "value": 460, "label": 460, "text": "days" }, { "value": 461, "label": 461, "text": "days" }, { "value": 462, "label": 462, "text": "days" }, { "value": 463, "label": 463, "text": "days" }, { "value": 464, "label": 464, "text": "days" }, { "value": 465, "label": 465, "text": "days" }, { "value": 466, "label": 466, "text": "days" }, { "value": 467, "label": 467, "text": "days" }, { "value": 468, "label": 468, "text": "days" }, { "value": 469, "label": 469, "text": "days" }, + { "value": 470, "label": 470, "text": "days" }, { "value": 471, "label": 471, "text": "days" }, { "value": 472, "label": 472, "text": "days" }, { "value": 473, "label": 473, "text": "days" }, { "value": 474, "label": 474, "text": "days" }, { "value": 475, "label": 475, "text": "days" }, { "value": 476, "label": 476, "text": "days" }, { "value": 477, "label": 477, "text": "days" }, { "value": 478, "label": 478, "text": "days" }, { "value": 479, "label": 479, "text": "days" }, + { "value": 480, "label": 480, "text": "days" }, { "value": 481, "label": 481, "text": "days" }, { "value": 482, "label": 482, "text": "days" }, { "value": 483, "label": 483, "text": "days" }, { "value": 484, "label": 484, "text": "days" }, { "value": 485, "label": 485, "text": "days" }, { "value": 486, "label": 486, "text": "days" }, { "value": 487, "label": 487, "text": "days" }, { "value": 488, "label": 488, "text": "days" }, { "value": 489, "label": 489, "text": "days" }, + { "value": 490, "label": 490, "text": "days" }, { "value": 491, "label": 491, "text": "days" }, { "value": 492, "label": 492, "text": "days" }, { "value": 493, "label": 493, "text": "days" }, { "value": 494, "label": 494, "text": "days" }, { "value": 495, "label": 495, "text": "days" }, { "value": 496, "label": 496, "text": "days" }, { "value": 497, "label": 497, "text": "days" }, { "value": 498, "label": 498, "text": "days" }, { "value": 499, "label": 499, "text": "days" } +] diff --git a/demos/vue/src/components/data/countries.json b/demos/vue/src/components/data/countries.json new file mode 100644 index 000000000..9e0fa9dd7 --- /dev/null +++ b/demos/vue/src/components/data/countries.json @@ -0,0 +1,245 @@ +[ + {"name": "Afghanistan", "code": "AF"}, + {"name": "ร…land Islands", "code": "AX"}, + {"name": "Albania", "code": "AL"}, + {"name": "Algeria", "code": "DZ"}, + {"name": "American Samoa", "code": "AS"}, + {"name": "AndorrA", "code": "AD"}, + {"name": "Angola", "code": "AO"}, + {"name": "Anguilla", "code": "AI"}, + {"name": "Antarctica", "code": "AQ"}, + {"name": "Antigua and Barbuda", "code": "AG"}, + {"name": "Argentina", "code": "AR"}, + {"name": "Armenia", "code": "AM"}, + {"name": "Aruba", "code": "AW"}, + {"name": "Australia", "code": "AU"}, + {"name": "Austria", "code": "AT"}, + {"name": "Azerbaijan", "code": "AZ"}, + {"name": "Bahamas", "code": "BS"}, + {"name": "Bahrain", "code": "BH"}, + {"name": "Bangladesh", "code": "BD"}, + {"name": "Barbados", "code": "BB"}, + {"name": "Belarus", "code": "BY"}, + {"name": "Belgium", "code": "BE"}, + {"name": "Belize", "code": "BZ"}, + {"name": "Benin", "code": "BJ"}, + {"name": "Bermuda", "code": "BM"}, + {"name": "Bhutan", "code": "BT"}, + {"name": "Bolivia", "code": "BO"}, + {"name": "Bosnia and Herzegovina", "code": "BA"}, + {"name": "Botswana", "code": "BW"}, + {"name": "Bouvet Island", "code": "BV"}, + {"name": "Brazil", "code": "BR"}, + {"name": "British Indian Ocean Territory", "code": "IO"}, + {"name": "Brunei Darussalam", "code": "BN"}, + {"name": "Bulgaria", "code": "BG"}, + {"name": "Burkina Faso", "code": "BF"}, + {"name": "Burundi", "code": "BI"}, + {"name": "Cambodia", "code": "KH"}, + {"name": "Cameroon", "code": "CM"}, + {"name": "Canada", "code": "CA"}, + {"name": "Cape Verde", "code": "CV"}, + {"name": "Cayman Islands", "code": "KY"}, + {"name": "Central African Republic", "code": "CF"}, + {"name": "Chad", "code": "TD"}, + {"name": "Chile", "code": "CL"}, + {"name": "China", "code": "CN"}, + {"name": "Christmas Island", "code": "CX"}, + {"name": "Cocos (Keeling) Islands", "code": "CC"}, + {"name": "Colombia", "code": "CO"}, + {"name": "Comoros", "code": "KM"}, + {"name": "Congo", "code": "CG"}, + {"name": "Congo, The Democratic Republic of the", "code": "CD"}, + {"name": "Cook Islands", "code": "CK"}, + {"name": "Costa Rica", "code": "CR"}, + {"name": "Cote D'Ivoire", "code": "CI"}, + {"name": "Croatia", "code": "HR"}, + {"name": "Cuba", "code": "CU"}, + {"name": "Cyprus", "code": "CY"}, + {"name": "Czech Republic", "code": "CZ"}, + {"name": "Denmark", "code": "DK"}, + {"name": "Djibouti", "code": "DJ"}, + {"name": "Dominica", "code": "DM"}, + {"name": "Dominican Republic", "code": "DO"}, + {"name": "Ecuador", "code": "EC"}, + {"name": "Egypt", "code": "EG"}, + {"name": "El Salvador", "code": "SV"}, + {"name": "Equatorial Guinea", "code": "GQ"}, + {"name": "Eritrea", "code": "ER"}, + {"name": "Estonia", "code": "EE"}, + {"name": "Ethiopia", "code": "ET"}, + {"name": "Falkland Islands (Malvinas)", "code": "FK"}, + {"name": "Faroe Islands", "code": "FO"}, + {"name": "Fiji", "code": "FJ"}, + {"name": "Finland", "code": "FI"}, + {"name": "France", "code": "FR"}, + {"name": "French Guiana", "code": "GF"}, + {"name": "French Polynesia", "code": "PF"}, + {"name": "French Southern Territories", "code": "TF"}, + {"name": "Gabon", "code": "GA"}, + {"name": "Gambia", "code": "GM"}, + {"name": "Georgia", "code": "GE"}, + {"name": "Germany", "code": "DE"}, + {"name": "Ghana", "code": "GH"}, + {"name": "Gibraltar", "code": "GI"}, + {"name": "Greece", "code": "GR"}, + {"name": "Greenland", "code": "GL"}, + {"name": "Grenada", "code": "GD"}, + {"name": "Guadeloupe", "code": "GP"}, + {"name": "Guam", "code": "GU"}, + {"name": "Guatemala", "code": "GT"}, + {"name": "Guernsey", "code": "GG"}, + {"name": "Guinea", "code": "GN"}, + {"name": "Guinea-Bissau", "code": "GW"}, + {"name": "Guyana", "code": "GY"}, + {"name": "Haiti", "code": "HT"}, + {"name": "Heard Island and Mcdonald Islands", "code": "HM"}, + {"name": "Holy See (Vatican City State)", "code": "VA"}, + {"name": "Honduras", "code": "HN"}, + {"name": "Hong Kong", "code": "HK"}, + {"name": "Hungary", "code": "HU"}, + {"name": "Iceland", "code": "IS"}, + {"name": "India", "code": "IN"}, + {"name": "Indonesia", "code": "ID"}, + {"name": "Iran, Islamic Republic Of", "code": "IR"}, + {"name": "Iraq", "code": "IQ"}, + {"name": "Ireland", "code": "IE"}, + {"name": "Isle of Man", "code": "IM"}, + {"name": "Israel", "code": "IL"}, + {"name": "Italy", "code": "IT"}, + {"name": "Jamaica", "code": "JM"}, + {"name": "Japan", "code": "JP"}, + {"name": "Jersey", "code": "JE"}, + {"name": "Jordan", "code": "JO"}, + {"name": "Kazakhstan", "code": "KZ"}, + {"name": "Kenya", "code": "KE"}, + {"name": "Kiribati", "code": "KI"}, + {"name": "Korea, Democratic People's Republic of", "code": "KP"}, + {"name": "Korea, Republic of", "code": "KR"}, + {"name": "Kuwait", "code": "KW"}, + {"name": "Kyrgyzstan", "code": "KG"}, + {"name": "Lao People's Democratic Republic", "code": "LA"}, + {"name": "Latvia", "code": "LV"}, + {"name": "Lebanon", "code": "LB"}, + {"name": "Lesotho", "code": "LS"}, + {"name": "Liberia", "code": "LR"}, + {"name": "Libyan Arab Jamahiriya", "code": "LY"}, + {"name": "Liechtenstein", "code": "LI"}, + {"name": "Lithuania", "code": "LT"}, + {"name": "Luxembourg", "code": "LU"}, + {"name": "Macao", "code": "MO"}, + {"name": "Macedonia, The Former Yugoslav Republic of", "code": "MK"}, + {"name": "Madagascar", "code": "MG"}, + {"name": "Malawi", "code": "MW"}, + {"name": "Malaysia", "code": "MY"}, + {"name": "Maldives", "code": "MV"}, + {"name": "Mali", "code": "ML"}, + {"name": "Malta", "code": "MT"}, + {"name": "Marshall Islands", "code": "MH"}, + {"name": "Martinique", "code": "MQ"}, + {"name": "Mauritania", "code": "MR"}, + {"name": "Mauritius", "code": "MU"}, + {"name": "Mayotte", "code": "YT"}, + {"name": "Mexico", "code": "MX"}, + {"name": "Micronesia, Federated States of", "code": "FM"}, + {"name": "Moldova, Republic of", "code": "MD"}, + {"name": "Monaco", "code": "MC"}, + {"name": "Mongolia", "code": "MN"}, + {"name": "Montserrat", "code": "MS"}, + {"name": "Morocco", "code": "MA"}, + {"name": "Mozambique", "code": "MZ"}, + {"name": "Myanmar", "code": "MM"}, + {"name": "Namibia", "code": "NA"}, + {"name": "Nauru", "code": "NR"}, + {"name": "Nepal", "code": "NP"}, + {"name": "Netherlands", "code": "NL"}, + {"name": "Netherlands Antilles", "code": "AN"}, + {"name": "New Caledonia", "code": "NC"}, + {"name": "New Zealand", "code": "NZ"}, + {"name": "Nicaragua", "code": "NI"}, + {"name": "Niger", "code": "NE"}, + {"name": "Nigeria", "code": "NG"}, + {"name": "Niue", "code": "NU"}, + {"name": "Norfolk Island", "code": "NF"}, + {"name": "Northern Mariana Islands", "code": "MP"}, + {"name": "Norway", "code": "NO"}, + {"name": "Oman", "code": "OM"}, + {"name": "Pakistan", "code": "PK"}, + {"name": "Palau", "code": "PW"}, + {"name": "Palestinian Territory, Occupied", "code": "PS"}, + {"name": "Panama", "code": "PA"}, + {"name": "Papua New Guinea", "code": "PG"}, + {"name": "Paraguay", "code": "PY"}, + {"name": "Peru", "code": "PE"}, + {"name": "Philippines", "code": "PH"}, + {"name": "Pitcairn", "code": "PN"}, + {"name": "Poland", "code": "PL"}, + {"name": "Portugal", "code": "PT"}, + {"name": "Puerto Rico", "code": "PR"}, + {"name": "Qatar", "code": "QA"}, + {"name": "Reunion", "code": "RE"}, + {"name": "Romania", "code": "RO"}, + {"name": "Russian Federation", "code": "RU"}, + {"name": "RWANDA", "code": "RW"}, + {"name": "Saint Helena", "code": "SH"}, + {"name": "Saint Kitts and Nevis", "code": "KN"}, + {"name": "Saint Lucia", "code": "LC"}, + {"name": "Saint Pierre and Miquelon", "code": "PM"}, + {"name": "Saint Vincent and the Grenadines", "code": "VC"}, + {"name": "Samoa", "code": "WS"}, + {"name": "San Marino", "code": "SM"}, + {"name": "Sao Tome and Principe", "code": "ST"}, + {"name": "Saudi Arabia", "code": "SA"}, + {"name": "Senegal", "code": "SN"}, + {"name": "Serbia and Montenegro", "code": "CS"}, + {"name": "Seychelles", "code": "SC"}, + {"name": "Sierra Leone", "code": "SL"}, + {"name": "Singapore", "code": "SG"}, + {"name": "Slovakia", "code": "SK"}, + {"name": "Slovenia", "code": "SI"}, + {"name": "Solomon Islands", "code": "SB"}, + {"name": "Somalia", "code": "SO"}, + {"name": "South Africa", "code": "ZA"}, + {"name": "South Georgia and the South Sandwich Islands", "code": "GS"}, + {"name": "Spain", "code": "ES"}, + {"name": "Sri Lanka", "code": "LK"}, + {"name": "Sudan", "code": "SD"}, + {"name": "Suriname", "code": "SR"}, + {"name": "Svalbard and Jan Mayen", "code": "SJ"}, + {"name": "Swaziland", "code": "SZ"}, + {"name": "Sweden", "code": "SE"}, + {"name": "Switzerland", "code": "CH"}, + {"name": "Syrian Arab Republic", "code": "SY"}, + {"name": "Taiwan, Province of China", "code": "TW"}, + {"name": "Tajikistan", "code": "TJ"}, + {"name": "Tanzania, United Republic of", "code": "TZ"}, + {"name": "Thailand", "code": "TH"}, + {"name": "Timor-Leste", "code": "TL"}, + {"name": "Togo", "code": "TG"}, + {"name": "Tokelau", "code": "TK"}, + {"name": "Tonga", "code": "TO"}, + {"name": "Trinidad and Tobago", "code": "TT"}, + {"name": "Tunisia", "code": "TN"}, + {"name": "Turkey", "code": "TR"}, + {"name": "Turkmenistan", "code": "TM"}, + {"name": "Turks and Caicos Islands", "code": "TC"}, + {"name": "Tuvalu", "code": "TV"}, + {"name": "Uganda", "code": "UG"}, + {"name": "Ukraine", "code": "UA"}, + {"name": "United Arab Emirates", "code": "AE"}, + {"name": "United Kingdom", "code": "GB"}, + {"name": "United States", "code": "US"}, + {"name": "United States Minor Outlying Islands", "code": "UM"}, + {"name": "Uruguay", "code": "UY"}, + {"name": "Uzbekistan", "code": "UZ"}, + {"name": "Vanuatu", "code": "VU"}, + {"name": "Venezuela", "code": "VE"}, + {"name": "Viet Nam", "code": "VN"}, + {"name": "Virgin Islands, British", "code": "VG"}, + {"name": "Virgin Islands, U.S.", "code": "VI"}, + {"name": "Wallis and Futuna", "code": "WF"}, + {"name": "Western Sahara", "code": "EH"}, + {"name": "Yemen", "code": "YE"}, + {"name": "Zambia", "code": "ZM"}, + {"name": "Zimbabwe", "code": "ZW"} +] diff --git a/demos/vue/src/components/data/country_names.json b/demos/vue/src/components/data/country_names.json new file mode 100644 index 000000000..1e1e3b363 --- /dev/null +++ b/demos/vue/src/components/data/country_names.json @@ -0,0 +1,245 @@ +[ + "Afghanistan", + "ร…land Islands", + "Albania", + "Algeria", + "American Samoa", + "AndorrA", + "Angola", + "Anguilla", + "Antarctica", + "Antigua and Barbuda", + "Argentina", + "Armenia", + "Aruba", + "Australia", + "Austria", + "Azerbaijan", + "Bahamas", + "Bahrain", + "Bangladesh", + "Barbados", + "Belarus", + "Belgium", + "Belize", + "Benin", + "Bermuda", + "Bhutan", + "Bolivia", + "Bosnia and Herzegovina", + "Botswana", + "Bouvet Island", + "Brazil", + "British Indian Ocean Territory", + "Brunei Darussalam", + "Bulgaria", + "Burkina Faso", + "Burundi", + "Cambodia", + "Cameroon", + "Canada", + "Cape Verde", + "Cayman Islands", + "Central African Republic", + "Chad", + "Chile", + "China", + "Christmas Island", + "Cocos (Keeling) Islands", + "Colombia", + "Comoros", + "Congo", + "Congo, The Democratic Republic of the", + "Cook Islands", + "Costa Rica", + "Cote D'Ivoire", + "Croatia", + "Cuba", + "Cyprus", + "Czech Republic", + "Denmark", + "Djibouti", + "Dominica", + "Dominican Republic", + "Ecuador", + "Egypt", + "El Salvador", + "Equatorial Guinea", + "Eritrea", + "Estonia", + "Ethiopia", + "Falkland Islands (Malvinas)", + "Faroe Islands", + "Fiji", + "Finland", + "France", + "French Guiana", + "French Polynesia", + "French Southern Territories", + "Gabon", + "Gambia", + "Georgia", + "Germany", + "Ghana", + "Gibraltar", + "Greece", + "Greenland", + "Grenada", + "Guadeloupe", + "Guam", + "Guatemala", + "Guernsey", + "Guinea", + "Guinea-Bissau", + "Guyana", + "Haiti", + "Heard Island and Mcdonald Islands", + "Holy See (Vatican City State)", + "Honduras", + "Hong Kong", + "Hungary", + "Iceland", + "India", + "Indonesia", + "Iran, Islamic Republic Of", + "Iraq", + "Ireland", + "Isle of Man", + "Israel", + "Italy", + "Jamaica", + "Japan", + "Jersey", + "Jordan", + "Kazakhstan", + "Kenya", + "Kiribati", + "Korea, Democratic People'S Republic of", + "Korea, Republic of", + "Kuwait", + "Kyrgyzstan", + "Lao People'S Democratic Republic", + "Latvia", + "Lebanon", + "Lesotho", + "Liberia", + "Libyan Arab Jamahiriya", + "Liechtenstein", + "Lithuania", + "Luxembourg", + "Macao", + "Macedonia, The Former Yugoslav Republic of", + "Madagascar", + "Malawi", + "Malaysia", + "Maldives", + "Mali", + "Malta", + "Marshall Islands", + "Martinique", + "Mauritania", + "Mauritius", + "Mayotte", + "Mexico", + "Micronesia, Federated States of", + "Moldova, Republic of", + "Monaco", + "Mongolia", + "Montserrat", + "Morocco", + "Mozambique", + "Myanmar", + "Namibia", + "Nauru", + "Nepal", + "Netherlands", + "Netherlands Antilles", + "New Caledonia", + "New Zealand", + "Nicaragua", + "Niger", + "Nigeria", + "Niue", + "Norfolk Island", + "Northern Mariana Islands", + "Norway", + "Oman", + "Pakistan", + "Palau", + "Palestinian Territory, Occupied", + "Panama", + "Papua New Guinea", + "Paraguay", + "Peru", + "Philippines", + "Pitcairn", + "Poland", + "Portugal", + "Puerto Rico", + "Qatar", + "Reunion", + "Romania", + "Russian Federation", + "RWANDA", + "Saint Helena", + "Saint Kitts and Nevis", + "Saint Lucia", + "Saint Pierre and Miquelon", + "Saint Vincent and the Grenadines", + "Samoa", + "San Marino", + "Sao Tome and Principe", + "Saudi Arabia", + "Senegal", + "Serbia and Montenegro", + "Seychelles", + "Sierra Leone", + "Singapore", + "Slovakia", + "Slovenia", + "Solomon Islands", + "Somalia", + "South Africa", + "South Georgia and the South Sandwich Islands", + "Spain", + "Sri Lanka", + "Sudan", + "Suriname", + "Svalbard and Jan Mayen", + "Swaziland", + "Sweden", + "Switzerland", + "Syrian Arab Republic", + "Taiwan, Province of China", + "Tajikistan", + "Tanzania, United Republic of", + "Thailand", + "Timor-Leste", + "Togo", + "Tokelau", + "Tonga", + "Trinidad and Tobago", + "Tunisia", + "Turkey", + "Turkmenistan", + "Turks and Caicos Islands", + "Tuvalu", + "Uganda", + "Ukraine", + "United Arab Emirates", + "United Kingdom", + "United States", + "United States Minor Outlying Islands", + "Uruguay", + "Uzbekistan", + "Vanuatu", + "Venezuela", + "Viet Nam", + "Virgin Islands, British", + "Virgin Islands, U.S.", + "Wallis and Futuna", + "Western Sahara", + "Yemen", + "Zambia", + "Zimbabwe" +] diff --git a/demos/vue/src/components/data/customers_100.json b/demos/vue/src/components/data/customers_100.json new file mode 100644 index 000000000..bee8511b9 --- /dev/null +++ b/demos/vue/src/components/data/customers_100.json @@ -0,0 +1,102 @@ +[ + {"name":"Ethel Price","gender":"female","company":"Enersol","id":1,"category":{"id":1,"name":"Gold"}}, + {"name":"Claudine Neal","gender":"female","company":"Sealoud","id":2,"category":{"id":1,"name":"Gold"}}, + {"name":"Beryl Rice","gender":"female","company":"Velity","id":3,"category":{"id":1,"name":"Gold"}}, + {"name":"Wilder Gonzales","gender":"male","company":"Geekko","id":4,"category":{"id":1,"name":"Gold"}}, + {"name":"Georgina Schultz","gender":"female","company":"Suretech","id":5,"category":{"id":1,"name":"Gold"}}, + {"name":"Carroll Buchanan","gender":"male","company":"Ecosys","id":6,"category":{"id":1,"name":"Gold"}}, + {"name":"Valarie Atkinson","gender":"female","company":"Hopeli","id":7,"category":{"id":1,"name":"Gold"}}, + {"name":"Schroeder Mathews","gender":"male","company":"Polarium","id":8,"category":{"id":1,"name":"Gold"}}, + {"name":"Lynda Mendoza","gender":"female","company":"Dogspa","id":9,"category":{"id":1,"name":"Gold"}}, + {"name":"Sarah Massey","gender":"female","company":"Bisba","id":10,"category":{"id":1,"name":"Gold"}}, + {"name":"Robles Boyle","gender":"male","company":"Comtract","id":11,"category":{"id":1,"name":"Gold"}}, + {"name":"Evans Hickman","gender":"male","company":"Parleynet","id":12,"category":{"id":1,"name":"Gold"}}, + {"name":"Dawson Barber","gender":"male","company":"Dymi","id":13,"category":{"id":1,"name":"Gold"}}, + {"name":"Bruce Strong","gender":"male","company":"Xyqag","id":14,"category":{"id":1,"name":"Gold"}}, + {"name":"Nellie Whitfield","gender":"female","company":"Exospace","id":15,"category":{"id":1,"name":"Gold"}}, + {"name":"Jackson Macias","gender":"male","company":"Aquamate","id":16,"category":{"id":1,"name":"Gold"}}, + {"name":"Pena Pena","gender":"male","company":"Quarx","id":17,"category":{"id":1,"name":"Gold"}}, + {"name":"Lelia Gates","gender":"female","company":"Proxsoft","id":18,"category":{"id":1,"name":"Gold"}}, + {"name":"Letitia Vasquez","gender":"female","company":"Slumberia","id":19,"category":{"id":1,"name":"Gold"}}, + {"name":"Trevino Moreno","gender":"male","company":"Conjurica","id":20,"category":{"id":1,"name":"Gold"}}, + {"name":"Barr Page","gender":"male","company":"Apex","id":21,"category":{"id":1,"name":"Gold"}}, + {"name":"Kirkland Merrill","gender":"male","company":"Utara","id":22,"category":{"id":1,"name":"Gold"}}, + {"name":"Blanche Conley","gender":"female","company":"Imkan","id":23,"category":{"id":1,"name":"Gold"}}, + {"name":"Atkins Dunlap","gender":"male","company":"Comveyor","id":24,"category":{"id":1,"name":"Gold"}}, + {"name":"Everett Foreman","gender":"male","company":"Maineland","id":25,"category":{"id":1,"name":"Gold"}}, + {"name":"Gould Randolph","gender":"male","company":"Intergeek","id":26,"category":{"id":1,"name":"Gold"}}, + {"name":"Kelli Leon","gender":"female","company":"Verbus","id":27,"category":{"id":1,"name":"Gold"}}, + {"name":"Freda Mason","gender":"female","company":"Accidency","id":28,"category":{"id":1,"name":"Gold"}}, + {"name":"Tucker Maxwell","gender":"male","company":"Lumbrex","id":29,"category":{"id":1,"name":"Gold"}}, + {"name":"Yvonne Parsons","gender":"female","company":"Zolar","id":30,"category":{"id":1,"name":"Gold"}}, + {"name":"Woods Key","gender":"male","company":"Bedder","id":31,"category":{"id":1,"name":"Gold"}}, + {"name":"Stephens Reilly","gender":"male","company":"Acusage","id":32,"category":{"id":1,"name":"Gold"}}, + {"name":"Mcfarland Sparks","gender":"male","company":"Comvey","id":33,"category":{"id":2,"name":"Silver"}}, + {"name":"Jocelyn Sawyer","gender":"female","company":"Fortean","id":34,"category":{"id":2,"name":"Silver"}}, + {"name":"Renee Barr","gender":"female","company":"Kiggle","id":35,"category":{"id":2,"name":"Silver"}}, + {"name":"Gaines Beck","gender":"male","company":"Sequitur","id":36,"category":{"id":2,"name":"Silver"}}, + {"name":"Luisa Farrell","gender":"female","company":"Cinesanct","id":37,"category":{"id":2,"name":"Silver"}}, + {"name":"Robyn Strickland","gender":"female","company":"Obones","id":38,"category":{"id":2,"name":"Silver"}}, + {"name":"Roseann Jarvis","gender":"female","company":"Aquazure","id":39,"category":{"id":2,"name":"Silver"}}, + {"name":"Johnston Park","gender":"male","company":"Netur","id":40,"category":{"id":2,"name":"Silver"}}, + {"name":"Wong Craft","gender":"male","company":"Opticall","id":41,"category":{"id":2,"name":"Silver"}}, + {"name":"Merritt Cole","gender":"male","company":"Techtrix","id":42,"category":{"id":2,"name":"Silver"}}, + {"name":"Dale Byrd","gender":"female","company":"Kneedles","id":43,"category":{"id":2,"name":"Silver"}}, + {"name":"Sara Delgado","gender":"female","company":"Netagy","id":44,"category":{"id":2,"name":"Silver"}}, + {"name":"Alisha Myers","gender":"female","company":"Intradisk","id":45,"category":{"id":2,"name":"Silver"}}, + {"name":"Felecia Smith","gender":"female","company":"Futurity","id":46,"category":{"id":2,"name":"Silver"}}, + {"name":"Neal Harvey","gender":"male","company":"Pyramax","id":47,"category":{"id":2,"name":"Silver"}}, + {"name":"Nola Miles","gender":"female","company":"Sonique","id":48,"category":{"id":2,"name":"Silver"}}, + {"name":"Herring Pierce","gender":"male","company":"Geeketron","id":49,"category":{"id":2,"name":"Silver"}}, + {"name":"Shelley Rodriquez","gender":"female","company":"Bostonic","id":50,"category":{"id":2,"name":"Silver"}}, + {"name":"Cora Chase","gender":"female","company":"Isonus","id":51,"category":{"id":2,"name":"Silver"}}, + {"name":"Mckay Santos","gender":"male","company":"Amtas","id":52,"category":{"id":2,"name":"Silver"}}, + {"name":"Hilda Crane","gender":"female","company":"Jumpstack","id":53,"category":{"id":2,"name":"Silver"}}, + {"name":"Jeanne Lindsay","gender":"female","company":"Genesynk","id":54,"category":{"id":2,"name":"Silver"}}, + {"name":"Frye Sharpe","gender":"male","company":"Eplode","id":55,"category":{"id":2,"name":"Silver"}}, + {"name":"Velma Fry","gender":"female","company":"Ronelon","id":56,"category":{"id":2,"name":"Silver"}}, + {"name":"Reyna Espinoza","gender":"female","company":"Prismatic","id":57,"category":{"id":2,"name":"Silver"}}, + {"name":"Spencer Sloan","gender":"male","company":"Comverges","id":58,"category":{"id":2,"name":"Silver"}}, + {"name":"Graham Marsh","gender":"male","company":"Medifax","id":59,"category":{"id":2,"name":"Silver"}}, + {"name":"Hale Boone","gender":"male","company":"Digial","id":60,"category":{"id":2,"name":"Silver"}}, + {"name":"Wiley Hubbard","gender":"male","company":"Zensus","id":61,"category":{"id":2,"name":"Silver"}}, + {"name":"Blackburn Drake","gender":"male","company":"Frenex","id":62,"category":{"id":2,"name":"Silver"}}, + {"name":"Franco Hunter","gender":"male","company":"Rockabye","id":63,"category":{"id":2,"name":"Silver"}}, + {"name":"Barnett Case","gender":"male","company":"Norali","id":64,"category":{"id":2,"name":"Silver"}}, + {"name":"Alexander Foley","gender":"male","company":"Geekosis","id":65,"category":{"id":3,"name":"Bronze"}}, + {"name":"Lynette Stein","gender":"female","company":"Macronaut","id":66,"category":{"id":3,"name":"Bronze"}}, + {"name":"Anthony Joyner","gender":"male","company":"Senmei","id":67,"category":{"id":3,"name":"Bronze"}}, + {"name":"Garrett Brennan","gender":"male","company":"Bluegrain","id":68,"category":{"id":3,"name":"Bronze"}}, + {"name":"Betsy Horton","gender":"female","company":"Zilla","id":69,"category":{"id":3,"name":"Bronze"}}, + {"name":"Patton Small","gender":"male","company":"Genmex","id":70,"category":{"id":3,"name":"Bronze"}}, + {"name":"Lakisha Huber","gender":"female","company":"Insource","id":71,"category":{"id":3,"name":"Bronze"}}, + {"name":"Lindsay Avery","gender":"female","company":"Unq","id":72,"category":{"id":3,"name":"Bronze"}}, + {"name":"Ayers Hood","gender":"male","company":"Accuprint","id":73,"category":{"id":3,"name":"Bronze"}}, + {"name":"Torres Durham","gender":"male","company":"Uplinx","id":74,"category":{"id":3,"name":"Bronze"}}, + {"name":"Vincent Hernandez","gender":"male","company":"Talendula","id":75,"category":{"id":3,"name":"Bronze"}}, + {"name":"Baird Ryan","gender":"male","company":"Aquasseur","id":76,"category":{"id":3,"name":"Bronze"}}, + {"name":"Georgia Mercer","gender":"female","company":"Skyplex","id":77,"category":{"id":3,"name":"Bronze"}}, + {"name":"Francesca Elliott","gender":"female","company":"Nspire","id":78,"category":{"id":3,"name":"Bronze"}}, + {"name":"Lyons Peters","gender":"male","company":"Quinex","id":79,"category":{"id":3,"name":"Bronze"}}, + {"name":"Kristi Brewer","gender":"female","company":"Oronoko","id":80,"category":{"id":3,"name":"Bronze"}}, + {"name":"Tonya Bray","gender":"female","company":"Insuron","id":81,"category":{"id":3,"name":"Bronze"}}, + {"name":"Valenzuela Huff","gender":"male","company":"Applideck","id":82,"category":{"id":3,"name":"Bronze"}}, + {"name":"Tiffany Anderson","gender":"female","company":"Zanymax","id":83,"category":{"id":3,"name":"Bronze"}}, + {"name":"Jerri King","gender":"female","company":"Eventex","id":84,"category":{"id":3,"name":"Bronze"}}, + {"name":"Rocha Meadows","gender":"male","company":"Goko","id":85,"category":{"id":3,"name":"Bronze"}}, + {"name":"Marcy Green","gender":"female","company":"Pharmex","id":86,"category":{"id":3,"name":"Bronze"}}, + {"name":"Kirk Cross","gender":"male","company":"Portico","id":87,"category":{"id":3,"name":"Bronze"}}, + {"name":"Hattie Mullen","gender":"female","company":"Zilencio","id":88,"category":{"id":3,"name":"Bronze"}}, + {"name":"Deann Bridges","gender":"female","company":"Equitox","id":89,"category":{"id":3,"name":"Bronze"}}, + {"name":"Chaney Roach","gender":"male","company":"Qualitern","id":90,"category":{"id":3,"name":"Bronze"}}, + {"name":"Consuelo Dickson","gender":"female","company":"Poshome","id":91,"category":{"id":3,"name":"Bronze"}}, + {"name":"Billie Rowe","gender":"female","company":"Cemention","id":92,"category":{"id":3,"name":"Bronze"}}, + {"name":"Bean Donovan","gender":"male","company":"Mantro","id":93,"category":{"id":3,"name":"Bronze"}}, + {"name":"Lancaster Patel","gender":"male","company":"Krog","id":94,"category":{"id":3,"name":"Bronze"}}, + {"name":"Rosa Dyer","gender":"female","company":"Netility","id":95,"category":{"id":3,"name":"Bronze"}}, + {"name":"Christine Compton","gender":"female","company":"Bleeko","id":96,"category":{"id":3,"name":"Bronze"}}, + {"name":"Milagros Finch","gender":"female","company":"Handshake","id":97,"category":{"id":3,"name":"Bronze"}}, + {"name":"Ericka Alvarado","gender":"female","company":"Lyrichord","id":98,"category":{"id":3,"name":"Bronze"}}, + {"name":"Sylvia Sosa","gender":"female","company":"Circum","id":99,"category":{"id":3,"name":"Bronze"}}, + {"name":"Humphrey Curtis","gender":"male","company":"Corepan","id":100,"category":{"id":3,"name":"Bronze"}} + ] \ No newline at end of file diff --git a/demos/vue/src/components/data/example-data.js b/demos/vue/src/components/data/example-data.js new file mode 100644 index 000000000..79252ef21 --- /dev/null +++ b/demos/vue/src/components/data/example-data.js @@ -0,0 +1,16 @@ +const data = []; + +for (let i = 0; i < 500; i++) { + const d = (data[i] = {}); + + d.id = i; + d['title'] = 'Task ' + i; + d['description'] = 'This is a sample task description.\n It can be multiline'; + d['duration'] = '5 days'; + d['percentComplete'] = Math.round(Math.random() * 100); + d['start'] = '01/01/2009'; + d['finish'] = '01/05/2009'; + d['effortDriven'] = i % 5 === 0; +} + +export default data; diff --git a/demos/vue/src/components/utilities.ts b/demos/vue/src/components/utilities.ts new file mode 100644 index 000000000..2c71a879b --- /dev/null +++ b/demos/vue/src/components/utilities.ts @@ -0,0 +1,4 @@ +export function zeroPadding(input: string | number) { + const number = parseInt(input as string, 10); + return number < 10 ? `0${number}` : number; +} diff --git a/demos/vue/src/main.ts b/demos/vue/src/main.ts new file mode 100644 index 000000000..8b8e7ef0b --- /dev/null +++ b/demos/vue/src/main.ts @@ -0,0 +1,34 @@ +import '@slickgrid-universal/common/dist/styles/sass/slickgrid-theme-bootstrap.scss'; +import 'bootstrap'; +import './styles.scss'; + +import i18next from 'i18next'; +import Backend from 'i18next-http-backend'; +import I18NextVue from 'i18next-vue'; +import { createApp } from 'vue'; + +import App from './App.vue'; +import localeEn from './assets/locales/en/translation.json'; +import localeFr from './assets/locales/fr/translation.json'; +import { router } from './router/index.js'; + +i18next.use(Backend).init({ + // the translations + // (tip move them in a JSON file and import them, + // backend: { + // loadPath: 'assets/locales/{{lng}}/{{ns}}.json', + // }, + resources: { + en: { translation: localeEn }, + fr: { translation: localeFr }, + }, + // ns: ['translation'], + // defaultNS: 'translation', + lng: 'en', + fallbackLng: 'en', + debug: false, + interpolation: { + escapeValue: false, + }, +}); +createApp(App).use(I18NextVue, { i18next }).use(router).mount('#app'); diff --git a/demos/vue/src/router/index.ts b/demos/vue/src/router/index.ts new file mode 100644 index 000000000..f5ff4e46f --- /dev/null +++ b/demos/vue/src/router/index.ts @@ -0,0 +1,94 @@ +import type { RouteRecordRaw } from 'vue-router'; +import { createRouter, createWebHashHistory } from 'vue-router'; + +import Example1 from '../components/Example01.vue'; +import Example2 from '../components/Example02.vue'; +import Example3 from '../components/Example03.vue'; +import Example4 from '../components/Example04.vue'; +import Example5 from '../components/Example05.vue'; +import Example6 from '../components/Example06.vue'; +import Example7 from '../components/Example07.vue'; +import Example8 from '../components/Example08.vue'; +import Example9 from '../components/Example09.vue'; +import Example10 from '../components/Example10.vue'; +import Example11 from '../components/Example11.vue'; +import Example12 from '../components/Example12.vue'; +import Example13 from '../components/Example13.vue'; +import Example14 from '../components/Example14.vue'; +import Example15 from '../components/Example15.vue'; +import Example16 from '../components/Example16.vue'; +import Example18 from '../components/Example18.vue'; +import Example19 from '../components/Example19.vue'; +import Example20 from '../components/Example20.vue'; +import Example21 from '../components/Example21.vue'; +import Example22 from '../components/Example22.vue'; +import Example23 from '../components/Example23.vue'; +import Example24 from '../components/Example24.vue'; +import Example25 from '../components/Example25.vue'; +import Example27 from '../components/Example27.vue'; +import Example28 from '../components/Example28.vue'; +import Example29 from '../components/Example29.vue'; +import Example30 from '../components/Example30.vue'; +import Example31 from '../components/Example31.vue'; +import Example32 from '../components/Example32.vue'; +import Example33 from '../components/Example33.vue'; +import Example34 from '../components/Example34.vue'; +import Example35 from '../components/Example35.vue'; +import Example36 from '../components/Example36.vue'; +import Example37 from '../components/Example37.vue'; +import Example38 from '../components/Example38.vue'; +import Example39 from '../components/Example39.vue'; +import Example40 from '../components/Example40.vue'; +import Example41 from '../components/Example41.vue'; +import Example42 from '../components/Example42.vue'; +import Home from '../Home.vue'; + +export const routes: RouteRecordRaw[] = [ + { path: '/', name: 'root', redirect: '/example1' }, + { path: '/home', name: 'home', component: Home }, + { path: '/example1', name: '1- Basic Grid / 2 Grids', component: Example1 }, + { path: '/example2', name: '2- Formatters', component: Example2 }, + { path: '/example3', name: '3- Editors / Delete', component: Example3 }, + { path: '/example4', name: '4- Client Side Sort/Filter', component: Example4 }, + { path: '/example5', name: '5- Backend OData Service', component: Example5 }, + { path: '/example6', name: '6- Backend GraphQL Service', component: Example6 }, + { path: '/example7', name: '7- Header Button Plugin', component: Example7 }, + { path: '/example8', name: '8- Header Menu Plugin', component: Example8 }, + { path: '/example9', name: '9- Grid Menu Control', component: Example9 }, + { path: '/example10', name: '10- Row Selection / 2 Grids', component: Example10 }, + { path: '/example11', name: '11- Add/Update Grid Item', component: Example11 }, + { path: '/example12', name: '12- Localization (i18n)', component: Example12 }, + { path: '/example13', name: '13- Grouping & Aggregators', component: Example13 }, + { path: '/example14', name: '14- Column Span & Header Grouping', component: Example14 }, + { path: '/example15', name: '15- Grid State & Local Storage', component: Example15 }, + { path: '/example16', name: '16- Row Move Plugin', component: Example16 }, + { path: '/example18', name: '18- Draggable Grouping', component: Example18 }, + { path: '/example19', name: '19- Row Detail View', component: Example19 }, + { path: '/example20', name: '20- Pinned Columns / Rows', component: Example20 }, + { path: '/example21', name: '21- Grid AutoHeight (full height)', component: Example21 }, + { path: '/example22', name: '22- with Bootstrap Tabs', component: Example22 }, + { path: '/example23', name: '23- Filter by Range of Values', component: Example23 }, + { path: '/example24', name: '24- Cell & Context Menu', component: Example24 }, + { path: '/example25', name: '25- GraphQL without Pagination', component: Example25 }, + { path: '/example27', name: '27- Tree Data (Parent/Child)', component: Example27 }, + { path: '/example28', name: '28- Tree Data (Hierarchical set)', component: Example28 }, + { path: '/example29', name: '29- Grid Header & Footer Slots', component: Example29 }, + { path: '/example30', name: '30- Composite Editor Model', component: Example30 }, + { path: '/example31', name: '31- Backend OData with RxJS', component: Example31 }, + { path: '/example32', name: '32- Columns Resize by Content', component: Example32 }, + { path: '/example33', name: '33- Regular & Custom Tooltip', component: Example33 }, + { path: '/example34', name: '34- Real-Time Trading Platform', component: Example34 }, + { path: '/example35', name: '35- Row Based Editing', component: Example35 }, + { path: '/example36', name: '36- Excel Export Formulas', component: Example36 }, + { path: '/example37', name: '37- Footer Totals Row', component: Example37 }, + { path: '/example38', name: '38- Infinite Scroll with OData', component: Example38 }, + { path: '/example39', name: '39- Infinite Scroll with GraphQL', component: Example39 }, + { path: '/example40', name: '40- Infinite Scroll from JSON data', component: Example40 }, + { path: '/example41', name: '41- Drag & Drop', component: Example41 }, + { path: '/example42', name: '42- Custom Pagination', component: Example42 }, +]; + +export const router = createRouter({ + history: createWebHashHistory(), + routes, +}); diff --git a/demos/vue/src/styles.scss b/demos/vue/src/styles.scss new file mode 100644 index 000000000..209c31227 --- /dev/null +++ b/demos/vue/src/styles.scss @@ -0,0 +1,234 @@ +@use 'sass:color'; + +$navbar-height: 56px; +$side-menu-width: 250px; +$button-border-color: #ababab; +$button-style-bg-color: #fff; +$primary-color: #0e6cfa; + +@use 'bootstrap/scss/bootstrap' with ( + $primary: $primary-color +); + +// -- 1. load with modern `@use` +// @use '@slickgrid-universal/common/dist/styles/sass/slickgrid-theme-bootstrap.scss'; +@use '@slickgrid-universal/common/dist/styles/sass/slickgrid-theme-bootstrap.scss' with ( + $slick-primary-color: $primary-color, + // $slick-input-focus-box-shadow: 0 0 0 0.25rem rgba($primary-color, 0.25), + ); + +// -- 2. load with legacy `@import` +// $slick-primary-color: red; +// $slick-link-color: red; +// @import '@slickgrid-universal/common/dist/styles/sass/slickgrid-theme-bootstrap.scss'; + +:root { + --ms-choice-border: var(--bs-border-width) solid var(--bs-border-color); + // --slick-button-style-bg-color: #fff; + // --slick-button-border-color: #c7c7c7; +} + +.bold { + font-weight: bold; +} + +.italic { + font-style: italic; +} + +.font18 { + font-size: 18px; +} + +.hidden { + display: none; +} + +.btn-icon { + display: inline-flex; + align-items: center; + gap: 4px; +} + +.gap-4px { + gap: 4px; +} + +.btn-group-xs > .btn, +.btn-xs { + padding: 1px 5px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; + margin: 0; + font-size: 12px; + height: 22px; + vertical-align: middle; +} + +.mdi-pencil.pointer:hover { + color: #00bfff; +} +.mdi-trash-can.pointer:hover { + color: #ff002b; +} + +.body-content { + margin-top: $navbar-height; +} + +.lightblue { + color: lightblue; +} + +.red { + color: red; +} + +.subtitle { + font-size: 0.875em; + font-style: italic; + color: grey; + margin-bottom: 10px; +} + +.faded { + opacity: 0.7; +} + +.faded:hover { + opacity: 0.9; +} + +section { + margin: 0; +} + +/** Sidebar (left) and Content (right) */ +@media (min-width: 1200px) { + .panel-wm-content .container { + width: calc(1170px - #{$side-menu-width}); + } +} + +.nav-docs { + background-color: #fff; + border-bottom: 1px solid #d6d6d6; +} + +.panel-wm { + padding: #{$navbar-height} 0 0 0; + + .nav-stacked { + padding-bottom: 30px; + + .nav-item { + width: 100%; + } + } + + .nav > li > a { + padding: 10px 15px; + border-radius: 0; + } + + .panel-wm-content { + margin-left: $side-menu-width; + padding: 0 1rem; + } + + .panel-wm-left { + position: fixed; + z-index: 400; + transition: left 0.15s; + top: $navbar-height; + bottom: 0; + left: 0; + background-color: #f5f5f5; + transform: translate3d(0, 0, 0); + border-right: 1px solid #d0d0d0; + overflow-y: auto; + width: $side-menu-width; + } +} + +.navbar-brand { + margin-right: 4px; +} + +.github-button-container { + position: relative; + margin: 0 5px; +} + +.slick-dark-mode { + --slick-button-style-bg-color: #212121; + --slick-button-border-color: #626262; + .text-primary { + color: #599bfe !important; + } +} + +.button-style { + cursor: pointer; + background-color: var(--slick-button-style-bg-color, $button-style-bg-color); + border: 1px solid #{var(--slick-button-border-color, $button-border-color)}; + border-radius: 2px; + justify-content: center; + text-align: center; + + &:hover { + border-color: color.adjust($button-border-color, $lightness: -10%); + } +} + +.panel-wm-content { + background-color: #fff; + /* the height is 100% minus the 2 navbars */ + height: calc(100vh - 56px); + padding: 0 10px; + + h3 { + color: #333; + } + + .subtitle { + color: #727272; + } +} + +.panel-wm-content.dark-mode { + background-color: #212529; + color: #dddddd; + + h3 { + color: #dddddd; + } + + .subtitle { + color: #cbcbcb; + } + + .btn-outline-secondary { + color: #dfdfdf; + } +} + +/* editable field with outline border */ +.slick-cell .editing-field, +.slick-cell.selected .editing-field { + border: 1px solid #dddbda; + padding: 3px 5px 3px 4px; + margin: -2px; + height: calc(100% + 4px); + border-radius: 3px; + background-color: #ffffff; + cursor: pointer; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + &:hover { + border: 1px solid #adadad; + } +} diff --git a/demos/vue/src/vite-env.d.ts b/demos/vue/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/demos/vue/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/demos/vue/test/cypress.config.mjs b/demos/vue/test/cypress.config.mjs new file mode 100644 index 000000000..c59a506f1 --- /dev/null +++ b/demos/vue/test/cypress.config.mjs @@ -0,0 +1,32 @@ +import { defineConfig } from 'cypress'; + +export default defineConfig({ + projectId: 'gtbpy4', + video: false, + viewportWidth: 1200, + viewportHeight: 1020, + fixturesFolder: 'test/cypress/fixtures', + screenshotsFolder: 'test/cypress/screenshots', + videosFolder: 'test/cypress/videos', + numTestsKeptInMemory: 5, + retries: { + experimentalStrategy: 'detect-flake-and-pass-on-threshold', + experimentalOptions: { + maxRetries: 2, + passesRequired: 1, + }, + + // you must also explicitly set openMode and runMode to + // either true or false when using experimental retries + openMode: false, // Cypress UI + runMode: true, // run in CI + }, + e2e: { + baseUrl: 'http://localhost:7000/#', + experimentalRunAllSpecs: true, + supportFile: 'test/cypress/support/index.ts', + specPattern: 'test/cypress/e2e/**/*.cy.ts', + excludeSpecPattern: process.env.CI ? ['**/node_modules/**', '**/000-*.cy.ts'] : ['**/node_modules/**'], + testIsolation: false, + }, +}); diff --git a/demos/vue/test/cypress/e2e/example01.cy.ts b/demos/vue/test/cypress/e2e/example01.cy.ts new file mode 100644 index 000000000..e2459891b --- /dev/null +++ b/demos/vue/test/cypress/e2e/example01.cy.ts @@ -0,0 +1,456 @@ +describe('Example 1 - Basic Grids', () => { + const fullTitles = ['Title', 'Duration (days)', '% Complete', 'Start', 'Finish', 'Effort Driven']; + + beforeEach(() => { + // add a serve mode to avoid adding the GitHub Stars link since that can slowdown Cypress considerably + // because it keeps waiting for it to load, we also preserve the cookie for all other tests + cy.setCookie('serve-mode', 'cypress'); + }); + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example1`, { timeout: 50000 }); + cy.get('h2').should('contain', 'Example 1: Basic Grids'); + cy.getCookie('serve-mode').its('value').should('eq', 'cypress'); + }); + + it('should have 2 grids of size 800 by 225px', () => { + cy.get('#slickGridContainer-grid1-1') + .should('have.css', 'width', '800px'); + + cy.get('#slickGridContainer-grid1-1 > .slickgrid-container') + .should($el => expect(parseInt(`${$el.height()}`, 10)).to.eq(225)); + + cy.get('#slickGridContainer-grid1-2') + .should('have.css', 'width', '800px'); + + cy.get('#slickGridContainer-grid1-2 > .slickgrid-container') + .should($el => expect(parseInt(`${$el.height()}`, 10)).to.eq(225)); + }); + + it('should have exact column titles on 1st grid', () => { + cy.get('#slickGridContainer-grid1-1') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + }); + + it('should hover over the Title column and click on "Sort Descending" command', () => { + cy.get('#slickGridContainer-grid1-1') + .find('.slick-header-column') + .first() + .trigger('mouseover') + .children('.slick-header-menu-button') + .should('be.hidden') + .invoke('show') + .click(); + + cy.get('.slick-header-menu .slick-menu-command-list') + .should('be.visible') + .children('.slick-menu-item:nth-of-type(4)') + .children('.slick-menu-content') + .should('contain', 'Sort Descending') + .click(); + + cy.get('.slick-row') + .first() + .children('.slick-cell') + .first() + .should('contain', 'Task 994'); + }); + + it('should hover over the "Title" column of 2nd grid and click on "Sort Ascending" command', () => { + const tasks = ['Task 0', 'Task 1', 'Task 10', 'Task 100', 'Task 101']; + + cy.get('#grid1-2') + .find('.slick-header-column') + .first() + .trigger('mouseover') + .children('.slick-header-menu-button') + .invoke('show') + .click(); + + cy.get('.slick-header-menu .slick-menu-command-list') + .should('be.visible') + .children('.slick-menu-item:nth-of-type(3)') + .children('.slick-menu-content') + .should('contain', 'Sort Ascending') + .click(); + + cy.get('#grid1-2') + .find('.slick-row') + .each(($row, index) => { + if (index > tasks.length - 1) { + return; + } + cy.wrap($row).children('.slick-cell') + .first() + .should('contain', tasks[index]); + }); + }); + + it('should hover over the "Duration" column of 2nd grid, Sort Ascending and have 2 sorts', () => { + cy.get('#grid1-2') + .find('.slick-header-column:nth-child(2)') + .trigger('mouseover') + .children('.slick-header-menu-button') + .invoke('show') + .click(); + + cy.get('#grid1-2') + .find('.slick-header-menu .slick-menu-command-list') + .should('be.visible') + .children('.slick-menu-item:nth-of-type(4)') + .click(); + + cy.get('#grid1-2') + .find('.slick-sort-indicator-asc') + .should('have.length', 1) + .siblings('.slick-sort-indicator-numbered') + .contains('1'); + + cy.get('#grid1-2') + .find('.slick-sort-indicator-desc') + .should('have.length', 1) + .siblings('.slick-sort-indicator-numbered') + .contains('2'); + }); + + it('should clear sorting of grid2 using the Grid Menu "Clear all Sorting" command', () => { + cy.get('#grid1-2') + .find('button.slick-grid-menu-button') + .trigger('click') + .click(); + }); + + it('should have no sorting in 2nd grid (back to default sorted by id)', () => { + let gridUid = ''; + const grid2Tasks = ['Task 0', 'Task 1', 'Task 2', 'Task 3', 'Task 4']; + + cy.get('#grid1-2') + .should(($grid) => { + const classes = $grid.prop('className').split(' '); + gridUid = classes.find(className => /slickgrid_.*/.test(className)); + expect(gridUid).to.not.be.null; + }) + .then(() => { + cy.get(`.slick-grid-menu.${gridUid}`) + .find('.slick-menu-item') + .first() + .find('span') + .contains('Clear all Sorting') + .click(); + + cy.get('#grid1-2') + .find('.slick-sort-indicator-asc') + .should('have.length', 0); + + cy.get('#grid1-2') + .find('.slick-sort-indicator-desc') + .should('have.length', 0); + + cy.get('#grid1-2') + .find('.slick-row') + .each(($row, index) => { + if (index > grid2Tasks.length - 1) { + return; + } + cy.wrap($row).children('.slick-cell') + .first() + .should('contain', grid2Tasks[index]); + }); + }); + }); + + it('should retain sorting in 1st grid', () => { + cy.get('#grid1-1') + .find('.slick-sort-indicator-desc') + .should('have.length', 1); + }); + + it('should have Pagination displayed and set on Grid2', () => { + cy.get('[data-test=page-number-input]') + .invoke('val') + .then(pageNumber => expect(pageNumber).to.eq('1')); + + cy.get('[data-test=page-count]') + .contains('199'); + + cy.get('[data-test=item-from]') + .contains('1'); + + cy.get('[data-test=item-to]') + .contains('5'); + + cy.get('[data-test=total-items]') + .contains('995'); + }); + + it('should change Page Number 52 and expect the Pagination to have correct values', () => { + cy.get('[data-test=page-number-input]') + .clear() + .type('52') + .type('{enter}'); + + cy.get('[data-test=page-count]') + .contains('199'); + + cy.get('[data-test=item-from]') + .contains('256'); + + cy.get('[data-test=item-to]') + .contains('260'); + + cy.get('[data-test=total-items]') + .contains('995'); + }); + + it('should open the Grid Menu on 1st Grid and expect all Columns to be checked', () => { + let gridUid = ''; + cy.get('#grid1-1') + .find('button.slick-grid-menu-button') + .click({ force: true }); + + cy.get('#grid1-1') + .should(($grid) => { + const classes = $grid.prop('className').split(' '); + gridUid = classes.find(className => /slickgrid_.*/.test(className)); + expect(gridUid).to.not.be.null; + }) + .then(() => { + cy.get(`.slick-grid-menu.${gridUid}`) + .find('.slick-column-picker-list') + .children('li') + .each(($child, index) => { + if (index <= 5) { + const $input = $child.find('input'); + const $label = $child.find('span.checkbox-label'); + expect($input.prop('checked')).to.eq(true); + expect($label.text()).to.eq(fullTitles[index]); + } + }); + }); + }); + + it('should then hide "Title" column from same 1st Grid and expect the column to be removed from 1st Grid', () => { + const newColumnList = ['Duration (days)', '% Complete', 'Start', 'Finish', 'Effort Driven']; + cy.get('#grid1-1') + .get('.slick-grid-menu:visible') + .find('.slick-column-picker-list') + .children('li:visible:nth(0)') + .children('label') + .should('contain', 'Title') + .click({ force: true }); + + cy.get('#grid1-1') + .get('.slick-grid-menu:visible') + .find('.close') + .click({ force: true }); + + cy.get('#grid1-1') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(newColumnList[index])); + }); + + it('should open the Grid Menu off 2nd Grid and expect all Columns to still be all checked', () => { + let gridUid = ''; + cy.get('#grid1-2') + .find('button.slick-grid-menu-button') + .click({ force: true }); + + cy.get('#grid1-2') + .should(($grid) => { + const classes = $grid.prop('className').split(' '); + gridUid = classes.find(className => /slickgrid_.*/.test(className)); + expect(gridUid).to.not.be.null; + }) + .then(() => { + cy.get(`.slick-grid-menu.${gridUid}`) + .find('.slick-column-picker-list') + .children('li') + .each(($child, index) => { + if (index <= 5) { + const $input = $child.find('input'); + const $label = $child.find('span.checkbox-label'); + expect($input.prop('checked')).to.eq(true); + expect($label.text()).to.eq(fullTitles[index]); + } + }); + }); + }); + + it('should then hide "% Complete" column from this same 2nd Grid and expect the column to be removed from 2nd Grid', () => { + const newColumnList = ['Title', 'Duration (days)', 'Start', 'Finish', 'Effort Driven']; + cy.get('#grid1-2') + .get('.slick-grid-menu:visible') + .find('.slick-column-picker-list') + .children('li:visible:nth(2)') + .children('label') + .should('contain', '% Complete') + .click({ force: true }); + + cy.get('#grid1-2') + .get('.slick-grid-menu:visible') + .find('.close') + .click({ force: true }); + + cy.get('#grid1-2') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(newColumnList[index])); + }); + + it('should go back to 1st Grid and open its Grid Menu and we expect this grid to stil have the "Title" column be hidden (unchecked)', () => { + cy.get('#grid1-1') + .find('button.slick-grid-menu-button') + .click({ force: true }); + + cy.get('.slick-column-picker-list') + .children('li') + .each(($child, index) => { + if (index <= 5) { + const $input = $child.find('input'); + const $label = $child.find('span.checkbox-label'); + if ($label.text() === 'Title') { + expect($input.attr('checked')).to.eq(undefined); + } else { + expect($input.prop('checked')).to.eq(true); + } + expect($label.text()).to.eq(fullTitles[index]); + } + }); + }); + + it('should hide "Start" column from 1st Grid and expect to have 2 hidden columns (Title, Start)', () => { + const newColumnList = ['Duration (days)', '% Complete', 'Finish', 'Effort Driven']; + cy.get('#grid1-1') + .get('.slick-grid-menu:visible') + .find('.slick-column-picker-list') + .children('li:visible:nth(3)') + .children('label') + .should('contain', 'Start') + .click({ force: true }); + + cy.get('#grid1-1') + .get('.slick-grid-menu:visible') + .find('.close') + .click({ force: true }); + + cy.get('#grid1-1') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(newColumnList[index])); + }); + + it('should open Column Picker of 2nd Grid and show the "% Complete" column back to visible', () => { + cy.get('#grid1-2') + .find('.slick-header-column') + .first() + .trigger('mouseover') + .trigger('contextmenu') + .invoke('show'); + + cy.get('.slick-column-picker') + .find('.slick-column-picker-list') + .children() + .each(($child, index) => { + if (index <= 5) { + expect($child.text()).to.eq(fullTitles[index]); + } + }); + + cy.get('.slick-column-picker') + .find('.slick-column-picker-list') + .children('li:nth-child(3)') + .children('label') + .should('contain', '% Complete') + .click(); + + cy.get('#grid1-2') + .find('.slick-header-columns') + .children() + .each(($child, index) => { + if (index <= 5) { + expect($child.text()).to.eq(fullTitles[index]); + } + }); + + cy.get('#grid1-2') + .get('.slick-column-picker:visible') + .find('.close') + .trigger('click') + .click(); + }); + + it('should open the Grid Menu on 2nd Grid and expect all Columns to be checked', () => { + let gridUid = ''; + cy.get('#grid1-2') + .find('button.slick-grid-menu-button') + .click({ force: true }); + + cy.get('#grid1-2') + .should(($grid) => { + const classes = $grid.prop('className').split(' '); + gridUid = classes.find(className => /slickgrid_.*/.test(className)); + expect(gridUid).to.not.be.null; + }) + .then(() => { + cy.get(`.slick-grid-menu.${gridUid}`) + .find('.slick-column-picker-list') + .children('li') + .each(($child, index) => { + if (index <= 5) { + const $input = $child.find('input'); + const $label = $child.find('span.checkbox-label'); + expect($input.prop('checked')).to.eq(true); + expect($label.text()).to.eq(fullTitles[index]); + } + }); + }); + }); + + it('should still expect 1st Grid to be unchanged from previous state and still have only 4 columns shown', () => { + const newColumnList = ['Duration (days)', '% Complete', 'Finish', 'Effort Driven']; + + cy.get('#grid1-1') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(newColumnList[index])); + }); + + it('should open the Grid Menu on 1st Grid and also expect to only have 4 columns checked (visible)', () => { + let gridUid = ''; + cy.get('#grid1-1') + .find('button.slick-grid-menu-button') + .click({ force: true }); + + cy.get('#grid1-1') + .should(($grid) => { + const classes = $grid.prop('className').split(' '); + gridUid = classes.find(className => /slickgrid_.*/.test(className)); + expect(gridUid).to.not.be.null; + }) + .then(() => { + cy.get(`.slick-grid-menu.${gridUid}`) + .find('.slick-column-picker-list') + .children('li') + .each(($child, index) => { + if (index <= 5) { + const $input = $child.find('input'); + const $label = $child.find('span.checkbox-label'); + if ($label.text() === 'Title' || $label.text() === 'Start') { + expect($input.attr('checked')).to.eq(undefined); + } else { + expect($input.prop('checked')).to.eq(true); + } + expect($label.text()).to.eq(fullTitles[index]); + } + }); + }); + + cy.get('#grid1-1') + .get('.slick-grid-menu:visible') + .find('.close') + .click({ force: true }); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example02.cy.ts b/demos/vue/test/cypress/e2e/example02.cy.ts new file mode 100644 index 000000000..654f3579f --- /dev/null +++ b/demos/vue/test/cypress/e2e/example02.cy.ts @@ -0,0 +1,25 @@ +import { removeExtraSpaces } from '../plugins/utilities'; + +describe('Example 2 - Grid with Formatters', () => { + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example2`); + cy.get('h2').should('contain', 'Example 2: Grid with Formatters'); + }); + + it('should show a custom text in the grid footer left portion', () => { + cy.get('#slickGridContainer-grid2') + .find('.slick-custom-footer') + .find('.left-footer') + .contains('custom footer text'); + }); + + it('should have some metrics shown in the grid footer', () => { + cy.get('#slickGridContainer-grid2') + .find('.slick-custom-footer') + .find('.right-footer') + .should($span => { + const text = removeExtraSpaces($span.text()); // remove all white spaces + expect(text).to.eq('500 items'); + }); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example03.cy.ts b/demos/vue/test/cypress/e2e/example03.cy.ts new file mode 100644 index 000000000..98831fa14 --- /dev/null +++ b/demos/vue/test/cypress/e2e/example03.cy.ts @@ -0,0 +1,312 @@ +describe('Example 3 - Grid with Editors', () => { + const GRID_ROW_HEIGHT = 35; + const fullTitles = [ + '', + '', + 'Title', + 'Title, Custom Editor', + 'Duration (days)', + '% Complete', + 'Start', + 'Finish', + 'City of Origin', + 'Country of Origin', + 'Country of Origin Name', + 'Effort Driven', + 'Prerequisites', + ]; + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example3`); + cy.get('h2').should('contain', 'Example 3: Editors / Delete'); + }); + + it('should have exact Column Titles in the grid', () => { + cy.get('#grid3') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + }); + + it('should be able to change Title with Custom Editor and expect to save when changing the value and then mouse clicking on a different cell', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3)`) + .should('contain', 'Task 1') + .click(); + cy.get('input.editor-text').type('Task 8888'); + + // mouse click on next cell on the right & expect a save + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(4)`).click(); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3)`).should('contain', 'Task 8888'); + }); + + it('should be able to undo the editor and expect it to be opened, then clicking on Escape should reveal the cell to have rolled back text of "Task 1"', () => { + cy.get('[data-test="undo-btn"]').click(); + + cy.get('input.editor-text').should('exist').type('{esc}'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3)`).should('contain', 'Task 1'); + }); + + it('should enable "Auto Commit Edit"', () => { + cy.get('[data-test=auto-commit]').click(); + }); + + it('should be able to change all values of 3rd row', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(2)`) + .should('contain', 'Task 2') + .click(); + + // change Title & Custom Title + cy.get('.editor-title > textarea').type('Task 2222'); + cy.get('.editor-title .btn-save').click(); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(2)`).should('contain', 'Task 2222'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(3)`).should('contain', 'Task 2222'); + + // change duration + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(4)`).click(); + cy.get('.slider-editor input[type=range]').as('range').invoke('val', 25).trigger('change', { force: true }); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(4)`).should('contain', '25'); + + // change % Complete + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(5)`).click(); + cy.get('[data-name=editor-complete].ms-drop > ul > li > label:nth(5)').contains('95').click(); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(5)`).find( + '.percent-complete-bar[style="background: green; width: 95%;"]' + ); + + // change Finish date + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(6)`).click(); + cy.get('.vanilla-calendar-month:visible').click(); + cy.get('.vanilla-calendar-months__month').contains('Jan').click(); + cy.get('.vanilla-calendar-year').click(); + cy.get('.vanilla-calendar-years__year').contains('2009').click(); + cy.get('.vanilla-calendar-day__btn').contains('22').click(); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(6)`).should('contain', '2009-01-22'); + + // change City of Origin + // cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(8)`).click({ force: true }); + // cy.get('input.autocomplete.editor-cityOfOrigin.ui-autocomplete-input') + // .type('Venice'); + + // change Effort Driven + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(11)`).click({ force: true }); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(11) > input.editor-checkbox.editor-effort-driven`).check(); + + cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('top'); + }); + + it('should dynamically add 2x new "Title" columns', () => { + const updatedTitles = [ + '', + '', + 'Title', + 'Title, Custom Editor', + 'Duration (days)', + '% Complete', + 'Start', + 'Finish', + 'City of Origin', + 'Country of Origin', + 'Country of Origin Name', + 'Effort Driven', + 'Prerequisites', + 'Title', + 'Title', + ]; + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', 'Task 0'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(13)`).should('not.exist'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(14)`).should('not.exist'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`) + .should('contain', 'Task 0') + .should('have.length', 1); + + cy.get('#grid3') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(updatedTitles[index])); + + cy.get('[data-test=add-title-column]').click().click(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', 'Task 0'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).should('contain', 'Task 0'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(13)`).should('contain', 'Task 0'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(14)`).should('contain', 'Task 0'); + }); + + it('should be able to change value of 1st row "Title" column and expect same value set in all 3 "Title" columns', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`) + .should('contain', 'Task 0') + .click(); + + // change Title & Custom Title + cy.get('.editor-title > textarea').type('Task 0000'); + cy.get('.editor-title .btn-save').click(); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', 'Task 0000'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).should('contain', 'Task 0000'); + + // change duration + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(4)`).click(); + cy.get('.slider-editor input[type=range]').as('range').invoke('val', 50).trigger('change', { force: true }); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(4)`).should('contain', '50'); + + // change % Complete + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5)`).click(); + cy.get('[data-name=editor-complete].ms-drop > ul > li > label:nth(5)').contains('95').click(); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5)`).find( + '.percent-complete-bar[style="background: green; width: 95%;"]' + ); + + // change Finish date + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(6)`).click(); + cy.get('.vanilla-calendar-month:visible').click(); + cy.get('.vanilla-calendar-months__month').contains('Jan').click(); + cy.get('.vanilla-calendar-year').click(); + cy.get('.vanilla-calendar-years__year').contains('2009').click(); + cy.get('.vanilla-calendar-day__btn').contains('22').click(); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(6)`).should('contain', '2009-01-22'); + + // change Effort Driven + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(11)`).click(); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(11) > input.editor-checkbox.editor-effort-driven`) + .check() + .blur(); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(10)`).click(); // the blur seems to not always work, so just click on another cell + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(11)`).find('.mdi-check.checkmark-icon'); + cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left').scrollTo('top'); + }); + + it('should be able to filter and search "Task 2222" in the new column and expect only 1 row showing in the grid', () => { + cy.get('input.search-filter.filter-title1').type('Task 2222', { force: true }).should('have.value', 'Task 2222'); + + cy.get('.slick-row').should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', 'Task 2222'); + }); + + it('should hover over the last "Title" column and click on "Clear Filter" and expect grid to have all rows shown', () => { + cy.get('.slick-header-column:nth-child(14)').first().trigger('mouseover').children('.slick-header-menu-button').invoke('show').click(); + + cy.get('.slick-header-menu .slick-menu-command-list') + .should('be.visible') + .children('.slick-menu-item:nth-of-type(6)') + .children('.slick-menu-content') + .should('contain', 'Remove Filter') + .click(); + + cy.get('.slick-row').should('have.length.greaterThan', 1); + }); + + it('should be able to dynamically remove last 2 added Title columns', () => { + cy.get('[data-test=remove-title-column]').click().click(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', 'Task 0'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(13)`).should('not.exist'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(14)`).should('not.exist'); + }); + + it('should be able to change values again of 1st row "Title" column and expect same value set in all 3 "Title" columns', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`) + .should('contain', 'Task 0') + .click(); + + // change Title & Custom Title + cy.get('.editor-title > textarea').type('Task 0000'); + cy.get('.editor-title .btn-save').click(); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', 'Task 0000'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).should('contain', 'Task 0000'); + + // change duration + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(4)`).click(); + cy.get('.slider-editor input[type=range]').as('range').invoke('val', 50).trigger('change', { force: true }); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(4)`).should('contain', '50'); + + // change % Complete + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5)`).click(); + cy.get('[data-name=editor-complete].ms-drop > ul > li > label:nth(3)').contains('97').click(); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5)`).find( + '.percent-complete-bar[style="background: green; width: 97%;"]' + ); + + // change Finish date + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(6)`).click(); + cy.get('.vanilla-calendar-day__btn').contains('21').click(); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(6)`).should('contain', '2009-01-21'); + + // // change Effort Driven + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(11)`).click(); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(11) > input.editor-checkbox.editor-effort-driven`).uncheck(); + }); + + it('should click Add Item button 2x times and expect "Task 100" and "Task 101" to be created', () => { + cy.get('[data-test="add-item-btn"]').click(); + cy.wait(200); + cy.get('[data-test="add-item-btn"]').click(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', 'Task 101'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2)`).should('contain', 'Task 100'); + + // cy.get('[data-test="toggle-filtering-btn"]').click(); // show it back + }); + + it('should open the "Prerequisites" Filter and expect to have Task 500 & 101 in the Filter', () => { + cy.get('div.ms-filter.filter-prerequisites').trigger('click'); + + cy.get('.ms-drop').find('span:nth(1)').contains('Task 101'); + + cy.get('.ms-drop').find('span:nth(2)').contains('Task 100'); + + cy.get('div.ms-filter.filter-prerequisites').trigger('click'); + }); + + it('should open the "Prerequisites" Editor and expect to have Task 100 & 101 in the Editor', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(12)`) + .should('contain', '') + .click(); + + cy.get('.ms-drop').find('span:nth(1)').contains('Task 101'); + + cy.get('.ms-drop').find('span:nth(2)').contains('Task 100'); + + cy.get('[data-name=editor-prerequisites].ms-drop ul > li:nth(0)').click(); + + cy.get('.ms-ok-button').last().click({ force: true }); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(12)`).should('contain', 'Task 101'); + }); + + it('should delete the last item "Task 101" and expect it to be removed from the Filter', () => { + cy.get('[data-test="delete-item-btn"]').click(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', 'Task 100'); + + cy.get('div.ms-filter.filter-prerequisites').trigger('click'); + + cy.get('.ms-drop').find('span:nth(1)').contains('Task 100'); + + cy.get('div.ms-filter.filter-prerequisites').trigger('click'); + }); + + it('should open the "Prerequisites" Filter then choose "Task 3", "Task 4" and "Task 8" from the list and expect to see 2 rows of data in the grid', () => { + cy.get('div.ms-filter.filter-prerequisites').trigger('click'); + + cy.get('.ms-drop') + .contains(/^Task 3$/) // use regexp to avoid finding first Task 3 which is in fact Task 399 + .click(); + + cy.get('.ms-drop') + .contains(/^Task 4$/) + .click(); + + cy.get('.ms-drop') + .contains(/^Task 8$/) + .click(); + + cy.get('.ms-ok-button').click(); + + cy.get('.slick-row').should('have.length', 2); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', 'Task 4'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2)`).should('contain', 'Task 8'); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example04.cy.ts b/demos/vue/test/cypress/e2e/example04.cy.ts new file mode 100644 index 000000000..7f5be80e6 --- /dev/null +++ b/demos/vue/test/cypress/e2e/example04.cy.ts @@ -0,0 +1,280 @@ +import { isAfter, isBefore, isEqual, parse } from '@formkit/tempo'; + +import { removeExtraSpaces } from '../plugins/utilities'; + +describe('Example 4 - Client Side Sort/Filter Grid', () => { + beforeEach(() => { + // create a console.log spy for later use + cy.window().then((win) => { + cy.spy(win.console, 'log'); + }); + }); + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example4`); + cy.get('h2').should('contain', 'Example 4: Client Side Sort/Filter'); + }); + + describe('Load Grid with Presets', () => { + const presetDurationValues = [98, 10]; + const presetUsDateShort = '4/20/25'; + + it('should have "Duration" fields within the inclusive range of the preset filters and be displayed in the Filter itself', () => { + cy.get('.ms-filter.search-filter.filter-duration.filled') + .find('.ms-choice') + .contains('98, 10'); + + cy.get('#grid4') + .find('.slick-row') + .each(($row) => { + cy.wrap($row) + .children('.slick-cell:nth(2)') + .each(($cell) => { + const value = parseInt($cell.text().trim(), 10); + if (!isNaN(value)) { + const foundItems = presetDurationValues.filter(acceptedValue => acceptedValue === value); + expect(foundItems).to.have.length(1); + } + }); + }); + }); + + it('should have US Date Short within the range of the preset filters', () => { + cy.get('.search-filter.filter-usDateShort') + .find('input') + .invoke('val') + .then(text => expect(text).to.eq(presetUsDateShort)); + + cy.get('#grid4') + .find('.slick-row') + .each(($row) => { + cy.wrap($row) + .children('.slick-cell:nth(5)') + .each(($cell) => { + const isDateValid = isBefore(parse($cell.text(), 'M/D/YY'), parse(presetUsDateShort, 'M/D/YY')); + expect(isDateValid).to.eq(true); + }); + }); + }); + + it('should have some metrics shown in the grid footer well below 10500 items', () => { + cy.get('#slickGridContainer-grid4') + .find('.slick-custom-footer') + .find('.right-footer') + .should($span => { + const text = removeExtraSpaces($span.text()); // remove all white spaces + expect(text).not.to.eq('10500 of 10500 items'); + }); + }); + + it('should expect the grid to be sorted by "Duration" descending and "% Complete" ascending', () => { + cy.get('#grid4') + .get('.slick-header-column:nth(2)') + .find('.slick-sort-indicator-desc') + .should('have.length', 1) + .siblings('.slick-sort-indicator-numbered') + .contains('1'); + + cy.get('#grid4') + .get('.slick-header-column:nth(3)') + .find('.slick-sort-indicator-asc') + .should('have.length', 1) + .siblings('.slick-sort-indicator-numbered') + .contains('2'); + + cy.get('.slick-row') + .first() + .children('.slick-cell:nth(2)') + .should('contain', '98'); + + cy.get('[data-test="scroll-bottom-btn"') + .click(); + + cy.get('.slick-row') + .last() + .children('.slick-cell:nth(2)') + .should('contain', '10'); + }); + }); + + describe('Set Dymamic Filters', () => { + const dynamicDurationValues = [2, 25, 48, 50]; + const dynamicMaxComplete = 95; + const dynamicStartDate = '2001-02-28'; + + it('should click on Set Dynamic Filters', () => { + cy.get('[data-test=set-dynamic-filter]') + .click(); + }); + + it('should have "% Complete" fields within the exclusive range of the filters presets', () => { + cy.get('#grid4') + .find('.slick-row') + .each(($row) => { + cy.wrap($row) + .children('.slick-cell:nth(3)') + .each(($cell) => { + const value = parseInt($cell.text().trim(), 10); + if (!isNaN(value)) { + expect(value < dynamicMaxComplete).to.eq(true); + } + }); + }); + }); + + it('should have "Duration" fields within the inclusive range of the dynamic filters', () => { + cy.get('#grid4') + .find('.slick-row') + .each(($row) => { + cy.wrap($row) + .children('.slick-cell:nth(2)') + .each(($cell) => { + const value = parseInt($cell.text().trim(), 10); + if (!isNaN(value)) { + const foundItems = dynamicDurationValues.filter(acceptedValue => acceptedValue === value); + expect(foundItems).to.have.length(1); + } + }); + }); + }); + + it('should have Start Date within the range of the dynamic filters', () => { + cy.get('.search-filter.filter-start') + .find('input') + .invoke('val') + .then(text => expect(text).to.eq(dynamicStartDate)); + + cy.get('#grid4') + .find('.slick-row') + .each(($row) => { + cy.wrap($row) + .children('.slick-cell:nth(4)') + .each(($cell) => { + const isDateValid = isEqual(parse($cell.text()), dynamicStartDate) || isAfter(parse($cell.text()), dynamicStartDate); + expect(isDateValid).to.eq(true); + }); + }); + }); + }); + + describe('Set Dynamic Sorting', () => { + it('should click on "Clear Filters" then "Set Dynamic Sorting" buttons', () => { + cy.get('[data-test=clear-filters]') + .click(); + + cy.get('[data-test=set-dynamic-sorting]') + .click(); + }); + + it('should have some metrics shown in the grid footer', () => { + cy.get('#slickGridContainer-grid4') + .find('.slick-custom-footer') + .find('.right-footer') + .should($span => { + const text = removeExtraSpaces($span.text()); // remove all white spaces + expect(text).to.eq('10500 of 10500 items'); + }); + }); + + it('should expect the grid to be sorted by "Duration" ascending and "Start" descending', () => { + cy.get('#grid4') + .get('.slick-header-column:nth(2)') + .find('.slick-sort-indicator-asc') + .should('have.length', 1) + .siblings('.slick-sort-indicator-numbered') + .contains('1'); + + cy.get('#grid4') + .get('.slick-header-column:nth(4)') + .find('.slick-sort-indicator-desc') + .should('have.length', 1) + .siblings('.slick-sort-indicator-numbered') + .contains('2'); + + cy.get('.slick-row') + .first() + .children('.slick-cell:nth(2)') + .should('contain', '0'); + + cy.get('[data-test="scroll-bottom-btn"') + .click(); + + cy.get('.slick-row') + .last() + .children('.slick-cell:nth(2)') + .should('contain', '100'); + + cy.get('.slick-row') + .last() + .children('.slick-cell:nth(4)') + .should('contain', ''); + }); + }); + + describe('Grid State Changes', () => { + const dynamicStartDate = '2001-02-28'; + + it('should click on Set Dynamic Filters', () => { + cy.get('[data-test=set-dynamic-filter]') + .click(); + }); + + it('should have Start Date within the range of the dynamic filters', () => { + cy.get('.search-filter.filter-start') + .find('input') + .invoke('val') + .then(text => expect(text).to.eq(dynamicStartDate)); + + cy.get('#grid4') + .find('.slick-row') + .each(($row) => { + cy.wrap($row) + .children('.slick-cell:nth(4)') + .each(($cell) => { + const isDateValid = isEqual(parse($cell.text()), dynamicStartDate) || isAfter(parse($cell.text()), dynamicStartDate); + expect(isDateValid).to.eq(true); + }); + }); + }); + + it('should focus on Start filter, then type Backspace and expect Start filter to no longer exists in the list of Filters in Grid State change', () => { + cy.get('.search-filter.filter-start').click(); + cy.wait(20); + cy.get('.search-filter.filter-start input.date-picker').type('{backspace}', { force: true }); + + cy.get('.search-filter.filter-start') + .find('input') + .invoke('val') + .then(text => expect(text).to.eq('')); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(1); + expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { + newValues: [ + { columnId: 'duration', searchTerms: ['2', '25', '48', '50'], operator: 'IN' }, + { columnId: 'complete', searchTerms: ['95'], operator: '<' }, + { columnId: 'effort-driven', searchTerms: ['true'], operator: 'EQ' } + ], type: 'filter' + }); + }); + }); + + it('should click on DOM body and reopen Start filter date picker and still expect it to be empty', () => { + cy.get('h2').click(); // just to simulate clicking outside of the date picker + + cy.get('.search-filter.filter-start') + .find('input') + .invoke('val') + .then(text => expect(text).to.eq('')); + + cy.get('.search-filter.filter-start') + .click(); + + cy.get('.vanilla-calendar:visible') + .find('.vanilla-calendar-day__btn_selected') + .should('not.exist'); + + cy.get('h2').click(); + }); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example05.cy.ts b/demos/vue/test/cypress/e2e/example05.cy.ts new file mode 100644 index 000000000..493ea7617 --- /dev/null +++ b/demos/vue/test/cypress/e2e/example05.cy.ts @@ -0,0 +1,805 @@ +describe('Example 5 - OData Grid', () => { + const GRID_ROW_HEIGHT = 35; + + beforeEach(() => { + // create a console.log spy for later use + cy.window().then((win) => { + cy.spy(win.console, 'log'); + }); + }); + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example5`); + cy.get('h2').should('contain', 'Example 5: Grid with Backend OData Service'); + }); + + describe('when "enableCount" is set', () => { + it('should have default OData query', () => { + cy.get('[data-test=alert-odata-query]').should('exist'); + cy.get('[data-test=alert-odata-query]').should('contain', 'OData Query'); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$inlinecount=allpages&$top=20&$skip=20&$orderby=Name asc&$filter=(Gender eq 'male')`); + }); + }); + + it('should change Pagination to next page', () => { + cy.get('.icon-seek-next').click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=page-number-input]') + .invoke('val') + .then((pageNumber) => expect(pageNumber).to.eq('3')); + + cy.get('[data-test=page-count]').contains('3'); + + cy.get('[data-test=item-from]').contains('41'); + + cy.get('[data-test=item-to]').contains('50'); + + cy.get('[data-test=total-items]').contains('50'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$inlinecount=allpages&$top=20&$skip=40&$orderby=Name asc&$filter=(Gender eq 'male')`); + }); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(1); + expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { + newValues: { pageNumber: 3, pageSize: 20 }, + type: 'pagination', + }); + }); + }); + + it('should change Pagination to first page with 10 items', () => { + cy.get('#items-per-page-label').select('10'); + + // wait for the query to start and finish + cy.get('[data-test=status]').should('contain', 'loading'); + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=page-number-input]') + .invoke('val') + .then((pageNumber) => expect(pageNumber).to.eq('1')); + + cy.get('[data-test=page-count]').contains('5'); + + cy.get('[data-test=item-from]').contains('1'); + + cy.get('[data-test=item-to]').contains('10'); + + cy.get('[data-test=total-items]').contains('50'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$inlinecount=allpages&$top=10&$orderby=Name asc&$filter=(Gender eq 'male')`); + }); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(1); + expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { + newValues: { pageNumber: 1, pageSize: 10 }, + type: 'pagination', + }); + }); + }); + + it('should change Pagination to last page', () => { + cy.get('.icon-seek-end').click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=page-number-input]') + .invoke('val') + .then((pageNumber) => expect(pageNumber).to.eq('5')); + + cy.get('[data-test=page-count]').contains('5'); + + cy.get('[data-test=item-from]').contains('41'); + + cy.get('[data-test=item-to]').contains('50'); + + cy.get('[data-test=total-items]').contains('50'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$inlinecount=allpages&$top=10&$skip=40&$orderby=Name asc&$filter=(Gender eq 'male')`); + }); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(1); + expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { + newValues: { pageNumber: 5, pageSize: 10 }, + type: 'pagination', + }); + }); + }); + + it('should change Pagination to first page using the external button', () => { + cy.get('[data-test=goto-first-page').click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=page-number-input]') + .invoke('val') + .then((pageNumber) => expect(pageNumber).to.eq('1')); + + cy.get('[data-test=page-count]').contains('5'); + + cy.get('[data-test=item-from]').contains('1'); + + cy.get('[data-test=item-to]').contains('10'); + + cy.get('[data-test=total-items]').contains('50'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$inlinecount=allpages&$top=10&$orderby=Name asc&$filter=(Gender eq 'male')`); + }); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(1); + expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { + newValues: { pageNumber: 1, pageSize: 10 }, + type: 'pagination', + }); + }); + }); + + it('should change Pagination to last page using the external button', () => { + cy.get('[data-test=goto-last-page').click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=page-number-input]') + .invoke('val') + .then((pageNumber) => expect(pageNumber).to.eq('5')); + + cy.get('[data-test=page-count]').contains('5'); + + cy.get('[data-test=item-from]').contains('41'); + + cy.get('[data-test=item-to]').contains('50'); + + cy.get('[data-test=total-items]').contains('50'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$inlinecount=allpages&$top=10&$skip=40&$orderby=Name asc&$filter=(Gender eq 'male')`); + }); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(1); + expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { + newValues: { pageNumber: 5, pageSize: 10 }, + type: 'pagination', + }); + }); + }); + + it('should Clear all Filters and expect to go back to first page', () => { + cy.get('#grid5').find('button.slick-grid-menu-button').click({ force: true }); + + cy.get(`.slick-grid-menu:visible`).find('.slick-menu-item').first().find('span').contains('Clear all Filters').click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=page-number-input]') + .invoke('val') + .then((pageNumber) => expect(pageNumber).to.eq('1')); + + cy.get('[data-test=page-count]').contains('10'); + + cy.get('[data-test=item-from]').contains('1'); + + cy.get('[data-test=item-to]').contains('10'); + + cy.get('[data-test=total-items]').contains('100'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$inlinecount=allpages&$top=10&$orderby=Name asc`); + }); + + cy.window().then((win) => { + // TODO look into, this should be called 2x times not 3x times + // expect(win.console.log).to.have.callCount(2); + expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { newValues: [], type: 'filter' }); + expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { + newValues: { pageNumber: 1, pageSize: 10 }, + type: 'pagination', + }); + }); + }); + + it('should Clear all Sorting', () => { + cy.get('#grid5').find('button.slick-grid-menu-button').click({ force: true }); + + cy.get(`.slick-grid-menu:visible`).find('.slick-menu-item:nth(1)').find('span').contains('Clear all Sorting').click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$inlinecount=allpages&$top=10`); + }); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(1); + expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { newValues: [], type: 'sorter' }); + }); + }); + + it('should use "substringof" when OData version is set to 2', () => { + cy.get('.search-filter.filter-name').find('input').type('John'); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$inlinecount=allpages&$top=10&$filter=(substringof('John', Name))`); + }); + + cy.get('#grid5').find('.slick-row').should('have.length', 1); + }); + + it('should use "contains" when OData version is set to 4', () => { + cy.get('[data-test=version4]').click(); + + cy.get('.search-filter.filter-name').find('input').type('John'); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$count=true&$top=10&$filter=(contains(Name, 'John'))`); + }); + + cy.get('#grid5').find('.slick-row').should('have.length', 1); + }); + + it('should return 3 rows using "C*n" (starts with "C" + ends with "n")', () => { + cy.get('input.filter-name').clear().type('C*n'); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$count=true&$top=10&$filter=(startswith(Name, 'C') and endswith(Name, 'n'))`); + }); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Carroll Buchanan'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Consuelo Dickson'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Christine Compton'); + }); + + it('should perform filterQueryOverride when operator "%%" is selected', () => { + cy.get('.search-filter.filter-name select') + .find('option') + .last() + .then((element) => { + cy.get('.search-filter.filter-name select').select(element.val()); + }); + + cy.get('.search-filter.filter-name').find('input').clear().type('Jo%yn%er'); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$count=true&$top=10&$filter=(matchesPattern(Name, '%5EJo%25yn%25er$'))`); + }); + + cy.get('.slick-row').should('have.length', 1); + }); + + it('should click on Set Dynamic Filter and expect query and filters to be changed', () => { + cy.get('[data-test=set-dynamic-filter]').click(); + + cy.get('.search-filter.filter-name select').should('have.value', 'a*'); + + cy.get('.search-filter.filter-name') + .find('input') + .invoke('val') + .then((text) => expect(text).to.eq('A')); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$count=true&$top=10&$filter=(startswith(Name, 'A'))`); + }); + + cy.get('#grid5').find('.slick-row').should('have.length', 5); + }); + + it('should use a range filter when searching with ".."', () => { + cy.get('.slick-header-columns').children('.slick-header-column:nth(1)').contains('Name').click(); + + cy.get('.search-filter.filter-name').find('input').clear().type('Anthony Joyner..Ayers Hood'); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$count=true&$top=10&$orderby=Name asc&$filter=(Name ge 'Anthony%20Joyner' and Name le 'Ayers%20Hood')`); + }); + + cy.get('#grid5').find('.slick-row').should('have.length', 3); + }); + }); + + describe('when "enableCount" is unchecked (not set)', () => { + it('should Clear all Filters and Sortings, set 20 items per page & uncheck "enableCount"', () => { + cy.get('#grid5').find('button.slick-grid-menu-button').click({ force: true }); + + cy.get(`.slick-grid-menu:visible`).find('.slick-menu-item').first().find('span').contains('Clear all Filters').click(); + + cy.get('#grid5').find('button.slick-grid-menu-button').trigger('click').click({ force: true }); + + cy.get(`.slick-grid-menu:visible`).find('.slick-menu-item:nth(1)').find('span').contains('Clear all Sorting').click(); + + cy.get('#items-per-page-label').select('20'); + + cy.get('[data-test=enable-count]').click(); + cy.get('[data-test=enable-count]').should('not.be.checked'); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$top=20`); + }); + }); + + it('should change Pagination to next page', () => { + cy.get('.icon-seek-next').click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$top=20&$skip=20`); + }); + }); + + it('should change Pagination to first page with 10 items', () => { + cy.get('#items-per-page-label').select('10'); + + // wait for the query to start and finish + cy.get('[data-test=status]').should('contain', 'loading'); + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$top=10`); + }); + }); + + it('should change Pagination to last page', () => { + cy.get('.icon-seek-end').click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$top=10&$skip=90`); + }); + }); + + it('should click on "Name" column to sort it Ascending', () => { + cy.get('.slick-header-columns').children('.slick-header-column:nth(1)').click(); + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(1)') + .find('.slick-sort-indicator.slick-sort-indicator-asc') + .should('be.visible'); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$top=10&$skip=90&$orderby=Name asc`); + }); + }); + + it('should Clear all Sorting', () => { + cy.get('#grid5').find('button.slick-grid-menu-button').click({ force: true }); + + cy.get(`.slick-grid-menu:visible`).find('.slick-menu-item:nth(1)').find('span').contains('Clear all Sorting').click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$top=10&$skip=90`); + }); + }); + + it('should click on Set Dynamic Filter and expect query and filters to be changed', () => { + cy.get('[data-test=set-dynamic-filter]').click(); + + cy.get('.search-filter.filter-name select').should('have.value', 'a*'); + + cy.get('.search-filter.filter-name') + .find('input') + .invoke('val') + .then((text) => expect(text).to.eq('A')); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$top=10&$filter=(startswith(Name, 'A'))`); + }); + + cy.get('#grid5').find('.slick-row').should('have.length', 5); + }); + + it('should use "substringof" when OData version is set to 2', () => { + cy.get('[data-test=version2]').click(); + + cy.get('.search-filter.filter-name').find('input').type('John'); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$top=10&$filter=(substringof('John', Name))`); + }); + + cy.get('#grid5').find('.slick-row').should('have.length', 1); + }); + + it('should use "contains" when OData version is set to 4', () => { + cy.get('[data-test=version4]').click(); + + cy.get('.search-filter.filter-name').find('input').type('John'); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$top=10&$filter=(contains(Name, 'John'))`); + }); + + cy.get('#grid5').find('.slick-row').should('have.length', 1); + }); + }); + + describe('General Pagination Behaviors', () => { + it('should type a filter which returns an empty dataset', () => { + cy.get('.search-filter.filter-name').find('input').clear().type('xy'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$top=10&$filter=(contains(Name, 'xy'))`); + }); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('.slick-empty-data-warning:visible').contains('No data to display.'); + }); + + it('should display page 0 of 0 but hide pagination from/to numbers when filtered data "xy" returns an empty dataset', () => { + cy.get('[data-test=page-count]').contains('0'); + + cy.get('[data-test=item-from]').should('not.be.visible'); + + cy.get('[data-test=item-to]').should('not.be.visible'); + + cy.get('[data-test=total-items]').contains('0'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$top=10&$filter=(contains(Name, 'xy'))`); + }); + + cy.get('[data-test=page-number-input]') + .invoke('val') + .then((pageNumber) => expect(pageNumber).to.eq('0')); + }); + + it('should erase part of the filter so that it filters with "x"', () => { + cy.get('.search-filter.filter-name').find('input').type('{backspace}'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$top=10&$filter=(contains(Name, 'x'))`); + }); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('.slick-empty-data-warning').contains('No data to display.').should('not.be.visible'); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(2); + expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { + newValues: [ + { + columnId: 'name', + operator: 'Contains', + searchTerms: ['x'], + targetSelector: 'input.form-control.filter-name.compound-input.filled', + }, + ], + type: 'filter', + }); + expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { + newValues: { pageNumber: 1, pageSize: 10 }, + type: 'pagination', + }); + }); + }); + + it('should display page 1 of 1 with 2 items after erasing part of the filter to be "x" which should return 1 page', () => { + cy.wait(50); + + cy.get('[data-test=page-count]').contains('1'); + + cy.get('[data-test=item-from]').contains('1'); + + cy.get('[data-test=item-to]').contains('2'); + + cy.get('[data-test=total-items]').contains('2'); + + cy.get('[data-test=page-number-input]') + .invoke('val') + .then((pageNumber) => expect(pageNumber).to.eq('1')); + }); + }); + + describe('Set Dynamic Sorting', () => { + it('should click on "Set Filters Dynamically" then on "Set Sorting Dynamically"', () => { + cy.get('[data-test=set-dynamic-filter]').click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'loading'); + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=set-dynamic-sorting]').click(); + + cy.get('[data-test=status]').should('contain', 'loading'); + cy.get('[data-test=status]').should('contain', 'finished'); + }); + + it('should expect the grid to be sorted by "Name" descending', () => { + cy.get('#grid5').get('.slick-header-column:nth(1)').find('.slick-sort-indicator-desc').should('have.length', 1); + + cy.get('.slick-row').first().children('.slick-cell:nth(1)').should('contain', 'Ayers Hood'); + + cy.get('.slick-row').last().children('.slick-cell:nth(1)').should('contain', 'Alexander Foley'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$top=10&$orderby=Name desc&$filter=(startswith(Name, 'A'))`); + }); + }); + + it('should display an error when trying to sort by "Company" and the query & sort icons should remain the same', () => { + cy.get('.slick-header-columns').children('.slick-header-column:nth(3)').click(); + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(3)') + .find('.slick-sort-indicator.slick-sort-indicator-asc') + .should('not.exist'); + + // wait for the query to finish + cy.get('[data-test=error-status]').should('contain', 'Server could not sort using the field "Company"'); + cy.get('[data-test=status]').should('contain', 'ERROR!!'); + + // same query string as prior test + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$top=10&$orderby=Name desc&$filter=(startswith(Name, 'A'))`); + }); + }); + + it('should change Gender filter to "female" and still expect previous sort (before the error) to still be in query', () => { + cy.get('.ms-filter.filter-gender:visible').click(); + + cy.get('[data-name="filter-gender"].ms-drop').find('li:visible:nth(2)').contains('female').click(); + + cy.get('#grid5').find('.slick-row').should('have.length', 1); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Alisha Myers'); + + // query should still contain previous sort by + new gender filter + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$top=10&$orderby=Name desc&$filter=(startswith(Name, 'A') and Gender eq 'female')`); + }); + }); + + it('should try the "Company" filter and expect an error to throw and also expect the filter to reset to empty after the error is displayed', () => { + cy.get('input.search-filter.filter-company').type('Core'); + + // wait for the query to finish + cy.get('[data-test=error-status]').should('contain', 'Server could not filter using the field "Company"'); + cy.get('[data-test=status]').should('contain', 'ERROR!!'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$top=10&$orderby=Name desc&$filter=(startswith(Name, 'A') and Gender eq 'female')`); + }); + + cy.get('#grid5').find('.slick-row').should('have.length', 1); + }); + + it('should clear the "Name" filter and expect query to be successfull with just 1 filter "Gender" to be filled but without the previous failed filter', () => { + cy.get('#grid5') + .find('.slick-header-left .slick-header-column:nth(1)') + .trigger('mouseover') + .children('.slick-header-menu-button') + .invoke('show') + .click(); + + cy.get('.slick-header-menu .slick-menu-command-list') + .should('be.visible') + .children('.slick-menu-item:nth-of-type(6)') + .children('.slick-menu-content') + .should('contain', 'Remove Filter') + .click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$top=10&$orderby=Name desc&$filter=(Gender eq 'female')`); + }); + + cy.get('[data-test=page-number-input]') + .invoke('val') + .then((pageNumber) => expect(pageNumber).to.eq('1')); + + cy.get('[data-test=page-count]').contains('5'); + + cy.get('[data-test=item-from]').contains('1'); + + cy.get('[data-test=item-to]').contains('10'); + + cy.get('[data-test=total-items]').contains('50'); + }); + + it('should display error when clicking on the "Throw Error..." button and not expect query and page to change', () => { + cy.get('[data-test="throw-page-error-btn"]').click({ force: true }); + cy.wait(50); + + cy.get('[data-test=error-status]').should('contain', 'Server timed out trying to retrieve data for the last page'); + cy.get('[data-test=status]').should('contain', 'ERROR!!'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$top=10&$orderby=Name desc&$filter=(Gender eq 'female')`); + }); + + cy.get('[data-test=page-number-input]') + .invoke('val') + .then((pageNumber) => expect(pageNumber).to.eq('1')); + + cy.get('[data-test=page-count]').contains('5'); + + cy.get('[data-test=item-from]').contains('1'); + + cy.get('[data-test=item-to]').contains('10'); + + cy.get('[data-test=total-items]').contains('50'); + }); + + it('should display error when trying to change items per to 50,000 items and expect query & page to remain the same', () => { + cy.get('#items-per-page-label').select('50000'); + + cy.get('[data-test=error-status]').should('contain', 'Server timed out retrieving 50,000 rows'); + cy.get('[data-test=status]').should('contain', 'ERROR!!'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$top=10&$orderby=Name desc&$filter=(Gender eq 'female')`); + }); + + cy.get('[data-test=page-number-input]') + .invoke('val') + .then((pageNumber) => expect(pageNumber).to.eq('1')); + + cy.get('[data-test=page-count]').contains('5'); + + cy.get('[data-test=item-from]').contains('1'); + + cy.get('[data-test=item-to]').contains('10'); + + cy.get('[data-test=total-items]').contains('50'); + }); + + it('should now go to next page without anymore problems and query & page should change as normal', () => { + cy.get('.icon-seek-next').click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$top=10&$skip=10&$orderby=Name desc&$filter=(Gender eq 'female')`); + }); + + cy.get('[data-test=page-number-input]') + .invoke('val') + .then((pageNumber) => expect(pageNumber).to.eq('2')); + + cy.get('[data-test=page-count]').contains('5'); + + cy.get('[data-test=item-from]').contains('11'); + + cy.get('[data-test=item-to]').contains('20'); + + cy.get('[data-test=total-items]').contains('50'); + }); + }); + + describe('Select and Expand Behaviors', () => { + it('should enable "enableSelect" and "enableExpand" and expect the query to select/expand all fields', () => { + cy.get('[data-test=enable-expand]').click(); + cy.get('[data-test=enable-expand]').should('be.checked'); + cy.wait(5); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$top=10&$orderby=Name desc&$expand=category`); + }); + + cy.get('[data-test=enable-select]').click(); + cy.get('[data-test=enable-select]').should('be.checked'); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$top=10&$orderby=Name desc&$select=id,name,gender,company&$expand=category($select=name)`); + }); + }); + + it('should try to sort and filter on "Category" and expect the query to be succesful', () => { + cy.get('#grid5').find('button.slick-grid-menu-button').click({ force: true }); + + cy.get(`.slick-grid-menu:visible`).find('.slick-menu-item').first().find('span').contains('Clear all Filters').click(); + + cy.get('#grid5').find('button.slick-grid-menu-button').click({ force: true }); + + cy.get(`.slick-grid-menu:visible`).find('.slick-menu-item').find('span').contains('Clear all Sorting').click(); + + cy.get('.slick-header-columns').children('.slick-header-column:nth(4)').click(); + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(4)') + .find('.slick-sort-indicator.slick-sort-indicator-asc') + .should('exist'); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq(`$top=10&$orderby=Category/name asc&$select=id,name,gender,company&$expand=category($select=name)`); + }); + + cy.get('input.search-filter.filter-category_name').type('Silver'); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=odata-query-result]').should(($span) => { + expect($span.text()).to.eq( + `$top=10&$orderby=Category/name asc&$filter=(contains(Category/name, 'Silver'))&$select=id,name,gender,company&$expand=category($select=name)` + ); + }); + + cy.get('[data-test=page-number-input]') + .invoke('val') + .then((pageNumber) => expect(pageNumber).to.eq('1')); + + cy.get('[data-test=page-count]').contains('4'); + + cy.get('[data-test=item-from]').contains('1'); + + cy.get('[data-test=item-to]').contains('10'); + + cy.get('[data-test=total-items]').contains('32'); + }); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example06.cy.ts b/demos/vue/test/cypress/e2e/example06.cy.ts new file mode 100644 index 000000000..98e345d25 --- /dev/null +++ b/demos/vue/test/cypress/e2e/example06.cy.ts @@ -0,0 +1,962 @@ +import { addDay, format } from '@formkit/tempo'; + +import { removeWhitespaces } from '../plugins/utilities'; + +const currentYear = new Date().getFullYear(); +const presetLowestDay = `${currentYear}-01-01`; +const presetHighestDay = `${currentYear}-02-15`; + +function removeSpaces(textS) { + return `${textS}`.replace(/\s+/g, ''); +} + +describe('Example 6 - GraphQL Grid', () => { + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example6`); + cy.get('h2').should('contain', 'Example 6: Grid with Backend GraphQL Service'); + }); + + it('should have a grid of size 900 by 200px', () => { + cy.get('#slickGridContainer-grid6') + .should('have.css', 'width', '900px'); + + cy.get('#slickGridContainer-grid6 > .slickgrid-container') + .should($el => expect(parseInt(`${$el.height()}`, 10)).to.eq(200)); + }); + + it('should have English Text inside some of the Filters', () => { + cy.get('.search-filter.filter-gender .ms-choice > span') + .contains('Male'); + }); + + it('should have GraphQL query with defined Grid Presets', () => { + cy.get('.search-filter.filter-name select') + .should('not.have.value'); + + cy.get('.search-filter.filter-name') + .find('input') + .invoke('val') + .then(text => expect(text).to.eq('Joh*oe')); + + cy.get('.search-filter.filter-gender .ms-choice > span') + .contains('Male'); + + cy.get('.search-filter.filter-company .ms-choice > span') + .contains('Company XYZ'); + + cy.get('.search-filter.filter-finish') + .find('input') + .invoke('val') + .then(text => expect(text).to.eq(`${presetLowestDay} โ€” ${presetHighestDay}`)); + + cy.get('[data-test=alert-graphql-query]').should('exist'); + cy.get('[data-test=alert-graphql-query]').should('contain', 'GraphQL Query'); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + const text = removeWhitespaces($span.text()); // remove all white spaces + expect(text).to.eq(removeWhitespaces(`query{users(first:20,offset:20, + orderBy:[{field:"name",direction:ASC},{field:"company",direction:DESC}], + filterBy:[ + {field:"gender",operator:EQ,value:"male"}, + {field:"name",operator:StartsWith,value:"Joh"},{field:"name",operator:EndsWith,value:"oe"}, + {field:"company",operator:IN,value:"xyz"},{field:"finish",operator:GE,value:"${presetLowestDay}"},{field:"finish",operator:LE,value:"${presetHighestDay}"} + ],locale:"en",userId:123){ + totalCount,nodes{id,name,gender,company,billing{address{street,zip}},finish}}}`)); + }); + }); + + it('should use fake smaller server wait delay for faster E2E tests', () => { + cy.get('[data-test="server-delay"]') + .clear() + .type('20'); + }); + + it('should change Pagination to next page', () => { + cy.get('.icon-seek-next').click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + const text = removeWhitespaces($span.text()); // remove all white spaces + expect(text).to.eq(removeWhitespaces(`query{users(first:20,offset:40, + orderBy:[{field:"name",direction:ASC},{field:"company",direction:DESC}], + filterBy:[ + {field:"gender",operator:EQ,value:"male"}, + {field:"name",operator:StartsWith,value:"Joh"},{field:"name",operator:EndsWith,value:"oe"}, + {field:"company",operator:IN,value:"xyz"},{field:"finish",operator:GE,value:"${presetLowestDay}"},{field:"finish",operator:LE,value:"${presetHighestDay}"} + ],locale:"en",userId:123){totalCount,nodes{id,name,gender,company,billing{address{street,zip}},finish}}}`)); + }); + }); + + it('should change Pagination to last page', () => { + cy.get('.icon-seek-end').click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + const text = removeWhitespaces($span.text()); // remove all white spaces + expect(text).to.eq(removeWhitespaces(`query{users(first:20,offset:80, + orderBy:[{field:"name",direction:ASC},{field:"company",direction:DESC}], + filterBy:[ + {field:"gender",operator:EQ,value:"male"}, + {field:"name",operator:StartsWith,value:"Joh"},{field:"name",operator:EndsWith,value:"oe"}, + {field:"company",operator:IN,value:"xyz"},{field:"finish",operator:GE,value:"${presetLowestDay}"},{field:"finish",operator:LE,value:"${presetHighestDay}"} + ],locale:"en",userId:123){totalCount,nodes{id,name,gender,company,billing{address{street,zip}},finish}}}`)); + }); + }); + + it('should change Pagination to first page using the external button', () => { + cy.get('[data-test=goto-first-page') + .click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + const text = removeWhitespaces($span.text()); // remove all white spaces + expect(text).to.eq(removeWhitespaces(`query { users (first:20,offset:0, + orderBy:[{field:"name",direction:ASC},{field:"company",direction:DESC}], + filterBy:[ + {field:"gender",operator:EQ,value:"male"}, + {field:"name",operator:StartsWith,value:"Joh"},{field:"name",operator:EndsWith,value:"oe"}, + {field:"company",operator:IN,value:"xyz"}, + {field:"finish",operator:GE,value:"${presetLowestDay}"}, + {field:"finish",operator:LE,value:"${presetHighestDay}"} + ],locale:"en",userId:123) { totalCount, nodes { id,name,gender,company,billing{address{street,zip}},finish } } }`)); + }); + }); + + it('should change Pagination to last page using the external button', () => { + cy.get('[data-test=goto-last-page') + .click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + const text = removeWhitespaces($span.text()); // remove all white spaces + expect(text).to.eq(removeWhitespaces(`query{users(first:20,offset:80, + orderBy:[{field:"name",direction:ASC},{field:"company",direction:DESC}], + filterBy:[ + {field:"gender",operator:EQ,value:"male"}, + {field:"name",operator:StartsWith,value:"Joh"},{field:"name",operator:EndsWith,value:"oe"}, + {field:"company",operator:IN,value:"xyz"},{field:"finish",operator:GE,value:"${presetLowestDay}"},{field:"finish",operator:LE,value:"${presetHighestDay}"} + ],locale:"en",userId:123){totalCount,nodes{id,name,gender,company,billing{address{street,zip}},finish}}}`)); + }); + }); + + it('should change Pagination to first page with 30 items', () => { + cy.get('.icon-seek-first').click(); + + cy.get('#items-per-page-label').select('30'); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + const text = removeWhitespaces($span.text()); // remove all white spaces + expect(text).to.eq(removeWhitespaces(`query{users(first:30,offset:0, + orderBy:[{field:"name",direction:ASC},{field:"company",direction:DESC}], + filterBy:[ + {field:"gender",operator:EQ,value:"male"}, + {field:"name",operator:StartsWith,value:"Joh"},{field:"name",operator:EndsWith,value:"oe"}, + {field:"company",operator:IN,value:"xyz"},{field:"finish",operator:GE,value:"${presetLowestDay}"},{field:"finish",operator:LE,value:"${presetHighestDay}"} + ],locale:"en",userId:123){totalCount,nodes{id,name,gender,company,billing{address{street,zip}},finish}}}`)); + }); + }); + + it('should clear a single filter, that is not empty, by the header menu and expect query change', () => { + cy.get('#grid6') + .find('.slick-header-left .slick-header-column:nth(0)') + .trigger('mouseover') + .children('.slick-header-menu-button') + .should('be.hidden') + .invoke('show') + .click(); + + cy.get('.slick-header-menu .slick-menu-command-list') + .should('be.visible') + .children('.slick-menu-item:nth-of-type(6)') + .children('.slick-menu-content') + .should('contain', 'Remove Filter') + .click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + const text = removeWhitespaces($span.text()); // remove all white spaces + expect(text).to.eq(removeWhitespaces(`query{users(first:30,offset:0, + orderBy:[{field:"name",direction:ASC},{field:"company",direction:DESC}], + filterBy:[ + {field:"gender",operator:EQ,value:"male"},{field:"company",operator:IN,value:"xyz"}, + {field:"finish",operator:GE,value:"${presetLowestDay}"},{field:"finish",operator:LE,value:"${presetHighestDay}"} + ],locale:"en",userId:123){totalCount,nodes{id,name,gender,company,billing{address{street,zip}},finish}}}`)); + }); + }); + + it('should try clearing same filter, which is now empty, by the header menu and expect same query without loading spinner', () => { + cy.get('[data-test="server-delay"]') + .clear() + .type('250'); + + cy.get('#grid6') + .find('.slick-header-left .slick-header-column:nth(0)') + .trigger('mouseover') + .children('.slick-header-menu-button') + .invoke('show') + .click(); + + cy.get('.slick-header-menu .slick-menu-command-list') + .should('be.visible') + .children('.slick-menu-item:nth-of-type(6)') + .children('.slick-menu-content') + .should('contain', 'Remove Filter') + .click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + const text = removeWhitespaces($span.text()); // remove all white spaces + expect(text).to.eq(removeWhitespaces(`query{users(first:30,offset:0, + orderBy:[{field:"name",direction:ASC},{field:"company",direction:DESC}], + filterBy:[ + {field:"gender",operator:EQ,value:"male"},{field:"company",operator:IN,value:"xyz"}, + {field:"finish",operator:GE,value:"${presetLowestDay}"},{field:"finish",operator:LE,value:"${presetHighestDay}"} + ],locale:"en",userId:123){totalCount,nodes{id,name,gender,company,billing{address{street,zip}},finish}}}`)); + }); + }); + + it('should clear the date range filter expect the query to have the 2 "finish" (GE, LE) filters removed', () => { + cy.get('#grid6') + .find('.slick-header-left .slick-header-column:nth(5)') + .trigger('mouseover') + .children('.slick-header-menu-button') + .should('be.hidden') + .invoke('show') + .click(); + + cy.get('.slick-header-menu .slick-menu-command-list') + .should('be.visible') + .children('.slick-menu-item[data-command=clear-filter]') + .children('.slick-menu-content') + .should('contain', 'Remove Filter') + .click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + const text = removeWhitespaces($span.text()); // remove all white spaces + expect(text).to.eq(removeWhitespaces(`query{users(first:30,offset:0, + orderBy:[{field:"name",direction:ASC},{field:"company",direction:DESC}], + filterBy:[{field:"gender",operator:EQ,value:"male"},{field:"company",operator:IN,value:"xyz"}], + locale:"en",userId:123){totalCount,nodes{id,name,gender,company,billing{address{street,zip}},finish}}}`)); + }); + }); + + it('should Clear all Filters & Sorts', () => { + cy.contains('Clear all Filter & Sorts').click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + const text = removeWhitespaces($span.text()); // remove all white spaces + expect(text).to.eq(removeWhitespaces(`query{users(first:30,offset:0,locale:"en",userId:123){totalCount,nodes{id,name,gender,company,billing{address{street,zip}},finish}}}`)); + }); + }); + + it('should click on "Name" column to sort it Ascending', () => { + cy.get('.slick-header-columns') + .children('.slick-header-left .slick-header-column:nth(0)') + .click(); + + cy.get('.slick-header-columns') + .children('.slick-header-left .slick-header-column:nth(0)') + .find('.slick-sort-indicator.slick-sort-indicator-asc') + .should('be.visible'); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + const text = removeWhitespaces($span.text()); // remove all white spaces + expect(text).to.eq(removeWhitespaces(`query{users(first:30,offset:0, + orderBy:[{field:"name",direction:ASC}], + locale:"en",userId:123){totalCount,nodes{id,name,gender,company,billing{address{street,zip}},finish}}}`)); + }); + }); + + it('should perform filterQueryOverride when operator "%%" is selected', () => { + cy.get('.search-filter.filter-name select').find('option').last().then((element) => { + cy.get('.search-filter.filter-name select').select(element.val()); + }); + + cy.get('.search-filter.filter-name') + .find('input') + .clear() + .type('Jo%yn%er'); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + const text = removeSpaces($span.text()); // remove all white spaces + expect(text).to.eq(removeSpaces(`query { users (first:30,offset:0, + orderBy:[{field:"name",direction:ASC}], + filterBy:[{field:"name",operator:Like,value:"Jo%yn%er"}], + locale:"en",userId:123) { totalCount, nodes { id,name,gender,company,billing{address{street,zip}},finish } } }`)); + }); + }); + + it('should click on Set Dynamic Filter and expect query and filters to be changed', () => { + cy.get('[data-test=set-dynamic-filter]') + .click(); + + cy.get('.search-filter.filter-name select') + .should('have.value', 'a*'); + + cy.get('.search-filter.filter-name') + .find('input') + .invoke('val') + .then(text => expect(text).to.eq('Jane')); + + cy.get('.search-filter.filter-gender .ms-choice > span') + .contains('Female'); + + cy.get('.search-filter.filter-company .ms-choice > span') + .contains('Acme'); + + cy.get('.search-filter.filter-billingAddressZip select') + .should('have.value', '>='); + + cy.get('.search-filter.filter-billingAddressZip') + .find('input') + .invoke('val') + .then(text => expect(text).to.eq('11')); + + cy.get('.search-filter.filter-finish') + .find('input') + .invoke('val') + .then(text => expect(text).to.eq(`${presetLowestDay} โ€” ${presetHighestDay}`)); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + const text = removeWhitespaces($span.text()); // remove all white spaces + expect(text).to.eq(removeWhitespaces(`query{users(first:30,offset:0, + orderBy:[{field:"name",direction:ASC}], + filterBy:[{field:"gender",operator:EQ,value:"female"},{field:"name",operator:StartsWith,value:"Jane"}, + {field:"company",operator:IN,value:"acme"},{field:"billing.address.zip",operator:GE,value:"11"}, + {field:"finish",operator:GE,value:"${presetLowestDay}"},{field:"finish",operator:LE,value:"${presetHighestDay}"}],locale:"en",userId:123) + {totalCount,nodes{id,name,gender,company,billing{address{street,zip}},finish}}}`)); + }); + }); + + it('should use a range filter when searching with ".."', () => { + cy.get('.slick-header-columns') + .children('.slick-header-left .slick-header-column:nth(0)') + .contains('Name') + .click(); + + cy.get('.search-filter.filter-name') + .find('input') + .clear() + .type('Anthony Joyner..Ayers Hood'); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + const text = removeWhitespaces($span.text()); // remove all white spaces + expect(text).to.eq(removeWhitespaces(`query { users (first:30,offset:0, + orderBy:[{field:"name",direction:DESC}], + filterBy:[{field:"gender",operator:EQ,value:"female"},{field:"name",operator:GE,value:"Anthony Joyner"},{field:"name",operator:LE,value:"Ayers Hood"}, + {field:"company",operator:IN,value:"acme"},{field:"billing.address.zip",operator:GE,value:"11"}, + {field:"finish",operator:GE,value:"${presetLowestDay}"},{field:"finish",operator:LE,value:"${presetHighestDay}"}],locale:"en",userId:123) + {totalCount,nodes{id,name,gender,company,billing{address{street,zip}},finish}}}`)); + }); + }); + + it('should open Date picker and expect date range between 01-Jan to 15-Feb', () => { + cy.get('.search-filter.filter-finish.filled input') + .click({ force: true }); + + cy.get('.vanilla-calendar:visible'); + + cy.get('.vanilla-calendar-column:nth(0) .vanilla-calendar-month') + .should('have.text', 'January'); + + cy.get('.vanilla-calendar-column:nth(1) .vanilla-calendar-month') + .should('have.text', 'February'); + + cy.get('.vanilla-calendar-year:nth(0)') + .should('have.text', currentYear); + + cy.get('.vanilla-calendar:visible') + .find('.vanilla-calendar-day__btn_selected') + .should('have.length', 46); + + cy.get('.vanilla-calendar:visible') + .find('.vanilla-calendar-day__btn_selected') + .first() + .should('have.text', '1'); + + cy.get('.vanilla-calendar:visible') + .find('.vanilla-calendar-day__btn_selected') + .last() + .should('have.text', '15'); + }); + + describe('Set Dynamic Sorting', () => { + it('should use slower server wait delay to test loading widget', () => { + cy.get('[data-test="server-delay"]') + .clear() + .type('250'); + }); + + it('should click on "Clear all Filters & Sorting" then "Set Dynamic Sorting" buttons', () => { + cy.get('[data-test=clear-filters-sorting]') + .click(); + + cy.get('[data-test=status]').should('contain', 'processing'); + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=set-dynamic-sorting]') + .click(); + + cy.get('[data-test=status]').should('contain', 'processing'); + cy.get('[data-test=status]').should('contain', 'finished'); + }); + + it('should use smaller server wait delay for faster E2E tests', () => { + cy.get('[data-test="server-delay"]') + .clear() + .type('20'); + }); + + it('should expect the grid to be sorted by "Zip" descending then by "Company" ascending', () => { + cy.get('#grid6') + .get('.slick-header-left .slick-header-column:nth(2)') + .find('.slick-sort-indicator-asc') + .should('have.length', 1) + .siblings('.slick-sort-indicator-numbered') + .contains('2'); + + cy.get('#grid6') + .get('.slick-header-left .slick-header-column:nth(3)') + .find('.slick-sort-indicator-desc') + .should('have.length', 1) + .siblings('.slick-sort-indicator-numbered') + .contains('1'); + + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + const text = removeWhitespaces($span.text()); // remove all white spaces + expect(text).to.eq(removeWhitespaces(`query{users(first:30,offset:0, + orderBy:[{field:"billing.address.zip",direction:DESC},{field:"company",direction:ASC}],locale:"en",userId:123){ + totalCount,nodes{id,name,gender,company,billing{address{street,zip}},finish}}}`)); + }); + }); + + it('should open Date picker and no longer expect date range selection in the picker', () => { + cy.get('.search-filter.filter-finish') + .should('not.have.class', 'filled') + .click(); + + cy.get('.vanilla-calendar-year:nth(0)') + .should('have.text', currentYear); + + cy.get('.vanilla-calendar:visible') + .find('.vanilla-calendar-day__btn_selected') + .should('not.exist'); + }); + }); + + describe('Translate by Language', () => { + it('should Clear all Filters & Sorts', () => { + cy.contains('Clear all Filter & Sorts').click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + }); + + it('should have English Column Titles in the grid after switching locale', () => { + const expectedColumnTitles = ['Name', 'Gender', 'Company', 'Billing Address Zip', 'Billing Address Street', 'Date']; + + cy.get('#grid6') + .find('.slick-header-left .slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(expectedColumnTitles[index])); + }); + + it('should have English Column Grouping Titles in the grid after switching locale', () => { + const expectedGroupTitles = ['Customer Information', 'Billing Information']; + cy.get('#grid6') + .find('.slick-preheader-panel .slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(expectedGroupTitles[index])); + }); + + it('should hover over the "Title" column header menu and expect all commands be displayed in English', () => { + cy.get('#grid6') + .find('.slick-header-columns.slick-header-columns-left .slick-header-column') + .first() + .trigger('mouseover') + .children('.slick-header-menu-button') + .invoke('show') + .click(); + + cy.get('.slick-header-menu .slick-menu-command-list') + .should('be.visible') + .children('.slick-menu-item:nth-of-type(3)') + .children('.slick-menu-content') + .should('contain', 'Sort Ascending'); + + cy.get('.slick-header-menu .slick-menu-command-list') + .children('.slick-menu-item:nth-of-type(4)') + .children('.slick-menu-content') + .should('contain', 'Sort Descending'); + + cy.get('.slick-header-menu .slick-menu-command-list') + .children('.slick-menu-item:nth-of-type(6)') + .children('.slick-menu-content') + .should('contain', 'Remove Filter'); + + cy.get('.slick-header-menu .slick-menu-command-list') + .children('.slick-menu-item:nth-of-type(7)') + .children('.slick-menu-content') + .should('contain', 'Remove Sort'); + + cy.get('.slick-header-menu .slick-menu-command-list') + .children('.slick-menu-item:nth-of-type(8)') + .children('.slick-menu-content') + .should('contain', 'Hide Column'); + }); + + it('should open the Grid Menu and expect all commands be displayed in English', () => { + cy.get('#grid6') + .find('button.slick-grid-menu-button') + .trigger('click'); + + cy.get('.slick-grid-menu .slick-menu-title:nth(0)') + .contains('Commands'); + + cy.get('.slick-grid-menu .slick-menu-item:nth(0) > span') + .contains('Clear all Filters'); + + cy.get('.slick-grid-menu .slick-menu-item:nth(1) > span') + .contains('Clear all Sorting'); + + cy.get('.slick-grid-menu .slick-menu-title:nth(1)') + .contains('Columns'); + + cy.get('.slick-grid-menu .slick-column-picker-list li:nth(0)') + .contains('Customer Information - Name'); + + cy.get('.slick-grid-menu .slick-column-picker-list li:nth(1)') + .contains('Customer Information - Gender'); + + cy.get('.slick-grid-menu [data-dismiss=slick-grid-menu].close') + .click({ force: true }); + }); + + it('should switch locale from English to French', () => { + cy.get('[data-test=selected-locale]') + .should('contain', 'en.json'); + + cy.get('[data-test=language-button]') + .click(); + + cy.get('[data-test=selected-locale]') + .should('contain', 'fr.json'); + }); + + it('should have French Column Titles in the grid after switching locale', () => { + const expectedColumnTitles = ['Nom', 'Sexe', 'Compagnie', 'Code zip de facturation', 'Adresse de facturation', 'Date']; + + cy.get('#grid6') + .find('.slick-header-left .slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(expectedColumnTitles[index])); + }); + + it('should have French Column Grouping Titles in the grid after switching locale', () => { + const expectedGroupTitles = ['Information Client', 'Information de Facturation']; + cy.get('#grid6') + .find('.slick-preheader-panel .slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(expectedGroupTitles[index])); + }); + + it('should display Pagination in French', () => { + cy.get('.slick-pagination-settings > span') + .contains('รฉlรฉments par page'); + + cy.get('.page-info-from-to') + .contains('de'); + + cy.get('[data-test=item-from]') + .contains('1'); + + cy.get('[data-test=item-to]') + .contains('30'); + + cy.get('[data-test=total-items]') + .contains('100'); + + cy.get('.page-info-total-items') + .contains('รฉlรฉments'); + }); + + it('should hover over the "Title" column header menu and expect all commands be displayed in French', () => { + cy.get('#grid6') + .find('.slick-header-columns.slick-header-columns-left .slick-header-column') + .first() + .trigger('mouseover') + .children('.slick-header-menu-button') + .invoke('show') + .click(); + + cy.get('.slick-header-menu .slick-menu-command-list') + .should('be.visible') + .children('.slick-menu-item:nth-of-type(3)') + .children('.slick-menu-content') + .should('contain', 'Trier par ordre croissant'); + + cy.get('.slick-header-menu .slick-menu-command-list') + .children('.slick-menu-item:nth-of-type(4)') + .children('.slick-menu-content') + .should('contain', 'Trier par ordre dรฉcroissant'); + + cy.get('.slick-header-menu .slick-menu-command-list') + .children('.slick-menu-item:nth-of-type(6)') + .children('.slick-menu-content') + .should('contain', 'Supprimer le filtre'); + + cy.get('.slick-header-menu .slick-menu-command-list') + .children('.slick-menu-item:nth-of-type(7)') + .children('.slick-menu-content') + .should('contain', 'Supprimer le tri'); + + cy.get('.slick-header-menu .slick-menu-command-list') + .children('.slick-menu-item:nth-of-type(8)') + .children('.slick-menu-content') + .should('contain', 'Cacher la colonne'); + }); + + it('should open the Grid Menu and expect all commands be displayed in French', () => { + cy.get('#grid6') + .find('button.slick-grid-menu-button') + .trigger('click'); + + cy.get('.slick-grid-menu .slick-menu-title:nth(0)') + .contains('Commandes'); + + cy.get('.slick-grid-menu .slick-menu-item:nth(0) > span') + .contains('Supprimer tous les filtres'); + + cy.get('.slick-grid-menu .slick-menu-item:nth(1) > span') + .contains('Supprimer tous les tris'); + + cy.get('.slick-grid-menu .slick-menu-title:nth(1)') + .contains('Colonnes'); + + cy.get('.slick-grid-menu .slick-column-picker-list li:nth(0)') + .contains('Information Client - Nom'); + + cy.get('.slick-grid-menu .slick-column-picker-list li:nth(1)') + .contains('Information Client - Sexe'); + + cy.get('.slick-grid-menu [data-dismiss=slick-grid-menu].close') + .click({ force: true }); + }); + + it('should click on Set Dynamic Filter and expect query and filters to be changed', () => { + cy.get('[data-test=set-dynamic-filter]') + .click(); + + cy.get('.search-filter.filter-name select') + .should('have.value', 'a*'); + + cy.get('.search-filter.filter-name') + .find('input') + .invoke('val') + .then(text => expect(text).to.eq('Jane')); + + cy.get('.search-filter.filter-gender .ms-choice > span') + .contains('Fรฉminin'); + + cy.get('.search-filter.filter-company .ms-choice > span') + .contains('Acme'); + + cy.get('.search-filter.filter-billingAddressZip select') + .should('have.value', '>='); + + cy.get('.search-filter.filter-billingAddressZip') + .find('input') + .invoke('val') + .then(text => expect(text).to.eq('11')); + + cy.get('.search-filter.filter-finish') + .find('input') + .invoke('val') + .then(text => expect(text).to.eq(`${presetLowestDay} โ€” ${presetHighestDay}`)); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + const text = removeWhitespaces($span.text()); // remove all white spaces + expect(text).to.eq(removeWhitespaces(`query{users(first:30,offset:0, + filterBy:[{field:"gender",operator:EQ,value:"female"},{field:"name",operator:StartsWith,value:"Jane"}, + {field:"company",operator:IN,value:"acme"},{field:"billing.address.zip",operator:GE,value:"11"}, + {field:"finish",operator:GE,value:"${presetLowestDay}"},{field:"finish",operator:LE,value:"${presetHighestDay}"}],locale:"fr",userId:123) + {totalCount,nodes{id,name,gender,company,billing{address{street,zip}},finish}}}`)); + }); + }); + + it('should have French Text inside some of the Filters', () => { + cy.get('div.ms-filter.filter-gender') + .trigger('click'); + + cy.get('.ms-drop') + .contains('Masculin') // use regexp to avoid finding first Task 3 which is in fact Task 399 + .click(); + + cy.get('.search-filter.filter-gender .ms-choice > span') + .contains('Masculin'); + }); + + it('should switch locale to English', () => { + cy.get('[data-test=language-button]') + .click(); + + cy.get('[data-test=selected-locale]') + .should('contain', 'en.json'); + }); + }); + + describe('Cursor Pagination', () => { + it('should re-initialize grid for cursor pagination', () => { + cy.get('[data-test="reset-presets"]').click(); // reset to same original presets + cy.get('[data-test=cursor]').click(); + cy.wait(1); + + // the page number input should be a label now + cy.get('[data-test=page-number-label]').should('exist').should('have.text', '1'); + }); + + it('should change Pagination to the last page', () => { + // Go to first page (if not already there) + cy.get('[data-test=goto-first-page').click(); + + cy.get('.icon-seek-end').click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + const text = removeWhitespaces($span.text()); // remove all white spaces + expect(text).to.eq(removeWhitespaces(`query{users(last:20, + orderBy:[{field:"name",direction:ASC},{field:"company",direction:DESC}], + filterBy:[ + {field:"gender",operator:EQ,value:"male"}, + {field:"name",operator:StartsWith,value:"Joh"},{field:"name",operator:EndsWith,value:"oe"}, + {field:"company",operator:IN,value:"xyz"},{field:"finish",operator:GE,value:"${presetLowestDay}"},{field:"finish",operator:LE,value:"${presetHighestDay}"} + ],locale:"en",userId:123){totalCount,nodes{id,name,gender,company,billing{address{street,zip}},finish},pageInfo{hasNextPage,hasPreviousPage,endCursor,startCursor},edges{cursor}}}`)); + }); + }); + + it('should change Pagination to the first page', () => { + // Go to first page (if not already there) + cy.get('[data-test=goto-last-page').click(); + + cy.get('.icon-seek-first').click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + const text = removeWhitespaces($span.text()); // remove all white spaces + expect(text).to.eq(removeWhitespaces(`query{users(first:20, + orderBy:[{field:"name",direction:ASC},{field:"company",direction:DESC}], + filterBy:[ + {field:"gender",operator:EQ,value:"male"}, + {field:"name",operator:StartsWith,value:"Joh"},{field:"name",operator:EndsWith,value:"oe"}, + {field:"company",operator:IN,value:"xyz"},{field:"finish",operator:GE,value:"${presetLowestDay}"},{field:"finish",operator:LE,value:"${presetHighestDay}"} + ],locale:"en",userId:123){totalCount,nodes{id,name,gender,company,billing{address{street,zip}},finish},pageInfo{hasNextPage,hasPreviousPage,endCursor,startCursor},edges{cursor}}}`)); + }); + }); + + it('should change Pagination to next page and all the way to the last', () => { + // Go to first page (if not already there) + cy.get('[data-test=goto-first-page').click(); + cy.get('[data-test=status]').should('contain', 'finished'); + + // on page 1, click 4 times to get to page 5 (the last page) + cy.wrap([0, 1, 2, 3]).each((el, i) => { + cy.wait(25); // Avoid clicking too fast and hitting race conditions because of the setTimeout in the example page (this timeout should be greater than in the page) + cy.get('.icon-seek-next').click().then(() => { + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + // First page is A-B + // first click is to get page after A-B + // => get first 20 after 'B' + const afterCursor = String.fromCharCode('B'.charCodeAt(0) + i); + + const text = removeWhitespaces($span.text()); // remove all white spaces + expect(text).to.eq(removeWhitespaces(`query{users(first:20,after:"${afterCursor}", + orderBy:[{field:"name",direction:ASC},{field:"company",direction:DESC}], + filterBy:[ + {field:"gender",operator:EQ,value:"male"}, + {field:"name",operator:StartsWith,value:"Joh"},{field:"name",operator:EndsWith,value:"oe"}, + {field:"company",operator:IN,value:"xyz"},{field:"finish",operator:GE,value:"${presetLowestDay}"},{field:"finish",operator:LE,value:"${presetHighestDay}"} + ],locale:"en",userId:123){totalCount,nodes{id,name,gender,company,billing{address{street,zip}},finish},pageInfo{hasNextPage,hasPreviousPage,endCursor,startCursor},edges{cursor}}}`)); + }); + }); + }); + }); + + it('should change Pagination from the last page all the way to the first', () => { + // Go to last page (if not already there) + cy.get('[data-test=goto-last-page').click(); + + // on page 5 (last page), click 4 times to go to page 1 + cy.wrap([0, 1, 2, 3]).each((el, i) => { + cy.wait(25); // Avoid clicking too fast and hitting race conditions because of the setTimeout in the example page (this timeout should be greater than in the page) + cy.get('.icon-seek-prev').click().then(() => { + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + // Last page is E-F + // first click is to get page before E-F + // => get last 20 before 'E' + const beforeCursor = String.fromCharCode('E'.charCodeAt(0) - i); + + const text = removeWhitespaces($span.text()); // remove all white spaces + expect(text).to.eq(removeWhitespaces(`query{users(last:20,before:"${beforeCursor}", + orderBy:[{field:"name",direction:ASC},{field:"company",direction:DESC}], + filterBy:[ + {field:"gender",operator:EQ,value:"male"}, + {field:"name",operator:StartsWith,value:"Joh"},{field:"name",operator:EndsWith,value:"oe"}, + {field:"company",operator:IN,value:"xyz"},{field:"finish",operator:GE,value:"${presetLowestDay}"},{field:"finish",operator:LE,value:"${presetHighestDay}"} + ],locale:"en",userId:123){totalCount,nodes{id,name,gender,company,billing{address{street,zip}},finish},pageInfo{hasNextPage,hasPreviousPage,endCursor,startCursor},edges{cursor}}}`)); + }); + }); + }); + }); + }); + + describe('Filter Shortcuts', () => { + const today = format(new Date(), 'YYYY-MM-DD'); + const next20Day = format(addDay(new Date(), 20), 'YYYY-MM-DD'); + + it('should open header menu of "Finish" again then choose "Filter Shortcuts -> In the Future" and expect date range of the next 20 days', () => { + cy.get('[data-test=offset]').click(); + + cy.get('#grid6') + .find('.slick-header-column:nth-of-type(6)') + .trigger('mouseover') + .children('.slick-header-menu-button') + .invoke('show') + .click(); + + cy.get('[data-command=filter-shortcuts-root-menu]') + .trigger('mouseover'); + + cy.get('.slick-header-menu.slick-menu-level-1') + .find('[data-command=next-20-days]') + .should('contain', 'Next 20 days') + .click(); + + cy.get('.search-filter.filter-finish input.date-picker') + .invoke('val') + .should('equal', `${today} โ€” ${next20Day}`); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + const text = removeSpaces($span.text()); // remove all white spaces + expect(text).to.eq(removeSpaces(`query { users (first:20,offset:0,orderBy:[{field:"name",direction:ASC}, + {field:"company",direction:DESC}],filterBy:[{field:"gender",operator:EQ,value:"male"}, + {field:"name",operator:StartsWith,value:"Joh"},{field:"name",operator:EndsWith,value:"oe"}, + {field:"company",operator:IN,value:"xyz"},{field:"finish",operator:GE,value:"${today}"}, + {field:"finish",operator:LE,value:"${next20Day}"}],locale:"en",userId:123) { + totalCount, nodes { id,name,gender,company,billing{address{street,zip}},finish}}}`)); + }); + }); + + it('should switch locale to French', () => { + cy.get('[data-test=language-button]') + .click(); + + cy.get('[data-test=selected-locale]') + .should('contain', 'fr.json'); + }); + + it('should open header menu of "Finish" again now expect French translations "Filter Shortcuts -> In the Future" and expect date range of the next 20 days', () => { + cy.get('#grid6') + .find('.slick-header-column:nth-of-type(6)') + .trigger('mouseover') + .children('.slick-header-menu-button') + .invoke('show') + .click(); + + cy.get('[data-command=filter-shortcuts-root-menu]') + .should('contain', 'Raccourcis de filtre') + .trigger('mouseover'); + + cy.get('.slick-header-menu.slick-menu-level-1') + .find('[data-command=next-20-days]') + .should('contain', '20 prochain jours') + .click(); + + cy.get('.search-filter.filter-finish input.date-picker') + .invoke('val') + .should('equal', `${today} โ€” ${next20Day}`); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + const text = removeSpaces($span.text()); // remove all white spaces + expect(text).to.eq(removeSpaces(`query { users (first:20,offset:0,orderBy:[{field:"name",direction:ASC}, + {field:"company",direction:DESC}],filterBy:[{field:"gender",operator:EQ,value:"male"}, + {field:"name",operator:StartsWith,value:"Joh"},{field:"name",operator:EndsWith,value:"oe"}, + {field:"company",operator:IN,value:"xyz"},{field:"finish",operator:GE,value:"${today}"}, + {field:"finish",operator:LE,value:"${next20Day}"}],locale:"fr",userId:123) { + totalCount, nodes { id,name,gender,company,billing{address{street,zip}},finish}}}`)); + }); + }); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example07.cy.ts b/demos/vue/test/cypress/e2e/example07.cy.ts new file mode 100644 index 000000000..140c28b24 --- /dev/null +++ b/demos/vue/test/cypress/e2e/example07.cy.ts @@ -0,0 +1,418 @@ +describe('Example 7 - Header Button Plugin', () => { + const titles = ['Resize me!', 'Hover me!', 'Column C', 'Column D', 'Column E', 'Column F', 'Column G', 'Column H', 'Column I', 'Column J']; + + beforeEach(() => { + // create a console.log spy for later use + cy.window().then((win) => { + cy.spy(win.console, 'log'); + }); + }); + + describe('Grid 1', () => { + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example7`); + cy.get('h2').should('contain', 'Example 7: Header Button Plugin'); + }); + + it('should have exact Column Titles in the grid', () => { + cy.get('#grid7-1 .slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(titles[index])); + }); + + it('should go over the 3rd column "Column C" and expect to see negative number in red after clicking on the red header button', () => { + cy.get('#grid7-1 .slick-header-columns') + .children('.slick-header-column:nth(2)') + .should('contain', 'Column C'); + + cy.get('#grid7-1 .slick-header-columns') + .children('.slick-header-column:nth(2)') + .find('.slick-header-button.mdi-lightbulb-outline.text-warning.faded') + .click(); + + cy.get('#grid7-1 .slick-header-columns') + .children('.slick-header-column:nth(2)') + .find('.slick-header-button.mdi-lightbulb-outline.text-warning.faded') + .should('not.exist'); // shouldn't be faded anymore + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(1); + expect(win.console.log).to.be.calledWith(`execute a callback action to "toggle-highlight" on Column C`); + }); + + cy.get('#grid7-1 .slick-row') + .each(($row, index) => { + if (index > 10) { + return; // check only the first 10 rows is enough + } + cy.wrap($row).children('.slick-cell:nth(2)') + .each($cell => { + const numberValue = $cell.text(); + const htmlValue = $cell.html(); + if (+numberValue < 0) { + expect(htmlValue).to.eq(`
${numberValue}
`); + } else { + expect(htmlValue).to.eq(numberValue); + } + }); + }); + }); + + it('should go over the 5th column "Column E" and not find the red header button', () => { + cy.get('#grid7-1 .slick-header-columns') + .children('.slick-header-column:nth(4)') + .should('contain', 'Column E'); + + // column E should not have the icon + cy.get('#grid7-1 .slick-header-columns') + .children('.slick-header-column:nth(4)') + .find('.slick-header-button') + .should('not.exist'); + }); + + it('should go over the last "Column J" and expect to find the button to have the disabled class and clicking it should not turn the negative numbers to red neither expect console log after clicking the disabled button', () => { + cy.get('#grid7-1 .slick-viewport-top.slick-viewport-left') + .scrollTo('right') + .wait(50); + + cy.get('#grid7-1 .slick-header-columns') + .children('.slick-header-column:nth(9)') + .should('contain', 'Column J') + .find('.slick-header-button-disabled') + .should('exist'); + + cy.get('#grid7-1 .slick-header-columns') + .children('.slick-header-column:nth(9)') + .find('.slick-header-button.slick-header-button-disabled.mdi-lightbulb-outline.text-warning.faded') + .should('exist') + .click(); + + cy.get('#grid7-1 .slick-header-columns') + .children('.slick-header-column:nth(9)') + .find('.slick-header-button.slick-header-button-disabled.mdi-lightbulb-outline.text-warning.faded') + .should('exist'); // should still be faded after previous click + + cy.get('#grid7-1 .slick-row') + .each(($row, index) => { + if (index > 10) { + return; + } + cy.wrap($row).children('.slick-cell:nth(9)') + .each($cell => expect($cell.html()).to.eq($cell.text())); + }); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(0); + }); + }); + + it('should resize 1st column and make it wider', () => { + cy.get('#grid7-1 .slick-viewport-top.slick-viewport-left') + .scrollTo('left') + .wait(50); + + cy.get('#grid7-1 .slick-header-columns') + .children('.slick-header-column:nth(0)') + .should('contain', 'Resize me!'); + + cy.get('#grid7-1 .slick-header-columns') + .children('.slick-header-column:nth(0)') + .find('.slick-header-button:nth(3)') + .should('be.hidden'); + + // Cypress does not yet support the .hover() method and because of that we need to manually resize the element + // this is not ideal since it only resizes the cell not the entire column but it's enough to test the functionality + cy.get('#grid7-1 .slick-header-column:nth(0)') + // resize the 1st column + .each($elm => $elm.width(140)) + .find('.slick-resizable-handle') + .should('be.visible') + .invoke('show'); + + cy.get('#grid7-1 .slick-header-column:nth(0)') + .should(($el) => { + const expectedWidth = 140; // calculate with a calculated width including a (+/-)1px precision + expect($el.width()).greaterThan(expectedWidth - 1); + expect($el.width()).lessThan(expectedWidth + 1); + }); + + cy.get('#grid7-1 .slick-header-columns') + .children('.slick-header-column:nth(0)') + .find('.slick-header-button') + .should('have.length', 4); + }); + + it('should resize column to its previous size and still expect some icons to be hidden', () => { + cy.get('#grid7-1 .slick-header-column:nth(0)') + // resize the 1st column + .each($elm => $elm.width(50)) + .find('.slick-resizable-handle') + .should('be.visible') + .invoke('show'); + + cy.get('#grid7-1 .slick-header-columns') + .children('.slick-header-column:nth(0)') + .find('.slick-header-button:nth(3)') + .should('be.hidden'); + + cy.get('#grid7-1 .slick-header-columns') + .children('.slick-header-column:nth(0)') + .find('.slick-header-button:nth(1)') + .should('be.hidden'); + }); + + it('should go on the 2nd column "Hover me!" and expect the header button to appear only when doing hover over it', () => { + cy.get('#grid7-1 .slick-header-columns') + .children('.slick-header-column:nth(1)') + .should('contain', 'Hover me!'); + + cy.get('#grid7-1 .slick-header-columns') + .children('.slick-header-column:nth(1)') + .find('.slick-header-button.slick-header-button-hidden') + .should('be.hidden') + .should('have.css', 'visibility', 'hidden'); + }); + }); + + describe('Grid 2', () => { + it('should have exact Column Titles in the grid', () => { + cy.get('#grid7-2 .slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(titles[index])); + }); + + it('should go over the 3rd column "Column C" and expect to see negative number in red after clicking on the red header button', () => { + cy.get('#grid7-2 .slick-header-columns') + .children('.slick-header-column:nth(2)') + .should('contain', 'Column C'); + + cy.get('#grid7-2 .slick-header-columns') + .children('.slick-header-column:nth(2)') + .find('.slick-header-button.mdi-lightbulb-outline.text-warning.faded') + .click({ force: true }); + + cy.get('#grid7-2 .slick-header-columns') + .children('.slick-header-column:nth(2)') + .find('.slick-header-button.mdi-lightbulb-outline.text-warning.faded') + .should('not.exist'); // shouldn't be faded anymore + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(1); + expect(win.console.log).to.be.calledWith(`execute a callback action to "toggle-highlight" on Column C`); + }); + + cy.get('#grid7-2 .slick-row') + .each(($row, index) => { + if (index > 10) { + return; // check only the first 10 rows is enough + } + cy.wrap($row).children('.slick-cell:nth(2)') + .each($cell => { + const numberValue = $cell.text(); + const htmlValue = $cell.html(); + if (+numberValue < 0) { + expect(htmlValue).to.eq(`
${numberValue}
`); + } else { + expect(htmlValue).to.eq(numberValue); + } + }); + }); + }); + + it('should go over the 5th column "Column E" and not find the red header button', () => { + cy.get('#grid7-2 .slick-header-columns') + .children('.slick-header-column:nth(4)') + .should('contain', 'Column E'); + + // column E should not have the icon + cy.get('#grid7-2 .slick-header-columns') + .children('.slick-header-column:nth(4)') + .find('.slick-header-button') + .should('not.exist'); + }); + + it('should go over the last "Column J" and expect to find the button to have the disabled class and clicking it should not turn the negative numbers to red neither expect console log after clicking the disabled button', () => { + cy.get('#grid7-2 .slick-viewport-top.slick-viewport-left') + .scrollTo('right') + .wait(50); + + cy.get('#grid7-2 .slick-header-columns') + .children('.slick-header-column:nth(9)') + .should('contain', 'Column J') + .find('.slick-header-button-disabled') + .should('exist'); + + cy.get('#grid7-2 .slick-header-columns') + .children('.slick-header-column:nth(9)') + .find('.slick-header-button.slick-header-button-disabled.mdi-lightbulb-outline.text-warning.faded') + .should('exist') + .click({ force: true }); + + cy.get('#grid7-2 .slick-header-columns') + .children('.slick-header-column:nth(9)') + .find('.slick-header-button.slick-header-button-disabled.mdi-lightbulb-outline.text-warning.faded') + .should('exist'); // should still be faded after previous click + + cy.get('#grid7-2 .slick-row') + .each(($row, index) => { + if (index > 10) { + return; + } + cy.wrap($row).children('.slick-cell:nth(9)') + .each($cell => expect($cell.html()).to.eq($cell.text())); + }); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(0); + }); + }); + + it('should resize 1st column and make it wider', () => { + cy.get('#grid7-2 .slick-viewport-top.slick-viewport-left') + .scrollTo('left') + .wait(50); + + cy.get('#grid7-2 .slick-header-columns') + .children('.slick-header-column:nth(0)') + .should('contain', 'Resize me!'); + + cy.get('#grid7-2 .slick-header-columns') + .children('.slick-header-column:nth(0)') + .find('.slick-header-button:nth(3)') + .should('be.hidden'); + + // Cypress does not yet support the .hover() method and because of that we need to manually resize the element + // this is not ideal since it only resizes the cell not the entire column but it's enough to test the functionality + cy.get('#grid7-2 .slick-header-column:nth(0)') + // resize the 1st column + .each($elm => $elm.width(140)) + .find('.slick-resizable-handle') + .should('be.visible') + .invoke('show'); + + cy.get('#grid7-2 .slick-header-column:nth(0)') + .should(($el) => { + const expectedWidth = 140; // calculate with a calculated width including a (+/-)1px precision + expect($el.width()).greaterThan(expectedWidth - 1); + expect($el.width()).lessThan(expectedWidth + 1); + }); + + cy.get('#grid7-2 .slick-header-columns') + .children('.slick-header-column:nth(0)') + .find('.slick-header-button') + .should('have.length', 4); + }); + + it('should resize column to its previous size and still expect some icons to be hidden', () => { + cy.get('#grid7-2 .slick-header-column:nth(0)') + // resize the 1st column + .each($elm => $elm.width(50)) + .find('.slick-resizable-handle') + .should('be.visible') + .invoke('show'); + + cy.get('#grid7-2 .slick-header-columns') + .children('.slick-header-column:nth(0)') + .find('.slick-header-button:nth(3)') + .should('be.hidden'); + + cy.get('#grid7-2 .slick-header-columns') + .children('.slick-header-column:nth(0)') + .find('.slick-header-button:nth(1)') + .should('be.hidden'); + }); + + it('should go on the 2nd column "Hover me!" and expect the header button to appear only when doing hover over it', () => { + cy.get('#grid7-2 .slick-header-columns') + .children('.slick-header-column:nth(1)') + .should('contain', 'Hover me!'); + + cy.get('#grid7-2 .slick-header-columns') + .children('.slick-header-column:nth(1)') + .find('.slick-header-button.slick-header-button-hidden') + .should('have.css', 'visibility', 'hidden'); + }); + + it('should filter "Column C" with positive number only and not expect any more red values', () => { + cy.get('#grid7-2 .search-filter.filter-2') + .type('>0'); + + cy.get('#grid7-2 .slick-row') + .each(($row, index) => { + if (index > 10) { + return; // check only the first 10 rows is enough + } + cy.wrap($row).children('.slick-cell:nth(2)') + .each($cell => { + const numberValue = $cell.text(); + expect(+numberValue).to.be.greaterThan(0); + }); + }); + }); + + it('should hover over the "Column C" and click on "Clear Filter" and expect grid to have all rows shown', () => { + cy.get('#grid7-2 .slick-header-column:nth(2)') + .first() + .trigger('mouseover') + .children('.slick-header-menu-button') + .invoke('show') + .click(); + + cy.get('#grid7-2 .slick-header-menu .slick-menu-command-list') + .should('be.visible') + .children('.slick-menu-item:nth-of-type(6)') + .children('.slick-menu-content') + .should('contain', 'Remove Filter') + .click(); + + cy.get('.slick-row').should('have.length.greaterThan', 1); + }); + + it('should Clear all Sorting', () => { + cy.get('#grid7-2') + .find('button.slick-grid-menu-button') + .trigger('click') + .click({ force: true }); + + cy.get(`.slick-grid-menu:visible`) + .find('.slick-menu-item:nth(1)') + .find('span') + .contains('Clear all Sorting') + .click(); + }); + + it('should hover over the "Column C" and click on "Sort Ascending"', () => { + cy.get('#grid7-2 .slick-header-column:nth(2)') + .first() + .trigger('mouseover') + .children('.slick-header-menu-button') + .click(); + + cy.get('#grid7-2 .slick-header-menu .slick-menu-command-list') + .should('be.visible') + .children('.slick-menu-item:nth-of-type(3)') + .children('.slick-menu-content') + .should('contain', 'Sort Ascending') + .click(); + }); + + it('should expect first few items of "Column C" to be negative numbers and be red', () => { + cy.get('#grid7-2 .slick-viewport-top.slick-viewport-left') + .scrollTo('top') + .wait(50); + + cy.get('#grid7-2 .slick-row') + .each(($row, index) => { + if (index > 10) { + return; // check only the first 10 rows is enough + } + cy.wrap($row).children('.slick-cell:nth(2)') + .each($cell => { + const numberValue = $cell.text(); + const htmlValue = $cell.html(); + expect(htmlValue).to.eq(`
${numberValue}
`); + }); + }); + }); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example08.cy.ts b/demos/vue/test/cypress/e2e/example08.cy.ts new file mode 100644 index 000000000..51f132cff --- /dev/null +++ b/demos/vue/test/cypress/e2e/example08.cy.ts @@ -0,0 +1,223 @@ +describe('Example 8 - Header Menu Plugin', () => { + const titles = ['Title', 'Duration', '% Complete', 'Start', 'Finish', 'Completed']; + + beforeEach(() => { + // create a console.log spy for later use + cy.window().then((win) => { + cy.spy(win.console, 'log'); + }); + }); + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example8`); + cy.get('h2').should('contain', 'Example 8: Header Menu Plugin'); + }); + + it('should have exact Column Titles in the grid', () => { + cy.get('#grid8') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(titles[index])); + }); + + it('should hover over the "Title" column and expect Sort & Hide commands to be disabled', () => { + cy.get('#grid8') + .find('.slick-header-column') + .first() + .trigger('mouseover') + .children('.slick-header-menu-button') + .should('be.hidden') + .invoke('show') + .click({ force: true }); + + cy.get('.slick-menu-item.slick-menu-item-disabled') + .contains('Help') + .should('exist'); + + cy.get('.slick-menu-item .slick-menu-content') + .contains('Hide Column') + .should('exist'); + + cy.get('[data-test=selected-locale]') + .click(); + }); + + it(`should be still be able to click on the Help command of 2nd column "Duration" and expect an alert`, () => { + const alertStub = cy.stub(); + cy.on('window:alert', alertStub); + + cy.get('#grid8') + .find('.slick-header-column:nth(1)') + .trigger('mouseover') + .children('.slick-header-menu-button') + .should('be.hidden') + .invoke('show') + .click({ force: true }); + + cy.get('.slick-menu-item.bold') + .find('.slick-menu-content.blue') + .contains('Help') + .click({ force: true }) + .then(() => expect(alertStub.getCall(0)).to.be.calledWith('Please help!!!')); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(1); + expect(win.console.log).to.be.calledWith('execute an action on Help'); + }); + }); + + it('should hover over "Duration" and execute "Sort Ascending" command and expect a sort icon', () => { + cy.get('#grid8') + .find('.slick-header-column:nth(1)') + .trigger('mouseover') + .children('.slick-header-menu-button') + .invoke('show') + .click({ force: true }); + + cy.get('.slick-menu-item .slick-menu-content') + .contains('Sort Ascending') + .click({ force: true }); + + cy.get('.slick-header-column:nth(1).slick-header-sortable.slick-header-column-sorted') + .find('.slick-sort-indicator.slick-sort-indicator-asc') + .should('exist'); + }); + + it('should hover over "% Complete" and not expect to find the Help menu', () => { + cy.get('#grid8') + .find('.slick-header-column:nth(2)') + .trigger('mouseover') + .children('.slick-header-menu-button') + .should('be.hidden') + .invoke('show') + .click({ force: true }); + + cy.get('.slick-header-menu .slick-menu-command-list') + .should('exist'); + + cy.get('.slick-menu-item .slick-menu-content') + .contains('Help') + .should('not.exist'); + }); + + it('should execute "Sort Descending" command from the menu left open and expect 2 sort icons afterward and "% Completed" to be descending with >80', () => { + cy.get('.slick-header-menu .slick-menu-command-list') + .should('exist'); + + cy.get('.slick-menu-item .slick-menu-content') + .contains('Sort Descending') + .click({ force: true }) + .wait(10); + + cy.get('.slick-header-column:nth(1).slick-header-sortable.slick-header-column-sorted') + .find('.slick-sort-indicator.slick-sort-indicator-asc') + .should('exist'); + + cy.get('.slick-header-column:nth(2).slick-header-sortable.slick-header-column-sorted') + .find('.slick-sort-indicator.slick-sort-indicator-desc') + .should('exist'); + + cy.get('#grid8') + .find('.slick-row .slick-cell:nth(1)') + .contains('0 days'); + + cy.get('#grid8') + .find('.slick-row .slick-cell:nth(2)') + .each($row => { + expect(+$row.text()).to.be.greaterThan(60); + }); + }); + + it('should hover over the "Completed" column and expect Help commands to be disabled', () => { + cy.get('#grid8') + .find('.slick-header-column:nth(5)') + .trigger('mouseover') + .children('.slick-header-menu-button') + .should('be.hidden') + .invoke('show') + .click({ force: true }); + + cy.get('.slick-menu-item.slick-menu-item-disabled') + .contains('Help') + .should('exist'); + }); + + it('should remain in the "Completed" column and execute "Hide Column" command and expect it gone from the grid', () => { + const newTitles = ['Title', 'Duration', '% Complete', 'Start', 'Finish']; + + cy.get('.slick-menu-item.slick-menu-item') + .contains('Hide Column') + .click({ force: true }); + + cy.get('#grid8') + .find('.slick-header-columns') + .children() + .should('have.length', 5) + .each(($child, index) => expect($child.text()).to.contain(newTitles[index])); + }); + + describe('with sub-menus', () => { + it(`should open Hello sub-menu and expect 3 options, then open Feedback->ContactUs sub-menus and expect previous Hello menu to no longer exists`, () => { + const subCommands1 = ['Hello World', 'Hello SlickGrid', `Let's play`]; + const subCommands2 = ['Request update from supplier', '', 'Contact Us']; + const subCommands2_1 = ['Email us', 'Chat with us', 'Book an appointment']; + + const stub = cy.stub(); + cy.on('window:alert', stub); + + cy.get('#grid8') + .find('.slick-header-column:nth(0)') + .trigger('mouseover') + .children('.slick-header-menu-button') + .should('be.hidden') + .invoke('show') + .click({ force: true }); + + cy.get('.slick-header-menu.slick-menu-level-0') + .find('.slick-menu-item.slick-menu-item') + .contains('Hello') + .should('exist') + .click(); + + cy.get('.slick-submenu').should('have.length', 1); + cy.get('.slick-header-menu.slick-menu-level-1.dropright') // right align + .should('exist') + .find('.slick-menu-item') + .each(($command, index) => expect($command.text()).to.contain(subCommands1[index])); + + // click different sub-menu + cy.get('.slick-header-menu.slick-menu-level-0') + .find('.slick-menu-item.slick-menu-item') + .contains('Feedback') + .should('exist') + .click(); + + cy.get('.slick-submenu').should('have.length', 1); + cy.get('.slick-header-menu.slick-menu-level-1') + .should('exist') + .find('.slick-menu-item') + .each(($command, index) => expect($command.text()).to.contain(subCommands2[index])); + + // click on Feedback->ContactUs + cy.get('.slick-header-menu.slick-menu-level-1.dropright') // right align + .find('.slick-menu-item.slick-menu-item') + .contains('Contact Us') + .should('exist') + .trigger('mouseover'); // mouseover or click should work + + cy.get('.slick-submenu').should('have.length', 2); + cy.get('.slick-header-menu.slick-menu-level-2.dropleft') // left align + .should('exist') + .find('.slick-menu-item') + .each(($command, index) => expect($command.text()).to.contain(subCommands2_1[index])); + + cy.get('.slick-header-menu.slick-menu-level-2') + .find('.slick-menu-item') + .contains('Chat with us') + .click() + .then(() => expect(stub.getCall(0)).to.be.calledWith('Command: contact-chat')); + + cy.get('.slick-submenu').should('have.length', 0); + }); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example09.cy.ts b/demos/vue/test/cypress/e2e/example09.cy.ts new file mode 100644 index 000000000..9993809d7 --- /dev/null +++ b/demos/vue/test/cypress/e2e/example09.cy.ts @@ -0,0 +1,483 @@ +describe('Example 9 - Grid Menu', () => { + const fullEnglishTitles = ['Title', 'Duration', '% Complete', 'Start', 'Finish', 'Completed']; + const fullFrenchTitles = ['Titre', 'Durรฉe', '% Achevรฉe', 'Dรฉbut', 'Fin', 'Terminรฉ']; + + beforeEach(() => { + // create a console.log spy for later use + cy.window().then((win) => { + cy.spy(win.console, 'log'); + }); + }); + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example9`); + cy.get('h2').should('contain', 'Example 9: Grid Menu Control'); + }); + + describe('use English locale', () => { + it('should have exact Column Titles in the grid', () => { + cy.get('#grid9') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullEnglishTitles[index])); + }); + + it('should open the Grid Menu and expect a title for "Custom Menus" and for "Columns"', () => { + cy.get('#grid9') + .find('button.slick-grid-menu-button') + .trigger('click') + .click({ force: true }); + + cy.get('.slick-menu-command-list') + .find('.slick-menu-title') + .contains('Custom Commands'); + + cy.get('.slick-grid-menu') + .find('.slick-menu-title') + .contains('Columns'); + + cy.get('#grid9') + .get('.slick-grid-menu:visible') + .find('.close') + .trigger('click') + .click({ force: true }); + }); + + it('should hover over the Title column and click on "Hide Column" command and remove 1st column from grid', () => { + const smallerTitleList = fullEnglishTitles.slice(1); + + cy.get('#grid9') + .find('.slick-header-column') + .first() + .trigger('mouseover') + .children('.slick-header-menu-button') + .should('be.hidden') + .invoke('show') + .trigger('click', { force: true }); + + cy.get('.slick-header-menu .slick-menu-command-list') + .should('be.visible') + .children('.slick-menu-item:nth-of-type(4)') + .children('.slick-menu-content') + .should('contain', 'Hide Column') + .click({ force: true }); + + cy.get('#grid9') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(smallerTitleList[index])); + }); + + it('should hide a column from the picker and then open the Grid Menu and expect the "Command 1" to NOT be usable', () => { + const alertStub = cy.stub(); + cy.on('window:alert', alertStub); + + cy.get('#grid9') + .find('button.slick-grid-menu-button') + .click({ force: true }); + + cy.get('.slick-menu-item.orange') + .find('.slick-menu-content') + .contains('Command 1') + .click() + .then(() => expect(alertStub.getCall(0)).to.be.null); + + cy.get('#grid9') + .get('.slick-grid-menu:visible') + .find('.close') + .click({ force: true }); + }); + + it('should type a filter and then open the Grid Menu and expect the "Command 2" to NOT be visible', () => { + const alertStub = cy.stub(); + cy.on('window:alert', alertStub); + + cy.get('input.search-filter.filter-duration') + .type('10'); + + cy.get('#grid9') + .find('button.slick-grid-menu-button') + .trigger('click') + .click({ force: true }); + + cy.get('.slick-menu-item.red') + .should('not.exist'); + }); + + it('should clear all filters and expect no filters in the grid', () => { + cy.get('#grid9') + .find('button.slick-grid-menu-button') + .trigger('click') + .click({ force: true }); + + cy.get('.slick-menu-item') + .find('.slick-menu-content') + .contains('Clear all Filters') + .click(); + + cy.get('input.search-filter.filter-duration') + .each(($elm) => expect($elm.text()).to.eq('')); + }); + + it('should clear the filters and then open the Grid Menu and expect the "Command 2" to now be visible', () => { + const alertStub = cy.stub(); + cy.on('window:alert', alertStub); + + cy.get('#grid9') + .find('button.slick-grid-menu-button') + .trigger('click') + .click({ force: true }); + + cy.get('.slick-menu-item.red') + .find('.slick-menu-content.italic') + .should('contain', 'Command 2'); + }); + + it('should click on the Grid Menu to show the Title as 1st column again', () => { + cy.get('#grid9') + .find('button.slick-grid-menu-button') + .trigger('click') + .click({ force: true }); + + cy.get('#grid9') + .get('.slick-grid-menu:visible') + .find('.slick-column-picker-list') + .children('li:nth-child(1)') + .children('label') + .should('contain', 'Title') + .click({ force: true }); + + cy.get('#grid9') + .get('.slick-grid-menu:visible') + .find('.close') + .trigger('click') + .click({ force: true }); + + cy.get('#grid9') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullEnglishTitles[index])); + }); + + it('should now expect the "Command 1" to be usable since all columns are visible', () => { + const alertStub = cy.stub(); + cy.on('window:alert', alertStub); + + cy.get('#grid9') + .find('button.slick-grid-menu-button') + .trigger('click') + .click({ force: true }); + + cy.get('.slick-menu-item.orange') + .find('.slick-menu-content') + .contains('Command 1') + .click() + .then(() => expect(alertStub.getCall(0)).to.be.calledWith('command1')); + }); + + it('should hover over the Title column and click on "Hide Column" command and remove 1st column from grid', () => { + const smallerTitleList = fullEnglishTitles.slice(1); + + cy.get('#grid9') + .find('.slick-header-column') + .first() + .trigger('mouseover') + .children('.slick-header-menu-button') + .should('be.hidden') + .invoke('show') + .trigger('click', { force: true }); + + cy.get('.slick-header-menu .slick-menu-command-list') + .should('be.visible') + .children('.slick-menu-item:nth-of-type(4)') + .children('.slick-menu-content') + .should('contain', 'Hide Column') + .click({ force: true }); + + cy.get('#grid9') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(smallerTitleList[index])); + }); + + it('should click on the External Grid Menu to show the Title as 1st column again', () => { + cy.get('[data-test=external-gridmenu]') + .trigger('click') + .click({ force: true }); + + cy.get('#grid9') + .get('.slick-grid-menu:visible') + .find('.slick-column-picker-list') + .children('li:nth-child(1)') + .children('label') + .should('contain', 'Title') + .click({ force: true }); + + cy.get('#grid9') + .get('.slick-grid-menu:visible') + .find('.close') + .trigger('click') + .click({ force: true }); + + cy.get('#grid9') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullEnglishTitles[index])); + }); + }); + + describe('switch to French language', () => { + it('should switch locale to French and have column header titles in French', () => { + cy.get('[data-test=language]') + .click({ force: true }); + + cy.get('[data-test=selected-locale]') + .should('contain', 'fr.json'); + + cy.get('#grid9') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullFrenchTitles[index])); + }); + + it('should hover over the Title column and click on "Cacher la colonne" command and remove 1st column from grid', () => { + const smallerTitleList = fullFrenchTitles.slice(1); + + cy.get('#grid9') + .find('.slick-header-column') + .first() + .trigger('mouseover') + .children('.slick-header-menu-button') + .should('be.hidden') + .invoke('show') + .trigger('click', { force: true }); + + cy.get('.slick-header-menu .slick-menu-command-list') + .should('be.visible') + .children('.slick-menu-item:nth-of-type(4)') + .children('.slick-menu-content') + .should('contain', 'Cacher la colonne') + .click({ force: true }); + + cy.get('#grid9') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(smallerTitleList[index])); + }); + + it('should click on the Grid Menu to show the Title as 1st column again', () => { + cy.get('#grid9') + .find('button.slick-grid-menu-button') + .trigger('click') + .click({ force: true }); + + cy.get('#grid9') + .get('.slick-grid-menu:visible') + .find('.slick-column-picker-list') + .children('li:nth-child(1)') + .children('label') + .should('contain', 'Titre') + .click({ force: true }); + + cy.get('#grid9') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullFrenchTitles[index])); + }); + + it('should hover over the Title column and click on "Hide Column" command and remove 1st column from grid', () => { + const smallerTitleList = fullFrenchTitles.slice(1); + + cy.get('#grid9') + .find('.slick-header-column') + .first() + .trigger('mouseover') + .children('.slick-header-menu-button') + .should('be.hidden') + .invoke('show') + .trigger('click', { force: true }); + + cy.get('.slick-header-menu .slick-menu-command-list') + .should('be.visible') + .children('.slick-menu-item:nth-of-type(4)') + .children('.slick-menu-content') + .should('contain', 'Cacher la colonne') + .click({ force: true }); + + cy.get('#grid9') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(smallerTitleList[index])); + }); + + it('should click on the External Grid Menu to show the Title as 1st column again', () => { + cy.get('[data-test=external-gridmenu]') + .trigger('click') + .click({ force: true }); + + cy.get('#grid9') + .get('.slick-grid-menu:visible') + .find('.slick-column-picker-list') + .children('li:nth-child(1)') + .children('label') + .should('contain', 'Titre') + .click({ force: true }); + + cy.get('#grid9') + .get('.slick-grid-menu:visible') + .find('.close') + .trigger('click') + .click({ force: true }); + + cy.get('#grid9') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullFrenchTitles[index])); + }); + }); + + describe('Grid Menu with sub-menus', () => { + it('should switch locale back to English', () => { + cy.get('[data-test=language]') + .click({ force: true }); + + cy.get('[data-test=selected-locale]') + .should('contain', 'en.json'); + + cy.get('#grid9') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullEnglishTitles[index])); + }); + + it('should be able to open Grid Menu and click on Export->Text and expect alert triggered with Text Export', () => { + const subCommands1 = ['Text', 'Excel']; + const stub = cy.stub(); + cy.on('window:alert', stub); + + cy.get('#grid9') + .find('button.slick-grid-menu-button') + .click({ force: true }); + + cy.get('.slick-grid-menu.slick-menu-level-0 .slick-menu-command-list') + .find('.slick-menu-item') + .contains('Exports') + .click(); + + cy.get('.slick-grid-menu.slick-menu-level-1 .slick-menu-command-list') + .should('exist') + .find('.slick-menu-item') + .each(($command, index) => expect($command.text()).to.contain(subCommands1[index])); + + cy.get('.slick-grid-menu.slick-menu-level-1 .slick-menu-command-list') + .find('.slick-menu-item') + .contains('Text (tab delimited)') + .click() + .then(() => expect(stub.getCall(0)).to.be.calledWith('Exporting as Text (tab delimited)')); + }); + + it('should be able to open Grid Menu and click on Export->Excel->xlsx and expect alert triggered with Excel (xlsx) Export', () => { + const subCommands1 = ['Text', 'Excel']; + const subCommands2 = ['Excel (csv)', 'Excel (xlsx)']; + const stub = cy.stub(); + cy.on('window:alert', stub); + + cy.get('#grid9') + .find('button.slick-grid-menu-button') + .click({ force: true }); + + cy.get('.slick-grid-menu.slick-menu-level-0 .slick-menu-command-list') + .find('.slick-menu-item') + .contains('Exports') + .click(); + + cy.get('.slick-grid-menu.slick-menu-level-1 .slick-menu-command-list') + .should('exist') + .find('.slick-menu-item') + .each(($command, index) => expect($command.text()).to.contain(subCommands1[index])); + + cy.get('.slick-submenu').should('have.length', 1); + cy.get('.slick-grid-menu.slick-menu-level-1 .slick-menu-command-list') + .find('.slick-menu-item') + .contains('Excel') + .click(); + + cy.get('.slick-grid-menu.slick-menu-level-2 .slick-menu-command-list').as('subMenuList2'); + + cy.get('@subMenuList2') + .find('.slick-menu-title') + .contains('available formats'); + + cy.get('@subMenuList2') + .should('exist') + .find('.slick-menu-item') + .each(($command, index) => expect($command.text()).to.contain(subCommands2[index])); + cy.get('.slick-submenu').should('have.length', 2); + + cy.get('.slick-grid-menu.slick-menu-level-2 .slick-menu-command-list') + .find('.slick-menu-item') + .contains('Excel (xlsx)') + .click() + .then(() => expect(stub.getCall(0)).to.be.calledWith('Exporting as Excel (xlsx)')); + cy.get('.slick-submenu').should('have.length', 0); + }); + + it('should open Export->Excel context sub-menu then open Feedback->ContactUs sub-menus and expect previous Export menu to no longer exists', () => { + const subCommands1 = ['Text', 'Excel']; + const subCommands2 = ['Request update from supplier', '', 'Contact Us']; + const subCommands2_1 = ['Email us', 'Chat with us', 'Book an appointment']; + + const stub = cy.stub(); + cy.on('window:alert', stub); + + cy.get('[data-test=external-gridmenu]') + .click(); + + cy.get('.slick-grid-menu.slick-menu-level-0 .slick-menu-command-list') + .find('.slick-menu-item') + .contains('Export') + .click(); + + cy.get('.slick-grid-menu.slick-menu-level-1 .slick-menu-command-list') + .should('exist') + .find('.slick-menu-item') + .each(($command, index) => expect($command.text()).to.contain(subCommands1[index])); + + // click different sub-menu + cy.get('.slick-grid-menu.slick-menu-level-0') + .find('.slick-menu-item') + .contains('Feedback') + .should('exist') + .trigger('mouseover'); // mouseover or click should work + + cy.get('.slick-submenu').should('have.length', 1); + cy.get('.slick-grid-menu.slick-menu-level-1') + .should('exist') + .find('.slick-menu-item') + .each(($command, index) => expect($command.text()).to.contain(subCommands2[index])); + + // click on Feedback->ContactUs + cy.get('.slick-grid-menu.slick-menu-level-1.dropright') // right align + .find('.slick-menu-item') + .contains('Contact Us') + .should('exist') + .click(); + + cy.get('.slick-submenu').should('have.length', 2); + cy.get('.slick-grid-menu.slick-menu-level-2.dropright') // right align + .should('exist') + .find('.slick-menu-item') + .each(($command, index) => expect($command.text()).to.eq(subCommands2_1[index])); + + cy.get('.slick-grid-menu.slick-menu-level-2'); + + cy.get('.slick-grid-menu.slick-menu-level-2 .slick-menu-command-list') + .find('.slick-menu-item') + .contains('Chat with us') + .click() + .then(() => expect(stub.getCall(0)).to.be.calledWith('Command: contact-chat')); + + cy.get('.slick-submenu').should('have.length', 0); + }); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example10.cy.ts b/demos/vue/test/cypress/e2e/example10.cy.ts new file mode 100644 index 000000000..06d6048e1 --- /dev/null +++ b/demos/vue/test/cypress/e2e/example10.cy.ts @@ -0,0 +1,594 @@ +describe('Example 10 - Multiple Grids with Row Selection', () => { + const titles = ['', 'Title', 'Duration (days)', '% Complete', 'Start', 'Finish', 'Effort Driven']; + + beforeEach(() => { + // create a console.log spy for later use + cy.window().then((win) => { + cy.spy(win.console, 'log'); + }); + }); + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example10`); + cy.get('h2').should('contain', 'Example 10: Multiple Grids with Row Selection'); + }); + + it('should have 2 grids of width of 800px and different height', () => { + cy.get('#slickGridContainer-grid1').as('grid1'); + cy.get('@grid1').should('have.css', 'width', '800px'); + + cy.get('@grid1') + .find('.slickgrid-container') + .should(($el) => expect(parseInt(`${$el.height()}`, 10)).to.eq(225)); + + cy.get('#slickGridContainer-grid2').as('grid2'); + cy.get('@grid2').should('have.css', 'width', '800px'); + + cy.get('@grid2') + .find('.slickgrid-container') + .should(($el) => expect(parseInt(`${$el.height()}`, 10)).to.eq(255)); + }); + + it('should have exact Titles on 1st grid', () => { + cy.get('#slickGridContainer-grid1') + .find('.slick-header-columns') + .children() + .each(($child, index) => { + expect($child.text()).to.eq(titles[index]); + }); + }); + + it('should have 1 rows (Task 3) pre-selected in 2nd grid on its first page but 5 rows selected in the entire dataset', () => { + cy.get('[data-test=grid2-selections]').should('contain', 'Task 3,Task 12,Task 13,Task 522'); + + cy.get('#grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 1); + }); + + it('should have 2 rows (Task 12,Task 13) selected in 2nd grid after typing in a search filter', () => { + cy.get('#grid2').find('.filter-title').type('Task 1'); + + cy.get('#grid2').find('.slick-row').should('not.have.length', 0); + + cy.get('[data-test=grid2-selections]').should('contain', ''); + + cy.get('#grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 2); + }); + + it('should make sure that first column is hidden from the Grid Menu (1st column definition has "excludeFromGridMenu" set) on 1st grid', () => { + cy.get('#grid1').find('button.slick-grid-menu-button').trigger('click').click(); + + cy.get('.slick-grid-menu') + .find('.slick-column-picker-list') + .children() + .each(($child, index) => { + if (index === 0) { + expect($child[0].className).to.eq('hidden'); + expect($child[0].offsetHeight).to.eq(0); + expect($child[0].offsetWidth).to.eq(0); + } + expect($child.text()).to.eq(titles[index]); + }); + }); + + it('should hide Title from the Grid Menu and expect 1 less column in the 1st grid', () => { + const newTitleList = ['', 'Duration (days)', '% Complete', 'Start', 'Finish', 'Effort Driven']; + + cy.get('#grid1') + .get('.slick-grid-menu:visible') + .find('.slick-column-picker-list') + .children('li:nth-child(2)') + .children('label') + .should('contain', 'Title') + .click(); + + cy.get('#grid1') + .find('.slick-header-columns') + .children() + .each(($child, index) => { + expect($child.text()).to.eq(newTitleList[index]); + }); + + cy.get('#grid1').get('.slick-grid-menu:visible').find('.close').trigger('click').click(); + }); + + it('should show the Title column again from the Column Picker in the 1st grid', () => { + cy.get('#grid1').find('.slick-header-column').first().trigger('mouseover').trigger('contextmenu').invoke('show'); + + cy.get('.slick-column-picker') + .find('.slick-column-picker-list') + .children() + .each(($child, index) => { + if (index === 0) { + expect($child[0].className).to.eq('hidden'); + expect($child[0].offsetHeight).to.eq(0); + expect($child[0].offsetWidth).to.eq(0); + } + expect($child.text()).to.eq(titles[index]); + }); + + cy.get('.slick-column-picker') + .find('.slick-column-picker-list') + .children('li:nth-child(2)') + .children('label') + .should('contain', 'Title') + .click(); + + cy.get('#grid1') + .find('.slick-header-columns') + .children() + .each(($child, index) => { + expect($child.text()).to.eq(titles[index]); + }); + + cy.get('#grid1').get('.slick-column-picker:visible').find('.close').trigger('click').click(); + }); + + describe('Pagination', () => { + it('should Clear all Filters on 2nd Grid', () => { + cy.get('#grid2').find('button.slick-grid-menu-button').trigger('click').click(); + + cy.get(`.slick-grid-menu:visible`).find('.slick-menu-item').first().find('span').contains('Clear all Filters').click(); + }); + + it('should have Pagination displayed and set on Grid1 and Grid2', () => { + cy.get('#slickGridContainer-grid1').as('grid1'); + cy.get('#slickGridContainer-grid2').as('grid2'); + + // 1st Grid + cy.get('@grid1') + .find('[data-test=page-number-input]') + .invoke('val') + .then((pageNumber) => expect(pageNumber).to.eq('2')); + + cy.get('@grid1').find('[data-test=page-count]').contains('99'); + + cy.get('@grid1').find('[data-test=item-from]').contains('6'); + + cy.get('@grid1').find('[data-test=item-to]').contains('10'); + + cy.get('@grid1').find('[data-test=total-items]').contains('495'); + + // 2nd Grid + cy.get('@grid2').find('[data-test=page-count]').contains('105'); + + cy.get('@grid2').find('[data-test=item-from]').contains('1'); + + cy.get('@grid2').find('[data-test=item-to]').contains('5'); + + cy.get('@grid2').find('[data-test=total-items]').contains('525'); + }); + + it('should change Page Number in Grid1 and expect the Pagination to have correct values', () => { + cy.get('#slickGridContainer-grid1').as('grid1'); + + cy.get('@grid1').find('[data-test=page-number-input]').clear().type('52').type('{enter}'); + + cy.get('@grid1').find('[data-test=page-count]').contains('99'); + + cy.get('@grid1').find('[data-test=item-from]').contains('256'); + + cy.get('@grid1').find('[data-test=item-to]').contains('260'); + + cy.get('@grid1').find('[data-test=total-items]').contains('495'); + }); + + it('should change Page Number and Page Size in Grid2 and expect the Pagination to have correct values', () => { + cy.get('#slickGridContainer-grid2').as('grid2'); + + cy.get('@grid2').find('[data-test=page-number-input]').clear().type('34').type('{enter}'); + + cy.get('@grid2').find('[data-test=page-count]').contains('105'); + + cy.get('@grid2').find('[data-test=item-from]').contains('166'); + + cy.get('@grid2').find('[data-test=item-to]').contains('170'); + + cy.get('@grid2').find('[data-test=total-items]').contains('525'); + + cy.get('@grid2').find('#items-per-page-label').select('75'); + + cy.get('@grid2').find('[data-test=page-count]').contains('7'); + + cy.get('@grid2').find('[data-test=item-from]').contains('1'); + + cy.get('@grid2').find('[data-test=item-to]').contains('75'); + }); + + it('should go back to Grid1 and expect the same value before changing Pagination of Grid2', () => { + cy.get('#slickGridContainer-grid1').as('grid1'); + + cy.get('@grid1').find('[data-test=page-count]').contains('99'); + + cy.get('@grid1').find('[data-test=item-from]').contains('256'); + + cy.get('@grid1').find('[data-test=item-to]').contains('260'); + + cy.get('@grid1').find('[data-test=total-items]').contains('495'); + }); + + it('should display page 0 of 0 with 0 items when applied filter returning an empty dataset', () => { + cy.get('#slickGridContainer-grid1').as('grid1'); + + cy.get('@grid1').find('.filter-title').type('000'); + + cy.get('.slick-empty-data-warning:visible').contains('No data to display.'); + + cy.get('@grid1').find('[data-test=page-count]').contains('0'); + + cy.get('@grid1').find('[data-test=item-from]').should('not.be.visible'); + + cy.get('@grid1').find('[data-test=item-to]').should('not.be.visible'); + + cy.get('@grid1').find('[data-test=total-items]').contains('0'); + }); + + it('should erase part of the filter to have "00" and expect 4 items in total with 1 page', () => { + cy.get('#slickGridContainer-grid1').as('grid1'); + + cy.get('@grid1').find('.filter-title').type('{backspace}'); + + cy.get('.slick-empty-data-warning').contains('No data to display.').should('not.be.visible'); + + cy.get('@grid1').find('[data-test=page-count]').contains('1'); + + cy.get('@grid1').find('[data-test=item-from]').contains('1'); + + cy.get('@grid1').find('[data-test=item-to]').contains('4'); + + cy.get('@grid1').find('[data-test=total-items]').contains('4'); + }); + + it('should also expect Grid2 to be unchanged (after changing Pagination in Grid1 in previous tests)', () => { + cy.get('#slickGridContainer-grid2').as('grid2'); + + cy.get('@grid2').find('[data-test=page-count]').contains('7'); + + cy.get('@grid2').find('[data-test=item-from]').contains('1'); + + cy.get('@grid2').find('[data-test=item-to]').contains('75'); + + cy.get('@grid2').find('[data-test=total-items]').contains('525'); + }); + + it('should have 4 rows (Task 3,Task 12,Task 13,Task 522) selected in the entire 2nd grid BUT only 1 selected in current Page 1', () => { + cy.get('#slickGridContainer-grid2').as('grid2'); + + cy.get('[data-test=grid2-selections]').should('contain', 'Task 3,Task 12,Task 13,Task 522'); + + cy.get('@grid2') + .find('[data-test=page-number-input]') + .invoke('val') + .then((pageNumber) => expect(pageNumber).to.eq('1')); + + cy.get('#grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 1); + }); + + it('should go to Page 3 of 2nd Grid and have 2 rows selected in that Page and also have 4 rows selected in the entire grid (Task 3,Task 12,Task 13,Task 522)', () => { + cy.get('#slickGridContainer-grid2').as('grid2'); + + cy.get('[data-test=grid2-selections]').should('contain', 'Task 3,Task 12,Task 13,Task 522'); + + cy.get('@grid2').find('#items-per-page-label').select('5'); + + cy.get('@grid2').find('[data-test=page-number-input]').clear().type('3').type('{enter}'); + + cy.get('@grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 2); + }); + + it('should go to last Page of 2nd Grid and have 1 rows selected in that Page and also have 4 rows selected in the entire grid (Task 3,Task 12,Task 13,Task 522)', () => { + cy.get('#slickGridContainer-grid2').as('grid2'); + + cy.get('@grid2').find('.icon-seek-end').click(); + + cy.get('[data-test=grid2-selections]').should('contain', 'Task 3,Task 12,Task 13,Task 522'); + + cy.get('@grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 1); + }); + + it(`should go to first Page of 2nd Grid and select another row (Task 1) in that Page, wich will now be (Task1,Task3) and now have 5 rows selected in the entire grid (Task 1,Task 3,Task 12,Task 13,Task 522)`, () => { + cy.get('#slickGridContainer-grid2').as('grid2'); + + cy.get('@grid2').find('.icon-seek-first').click().wait(10); + + cy.get('@grid2').find('.slick-row:nth(1) .slick-cell:nth(0) input[type=checkbox]').click({ force: true }); + + cy.get('[data-test=grid2-selections]').should('contain', 'Task 1,Task 3,Task 12,Task 13,Task 522'); + + cy.get('@grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 2); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(6); + // going to 1st page + expect(win.console.log).to.be.calledWith('Grid State changed:: ', { + newValues: { gridRowIndexes: [3], dataContextIds: [12, 13, 3, 522], filteredDataContextIds: [3, 12, 13, 522] }, + type: 'rowSelection', + }); + // after selecting 1st row + expect(win.console.log).to.be.calledWith('Grid State changed:: ', { + newValues: { gridRowIndexes: [1, 3], dataContextIds: [1, 12, 13, 3, 522], filteredDataContextIds: [1, 3, 12, 13, 522] }, + type: 'rowSelection', + }); + }); + }); + + it('should go back to Page 3 of 2nd Grid and have 2 rows selected in that Page and also retain 5 selected rows in the entire grid (Task 1,Task 3,Task 12,Task 13,Task 522)', () => { + cy.get('#slickGridContainer-grid2').as('grid2'); + + cy.get('[data-test=grid2-selections]').should('contain', 'Task 1,Task 3,Task 12,Task 13,Task 522'); + + cy.get('@grid2').find('#items-per-page-label').select('5'); + + cy.get('@grid2').find('[data-test=page-number-input]').clear().type('3').type('{enter}'); + + cy.get('@grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 2); + }); + + it('should go to last Page of 2nd Grid and still have 1 row selected in that Page and also retain 5 selected rows in the entire grid (Task 1,Task 3,Task 12,Task 13,Task 522)', () => { + cy.get('#slickGridContainer-grid2').as('grid2'); + + cy.get('@grid2').find('.icon-seek-end').click(); + + cy.get('[data-test=grid2-selections]').should('contain', 'Task 1,Task 3,Task 12,Task 13,Task 522'); + + cy.get('@grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 1); + }); + }); + + describe('Row Selection', () => { + it('should click on 3rd row and of the Grid1 and expect to see "Task 300" selected', () => { + cy.get('#slickGridContainer-grid1').as('grid1'); + + cy.get('@grid1'); + cy.get('.slick-row:nth(2) .slick-cell:nth(0) input[type=checkbox]').click({ force: true }); + + cy.get('[data-test=grid1-selections]').contains('Task 300'); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(2); + expect(win.console.log).to.be.calledWith('Grid State changed:: ', { + newValues: { gridRowIndexes: [2], dataContextIds: [300], filteredDataContextIds: [300] }, + type: 'rowSelection', + }); + }); + }); + + it('should remove the filter from Grid1', () => { + cy.get('#slickGridContainer-grid1').as('grid1'); + + cy.get('@grid1') + .find('.filter-title') + .type('{backspace}{backspace}') + .invoke('text') + .then((text) => { + expect(text.trim()).to.eq(''); + }); + }); + + it('should go to Page 61 of Grid1 and expect to find "Task 300" still be selected', () => { + cy.get('#slickGridContainer-grid1').as('grid1'); + + cy.get('@grid1').find('[data-test=page-number-input]').clear().type('61').type('{enter}'); + + cy.get('[data-test=grid1-selections]').contains('Task 300'); + + cy.get('.slick-cell.l0.r0.slick-cell-checkboxsel.selected').should('exist'); + + cy.get('[data-test=grid1-selections]').contains('Task 300'); + }); + + it('should go to a different page for next test to confirm that it will then go to page 1', () => { + cy.get('#slickGridContainer-grid2').as('grid2'); + + cy.get('@grid2').find('[data-test=page-number-input]').clear().type('22').type('{enter}'); + + cy.get('@grid2').find('[data-test=page-count]').contains('105'); + + cy.get('@grid2').find('[data-test=item-from]').contains('106'); + + cy.get('@grid2').find('[data-test=item-to]').contains('110'); + + cy.get('@grid2').find('[data-test=total-items]').contains('525'); + }); + + it('should have 2 rows (Task 3,Task 13) selected in 2nd grid after typing in a search filter (3)', () => { + cy.get('#slickGridContainer-grid2').as('grid2'); + + cy.get('@grid2').find('.filter-title').type('3').wait(100); + + cy.get('@grid2') + .find('[data-test=page-number-input]') + .invoke('val') + .then((pageNumber) => expect(pageNumber).to.eq('1')); + + cy.get('@grid2').find('.slick-row').should('not.have.length', 0); + + cy.get('[data-test=grid2-selections]').should('contain', 'Task 3,Task 13'); + + cy.get('@grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 2); + + cy.window().then((win) => { + expect(win.console.log).to.be.calledWith('Grid State changed:: ', { + newValues: { gridRowIndexes: [1, 0], dataContextIds: [1, 12, 13, 3, 522], filteredDataContextIds: [3, 13] }, + type: 'rowSelection', + }); + expect(win.console.log).to.be.calledWith('Grid State changed:: ', { + newValues: [ + { + columnId: 'title', + operator: 'Contains', + searchTerms: ['3'], + targetSelector: 'input.form-control.filter-title.search-filter.slick-filter.filled', + }, + ], + type: 'filter', + }); + }); + }); + + it('should remove filter from Grid2', () => { + cy.get('#slickGridContainer-grid2').as('grid2'); + + cy.get('@grid2').find('.filter-title').type('{backspace}'); + }); + }); + + describe('Remove Pagination', () => { + it('should remove Pagination and not expect any DOM elements of it', () => { + cy.get('[data-test=toggle-pagination-grid2]').click(); + + cy.get('#slickGridContainer-grid2 .slick-pagination').should('not.exist'); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(2); + expect(win.console.log).to.be.calledWith('Grid State changed:: ', { + newValues: { + gridRowIndexes: [1, 12, 13, 3, 522], + dataContextIds: [1, 12, 13, 3, 522], + filteredDataContextIds: [1, 3, 12, 13, 522], + }, + type: 'rowSelection', + }); + }); + }); + + it('should have 5 rows (Task 1,Task 3,Task 12,Task 13,Task 522) selected in the entire 2nd grid BUT only 2 shown in the DOM in the top portion of the grid (because SlickGrid uses virtual rendering)', () => { + cy.get('#slickGridContainer-grid2').as('grid2'); + + cy.get('[data-test=grid2-selections]').should('contain', 'Task 1,Task 3,Task 12,Task 13,Task 522'); + + cy.get('@grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 2); + }); + + it('should scroll to the bottom of 2nd Grid and still have 5 rows (Task 1,Task 3,Task 12,Task 13,Task 522) selected and find 2 row selected because we now have 2 rows that got rendered (first and last)', () => { + cy.get('#slickGridContainer-grid2').as('grid2'); + + cy.get('[data-test=grid2-selections]').should('contain', 'Task 1,Task 3,Task 12,Task 13,Task 522'); + + cy.get('@grid2').find('.slick-viewport-top.slick-viewport-left').scrollTo('bottom').wait(10); + + cy.get('@grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 2); + }); + + it('should have 2 rows (Task 3,Task 13) selected in 2nd grid after typing in a search filter (3)', () => { + cy.get('#slickGridContainer-grid2').as('grid2'); + + cy.get('@grid2').find('.filter-title').type('3'); + + cy.get('@grid2').find('.slick-viewport-top.slick-viewport-left').scrollTo('top').wait(10); + + cy.get('@grid2').find('.slick-row').should('not.have.length', 0); + + cy.wait(50); + + cy.get('[data-test=grid2-selections]').should('contain', 'Task 3,Task 13'); + + cy.get('@grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 2); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(4); + expect(win.console.log).to.be.calledWith('Grid State changed:: ', { + newValues: { gridRowIndexes: [1, 0], dataContextIds: [1, 12, 13, 3, 522], filteredDataContextIds: [3, 13] }, + type: 'rowSelection', + }); + expect(win.console.log).to.be.calledWith('Grid State changed:: ', { + newValues: [ + { + columnId: 'title', + operator: 'Contains', + searchTerms: ['3'], + targetSelector: 'input.form-control.filter-title.search-filter.slick-filter.filled', + }, + ], + type: 'filter', + }); + }); + }); + + it('should remove filter from Grid2', () => { + cy.get('#slickGridContainer-grid2').as('grid2'); + + cy.get('@grid2').find('.filter-title').type('{backspace}'); + }); + }); + + describe('Re-enable Pagination', () => { + it('should re-enable the Pagination and expect to see it show it again below the grid at Page 1', () => { + cy.get('#slickGridContainer-grid2').as('grid2'); + + cy.get('[data-test=toggle-pagination-grid2]').click(); + + cy.get('#slickGridContainer-grid2 .slick-pagination').should('exist'); + + cy.get('@grid2') + .find('[data-test=page-number-input]') + .invoke('val') + .then((pageNumber) => expect(pageNumber).to.eq('1')); + + cy.get('@grid2').find('[data-test=page-number-input]').click(); + + cy.get('@grid2').find('[data-test=page-count]').contains('105'); + + cy.get('@grid2').find('[data-test=item-from]').contains('1'); + + cy.get('@grid2').find('[data-test=item-to]').contains('5'); + + cy.get('@grid2').find('[data-test=total-items]').contains('525'); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(4); + expect(win.console.log).to.be.calledWith('Grid State changed:: ', { + newValues: { gridRowIndexes: [1, 3], dataContextIds: [1, 12, 13, 3, 522], filteredDataContextIds: [1, 3, 12, 13, 522] }, + type: 'rowSelection', + }); + expect(win.console.log).to.be.calledWith('Grid State changed:: ', { + newValues: { pageNumber: 1, pageSize: 5 }, + type: 'pagination', + }); + }); + }); + + it('should have 2 rows (Task 3,Task 13) selected in 2nd grid after typing in a search filter (3)', () => { + cy.get('#slickGridContainer-grid2').as('grid2'); + + cy.get('@grid2').find('.filter-title').type('3'); + + cy.get('@grid2').find('.slick-row').should('not.have.length', 0); + + cy.get('[data-test=grid2-selections]').should('contain', 'Task 3,Task 13'); + + cy.get('@grid2').find('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 2); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(4); + expect(win.console.log).to.be.calledWith('Grid State changed:: ', { + newValues: { gridRowIndexes: [1, 0], dataContextIds: [1, 12, 13, 3, 522], filteredDataContextIds: [3, 13] }, + type: 'rowSelection', + }); + expect(win.console.log).to.be.calledWith('Grid State changed:: ', { + newValues: [ + { + columnId: 'title', + operator: 'Contains', + searchTerms: ['3'], + targetSelector: 'input.form-control.filter-title.search-filter.slick-filter.filled', + }, + ], + type: 'filter', + }); + }); + + cy.get('@grid2') + .find('[data-test=page-number-input]') + .invoke('val') + .then((pageNumber) => expect(pageNumber).to.eq('1')); + + cy.get('@grid2').find('[data-test=page-count]').contains('3'); + + cy.get('@grid2').find('[data-test=item-from]').contains('1'); + + cy.get('@grid2').find('[data-test=item-to]').contains('5'); + + cy.get('@grid2').find('[data-test=total-items]').contains('179'); + }); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example11.cy.ts b/demos/vue/test/cypress/e2e/example11.cy.ts new file mode 100644 index 000000000..0d701ce0e --- /dev/null +++ b/demos/vue/test/cypress/e2e/example11.cy.ts @@ -0,0 +1,101 @@ +describe('Example 11 - Add / Update / Highlight a Datagrid Item', () => { + const GRID_ROW_HEIGHT = 35; + const fullTitles = ['', 'Title', 'Duration (days)', '% Complete', 'Start', 'Finish', 'Effort Driven']; + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example11`); + cy.get('h2').should('contain', 'Example 11: Add / Update / Highlight a Datagrid Item'); + }); + + it('should have exact column titles on 1st grid', () => { + cy.get('#slickGridContainer-grid11') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + }); + + it('should expect Task 0 to Task 4 on first 5 rows', () => { + const expectedTasks = ['Task 0', 'Task 1', 'Task 2', 'Task 3', 'Task 4']; + + cy.get('#grid11') + .find('.slick-row') + .each(($row, index) => { + if (index > expectedTasks.length - 1) { + return; + } + cy.wrap($row).children('.slick-cell:nth(1)') + .should('contain', expectedTasks[index]); + }); + }); + + it('should delete first row Task 0 from the grid', () => { + const expectedTasks = ['Task 1', 'Task 2', 'Task 3', 'Task 4']; + + cy.get('#grid11') + .find('.slick-row') + .first() + .children('.slick-cell:nth(0)') + .click(); + + cy.get('#grid11') + .find('.slick-row') + .each(($row, index) => { + if (index > expectedTasks.length - 1) { + return; + } + cy.wrap($row).children('.slick-cell:nth(1)') + .should('contain', expectedTasks[index]); + }); + }); + + it('should add 2 rows on the top of the grid', () => { + const expectedTasks = ['Task 1001', 'Task 1000', 'Task 1', 'Task 2', 'Task 3', 'Task 4']; + + cy.get('[data-test="add-new-item-top-btn"]') + .click().click(); + + cy.get('#grid11') + .find('.slick-row') + .each(($row, index) => { + if (index > expectedTasks.length - 1) { + return; + } + cy.wrap($row).children('.slick-cell:nth(1)') + .should('contain', expectedTasks[index]); + }); + }); + + it('should add 2 rows on the bottom of the grid', () => { + cy.get('[data-test="add-new-item-bottom-btn"]') + .click(); + + cy.get('#grid11') + .find('.slick-row') + .last() + .should('contain.text', 'Task 1002'); + }); + + it('should click on highlight "Duration over 40" and expect few rows being highlighted in purple', () => { + cy.get('[data-test="highlight-duration40-btn"]').click(); + + cy.get('.slick-row.duration-bg') + .should('have.length.greaterThan', 1); + }); + + it('should scroll to bottom and expect last row to be "Task 1002"', () => { + cy.get('[data-test="scroll-bottom-btn"]').click(); + + cy.get('#grid11') + .find('.slick-row') + .last() + .should('contain.text', 'Task 1002'); + }); + + it('should scroll to top and expect certain rows on top', () => { + cy.get('[data-test="scroll-top-btn"]').click(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 1001'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 100'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 1'); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example12.cy.ts b/demos/vue/test/cypress/e2e/example12.cy.ts new file mode 100644 index 000000000..d667c81ea --- /dev/null +++ b/demos/vue/test/cypress/e2e/example12.cy.ts @@ -0,0 +1,334 @@ +import { format } from '@formkit/tempo'; + +import { removeExtraSpaces } from '../plugins/utilities'; + +describe('Example 12: Localization (i18n)', () => { + const fullEnglishTitles = ['', 'Title', 'Description', 'Duration', 'Start', 'Finish', 'Completed', 'Completed']; + const fullFrenchTitles = ['', 'Titre', 'Description', 'Durรฉe', 'Dรฉbut', 'Fin', 'Terminรฉ', 'Terminรฉ']; + + beforeEach(() => { + cy.restoreLocalStorage(); + + // create a console.log spy for later use + cy.window().then((win) => { + cy.spy(win.console, 'log'); + }); + }); + + afterEach(() => { + cy.saveLocalStorage(); + }); + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example12`); + cy.get('h2') + .should('contain', 'Example 12: Localization (i18n)'); + }); + + describe('English Locale', () => { + it('should have exact English Column Titles in the grid', () => { + cy.get('#grid12') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullEnglishTitles[index])); + }); + + it('should have 0 row selection count shown in the grid left footer', () => { + cy.get('#slickGridContainer-grid12') + .find('.slick-custom-footer') + .find('div.left-footer') + .should($span => { + const text = removeExtraSpaces($span.text()); // remove all white spaces + expect(text).to.eq(`0 items selected`); + }); + }); + + it('should have some metrics shown in the grid right footer', () => { + cy.get('#slickGridContainer-grid12') + .find('.slick-custom-footer') + .find('.right-footer') + .should($span => { + const text = removeExtraSpaces($span.text()); // remove all white spaces + const dateFormatted = format(new Date(), 'YYYY-MM-DD hh:mm a'); + expect(text).to.eq(`Last Update ${dateFormatted} | 1500 of 1500 items`); + }); + }); + + it('should filter certain tasks with the word "ask 1" and expect filter to use contain/include text', () => { + const tasks = ['Task 1', 'Task 10', 'Task 11', 'Task 12']; + + cy.get('.grid-canvas') + .find('.slick-row') + .should('be.visible'); + + cy.get('input.filter-title') + .type('ask 1'); + + cy.get('#grid12') + .find('.slick-row') + .each(($row, index) => { + if (index > tasks.length - 1) { + return; + } + cy.wrap($row).children('.slick-cell:nth(1)') + .should('contain', tasks[index]); + }); + }); + }); + + describe('French locale', () => { + it('should reset filters and switch locale to French', () => { + cy.get('#grid12') + .find('button.slick-grid-menu-button') + .click(); + + cy.get(`.slick-grid-menu:visible`) + .find('.slick-menu-item') + .first() + .find('span') + .contains('Clear all Filters') + .click(); + + cy.get('[data-test=language-button]') + .click(); + + cy.get('[data-test=selected-locale]') + .should('contain', 'fr.json'); + }); + + it('should have French Column Titles in the grid after switching locale', () => { + cy.get('#grid12') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullFrenchTitles[index])); + }); + + it('should have 0 row selection count shown in the grid left footer', () => { + cy.get('#slickGridContainer-grid12') + .find('.slick-custom-footer') + .find('div.left-footer') + .should($span => { + const text = removeExtraSpaces($span.text()); // remove all white spaces + expect(text).to.eq(`0 รฉlรฉments sรฉlectionnรฉs`); + }); + }); + + it('should have some metrics shown in the grid right footer', () => { + cy.get('#slickGridContainer-grid12') + .find('.slick-custom-footer') + .find('.right-footer') + .should($span => { + const text = removeExtraSpaces($span.text()); // remove all white spaces + const dateFormatted = format(new Date(), 'YYYY-MM-DD hh:mm a'); + expect(text).to.eq(`Derniรจre mise ร  jour ${dateFormatted} | 1500 de 1500 รฉlรฉments`); + }); + }); + + it('should filter certain tasks', () => { + const tasks = ['Tรขche 1', 'Tรขche 10', 'Tรขche 11', 'Tรขche 12']; + + cy.get('.grid-canvas') + .find('.slick-row') + .should('be.visible'); + + cy.get('input.filter-title') + .type('รขche 1'); + + cy.get('#grid12') + .find('.slick-row') + .each(($row, index) => { + if (index > tasks.length - 1) { + return; + } + cy.wrap($row).children('.slick-cell:nth(1)') + .should('contain', tasks[index]); + }); + }); + + it('should reset filters before filtering duration', () => { + cy.get('#grid12') + .find('button.slick-grid-menu-button') + .click(); + + cy.get(`.slick-grid-menu:visible`) + .find('.slick-menu-item') + .first() + .find('span') + .contains('Supprimer tous les filtres') + .click(); + }); + + it('should filter duration with slider filter', () => { + cy.get('.filter-duration input[type=range]').as('range') + .invoke('val', 30) + .trigger('change', { force: true }); + + cy.wait(10); + + cy.get('#grid12') + .find('.slick-row') + .each(($row, index) => { + let fullCellWidth; + + // only checks first 5 rows + if (index > 5) { + return; + } + + // get full cell width of the first cell, then return + cy.wrap($row).children('.slick-cell:nth(3)') + .first() + .then(($cell) => fullCellWidth = $cell.width()); + + + cy.wrap($row) + .children('.slick-cell:nth(3)') + .children() + .should('not.have.css', 'background-color', 'rgb(255, 0, 0)') + .should(($el) => { + // calculate 25% and expect the element width to be about the calculated size with a (+/-)1px precision + const expectedWidth = (fullCellWidth * .30); + expect($el.width() + 1).greaterThan(expectedWidth); + }); + }); + }); + }); + + describe('Row Selection', () => { + it('should switch locale back to English and reset all Filters', () => { + cy.get('[data-test=language-button]') + .click(); + + cy.get('[data-test=selected-locale]') + .should('contain', 'en.json'); + + cy.get('#grid12') + .find('button.slick-grid-menu-button') + .click(); + + cy.get(`.slick-grid-menu:visible`) + .find('.slick-menu-item') + .first() + .find('span') + .contains('Clear all Filters') + .click(); + }); + + it('should hover over the Title column and click on "Sort Descending" command', () => { + cy.get('#slickGridContainer-grid12') + .find('.slick-header-column:nth(1)') + .trigger('mouseover') + .children('.slick-header-menu-button') + .should('be.hidden') + .invoke('show') + .click(); + + cy.get('.slick-header-menu .slick-menu-command-list') + .should('be.visible') + .children('.slick-menu-item:nth-of-type(4)') + .children('.slick-menu-content') + .should('contain', 'Sort Descending') + .click(); + + cy.get('.slick-row') + .children('.slick-cell:nth(1)') + .first() + .should('contain', 'Task 1499'); + }); + + it('should select the row with "Task 1497" and expect the Grid State to be called with it in the console', () => { + cy.get('#slickGridContainer-grid12').as('grid12'); + + cy.get('#grid12') + .contains('Task 1497') + .parent() + .children('.slick-cell-checkboxsel') + .find('input[type=checkbox]') + .click({ force: true }); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(2); + expect(win.console.log).to.be.calledWith('Grid State changed:: ', { newValues: { gridRowIndexes: [2], dataContextIds: [1497], filteredDataContextIds: [1497] }, type: 'rowSelection' }); + }); + }); + + it('should scroll to bottom of the grid then select "Task 4"', () => { + cy.get('#slickGridContainer-grid12').as('grid12'); + + cy.get('@grid12') + .find('.slick-viewport-top.slick-viewport-left') + .scrollTo('bottom') + .wait(10); + + cy.get('#grid12') + .contains('Task 4') + .parent() + .children('.slick-cell-checkboxsel') + .find('input[type=checkbox]') + .click({ force: true }); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(2); + expect(win.console.log).to.be.calledWith('Grid State changed:: ', { newValues: { gridRowIndexes: [1495, 2], dataContextIds: [1497, 4], filteredDataContextIds: [1497, 4] }, type: 'rowSelection' }); + }); + }); + + it('should filter the Tasks column with number 4 and expect only "Task 4" visible in the grid', () => { + cy.get('#slickGridContainer-grid12').as('grid12'); + + cy.get('.grid-canvas') + .find('.slick-row') + .should('be.visible'); + + cy.get('input.filter-title') + .type('4'); + + cy.get('@grid12') + .find('.slick-row') + .children() + .filter('.slick-cell-checkboxsel.selected') + .should('have.length', 1); + + cy.get('@grid12') + .find('.slick-row') + .children() + .filter('.slick-cell.selected:nth(1)') + .contains('Task 4'); + }); + + it('should scroll back to the top and expect to see "Task 1497" still selected', () => { + cy.get('#slickGridContainer-grid12').as('grid12'); + + cy.get('.grid-canvas') + .find('.slick-row') + .should('be.visible'); + + cy.get('@grid12') + .find('.slick-viewport-top.slick-viewport-left') + .scrollTo('top') + .wait(10); + + cy.get('@grid12') + .find('.slick-row') + .children() + .filter('.slick-cell-checkboxsel.selected') + .should('have.length', 1); + + cy.get('@grid12') + .find('.slick-row') + .children() + .filter('.slick-cell.selected:nth(1)') + .contains('Task 1497'); + }); + + it('should have 2 row selection count shown in the grid left footer', () => { + cy.get('#slickGridContainer-grid12') + .find('.slick-custom-footer') + .find('div.left-footer') + .should($span => { + const text = removeExtraSpaces($span.text()); // remove all white spaces + expect(text).to.eq(`2 items selected`); + }); + }); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example13.cy.ts b/demos/vue/test/cypress/e2e/example13.cy.ts new file mode 100644 index 000000000..52e8f4258 --- /dev/null +++ b/demos/vue/test/cypress/e2e/example13.cy.ts @@ -0,0 +1,223 @@ +describe('Example 13 - Grouping & Aggregators', () => { + const fullTitles = ['Id Click me', 'Title', 'Duration', '% Complete', 'Start', 'Finish', 'Cost', 'Effort Driven']; + const GRID_ROW_HEIGHT = 35; + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example13`); + cy.get('h2').should('contain', 'Example 13: Grouping & Aggregators'); + }); + + it('should have exact column titles on 1st grid', () => { + cy.get('#grid13') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + }); + + describe('Grouping Tests', () => { + it('should "Group by Duration & sort groups by value" then Collapse All and expect only group titles', () => { + cy.get('[data-test="add-50k-rows-btn"]').click(); + cy.get('[data-test="group-duration-sort-value-btn"]').click(); + cy.get('[data-test="collapse-all-btn"]').click(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-toggle.collapsed`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Duration: 0'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Duration: 1'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Duration: 2'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Duration: 3'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Duration: 4'); + }); + + it('should click on Expand All columns and expect 1st row as grouping title and 2nd row as a regular row', () => { + cy.get('[data-test="add-50k-rows-btn"]').click(); + cy.get('[data-test="group-duration-sort-value-btn"]').click(); + cy.get('[data-test="expand-all-btn"]').click(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-toggle.expanded`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Duration: 0'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Task'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2)`).should('contain', '0'); + }); + + it('should "Group by Duration then Effort-Driven" and expect 1st row to be expanded, 2nd row to be collapsed and 3rd row to have group totals', () => { + cy.get('[data-test="group-duration-effort-btn"]').click(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"].slick-group-level-0 > .slick-cell:nth(0) .slick-group-toggle.expanded`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"].slick-group-level-0 > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Duration: 0'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"].slick-group-level-1 .slick-group-toggle.collapsed`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"].slick-group-level-1 .slick-group-title`).should('contain', 'Effort-Driven: False'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"].slick-group-level-1 .slick-group-toggle.collapsed`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"].slick-group-level-1 .slick-group-title`).should('contain', 'Effort-Driven: True'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"].slick-group-totals.slick-group-level-0 .slick-cell:nth(2)`).should('contain', 'Total: 0'); + }); + + it('should "Group by Duration then Effort-Driven then Percent" and expect fist 2 rows to be expanded, 3rd row to be collapsed then 4th row to have group total', () => { + cy.get('[data-test="group-duration-effort-percent-btn"]').click(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"].slick-group-level-0 > .slick-cell:nth(0) .slick-group-toggle.expanded`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"].slick-group-level-0 > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Duration: 0'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"].slick-group-level-1 .slick-group-toggle.expanded`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"].slick-group-level-1 .slick-group-title`).should('contain', 'Effort-Driven: False'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"].slick-group-level-2 .slick-group-toggle.collapsed`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"].slick-group-level-2 .slick-group-title`).contains(/^% Complete: [0-9]/); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"].slick-group-totals.slick-group-level-2 .slick-cell:nth(3)`).contains(/^Avg: [0-9]%$/); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"].slick-group-totals.slick-group-level-2`) + .find('.slick-cell:nth(3)').contains('Avg: '); + }); + }); + + describe('Diverse Input Text Filters with multiple symbol variances', () => { + it('should clear all Groupings', () => { + cy.get('[data-test="clear-grouping-btn"]').click(); + }); + + it('should return 500 rows using "Ta*33" (starts with "Ta" + ends with 33)', () => { + cy.get('.search-filter.filter-title') + .clear() + .type('Ta*3'); + + cy.get('.item-count') + .should('contain', 5000); + + cy.get('.search-filter.filter-title') + .clear() + .type('Ta*33'); + + cy.get('.item-count') + .should('contain', 500); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 33'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 133'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 233'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 333'); + }); + + it('should return 40000 rows using "Ta*" (starts with "Ta")', () => { + cy.get('.search-filter.filter-title') + .clear() + .type('Ta*'); + + cy.get('.item-count') + .should('contain', 50000); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 0'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 1'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 2'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 3'); + }); + + it('should return 500 rows using "*11" (ends with "11")', () => { + cy.get('.search-filter.filter-title') + .clear() + .type('*11'); + + cy.get('.item-count') + .should('contain', 500); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 1'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 11'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 21'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 31'); + }); + + it('should return 497 rows using ">222" (greater than 222)', () => { + cy.get('.search-filter.filter-sel') + .clear() + .type('>222'); + + cy.get('.item-count') + .should('contain', 497); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 311'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 411'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 511'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 611'); + }); + + it('should return 499 rows using "<>311" (not equal to 311)', () => { + cy.get('.search-filter.filter-sel') + .clear() + .type('<>311'); + + cy.get('.item-count') + .should('contain', 499); + + cy.get('.search-filter.filter-sel') + .clear() + .type('!=311'); + + cy.get('.item-count') + .should('contain', 499); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 11'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 111'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 211'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 411'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 511'); + }); + + it('should return 1 rows using "=311" or "==311" (equal to 311)', () => { + cy.get('.search-filter.filter-sel') + .clear() + .type('=311'); + + cy.get('.item-count') + .should('contain', 1); + + cy.get('.search-filter.filter-sel') + .clear() + .type('==311'); + + cy.get('.item-count') + .should('contain', 1); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Task 311'); + }); + }); + + describe('Column Header with HTML Elements', () => { + it('should trigger an alert when clicking on the 1st column button inside its header', () => { + const stub = cy.stub(); + cy.on('window:alert', stub); + + cy.get('button[data-test=col1-hello-btn]') + .click({ force: true }) + .then(() => expect(stub.getCall(0)).to.be.calledWith('Hello World')); + }); + + it('should open Column Picker and have a "Custom Label" as the 1st column label', () => { + cy.get('#grid13') + .find('.slick-header-column') + .first() + .trigger('mouseover') + .trigger('contextmenu') + .invoke('show'); + + cy.get('.slick-column-picker') + .find('.slick-column-picker-list li:nth-child(1) .checkbox-label') + .should('have.text', 'Custom Label'); + }); + + it('should open Grid Menu and have a "Custom Label" as the 1st column label', () => { + cy.get('#grid13') + .find('button.slick-grid-menu-button') + .trigger('click') + .click({ force: true }); + + cy.get(`.slick-grid-menu:visible`) + .find('.slick-column-picker-list li:nth-child(1) .checkbox-label') + .should('have.text', 'Custom Label'); + + cy.get('[data-dismiss="slick-grid-menu"]') + .click(); + }); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example14.cy.ts b/demos/vue/test/cypress/e2e/example14.cy.ts new file mode 100644 index 000000000..6b57efaca --- /dev/null +++ b/demos/vue/test/cypress/e2e/example14.cy.ts @@ -0,0 +1,119 @@ +describe('Example 14 - Column Span & Header Grouping', () => { + // NOTE: everywhere there's a * 2 is because we have a top+bottom (frozen rows) containers even after Unfreeze Columns/Rows + const fullPreTitles = ['', 'Common Factor', 'Period', 'Analysis']; + const fullTitles = ['#', 'Title', 'Duration', 'Start', 'Finish', '% Complete', 'Effort Driven']; + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example14`); + cy.get('h2').should('contain', 'Example 14: Column Span & Header Grouping'); + }); + + it('should have exact Column Pre-Header & Column Header Titles in the grid', () => { + cy.get('#grid2') + .find('.slick-header-columns:nth(0)') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullPreTitles[index])); + + cy.get('#grid2') + .find('.slick-header-columns:nth(1)') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + }); + + it('should have a frozen grid on page load with 3 columns on the left and 4 columns on the right', () => { + cy.get('#grid2').find('[style="top: 0px;"]').should('have.length', 2); + cy.get('#grid2').find('.grid-canvas-left > [style="top: 0px;"]').children().should('have.length', 3); + cy.get('#grid2').find('.grid-canvas-right > [style="top: 0px;"]').children().should('have.length', 4); + + cy.get('#grid2').find('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(0)').should('contain', '0'); + cy.get('#grid2').find('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(1)').should('contain', 'Task 0'); + cy.get('#grid2').find('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(2)').should('contain', '5 days'); + + cy.get('#grid2').find('.grid-canvas-right > [style="top: 0px;"] > .slick-cell:nth(0)').should('contain', '01/01/2009'); + cy.get('#grid2').find('.grid-canvas-right > [style="top: 0px;"] > .slick-cell:nth(1)').should('contain', '01/05/2009'); + }); + + it('should have exact Column Pre-Header & Column Header Titles in the grid', () => { + cy.get('#grid2') + .find('.slick-header-columns:nth(0)') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullPreTitles[index])); + + cy.get('#grid2') + .find('.slick-header-columns:nth(1)') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + }); + + it('should click on the "Remove Frozen Columns" button to switch to a regular grid without frozen columns and expect 7 columns on the left container', () => { + cy.contains('Remove Frozen Columns') + .click({ force: true }); + + cy.get('#grid2').find('[style="top: 0px;"]').should('have.length', 1); + cy.get('#grid2').find('.grid-canvas-left > [style="top: 0px;"]').children().should('have.length', 7); + + cy.get('#grid2').find('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(0)').should('contain', '0'); + cy.get('#grid2').find('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(1)').should('contain', 'Task 0'); + cy.get('#grid2').find('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(2)').should('contain', '5 days'); + cy.get('#grid2').find('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(3)').should('contain', '01/01/2009'); + cy.get('#grid2').find('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(4)').should('contain', '01/05/2009'); + }); + + it('should have exact Column Pre-Header & Column Header Titles in the grid', () => { + cy.get('#grid2') + .find('.slick-header-columns:nth(0)') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullPreTitles[index])); + + cy.get('#grid2') + .find('.slick-header-columns:nth(1)') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + }); + + it('should click on the "Set 3 Frozen Columns" button to switch frozen columns grid and expect 3 frozen columns on the left and 4 columns on the right', () => { + cy.contains('Set 3 Frozen Columns') + .click({ force: true }); + + cy.get('#grid2').find('[style="top: 0px;"]').should('have.length', 2); + cy.get('#grid2').find('.grid-canvas-left > [style="top: 0px;"]').children().should('have.length', 3); + cy.get('#grid2').find('.grid-canvas-right > [style="top: 0px;"]').children().should('have.length', 4); + + cy.get('#grid2').find('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(0)').should('contain', '0'); + cy.get('#grid2').find('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(1)').should('contain', 'Task 0'); + cy.get('#grid2').find('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(2)').should('contain', '5 days'); + + cy.get('#grid2').find('.grid-canvas-right > [style="top: 0px;"] > .slick-cell:nth(0)').should('contain', '01/01/2009'); + cy.get('#grid2').find('.grid-canvas-right > [style="top: 0px;"] > .slick-cell:nth(1)').should('contain', '01/05/2009'); + }); + + it('should have exact Column Pre-Header & Column Header Titles in the grid', () => { + cy.get('#grid2') + .find('.slick-header-columns:nth(0)') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullPreTitles[index])); + + cy.get('#grid2') + .find('.slick-header-columns:nth(1)') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + }); + + it('should click on the Grid Menu command "Unfreeze Columns/Rows" to switch to a regular grid without frozen columns and expect 7 columns on the left container', () => { + cy.get('#grid2') + .find('button.slick-grid-menu-button') + .click({ force: true }); + + cy.contains('Unfreeze Columns/Rows') + .click({ force: true }); + + cy.get('#grid2').find('[style="top: 0px;"]').should('have.length', 1); + cy.get('#grid2').find('.grid-canvas-left > [style="top: 0px;"]').children().should('have.length', 7); + + cy.get('#grid2').find('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(0)').should('contain', '0'); + cy.get('#grid2').find('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(1)').should('contain', 'Task 0'); + cy.get('#grid2').find('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(2)').should('contain', '5 days'); + cy.get('#grid2').find('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(3)').should('contain', '01/01/2009'); + cy.get('#grid2').find('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(4)').should('contain', '01/05/2009'); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example15.cy.ts b/demos/vue/test/cypress/e2e/example15.cy.ts new file mode 100644 index 000000000..7e4c3a6e2 --- /dev/null +++ b/demos/vue/test/cypress/e2e/example15.cy.ts @@ -0,0 +1,721 @@ +import { format } from '@formkit/tempo'; + +describe('Example 15: Grid State & Presets using Local Storage', () => { + const GRID_ROW_HEIGHT = 35; + const fullEnglishTitles = ['', 'Title', 'Description', 'Duration', '% Complete', 'Start', 'Completed']; + // const fullFrenchTitles = ['', 'Titre', 'Description', 'Durรฉe', '% Achevรฉe', 'Dรฉbut', 'Terminรฉ']; + + beforeEach(() => { + cy.restoreLocalStorage(); + }); + + afterEach(() => { + cy.saveLocalStorage(); + }); + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example15`); + cy.get('h2').should('contain', 'Example 15: Grid State & Presets using Local Storage'); + + cy.clearLocalStorage(); + cy.get('[data-test=reset-button]').click(); + }); + + it('should reload the page', () => { + cy.reload().wait(50); + }); + + it('should have exact Column Titles in the grid', () => { + cy.get('#grid15') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullEnglishTitles[index])); + }); + + it('should have Pagination displayed with default values', () => { + cy.get('#slickGridContainer-grid15').as('grid15'); + + // 1st Grid + cy.get('@grid15') + .find('[data-test=page-number-input]') + .invoke('val') + .then(pageNumber => expect(pageNumber).to.eq('1')); + + cy.get('@grid15') + .find('[data-test=page-count]') + .contains('20'); + + cy.get('@grid15') + .find('[data-test=item-from]') + .contains('1'); + + cy.get('@grid15') + .find('[data-test=item-to]') + .contains('25'); + + cy.get('@grid15') + .find('[data-test=total-items]') + .contains('500'); + }); + + it('should drag "Title" column to 3rd position in the grid', () => { + const expectedTitles = ['', 'Description', 'Duration', 'Title', '% Complete', 'Start', 'Completed']; + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(1)') + .contains('Title') + .drag('.slick-header-column:nth(3)'); + + cy.get('.slick-header-column:nth(3)').contains('Title'); + + cy.get('#grid15') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(expectedTitles[index])); + }); + + // -- + // Cypress does not yet implement the .hover() method and this test won't work until then + // xit('should resize "Title" column and make it wider', () => { + // cy.get('#grid15 .slick-viewport-top.slick-viewport-left') + // .scrollTo('left') + // .wait(50); + + // cy.get('.slick-header-columns') + // .children('.slick-header-column:nth(3)') + // .should('contain', 'Title'); + + // cy.get('.slick-header-columns') + // .children('.slick-header-column:nth(3)') + // .find('.slick-resizable-handle') + // .trigger('mouseover', -2, 50, { which: 1, force: true }) + // .should('be.visible') + // .invoke('show') + // .hover() + // .trigger('mousedown', -2, 50, { which: 1, force: true }); + + // cy.get('.slick-header-columns') + // .children('.slick-header-column:nth(5)') + // .trigger('mousemove', 'bottomLeft') + // .trigger('mouseup', 'bottomLeft', { force: true }); + // }); + + it('should hide the "Start" column from the Column Picker', () => { + const expectedTitles = ['', 'Description', 'Duration', 'Title', '% Complete', 'Start', 'Completed']; + + cy.get('#grid15') + .find('.slick-header-column') + .first() + .trigger('mouseover') + .trigger('contextmenu') + .invoke('show'); + + cy.get('.slick-column-picker') + .find('.slick-column-picker-list') + .children() + .each(($child, index) => { + if (index === 0) { + expect($child[0].className).to.eq('hidden'); + expect($child[0].offsetHeight).to.eq(0); + expect($child[0].offsetWidth).to.eq(0); + } + + expect($child.text()).to.eq(expectedTitles[index]); + }); + + cy.get('.slick-column-picker') + .find('.slick-column-picker-list') + .children('li:nth-child(6)') + .children('label') + .should('contain', 'Start') + .click(); + + cy.get('.slick-column-picker:visible') + .find('.close') + .trigger('click') + .click(); + }); + + it('should filter certain tasks', () => { + cy.get('.grid-canvas') + .find('.slick-row') + .should('be.visible'); + + cy.get('.filter-title input') + .type('Task 1'); + }); + + it('should click on "Title" column to sort it Ascending', () => { + const expectedTasks = ['Task 1', 'Task 10', 'Task 100', 'Task 101']; + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(3)') + .click(); + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(3)') + .find('.slick-sort-indicator.slick-sort-indicator-asc') + .should('be.visible'); + + cy.get('#grid15') + .find('.slick-row') + .each(($row, index) => { + if (index > expectedTasks.length - 1) { + return; + } + cy.wrap($row).children('.slick-cell:nth(3)') + .should('contain', expectedTasks[index]); + }); + }); + + it('should hover over the "Duration" column click on "Sort Descending" command', () => { + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(2)') + .trigger('mouseover') + .children('.slick-header-menu-button') + .should('be.hidden') + .invoke('show') + .click(); + + cy.get('.slick-header-menu .slick-menu-command-list') + .should('be.visible') + .children('.slick-menu-item:nth-of-type(5)') + .children('.slick-menu-content') + .should('contain', 'Sort Descending') + .click(); + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(2)') + .find('.slick-sort-indicator.slick-sort-indicator-desc') + .should('be.visible'); + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(2)') + .find('.slick-sort-indicator-numbered') + .should('be.visible') + .should('contain', '2'); + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(3)') + .find('.slick-sort-indicator-numbered') + .should('be.visible') + .should('contain', '1'); + }); + + it('should select row (Task 105)', () => { + cy.get('#grid15') + .contains('Task 105') + .parent() + .children('.slick-cell-checkboxsel') + .find('input[type=checkbox]') + .click({ force: true }); + }); + + it('should change Page Size and Page Number then expect the Pagination to have correct values', () => { + const expectedTasks = ['Task 135', 'Task 136', 'Task 137', 'Task 138', 'Task 139', 'Task 14']; + + cy.get('#slickGridContainer-grid15').as('grid15'); + + cy.get('@grid15') + .find('#items-per-page-label').select('20'); + + cy.get('@grid15'); + cy.get('.icon-seek-next').click().click(); + + cy.wait(100); + + cy.get('@grid15') + .find('[data-test=page-number-input]') + .invoke('val') + .then(pageNumber => expect(pageNumber).to.eq('3')); + + cy.get('@grid15') + .find('[data-test=page-count]') + .contains('6'); + + cy.get('@grid15') + .find('[data-test=item-from]') + .contains('41'); + + cy.get('@grid15') + .find('[data-test=item-to]') + .contains('60'); + + cy.get('@grid15') + .find('[data-test=total-items]') + .contains('111'); + + cy.get('@grid15') + .find('.slick-row') + .each(($row, index) => { + if (index > expectedTasks.length - 1) { + return; + } + cy.wrap($row).children('.slick-cell:nth(3)') + .should('contain', expectedTasks[index]); + }); + }); + + it('should select row (Task 144)', () => { + cy.get('#grid15') + .contains('Task 144') + .parent() + .children('.slick-cell-checkboxsel') + .find('input[type=checkbox]') + .click({ force: true }); + }); + + it('should reload the page', () => { + cy.reload().wait(50); + }); + + it('should expect the same Grid State to persist after the page got reloaded', () => { + const expectedTitles = ['', 'Description', 'Duration', 'Title', '% Complete', 'Completed']; + + cy.get('#grid15') + .find('.grid-canvas') + .find('.slick-row') + .should('be.visible'); + + cy.get('#grid15') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.find('.slick-column-name').text()).to.eq(expectedTitles[index])); + }); + + it('should expect the same Pagination to persist after reload', () => { + const expectedTasks = ['Task 135', 'Task 136', 'Task 137', 'Task 138', 'Task 139', 'Task 14']; + + cy.get('#slickGridContainer-grid15').as('grid15'); + + cy.get('@grid15') + .find('[data-test=page-number-input]') + .invoke('val') + .then(pageNumber => expect(pageNumber).to.eq('3')); + + cy.get('@grid15') + .find('[data-test=page-count]') + .contains('6'); + + cy.get('@grid15') + .find('[data-test=item-from]') + .contains('41'); + + cy.get('@grid15') + .find('[data-test=item-to]') + .contains('60'); + + cy.get('@grid15') + .find('[data-test=total-items]') + .contains('111'); + + cy.get('@grid15') + .find('.slick-row') + .each(($row, index) => { + if (index > expectedTasks.length - 1) { + return; + } + cy.wrap($row).children('.slick-cell:nth(3)') + .should('contain', expectedTasks[index]); + }); + }); + + it('should expect row selection (Task 144) to be persisted', () => { + cy.get('#grid15') + .contains('Task 144') + .parent() + .children() + .each($child => { + console.log($child); + expect($child.attr('class')).to.contain('selected'); + }); + }); + + it('should have French titles in Column Picker after switching to Language', () => { + const expectedTitles = ['', 'Description', 'Durรฉe', 'Titre', '% Achevรฉe', 'Dรฉbut', 'Terminรฉ']; + + cy.get('[data-test=language-button]') + .click(); + + cy.get('[data-test=selected-locale]') + .should('contain', 'fr.json'); + + cy.get('#grid15') + .find('.slick-header-column') + .first() + .trigger('mouseover') + .trigger('contextmenu') + .invoke('show'); + + cy.get('.slick-column-picker') + .find('.slick-column-picker-list') + .children() + .each(($child, index) => { + if (index === 0) { + expect($child[0].className).to.eq('hidden'); + expect($child[0].offsetHeight).to.eq(0); + expect($child[0].offsetWidth).to.eq(0); + } + + expect($child.text()).to.eq(expectedTitles[index]); + }); + + cy.get('.slick-column-picker:visible') + .find('.close') + .trigger('click') + .click(); + }); + + it('should have French titles in Grid Menu after switching to Language', () => { + const expectedTitles = ['', 'Description', 'Durรฉe', 'Titre', '% Achevรฉe', 'Dรฉbut', 'Terminรฉ']; + + cy.get('#grid15') + .find('button.slick-grid-menu-button') + .trigger('click') + .click(); + + cy.get('.slick-grid-menu') + .find('.slick-column-picker-list') + .children() + .each(($child, index) => { + if (index === 0) { + expect($child[0].className).to.eq('hidden'); + expect($child[0].offsetHeight).to.eq(0); + expect($child[0].offsetWidth).to.eq(0); + } + + expect($child.text()).to.eq(expectedTitles[index]); + }); + + cy.get('.slick-grid-menu:visible') + .find('.close') + .trigger('click') + .click(); + }); + + it('should hover over the "Terminรฉ" column and click on "Cacher la colonne" remove the column from grid', () => { + const expectedTitles = ['', 'Description', 'Durรฉe', 'Titre', '% Achevรฉe']; + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(5)') + .trigger('mouseover') + .children('.slick-header-menu-button') + .should('be.hidden') + .invoke('show') + .click(); + + cy.get('.slick-header-menu .slick-menu-command-list') + .should('be.visible') + .children('.slick-menu-item:nth-of-type(9)') + .children('.slick-menu-content') + .should('contain', 'Cacher la colonne') + .click(); + + cy.get('#grid15') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.find('.slick-column-name').text()).to.eq(expectedTitles[index])); + }); + + it('should be able to freeze "Description" column', () => { + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(1)') + .trigger('mouseover') + .children('.slick-header-menu-button') + .should('be.hidden') + .invoke('show') + .click(); + + cy.get('.slick-header-menu .slick-menu-command-list') + .should('be.visible') + .children('.slick-menu-item:nth-of-type(1)') + .children('.slick-menu-content') + .should('contain', 'Geler les colonnes') + .click(); + }); + + it('should reload the page', () => { + cy.reload().wait(50); + }); + + it('should expect the same Grid State to persist after the page got reloaded, however we always load in English', () => { + const expectedTitles = ['', 'Description', 'Duration', 'Title', '% Complete']; + + cy.get('#grid15') + .find('.grid-canvas') + .find('.slick-row') + .should('be.visible'); + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(2)') + .find('.slick-sort-indicator-numbered') + .should('be.visible') + .should('contain', '2'); + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(3)') + .find('.slick-sort-indicator.slick-sort-indicator-asc') + .should('be.visible'); + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(3)') + .find('.slick-sort-indicator-numbered') + .should('be.visible') + .should('contain', '1'); + + cy.get('#grid15') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.find('.slick-column-name').text()).to.eq(expectedTitles[index])); + }); + + it('should expect row selection (Task 144) to be persisted', () => { + cy.get('#grid15') + .contains('Task 144') + .parent() + .children() + .each($child => { + console.log($child); + expect($child.attr('class')).to.contain('selected'); + }); + }); + + it('should go back to first page and expect row selection (Task 105) to be persisted', () => { + cy.get('#slickGridContainer-grid15').as('grid15'); + + cy.get('@grid15') + .find('.icon-seek-first') + .click() + .wait(10); + + cy.get('#grid15') + .contains('Task 105') + .parent() + .children() + .each($child => { + console.log($child); + expect($child.attr('class')).to.contain('selected'); + }); + }); + + it('should have a persisted frozen column after "Description" and a grid with 4 containers on page load with 2 columns on the left and 3 columns on the right', () => { + cy.get('[style="top: 0px;"]').should('have.length', 2); + cy.get('.grid-canvas-left > [style="top: 0px;"]').children().should('have.length', 2); + cy.get('.grid-canvas-right > [style="top: 0px;"]').children().should('have.length', 3); + }); + + it('should click on the reset button and have exact Column Titles position as in beginning', () => { + cy.get('[data-test="reset-button"]') + .click(); + + cy.get('#grid15') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullEnglishTitles[index])); + }); + + it('should reload the page', () => { + cy.reload().wait(50); + }); + + it('should have same columns position after reload', () => { + const expectedTitles = ['', 'Title', 'Description', 'Duration', '% Complete', 'Start', 'Completed']; + + cy.get('#grid15') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(expectedTitles[index])); + }); + + it('should be able to freeze "Description" 3rd column', () => { + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(2)') + .trigger('mouseover') + .children('.slick-header-menu-button') + .should('be.hidden') + .invoke('show') + .click(); + + cy.get('.slick-header-menu .slick-menu-command-list') + .should('be.visible') + .children('.slick-menu-item:nth-of-type(1)') + .children('.slick-menu-content') + .should('contain', 'Freeze Columns') + .click(); + }); + + it('should swap "Duration" and "% Complete" columns', () => { + const expectedTitles = ['', 'Title', 'Description', '% Complete', 'Duration', 'Start', 'Completed']; + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(3)') + .contains('Duration') + .drag('.slick-header-column:nth(4)'); + + cy.get('#grid15') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(expectedTitles[index])); + }); + + it('should be able to freeze "% Complete" and expect 4th column to be freezed', () => { + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(3)') + .trigger('mouseover') + .children('.slick-header-menu-button') + .should('be.hidden') + .invoke('show') + .click(); + + cy.get('.slick-header-menu .slick-menu-command-list') + .should('be.visible') + .children('.slick-menu-item:nth-of-type(1)') + .children('.slick-menu-content') + .should('contain', 'Freeze Columns') + .click(); + }); + + it('should have a persisted frozen column after "Description" and a grid with 4 containers on page load with 2 columns on the left and 3 columns on the right', () => { + cy.get('[style="top: 0px;"]').should('have.length', 2); + cy.get('.grid-canvas-left > [style="top: 0px;"]').children().should('have.length', 4); + cy.get('.grid-canvas-right > [style="top: 0px;"]').children().should('have.length', 3); + }); + + describe('Filter Shortcuts', () => { + it('should clear locale storage & set language to French', () => { + cy.clearLocalStorage(); + cy.get('[data-test=reset-button]').click(); + + cy.get('[data-test=language-button]') + .click(); + + cy.get('[data-test="selected-locale"]') + .should('contain', 'fr.json'); + }); + + it('should open header menu of "Start" column and choose "Filter Shortcuts -> Past" and expect over 200 rows', () => { + cy.get('#grid15') + .find('.slick-header-column:nth-of-type(6)') + .trigger('mouseover') + .children('.slick-header-menu-button') + .invoke('show') + .click(); + + cy.get('[data-command=filter-shortcuts-root-menu]') + .should('contain', 'Raccourcis de filtre') + .trigger('mouseover'); + + cy.get('.slick-header-menu.slick-menu-level-1') + .find('[data-command=past]') + .should('contain', 'Passรฉ') + .click(); + + cy.get('.search-filter.filter-start .input-group-prepend.operator select') + .contains('<'); + + cy.get('.search-filter.filter-start input.date-picker') + .invoke('val') + .should('equal', format(new Date(), 'YYYY-MM-DD')); + + cy.get('[data-test="total-items"]') + .should($span => { + expect(Number($span.text())).to.gt(200); + }); + }); + + it('should open header menu of "Start" column and choose "Filter Shortcuts -> Future" and expect over 100 rows', () => { + cy.get('#grid15') + .find('.slick-header-column:nth-of-type(6)') + .trigger('mouseover') + .children('.slick-header-menu-button') + .invoke('show') + .click(); + + cy.get('[data-command=filter-shortcuts-root-menu]') + .should('contain', 'Raccourcis de filtre') + .trigger('mouseover'); + + cy.get('.slick-header-menu.slick-menu-level-1') + .find('[data-command=future]') + .click(); + + cy.get('.search-filter.filter-start .input-group-prepend.operator select') + .contains('>'); + + cy.get('.search-filter.filter-start input.date-picker') + .invoke('val') + .should('equal', format(new Date(), 'YYYY-MM-DD')); + + cy.get('[data-test="total-items"]') + .should($span => { + expect(Number($span.text())).to.gt(100); + }); + }); + + it('should open header menu of "Description" column and choose "Filter Shortcuts -> Blank Values" and expect over 10 rows', () => { + cy.get('#grid15') + .find('.slick-header-column:nth-of-type(3)') + .trigger('mouseover') + .children('.slick-header-menu-button') + .invoke('show') + .click(); + + cy.get('[data-command=filter-shortcuts-root-menu]') + .should('contain', 'Raccourcis de filtre') + .trigger('mouseover'); + + cy.get('.slick-header-menu.slick-menu-level-1') + .find('[data-command=blank-values]') + .should('contain', 'Valeurs nulles') + .click(); + + cy.get('.search-filter.filter-description') + .invoke('val') + .should('equal', '< A'); + + cy.get('[data-test="total-items"]') + .should($span => { + expect(Number($span.text())).to.gt(10); + }); + }); + + it('should switch back to English', () => { + cy.get('[data-test=language-button]') + .click(); + + cy.get('[data-test="selected-locale"]') + .should('contain', 'en.json'); + }); + + it('should open header menu of "Description" column and choose "Filter Shortcuts -> Non-Blank Values" and expect over 80 rows', () => { + cy.get('#grid15') + .find('.slick-header-column:nth-of-type(3)') + .trigger('mouseover') + .children('.slick-header-menu-button') + .invoke('show') + .click(); + + cy.get('[data-command=filter-shortcuts-root-menu]') + .should('contain', 'Filter Shortcuts') + .trigger('mouseover'); + + cy.get('.slick-header-menu.slick-menu-level-1') + .find('[data-command=non-blank-values]') + .should('contain', 'Non-Blank Values') + .click(); + + cy.get('.search-filter.filter-description') + .invoke('val') + .should('equal', '> A'); + + cy.get('[data-test="total-items"]') + .should($span => { + expect(Number($span.text())).to.gt(80); + }); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).contains('desc'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2)`).contains('desc'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(2)`).contains('desc'); + }); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example16.cy.ts b/demos/vue/test/cypress/e2e/example16.cy.ts new file mode 100644 index 000000000..850ab5d7b --- /dev/null +++ b/demos/vue/test/cypress/e2e/example16.cy.ts @@ -0,0 +1,431 @@ +describe('Example 16 - Row Move & Checkbox Selector Selector Plugins', () => { + const GRID_ROW_HEIGHT = 35; + const fullTitles = ['', '', 'Title', 'Duration', '% Complete', 'Start', 'Finish', 'Completed']; + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example16`); + cy.get('h2').should('contain', 'Example 16: Row Move & Checkbox Selector'); + }); + + it('should have exact Column Titles in the grid', () => { + cy.get('#grid16') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + }); + + it('should have 4 rows selected count shown in the grid left footer', () => { + cy.get('.slick-custom-footer') + .find('div.left-footer') + .should($span => { + expect($span.text()).to.eq(`4 items selected`); + }); + }); + + it('should drag opened row to another position in the grid', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell.cell-reorder`).as('moveIconTask1'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell.cell-reorder`).as('moveIconTask2'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell.cell-reorder`).as('moveIconTask3'); + + cy.get('@moveIconTask3').should('have.length', 1); + + cy.get('@moveIconTask3') + .trigger('mousedown', { which: 1, force: true }) + .trigger('mousemove', 'bottomRight'); + + cy.get('@moveIconTask1') + .trigger('mousemove', 'bottomRight') + .trigger('mouseup', 'bottomRight', { which: 1, force: true }); + + cy.get('@moveIconTask2').trigger('mouseover', { force: true }); + + cy.get('input[type="checkbox"]:checked') + .should('have.length', 4); + }); + + it('should expect the row to have moved to another row index', () => { + cy.get('.slick-viewport-top.slick-viewport-left') + .scrollTo('top'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', 'Task 0'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2)`).should('contain', 'Task 1'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(2)`).should('contain', 'Task 3'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(2)`).should('contain', 'Task 2'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(2)`).should('contain', 'Task 4'); + + cy.get('input[type="checkbox"]:checked') + .should('have.length', 4); + }); + + it('should uncheck all rows', () => { + // click twice to check then uncheck all + cy.get('.slick-headerrow-column input[type=checkbox]') + .click({ force: true }) + .click({ force: true }); + }); + + it('should have 0 row selected count shown in the grid left footer', () => { + cy.get('.slick-custom-footer') + .find('div.left-footer') + .should($span => { + expect($span.text()).to.eq(`0 items selected`); + }); + }); + + it('should select 2 rows (Task 3,4), then move the rows and expect both rows to still be selected without any other rows', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).click(); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(1)`).click(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell.cell-reorder`).as('moveIconTask3'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell.cell-reorder`).as('moveIconTask5'); + + cy.get('@moveIconTask3').should('have.length', 1); + + cy.get('@moveIconTask3') + .trigger('mousedown', { which: 1, force: true }) + .trigger('mousemove', 'bottomRight'); + + cy.get('@moveIconTask5') + .trigger('mousemove', 'bottomRight') + .trigger('mouseup', 'bottomRight', { which: 1, force: true }); + + cy.get('.slick-viewport-top.slick-viewport-left') + .scrollTo('top'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', 'Task 0'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2)`).should('contain', 'Task 1'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(2)`).should('contain', 'Task 2'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(2)`).should('contain', 'Task 4'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(2)`).should('contain', 'Task 5'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell:nth(2)`).should('contain', 'Task 3'); + + // Task 4 and 3 should be selected + cy.get('input[type="checkbox"]:checked').should('have.length', 2); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1) input[type="checkbox"]:checked`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell:nth(1) input[type="checkbox"]:checked`).should('have.length', 1); + }); + + it('should move "Duration" column to a different position in the grid', () => { + const expectedTitles = ['', '', 'Title', '% Complete', 'Start', 'Finish', 'Duration', 'Completed', 'Title']; + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(3)') + .contains('Duration') + .drag('.slick-header-column:nth(6)'); + + cy.get('.slick-header-column:nth(6)').contains('Duration'); + + cy.get('#grid16') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(expectedTitles[index])); + }); + + it('should be able to hide "Duration" column', () => { + const expectedTitles = ['', '', 'Title', '% Complete', 'Start', 'Finish', 'Completed', 'Title']; + + cy.get('[data-test="hide-duration-btn"]').click(); + + cy.get('#grid16') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(expectedTitles[index])); + }); + + it('should be able to click disable Filters functionality button and expect no Filters', () => { + const expectedTitles = ['', '', 'Title', '% Complete', 'Start', 'Finish', 'Completed', 'Title']; + + cy.get('[data-test="disable-filters-btn"]').click().click(); // even clicking twice should have same result + + cy.get('.slick-headerrow').should('not.be.visible'); + cy.get('.slick-headerrow-columns .slick-headerrow-column').should('have.length', 0); + + cy.get('#grid16') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(expectedTitles[index])); + + cy.get('[data-test="toggle-filtering-btn"]').click(); // show it back + }); + + it('should expect "Clear all Filters" command to be hidden in the Grid Menu', () => { + const expectedFullHeaderMenuCommands = ['Clear all Filters', 'Clear all Sorting', 'Toggle Filter Row', 'Export to Excel']; + + cy.get('#grid16') + .find('button.slick-grid-menu-button') + .trigger('click') + .click({ force: true }); + + cy.get('.slick-menu-command-list') + .find('.slick-menu-item') + .each(($child, index) => { + const commandTitle = $child.text(); + expect(commandTitle).to.eq(expectedFullHeaderMenuCommands[index]); + + // expect all Sorting commands to be hidden + if (commandTitle === 'Clear all Filters' || commandTitle === 'Toggle Filter Row') { + expect($child).to.be.visible; + } + }); + }); + + it('should be able to toggle Filters functionality', () => { + const expectedTitles = ['', '', 'Title', '% Complete', 'Start', 'Finish', 'Completed', 'Title']; + + cy.get('[data-test="toggle-filtering-btn"]').click(); // hide it + + cy.get('.slick-headerrow').should('not.be.visible'); + cy.get('.slick-headerrow-columns .slick-headerrow-column').should('have.length', 0); + + cy.get('#grid16') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(expectedTitles[index])); + + cy.get('[data-test="toggle-filtering-btn"]').click(); // show it + cy.get('.slick-headerrow-columns .slick-headerrow-column').should('have.length', 7); + + cy.get('#grid16') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(expectedTitles[index])); + }); + + it('should be able to toggle Sorting functionality (disable) and expect all header menu Sorting commands to be hidden and also not show Sort hint while hovering a column', () => { + const expectedFullHeaderMenuCommands = ['Resize by Content', '', 'Sort Ascending', 'Sort Descending', '', 'Remove Filter', 'Remove Sort', 'Hide Column']; + + cy.get('.slick-sort-indicator').should('have.length.greaterThan', 0); // sort icon hints + cy.get('[data-test="toggle-sorting-btn"]').click(); // disable it + cy.get('.slick-sort-indicator').should('have.length', 0); + + cy.get('#grid16') + .find('.slick-header-column:nth(5)') + .trigger('mouseover') + .children('.slick-header-menu-button') + .should('be.hidden') + .invoke('show') + .click(); + + cy.get('.slick-header-menu .slick-menu-command-list') + .children('.slick-menu-item') + .each(($child, index) => { + const commandTitle = $child.text(); + expect(commandTitle).to.eq(expectedFullHeaderMenuCommands[index]); + + // expect all Sorting commands to be hidden + if (commandTitle === 'Sort Ascending' || commandTitle === 'Sort Descending' || commandTitle === 'Remove Sort') { + expect($child).not.to.be.visible; + } + }); + }); + + it('should expect "Clear Sorting" command to be hidden in the Grid Menu', () => { + const expectedFullHeaderMenuCommands = ['Clear all Filters', 'Clear all Sorting', 'Toggle Filter Row', 'Export to Excel']; + + cy.get('#grid16') + .find('button.slick-grid-menu-button') + .trigger('click') + .click(); + + cy.get('.slick-menu-command-list') + .find('.slick-menu-item') + .each(($child, index) => { + const commandTitle = $child.text(); + expect(commandTitle).to.eq(expectedFullHeaderMenuCommands[index]); + + // expect all Sorting commands to be hidden + if (commandTitle === 'Clear all Sorting') { + expect($child).not.to.be.visible; + } + }); + }); + + it('should be able to toggle Sorting functionality (re-enable) and expect all Sorting header menu commands to be hidden and also not show Sort hint while hovering a column', () => { + const expectedFullHeaderMenuCommands = ['Resize by Content', '', 'Sort Ascending', 'Sort Descending', '', 'Remove Filter', 'Remove Sort', 'Hide Column']; + + cy.get('.slick-sort-indicator').should('have.length', 0); // sort icon hints + cy.get('[data-test="toggle-sorting-btn"]').click(); // enable it back + cy.get('.slick-sort-indicator').should('have.length.greaterThan', 0); + + cy.get('#grid16') + .find('.slick-header-column:nth(5)') + .trigger('mouseover') + .children('.slick-header-menu-button') + .should('be.hidden') + .invoke('show') + .click(); + + cy.get('.slick-header-menu .slick-menu-command-list') + .children('.slick-menu-item') + .each(($child, index) => { + const commandTitle = $child.text(); + expect(commandTitle).to.eq(expectedFullHeaderMenuCommands[index]); + expect($child).to.be.visible; + }); + }); + + it('should expect "Clear Sorting" command to be hidden in the Grid Menu', () => { + const expectedFullHeaderMenuCommands = ['Clear all Filters', 'Clear all Sorting', 'Toggle Filter Row', 'Export to Excel']; + + cy.get('#grid16') + .find('button.slick-grid-menu-button') + .trigger('click') + .click(); + + cy.get('.slick-menu-command-list') + .find('.slick-menu-item') + .each(($child, index) => { + const commandTitle = $child.text(); + expect(commandTitle).to.eq(expectedFullHeaderMenuCommands[index]); + + // expect all Sorting commands to be hidden + if (commandTitle === 'Clear all Sorting') { + expect($child).to.be.visible; + } + }); + }); + + it('should be able to click disable Sorting functionality button and expect all Sorting commands to be hidden and also not show Sort hint while hovering a column', () => { + const expectedFullHeaderMenuCommands = ['Resize by Content', '', 'Sort Ascending', 'Sort Descending', '', 'Remove Filter', 'Remove Sort', 'Hide Column']; + + cy.get('.slick-sort-indicator').should('have.length.greaterThan', 0); // sort icon hints + cy.get('[data-test="disable-sorting-btn"]').click().click(); // even clicking twice should have same result + cy.get('.slick-sort-indicator').should('have.length', 0); + + cy.get('#grid16') + .find('.slick-header-column:nth(5)') + .trigger('mouseover') + .children('.slick-header-menu-button') + .should('be.hidden') + .invoke('show') + .click(); + + cy.get('.slick-header-menu .slick-menu-command-list') + .children('.slick-menu-item') + .each(($child, index) => { + const commandTitle = $child.text(); + expect(commandTitle).to.eq(expectedFullHeaderMenuCommands[index]); + + // expect all Sorting commands to be hidden + if (commandTitle === 'Sort Ascending' || commandTitle === 'Sort Descending' || commandTitle === 'Remove Sort') { + expect($child).not.to.be.visible; + } + }); + }); + + it('should be able to click disable Filter functionality button and expect all Filter commands to be hidden and also not show Sort hint while hovering a column', () => { + const expectedFullHeaderMenuCommands = ['Resize by Content', '', 'Sort Ascending', 'Sort Descending', '', 'Remove Filter', 'Remove Sort', 'Hide Column']; + + cy.get('[data-test="disable-filters-btn"]').click().click(); // even clicking twice should have same result + + cy.get('#grid16') + .find('.slick-header-column:nth(5)') + .trigger('mouseover') + .children('.slick-header-menu-button') + .should('be.hidden') + .invoke('show') + .click(); + + cy.get('.slick-header-menu .slick-menu-command-list') + .children('.slick-menu-item') + .each(($child, index) => { + const commandTitle = $child.text(); + expect(commandTitle).to.eq(expectedFullHeaderMenuCommands[index]); + + // expect all Sorting commands to be hidden + if (commandTitle === 'Remove Filter') { + expect($child).not.to.be.visible; + } + }); + }); + + it('should expect "Clear all Filters" command to be hidden in the Grid Menu', () => { + const expectedFullHeaderMenuCommands = ['Clear all Filters', 'Clear all Sorting', 'Toggle Filter Row', 'Export to Excel']; + + cy.get('#grid16') + .find('button.slick-grid-menu-button') + .trigger('click') + .click(); + + cy.get('.slick-menu-command-list') + .find('.slick-menu-item') + .each(($child, index) => { + const commandTitle = $child.text(); + expect(commandTitle).to.eq(expectedFullHeaderMenuCommands[index]); + + // expect all Sorting commands to be hidden + if (commandTitle === 'Clear all Filters' || commandTitle === 'Toggle Filter Row') { + expect($child).not.to.be.visible; + } + }); + }); + + it('should open Column Picker and show the "Duration" column back to visible and expect it to have kept its position after toggling filter/sorting', () => { + // first 2 cols are hidden but they do count as li item + const expectedFullPickerTitles = ['', '', 'Title', '% Complete', 'Start', 'Finish', 'Duration', 'Completed']; + + cy.get('#grid16') + .find('.slick-header-column') + .first() + .trigger('mouseover') + .trigger('contextmenu') + .invoke('show'); + + cy.get('.slick-column-picker') + .find('.slick-column-picker-list') + .children() + .each(($child, index) => { + if (index < expectedFullPickerTitles.length) { + expect($child.text()).to.eq(expectedFullPickerTitles[index]); + } + }); + + cy.get('.slick-column-picker') + .find('.slick-column-picker-list') + .children('li:nth-child(7)') + .children('label') + .should('contain', 'Duration') + .click(); + + cy.get('#grid16') + .get('.slick-column-picker:visible') + .find('.close') + .trigger('click') + .click(); + + cy.get('#grid16') + .find('.slick-header-columns') + .children() + .each(($child, index) => { + if (index <= 5) { + expect($child.text()).to.eq(expectedFullPickerTitles[index]); + } + }); + }); + + it('should add Edit/Delete columns and expect 2 new columns added at the beginning of the grid', () => { + const newExpectedColumns = ['', '', ...fullTitles]; + cy.get('[data-test="add-crud-columns-btn"]').click(); + + cy.get('#grid16') + .find('.slick-header-columns') + .children() + .each(($child, index) => { + if (index <= 5) { + expect($child.text()).to.eq(newExpectedColumns[index]); + } + }); + + cy.get('.slick-row') + .first() + .children('.slick-cell') + .children('.mdi.mdi-pencil') + .should('have.length', 1); + + cy.get('.slick-row') + .first() + .children('.slick-cell:nth(1)') + .children('.mdi.mdi-trash-can') + .should('have.length', 1); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example18.cy.ts b/demos/vue/test/cypress/e2e/example18.cy.ts new file mode 100644 index 000000000..ca47ad75c --- /dev/null +++ b/demos/vue/test/cypress/e2e/example18.cy.ts @@ -0,0 +1,381 @@ +describe('Example 18 - Draggable Grouping & Aggregators', () => { + const preHeaders = ['Common Factor', 'Period', 'Analysis', '']; + const fullTitles = ['Title', 'Duration', 'Start', 'Finish', 'Cost', '% Complete', 'Effort-Driven']; + const GRID_ROW_HEIGHT = 35; + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example18`); + cy.get('h2').should('contain', 'Example 18: Draggable Grouping & Aggregators'); + }); + + it('should have exact column (pre-header) grouping titles in grid', () => { + cy.get('#grid18') + .find('.slick-preheader-panel .slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(preHeaders[index])); + }); + + it('should have exact column titles in grid', () => { + cy.get('#grid18') + .find('.slick-header:not(.slick-preheader-panel) .slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + }); + + it('should have a draggable dropzone on top of the grid in the top-header section', () => { + cy.get('#grid18').find('.slick-topheader-panel .slick-dropzone:visible').contains('Drop a column header here to group by the column'); + }); + + describe('Grouping Tests', () => { + it('should "Group by Duration & sort groups by value" then Collapse All and expect only group titles', () => { + cy.get('[data-test="add-50k-rows-btn"]').click(); + cy.get('[data-test="group-duration-sort-value-btn"]').click(); + cy.get('[data-test="collapse-all-btn"]').click(); + + cy.get('.grouping-selects select:nth(0)').should('have.value', 'duration'); + cy.get('.grouping-selects select:nth(1)').should('not.have.value'); + cy.get('.grouping-selects select:nth(2)').should('not.have.value'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-toggle.collapsed`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Duration: 0'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Duration: 1'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Duration: 2'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Duration: 3'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Duration: 4'); + }); + + it('should click on Expand All columns and expect 1st row as grouping title and 2nd row as a regular row', () => { + cy.get('[data-test="add-50k-rows-btn"]').click(); + cy.get('[data-test="group-duration-sort-value-btn"]').click(); + cy.get('[data-test="expand-all-btn"]').click(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-toggle.expanded`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Duration: 0'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'Task'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', '0'); + }); + + it('should show 1 column title (Duration) shown in the pre-header section', () => { + cy.get('.slick-dropped-grouping:nth(0) div').contains('Duration'); + cy.get('.grouping-selects select:nth(0)').should('have.value', 'duration'); + cy.get('.grouping-selects select:nth(1)').should('not.have.value'); + cy.get('.grouping-selects select:nth(2)').should('not.have.value'); + }); + + it('should "Group by Duration then Effort-Driven" and expect 1st row to be expanded, 2nd row to be expanded and 3rd row to be a regular row', () => { + cy.get('[data-test="group-duration-effort-btn"]').click(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"].slick-group-level-0 > .slick-cell:nth(0) .slick-group-toggle.expanded`).should( + 'have.length', + 1 + ); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"].slick-group-level-0 > .slick-cell:nth(0) .slick-group-title`).should( + 'contain', + 'Duration: 0' + ); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"].slick-group-level-1 .slick-group-toggle.expanded`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"].slick-group-level-1 .slick-group-title`).should('contain', 'Effort-Driven: False'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'Task'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', '0'); + }); + + it('should show 2 column titles (Duration, Effort-Driven) shown in the pre-header section & same select dropdown', () => { + cy.get('.slick-dropped-grouping:nth(0) div').contains('Duration'); + cy.get('.slick-dropped-grouping:nth(1) div').contains('Effort-Driven'); + cy.get('.grouping-selects select:nth(0)').should('have.value', 'duration'); + cy.get('.grouping-selects select:nth(1)').should('have.value', 'effortDriven'); + cy.get('.grouping-selects select:nth(2)').should('not.have.value'); + }); + + it('should be able to drag and swap grouped column titles inside the pre-header', () => { + cy.get('.slick-dropped-grouping:nth(0) div').contains('Duration').drag('.slick-dropped-grouping:nth(1) div'); + + cy.get('.slick-dropped-grouping:nth(0) div').contains('Effort-Driven'); + cy.get('.slick-dropped-grouping:nth(1) div').contains('Duration'); + cy.get('.grouping-selects select:nth(0)').should('have.value', 'effortDriven'); + cy.get('.grouping-selects select:nth(1)').should('have.value', 'duration'); + cy.get('.grouping-selects select:nth(2)').should('not.have.value'); + }); + + it('should expect the grouping to be swapped as well in the grid', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"].slick-group-level-0 > .slick-cell:nth(0) .slick-group-toggle.expanded`).should( + 'have.length', + 1 + ); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"].slick-group-level-0 > .slick-cell:nth(0) .slick-group-title`).should( + 'contain', + 'Effort-Driven: False' + ); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"].slick-group-level-1 .slick-group-toggle.expanded`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"].slick-group-level-1 .slick-group-title`).should('contain', 'Duration: 0'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'Task'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', '0'); + }); + + it('should expand all rows with "Expand All" from context menu and expect all the Groups to be expanded and the Toogle All icon to be collapsed', () => { + cy.get('#grid18').find('.slick-row .slick-cell:nth(1)').rightclick({ force: true }); + + cy.get('.slick-context-menu .slick-menu-command-list') + .find('.slick-menu-item') + .find('.slick-menu-content') + .contains('Expand all Groups') + .click(); + + cy.get('#grid18').find('.slick-group-toggle.collapsed').should('have.length', 0); + + cy.get('#grid18') + .find('.slick-group-toggle.expanded') + .should(($rows) => expect($rows).to.have.length.greaterThan(0)); + + cy.get('.slick-group-toggle-all-icon.expanded').should('exist'); + }); + + it('should collapse all rows with "Collapse All" from context menu and expect all the Groups to be collapsed and the Toogle All icon to be collapsed', () => { + cy.get('#grid18').find('.slick-row .slick-cell:nth(1)').rightclick({ force: true }); + + cy.get('.slick-context-menu .slick-menu-command-list') + .find('.slick-menu-item') + .find('.slick-menu-content') + .contains('Collapse all Groups') + .click(); + + cy.get('#grid18').find('.slick-group-toggle.expanded').should('have.length', 0); + + cy.get('#grid18') + .find('.slick-group-toggle.collapsed') + .should(($rows) => expect($rows).to.have.length.greaterThan(0)); + + cy.get('.slick-group-toggle-all-icon.collapsed').should('exist'); + }); + + it('should use the topheader Toggle All button and expect all groups to now be expanded', () => { + cy.get('.slick-topheader-panel .slick-group-toggle-all').click(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-toggle.expanded`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Effort-Driven: False'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Duration: 0'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-toggle.expanded`) + .should('have.css', 'marginLeft') + .and('eq', `0px`); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0) .slick-group-toggle.expanded`) + .should('have.css', 'marginLeft') + .and('eq', `15px`); + }); + + it('should use the topheader Toggle All button again and expect all groups to now be collapsed', () => { + cy.get('.slick-topheader-panel .slick-group-toggle-all').click(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-toggle.collapsed`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Effort-Driven: False'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Effort-Driven: True'); + }); + + it('should clear all groups with "Clear all Grouping" from context menu and expect all the Groups to be collapsed and the Toogle All icon to be collapsed', () => { + cy.get('#grid18').find('.slick-row .slick-cell:nth(1)').rightclick({ force: true }); + + cy.get('.slick-context-menu .slick-menu-command-list') + .find('.slick-menu-item') + .find('.slick-menu-content') + .contains('Clear all Grouping') + .click(); + + cy.get('#grid18').find('.slick-group-toggle-all').should('be.hidden'); + + cy.get('#grid18') + .find('.slick-draggable-dropzone-placeholder') + .should('be.visible') + .should('have.text', 'Drop a column header here to group by the column'); + }); + + it('should add 500 items and expect 500 of 500 items displayed', () => { + cy.get('[data-test="add-500-rows-btn"]').click(); + + cy.get('.right-footer').contains('500 of 500 items'); + }); + + it('should clear all grouping and expect all select dropdown to be cleared too', () => { + cy.get('[data-test="clear-grouping-btn"]').click(); + cy.get('.grouping-selects select:nth(0)').should('not.have.value'); + cy.get('.grouping-selects select:nth(1)').should('not.have.value'); + cy.get('.grouping-selects select:nth(2)').should('not.have.value'); + }); + }); + + describe('Column Picker tests', () => { + it('should open Column Picker from 2nd header column and hide Title & Duration which will hide Common Factor Group as well', () => { + const fullTitlesWithGroupNames = [ + 'Common Factor - Title', + 'Common Factor - Duration', + 'Period - Start', + 'Period - Finish', + 'Analysis - Cost', + 'Analysis - % Complete', + 'Analysis - Effort-Driven', + ]; + + cy.get('#grid18').find('.slick-header-column:nth(1)').trigger('mouseover').trigger('contextmenu').invoke('show'); + + cy.get('.slick-column-picker') + .find('.slick-column-picker-list') + .children() + .each(($child, index) => { + if (index <= 5) { + expect($child.text()).to.eq(fullTitlesWithGroupNames[index]); + } + }); + + cy.get('.slick-column-picker') + .find('.slick-column-picker-list') + .children('li:nth-child(1)') + .children('label') + .should('contain', 'Title') + .click(); + + cy.get('.slick-column-picker .close').click(); + }); + + it('should open Column Picker from 2nd header column name and hide Duration which will hide Common Factor Group as well', () => { + const fullTitlesWithGroupNames = [ + 'Common Factor - Title', + 'Common Factor - Duration', + 'Period - Start', + 'Period - Finish', + 'Analysis - Cost', + 'Analysis - % Complete', + 'Analysis - Effort-Driven', + ]; + + cy.get('#grid18').find('.slick-header-column:nth(1) .slick-column-name').trigger('mouseover').trigger('contextmenu').invoke('show'); + + cy.get('.slick-column-picker') + .find('.slick-column-picker-list') + .children() + .each(($child, index) => { + if (index <= 5) { + expect($child.text()).to.eq(fullTitlesWithGroupNames[index]); + } + }); + + cy.get('.slick-column-picker') + .find('.slick-column-picker-list') + .children('li:nth-child(2)') + .children('label') + .should('contain', 'Duration') + .click(); + + cy.get('.slick-column-picker .close').click(); + }); + + it('should expect headers to be without Title/Duration and pre-headers without Common Factor Group header titles', () => { + const preHeadersWithoutFactor = ['Period', 'Analysis']; + const titlesWithoutTitleDuration = ['Start', 'Finish', 'Cost', '% Complete', 'Effort-Driven']; + + // Column Pre-Headers without Common Factor group + cy.get('#grid18') + .find('.slick-header:not(.slick-preheader-panel) .slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(titlesWithoutTitleDuration[index])); + + // Column Headers without Title & Duration + cy.get('#grid18') + .find('.slick-preheader-panel .slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(preHeadersWithoutFactor[index])); + }); + + it('should open Column Picker from Pre-Header column and show again Title column', () => { + const fullTitlesWithGroupNames = [ + 'Common Factor - Title', + 'Common Factor - Duration', + 'Period - Start', + 'Period - Finish', + 'Analysis - Cost', + 'Analysis - % Complete', + 'Analysis - Effort-Driven', + ]; + + cy.get('#grid18') + .find('.slick-preheader-panel .slick-header-column:nth(1)') + .trigger('mouseover') + .trigger('contextmenu') + .invoke('show'); + + cy.get('.slick-column-picker') + .find('.slick-column-picker-list') + .children() + .each(($child, index) => { + if (index <= 5) { + expect($child.text()).to.eq(fullTitlesWithGroupNames[index]); + } + }); + + cy.get('.slick-column-picker') + .find('.slick-column-picker-list') + .children('li:nth-child(1)') + .children('label') + .should('contain', 'Title') + .click(); + + // close picker & reopen from a pre-header column name instead + cy.get('.slick-column-picker .close').click(); + }); + + it('should open Column Picker from Pre-Header column name and show again Duration column', () => { + const fullTitlesWithGroupNames = [ + 'Common Factor - Title', + 'Common Factor - Duration', + 'Period - Start', + 'Period - Finish', + 'Analysis - Cost', + 'Analysis - % Complete', + 'Analysis - Effort-Driven', + ]; + + cy.get('#grid18') + .find('.slick-preheader-panel .slick-header-column:nth(1)') + .trigger('mouseover') + .trigger('contextmenu') + .invoke('show'); + + cy.get('.slick-column-picker') + .find('.slick-column-picker-list') + .children() + .each(($child, index) => { + if (index <= 5) { + expect($child.text()).to.eq(fullTitlesWithGroupNames[index]); + } + }); + + cy.get('.slick-column-picker') + .find('.slick-column-picker-list') + .children('li:nth-child(2)') + .children('label') + .should('contain', 'Duration') + .click(); + + cy.get('.slick-column-picker .close').click(); + }); + + it('should expect header titles to show again Title/Duration and pre-headers with Common Factor Group header titles', () => { + const preHeadersWithFactor = ['Common Factor', 'Period', 'Analysis', '']; + const titlesWithTitleDuration = ['Title', 'Duration', 'Start', 'Finish', 'Cost', '% Complete', 'Effort-Driven']; + + // Column Pre-Headers without Common Factor group + cy.get('#grid18') + .find('.slick-header:not(.slick-preheader-panel) .slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(titlesWithTitleDuration[index])); + + // Column Headers without Title & Duration + cy.get('#grid18') + .find('.slick-preheader-panel .slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(preHeadersWithFactor[index])); + }); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example19.cy.ts b/demos/vue/test/cypress/e2e/example19.cy.ts new file mode 100644 index 000000000..a5193b7a4 --- /dev/null +++ b/demos/vue/test/cypress/e2e/example19.cy.ts @@ -0,0 +1,344 @@ +describe('Example 19 - Row Detail View', () => { + const titles = ['', 'Title', 'Duration (days)', '% Complete', 'Start', 'Finish', 'Effort Driven']; + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example19`); + cy.get('h2').should('contain', 'Example 19: Row Detail View'); + }); + + it('should have exact column titles on 1st grid', () => { + cy.get('#grid19') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(titles[index])); + }); + + it('should display first few rows of Task 0 to 5', () => { + const expectedTasks = ['Task 0', 'Task 1', 'Task 2', 'Task 3', 'Task 4', 'Task 5']; + + cy.get('#grid19') + .find('.slick-row') + .each(($row, index) => { + if (index > expectedTasks.length - 1) { + return; + } + cy.wrap($row).children('.slick-cell:nth(1)') + .first() + .should('contain', expectedTasks[index]); + }); + }); + + it('should click anywhere on 3rd row to open its Row Detail and expect its title to be Task 2 in an H2 tag', () => { + cy.get('#grid19') + .find('.slick-row:nth(2)') + .click(); + + cy.get('#grid19') + .find('.slick-cell + .dynamic-cell-detail .innerDetailView_2 .container_2') + .as('detailContainer'); + + cy.get('@detailContainer') + .find('h3') + .contains('Task 2'); + }); + + it('should click on the "Click Me" button and expect the assignee name to showing in uppercase in an Alert', () => { + let assignee = ''; + const alertStub = cy.stub(); + cy.on('window:alert', alertStub); + + cy.get('#grid19') + .find('.slick-cell + .dynamic-cell-detail .innerDetailView_2 .container_2') + .as('detailContainer'); + + cy.get('@detailContainer') + .find('input') + .invoke('val') + .then(text => assignee = text as string); + + cy.get('@detailContainer') + .find('[data-test=assignee-btn]') + .click() + .then(() => { + if (assignee === '') { + expect(alertStub.getCall(0)).to.be.calledWith(`No one is assigned to this task.`); + } else { + expect(alertStub.getCall(0)).to.be.calledWith(`Assignee on this task is: ${assignee.toUpperCase()}`); + } + }); + }); + + it('should click on the "Call Parent Method" button and expect a Bootstrap Alert to show up with some text containing the Task 2', () => { + cy.get('#grid19') + .find('.slick-cell + .dynamic-cell-detail .innerDetailView_2 .container_2') + .as('detailContainer'); + + cy.get('@detailContainer') + .find('[data-test=parent-btn]') + .click(); + + cy.get('.alert-info[data-test=flash-msg]') + .contains('We just called Parent Method from the Row Detail Child Component on Task 2'); + }); + + it('should click on the "Delete Row" button and expect the Task 2 to be deleted from the grid', () => { + const expectedTasks = ['Task 0', 'Task 1', 'Task 3', 'Task 4', 'Task 5']; + + cy.get('#grid19') + .find('.slick-cell + .dynamic-cell-detail .innerDetailView_2 .container_2') + .as('detailContainer'); + + cy.get('@detailContainer') + .find('[data-test=delete-btn]') + .click(); + + cy.get('.slick-viewport-top.slick-viewport-left') + .scrollTo('top'); + + cy.get('#grid19') + .find('.slick-row') + .each(($row, index) => { + if (index > expectedTasks.length - 1) { + return; + } + cy.wrap($row).children('.slick-cell:nth(1)') + .first() + .should('contain', expectedTasks[index]); + }); + + cy.get('.alert-danger[data-test=flash-msg]') + .contains('Deleted row with Task 2'); + }); + + it('should open a few Row Details and expect them to be closed after clicking on the "Close All Row Details" button', () => { + const expectedTasks = ['Task 0', 'Task 1', 'Task 3', 'Task 4', 'Task 5']; + + cy.get('#grid19') + .find('.slick-row:nth(2)') + .click(); + + cy.get('#grid19') + .find('.slick-cell + .dynamic-cell-detail .innerDetailView_3 .container_3') + .as('detailContainer3'); + + cy.get('@detailContainer3') + .find('h3') + .contains('Task 3'); + + cy.get('#grid19') + .find('.slick-row:nth(0)') + .click(); + + cy.get('#grid19') + .find('.slick-cell + .dynamic-cell-detail .innerDetailView_0 .container_0') + .as('detailContainer0'); + + cy.get('@detailContainer0') + .find('h3') + .contains('Task 0'); + + cy.get('[data-test=collapse-all-btn]') + .click(); + + cy.get('.slick-viewport-top.slick-viewport-left') + .scrollTo('top'); + + cy.get('#grid19') + .find('.slick-cell + .dynamic-cell-detail .innerDetailView_0 .container_0') + .should('not.exist'); + + cy.get('#grid19') + .find('.slick-cell + .dynamic-cell-detail .innerDetailView_1 .container_1') + .should('not.exist'); + + cy.get('#grid19') + .find('.slick-row') + .each(($row, index) => { + if (index > expectedTasks.length - 1) { + return; + } + cy.wrap($row).children('.slick-cell:nth(1)') + .first() + .should('contain', expectedTasks[index]); + }); + }); + + it('should open a few Row Details, then sort by Title and expect all Row Details to be closed afterward', () => { + const expectedTasks = ['Task 0', 'Task 1', 'Task 10', 'Task 100', 'Task 101', 'Task 102', 'Task 103', 'Task 104']; + + cy.get('#grid19') + .find('.slick-row:nth(0)') + .click(); + + cy.get('#grid19') + .find('.slick-cell + .dynamic-cell-detail .innerDetailView_0 .container_0') + .as('detailContainer0'); + + cy.get('@detailContainer0') + .find('h3') + .contains('Task 0'); + + cy.get('#grid19') + .find('.slick-row:nth(9)') + .click(); + + cy.get('#grid19') + .find('.slick-cell + .dynamic-cell-detail .innerDetailView_3 .container_3') + .as('detailContainer3'); + + cy.get('@detailContainer3') + .find('h3') + .contains('Task 3'); + + cy.get('#slickGridContainer-grid19') + .find('.slick-header-column:nth(1)') + .trigger('mouseover') + .children('.slick-header-menu-button') + .should('be.hidden') + .invoke('show') + .click(); + + cy.get('.slick-header-menu .slick-menu-command-list') + .should('be.visible') + .children('.slick-menu-item:nth-of-type(4)') + .children('.slick-menu-content') + .should('contain', 'Sort Descending') + .click(); + + cy.get('#slickGridContainer-grid19') + .find('.slick-header-column:nth(1)') + .trigger('mouseover') + .children('.slick-header-menu-button') + .invoke('show') + .click(); + + cy.get('.slick-header-menu .slick-menu-command-list') + .should('be.visible') + .children('.slick-menu-item:nth-of-type(3)') + .children('.slick-menu-content') + .should('contain', 'Sort Ascending') + .click(); + + cy.get('#grid19') + .find('.slick-header-column:nth(1)') + .find('.slick-sort-indicator-asc') + .should('have.length', 1); + + cy.get('.slick-viewport-top.slick-viewport-left') + .scrollTo('top'); + + cy.get('#grid19') + .find('.slick-cell + .dynamic-cell-detail .innerDetailView_0 .container_0') + .should('not.exist'); + + cy.get('#grid19') + .find('.slick-cell + .dynamic-cell-detail .innerDetailView_3 .container_3') + .should('not.exist'); + + cy.get('#grid19') + .find('.slick-row') + .each(($row, index) => { + if (index > expectedTasks.length - 1) { + return; + } + cy.wrap($row).children('.slick-cell:nth(1)') + .first() + .should('contain', expectedTasks[index]); + }); + }); + + it('should click open Row Detail of Task 1 and Task 101 then type a title filter of "Task 101" and expect Row Detail to be opened and still be rendered', () => { + cy.get('#grid19') + .find('.slick-row:nth(4)') + .click(); + + cy.get('#grid19') + .find('.slick-row:nth(1)') + .click(); + + cy.get('#grid19') + .find('.slick-cell + .dynamic-cell-detail .innerDetailView_101') + .as('detailContainer'); + + cy.get('@detailContainer') + .find('h3') + .contains('Task 101'); + + cy.get('.search-filter.filter-title') + .type('Task 101'); + }); + + it('should call "Clear all Filters" from Grid Menu and expect "Task 101" to still be rendered correctly', () => { + cy.get('#grid19') + .find('button.slick-grid-menu-button') + .trigger('click') + .click(); + + cy.get(`.slick-grid-menu:visible`) + .find('.slick-menu-item') + .first() + .find('span') + .contains('Clear all Filters') + .click(); + + cy.get('#grid19') + .find('.slick-cell + .dynamic-cell-detail .innerDetailView_101') + .as('detailContainer'); + + cy.get('@detailContainer') + .find('h3') + .contains('Task 101'); + }); + + it('should call "Clear all Sorting" from Grid Menu and expect all row details to be collapsed', () => { + cy.get('#grid19') + .find('button.slick-grid-menu-button') + .trigger('click') + .click(); + + cy.get(`.slick-grid-menu:visible`) + .find('.slick-menu-item') + .find('span') + .contains('Clear all Sorting') + .click(); + + cy.get('#grid19') + .find('.slick-sort-indicator-asc') + .should('have.length', 0); + + cy.get('.dynamic-cell-detail').should('have.length', 0); + }); + + it('should close all row details & make grid editable', () => { + cy.get('[data-test="collapse-all-btn"]').click(); + cy.get('[data-test="editable-grid-btn"]').click(); + }); + + it('should click on 5th row detail open icon and expect it to open', () => { + cy.get('#grid19') + .find('.slick-row:nth(4) .slick-cell:nth(0)') + .click(); + + cy.get('#grid19') + .find('.slick-cell + .dynamic-cell-detail .innerDetailView_101') + .as('detailContainer'); + + cy.get('@detailContainer') + .find('h3') + .contains('Task 101'); + }); + + it('should click on 2nd row "Title" cell to edit it and expect Task 5 row detail to get closed', () => { + cy.get('#grid19') + .find('.slick-row:nth(1) .slick-cell:nth(1)') + .click(); + + cy.get('.editor-title') + .invoke('val') + .then(text => expect(text).to.eq('Task 1')); + + cy.get('#grid19') + .find('.slick-cell + .dynamic-cell-detail .innerDetailView_101') + .should('not.exist'); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example20.cy.ts b/demos/vue/test/cypress/e2e/example20.cy.ts new file mode 100644 index 000000000..cc8a58c56 --- /dev/null +++ b/demos/vue/test/cypress/e2e/example20.cy.ts @@ -0,0 +1,222 @@ +describe('Example 20 - Frozen Grid', () => { + // NOTE: everywhere there's a * 2 is because we have a top+bottom (frozen rows) containers even after Unfreeze Columns/Rows + + const fullTitles = ['#', 'Title', '% Complete', 'Start', 'Finish', 'Cost | Duration', 'Effort Driven', 'Title 1', 'Title 2', 'Title 3', 'Title 4']; + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example20`); + cy.get('h2').should('contain', 'Example 20: Pinned (frozen) Columns/Rows'); + }); + + it('should have exact column titles on 1st grid', () => { + cy.get('#grid20') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + }); + + it('should have exact Column Header Titles in the grid', () => { + cy.get('#grid20') + .find('.slick-header-columns:nth(0)') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + }); + + it('should have a frozen grid with 4 containers on page load with 3 columns on the left and 4 columns on the right', () => { + cy.get('[style="top: 0px;"]').should('have.length', 2 * 2); + cy.get('.grid-canvas-left > [style="top: 0px;"]').children().should('have.length', 3 * 2); + cy.get('.grid-canvas-right > [style="top: 0px;"]').children().should('have.length', 8 * 2); + + cy.get('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(0)').should('contain', '0'); + cy.get('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(1)').should('contain', 'Task 0'); + + cy.get('.grid-canvas-right > [style="top: 0px;"] > .slick-cell:nth(0)').should('contain', '2009-01-01'); + cy.get('.grid-canvas-right > [style="top: 0px;"] > .slick-cell:nth(1)').should('contain', '2009-05-05'); + }); + + + it('should hide "Title" column from Grid Menu and expect last frozen column to be "% Complete"', () => { + const newColumnList = ['#', '% Complete', 'Start', 'Finish', 'Cost | Duration', 'Effort Driven', 'Title 1', 'Title 2', 'Title 3', 'Title 4']; + + cy.get('#grid20') + .find('button.slick-grid-menu-button') + .click({ force: true }); + + cy.get('#grid20') + .get('.slick-grid-menu:visible') + .find('.slick-column-picker-list') + .children('li:visible:nth(1)') + .children('label') + .should('contain', 'Title') + .click({ force: true }); + + cy.get('#grid20') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(newColumnList[index])); + + cy.get('.grid-canvas-left > [style="top: 0px;"]').children().should('have.length', 2 * 2); + cy.get('.grid-canvas-right > [style="top: 0px;"]').children().should('have.length', 8 * 2); + + cy.get('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(0)').should('contain', ''); + + cy.get('.grid-canvas-right > [style="top: 0px;"] > .slick-cell:nth(0)').should('contain', '2009-01-01'); + cy.get('.grid-canvas-right > [style="top: 0px;"] > .slick-cell:nth(1)').should('contain', '2009-05-05'); + }); + + it('should show again "Title" column from Grid Menu and expect last frozen column to still be "% Complete"', () => { + cy.get('#grid20') + .get('.slick-grid-menu:visible') + .find('.slick-column-picker-list') + .children('li:visible:nth(1)') + .children('label') + .should('contain', 'Title') + .click({ force: true }); + + cy.get('#grid20') + .get('.slick-grid-menu:visible') + .find('.close') + .click({ force: true }); + + cy.get('#grid20') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + + cy.get('.grid-canvas-left > [style="top: 0px;"]').children().should('have.length', 3 * 2); + cy.get('.grid-canvas-right > [style="top: 0px;"]').children().should('have.length', 8 * 2); + + cy.get('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(0)').should('contain', ''); + cy.get('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(1)').should('contain', 'Task 0'); + + cy.get('.grid-canvas-right > [style="top: 0px;"] > .slick-cell:nth(0)').should('contain', '2009-01-01'); + cy.get('.grid-canvas-right > [style="top: 0px;"] > .slick-cell:nth(1)').should('contain', '2009-05-05'); + }); + + it('should hide "Title" column from Header Menu and expect last frozen column to be "% Complete"', () => { + const newColumnList = ['#', '% Complete', 'Start', 'Finish', 'Cost | Duration', 'Effort Driven', 'Title 1', 'Title 2', 'Title 3', 'Title 4']; + + cy.get('#grid20') + .find('.slick-header-column:nth(1)') + .trigger('mouseover') + .children('.slick-header-menu-button') + .should('be.hidden') + .invoke('show') + .click(); + + cy.get('.slick-header-menu .slick-menu-command-list') + .should('be.visible') + .children('.slick-menu-item:nth-of-type(8)') + .children('.slick-menu-content') + .should('contain', 'Hide Column') + .click(); + + cy.get('#grid20') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(newColumnList[index])); + + cy.get('.grid-canvas-left > [style="top: 0px;"]').children().should('have.length', 2 * 2); + cy.get('.grid-canvas-right > [style="top: 0px;"]').children().should('have.length', 8 * 2); + + cy.get('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(0)').should('contain', ''); + + cy.get('.grid-canvas-right > [style="top: 0px;"] > .slick-cell:nth(0)').should('contain', '2009-01-01'); + cy.get('.grid-canvas-right > [style="top: 0px;"] > .slick-cell:nth(1)').should('contain', '2009-05-05'); + }); + + it('should show again "Title" column from Column Picker and expect last frozen column to still be "% Complete"', () => { + cy.get('#grid20') + .find('.slick-header-column:nth(5)') + .trigger('mouseover') + .trigger('contextmenu') + .invoke('show'); + + cy.get('.slick-column-picker') + .find('.slick-column-picker-list') + .children('li:nth-child(2)') + .children('label') + .should('contain', 'Title') + .click(); + + cy.get('.slick-column-picker:visible') + .find('.close') + .trigger('click') + .click(); + + cy.get('#grid20') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + + cy.get('.grid-canvas-left > [style="top: 0px;"]').children().should('have.length', 3 * 2); + cy.get('.grid-canvas-right > [style="top: 0px;"]').children().should('have.length', 8 * 2); + + cy.get('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(0)').should('contain', ''); + cy.get('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(1)').should('contain', 'Task 0'); + + cy.get('.grid-canvas-right > [style="top: 0px;"] > .slick-cell:nth(0)').should('contain', '2009-01-01'); + cy.get('.grid-canvas-right > [style="top: 0px;"] > .slick-cell:nth(1)').should('contain', '2009-05-05'); + }); + + it('should click on the "Remove Frozen Columns" button to switch to a regular grid without frozen columns and expect 7 columns on the left container', () => { + cy.get('[data-test=remove-frozen-column-button]') + .click({ force: true }); + + cy.get('[style="top: 0px;"]').should('have.length', 1 * 2); + cy.get('.grid-canvas-left > [style="top: 0px;"]').children().should('have.length', 11 * 2); + + cy.get('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(0)').should('contain', '0'); + cy.get('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(1)').should('contain', 'Task 0'); + + cy.get('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(3)').should('contain', '2009-01-01'); + cy.get('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(4)').should('contain', '2009-05-05'); + }); + + it('should have exact Column Header Titles in the grid', () => { + cy.get('#grid20') + .find('.slick-header-columns:nth(0)') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + }); + + it('should click on the "Set 3 Frozen Columns" button to switch frozen columns grid and expect 3 frozen columns on the left and 4 columns on the right', () => { + cy.get('[data-test=set-3frozen-columns]') + .click({ force: true }); + + cy.get('[style="top: 0px;"]').should('have.length', 2 * 2); + cy.get('.grid-canvas-left > [style="top: 0px;"]').children().should('have.length', 3 * 2); + cy.get('.grid-canvas-right > [style="top: 0px;"]').children().should('have.length', 8 * 2); + + cy.get('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(0)').should('contain', '0'); + cy.get('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(1)').should('contain', 'Task 0'); + + cy.get('.grid-canvas-right > [style="top: 0px;"] > .slick-cell:nth(0)').should('contain', '2009-01-01'); + cy.get('.grid-canvas-right > [style="top: 0px;"] > .slick-cell:nth(1)').should('contain', '2009-05-05'); + }); + + it('should have exact Column Header Titles in the grid', () => { + cy.get('#grid20') + .find('.slick-header-columns:nth(0)') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + }); + + it('should click on the Grid Menu command "Unfreeze Columns/Rows" to switch to a regular grid without frozen columns and expect 7 columns on the left container', () => { + cy.get('#grid20') + .find('button.slick-grid-menu-button') + .click({ force: true }); + + cy.contains('Unfreeze Columns/Rows') + .click({ force: true }); + + cy.get('[style="top: 0px;"]').should('have.length', 1); + cy.get('.grid-canvas-left > [style="top: 0px;"]').children().should('have.length', 11); + + cy.get('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(0)').should('contain', '0'); + cy.get('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(1)').should('contain', 'Task 0'); + + cy.get('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(3)').should('contain', '2009-01-01'); + cy.get('.grid-canvas-left > [style="top: 0px;"] > .slick-cell:nth(4)').should('contain', '2009-05-05'); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example21.cy.ts b/demos/vue/test/cypress/e2e/example21.cy.ts new file mode 100644 index 000000000..c92d4ada8 --- /dev/null +++ b/demos/vue/test/cypress/e2e/example21.cy.ts @@ -0,0 +1,98 @@ +describe('Example 21 - Grid AutoHeight', () => { + const fullTitles = ['Title', 'Duration (days)', '% Complete', 'Start', 'Finish', 'Effort Driven']; + const GRID_ROW_HEIGHT = 35; + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example21`); + cy.get('h2').should('contain', 'Example 21: Grid AutoHeight'); + }); + + it('should have exact column titles in grid', () => { + cy.get('#slickGridContainer-grid21') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + }); + + it('should search for Duration over 50 and expect rows to be that', () => { + cy.get('[data-test="search-column-list"]') + .select('Duration (days)', { force: true }); + + cy.get('[data-test="search-operator-list"]') + .select('>', { force: true }); + + cy.get('[data-test="search-value-input"]') + .type('50', { force: true }); + + cy.get('#grid21') + .find('.slick-row .slick-cell:nth(1)') + .each(($child, index) => { + if (index > 10) { + return; + } + expect(+$child.text()).to.be.gt(50); + }); + }); + + it('should search for Duration below 50 and expect rows to be that', () => { + cy.get('[data-test="search-operator-list"]') + .select('<'); + + cy.wait(200); + + cy.get('#grid21') + .find('.slick-row .slick-cell:nth(1)') + .each(($child, index) => { + if (index > 10) { + return; + } + expect(+$child.text()).to.be.lt(50); + }); + }); + + it('should type a filter which returns an empty dataset', () => { + cy.get('[data-test="search-value-input"]') + .clear() + .type('zzz'); + + cy.get('.slick-empty-data-warning:visible') + .contains('No data to display.'); + }); + + it('should search for Title ending with text "5" expect rows to be (Task 5, 15, 25, ...)', () => { + cy.get('[data-test="clear-search-value"]') + .click(); + + cy.get('[data-test="search-column-list"]') + .select('Title', { force: true }); + + cy.get('[data-test="search-operator-list"]') + .select('EndsWith', { force: true }); + + cy.get('[data-test="search-value-input"]') + .type('5', { force: true }); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 5'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 15'); + }); + + it('should type a filter which returns an empty dataset', () => { + cy.get('[data-test="search-value-input"]') + .clear() + .type('zzz'); + + cy.get('.slick-empty-data-warning:visible') + .contains('No data to display.'); + }); + + it('should clear search input and expect empty dataset warning to go away and also expect data back (Task 0, 1, 2, ...)', () => { + cy.get('[data-test="clear-search-value"]') + .click(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 0'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 1'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 2'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 3'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 4'); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example22.cy.ts b/demos/vue/test/cypress/e2e/example22.cy.ts new file mode 100644 index 000000000..d359b54f4 --- /dev/null +++ b/demos/vue/test/cypress/e2e/example22.cy.ts @@ -0,0 +1,49 @@ +describe('Example 22 - Grids in Bootstrap Tabs', () => { + const GRID_ROW_HEIGHT = 35; + const grid1FullTitles = ['Title', 'Duration (days)', '% Complete', 'Start', 'Finish', 'Effort Driven']; + const grid2FullTitles = ['Name', 'Gender', 'Company']; + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example22`); + cy.get('h2').should('contain', 'Example 22: Grids in Bootstrap Tabs'); + }); + + it('should have exact column titles in grid', () => { + cy.get('#slickGridContainer-grid1') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(grid1FullTitles[index])); + }); + + it('should have "Task 0" incremented by 1 after each row', () => { + cy.get(`.tab-pane#javascript [style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 0'); + cy.get(`.tab-pane#javascript [style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 1'); + cy.get(`.tab-pane#javascript [style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 2'); + cy.get(`.tab-pane#javascript [style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 3'); + cy.get(`.tab-pane#javascript [style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 4'); + cy.get(`.tab-pane#javascript [style="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 5'); + }); + + it('should change open next Tab "Fetch-Client" and expect a grid with 3 columns', () => { + cy.get('#fetch-tab').click(); + + cy.get('#slickGridContainer-grid2') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(grid2FullTitles[index])); + }); + + it('should expect first 3 rows to be an exact match of data provided by the external JSON file', () => { + cy.get(`.tab-pane#fetch [style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l0`).should('contain', 'Ethel Price'); + cy.get(`.tab-pane#fetch [style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l1`).should('contain', 'female'); + cy.get(`.tab-pane#fetch [style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l2`).should('contain', 'Enersol'); + + cy.get(`.tab-pane#fetch [style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell.l0`).should('contain', 'Claudine Neal'); + cy.get(`.tab-pane#fetch [style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell.l1`).should('contain', 'female'); + cy.get(`.tab-pane#fetch [style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell.l2`).should('contain', 'Sealoud'); + + cy.get(`.tab-pane#fetch [style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell.l0`).should('contain', 'Beryl Rice'); + cy.get(`.tab-pane#fetch [style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell.l1`).should('contain', 'female'); + cy.get(`.tab-pane#fetch [style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell.l2`).should('contain', 'Velity'); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example23.cy.ts b/demos/vue/test/cypress/e2e/example23.cy.ts new file mode 100644 index 000000000..96b4203c8 --- /dev/null +++ b/demos/vue/test/cypress/e2e/example23.cy.ts @@ -0,0 +1,274 @@ +import { addDay, format, isAfter, isBefore, isEqual } from '@formkit/tempo'; + +const presetMinComplete = 5; +const presetMaxComplete = 80; +const presetMinDuration = 4; +const presetMaxDuration = 88; +const today = new Date(); +const presetLowestDay = format(addDay(new Date(), -2), 'YYYY-MM-DD'); +const presetHighestDay = format(addDay(new Date(), today.getDate() < 14 ? 28 : 25), 'YYYY-MM-DD'); + +function isBetween(inputDate: Date | string, minDate: Date | string, maxDate: Date | string, isInclusive = false) { + let valid = false; + if (isInclusive) { + valid = isEqual(inputDate, minDate) || isEqual(inputDate, maxDate); + } + if (!valid) { + valid = isAfter(inputDate, minDate) && isBefore(inputDate, maxDate); + } + return valid; +} + +describe('Example 23 - Range Filters', () => { + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example23`); + cy.get('h2').should('contain', 'Example 23: Filtering from Range of Search Values'); + }); + + it('should expect the grid to be sorted by "% Complete" descending and then by "Duration" ascending', () => { + cy.get('#grid23') + .get('.slick-header-column:nth(2)') + .find('.slick-sort-indicator-desc') + .should('have.length', 1) + .siblings('.slick-sort-indicator-numbered') + .contains('1'); + + cy.get('#grid23') + .get('.slick-header-column:nth(5)') + .find('.slick-sort-indicator-asc') + .should('have.length', 1) + .siblings('.slick-sort-indicator-numbered') + .contains('2'); + }); + + it('should have "% Complete" fields within the range (inclusive) of the filters presets', () => { + cy.get('#grid23') + .find('.slick-row') + .each(($row) => { + cy.wrap($row) + .children('.slick-cell:nth(2)') + .each(($cell) => { + const value = parseInt($cell.text().trim(), 10); + if (!isNaN(value)) { + expect(value >= presetMinComplete).to.eq(true); + expect(value <= presetMaxComplete).to.eq(true); + } + }); + }); + }); + + it('should have Finish Dates within the range (inclusive) of the filters presets', () => { + cy.get('#grid23') + .find('.slick-row') + .each(($row) => { + cy.wrap($row) + .children('.slick-cell:nth(4)') + .each(($cell) => { + const isDateBetween = isBetween($cell.text(), presetLowestDay, presetHighestDay, true); + expect(isDateBetween).to.eq(true); + }); + }); + }); + + it('should have "Duration" fields within the range (exclusive by default) of the filters presets', () => { + cy.get('#grid23') + .find('.slick-row') + .each(($row) => { + cy.wrap($row) + .children('.slick-cell:nth(5)') + .each(($cell) => { + const value = parseInt($cell.text().trim(), 10); + if (!isNaN(value)) { + expect(value >= presetMinDuration).to.eq(true); + expect(value <= presetMaxDuration).to.eq(true); + } + }); + }); + }); + + it('should change "% Complete" filter range by using the slider left handle (min value) to make it a higher min value and expect all rows to be within new range', () => { + let newLowest = presetMinComplete; + let newHighest = presetMaxComplete; + const allowedBuffer = 0.8; + + // first input is the lowest range + cy.get('.slider-filter-input:nth(0)') + .as('range').invoke('val', 10).trigger('change', { force: true }); + + cy.get('.lowest-range-percentComplete') + .then(($lowest) => { + newLowest = parseInt($lowest.text(), 10); + }); + + cy.get('.highest-range-percentComplete') + .then(($highest) => { + newHighest = parseInt($highest.text(), 10); + }); + + cy.wait(5); + + cy.get('#grid23') + .find('.slick-row') + .each(($row, idx) => { + if (idx > 6) { + return; + } + cy.wrap($row) + .children('.slick-cell:nth(2)') + .each(($cell) => { + const value = parseInt($cell.text().trim(), 10); + if (!isNaN(value) && $cell.text() !== '') { + expect(value >= (newLowest - allowedBuffer)).to.eq(true); + expect(value <= (newHighest + allowedBuffer)).to.eq(true); + } + }); + }); + }); + + it('should change the "Finish" date in the picker and expect all rows to be within new dates range', () => { + cy.get('.date-picker.search-filter.filter-finish') + .click(); + + cy.get('.vanilla-calendar-day_selected-first') + .should('exist'); + + cy.get('.vanilla-calendar-day_selected-intermediate') + .should('have.length.gte', 2); + + cy.get('.vanilla-calendar-day_selected-last') + .should('exist'); + }); + + it('should change the "Duration" input filter and expect all rows to be within new range', () => { + const newMin = 10; + const newMax = 40; + + cy.get('[data-test=clear-filters]') + .click({ force: true }); + + cy.get('.search-filter.filter-duration') + .focus() + .type(`${newMin}..${newMax}`); + + cy.get('#grid23') + .find('.slick-row') + .each(($row, idx) => { + cy.wrap($row) + .children('.slick-cell:nth(5)') + .each(($cell) => { + if (idx > 8) { + return; + } + const value = parseInt($cell.text().trim(), 10); + if (!isNaN(value)) { + expect(value >= newMin).to.eq(true); + expect(value <= newMax).to.eq(true); + } + }); + }); + }); + + describe('Set Dymamic Filters', () => { + const dynamicMinComplete = 15; + const dynamicMaxComplete = 85; + const dynamicMinDuration = 14; + const dynamicMaxDuration = 78; + const currentYear = new Date().getFullYear(); + const dynamicLowestDay = format(addDay(new Date(), -5), 'YYYY-MM-DD'); + const dynamicHighestDay = format(addDay(new Date(), 25), 'YYYY-MM-DD'); + + it('should click on Set Dynamic Filters', () => { + cy.get('[data-test=set-dynamic-filter]') + .click(); + }); + + it('should have "% Complete" fields within the exclusive range of the filters presets', () => { + cy.get('#grid23') + .find('.slick-row') + .each(($row) => { + cy.wrap($row) + .children('.slick-cell:nth(2)') + .each(($cell) => { + const value = parseInt($cell.text().trim(), 10); + if (!isNaN(value)) { + expect(value >= dynamicMinComplete).to.eq(true); + expect(value <= dynamicMaxComplete).to.eq(true); + } + }); + }); + }); + + it('should have "Duration" fields within the inclusive range of the dynamic filters', () => { + cy.get('#grid23') + .find('.slick-row') + .each(($row) => { + cy.wrap($row) + .children('.slick-cell:nth(5)') + .each(($cell) => { + const value = parseInt($cell.text().trim(), 10); + if (!isNaN(value)) { + expect(value >= dynamicMinDuration).to.eq(true); + expect(value <= dynamicMaxDuration).to.eq(true); + } + }); + }); + }); + + it('should have Finish Dates within the range (inclusive) of the dynamic filters', () => { + cy.get('.search-filter.filter-finish') + .find('input') + .invoke('val') + .then(text => expect(text).to.eq(`${dynamicLowestDay} โ€” ${dynamicHighestDay}`)); + + cy.get('#grid23') + .find('.slick-row') + .each(($row) => { + cy.wrap($row) + .children('.slick-cell:nth(4)') + .each(($cell) => { + const isDateBetween = isBetween($cell.text(), dynamicLowestDay, dynamicHighestDay, true); + expect(isDateBetween).to.eq(true); + }); + }); + }); + + it('should change dynamic filters from the select and choose "Current Year Completed Tasks" and expect 2 filters set', () => { + cy.get('[data-test=select-dynamic-filter]') + .select('Current Year Completed Tasks'); + + cy.get('.search-filter.filter-finish') + .find('input') + .invoke('val') + .then(text => expect(text).to.eq(`${currentYear}-01-01 โ€” ${currentYear}-12-31`)); + + cy.get('.ms-parent.search-filter.filter-completed > button > span') + .should('contain', 'True'); + }); + }); + + describe('Set Dynamic Sorting', () => { + it('should click on "Clear Filters" then "Set Dynamic Sorting" buttons', () => { + cy.get('[data-test=clear-filters]') + .click(); + + cy.get('[data-test=set-dynamic-sorting]') + .click(); + }); + + it('should expect the grid to be sorted by "Duration" ascending and "Start" descending', () => { + cy.get('#grid23') + .get('.slick-header-column:nth(2)') + .find('.slick-sort-indicator-asc') + .should('have.length', 1) + .siblings('.slick-sort-indicator-numbered') + .contains('2'); + + cy.get('#grid23') + .get('.slick-header-column:nth(4)') + .find('.slick-sort-indicator-desc') + .should('have.length', 1) + .siblings('.slick-sort-indicator-numbered') + .contains('1'); + }); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example24.cy.ts b/demos/vue/test/cypress/e2e/example24.cy.ts new file mode 100644 index 000000000..ffcc6042c --- /dev/null +++ b/demos/vue/test/cypress/e2e/example24.cy.ts @@ -0,0 +1,917 @@ +describe('Example 24 - Cell Menu & Context Menu Plugins', () => { + const GRID_ROW_HEIGHT = 35; + const fullEnglishTitles = ['#', 'Title', '% Complete', 'Start', 'Finish', 'Priority', 'Completed', 'Action']; + const fullFrenchTitles = ['#', 'Titre', '% Achevรฉe', 'Dรฉbut', 'Fin', 'Prioritรฉ', 'Terminรฉ', 'Action']; + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example24`); + cy.get('h2').should('contain', 'Example 24: Cell Menu & Context Menu Plugins'); + }); + + describe('English Locale', () => { + it('should have exact Column Titles in the grid', () => { + cy.get('#grid24') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.contain(fullEnglishTitles[index])); + }); + + it('should have first row with "Task 0" and a Priority set to a Yellow Star (low) with the Action cell disabled and not clickable', () => { + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(1)') + .contains('Task 0'); + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(5)') + .find('.mdi-star.yellow'); + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(6)') + .find('.mdi-check.checkmark-icon'); + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(7) .disabled') + .contains('Action'); + + cy.get('.slick-cell-menu') + .should('not.exist'); + }); + + it('should expect the Context Menu to not have the "Help" menu when there is Completed set to True', () => { + const commands = ['Copy', 'Export to Excel', '', 'Delete Row', '', 'Disabled Command', '', 'Exports', 'Feedback']; + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(1)') + .contains('Task 0'); + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(1)') + .rightclick({ force: true }); + + cy.get('.slick-context-menu.dropright .slick-menu-command-list') + .find('.slick-menu-item') + .each(($command, index) => { + expect($command.text()).to.contain(commands[index]); + expect($command.text()).not.include('Help'); + }); + }); + + it('should be able to click on the Context Menu (x) close button, on top right corner, to close the menu', () => { + cy.get('.slick-context-menu.dropright') + .should('exist'); + + cy.get('.slick-context-menu button.close') + .click(); + }); + + it('should change "Task 0" Priority to "High" with Context Menu and expect the Action Menu to become clickable and usable', () => { + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(1)') + .contains('Task 0'); + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(5)') + .rightclick({ force: true }); + + cy.get('.slick-context-menu .slick-menu-option-list') + .contains('High') + .click(); + + cy.get('.slick-context-menu .slick-menu-command-list') + .should('not.exist'); + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(7)') + .contains('Action') + .click({ force: true }); + + cy.get('.slick-cell-menu.dropleft') + .should('exist'); + }); + + it('should expect a "Command 2" to be disabled and not clickable (menu will remain open), in that same Action menu', () => { + cy.get('.slick-cell-menu .slick-menu-item.slick-menu-item-disabled') + .contains('Command 2') + .click({ force: true }); + + cy.get('.slick-cell-menu.dropleft') + .should('exist'); + }); + + it('should change the Completed to "False" in that same Action and then expect the "Command 2" to enabled and clickable', () => { + const alertStub = cy.stub(); + cy.on('window:alert', alertStub); + + cy.get('.slick-cell-menu .slick-menu-option-list') + .find('.slick-menu-item') + .contains('False') + .click(); + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(7)') + .contains('Action') + .click({ force: true }); + + cy.get('.slick-cell-menu .slick-menu-item') + .contains('Command 2') + .click() + .then(() => expect(alertStub.getCall(0)).to.be.calledWith('Command 2')); + }); + + it('should expect the Context Menu now have the "Help" menu when Completed is set to False', () => { + const commands = ['Copy', 'Export to Excel', '', 'Delete Row', '', 'Help', 'Disabled Command', '', 'Exports', 'Feedback']; + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(1)') + .contains('Task 0'); + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(6)') + .rightclick({ force: true }); + + cy.get('.slick-context-menu.dropleft .slick-menu-command-list') + .find('.slick-menu-item') + .each(($command, index) => expect($command.text()).to.contain(commands[index])); + + cy.get('.slick-context-menu button.close') + .click(); + }); + + it('should be able to click on the Action Cell Menu (x) close button, on top right corner, to close the menu', () => { + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(7)') + .contains('Action') + .click({ force: true }); + + cy.get('.slick-cell-menu.dropleft') + .should('exist'); + + cy.get('.slick-cell-menu button.close') + .click(); + + cy.get('.slick-cell-menu.dropleft') + .should('not.exist'); + }); + + it('should click on the "Show Commands & Priority Options" button and see both list when opening Context Menu', () => { + cy.get('[data-test=context-menu-commands-and-priority-button]') + .click(); + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(5)') + .rightclick({ force: true }); + + cy.get('.slick-context-menu .slick-menu-option-list') + .should('exist') + .contains('High'); + + cy.get('.slick-context-menu .slick-menu-command-list') + .find('.slick-menu-item.red') + .find('.slick-menu-content.bold') + .should('exist') + .contains('Delete Row'); + + cy.get('.slick-context-menu button.close') + .click(); + }); + + it('should click on the "Show Priority Options Only" button and see both list when opening Context Menu', () => { + cy.get('[data-test=context-menu-priority-only-button]') + .click(); + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(5)') + .rightclick({ force: true }); + + cy.get('.slick-context-menu .slick-menu-option-list') + .should('exist') + .contains('High'); + + cy.get('.slick-context-menu .slick-menu-command-list') + .should('not.exist'); + + cy.get('.slick-context-menu button.close') + .click(); + }); + + it('should click on the "Show Actions Commands & Completed Options" button and see both list when opening Action Cell Menu', () => { + cy.get('[data-test=cell-menu-commands-and-options-true-button]') + .click(); + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(7)') + .contains('Action') + .click({ force: true }); + + cy.get('.slick-cell-menu .slick-menu-option-list') + .should('exist') + .contains('True'); + + cy.get('.slick-cell-menu .slick-menu-command-list') + .should('exist') + .contains('Delete Row'); + + cy.get('.slick-cell-menu button.close') + .click(); + }); + + it('should open the Action Cell Menu and expect the Completed "null" option when this Effort is set to False', () => { + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(7)') + .contains('Action') + .click({ force: true }); + + cy.get('.slick-cell-menu.dropleft') + .should('exist'); + + cy.get('.slick-cell-menu') + .find('.slick-menu-option-list') + .find('.slick-menu-item.italic') + .find('.slick-menu-content') + .contains('null'); + }); + + it('should open the Action Cell Menu and not expect the Completed "null" option when this Effort is set to True', () => { + cy.get('.slick-cell-menu.dropleft') + .should('exist'); + + cy.get('.slick-cell-menu') + .find('.slick-menu-option-list') + .contains('True') + .click(); + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(7)') + .contains('Action') + .click({ force: true }); + + cy.get('.slick-cell-menu') + .each($row => { + expect($row.text()).not.include('null'); + }); + }); + + it('should reset Completed to False for the next test to include all commands', () => { + cy.get('.slick-cell-menu') + .find('.slick-menu-option-list') + .contains('False') + .click(); + }); + + it('should click on the "Show Action Commands Only" button and see both list when opening Context Menu', () => { + const commands = ['Command 1', 'Command 2', '', 'Delete Row', 'Help', 'Disabled Command', '', 'Exports', 'Feedback']; + + cy.get('[data-test=cell-menu-commands-and-options-false-button]') + .click(); + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(7)') + .contains('Action') + .click({ force: true }); + + cy.get('.slick-cell-menu .slick-menu-command-list') + .should('exist') + .find('.slick-menu-item') + .each(($command, index) => expect($command.text()).to.contain(commands[index])); + + cy.get('.slick-menu-option-list') + .should('not.exist'); + + cy.get('.slick-cell-menu button.close') + .click(); + }); + + it('should be able to delete first row by using the "Delete Row" command from the Context Menu', () => { + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(1)') + .contains('Task 0'); + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(1)') + .rightclick({ force: true }); + + cy.get('.slick-context-menu .slick-menu-command-list') + .find('.slick-menu-item.red') + .find('.slick-menu-content.bold') + .should('exist') + .contains('Delete Row') + .click(); + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(1)') + .each($row => { + expect($row.text()).not.include('Task 0'); + }); + }); + + it('should be able to delete the 3rd row "Task 3" by using the "Delete Row" command from the Action Cell Menu', () => { + cy.get('#grid24') + .find('.slick-row:nth(2) .slick-cell:nth(1)') + .contains('Task 3'); + + cy.get('#grid24') + .find('.slick-row:nth(2) .slick-cell:nth(7)') + .contains('Action') + .click({ force: true }); + + cy.get('.slick-cell-menu .slick-menu-command-list') + .find('.slick-menu-item.red') + .find('.slick-menu-content.bold') + .should('exist') + .contains('Delete Row') + .click(); + + cy.get('#grid24') + .find('.slick-row:nth(2) .slick-cell:nth(1)') + .each($row => { + expect($row.text()).not.include('Task 3'); + }); + }); + + it('should check Context Menu "menuUsabilityOverride" condition and expect to not be able to open Context Menu from rows than are >= to Task 21', () => { + cy.get('.slick-viewport-top.slick-viewport-left') + .scrollTo('bottom') + .wait(50); + + cy.get('#grid24') + .find('.slick-row:nth(3) .slick-cell:nth(1)') + .rightclick({ force: true }); + + cy.get('.slick-context-menu .slick-menu-command-list') + .should('not.exist'); + }); + + it('should scroll back to top row and be able to open Context Menu', () => { + cy.get('.slick-viewport-top.slick-viewport-left') + .scrollTo('top') + .wait(50); + + cy.get('#grid24') + .find('.slick-row:nth(1) .slick-cell:nth(1)') + .rightclick({ force: true }); + + cy.get('.slick-context-menu .slick-menu-command-list') + .should('exist'); + + cy.get('.slick-context-menu button.close') + .click(); + }); + }); + + describe('French Locale', () => { + it('should switch locale to French', () => { + cy.get('#grid24') + .find('button.slick-grid-menu-button') + .trigger('click') + .click(); + + cy.get('[data-test=language-button]') + .click(); + + cy.get('[data-test=selected-locale]') + .should('contain', 'fr.json'); + }); + + it('should show both Commands & Options on the Action Cell Menu', () => { + cy.get('[data-test=cell-menu-commands-and-options-true-button]') + .click(); + }); + + it('should have exact Column Titles in the grid', () => { + cy.get('#grid24') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.contain(fullFrenchTitles[index])); + }); + + it('should have first row with "Tรขche 1" and a Priority set to a Orange Star (medium) with the Action cell disabled and not clickable', () => { + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(1)') + .contains('Tรขche 1'); + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(5)') + .find('.mdi-star.orange'); + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(7) .disabled') + .contains('Action'); + + cy.get('.slick-cell-menu') + .should('not.exist'); + }); + + it('should expect the Context Menu to not have the "Aide" menu when there is Completed set to False', () => { + const commands = ['Copier', 'Exporter vers Excel', '', 'Supprimer la ligne', '', 'Aide', 'Commande dรฉsactivรฉe', '', 'Exports', 'Feedback']; + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(1)') + .contains('Tรขche 1'); + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(1)') + .rightclick({ force: true }); + + cy.get('.slick-context-menu.dropright .slick-menu-command-list') + .find('.slick-menu-item') + .each(($command, index) => expect($command.text()).to.contain(commands[index])); + }); + + it('should be able to click on the Context Menu (x) close button, on top right corner, to close the menu', () => { + cy.get('.slick-context-menu.dropright') + .should('exist'); + + cy.get('.slick-context-menu button.close') + .click(); + }); + + it('should change "Tรขche 1" Priority to "Haut" with Context Menu and expect the Action Menu to become clickable and usable', () => { + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(1)') + .contains('Tรขche 1'); + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(5)') + .rightclick({ force: true }); + + cy.get('.slick-context-menu .slick-menu-option-list') + .contains('Haut') + .click(); + + cy.get('.slick-context-menu .slick-menu-command-list') + .should('not.exist'); + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(7)') + .contains('Action') + .click({ force: true }); + + cy.get('.slick-cell-menu.dropleft') + .should('exist'); + }); + + it('should expect a "Command 2" to be enabled and clickable in that same Action menu', () => { + const stub = cy.stub(); + cy.on('window:alert', stub); + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(7)') + .contains('Action') + .click({ force: true }); + + cy.get('.slick-cell-menu .slick-menu-item') + .contains('Command 2') + .click() + .then(() => expect(stub.getCall(0)).to.be.calledWith('Command 2')); + }); + + it('should expect the Context Menu now have the "Aide" menu when Completed is set to False', () => { + const commands = ['Copier', 'Exporter vers Excel', '', 'Supprimer la ligne', '', 'Aide', 'Commande dรฉsactivรฉe', '', 'Exports', 'Feedback']; + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(1)') + .contains('Tรขche 1'); + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(6)') + .rightclick({ force: true }); + + cy.get('.slick-context-menu.dropleft .slick-menu-command-list') + .find('.slick-menu-item') + .each(($command, index) => expect($command.text()).to.contain(commands[index])); + + cy.get('.slick-context-menu button.close') + .click(); + }); + + it('should be able to click on the Action Cell Menu (x) close button, on top right corner, to close the menu', () => { + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(7)') + .contains('Action') + .click({ force: true }); + + cy.get('.slick-cell-menu.dropleft') + .should('exist'); + + cy.get('.slick-cell-menu button.close') + .click(); + + cy.get('.slick-cell-menu.dropleft') + .should('not.exist'); + }); + + it('should click on the "Show Commands & Priority Options" button and see both list when opening Context Menu', () => { + cy.get('[data-test=context-menu-commands-and-priority-button]') + .click(); + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(5)') + .rightclick({ force: true }); + + cy.get('.slick-context-menu .slick-menu-option-list') + .should('exist') + .contains('Haut'); + + cy.get('.slick-context-menu .slick-menu-command-list') + .find('.slick-menu-item.red') + .find('.slick-menu-content.bold') + .should('exist') + .contains('Supprimer la ligne'); + + cy.get('.slick-context-menu button.close') + .click(); + }); + + it('should click on the "Show Priority Options Only" button and see both list when opening Context Menu', () => { + cy.get('[data-test=context-menu-priority-only-button]') + .click(); + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(5)') + .rightclick({ force: true }); + + cy.get('.slick-context-menu .slick-menu-option-list') + .should('exist') + .contains('Haut'); + + cy.get('.slick-context-menu .slick-menu-command-list') + .should('not.exist'); + + cy.get('.slick-context-menu button.close') + .click(); + }); + + it('should click on the "Show Actions Commands & Completed Options" button and see both list when opening Action Cell Menu', () => { + cy.get('[data-test=cell-menu-commands-and-options-true-button]') + .click(); + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(7)') + .contains('Action') + .click({ force: true }); + + cy.get('.slick-cell-menu .slick-menu-option-list') + .should('exist') + .contains('Vrai'); + + cy.get('.slick-menu-command-list') + .should('exist') + .contains('Supprimer la ligne'); + + cy.get('.slick-cell-menu button.close') + .click(); + }); + + it('should open the Action Cell Menu and expect the Completed "null" option when this Effort is set to Faux', () => { + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(7)') + .contains('Action') + .click({ force: true }); + + cy.get('.slick-cell-menu.dropleft') + .should('exist'); + + cy.get('.slick-cell-menu') + .find('.slick-menu-option-list') + .find('.slick-menu-item.italic') + .find('.slick-menu-content') + .contains('null'); + }); + + it('should open the Action Cell Menu and not expect the Completed "null" option when this Effort is set to True', () => { + cy.get('.slick-cell-menu.dropleft') + .should('exist'); + + cy.get('.slick-cell-menu') + .find('.slick-menu-option-list') + .contains('Vrai') + .click(); + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(7)') + .contains('Action') + .click({ force: true }); + + cy.get('.slick-cell-menu') + .each($row => { + expect($row.text()).not.include('null'); + }); + }); + + it('should reset Completed to Faux for the next test to include all commands', () => { + cy.get('.slick-cell-menu') + .find('.slick-menu-option-list') + .contains('Faux') + .click(); + }); + + it('should click on the "Show Action Commands Only" button and see both list when opening Context Menu', () => { + const commands = ['Command 1', 'Command 2', '', 'Supprimer la ligne', 'Aide', 'Commande dรฉsactivรฉe', '', 'Exports', 'Feedback']; + + cy.get('[data-test=cell-menu-commands-and-options-false-button]') + .click(); + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(7)') + .contains('Action') + .click({ force: true }); + + cy.get('.slick-cell-menu .slick-menu-command-list') + .should('exist') + .find('.slick-menu-item') + .each(($command, index) => expect($command.text()).to.contain(commands[index])); + + cy.get('.slick-menu-option-list') + .should('not.exist'); + + cy.get('.slick-cell-menu button.close') + .click(); + }); + + it('should be able to delete first row by using the "Supprimer la ligne" command from the Context Menu', () => { + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(1)') + .contains('Tรขche 1'); + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(1)') + .rightclick({ force: true }); + + cy.get('.slick-context-menu .slick-menu-command-list') + .find('.slick-menu-item.red') + .find('.slick-menu-content.bold') + .should('exist') + .contains('Supprimer la ligne') + .click(); + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(1)') + .each($row => { + expect($row.text()).not.include('Tรขche 1'); + }); + }); + + it('should be able to delete the 4th row "Tรขche 6" by using the "Supprimer la ligne" command from the Action Cell Menu', () => { + cy.get('#grid24') + .find('.slick-row:nth(3) .slick-cell:nth(1)') + .contains('Tรขche 6'); + + cy.get('#grid24') + .find('.slick-row:nth(3) .slick-cell:nth(7)') + .contains('Action') + .click({ force: true }); + + cy.get('.slick-cell-menu .slick-menu-command-list') + .find('.slick-menu-item.red') + .find('.slick-menu-content.bold') + .should('exist') + .contains('Supprimer la ligne') + .click(); + + cy.get('#grid24') + .find('.slick-row:nth(3) .slick-cell:nth(1)') + .each($row => { + expect($row.text()).not.include('Tรขche 6'); + }); + }); + + it('should switch back locale to English before leaving', () => { + cy.get('#grid24') + .find('button.slick-grid-menu-button') + .trigger('click') + .click(); + + cy.get('[data-test=language-button]') + .click(); + + cy.get('[data-test=selected-locale]') + .should('contain', 'en.json'); + }); + }); + + describe('with sub-menus', () => { + it('should reopen Context Menu hover "Priority" column then open options sub-menu & select "High" option and expect Task to be set to High in the UI', () => { + const subOptions = ['Low', 'Medium', 'High']; + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] .slick-cell:nth(5)`) + .rightclick({ force: true }); + + cy.get('.slick-context-menu.slick-menu-level-0 .slick-menu-option-list') + .find('.slick-menu-item .slick-menu-content') + .contains('Sub-Options (demo)') + .click(); + + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-option-list').as('subMenuList'); + cy.get('@subMenuList').find('.slick-menu-title').contains('Change Priority'); + cy.get('@subMenuList') + .should('exist') + .find('.slick-menu-item .slick-menu-content') + .each(($command, index) => expect($command.text()).to.eq(subOptions[index])); + + cy.get('@subMenuList') + .find('.slick-menu-item .slick-menu-content') + .contains('High') + .click(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] .slick-cell:nth(5)`) + .find('.mdi-star.red'); + }); + + it('should be able to open Context Menu from any other cell and click on Export->Text and expect alert triggered with Text Export', () => { + const subCommands1 = ['Text', 'Excel']; + const stub = cy.stub(); + cy.on('window:alert', stub); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] .slick-cell:nth(1)`) + .should('contain', 'Task 2'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] .slick-cell:nth(1)`) + .rightclick({ force: true }); + + cy.get('.slick-context-menu.slick-menu-level-0 .slick-menu-command-list') + .find('.slick-menu-item .slick-menu-content') + .contains(/^Exports$/) + .click(); + + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list') + .should('exist') + .find('.slick-menu-item') + .each(($command, index) => expect($command.text()).to.contain(subCommands1[index])); + + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list') + .find('.slick-menu-item') + .contains('Text') + .click() + .then(() => expect(stub.getCall(0)).to.be.calledWith('Exporting as Text (tab delimited)')); + }); + + it('should be able to open Context Menu and click on Export->Excel-> sub-commands to see 1 context menu + 1 sub-menu then clicking on Text should call alert action', () => { + const subCommands1 = ['Text', 'Excel']; + const subCommands2 = ['Excel (csv)', 'Excel (xlsx)']; + const stub = cy.stub(); + cy.on('window:alert', stub); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] .slick-cell:nth(1)`).should('contain', 'Task 2'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] .slick-cell:nth(1)`) + .rightclick({ force: true }); + + cy.get('.slick-context-menu.slick-menu-level-0 .slick-menu-command-list') + .find('.slick-menu-item .slick-menu-content') + .contains(/^Exports$/) + .click(); + + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list') + .should('exist') + .find('.slick-menu-item .slick-menu-content') + .each(($command, index) => expect($command.text()).to.contain(subCommands1[index])); + + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list') + .find('.slick-menu-item .slick-menu-content') + .contains('Excel') + .click(); + + cy.get('.slick-context-menu.slick-menu-level-2 .slick-menu-command-list').as('subMenuList2'); + + cy.get('@subMenuList2') + .find('.slick-menu-title') + .contains('available formats'); + + cy.get('@subMenuList2') + .should('exist') + .find('.slick-menu-item .slick-menu-content') + .each(($command, index) => expect($command.text()).to.contain(subCommands2[index])); + + cy.get('.slick-context-menu.slick-menu-level-2 .slick-menu-command-list') + .find('.slick-menu-item .slick-menu-content') + .contains('Excel (xlsx)') + .click() + .then(() => expect(stub.getCall(0)).to.be.calledWith('Exporting as Excel (xlsx)')); + }); + + it('should click on the "Show Commands & Priority Options" button and see both list when opening Context Menu', () => { + cy.get('[data-test=context-menu-commands-and-priority-button]') + .click(); + + cy.get('#grid24') + .find('.slick-row .slick-cell:nth(5)') + .rightclick({ force: true }); + + cy.get('.slick-context-menu .slick-menu-option-list') + .should('exist') + .contains('High'); + + cy.get('.slick-context-menu .slick-menu-command-list') + .find('.slick-menu-item.red') + .find('.slick-menu-content.bold') + .should('exist') + .contains('Delete Row'); + + cy.get('.slick-context-menu button.close') + .click(); + }); + + it('should open Export->Excel sub-menu & open again Sub-Options on top and expect sub-menu to be recreated with that Sub-Options list instead of the Export->Excel list', () => { + const subCommands1 = ['Text', 'Excel']; + const subCommands2 = ['Excel (csv)', 'Excel (xlsx)']; + const subOptions = ['Low', 'Medium', 'High']; + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5)`); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5)`) + .rightclick({ force: true }); + + cy.get('.slick-context-menu.slick-menu-level-0 .slick-menu-command-list') + .find('.slick-menu-item .slick-menu-content') + .contains(/^Exports$/) + .click(); + + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list') + .should('exist') + .find('.slick-menu-item .slick-menu-content') + .each(($command, index) => expect($command.text()).to.contain(subCommands1[index])); + + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list') + .find('.slick-menu-item .slick-menu-content') + .contains('Excel') + .click(); + + cy.get('.slick-context-menu.slick-menu-level-2 .slick-menu-command-list') + .should('exist') + .find('.slick-menu-item .slick-menu-content') + .each(($command, index) => expect($command.text()).to.contain(subCommands2[index])); + + cy.get('.slick-context-menu.slick-menu-level-0 .slick-menu-option-list') + .find('.slick-menu-item .slick-menu-content') + .contains('Sub-Options') + .click(); + + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-option-list').as('optionSubList2'); + + cy.get('@optionSubList2') + .find('.slick-menu-title') + .contains('Change Priority'); + + cy.get('@optionSubList2') + .should('exist') + .find('.slick-menu-item .slick-menu-content') + .each(($option, index) => expect($option.text()).to.contain(subOptions[index])); + }); + + it('should open Export->Excel context sub-menu then open Feedback->ContactUs sub-menus and expect previous Export menu to no longer exists', () => { + const subCommands1 = ['Text', 'Excel']; + const subCommands2 = ['Request update from supplier', '', 'Contact Us']; + const subCommands2_1 = ['Email us', 'Chat with us', 'Book an appointment']; + + const stub = cy.stub(); + cy.on('window:alert', stub); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5)`); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5)`) + .rightclick({ force: true }); + + cy.get('.slick-context-menu.slick-menu-level-0 .slick-menu-command-list') + .find('.slick-menu-item .slick-menu-content') + .contains(/^Exports$/) + .click(); + + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list') + .should('exist') + .find('.slick-menu-item .slick-menu-content') + .each(($command, index) => expect($command.text()).to.contain(subCommands1[index])); + + // click different sub-menu + cy.get('.slick-context-menu.slick-menu-level-0 .slick-menu-command-list') + .find('.slick-menu-item .slick-menu-content') + .contains('Feedback') + .should('exist') + .click(); + + cy.get('.slick-submenu').should('have.length', 1); + cy.get('.slick-context-menu.slick-menu-level-1 .slick-menu-command-list') + .should('exist') + .find('.slick-menu-item .slick-menu-content') + .each(($command, index) => expect($command.text()).to.contain(subCommands2[index])); + + // click on Feedback->ContactUs + cy.get('.slick-context-menu.slick-menu-level-1.dropleft') // left align + .find('.slick-menu-item .slick-menu-content') + .contains('Contact Us') + .should('exist') + .trigger('mouseover'); // mouseover or click should work + + cy.get('.slick-submenu').should('have.length', 2); + cy.get('.slick-context-menu.slick-menu-level-2.dropright') // right align + .should('exist') + .find('.slick-menu-item .slick-menu-content') + .each(($command, index) => expect($command.text()).to.eq(subCommands2_1[index])); + + cy.get('.slick-context-menu.slick-menu-level-2'); + + cy.get('.slick-context-menu.slick-menu-level-2 .slick-menu-command-list') + .find('.slick-menu-item .slick-menu-content') + .contains('Chat with us') + .click() + .then(() => expect(stub.getCall(0)).to.be.calledWith('Command: contact-chat')); + + cy.get('.slick-submenu').should('have.length', 0); + }); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example25.cy.ts b/demos/vue/test/cypress/e2e/example25.cy.ts new file mode 100644 index 000000000..b6db3ce89 --- /dev/null +++ b/demos/vue/test/cypress/e2e/example25.cy.ts @@ -0,0 +1,200 @@ + + +describe('Example 25 - GraphQL Basic API without Pagination', () => { + const GRID_ROW_HEIGHT = 35; + const fullPreTitles = ['Country', 'Language', 'Continent']; + const fullTitles = ['Code', 'Name', 'Native', 'Phone Area Code', 'Currency', 'Emoji', 'Names', 'Native', 'Codes', 'Name', 'Code']; + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example25`); + cy.get('h2').should('contain', 'Example 25: GraphQL Basic API without Pagination'); + }); + + it('should display a processing alert which will change to done', () => { + // cy.get('[data-test=status]').should('contain', 'processing'); + cy.get('[data-test=status]').should('contain', 'finished'); + }); + + it('should have exact Column Pre-Header & Column Header Titles in the grid', () => { + cy.get('#grid25') + .find('.slick-header-columns:nth(0)') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullPreTitles[index])); + + cy.get('#grid25') + .find('.slick-header-columns:nth(1)') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + }); + + it('should expect first 3 rows to be an exact match of data provided by the external GraphQL API', () => { + cy.get('.right-footer.metrics') + .contains('250 of 250 items'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'AD'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Andorra'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', 'Andorra'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).should('contain', '376'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(4)`).should('contain', 'EUR'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(6)`).should('contain', 'Catalan'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(7)`).should('contain', 'Catalร '); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(8)`).should('contain', 'ca'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(9)`).should('contain', 'Europe'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(10)`).should('contain', 'EU'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'AE'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'United Arab Emirates'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2)`).should('contain', 'ุฏูˆู„ุฉ ุงู„ุฅู…ุงุฑุงุช ุงู„ุนุฑุจูŠุฉ ุงู„ู…ุชุญุฏุฉ'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3)`).should('contain', '971'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(4)`).should('contain', 'AED'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(6)`).should('contain', 'Arabic'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(7)`).should('contain', 'ุงู„ุนุฑุจูŠุฉ'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(8)`).should('contain', 'ar'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(9)`).should('contain', 'Asia'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(10)`).should('contain', 'AS'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'AF'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Afghanistan'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(2)`).should('contain', 'ุงูุบุงู†ุณุชุงู†'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(3)`).should('contain', '93'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(4)`).should('contain', 'AFN'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(6)`).should('contain', 'Pashto, Uzbek, Turkmen'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(7)`).should('contain', 'ูพฺšุชูˆ, ะŽะทะฑะตะบ, ะขัƒั€ะบะผะตะฝ / ุชุฑูƒู…ู†'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(8)`).should('contain', 'ps, uz, tk'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(9)`).should('contain', 'Asia'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(10)`).should('contain', 'AS'); + }); + + it('should sort by country name and expect first 2 rows as Afghanistan and Albania', () => { + cy.get(`.slick-header-columns:nth(1) .slick-header-column:nth-child(2)`).contains('Name').click(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'AF'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Afghanistan'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', 'ุงูุบุงู†ุณุชุงู†'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).should('contain', '93'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'AL'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Albania'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2)`).should('contain', 'Shqipรซria'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3)`).should('contain', '355'); + }); + + it('should filter by Language Codes "fr, de" and expect 2 rows of data in the grid', () => { + cy.get('.search-filter.filter-languageCode') + .type('fr, de'); + + cy.get('.right-footer.metrics') + .contains('2 of 250 items'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'BE'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Belgium'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', 'Belgiรซ'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).should('contain', '32'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(4)`).should('contain', 'EUR'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(6)`).should('contain', 'Dutch, French, German'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(7)`).should('contain', 'Nederlands, Franรงais, Deutsch'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(8)`).should('contain', 'nl, fr, de'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(9)`).should('contain', 'Europe'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(10)`).should('contain', 'EU'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'LU'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Luxembourg'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2)`).should('contain', 'Luxembourg'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3)`).should('contain', '352'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(4)`).should('contain', 'EUR'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(6)`).should('contain', 'French, German, Luxembourgish'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(7)`).should('contain', 'Franรงais, Deutsch, Lรซtzebuergesch'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(8)`).should('contain', 'fr, de, lb'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(9)`).should('contain', 'Europe'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(10)`).should('contain', 'EU'); + }); + + it('should Clear all Filters and expect all rows to be back', () => { + cy.get('#grid25') + .find('button.slick-grid-menu-button') + .click(); + + cy.get(`.slick-grid-menu:visible`) + .find('.slick-menu-item') + .first() + .find('span') + .contains('Clear all Filters') + .click(); + + cy.get('.right-footer.metrics') + .contains('250 of 250 items'); + }); + + it('should filter Language Native with "Aymar" and expect only 1 row in the grid', () => { + cy.get('div.ms-filter.filter-languageNative') + .trigger('click'); + + cy.get('.ms-search:visible') + .type('Aymar'); + + cy.get('.ms-drop:visible') + .contains('Aymar') + .click(); + + cy.get('.ms-ok-button:visible') + .click(); + + cy.get('.right-footer.metrics') + .contains('1 of 250 items'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'BO'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Bolivia'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', 'Bolivia'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).should('contain', '591'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(4)`).should('contain', 'BOB,BOV'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(6)`).should('contain', 'Spanish, Aymara, Quechua'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(7)`).should('contain', 'Espaรฑol, Aymar, Runa Simi'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(8)`).should('contain', 'es, ay, qu'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(9)`).should('contain', 'South America'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(10)`).should('contain', 'SA'); + }); + + it('should Clear all Filters and expect all rows to be back', () => { + cy.get('#grid25') + .find('button.slick-grid-menu-button') + .click(); + + cy.get(`.slick-grid-menu:visible`) + .find('.slick-menu-item') + .first() + .find('span') + .contains('Clear all Filters') + .click(); + + cy.get('.right-footer.metrics') + .contains('250 of 250 items'); + }); + + it('should filter Language Native with "French" language and expect only 40 rows in the grid', () => { + cy.get('div.ms-filter.filter-languageName') + .trigger('click'); + + cy.get('.ms-search:visible') + .type('French'); + + cy.get('.ms-drop:visible') + .contains('French') + .click(); + + cy.get('.ms-ok-button:visible') + .click(); + + cy.get('.right-footer.metrics') + .contains('44 of 250 items'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Belgium'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(6)`).should('contain', 'Dutch, French, German'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(8)`).should('contain', 'nl, fr, de'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(9)`).should('contain', 'Europe'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1)`).should('contain', 'Benin'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(6)`).should('contain', 'French'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(8)`).should('contain', 'fr'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(9)`).should('contain', 'Africa'); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example27.cy.ts b/demos/vue/test/cypress/e2e/example27.cy.ts new file mode 100644 index 000000000..3aeb58d0a --- /dev/null +++ b/demos/vue/test/cypress/e2e/example27.cy.ts @@ -0,0 +1,315 @@ + +import { changeTimezone, zeroPadding } from '../plugins/utilities'; + +function removeExtraSpaces(text) { + return `${text}`.replace(/\s+/g, ' ').trim(); +} + +describe('Example 27 - Tree Data (from a flat dataset with parentId references)', () => { + const GRID_ROW_HEIGHT = 40; + const titles = ['Title', 'Duration', '% Complete', 'Start', 'Finish', 'Effort Driven']; + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example27`); + cy.get('h2').should('contain', 'Example 27: Tree Data'); + cy.get('h2').should('contain', 'from a flat dataset with parentId references'); + }); + + it('should have exact column titles in grid', () => { + cy.get('#grid27') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(titles[index])); + }); + + it('should expect all rows to be collapsed on first page load', () => { + cy.get('#grid27') + .find('.slick-group-toggle.expanded') + .should('have.length', 0); + + cy.get('#grid27') + .find('.slick-group-toggle.collapsed') + .should(($rows) => expect($rows).to.have.length.greaterThan(0)); + }); + + it('should have a Grid Preset Filter on 3rd column "% Complete" and expect all rows to be filtered as well', () => { + cy.get('.input-group-text.highest-range-percentComplete') + .contains('25'); + + cy.get('.search-filter.filter-percentComplete') + .find('.input-group-addon.operator select') + .contains('>='); + }); + + it('should expand all rows from "Expand All" button', () => { + cy.get('[data-test=expand-all-btn]') + .contains('Expand All') + .click(); + + cy.get('#grid27') + .find('.slick-group-toggle.collapsed') + .should('have.length', 0); + + cy.get('#grid27') + .find('.slick-group-toggle.expanded') + .should(($rows) => expect($rows).to.have.length.greaterThan(0)); + }); + + it('should collapsed all rows from "Collapse All" button', () => { + cy.get('[data-test=collapse-all-btn]') + .contains('Collapse All') + .click(); + + cy.get('#grid27') + .find('.slick-group-toggle.expanded') + .should('have.length', 0); + + cy.get('#grid27') + .find('.slick-group-toggle.collapsed') + .should(($rows) => expect($rows).to.have.length.greaterThan(0)); + }); + + it('should collapsed all rows from "Collapse All" context menu', () => { + cy.get('#grid27') + .contains('5 days'); + + cy.get('#grid27') + .find('.slick-row .slick-cell:nth(1)') + .rightclick({ force: true }); + + cy.get('.slick-context-menu.dropright .slick-menu-command-list') + .find('.slick-menu-item') + .find('.slick-menu-content') + .contains('Collapse all Groups') + .click(); + + cy.get('#grid27') + .find('.slick-group-toggle.expanded') + .should('have.length', 0); + + cy.get('#grid27') + .find('.slick-group-toggle.collapsed') + .should(($rows) => expect($rows).to.have.length.greaterThan(0)); + }); + + it('should collapsed all rows from "Expand All" context menu', () => { + cy.get('#grid27') + .contains('5 days'); + + cy.get('#grid27') + .find('.slick-row .slick-cell:nth(1)') + .rightclick({ force: true }); + + cy.get('.slick-context-menu.dropright .slick-menu-command-list') + .find('.slick-menu-item') + .find('.slick-menu-content') + .contains('Expand all Groups') + .click(); + + cy.get('#grid27') + .find('.slick-group-toggle.collapsed') + .should('have.length', 0); + + cy.get('#grid27') + .find('.slick-group-toggle.expanded') + .should(($rows) => expect($rows).to.have.length.greaterThan(0)); + }); + + it('should have data filtered, with "% Complete" >=25, and not show the full item count in the footer', () => { + cy.get('.search-filter.filter-percentComplete .operator .form-control') + .should('have.value', '>='); + + cy.get('input.slider-filter-input') + .invoke('val') + .then(text => expect(text).to.eq('25')); + + cy.get('.search-filter .input-group-text') + .should($span => expect($span.text()).to.eq('25')); + + cy.get('.right-footer') + .should($span => { + const text = removeExtraSpaces($span.text()); // remove all white spaces + expect(text).not.to.eq('500 of 500 items'); + }); + }); + + it('should open the Grid Menu "Clear all Filters" command', () => { + cy.get('#grid27') + .find('button.slick-grid-menu-button') + .click(); + + cy.get(`.slick-grid-menu:visible`) + .find('.slick-menu-item') + .first() + .find('span') + .contains('Clear all Filters') + .click(); + }); + + it('should no longer have filters and it should show the full item count in the footer', () => { + cy.get('.search-filter.filter-percentComplete .operator .form-control') + .should('have.value', ''); + + cy.get('input.slider-filter-input') + .invoke('val') + .then(text => expect(text).to.eq('0')); + + cy.get('.search-filter .input-group-text') + .should($span => expect($span.text()).to.eq('0')); + + cy.get('.right-footer') + .should($span => { + const text = removeExtraSpaces($span.text()); // remove all white spaces + expect(text).to.eq('500 of 500 items'); + }); + }); + + it('should click on the "Dynamically Change Filter" button and expect all child items to have a "% Complete" lower than 40', () => { + cy.get('[data-test="change-filter-dynamically"]').click(); + cy.get('[data-test=expand-all-btn]') + .contains('Expand All') + .click(); + + const readLineCount = 10; + for (let row = 0; row < readLineCount; row++) { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * row}px;"]`) + .should($elm => { + // only read the percent complete value if it's not a parent + const $slickGroupToggleNotExpanded = $elm.children('.slick-cell:nth(0)').children('.slick-group-toggle:not(.expanded)'); + if ($slickGroupToggleNotExpanded.length > 1) { + const percentComplete = $elm.children('.slick-cell:nth(2)').first().text(); + expect(+percentComplete).to.be.lt(40); + } + }); + } + }); + + it('should open the Grid Menu "Clear all Filters" command', () => { + cy.get('#grid27') + .find('button.slick-grid-menu-button') + .trigger('click') + .click(); + + cy.get(`.slick-grid-menu:visible`) + .find('.slick-menu-item') + .first() + .find('span') + .contains('Clear all Filters') + .click(); + }); + + it('should add an item (Task 500) in the first parent it finds and so we should expect it to be inserted at tree level 1', () => { + cy.get('[data-test=add-item-btn]') + .contains('Add New Item') + .click(); + + cy.get('.slick-tree-title[level=1]') + .get('.slick-cell') + .contains('Task 500'); + }); + + it('should be able to update the 1st row item (Task 0)', () => { + cy.get('[data-test=update-item-btn]') + .contains('Update 1st Row Item') + .click(); + + cy.get('.slick-viewport-top.slick-viewport-left') + .scrollTo('top'); + + const now = new Date(); + const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; + const today = changeTimezone(now, tz); + + const currentDate = today.getDate(); + let currentMonth: number | string = today.getMonth() + 1; // month is zero based, let's add 1 to it + if (currentMonth < 10) { + currentMonth = `0${currentMonth}`; // add zero padding + } + const currentYear = today.getFullYear(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 0'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', '11 days'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '77%'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).should('contain', `${currentYear}-${zeroPadding(currentMonth)}-${zeroPadding(currentDate)}`); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(4)`).should('contain', `${currentYear}-${zeroPadding(currentMonth)}-${zeroPadding(currentDate)}`); + }); + + it('should collapse the Tree and not expect to see the newly inserted item (Task 500) because all child will be collapsed', () => { + cy.get('[data-test=collapse-all-btn]') + .contains('Collapse All') + .click(); + + cy.get('.slick-tree-title[level=1]') + .should('have.length', 0); + + cy.get('.slick-tree-title') + .get('.slick-cell') + .contains(/^((?!Task 500).)*$/); + }); + + it('should open the Grid Menu "Clear all Filters" command', () => { + cy.get('#grid27') + .find('button.slick-grid-menu-button') + .trigger('click') + .click(); + + cy.get(`.slick-grid-menu:visible`) + .find('.slick-menu-item') + .first() + .find('span') + .contains('Clear all Filters') + .click(); + + cy.get('.slick-viewport-top.slick-viewport-left') + .scrollTo('top'); + }); + + it('should be able to open "Task 1" and "Task 3" parents', () => { + /* + we should find this structure + Task 0 + Task 1 + Task 2 + Task 3 + Task 4 + ... + */ + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 1'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0) .slick-group-toggle.collapsed`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0) .slick-group-toggle.collapsed`).click({ force: true }); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 2'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 3'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(0) .slick-group-toggle.collapsed`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(0) .slick-group-toggle.collapsed`).click({ force: true }); + }); + + it('should be able to click on the "Collapse All (wihout event)" button', () => { + cy.get('[data-test=collapse-all-noevent-btn]') + .contains('Collapse All (without triggering event)') + .click(); + }); + + it('should be able to click on the "Reapply Previous Toggled Items" button and expect "Task 1" and "Task 3" parents to become open (via Grid State change) while every other parents remains collapsed', () => { + cy.get('[data-test=reapply-toggled-items-btn]') + .contains('Reapply Previous Toggled Items') + .click({ force: true }); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 1'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0) .slick-group-toggle.expanded`).should('have.length', 1); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 2'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 3'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(0) .slick-group-toggle.expanded`).should('have.length', 1); + + cy.get(`#grid27 .slick-group-toggle.expanded`).should('have.length', 2); + }); + + it('should be able to click on "Dynamically Toggle First Parent" expect only the first parent item to get collapsed', () => { + cy.get('[data-test=dynamically-toggle-first-parent-btn]') + .contains('Dynamically Toggle First Parent') + .click(); + + cy.get(`#grid27 .slick-group-toggle.expanded`).should('have.length', 0); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example28.cy.ts b/demos/vue/test/cypress/e2e/example28.cy.ts new file mode 100644 index 000000000..e2439370a --- /dev/null +++ b/demos/vue/test/cypress/e2e/example28.cy.ts @@ -0,0 +1,509 @@ +describe('Example 28 - Tree Data (from a Hierarchical Dataset)', () => { + const GRID_ROW_HEIGHT = 33; + const titles = ['Files', 'Date Modified', 'Description', 'Size']; + // const defaultSortAscList = ['bucket-list.txt', 'documents', 'misc', 'warranties.txt', 'pdf', 'internet-bill.pdf', 'map.pdf', 'map2.pdf', 'phone-bill.pdf', 'txt', 'todo.txt', 'unclassified.csv', 'unresolved.csv', 'xls', 'compilation.xls', 'music', 'mp3', 'other', 'pop', 'song.mp3', 'theme.mp3', 'rock', 'soft.mp3', 'something.txt']; + // const defaultSortDescList = ['something.txt', 'music', 'mp3', 'rock', 'soft.mp3', 'other', 'pop', 'theme.mp3', 'song.mp3', 'documents', 'xls', 'compilation.xls', 'txt', 'todo.txt', 'unclassified.csv', 'unresolved.csv', 'pdf', 'phone-bill.pdf', 'map2.pdf', 'map.pdf', 'internet-bill.pdf', 'misc', 'todo.txt', 'bucket-list.txt']; + const defaultGridPresetWithoutPdfDocs = ['bucket-list.txt', 'documents', 'misc', 'warranties.txt', 'pdf', 'txt', 'todo.txt', 'unclassified.csv', 'unresolved.csv', 'xls', 'compilation.xls']; + const defaultSortAscList = ['bucket-list.txt', 'documents', 'misc', 'warranties.txt', 'pdf', 'internet-bill.pdf', 'map.pdf', 'map2.pdf', 'phone-bill.pdf']; + // const defaultSortDescList = ['something.txt', 'music', 'mp3', 'rock', 'soft.mp3', 'other', 'pop', 'theme.mp3', 'song.mp3', 'documents', 'xls', 'compilation.xls', 'txt', 'todo.txt']; + const defaultSortDescListWithExtraSongs = ['something.txt', 'recipes', 'coffee-cake', 'chocolate-cake', 'cheesecake', 'music', 'mp3', 'rock', 'soft.mp3', 'pop', 'theme.mp3', 'song.mp3', 'pop-80.mp3', 'pop-79.mp3', 'other', 'documents', 'xls']; + const popMusicWith3ExtraSongs = ['music', 'mp3', 'other', 'pop', 'pop-79.mp3', 'pop-80.mp3', 'pop-81.mp3', 'song.mp3', 'theme.mp3',]; + const popMusicWith3ExtraSongsWithoutEmpty = ['music', 'mp3', 'pop', 'pop-79.mp3', 'pop-80.mp3', 'pop-81.mp3', 'song.mp3', 'theme.mp3',]; + + describe('without Auto-Recalc feature', () => { + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example28`); + cy.get('h2').should('contain', 'Example 28: Tree Data with Aggregators'); + cy.get('h2').should('contain', 'from a Hierarchical Dataset'); + }); + + it('should have exact column titles on 1st grid', () => { + cy.get('#slickGridContainer-grid28') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(titles[index])); + }); + + it('should expect the "pdf" folder to be closed by the collapsed items grid preset with aggregators of Sum(8.8MB) / Avg(2.2MB)', () => { + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(0)`).should('contain', 'pdf'); + cy.get(`.slick-group-toggle.collapsed`).should('have.length', 1); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 8.8 MB / avg: 2.2 MB'); + + defaultGridPresetWithoutPdfDocs.forEach((_colName, rowIdx) => { + if (rowIdx < defaultGridPresetWithoutPdfDocs.length - 1) { + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * rowIdx}px;"] > .slick-cell:nth(0)`).should('contain', defaultGridPresetWithoutPdfDocs[rowIdx]); + } + }); + }); + + it('should have documents folder with aggregation of Sum(14.46MB) / Avg(1.45MB)', () => { + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'documents'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 14.46 MB / avg: 1.45 MB'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'misc'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 0.4 MB / avg: 0.4 MB'); + }); + + it('should expand "pdf" folder and expect all folders to be expanded', () => { + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(0) .slick-group-toggle.collapsed`) + .click(); + + cy.get('.slick-viewport-top.slick-viewport-left') + .scrollTo('top', { force: true } as any); + }); + + it('should have default Files list', () => { + defaultSortAscList.forEach((_colName, rowIdx) => { + if (rowIdx > defaultSortAscList.length - 1) { + return; + } + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * rowIdx}px;"] > .slick-cell:nth(0)`).should('contain', defaultSortAscList[rowIdx]); + }); + }); + + it('should have pop songs folder with aggregations of Sum(53.3MB) / Avg(26.65MB)', () => { + cy.get('.slick-viewport-top.slick-viewport-left') + .scrollTo('center', { force: true } as any); + + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 16}px;"] > .slick-cell:nth(0)`).should('contain', 'music'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 16}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 151.3 MB / avg: 50.43 MB'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 17}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 151.3 MB / avg: 50.43 MB'); + // next folder is "other" and is empty without aggregations + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 19}px;"] > .slick-cell:nth(0)`).should('contain', 'pop'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 19}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 53.3 MB / avg: 26.65 MB'); + }); + + it('should be able to add 2 new pop songs into the Music folder', () => { + cy.get('[data-test=add-item-btn]') + .contains('Add New Pop Song') + .click() + .click(); + + cy.get('.slick-group-toggle[level=3]') + .get('.slick-cell') + .contains('pop-79.mp3'); + + cy.get('.slick-group-toggle[level=3]') + .get('.slick-cell') + .contains('pop-80.mp3'); + + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 20}px;"] > .slick-cell:nth(3)`).should('contain', '82 MB'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 21}px;"] > .slick-cell:nth(3)`).should('contain', '83 MB'); + + }); + + it('should have pop songs folder with updated aggregations including new pop songs of Sum(218.3MB) / Avg(54.58MB)', () => { + cy.get('.slick-viewport-top.slick-viewport-left') + .scrollTo('bottom', { force: true } as any); + + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 16}px;"] > .slick-cell:nth(0)`).should('contain', 'music'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 16}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 316.3 MB / avg: 63.26 MB'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 17}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 316.3 MB / avg: 63.26 MB'); + // next folder is "other" and is empty without aggregations + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 19}px;"] > .slick-cell:nth(0)`).should('contain', 'pop'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 19}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 218.3 MB / avg: 54.58 MB'); + }); + + it('should filter the Files column with the word "map" and expect only 4 rows left', () => { + const filteredFiles = ['documents', 'pdf', 'map.pdf', 'map2.pdf']; + const filteredSizes = ['', '', '3.1', '2.9']; + + cy.get('.search-filter.filter-file') + .type('map'); + + cy.get('#slickGridContainer-grid28') + .find('.slick-row') + .each(($row, index) => { + cy.wrap($row).children('.slick-cell:nth(0)').should('contain', filteredFiles[index]); + cy.wrap($row).children('.slick-cell:nth(3)').should('contain', filteredSizes[index]); + }); + }); + + it('should add filter with "Size < 3" and expect 3 rows left', () => { + const filteredFiles = ['documents', 'pdf', 'map2.pdf']; + + cy.get('.search-filter.filter-size') + .find('input') + .type('3'); + + cy.get('.search-filter.filter-size') + .find('.input-group-addon.operator select') + .select('<'); + + cy.get('#slickGridContainer-grid28') + .find('.slick-row .slick-cell:nth(0)') + .each(($cell, index) => { + expect($cell.text().trim()).to.contain(filteredFiles[index]); + }); + }); + + it('should add filter with Size >3 and expect 3 rows left', () => { + const filteredFiles = ['documents', 'pdf', 'map.pdf']; + + cy.get('.search-filter.filter-size') + .find('.input-group-addon.operator select') + .select('>'); + + cy.get('#slickGridContainer-grid28') + .find('.slick-row .slick-cell:nth(0)') + .each(($cell, index) => { + expect($cell.text().trim()).to.contain(filteredFiles[index]); + }); + }); + + it('should add filter with Size <=3.1 and expect 3 rows left', () => { + const filteredFiles = ['documents', 'pdf', 'map.pdf', 'map2.pdf']; + + cy.get('.search-filter.filter-size') + .find('input') + .type('.1'); + + cy.get('.search-filter.filter-size') + .find('.input-group-addon.operator select') + .select('<='); + + cy.get('#slickGridContainer-grid28') + .find('.slick-row .slick-cell:nth(0)') + .each(($cell, index) => { + expect($cell.text().trim()).to.contain(filteredFiles[index]); + }); + }); + + it('should Clear all Filters and expect default list', () => { + cy.get('#slickGridContainer-grid28') + .find('button.slick-grid-menu-button') + .trigger('click') + .click({ force: true }); + + cy.get(`.slick-grid-menu:visible`) + .find('.slick-menu-item') + .first() + .find('span') + .contains('Clear all Filters') + .click({ force: true }); + + defaultSortAscList.forEach((_colName, rowIdx) => { + if (rowIdx < defaultSortAscList.length - 1) { + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * rowIdx}px;"] > .slick-cell:nth(0)`).should('contain', defaultSortAscList[rowIdx]); + } + }); + }); + + it('should click on "Files" column to sort descending', () => { + cy.get('.slick-header-columns .slick-header-column:nth(0)') + .click(); + + defaultSortDescListWithExtraSongs.forEach((_colName, rowIdx) => { + if (rowIdx < defaultSortDescListWithExtraSongs.length - 1) { + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * rowIdx}px;"] > .slick-cell:nth(0)`).should('contain', defaultSortDescListWithExtraSongs[rowIdx]); + } + }); + }); + + it('should filter the Files by the input search string and expect 4 rows and 1st column to have ', () => { + const filteredFiles = ['documents', 'pdf', 'map2.pdf', 'map.pdf']; + + cy.get('[data-test=search-string]') + .type('map'); + + cy.get('.search-filter.filter-file') + .should(($input) => { + expect($input.val()).to.eq('map'); + }); + + cy.get('#slickGridContainer-grid28') + .find('.slick-row .slick-cell:nth(0)') + .each(($cell, index) => { + expect($cell.text().trim()).to.contain(filteredFiles[index]); + }); + }); + + it('should clear search string and expect default list', () => { + cy.get('[data-test=clear-search-string]') + .click(); + + defaultSortDescListWithExtraSongs.forEach((_colName, rowIdx) => { + if (rowIdx < defaultSortDescListWithExtraSongs.length - 1) { + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * rowIdx}px;"] > .slick-cell:nth(0)`).should('contain', defaultSortDescListWithExtraSongs[rowIdx]); + } + }); + }); + + it('should be able to add a 3rd new pop song into the Music folder and see it show up in the UI', () => { + cy.get('[data-test=add-item-btn]') + .contains('Add New Pop Song') + .click(); + + cy.get('.slick-group-toggle[level=3]') + .get('.slick-cell') + .contains('pop-81.mp3'); + + cy.get('.slick-group-toggle[level=3]') + .get('.slick-cell') + .contains('pop-81.mp3'); + }); + + it('should have pop songs folder with updated aggregations including 4 pop songs of Sum(400.3MB) / Avg(66.72MB)', () => { + cy.get('.slick-viewport-top.slick-viewport-left') + .scrollTo('bottom', { force: true } as any); + + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 16}px;"] > .slick-cell:nth(0)`).should('contain', 'music'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 16}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 400.3 MB / avg: 66.72 MB'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 17}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 400.3 MB / avg: 66.72 MB'); + // next folder is "other" and is empty without aggregations + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 19}px;"] > .slick-cell:nth(0)`).should('contain', 'pop'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 19}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 302.3 MB / avg: 60.46 MB'); + }); + + it('should return 8 rows when filtering the word "pop" music without excluding children', () => { + cy.get('.search-filter.filter-file') + .type('pop'); + + cy.get('.right-footer .item-count') + .contains('8'); + + popMusicWith3ExtraSongsWithoutEmpty.forEach((_colName, rowIdx) => { + if (rowIdx < popMusicWith3ExtraSongsWithoutEmpty.length - 1) { + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * rowIdx}px;"] > .slick-cell:nth(0)`).should('contain', popMusicWith3ExtraSongsWithoutEmpty[rowIdx]); + } + }); + }); + + it('should return 6 rows when using same filter "pop" music AND selecting checkbox to "Exclude Children when Filtering Tree"', () => { + cy.get('[data-test="exclude-child-when-filtering"]') + .check(); + + cy.get('.right-footer .item-count') + .contains('6'); + + popMusicWith3ExtraSongsWithoutEmpty.forEach((_colName, rowIdx) => { + if (rowIdx < popMusicWith3ExtraSongsWithoutEmpty.length - 3) { + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * rowIdx}px;"] > .slick-cell:nth(0)`).should('contain', popMusicWith3ExtraSongsWithoutEmpty[rowIdx]); + } + }); + }); + + it('should change filter to the word "music" and expect only 1 row (the music folder) to show up when still Excluding Children from the Tree', () => { + cy.get('#slickGridContainer-grid28') + .find('button.slick-grid-menu-button') + .click(); + + cy.get('.slick-grid-menu:visible') + .find('.slick-menu-item') + .first() + .find('span') + .contains('Clear all Filters') + .click(); + + cy.get('.search-filter.filter-file') + .type('music'); + + cy.get('.right-footer .item-count') + .contains('1'); + + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'music'); + }); + + it('should use same filter "music" and now expect to see 10 rows (entire music folder content) to show up when "Exclude Children when Filtering Tree" becomes uncheck', () => { + cy.get('[data-test="exclude-child-when-filtering"]') + .uncheck(); + + cy.get('.right-footer .item-count') + .contains('11'); + + const allMusic = [...popMusicWith3ExtraSongs, 'rock', 'soft.mp3']; + + allMusic.forEach((_colName, rowIdx) => { + if (rowIdx < allMusic.length - 3) { + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * rowIdx}px;"] > .slick-cell:nth(0)`).should('contain', allMusic[rowIdx]); + } + }); + }); + + it('should use same filter "music" and add extra filter of "size >= 50" and expect 1+ songs (>=6 rows) to show up in the grid when "Exclude Children when Filtering Tree" is unchecked and "Skip Other Criteria..." is checked', () => { + cy.get('.search-filter.filter-size') + .find('input') + .type('50'); + + cy.get('.search-filter.filter-size') + .find('.input-group-addon.operator select') + .select('>='); + + cy.wait(50) + .get('.right-footer .item-count') + .then($row => { + expect(+$row.text()).to.be.at.least(6); + }); + + const expectedFiles = ['music', 'mp3', 'pop', 'pop-79.mp3', 'rock', 'soft.mp3']; + + expectedFiles.forEach((_colName, rowIdx) => { + if (rowIdx < expectedFiles.length - 3) { + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * rowIdx}px;"] > .slick-cell:nth(0)`).should('contain', expectedFiles[rowIdx]); + } + }); + }); + + it('should use same filter "music" and "size > 70" then unchecked "Skip Other Criteria..." and now expect 0 rows in the grid because there 0 rows having these 2 filters criteria', () => { + cy.get('[data-test="auto-approve-parent-item"]') + .uncheck(); + + cy.get('.right-footer .item-count') + .contains('0'); + }); + + it('should clear all filters', () => { + cy.get('[data-test="clear-filters-btn"]') + .click(); + }); + + it('should have pop songs folder with updated aggregations including 4 pop songs of Sum(400.3MB) / Avg(66.72MB)', () => { + cy.get('.slick-viewport-top.slick-viewport-left') + .scrollTo('center', { force: true } as any); + + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 16}px;"] > .slick-cell:nth(0)`).should('contain', 'music'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 16}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 400.3 MB / avg: 66.72 MB'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 17}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 400.3 MB / avg: 66.72 MB'); + // next folder is "other" and is empty without aggregations + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 19}px;"] > .slick-cell:nth(0)`).should('contain', 'pop'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 19}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 302.3 MB / avg: 60.46 MB'); + }); + + it('should remove last inserted pop song 81 and expect aggregations to be updated with Sum(316.3MB) / Avg(63.26MB)', () => { + cy.get('[data-test="remove-item-btn"]') + .click(); + + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 16}px;"] > .slick-cell:nth(0)`).should('contain', 'music'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 16}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 316.3 MB / avg: 63.26 MB'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 17}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 316.3 MB / avg: 63.26 MB'); + // next folder is "other" and is empty without aggregations + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 19}px;"] > .slick-cell:nth(0)`).should('contain', 'pop'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 19}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 218.3 MB / avg: 54.58 MB'); + }); + }); + + describe('Auto-Recalc Tree Totals feature enabled', () => { + it('should enable auto-recalc Tree Totals', () => { + cy.get('[data-test="auto-recalc-totals"]') + .check(); + }); + + it('should have pop songs folder with aggregation reflecting what is displayed, Sum(316.3MB) / Avg(63.26MB)', () => { + cy.get('.slick-viewport-top.slick-viewport-left') + .scrollTo('center', { force: true } as any); + + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 16}px;"] > .slick-cell:nth(0)`).should('contain', 'music'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 16}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 316.3 MB / avg: 63.26 MB'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 17}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 316.3 MB / avg: 63.26 MB'); + // next folder is "other" and is empty without aggregations + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 19}px;"] > .slick-cell:nth(0)`).should('contain', 'pop'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 19}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 218.3 MB / avg: 54.58 MB'); + }); + + it('should have documents with same Sum as the beginning since auto-recalc is disabled, aggregation should be Sum(14.46MB) / Avg(1.45MB)', () => { + cy.get('.slick-viewport-top.slick-viewport-left') + .scrollTo('top', { force: true } as any); + + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'documents'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 14.46 MB / avg: 1.45 MB (total)'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'misc'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 0.4 MB / avg: 0.4 MB (sub-total)'); + }); + + it('should retype filter "map" and expect totals to be updated with a lower Sum(6MB) / Avg(3MB) of only what is displayed', () => { + cy.get('.search-filter.filter-file') + .type('map'); + + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'documents'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 6 MB / avg: 3 MB (total)'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'pdf'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 6 MB / avg: 3 MB (sub-total)'); + + + cy.get('.right-footer .item-count').contains('4'); + cy.get('.right-footer .total-count').contains('31'); + }); + + it('should enable auto-recalc Tree Totals', () => { + cy.get('[data-test="clear-filters-btn"]') + .click(); + }); + + it('should type filter "b" and expect totals to be updated with a lower Sum(6MB) / Avg(3MB) of only what is displayed', () => { + cy.get('.search-filter.filter-file') + .type('b'); + + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'bucket-list.txt'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'documents'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 4.02 MB / avg: 1.34 MB (total)'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'pdf'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 2.8 MB / avg: 1.4 MB (sub-total)'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(0)`).should('contain', 'internet-bill.pdf'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(3)`).should('contain', '1.3 MB'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(0)`).should('contain', 'phone-bill.pdf'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(3)`).should('contain', '1.5 MB'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell:nth(0)`).should('contain', 'zebra.dll'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell:nth(3)`).should('contain', '1.22 MB'); + + cy.get('.right-footer .item-count').contains('6'); + cy.get('.right-footer .total-count').contains('31'); + }); + + it('should type filter "b" and expect totals to be updated with a lower Sum(6MB) / Avg(3MB) of only what is displayed', () => { + cy.get('.search-filter.filter-file') + .type('i'); // will become "bi" + + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'documents'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 2.8 MB / avg: 1.4 MB (total)'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'pdf'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 2.8 MB / avg: 1.4 MB (sub-total)'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'internet-bill.pdf'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(3)`).should('contain', '1.3 MB'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(0)`).should('contain', 'phone-bill.pdf'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(3)`).should('contain', '1.5 MB'); + + cy.get('.right-footer .item-count').contains('4'); + cy.get('.right-footer .total-count').contains('31'); + }); + + it('should clear all filters', () => { + cy.get('[data-test="clear-filters-btn"]') + .click(); + }); + + it('should collapse "pdf" folder and filter with "b" again and expect same updated tree totals as earlier collapsed or expanded should still be Sum(2.8MB) / Avg(1.4MB)', () => { + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(0) .slick-group-toggle.expanded`) + .click(); + + cy.get('.search-filter.filter-file') + .type('b'); + + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'bucket-list.txt'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'documents'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 4.02 MB / avg: 1.34 MB (total)'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'pdf'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 2.8 MB / avg: 1.4 MB (sub-total)'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(0)`).should('contain', 'zebra.dll'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(3)`).should('contain', '1.22 MB'); + + cy.get('.right-footer .item-count').contains('4'); + cy.get('.right-footer .total-count').contains('31'); + }); + + it('should clear all filters and collapse all Tree groups (folders) then type "so" and expect updated "music" totals Sum(104.3MB) / Avg(52.15MB)', () => { + cy.get('[data-test="clear-filters-btn"]').click(); + cy.get('[data-test="collapse-all-btn"]').click(); + + cy.get('.search-filter.filter-file').type('so'); + + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'documents'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 0.79 MB / avg: 0.79 MB (total)'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'music'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3)`).should('contain', 'sum: 104.3 MB / avg: 52.15 MB (total)'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'something.txt'); + cy.get(`#slickGridContainer-grid28 [style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(3)`).should('contain', '90 MB'); + + cy.get('.right-footer .item-count').contains('3'); + cy.get('.right-footer .total-count').contains('31'); + }); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example29.cy.ts b/demos/vue/test/cypress/e2e/example29.cy.ts new file mode 100644 index 000000000..6ca671404 --- /dev/null +++ b/demos/vue/test/cypress/e2e/example29.cy.ts @@ -0,0 +1,14 @@ +describe('Example 29 - Header and Footer slots', () => { + it('should display a custom header as slot', () => { + cy.visit(`${Cypress.config('baseUrl')}/example29`); + cy.get('div.custom-header-slot').find('h3').contains('Grid with header and footer slot'); + }); + + it('should render a footer slot', () => { + cy.get('div.custom-footer-slot').should('exist'); + }); + + it('should render a custom element inside footer slot', () => { + cy.get('div.custom-footer-slot').find('button').click().click().click().siblings('div').should('contain', '3 time(s)'); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example30.cy.ts b/demos/vue/test/cypress/e2e/example30.cy.ts new file mode 100644 index 000000000..e531ad23a --- /dev/null +++ b/demos/vue/test/cypress/e2e/example30.cy.ts @@ -0,0 +1,753 @@ +import { changeTimezone, zeroPadding } from '../plugins/utilities'; + +describe('Example 30 Composite Editor Modal', () => { + const fullPreTitles = ['', 'Common Factor', 'Analysis', 'Period', 'Item', '']; + const fullTitles = [ + '', + ' Title ', + 'Duration', + 'Cost', + '% Complete', + 'Complexity', + 'Start', + 'Completed', + 'Finish', + 'Product', + 'Country of Origin', + 'Action', + ]; + + const GRID_ROW_HEIGHT = 35; + const EDITABLE_CELL_RGB_COLOR = 'rgba(227, 240, 251, 0.57)'; + const UNSAVED_RGB_COLOR = 'rgb(251, 253, 209)'; + + beforeEach(() => { + // create a console.log spy for later use + cy.window().then((win) => { + cy.spy(win.console, 'log'); + }); + }); + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example30`); + cy.get('h2').should('contain', 'Example 30: Composite Editor Modal'); + }); + + it('should have exact Column Pre-Header & Column Header Titles in the grid', () => { + cy.get('#grid30') + .find('.slick-header-columns:nth(0)') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullPreTitles[index])); + + cy.get('#grid30') + .find('.slick-header-columns:nth(1)') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + }); + + it('should display 2 different tooltips when hovering icons on "Title" column', () => { + cy.get('.slick-column-name').as('title-column'); + cy.get('@title-column').find('.mdi-alert-outline').trigger('mouseover'); + + cy.get('.slick-custom-tooltip').should('be.visible'); + cy.get('.slick-custom-tooltip .tooltip-body').contains('Task must always be followed by a number'); + + cy.get('@title-column').find('.mdi-information-outline').trigger('mouseover'); + + cy.get('.slick-custom-tooltip').should('be.visible'); + cy.get('.slick-custom-tooltip .tooltip-body').contains('Title is always rendered as UPPERCASE'); + }); + + it('should have "TASK 0" (uppercase) incremented by 1 after each row', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`) + .contains('TASK 0', { matchCase: false }) + .should('have.css', 'text-transform', 'uppercase'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).contains('TASK 1', { matchCase: false }); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).contains('TASK 2', { matchCase: false }); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1)`).contains('TASK 3', { matchCase: false }); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(1)`).contains('TASK 4', { matchCase: false }); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell:nth(1)`).contains('TASK 5', { matchCase: false }); + }); + + it('should be able to change "Duration" values of first 4 rows', () => { + // change duration + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`) + .should('contain', 'days') + .click(); + cy.get('.editor-duration').type('0{enter}'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`) + .should('contain', '0 day') + .should('have.css', 'background-color') + .and('eq', UNSAVED_RGB_COLOR); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2)`) + .click() + .type('1{enter}'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2)`) + .should('contain', '1 day') + .should('have.css', 'background-color') + .and('eq', UNSAVED_RGB_COLOR); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(2)`) + .click() + .type('2{enter}'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(2)`) + .should('contain', '2 days') + .should('have.css', 'background-color') + .and('eq', UNSAVED_RGB_COLOR); + }); + + it('should be able to change "Title" values of row indexes 1-3', () => { + // change title + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`) + .contains('TASK 1', { matchCase: false }) + .click(); + cy.get('.editor-title').type('task 1111'); + cy.get('.editor-title .editor-footer .btn-save').click(); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`) + .contains('TASK 1111', { matchCase: false }) + .should('have.css', 'background-color') + .and('eq', UNSAVED_RGB_COLOR); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`) + .contains('TASK 2', { matchCase: false }) + .click(); + cy.get('.editor-title').type('task 2222'); + cy.get('.editor-title .editor-footer .btn-save').click(); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`) + .contains('TASK 2222', { matchCase: false }) + .should('have.css', 'background-color') + .and('eq', UNSAVED_RGB_COLOR); + }); + + it('should be able to change "% Complete" values of row indexes 2-4', () => { + // change % complete + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(4)`).click(); + cy.get('.slider-editor input[type=range]').as('range').invoke('val', 5).trigger('change', { force: true }); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(4)`) + .should('contain', '5') + .should('have.css', 'background-color') + .and('eq', UNSAVED_RGB_COLOR); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(4)`).click(); + cy.get('.slider-editor input[type=range]').as('range').invoke('val', 6).trigger('change', { force: true }); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(4)`) + .should('contain', '6') + .should('have.css', 'background-color') + .and('eq', UNSAVED_RGB_COLOR); + }); + + it('should not be able to change the "Finish" dates on first 2 rows', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(8)`) + .should('contain', '') + .click({ force: true }); // this date should also always be initially empty + cy.get(`.vanilla-calendar-day__btn_today:visible`).should('not.exist'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(8)`) + .should('contain', '') + .click({ force: true }); // this date should also always be initially empty + cy.get(`.vanilla-calendar-day__btn_today:visible`).should('not.exist'); + }); + + it('should be able to change "Completed" values of row indexes 2-4', () => { + // change Completed + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(7)`).click(); + cy.get('.editor-completed').check(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(7)`).click(); + cy.get('.editor-completed').check(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(7)`).click(); + cy.get('.editor-completed').check(); + }); + + it('should be able to change "Finish" values of row indexes 0-2', () => { + const now = new Date(); + const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; + const today = changeTimezone(now, tz); + + const currentDate = today.getDate(); + let currentMonth: number | string = today.getMonth() + 1; // month is zero based, let's add 1 to it + if (currentMonth < 10) { + currentMonth = `0${currentMonth}`; // add zero padding + } + const currentYear = today.getFullYear(); + + // change Finish date to today's date + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(8)`) + .should('contain', '') + .click(); // this date should also always be initially empty + cy.get(`.vanilla-calendar-day__btn_today:visible`).click('bottom', { force: true }); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(8)`) + .should('contain', `${zeroPadding(currentMonth)}/${zeroPadding(currentDate)}/${currentYear}`) + .should('have.css', 'background-color') + .and('eq', UNSAVED_RGB_COLOR); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(8)`).click(); + cy.get(`.vanilla-calendar-day__btn_today:visible`).click('bottom', { force: true }); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(8)`) + .should('contain', `${zeroPadding(currentMonth)}/${zeroPadding(currentDate)}/${currentYear}`) + .should('have.css', 'background-color') + .and('eq', UNSAVED_RGB_COLOR); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(8)`).click(); + cy.get(`.vanilla-calendar-day__btn_today:visible`).click('bottom', { force: true }); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(8)`) + .should('contain', `${zeroPadding(currentMonth)}/${zeroPadding(currentDate)}/${currentYear}`) + .should('have.css', 'background-color') + .and('eq', UNSAVED_RGB_COLOR); + + cy.get('.unsaved-editable-field').should('have.length', 13); + }); + + it('should undo last edit and expect the date editor to be opened as well when clicking the associated last undo with editor button', () => { + cy.get('[data-test=undo-open-editor-btn]').click(); + + cy.get('.vanilla-calendar').should('exist'); + + cy.get('.unsaved-editable-field').should('have.length', 12); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(8)`) + .should('contain', '') + .should('have.css', 'background-color') + .and('eq', EDITABLE_CELL_RGB_COLOR); + }); + + it('should undo last edit and expect the date editor to NOT be opened when clicking undo last edit button', () => { + cy.get('[data-test=undo-last-edit-btn]').click(); + + cy.get('.vanilla-calendar:visible').should('not.exist'); + + cy.get('.unsaved-editable-field').should('have.length', 11); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(8)`) + .should('contain', '') + .should('have.css', 'background-color') + .and('eq', EDITABLE_CELL_RGB_COLOR); + }); + + it('should click on the "Save" button and expect 2 console log calls with the queued items & also expect no more unsaved cells', () => { + cy.get('[data-test=save-all-btn]').click(); + + cy.get('.unsaved-editable-field').should('have.length', 0); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(2); + }); + }); + + it('should be able to toggle the grid to readonly', () => { + cy.get('[data-test=toggle-readonly-btn]').click(); + + cy.get('.editable-field').should('have.length', 0); + }); + + it('should be able to toggle back the grid to editable', () => { + cy.get('[data-test=toggle-readonly-btn]').click(); + + cy.get('.editable-field').should('not.have.length', 0); + }); + + it('should open the Composite Editor (Create Item) and expect all form inputs to be empty', () => { + cy.get('[data-test="open-modal-create-btn"]').click(); + + cy.get('.slick-editor-modal-title').contains('Inserting New Task'); + + cy.get('textarea').should('be.empty'); + cy.get('.item-details-editor-container .input-group-text').contains('0'); + cy.get('.editor-checkbox').should('be.not.checked'); + cy.get('.item-details-container.editor-product .autocomplete').should('be.empty'); + cy.get('.item-details-container.editor-duration .editor-text').should('be.empty'); + cy.get('.item-details-container.editor-start input.date-picker').invoke('val').should('be.empty'); + cy.get('.item-details-container.editor-finish input.date-picker').invoke('val').should('be.empty'); + cy.get('.item-details-container.editor-finish input.date-picker').should('be.disabled'); + cy.get('.item-details-container.editor-origin .autocomplete').should('be.empty'); + }); + + it('should not be able to save, neither expect the modal window to close when having invalid fields', () => { + cy.get('.btn-save').contains('Save').click(); + + cy.get('.slick-editor-modal').should('exist'); + cy.get('.item-details-container.editor-title .item-details-validation').contains('* This is a required field.'); + }); + + it('should fill in the (Create Item) form inputs and expect a new row in the grid', () => { + cy.get('textarea').type('Task'); + cy.get('.item-details-container.editor-title .item-details-validation').contains( + '* Your title is invalid, it must start with "Task" followed by a number.' + ); + cy.get('textarea').type(' 8888'); + cy.get('.item-details-container.editor-title .item-details-validation').should('be.empty'); + cy.get('.item-details-container.editor-title .modified').should('have.length', 1); + + // cy.get('.slick-large-editor-text.editor-title') + // .should('have.css', 'border') + // .and('eq', `1px solid ${UNSAVED_RGB_COLOR}`); + + cy.get('.item-details-editor-container .slider-editor-input.editor-percentComplete') + .as('range') + .invoke('val', 5) + .trigger('change', { force: true }); + cy.get('.item-details-editor-container .input-group-text').contains('5'); + cy.get('.item-details-container.editor-percentComplete .modified').should('have.length', 1); + + cy.get('.editor-completed .editor-checkbox').check(); + cy.get('.item-details-container.editor-completed .modified').should('have.length', 1); + + cy.get('.item-details-container.editor-product .autocomplete').type('granite'); + cy.get('.slick-autocomplete.autocomplete-custom-four-corners').should('be.visible'); + cy.get('.slick-autocomplete.autocomplete-custom-four-corners').find('div:nth(0)').click(); + cy.get('.item-details-container.editor-product .modified').should('have.length', 1); + + cy.get('.item-details-container.editor-duration .editor-text').type('22'); + cy.get('.item-details-container.editor-duration .modified').should('have.length', 1); + + cy.get('.item-details-container.editor-finish > .item-details-validation').contains( + '* You must provide a "Finish" date when "Completed" is checked.' + ); + cy.get('.item-details-container.editor-finish input.date-picker').click(); + cy.get(`.vanilla-calendar-day__btn_today:visible`).click('bottom', { force: true }); + cy.get('.item-details-container.editor-finish .modified').should('have.length', 1); + + cy.get('.item-details-container.editor-origin .autocomplete').type('c'); + cy.get('.slick-autocomplete:visible').find('div:nth(1)').click(); + cy.get('.item-details-container.editor-origin .autocomplete') + .invoke('val') + .then((text) => expect(text).to.eq('Antarctica')); + cy.get('.item-details-container.editor-origin .modified').should('have.length', 1); + + cy.get('.btn-save').contains('Save').click(); + cy.get('.slick-editor-modal').should('not.exist'); + }); + + it('should have new TASK 8888 displayed on first row', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).contains('TASK 8888', { matchCase: false }); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '22 days'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(4)`).should('contain', '5'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5).editable-field`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(7)`) + .find('.mdi.mdi-check.checkmark-icon') + .should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(8)`).should('not.be.empty'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(9)`).should('contain', 'Tasty Granite Table'); + + // next few rows Title should be unchanged + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).contains('TASK 0', { matchCase: false }); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).contains('TASK 1111', { matchCase: false }); + }); + + it('should open the Composite Editor (Edit Item) and expect all form inputs to be filled with TASK 8888 data of previous create item', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).click({ force: true }); + cy.get('[data-test="open-modal-edit-btn"]').click(); + cy.get('.slick-editor-modal-title').contains('Editing - Task 8888 (id: 501)'); + + cy.get('textarea').contains('Task 8888').type('Task 8899'); + cy.get('.item-details-editor-container .slider-editor-input.editor-percentComplete') + .as('range') + .invoke('val', 7) + .trigger('change', { force: true }); + cy.get('.item-details-editor-container .slider-editor-input.editor-percentComplete') + .as('range') + .invoke('val', 17) + .trigger('change', { force: true }); + cy.get('.item-details-container.editor-percentComplete .modified').should('have.length', 1); + + cy.get('.item-details-editor-container .editor-checkbox').uncheck(); + cy.get('.item-details-container.editor-duration input.editor-text').type('33'); + cy.get('.item-details-container.editor-duration .modified').should('have.length', 1); + + cy.get('.modified').should('have.length.greaterThan', 1); + + cy.get('.btn-save').contains('Save').click(); + cy.get('.slick-editor-modal').should('not.exist'); + }); + + it('should have new TASK 8899 displayed on first row', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).contains('TASK 8899', { matchCase: false }); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '33 days'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(4)`).should('contain', '17'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5).editable-field`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(7)`) + .find('.mdi.mdi-check.checkmark-icon') + .should('not.exist'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(8)`).should('be.empty'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(9)`).should('contain', 'Tasty Granite Table'); + + // next few rows Title should be unchanged + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).contains('TASK 0', { matchCase: false }); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).contains('TASK 1111', { matchCase: false }); + }); + + it('should open the Composite Editor (Mass Update) and be able to change some of the inputs in the form', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).click(); + cy.get('[data-test="open-modal-mass-update-btn"]').wait(200).click(); + cy.get('.slick-editor-modal-title').should('contain', 'Mass Update All Records'); + cy.get('.footer-status-text').should('contain', 'All 501 records selected'); + + cy.get('.item-details-editor-container .editor-checkbox').check(); + cy.get('.item-details-container.editor-completed .modified').should('have.length', 1); + + cy.get('.item-details-editor-container div.editor-complexity').click(); + cy.get('[data-name=editor-complexity].ms-drop > ul > li > label:nth(2)').contains('Straightforward').click(); + cy.get('.item-details-container.editor-complexity .modified').should('have.length', 1); + + cy.get('.item-details-container.editor-finish > .item-details-validation').contains( + '* You must provide a "Finish" date when "Completed" is checked.' + ); + cy.get('.item-details-container.editor-finish .date-picker').click().click(); + cy.get(`.vanilla-calendar-day__btn_today:visible`).click(); + cy.get('.item-details-container.editor-finish .modified').should('have.length', 1); + + cy.get('.item-details-container.editor-origin .autocomplete').type('bel'); + cy.get('.slick-autocomplete:visible').find('div:nth(1)').click(); + cy.get('.item-details-container.editor-origin .modified').should('have.length', 1); + cy.get('.item-details-container.editor-origin .autocomplete') + .invoke('val') + .then((text) => expect(text).to.eq('Belgium')); + + cy.get('.btn-save').contains('Apply Mass Update').click(); + cy.get('.validation-summary').contains('Unfortunately we only accept a minimum of 50% Completion...'); + + cy.get('.item-details-editor-container .slider-editor-input.editor-percentComplete') + .as('range') + .invoke('val', 5) + .trigger('change', { force: true }); + cy.get('.item-details-editor-container .slider-editor-input.editor-percentComplete') + .as('range') + .invoke('val', 51) + .trigger('change', { force: true }); + cy.get('.item-details-editor-container .input-group-text').contains('51'); + + cy.get('.btn-save').contains('Apply Mass Update').click(); + cy.get('.slick-editor-modal').should('not.exist'); + }); + + it('should have updated values in the entire grid', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(4)`).should('contain', '51'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5)`).should('contain', 'Straightforward'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(7)`) + .find('.mdi.mdi-check.checkmark-icon') + .should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(8)`).should('not.be.empty'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(10)`).should('contain', 'Belgium'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(4)`).should('contain', '51'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5)`).should('contain', 'Straightforward'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(7)`) + .find('.mdi.mdi-check.checkmark-icon') + .should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(8)`).should('not.be.empty'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(10)`).should('contain', 'Belgium'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(4)`).should('contain', '51'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5)`).should('contain', 'Straightforward'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(7)`) + .find('.mdi.mdi-check.checkmark-icon') + .should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(8)`).should('not.be.empty'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(10)`).should('contain', 'Belgium'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(4)`).should('contain', '51'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5)`).should('contain', 'Straightforward'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(7)`) + .find('.mdi.mdi-check.checkmark-icon') + .should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(8)`).should('not.be.empty'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(10)`).should('contain', 'Belgium'); + }); + + it('should open the Composite Editor (Mass Update) change some inputs', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).click(); + cy.get('[data-test="open-modal-mass-update-btn"]').wait(200).click(); + cy.get('.slick-editor-modal-title').should('contain', 'Mass Update All Records'); + cy.get('.footer-status-text').should('contain', 'All 501 records selected'); + + cy.get('.item-details-editor-container .editor-checkbox').check(); + cy.get('.item-details-container.editor-completed .modified').should('have.length', 1); + + cy.get('.item-details-editor-container div.editor-complexity').click(); + cy.get('[data-name=editor-complexity].ms-drop > ul > li > label:nth(2)').contains('Straightforward').click(); + cy.get('.item-details-container.editor-complexity .modified').should('have.length', 1); + + cy.get('.item-details-container.editor-finish > .item-details-validation').contains( + '* You must provide a "Finish" date when "Completed" is checked.' + ); + cy.get('.item-details-container.editor-finish .date-picker').click().click(); + cy.get(`.vanilla-calendar-day__btn_today:visible`).click(); + cy.get('.item-details-container.editor-finish .modified').should('have.length', 1); + + cy.get('.item-details-container.editor-origin .autocomplete').type('bel'); + cy.get('.slick-autocomplete:visible').find('div:nth(1)').click(); + cy.get('.item-details-container.editor-origin .modified').should('have.length', 1); + cy.get('.item-details-container.editor-origin .autocomplete') + .invoke('val') + .then((text) => expect(text).to.eq('Belgium')); + }); + + it('should be able to clear the "Country of Origin" autocomplete field in the modal form via the Clear button from the editor', () => { + cy.get('.item-details-container.editor-origin .modified').should('have.length', 1); + cy.get('.item-details-container.editor-origin .autocomplete-container button.btn-clear').click(); + cy.get('.item-details-container.editor-origin .modified').should('have.length', 1); + cy.get('.item-details-container.editor-origin .autocomplete') + .invoke('val') + .then((text) => expect(text).to.eq('')); + }); + + it('should be able to click on the "Reset Form" button from the (Mass Update) and expect the form to be empty and not be able to Save', () => { + const alertStub = cy.stub(); + cy.on('window:alert', alertStub); + + cy.get('.item-details-container .modified').should('have.length', 4); + cy.get('.reset-form').contains('Reset Form').click(); + cy.get('.item-details-container .modified').should('have.length', 0); + + cy.get('.btn-save') + .click() + .then(() => expect(alertStub.getCall(0)).to.be.calledWith('Sorry we could not detect any changes.')); + + cy.get('.btn-cancel').click(); + }); + + it('should have the "Mass Selection" button disabled when no rows are selected', () => { + cy.get('[data-test="open-modal-mass-selection-btn"]').should('be.disabled'); + }); + + it('should select row 1 and 2', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).click(); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).click(); + cy.get('[data-test="open-modal-mass-selection-btn"]').should('not.be.disabled'); + cy.get('[data-test="open-modal-mass-selection-btn"]').wait(50).click(); + }); + + it('should be able to open the Composite Editor (Mass Selection) and be able to change some of the inputs in the form', () => { + cy.get('.slick-editor-modal-title').should('contain', 'Update Selected Records'); + cy.get('.footer-status-text').should('contain', '2 of 501 selected'); + + cy.get('.item-details-editor-container .editor-checkbox').check(); + cy.get('.item-details-container.editor-completed .modified').should('have.length', 1); + + cy.get('.item-details-container.editor-finish > .item-details-validation').contains( + '* You must provide a "Finish" date when "Completed" is checked.' + ); + cy.get('.item-details-container.editor-finish input.date-picker').click({ force: true }); + cy.get(`.vanilla-calendar-day__btn_today:visible`).click('bottom', { force: true }); + cy.get('.item-details-container.editor-finish .modified').should('have.length', 1); + + cy.get('.item-details-container.editor-origin .autocomplete').type('ze'); + cy.get('.slick-autocomplete:visible').find('div:nth(1)').click(); + cy.get('.item-details-container.editor-origin .modified').should('have.length', 1); + cy.get('.item-details-container.editor-origin .autocomplete') + .invoke('val') + .then((text) => expect(text).to.eq('Belize')); + + cy.get('.btn-save').contains('Update Selection').click(); + cy.get('.validation-summary').contains('Unfortunately we only accept a minimum of 50% Completion...'); + + cy.get('.item-details-editor-container .slider-editor-input.editor-percentComplete') + .as('range') + .invoke('val', 77) + .trigger('change', { force: true }); + cy.get('.item-details-editor-container .input-group-text').contains('77'); + cy.get('.btn-save').contains('Update Selection').click(); + + cy.get('.slick-editor-modal').should('not.exist'); + }); + + it('should not have any row selected after the mass-selection save is over', () => { + cy.get('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 0); + }); + + it('should have updated all the changed values BUT only on the 2 selected rows', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(4)`).should('contain', '51'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(7)`) + .find('.mdi.mdi-check.checkmark-icon') + .should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(8)`).should('not.be.empty'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(10)`).should('contain', 'Belgium'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(4)`).should('contain', '77'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(7)`) + .find('.mdi.mdi-check.checkmark-icon') + .should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(8)`).should('not.be.empty'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(10)`).should('contain', 'Belize'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(4)`).should('contain', '77'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(7)`) + .find('.mdi.mdi-check.checkmark-icon') + .should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(8)`).should('not.be.empty'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(10)`).should('contain', 'Belize'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(4)`).should('contain', '51'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(7)`) + .find('.mdi.mdi-check.checkmark-icon') + .should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(8)`).should('not.be.empty'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(10)`).should('contain', 'Belgium'); + }); + + it(`should open the Composite Editor (Mass Update) change "Percent Complete" to 100% and expect "Completed" to become checked and "Finish" date to be today's date`, () => { + const now = new Date(); + const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; + const today = changeTimezone(now, tz); + + const currentDate = today.getDate(); + let currentMonth: number | string = today.getMonth() + 1; // month is zero based, let's add 1 to it + if (currentMonth < 10) { + currentMonth = `0${currentMonth}`; // add zero padding + } + const currentYear = today.getFullYear(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).click(); + cy.get('[data-test="open-modal-mass-update-btn"]').click(); + cy.get('.slick-editor-modal-title').contains('Mass Update All Records'); + + cy.get('.item-details-editor-container .slider-editor-input.editor-percentComplete') + .as('range') + .invoke('val', 100) + .trigger('change', { force: true }); + cy.get('.item-details-container.editor-percentComplete .modified').should('have.length', 1); + + cy.get('.item-details-container.editor-completed input.editor-checkbox:checked').should('have.length', 1); + cy.get('.item-details-container.editor-completed .modified').should('have.length', 1); + + cy.get('.item-details-container.editor-finish input.date-picker').should( + 'contain.value', + `${zeroPadding(currentMonth)}/${zeroPadding(currentDate)}/${currentYear}` + ); + cy.get('.item-details-container.editor-finish .modified').should('have.length', 1); + + cy.get('.btn-cancel').click(); + }); + + it('should not have any row selected after the mass-update save is over', () => { + cy.get('.slick-row').children().filter('.slick-cell-checkboxsel.selected').should('have.length', 0); + }); + + it('should focus on first row and open the Composite Editor (Clone Item) and expect all form inputs to be filled with first row data', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).click({ force: true }); + cy.get('[data-test="open-modal-clone-btn"]').click(); + cy.get('.slick-editor-modal-title').contains('Clone - Task 8899'); + + cy.get('textarea').contains('Task 8899'); + cy.get('.item-details-editor-container .slider-editor .input-group-text').contains('51'); + cy.get('.item-details-container.editor-completed input.editor-checkbox:checked').should('have.length', 1); + cy.get('.item-details-container.editor-duration input.editor-text') + .invoke('val') + .then((text) => expect(text).to.eq('33.00')); + }); + + it('should change the "Title" & "Duration" from the Clone form, then click on "Cancel" button and expect no changes in the grid', () => { + cy.get('.slick-editor-modal-title').contains('Clone - Task 8899'); + + cy.get('textarea').contains('Task 8899').type('Task 9999'); + cy.get('.item-details-editor-container .slider-editor-input.editor-percentComplete') + .as('range') + .invoke('val', 7) + .trigger('change', { force: true }); + cy.get('.item-details-editor-container .slider-editor-input.editor-percentComplete') + .as('range') + .invoke('val', 17) + .trigger('change', { force: true }); + cy.get('.item-details-container.editor-percentComplete .modified').should('have.length', 1); + + cy.get('.item-details-editor-container .editor-checkbox').uncheck(); + + cy.get('.btn-cancel').click(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).contains('TASK 8899', { matchCase: false }); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '33 days'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(4)`).should('contain', '51'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(7)`) + .find('.mdi.mdi-check.checkmark-icon') + .should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(8)`).should('not.be.empty'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(10)`).should('contain', 'Belgium'); + }); + + it('should focus again on first row and open the Composite Editor (Clone Item) and expect all form inputs to be filled with first row data', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).click({ force: true }); + cy.get('[data-test="open-modal-clone-btn"]').click(); + cy.get('.slick-editor-modal-title').contains('Clone - Task 8899'); + + cy.get('textarea').contains('Task 8899'); + cy.get('.item-details-editor-container .slider-editor .input-group-text').contains('51'); + cy.get('.item-details-container.editor-completed input.editor-checkbox:checked').should('have.length', 1); + cy.get('.item-details-container.editor-duration input.editor-text') + .invoke('val') + .then((text) => expect(text).to.eq('33.00')); + }); + + it('should change the "Title" & "Duration" from the Clone form, then click on "Clone" button and expect a new row to show up on top of the grid', () => { + cy.get('.slick-editor-modal-title').contains('Clone - Task 8899'); + + cy.get('textarea').contains('Task 8899').type('Task 9999'); + cy.get('.item-details-editor-container .slider-editor-input.editor-percentComplete') + .as('range') + .invoke('val', 7) + .trigger('change', { force: true }); + cy.get('.item-details-editor-container .slider-editor-input.editor-percentComplete') + .as('range') + .invoke('val', 17) + .trigger('change', { force: true }); + cy.get('.item-details-container.editor-percentComplete .modified').should('have.length', 1); + + cy.get('.item-details-editor-container .editor-checkbox').uncheck(); + + cy.get('.item-details-container.editor-duration input.editor-text').type('44'); + cy.get('.item-details-container.editor-duration .modified').should('have.length', 1); + + cy.get('.btn-save').contains('Clone').click(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).contains('TASK 9999', { matchCase: false }); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '44 days'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(4)`).should('contain', '17'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(7)`) + .find('.mdi.mdi-check.checkmark-icon') + .should('have.length', 0); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(8)`).should('be.empty'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(10)`).should('contain', 'Belgium'); + }); + + it('should expect original, that was originally used to clone, to now be exist as that 2nd row in the grid', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', '8899'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2)`).should('contain', '33 days'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(4)`).should('contain', '51'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(7)`) + .find('.mdi.mdi-check.checkmark-icon') + .should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(8)`).should('not.be.empty'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(10)`).should('contain', 'Belgium'); + }); + + it('should be able to clear the "Country of Origin" autocomplete field in the grid via the Clear button from the editor', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(10)`).should('contain', 'Belgium'); + + // clear Country + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(10)`).click(); + cy.get('.autocomplete-container button.btn-clear').click(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(10)`).should('contain', ''); + }); + + it('should open Edit Composite Editor from Cell Menu and expect Task 4 on 6th row', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 6}px;"] > .slick-cell:nth(11)`).click(); + + cy.get('.slick-menu-item .slick-menu-content').first().should('contain', 'Edit Row').click(); + + cy.get('.slick-editor-modal-title').should('contain', 'Editing - Task 4'); + + cy.get('.slick-editor-modal-footer .btn-cancel').click(); + }); + + it('should open Clone Composite Editor from Cell Menu and expect Task 4 on 6th row', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 6}px;"] > .slick-cell:nth(11)`).click(); + + cy.get('.slick-menu-item .slick-menu-content:nth(1)').should('contain', 'Clone Row').click(); + + cy.get('.slick-editor-modal-title').should('contain', 'Clone - Task 4'); + + cy.get('.slick-editor-modal-footer .btn-cancel').click(); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example31.cy.ts b/demos/vue/test/cypress/e2e/example31.cy.ts new file mode 100644 index 000000000..00ea8aab5 --- /dev/null +++ b/demos/vue/test/cypress/e2e/example31.cy.ts @@ -0,0 +1,825 @@ +describe('Example 31 - OData Grid using RxJS', () => { + const GRID_ROW_HEIGHT = 33; + + beforeEach(() => { + // create a console.log spy for later use + cy.window().then(win => cy.spy(win.console, 'log')); + }); + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example31`); + cy.get('h2').should('contain', 'Example 31: Grid with OData Backend Service using RxJS Observables'); + }); + + describe('when "enableCount" is set', () => { + it('should have default OData query', () => { + cy.get('[data-test=alert-odata-query]').should('exist'); + cy.get('[data-test=alert-odata-query]').should('contain', 'OData Query'); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished!!'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$inlinecount=allpages&$top=20&$skip=20&$orderby=Name asc&$filter=(Gender eq 'male')`); + }); + }); + + it('should change Pagination to next page', () => { + cy.get('.icon-seek-next').click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished!!'); + + cy.get('[data-test=page-number-input]') + .invoke('val') + .then(pageNumber => expect(pageNumber).to.eq('3')); + + cy.get('[data-test=page-count]') + .contains('3'); + + cy.get('[data-test=item-from]') + .contains('41'); + + cy.get('[data-test=item-to]') + .contains('50'); + + cy.get('[data-test=total-items]') + .contains('50'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$inlinecount=allpages&$top=20&$skip=40&$orderby=Name asc&$filter=(Gender eq 'male')`); + }); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(1); + expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { newValues: { pageNumber: 3, pageSize: 20 }, type: 'pagination' }); + }); + }); + + it('should change Pagination to first page with 10 items', () => { + cy.get('#items-per-page-label').select('10'); + + // wait for the query to start and finish + cy.get('[data-test=status]').should('contain', 'loading...'); + cy.get('[data-test=status]').should('contain', 'finished!!'); + + cy.get('[data-test=page-number-input]') + .invoke('val') + .then(pageNumber => expect(pageNumber).to.eq('1')); + + cy.get('[data-test=page-count]') + .contains('5'); + + cy.get('[data-test=item-from]') + .contains('1'); + + cy.get('[data-test=item-to]') + .contains('10'); + + cy.get('[data-test=total-items]') + .contains('50'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$inlinecount=allpages&$top=10&$orderby=Name asc&$filter=(Gender eq 'male')`); + }); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(1); + expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { newValues: { pageNumber: 1, pageSize: 10 }, type: 'pagination' }); + }); + }); + + it('should change Pagination to last page', () => { + cy.get('.icon-seek-end').click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished!!'); + + cy.get('[data-test=page-number-input]') + .invoke('val') + .then(pageNumber => expect(pageNumber).to.eq('5')); + + cy.get('[data-test=page-count]') + .contains('5'); + + cy.get('[data-test=item-from]') + .contains('41'); + + cy.get('[data-test=item-to]') + .contains('50'); + + cy.get('[data-test=total-items]') + .contains('50'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$inlinecount=allpages&$top=10&$skip=40&$orderby=Name asc&$filter=(Gender eq 'male')`); + }); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(1); + expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { newValues: { pageNumber: 5, pageSize: 10 }, type: 'pagination' }); + }); + }); + + it('should change Pagination to first page using the external button', () => { + cy.get('[data-test=goto-first-page') + .click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished!!'); + + cy.get('[data-test=page-number-input]') + .invoke('val') + .then(pageNumber => expect(pageNumber).to.eq('1')); + + cy.get('[data-test=page-count]') + .contains('5'); + + cy.get('[data-test=item-from]') + .contains('1'); + + cy.get('[data-test=item-to]') + .contains('10'); + + cy.get('[data-test=total-items]') + .contains('50'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$inlinecount=allpages&$top=10&$orderby=Name asc&$filter=(Gender eq 'male')`); + }); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(1); + expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { newValues: { pageNumber: 1, pageSize: 10 }, type: 'pagination' }); + }); + }); + + it('should change Pagination to last page using the external button', () => { + cy.get('[data-test=goto-last-page') + .click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished!!'); + + cy.get('[data-test=page-number-input]') + .invoke('val') + .then(pageNumber => expect(pageNumber).to.eq('5')); + + cy.get('[data-test=page-count]') + .contains('5'); + + cy.get('[data-test=item-from]') + .contains('41'); + + cy.get('[data-test=item-to]') + .contains('50'); + + cy.get('[data-test=total-items]') + .contains('50'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$inlinecount=allpages&$top=10&$skip=40&$orderby=Name asc&$filter=(Gender eq 'male')`); + }); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(1); + expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { newValues: { pageNumber: 5, pageSize: 10 }, type: 'pagination' }); + }); + }); + + it('should Clear all Filters and expect to go back to first page', () => { + cy.get('#grid31') + .find('button.slick-grid-menu-button') + .trigger('click') + .click({ force: true }); + + cy.get(`.slick-grid-menu:visible`) + .find('.slick-menu-item') + .first() + .find('span') + .contains('Clear all Filters') + .click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished!!'); + + cy.get('[data-test=page-number-input]') + .invoke('val') + .then(pageNumber => expect(pageNumber).to.eq('1')); + + cy.get('[data-test=page-count]') + .contains('10'); + + cy.get('[data-test=item-from]') + .contains('1'); + + cy.get('[data-test=item-to]') + .contains('10'); + + cy.get('[data-test=total-items]') + .contains('100'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$inlinecount=allpages&$top=10&$orderby=Name asc`); + }); + + cy.window().then((win) => { + // TODO look into, this should be called 2x times not 3x times + // expect(win.console.log).to.have.callCount(2); + expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { newValues: [], type: 'filter' }); + expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { newValues: { pageNumber: 1, pageSize: 10 }, type: 'pagination' }); + }); + }); + + it('should Clear all Sorting', () => { + cy.get('#grid31') + .find('button.slick-grid-menu-button') + .trigger('click') + .click(); + + cy.get(`.slick-grid-menu:visible`) + .find('.slick-menu-item:nth(1)') + .find('span') + .contains('Clear all Sorting') + .click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished!!'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$inlinecount=allpages&$top=10`); + }); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(1); + expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { newValues: [], type: 'sorter' }); + }); + }); + + it('should use "substringof" when OData version is set to 2', () => { + cy.get('.search-filter.filter-name') + .find('input') + .type('John'); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished!!'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$inlinecount=allpages&$top=10&$filter=(substringof('John', Name))`); + }); + + cy.get('#grid31') + .find('.slick-row') + .should('have.length', 1); + }); + + it('should use "contains" when OData version is set to 4', () => { + cy.get('[data-test=version4]') + .click(); + + cy.get('.search-filter.filter-name') + .find('input') + .type('John'); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished!!'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$count=true&$top=10&$filter=(contains(Name, 'John'))`); + }); + + cy.get('#grid31') + .find('.slick-row') + .should('have.length', 1); + }); + + it('should click on Set Dynamic Filter and expect query and filters to be changed', () => { + cy.get('[data-test=set-dynamic-filter]') + .click(); + + cy.get('.search-filter.filter-name select') + .should('have.value', 'a*'); + + cy.get('.search-filter.filter-name') + .find('input') + .invoke('val') + .then(text => expect(text).to.eq('A')); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished!!'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$count=true&$top=10&$filter=(startswith(Name, 'A'))`); + }); + + cy.get('#grid31') + .find('.slick-row') + .should('have.length', 5); + }); + }); + + describe('when "enableCount" is unchecked (not set)', () => { + it('should Clear all Filters, set 20 items per page & uncheck "enableCount"', () => { + cy.get('#grid31') + .find('button.slick-grid-menu-button') + .trigger('click') + .click(); + + cy.get(`.slick-grid-menu:visible`) + .find('.slick-menu-item') + .first() + .find('span') + .contains('Clear all Filters') + .click(); + + cy.get('#items-per-page-label').select('20'); + + cy.get('[data-test=enable-count]').click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished!!'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$top=20`); + }); + }); + + it('should change Pagination to next page', () => { + cy.get('.icon-seek-next').click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished!!'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$top=20&$skip=20`); + }); + }); + + it('should change Pagination to first page with 10 items', () => { + cy.get('#items-per-page-label').select('10'); + + // wait for the query to start and finish + cy.get('[data-test=status]').should('contain', 'loading...'); + cy.get('[data-test=status]').should('contain', 'finished!!'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$top=10`); + }); + }); + + it('should change Pagination to last page', () => { + cy.get('.icon-seek-end').click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished!!'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$top=10&$skip=90`); + }); + }); + + it('should click on "Name" column to sort it Ascending', () => { + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(1)') + .click(); + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(1)') + .find('.slick-sort-indicator.slick-sort-indicator-asc') + .should('be.visible'); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished!!'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$top=10&$skip=90&$orderby=Name asc`); + }); + }); + + it('should Clear all Sorting', () => { + cy.get('#grid31') + .find('button.slick-grid-menu-button') + .trigger('click') + .click(); + + cy.get(`.slick-grid-menu:visible`) + .find('.slick-menu-item:nth(1)') + .find('span') + .contains('Clear all Sorting') + .click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished!!'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$top=10&$skip=90`); + }); + }); + + it('should click on Set Dynamic Filter and expect query and filters to be changed', () => { + cy.get('[data-test=set-dynamic-filter]') + .click(); + + cy.get('.search-filter.filter-name select') + .should('have.value', 'a*'); + + cy.get('.search-filter.filter-name') + .find('input') + .invoke('val') + .then(text => expect(text).to.eq('A')); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished!!'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$top=10&$filter=(startswith(Name, 'A'))`); + }); + + cy.get('#grid31') + .find('.slick-row') + .should('have.length', 5); + }); + + it('should use "substringof" when OData version is set to 2', () => { + cy.get('[data-test=version2]') + .click(); + + cy.get('.search-filter.filter-name') + .find('input') + .type('John'); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished!!'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$top=10&$filter=(substringof('John', Name))`); + }); + + cy.get('#grid31') + .find('.slick-row') + .should('have.length', 1); + }); + + it('should use "contains" when OData version is set to 4', () => { + cy.get('[data-test=version4]') + .click(); + + cy.get('.search-filter.filter-name') + .find('input') + .type('John'); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished!!'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$top=10&$filter=(contains(Name, 'John'))`); + }); + + cy.get('#grid31') + .find('.slick-row') + .should('have.length', 1); + }); + }); + + describe('General Pagination Behaviors', () => { + it('should type a filter which returns an empty dataset', () => { + cy.get('.search-filter.filter-name') + .find('input') + .clear() + .type('xy'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$top=10&$filter=(contains(Name, 'xy'))`); + }); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished!!'); + + cy.get('.slick-empty-data-warning:visible') + .contains('No data to display.'); + }); + + it('should display page 0 of 0 but hide pagination from/to numbers when filtered data "xy" returns an empty dataset', () => { + cy.get('[data-test=page-count]') + .contains('0'); + + cy.get('[data-test=item-from]') + .should('not.be.visible'); + + cy.get('[data-test=item-to]') + .should('not.be.visible'); + + cy.get('[data-test=total-items]') + .contains('0'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$top=10&$filter=(contains(Name, 'xy'))`); + }); + + cy.get('[data-test=page-number-input]') + .invoke('val') + .then(pageNumber => expect(pageNumber).to.eq('0')); + }); + + it('should erase part of the filter so that it filters with "x"', () => { + cy.get('.search-filter.filter-name') + .find('input') + .type('{backspace}'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$top=10&$filter=(contains(Name, 'x'))`); + }); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished!!'); + + cy.get('.slick-empty-data-warning') + .contains('No data to display.') + .should('not.be.visible'); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(2); + expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { newValues: [{ columnId: 'name', operator: 'Contains', searchTerms: ['x'], targetSelector: 'input.form-control.filter-name.compound-input.filled' }], type: 'filter' }); + expect(win.console.log).to.be.calledWith('Client sample, Grid State changed:: ', { newValues: { pageNumber: 1, pageSize: 10 }, type: 'pagination' }); + }); + }); + + it('should display page 1 of 1 with 2 items after erasing part of the filter to be "x" which should return 1 page', () => { + cy.wait(50); + + cy.get('[data-test=page-count]') + .contains('1'); + + cy.get('[data-test=item-from]') + .contains('1'); + + cy.get('[data-test=item-to]') + .contains('2'); + + cy.get('[data-test=total-items]') + .contains('2'); + + cy.get('[data-test=page-number-input]') + .invoke('val') + .then(pageNumber => expect(pageNumber).to.eq('1')); + }); + }); + + describe('Set Dynamic Sorting', () => { + it('should click on "Set Filters Dynamically" then on "Set Sorting Dynamically"', () => { + cy.get('[data-test=set-dynamic-filter]') + .click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'loading...'); + cy.get('[data-test=status]').should('contain', 'finished!!'); + + cy.get('[data-test=set-dynamic-sorting]') + .click(); + + cy.get('[data-test=status]').should('contain', 'loading...'); + cy.get('[data-test=status]').should('contain', 'finished!!'); + }); + + it('should expect the grid to be sorted by "Name" descending', () => { + cy.get('#grid31') + .get('.slick-header-column:nth(1)') + .find('.slick-sort-indicator-desc') + .should('have.length', 1); + + cy.get('.slick-row') + .first() + .children('.slick-cell:nth(1)') + .should('contain', 'Ayers Hood'); + + cy.get('.slick-row') + .last() + .children('.slick-cell:nth(1)') + .should('contain', 'Alexander Foley'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$top=10&$orderby=Name desc&$filter=(startswith(Name, 'A'))`); + }); + }); + }); + + describe('Editors & Filters with RxJS Observable', () => { + it('should open the "Gender" filter and expect to find 3 options in its list ([blank], male, female)', () => { + const expectedOptions = ['', 'male', 'female']; + cy.get('.ms-filter.filter-gender:visible').click(); + + cy.get('[data-name="filter-gender"].ms-drop') + .find('li:visible') + .should('have.length', 3); + + cy.get('[data-name="filter-gender"].ms-drop') + .find('li:visible span') + .each(($li, index) => expect($li.text()).to.eq(expectedOptions[index])); + + cy.get('#grid31') + .find('.slick-row') + .should('have.length', 5); + }); + + it('should select "male" Gender and expect only 4 rows left in the grid', () => { + cy.get('[data-name="filter-gender"].ms-drop') + .find('li:visible:nth(1)') + .contains('male') + .click(); + + cy.get('#grid31') + .find('.slick-row') + .should('have.length', 4); + }); + + it('should be able to open "Gender" on the first row and expect to find 2 options the editor list (male, female) and expect male to be selected', () => { + const expectedOptions = ['male', 'female']; + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`) + .click(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`) + .should('contain', 'male') + .click() + .type('{enter}'); + + cy.get('[data-name="editor-gender"].ms-drop') + .find('li:visible') + .should('have.length', 2); + + cy.get('[data-name="editor-gender"].ms-drop') + .find('li:visible span') + .each(($li, index) => expect($li.text()).to.eq(expectedOptions[index])); + + cy.get('[data-name="editor-gender"]') + .find('li.selected') + .find('input[data-name=selectItemeditor-gender][value=male]') + .should('exist'); + }); + + it('should click on "Add Other Gender via RxJS" button', () => { + cy.get('[data-test="add-gender-button"]').should('not.be.disabled'); + cy.get('[data-test="add-gender-button"]').click(); + cy.get('[data-test="add-gender-button"]').should('be.disabled'); + }); + + it('should select 1st row', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`) + .click(); + + cy.get('#grid31') + .find('.slick-row') + .children() + .filter('.slick-cell-checkboxsel.selected') + .should('have.length', 1); + }); + + it('should open the "Gender" editor on the first row and expect to find 1 more option the editor list (male, female, other)', () => { + const expectedOptions = ['male', 'female', 'other']; + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`) + .should('contain', 'male') + .click(); + + cy.get('[data-name="editor-gender"].ms-drop') + .find('li:visible') + .should('have.length', 3); + + cy.get('[data-name="editor-gender"].ms-drop') + .find('li:visible span') + .each(($li, index) => expect($li.text()).to.eq(expectedOptions[index])); + + cy.get('[data-name="editor-gender"]') + .find('li.selected') + .find('input[data-name=selectItemeditor-gender][value=male]') + .should('exist'); + }); + + it('should be able to change the Gender editor on the first row to the new option "other"', () => { + cy.get('[data-name="editor-gender"].ms-drop') + .find('li:visible:nth(2)') + .contains('other') + .click(); + }); + + it('should open Gender filter and now expect to see 1 more option in its list ([blank], male, female, other)', () => { + const expectedOptions = ['', 'male', 'female', 'other']; + cy.get('.ms-filter.filter-gender:visible').click(); + + cy.get('[data-name="filter-gender"].ms-drop') + .find('li:visible') + .should('have.length', 4); + + cy.get('[data-name="filter-gender"].ms-drop') + .find('li:visible span') + .each(($li, index) => expect($li.text()).to.eq(expectedOptions[index])); + }); + + it('should choose "other" form the Gender filter and expect 1 row left in the grid', () => { + cy.get('[data-name="filter-gender"].ms-drop') + .find('li:visible:nth(3)') + .contains('other') + .click(); + + cy.get('#grid31') + .find('.slick-row') + .should('have.length', 0); + }); + }); + + describe('Select and Expand Behaviors', () => { + it('should enable "enableSelect" and "enableExpand" and expect the query to select/expand all fields', () => { + cy.get('[data-test=enable-expand]').click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$top=10&$orderby=Name desc&$expand=category`); + }); + + cy.get('[data-test=enable-select]').click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$top=10&$orderby=Name desc&$select=id,name,gender,company&$expand=category($select=name)`); + }); + }); + + it('should try to sort and filter on "Category" and expect the query to be succesful', () => { + cy.get('[data-test=clear-filters-sorting]').click(); + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(4)') + .click(); + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(4)') + .find('.slick-sort-indicator.slick-sort-indicator-asc') + .should('exist'); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$top=10&$orderby=Category/name asc&$select=id,name,gender,company&$expand=category($select=name)`); + }); + + cy.get('input.search-filter.filter-category_name') + .type('Silver'); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$top=10&$orderby=Category/name asc&$filter=(contains(Category/name, 'Silver'))&$select=id,name,gender,company&$expand=category($select=name)`); + }); + + cy.get('[data-test=page-number-input]') + .invoke('val') + .then(pageNumber => expect(pageNumber).to.eq('1')); + + cy.get('[data-test=page-count]') + .contains('4'); + + cy.get('[data-test=item-from]') + .contains('1'); + + cy.get('[data-test=item-to]') + .contains('10'); + + cy.get('[data-test=total-items]') + .contains('32'); + }); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example32.cy.ts b/demos/vue/test/cypress/e2e/example32.cy.ts new file mode 100644 index 000000000..dd5f0120b --- /dev/null +++ b/demos/vue/test/cypress/e2e/example32.cy.ts @@ -0,0 +1,325 @@ +describe('Example 32 - Columns Resize by Content', () => { + const GRID_ROW_HEIGHT = 33; + + beforeEach(() => { + // create a console.log spy for later use + cy.window().then((win) => { + cy.spy(win.console, 'log'); + }); + }); + + describe('Main Tests', () => { + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example32`); + cy.get('h2').should('contain', 'Example 32: Columns Resize by Content'); + }); + + it('should have cell that fit the text content', () => { + cy.get('.slick-row').find('.slick-cell:nth(1)').invoke('width').should('be.gt', 75); + cy.get('.slick-row').find('.slick-cell:nth(2)').invoke('width').should('be.gt', 67); + cy.get('.slick-row').find('.slick-cell:nth(3)').invoke('width').should('be.gt', 59); + cy.get('.slick-row').find('.slick-cell:nth(4)').invoke('width').should('be.gt', 102); + cy.get('.slick-row').find('.slick-cell:nth(5)').invoke('width').should('be.gt', 89); + cy.get('.slick-row').find('.slick-cell:nth(6)').invoke('width').should('be.gt', 72); + cy.get('.slick-row').find('.slick-cell:nth(7)').invoke('width').should('be.gt', 67); + cy.get('.slick-row').find('.slick-cell:nth(8)').invoke('width').should('be.gt', 72); + cy.get('.slick-row').find('.slick-cell:nth(9)').invoke('width').should('be.gt', 179); + cy.get('.slick-row').find('.slick-cell:nth(10)').invoke('width').should('be.gt', 94); + cy.get('.slick-row').find('.slick-cell:nth(11)').invoke('width').should('equal', 58); + }); + + it('should make the grid readonly and expect to fit the text by content and expect column width to be the same as earlier', () => { + cy.get('[data-test="toggle-readonly-btn"]').click(); + + cy.get('.slick-row').find('.slick-cell:nth(1)').invoke('width').should('be.gt', 75); + cy.get('.slick-row').find('.slick-cell:nth(2)').invoke('width').should('be.gt', 67); + cy.get('.slick-row').find('.slick-cell:nth(3)').invoke('width').should('be.gt', 59); + cy.get('.slick-row').find('.slick-cell:nth(4)').invoke('width').should('be.gt', 102); + cy.get('.slick-row').find('.slick-cell:nth(5)').invoke('width').should('be.gt', 89); + cy.get('.slick-row').find('.slick-cell:nth(6)').invoke('width').should('be.gt', 72); + cy.get('.slick-row').find('.slick-cell:nth(7)').invoke('width').should('be.gt', 67); + cy.get('.slick-row').find('.slick-cell:nth(8)').invoke('width').should('be.gt', 72); + cy.get('.slick-row').find('.slick-cell:nth(9)').invoke('width').should('be.gt', 179); + cy.get('.slick-row').find('.slick-cell:nth(10)').invoke('width').should('be.gt', 94); + cy.get('.slick-row').find('.slick-cell:nth(11)').invoke('width').should('equal', 58); + }); + + it('should click on (default resize "autosizeColumns") and expect column to be much thinner and fit all its column within the grid container', () => { + cy.get('[data-test="autosize-columns-btn"]').click(); + + cy.get('.slick-row').find('.slick-cell:nth(1)').invoke('width').should('be.lt', 75); + cy.get('.slick-row').find('.slick-cell:nth(2)').invoke('width').should('be.lt', 95); + cy.get('.slick-row').find('.slick-cell:nth(3)').invoke('width').should('be.lt', 70); + cy.get('.slick-row').find('.slick-cell:nth(4)').invoke('width').should('be.lt', 100); + cy.get('.slick-row').find('.slick-cell:nth(5)').invoke('width').should('be.lt', 100); + cy.get('.slick-row').find('.slick-cell:nth(6)').invoke('width').should('be.lt', 85); + cy.get('.slick-row').find('.slick-cell:nth(7)').invoke('width').should('be.lt', 70); + cy.get('.slick-row').find('.slick-cell:nth(8)').invoke('width').should('be.lt', 85); + cy.get('.slick-row').find('.slick-cell:nth(9)').invoke('width').should('be.lt', 120); + cy.get('.slick-row').find('.slick-cell:nth(10)').invoke('width').should('be.lt', 100); + cy.get('.slick-row').find('.slick-cell:nth(11)').invoke('width').should('equal', 58); + }); + + it('should double-click on the "Complexity" column resize handle and expect the column to become wider and show all text', () => { + cy.get('.slick-row').find('.slick-cell:nth(5)').invoke('width').should('be.lt', 80); + + cy.get('.slick-header-column:nth-child(6) .slick-resizable-handle') + .dblclick(); + + cy.get('.slick-row').find('.slick-cell:nth(5)').invoke('width').should('be.gt', 95); + }); + + it('should open the "Product" header menu and click on "Resize by Content" and expect the column to become wider and show all text', () => { + cy.get('.slick-row').find('.slick-cell:nth(9)').invoke('width').should('be.lt', 120); + + cy.get('#grid32') + .find('.slick-header-column:nth-child(10)') + .trigger('mouseover') + .children('.slick-header-menu-button') + .invoke('show') + .click(); + + cy.get('.slick-header-menu .slick-menu-command-list') + .should('be.visible') + .children('.slick-menu-item:nth-of-type(1)') + .children('.slick-menu-content') + .should('contain', 'Resize by Content') + .click(); + + cy.get('.slick-row').find('.slick-cell:nth(9)').invoke('width').should('be.gt', 120); + }); + + it('should change row selection across multiple pages, first page should have 2 selected', () => { + cy.get('[data-test="set-dynamic-rows-btn"]').click(); + + // Row index 3, 4 and 11 (last one will be on 2nd page) + cy.get('input[type="checkbox"]:checked').should('have.length', 2); // 2x in current page and 1x in next page + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(0) input[type="checkbox"]`).should('be.checked'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(0) input[type="checkbox"]`).should('be.checked'); + }); + + it('should go to next page and expect 1 row selected in that second page', () => { + cy.get('.icon-seek-next').click(); + + cy.get('input[type="checkbox"]:checked').should('have.length', 1); // only 1x row in page 2 + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0) input[type="checkbox"]`).should('be.checked'); + }); + + it('should click on "Select All" checkbox and expect all rows selected in current page', () => { + const expectedRowIds = [11, 3, 4]; + + // go back to 1st page + cy.get('.icon-seek-prev') + .click(); + + cy.get('#filter-checkbox-selectall-container input[type=checkbox]') + .click({ force: true }); + + cy.window().then((win) => { + expect(win.console.log).to.have.callCount(3); + expect(win.console.log).to.be.calledWith('Selected Ids:', expectedRowIds); + }); + }); + + it('should go to the next 2 pages and expect all rows selected in each page', () => { + cy.get('.icon-seek-next') + .click(); + + cy.get('.slick-cell-checkboxsel input:checked') + .should('have.length', 10); + + cy.get('.icon-seek-next') + .click(); + + cy.get('.slick-cell-checkboxsel input:checked') + .should('have.length', 10); + }); + + it('should uncheck 1 row and expect current and next page to have "Select All" uncheck', () => { + cy.get('.slick-row:nth(0) .slick-cell:nth(0) input[type=checkbox]') + .click({ force: true }); + + cy.get('#filter-checkbox-selectall-container input[type=checkbox]') + .should('not.be.checked', true); + + cy.get('.icon-seek-next') + .click(); + + cy.get('#filter-checkbox-selectall-container input[type=checkbox]') + .should('not.be.checked', true); + }); + + it('should go back to previous page, select the row that was unchecked and expect "Select All" to be selected again', () => { + cy.get('.icon-seek-prev') + .click(); + + cy.get('.slick-row:nth(0) .slick-cell:nth(0) input[type=checkbox]') + .click({ force: true }); + + cy.get('#filter-checkbox-selectall-container input[type=checkbox]') + .should('be.checked', true); + + cy.get('.icon-seek-next') + .click(); + + cy.get('#filter-checkbox-selectall-container input[type=checkbox]') + .should('be.checked', true); + }); + + it('should Unselect All and expect all pages to no longer have any row selected', () => { + cy.get('#filter-checkbox-selectall-container input[type=checkbox]') + .click({ force: true }); + + cy.get('.slick-cell-checkboxsel input:checked') + .should('have.length', 0); + + cy.get('.icon-seek-prev') + .click(); + + cy.get('.slick-cell-checkboxsel input:checked') + .should('have.length', 0); + + cy.get('.icon-seek-prev') + .click(); + + cy.get('.slick-cell-checkboxsel input:checked') + .should('have.length', 0); + }); + }); + + describe('Filter Predicate on "Title" column that act similarly to an SQL LIKE matcher', () => { + it('should return 4 rows using "%10" (ends with 10)', () => { + cy.get('.search-filter.filter-title') + .clear() + .type('%10'); + + cy.get('[data-test="total-items"]') + .should('have.text', 4); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 10'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 110'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 210'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 310'); + }); + + it('should return 4 rows using "%ask%20" (contains "ask" + ends with 20)', () => { + cy.get('.search-filter.filter-title') + .clear() + .type('%ask%20'); + + cy.get('[data-test="total-items"]') + .should('have.text', 4); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 20'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 120'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 220'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 320'); + }); + + it('should return all 400 rows using "%ask%" (contains "ask")', () => { + cy.get('.search-filter.filter-title') + .clear() + .type('%ask%'); + + cy.get('[data-test="total-items"]') + .should('have.text', 400); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 0'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 1'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 2'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 3'); + }); + + it('should return 4 rows using "Ta%30" (starts with "Ta" + ends with 30)', () => { + cy.get('.search-filter.filter-title') + .clear() + .type('Ta%30'); + + cy.get('[data-test="total-items"]') + .should('have.text', 4); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 30'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 130'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 230'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 330'); + }); + + it('should return 14 rows using "Ta%30%" (starts with "Ta" + ends with 30)', () => { + cy.get('.search-filter.filter-title') + .clear() + .type('Ta%30%'); + + cy.get('[data-test="total-items"]') + .should('have.text', 14); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 30'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 130'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 230'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 300'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 301'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 302'); + }); + + it('should return all 400 rows using "Ta%" (starts with "Ta")', () => { + cy.get('.search-filter.filter-title') + .clear() + .type('Ta%'); + + cy.get('[data-test="total-items"]') + .should('have.text', 400); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 0'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 1'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 2'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 3'); + }); + + it('should return 14 rows using "25" (contains 25)', () => { + cy.get('.search-filter.filter-title') + .clear() + .type('25'); + + cy.get('[data-test="total-items"]') + .should('have.text', 14); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 25'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 125'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 225'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 250'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 251'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell:nth(1)`).should('have.text', 'Task 252'); + }); + + it('should not return any row when filtering Title with "%%"', () => { + cy.get('.search-filter.filter-title') + .clear() + .type('%%'); + + cy.get('[data-test="total-items"]') + .should('have.text', 0); + }); + + it('return all 400 rows when filtering Title as "%ask%"', () => { + cy.get('.search-filter.filter-duration').clear(); + cy.get('.search-filter.filter-title') + .clear() + .type('%ask%'); + + cy.get('[data-test="total-items"]') + .should('have.text', 400); + }); + + it('return some rows (not all 400) when filtering Title as "%ask%" AND a Duration ">50" to test few filters still working', () => { + cy.get('.search-filter.filter-title').clear(); + cy.get('.search-filter.filter-duration') + .clear() + .type('>50'); + + cy.get('[data-test="total-items"]') + .should('not.have.text', 0); + + cy.get('[data-test="total-items"]') + .should('not.have.text', 400); + }); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example33.cy.ts b/demos/vue/test/cypress/e2e/example33.cy.ts new file mode 100644 index 000000000..3fcbe1106 --- /dev/null +++ b/demos/vue/test/cypress/e2e/example33.cy.ts @@ -0,0 +1,257 @@ +describe('Example 33 - Regular & Custom Tooltips', () => { + const titles = ['', 'Title', 'Duration', 'Description', 'Description 2', 'Cost', '% Complete', 'Start', 'Finish', 'Effort Driven', 'Prerequisites', 'Action']; + const GRID_ROW_HEIGHT = 33; + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example33`); + cy.get('h2').should('contain', 'Example 33: Regular & Custom Tooltips'); + }); + + it('should have exact column titles on 1st grid', () => { + cy.get('#grid33') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(titles[index])); + }); + + it('should change server delay to 10ms for faster testing', () => { + cy.get('[data-test="server-delay"]').type('{backspace}{backspace}{backspace}10'); + }); + + it('should mouse over 1st row checkbox column and NOT expect any tooltip to show since it is disabled on that column', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).as('checkbox0-cell'); + cy.get('@checkbox0-cell').trigger('mouseover'); + + cy.get('.slick-custom-tooltip').should('not.exist'); + cy.get('@checkbox0-cell').trigger('mouseout'); + }); + + it('should mouse over Task 2 cell and expect async tooltip to show', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).as('task1-cell'); + cy.get('@task1-cell').should('contain', 'Task 2'); + cy.get('@task1-cell').trigger('mouseover'); + cy.get('.slick-custom-tooltip').contains('loading...'); + + cy.wait(10); + cy.get('.slick-custom-tooltip').should('be.visible'); + cy.get('.slick-custom-tooltip').contains('Task 2 - (async tooltip)'); + + cy.get('.tooltip-2cols-row:nth(0)').find('div:nth(0)').contains('Completion:'); + cy.get('.tooltip-2cols-row:nth(0)').find('div').should('have.class', 'percent-complete-bar-with-text'); + + cy.get('.tooltip-2cols-row:nth(1)').find('div:nth(0)').contains('Lifespan:'); + cy.get('.tooltip-2cols-row:nth(1)').find('div:nth(1)').contains(/\d+$/); // use regexp to make sure it's a number + + cy.get('.tooltip-2cols-row:nth(2)').find('div:nth(0)').contains('Ratio:'); + cy.get('.tooltip-2cols-row:nth(2)').find('div:nth(1)').contains(/\d+$/); // use regexp to make sure it's a number + + cy.get('@task1-cell').trigger('mouseout'); + }); + + it('should mouse over Task 6 cell and expect async tooltip to show', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).as('task6-cell'); + cy.get('@task6-cell').should('contain', 'Task 6'); + cy.get('@task6-cell').trigger('mouseover'); + cy.get('.slick-custom-tooltip').contains('loading...'); + + cy.wait(10); + cy.get('.slick-custom-tooltip').should('be.visible'); + cy.get('.slick-custom-tooltip').contains('Task 6 - (async tooltip)'); + + cy.get('.tooltip-2cols-row:nth(1)').find('div:nth(0)').contains('Lifespan:'); + cy.get('.tooltip-2cols-row:nth(1)').find('div:nth(1)').contains(/\d+$/); // use regexp to make sure it's a number + + cy.get('.tooltip-2cols-row:nth(2)').find('div:nth(0)').contains('Ratio:'); + cy.get('.tooltip-2cols-row:nth(2)').find('div:nth(1)').contains(/\d+$/); // use regexp to make sure it's a number + + cy.get('@task6-cell').trigger('mouseout'); + }); + + it('should mouse over Task 6 cell on "Start" column and expect a delayed tooltip opening via async process', () => { + cy.get('.slick-custom-tooltip').should('not.exist'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(7)`).as('start6-cell'); + cy.get('@start6-cell').contains(/\d{4}-\d{2}-\d{2}$/); // use regexp to make sure it's a number + cy.get('@start6-cell').trigger('mouseover'); + + cy.wait(10); + cy.get('.slick-custom-tooltip').should('be.visible'); + cy.get('.slick-custom-tooltip').contains('Custom Tooltip'); + + cy.get('.tooltip-2cols-row:nth(0)').find('div:nth(0)').contains('Id:'); + cy.get('.tooltip-2cols-row:nth(0)').find('div:nth(1)').contains('6'); + + cy.get('.tooltip-2cols-row:nth(1)').find('div:nth(0)').contains('Title:'); + cy.get('.tooltip-2cols-row:nth(1)').find('div:nth(1)').contains('Task 6'); + + cy.get('.tooltip-2cols-row:nth(2)').find('div:nth(0)').contains('Effort Driven:'); + cy.get('.tooltip-2cols-row:nth(2)').find('div:nth(1)').should('be.empty'); + + cy.get('.tooltip-2cols-row:nth(3)').find('div:nth(0)').contains('Completion:'); + cy.get('.tooltip-2cols-row:nth(3)').find('div:nth(1)').find('.mdi-check-circle-outline').should('exist'); + + cy.get('@start6-cell').trigger('mouseout'); + }); + + it('should mouse over 6th row Description and expect full cell content to show in a tooltip because cell has ellipsis and is too long for the cell itself', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(3)`).as('desc6-cell'); + cy.get('@desc6-cell').should('contain', 'This is a sample task description.'); + cy.get('@desc6-cell').trigger('mouseover'); + + cy.get('.slick-custom-tooltip').should('be.visible'); + cy.get('.slick-custom-tooltip').should('not.contain', `regular tooltip (from title attribute)\nTask 6 cell value:\n\nThis is a sample task description.\nIt can be multiline\n\nAnother line...`); + cy.get('.slick-custom-tooltip').should('contain', `This is a sample task description.\nIt can be multiline\n\nAnother line...`); + + cy.get('@desc6-cell').trigger('mouseout'); + }); + + it('should mouse over 6th row Description 2 and expect regular tooltip title + concatenated full cell content when using "useRegularTooltipFromFormatterOnly: true"', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(4)`).as('desc2-5-cell'); + cy.get('@desc2-5-cell').should('contain', 'This is a sample task description.'); + cy.get('@desc2-5-cell').trigger('mouseover'); + + cy.get('.slick-custom-tooltip').should('be.visible'); + cy.get('.slick-custom-tooltip').should('contain', `regular tooltip (from title attribute)\nTask 6 cell value:\n\nThis is a sample task description.\nIt can be multiline\n\nAnother line...`); + + cy.get('@desc2-5-cell').trigger('mouseout'); + }); + + it('should mouse over 2nd row Duration and expect a custom tooltip shown with 4 label/value pairs displayed', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(2)`).as('duration2-cell'); + cy.get('@duration2-cell').contains(/\d+\sday[s]?$/); + cy.get('@duration2-cell').trigger('mouseover'); + + cy.get('.slick-custom-tooltip').should('be.visible'); + cy.get('.slick-custom-tooltip').contains('Custom Tooltip'); + + cy.get('.tooltip-2cols-row:nth(0)').find('div:nth(0)').contains('Id:'); + cy.get('.tooltip-2cols-row:nth(0)').find('div:nth(1)').contains('6'); + + cy.get('.tooltip-2cols-row:nth(1)').find('div:nth(0)').contains('Title:'); + cy.get('.tooltip-2cols-row:nth(1)').find('div:nth(1)').contains('Task 6'); + + cy.get('.tooltip-2cols-row:nth(2)').find('div:nth(0)').contains('Effort Driven:'); + cy.get('.tooltip-2cols-row:nth(2)').find('div:nth(1)').should('be.empty'); + + cy.get('.tooltip-2cols-row:nth(3)').find('div:nth(0)').contains('Completion:'); + cy.get('.tooltip-2cols-row:nth(3)').find('div:nth(1)').find('.mdi-check-circle-outline').should('exist'); + + cy.get('@duration2-cell').trigger('mouseout'); + }); + + it('should mouse over % Complete cell of Task 6 and expect regular tooltip to show with content "x %" where x is a number', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(6)`).as('percentage-cell'); + cy.get('@percentage-cell').find('.percent-complete-bar').should('exist'); + cy.get('@percentage-cell').trigger('mouseover'); + + cy.get('.slick-custom-tooltip').should('be.visible'); + cy.get('.slick-custom-tooltip').contains(/\d+%$/); + + cy.get('@percentage-cell').trigger('mouseout'); + }); + + it('should mouse over Prerequisite cell of Task 6 and expect regular tooltip to show with content "Task 6, Task 5"', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(10)`).as('prereq-cell'); + cy.get('@prereq-cell').should('contain', 'Task 6, Task 5'); + cy.get('@prereq-cell').trigger('mouseover'); + + cy.get('.slick-custom-tooltip').should('be.visible'); + cy.get('.slick-custom-tooltip').should('contain', 'Task 6, Task 5'); + + cy.get('@prereq-cell').trigger('mouseout'); + }); + + it('should mouse over header-row (filter) 1st column checkbox and NOT expect any tooltip to show since it is disabled on that column', () => { + cy.get(`.slick-headerrow-columns .slick-headerrow-column:nth(0)`).as('checkbox0-filter'); + cy.get('@checkbox0-filter').trigger('mouseover'); + + cy.get('.slick-custom-tooltip').should('not.exist'); + cy.get('@checkbox0-filter').trigger('mouseout'); + }); + + it('should mouse over header-row (filter) 2nd column Title and expect a tooltip to show rendered from an headerRowFormatter', () => { + cy.get(`.slick-headerrow-columns .slick-headerrow-column:nth(1)`).as('checkbox0-filter'); + cy.get('@checkbox0-filter').trigger('mouseover'); + + cy.get('.slick-custom-tooltip').should('be.visible'); + cy.get('.slick-custom-tooltip').contains('Custom Tooltip - Header Row (filter)'); + + cy.get('.tooltip-2cols-row:nth(0)').find('div:nth(0)').contains('Column:'); + cy.get('.tooltip-2cols-row:nth(0)').find('div:nth(1)').contains('title'); + + cy.get('@checkbox0-filter').trigger('mouseout'); + }); + + it('should mouse over header-row (filter) Finish column and NOT expect any tooltip to show since it is disabled on that column', () => { + cy.get(`.slick-headerrow-columns .slick-headerrow-column:nth(8)`).as('finish-filter'); + cy.get('@finish-filter').trigger('mouseover'); + + cy.get('.slick-custom-tooltip').should('not.exist'); + cy.get('@finish-filter').trigger('mouseout'); + }); + + it('should mouse over header-row (filter) Prerequisite column and expect to see tooltip of selected filter options', () => { + cy.get(`.slick-headerrow-columns .slick-headerrow-column:nth(10)`).as('checkbox10-header'); + cy.get('@checkbox10-header').trigger('mouseover'); + + cy.get('.filter-prerequisites .ms-choice span').contains('15 of 500 selected'); + cy.get('.slick-custom-tooltip').should('be.visible'); + cy.get('.slick-custom-tooltip').contains('Task 1, Task 3, Task 5, Task 7, Task 9, Task 12, Task 15, Task 18, Task 21, Task 25, Task 28, Task 29, Task 30, Task 32, Task 34'); + + cy.get('@checkbox10-header').trigger('mouseout'); + }); + + it('should mouse over header title on 1st column with checkbox and NOT expect any tooltip to show since it is disabled on that column', () => { + cy.get(`.slick-header-columns .slick-header-column:nth(0)`).as('checkbox-header'); + cy.get('@checkbox-header').trigger('mouseover'); + + cy.get('.slick-custom-tooltip').should('not.exist'); + cy.get('@checkbox-header').trigger('mouseout'); + }); + + it('should mouse over header title on 2nd column with Title name and expect a tooltip to show rendered from an headerFormatter', () => { + cy.get(`.slick-header-columns .slick-header-column:nth(1)`).as('checkbox0-header'); + cy.get('@checkbox0-header').trigger('mouseover'); + + cy.get('.slick-custom-tooltip').should('be.visible'); + cy.get('.slick-custom-tooltip').contains('Custom Tooltip - Header'); + + cy.get('.tooltip-2cols-row:nth(0)').find('div:nth(0)').contains('Column:'); + cy.get('.tooltip-2cols-row:nth(0)').find('div:nth(1)').contains('Title'); + + cy.get('@checkbox0-header').trigger('mouseout'); + }); + + it('should mouse over header title on 2nd column with Finish name and NOT expect any tooltip to show since it is disabled on that column', () => { + cy.get(`.slick-header-columns .slick-header-column:nth(8)`).as('finish-header'); + cy.get('@finish-header').trigger('mouseover'); + + cy.get('.slick-custom-tooltip').should('not.exist'); + cy.get('@finish-header').trigger('mouseout'); + }); + + it('should click Prerequisite editor of 1st row (Task 2) and expect Task1 & 2 to be selected in the multiple-select drop', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(10)`).as('prereq-cell'); + cy.get('@prereq-cell') + .should('contain', 'Task 2, Task 1') + .click(); + + cy.get('div.ms-drop[data-name=editor-prerequisites]') + .find('li.selected') + .should('have.length', 2); + + cy.get('div.ms-drop[data-name=editor-prerequisites]') + .find('li.selected:nth(0) span') + .should('contain', 'Task 1'); + + cy.get('div.ms-drop[data-name=editor-prerequisites]') + .find('li.selected:nth(1) span') + .should('contain', 'Task 2'); + + cy.get('div.ms-drop[data-name=editor-prerequisites]') + .find('.ms-ok-button') + .click(); + + cy.get('div.ms-drop[data-name=editor-prerequisites]') + .should('not.exist'); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example34.cy.ts b/demos/vue/test/cypress/e2e/example34.cy.ts new file mode 100644 index 000000000..4bd651dfa --- /dev/null +++ b/demos/vue/test/cypress/e2e/example34.cy.ts @@ -0,0 +1,63 @@ +describe('Example 34 - Real-Time Trading Platform', () => { + const titles = ['Currency', 'Symbol', 'Market', 'Company', 'Type', 'Change', 'Price', 'Quantity', 'Amount', 'Price History', 'Execution Timestamp']; + const GRID_ROW_HEIGHT = 35; + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example34`); + cy.get('h2').should('contain', 'Example 34: Real-Time Trading Platform'); + }); + + it('should have exact column titles on 1st grid', () => { + cy.get('#grid34') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(titles[index])); + }); + + it('should check first 5 rows and expect certain data', () => { + for (let i = 0; i < 5; i++) { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * i}px;"] > .slick-cell:nth(0)`).contains(/CAD|USD$/); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * i}px;"] > .slick-cell:nth(4)`).contains(/Buy|Sell$/); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * i}px;"] > .slick-cell:nth(5)`).contains(/\$\(?[0-9.]*\)?/); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * i}px;"] > .slick-cell:nth(6)`).contains(/\$[0-9.]*/); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * i}px;"] > .slick-cell:nth(7)`).contains(/\d$/); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * i}px;"] > .slick-cell:nth(8)`).contains(/\$[0-9.]*/); + } + }); + + it('should find multiple green & pink backgrounds to show gains & losses when in real-time mode', () => { + cy.get('#refreshRateRange').invoke('val', 5).trigger('change'); + + cy.get('.changed-gain').should('have.length.gt', 2); + cy.get('.changed-loss').should('have.length.gt', 2); + }); + + it('should NOT find any green neither pink backgrounds when in real-time is stopped', () => { + cy.get('[data-test="highlight-input"]').type('{backspace}{backspace}'); + cy.get('[data-test="stop-btn"]').click(); + + cy.wait(5); + cy.get('.changed-gain').should('have.length', 0); + cy.get('.changed-loss').should('have.length', 0); + cy.wait(1); + cy.get('.changed-gain').should('have.length', 0); + cy.get('.changed-loss').should('have.length', 0); + }); + + it('should Group by 1st column "Currency" and expect 2 groups with Totals when collapsed', () => { + cy.get('.slick-header-column:nth(0)') + .contains('Currency') + .drag('.slick-dropzone', { force: true }); + + cy.get('.slick-group-toggle-all') + .click(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-toggle.collapsed`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Currency: CAD'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(8)`).contains(/\$[0-9,.]*/); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0) .slick-group-toggle.collapsed`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Currency: USD'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(8)`).contains(/\$[0-9,.]*/); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example35.cy.ts b/demos/vue/test/cypress/e2e/example35.cy.ts new file mode 100644 index 000000000..029fb2b4e --- /dev/null +++ b/demos/vue/test/cypress/e2e/example35.cy.ts @@ -0,0 +1,169 @@ +describe('Example 35 - Row Based Editing', () => { + const fullTitles = ['Title', 'Duration (days)', '% Complete', 'Start', 'Finish', 'Effort Driven', 'Actions']; + const GRID_ROW_HEIGHT = 35; + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example35`); + cy.get('h2').should('contain', 'Example 35: Row Based Editing'); + }); + + it('should have exact column titles on grid', () => { + cy.get('#grid35') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + }); + + it('should render edit and delete buttons in the actions column', () => { + cy.get('.slick-cell.l6.r6').each(($child) => { + cy.wrap($child).find('.action-btns--edit, .action-btns--delete').should('have.length', 2); + }); + }); + + it('should only allow to toggle a single row into editmode on single mode', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] .action-btns--edit`).click(); + cy.get('.action-btns--edit:nth(0)').click({ force: true }); + + cy.get('.slick-row.slick-rbe-editmode').should('have.length', 1); + }); + + it('should allow to toggle a multiple rows into editmode on multiple mode', () => { + cy.reload(); + cy.get('[data-test="single-multi-toggle"]').click(); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] .action-btns--edit`).click({ force: true }); + cy.get('.action-btns--edit').eq(1).click({ force: true }); + cy.get('.action-btns--edit').eq(2).click({ force: true }); + + cy.get('.slick-row.slick-rbe-editmode').should('have.length', 3); + }); + + it('should not display editor in rows not being in editmode', () => { + cy.reload(); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l2.r2`).click({ force: true }); + + cy.get('input').should('have.length', 0); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] .action-btns--edit`).click({ force: true }); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l2.r2`).click({ force: true }); + + cy.get('input').should('have.length', 1); + }); + + it('should highlight modified cells and maintain proper index on sorting', () => { + cy.reload(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] .action-btns--edit`).click({ force: true }); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l0.r0`).click().type('abc{enter}'); + cy.get('.slick-cell').first().should('have.class', 'slick-rbe-unsaved-cell'); + cy.get('[data-id="title"]').click(); + cy.get('.slick-cell').first().should('not.have.class', 'slick-rbe-unsaved-cell'); + cy.get('[data-id="title"]').click(); + cy.get('.slick-cell').first().should('have.class', 'slick-rbe-unsaved-cell'); + }); + + it('should stay in editmode if saving failed', () => { + cy.reload(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] .action-btns--edit`).click({ force: true }); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l1.r1`).click().type('50{enter}'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l2.r2`).click().type('50'); + + cy.get('.action-btns--update').first().click(); + cy.on('window:confirm', () => true); + cy.on('window:alert', (str) => { + expect(str).to.equal('Sorry, 40 is the maximum allowed duration.'); + }); + + cy.get('.slick-row.slick-rbe-editmode').should('have.length', 1); + }); + + it('should save changes on update button click', () => { + cy.reload(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] .action-btns--edit`).click({ force: true }); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l1.r1`).click().type('30{enter}'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l2.r2`).type('30'); + + cy.get('.action-btns--update').first().click({ force: true }); + + cy.get('[data-test="fetch-result"]') + .should('contain', 'success'); + + cy.get('.slick-cell.l1.r1').first().should('contain', '30'); + cy.get('.slick-cell.l2.r2').first().should('contain', '30'); + }); + + it('should cleanup status when starting a new edit mode', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] .action-btns--edit`).click({ force: true }); + + cy.get('[data-test="fetch-result"]').should('be.empty'); + + cy.get('.action-btns--cancel').first().click({ force: true }); + }); + + it('should revert changes on cancel click', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] .action-btns--edit`).click({ force: true }); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l1.r1`).click().type('50{enter}'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l2.r2`).type('50{enter}'); + + cy.get('.action-btns--cancel').first().click({ force: true }); + + cy.get('.slick-cell.l1.r1').first().should('contain', '30'); + cy.get('.slick-cell.l2.r2').first().should('contain', '30'); + }); + + it('should delete a row when clicking it', () => { + cy.get('.action-btns--delete').first().click({ force: true }); + + cy.on('window:confirm', () => true); + + cy.get('.slick-row').first().find('.slick-cell.l0.r0').should('contain', 'Task 1'); + }); + + it('should support translation keys on buttons', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] .action-btns--edit`).click({ force: true }); + + cy.get('.action-btns--update') + .first() + .invoke('attr', 'title') + .then((title) => { + expect(title).to.equal('Update the current row'); + }); + + cy.get('.action-btns--cancel') + .first() + .invoke('attr', 'title') + .then((title) => { + expect(title).to.equal('Cancel changes of the current row'); + }); + + cy.get('[data-test="toggle-language"]').click(); + cy.get('[data-test="selected-locale"]').should('contain', 'fr.json'); + + cy.get('.action-btns--edit').first().click({ force: true }); + + cy.get('.action-btns--cancel').first().as('cancel-btn'); + cy.get('@cancel-btn').should(($btn) => { + expect($btn.attr('title')).to.equal('Annuler la ligne actuelle'); + }); + cy.get('@cancel-btn').trigger('mouseover', { position: 'top' }); + cy.get('.slick-custom-tooltip').should('be.visible'); + cy.get('.slick-custom-tooltip .tooltip-body').contains('Annuler la ligne actuelle'); + + cy.get('.action-btns--update').first().as('update-btn'); + cy.get('@update-btn').should(($btn) => { + expect($btn.attr('title')).to.equal('Mettre ร  jour la ligne actuelle'); + }); + + cy.get('@update-btn').trigger('mouseover', { position: 'top' }); + + cy.get('.slick-custom-tooltip').should('be.visible'); + cy.get('.slick-custom-tooltip .tooltip-body').contains('Mettre ร  jour la ligne actuelle'); + cy.get('@update-btn').first().click({ force: true }); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example36.cy.ts b/demos/vue/test/cypress/e2e/example36.cy.ts new file mode 100644 index 000000000..b5a007f99 --- /dev/null +++ b/demos/vue/test/cypress/e2e/example36.cy.ts @@ -0,0 +1,193 @@ +describe('Example 36 - Excel Export Formula', () => { + const GRID_ROW_HEIGHT = 33; + const fullTitles = ['#', 'Name', 'Price', 'Quantity', 'Sub-Total', 'Taxable', 'Taxes', 'Total']; + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example36`); + cy.get('h2').should('contain', 'Example 36: Excel Export Formula'); + }); + + it('should have exact column titles on grid', () => { + cy.get('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + }); + + it('should check first 3 rows with calculated totals', () => { + // 1st row + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).contains('1'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).contains('Oranges'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).contains('$2.22'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).contains('4'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(4)`).contains('$8.88'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5)`).should('have.text', ''); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(6)`).should('have.text', ''); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(7)`).contains('$8.88'); + + // 2nd row + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).contains('2'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).contains('Apples'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2)`).contains('$1.55'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3)`).contains('3'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(4)`).contains('$4.65'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(5)`).should('have.text', ''); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(6)`).should('have.text', ''); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(7)`).contains('$4.65'); + + // 3rd row + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).contains('3'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).contains('Honeycomb Cereals'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(2)`).contains('$4.55'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(3)`).contains('2'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(4)`).contains('$9.10'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(4)`).should('have.css', 'color').and('eq', 'rgb(33, 80, 115)'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(5)`).find('.mdi-check'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(6)`).contains('$0.68'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(6)`).should('have.css', 'color').and('eq', 'rgb(198, 89, 17)'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(7)`).contains('$9.78'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(7)`).should('have.css', 'color').and('eq', 'rgb(0, 90, 158)'); + }); + + it('should change Price & Qty on first 3 rows and expect calculated values to be updated', () => { + // 1st row + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).contains('1'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).click(); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2) input`).clear().type('2.44{enter}'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).click(); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3) input`).clear().type('7{enter}'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(4)`).contains('$17.08'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(7)`).contains('$17.08'); + + // 2nd row + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).contains('2'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2)`).click(); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2) input`).clear().type('1.4{enter}'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3)`).click(); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3) input`).clear().type('3{enter}'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(4)`).contains('$4.20'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(7)`).contains('$4.20'); + + // 3rd row + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).contains('3'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(2)`).click(); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(2) input`).clear().type('4.23{enter}'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(3)`).click(); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(3) input`).clear().type('3{enter}'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(4)`).contains('$12.69'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(6)`).contains('$0.95'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(7)`).contains('$13.64'); + }); + + it('should be able to change Tax Rate and for first 3 rows only expect the 3rd one to be updated with different taxes and total', () => { + cy.get('[data-test="taxrate"]').clear().type('6.25'); + cy.get('[data-test="update-btn"]').click(); + + // 1st row + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).contains('1'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).contains('$2.44'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).contains('7'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(4)`).contains('$17.08'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(7)`).contains('$17.08'); + + // 2nd row + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).contains('2'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2)`).contains('$1.40'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3)`).contains('3'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(4)`).contains('$4.20'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(7)`).contains('$4.20'); + + // 3rd row + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).contains('3'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(2)`).contains('$4.23'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(3)`).contains('3'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(4)`).contains('$12.69'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(6)`).contains('$0.79'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(7)`).contains('$13.48'); + }); + + it('should Group by Taxable and expect calculated totals', () => { + cy.get('[data-test="group-by-btn"]').click(); + + // last and 5th row of first Group + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell:nth(0)`).contains('11'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell:nth(1)`).contains('Milk'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell:nth(2)`).contains('$3.11'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell:nth(3)`).contains('3'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell:nth(4)`).contains('$9.33'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell:nth(5)`).find('.mdi-check'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell:nth(6)`).contains('$0.58'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell:nth(7)`).contains('$9.91'); + + // Taxable group total row + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 6}px;"] > .slick-cell:nth(2)`).contains('$15.71'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 6}px;"] > .slick-cell:nth(3)`).contains('25'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 6}px;"] > .slick-cell:nth(4)`).contains('$42.32'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 6}px;"] > .slick-cell:nth(6)`).contains('$2.65'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 6}px;"] > .slick-cell:nth(7)`).contains('$44.97'); + + // Non-Taxable group total row + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 14}px;"] > .slick-cell:nth(2)`).contains('$21.61'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 14}px;"] > .slick-cell:nth(3)`).contains('92'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 14}px;"] > .slick-cell:nth(4)`).contains('$60.29'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 14}px;"] > .slick-cell:nth(6)`).contains('$0.00'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 14}px;"] > .slick-cell:nth(7)`).contains('$60.29'); + }); + + it('should change Price & Qty of item 10,11 and expect calculated values to be updated in group total', () => { + // item 10 + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(0)`).contains('10'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(1)`).contains('Drinkable Yogurt'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(2)`).click(); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(2) input`).clear().type('1.96{enter}'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(3)`).click(); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(3) input`).clear().type('4{enter}'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(4)`).contains('$7.84'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(6)`).contains('$0.49'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(7)`).contains('$8.33'); + + // item 11 + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell:nth(0)`).contains('11'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell:nth(1)`).contains('Milk'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell:nth(2)`).click(); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell:nth(2) input`).clear().type('3.85{enter}'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell:nth(3)`).click(); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell:nth(3) input`).clear().type('2{enter}'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell:nth(4)`).contains('$7.70'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell:nth(6)`).contains('$0.48'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell:nth(7)`).contains('$8.18'); + + // group total row + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 6}px;"] > .slick-cell:nth(2)`).contains('$17.19'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 6}px;"] > .slick-cell:nth(3)`).contains('22'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 6}px;"] > .slick-cell:nth(4)`).contains('$41.21'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 6}px;"] > .slick-cell:nth(6)`).contains('$2.58'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 6}px;"] > .slick-cell:nth(7)`).contains('$43.79'); + + // Non-Taxable group total row + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 14}px;"] > .slick-cell:nth(2)`).contains('$21.61'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 14}px;"] > .slick-cell:nth(3)`).contains('92'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 14}px;"] > .slick-cell:nth(4)`).contains('$60.29'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 14}px;"] > .slick-cell:nth(6)`).contains('$0.00'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 14}px;"] > .slick-cell:nth(7)`).contains('$60.29'); + }); + + it('should change Tax Rate again and expect Taxes and Total to be recalculated', () => { + cy.get('[data-test="taxrate"]').clear().type('7'); + cy.get('[data-test="update-btn"]').click(); + + // item 10 + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(6)`).contains('$0.55'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(7)`).contains('$8.39'); + + // item 11 + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell:nth(6)`).contains('$0.54'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell:nth(7)`).contains('$8.24'); + + // group total row + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 6}px;"] > .slick-cell:nth(2)`).contains('$17.19'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 6}px;"] > .slick-cell:nth(3)`).contains('22'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 6}px;"] > .slick-cell:nth(4)`).contains('$41.21'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 6}px;"] > .slick-cell:nth(6)`).contains('$2.88'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 6}px;"] > .slick-cell:nth(7)`).contains('$44.09'); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example37.cy.ts b/demos/vue/test/cypress/e2e/example37.cy.ts new file mode 100644 index 000000000..57b833db4 --- /dev/null +++ b/demos/vue/test/cypress/e2e/example37.cy.ts @@ -0,0 +1,57 @@ +describe('Example 37 - Footer Totals Row', () => { + const fullTitles = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J']; + const GRID_ROW_HEIGHT = 33; + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example37`); + cy.get('h2').should('contain', 'Example 37: Footer Totals Row'); + }); + + it('should have exact Column Header Titles in the grid', () => { + cy.get('#grid37') + .find('.slick-header-columns:nth(0)') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + }); + + it('should have a total sum displayed in the footer for each column', () => { + for (let i = 0; i < 10; i++) { + cy.get(`.slick-footerrow-columns .slick-footerrow-column:nth(${i})`) + .should($span => { + const totalStr = $span.text(); + const totalVal = Number(totalStr.replace('Sum: ', '')); + + expect(totalStr).to.contain('Sum:'); + expect(totalVal).to.gte(400); + }); + + } + }); + + it('should be able to increase cell value by a number of 5 and expect column sum to be increased by 5 as well', () => { + let cellVal = 0; + let totalVal = 0; + const increasingVal = 50; + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`) + .should($span => { + cellVal = Number($span.text()); + expect(cellVal).to.gte(0); + }); + cy.get('.slick-footerrow-columns .slick-footerrow-column:nth(0)') + .should($span => { + totalVal = parseInt($span.text().replace('Sum: ', '')); + expect(totalVal).to.gte(400); + }); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).click(); + cy.get('.editor-0').type(`${increasingVal}{enter}`); + cy.wait(1); + + cy.get('.slick-footerrow-columns .slick-footerrow-column:nth(0)') + .should($span => { + const newTotalVal = parseInt($span.text().replace('Sum: ', '')); + expect(newTotalVal).to.eq(totalVal - cellVal + increasingVal); + }); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example38.cy.ts b/demos/vue/test/cypress/e2e/example38.cy.ts new file mode 100644 index 000000000..ca789fb19 --- /dev/null +++ b/demos/vue/test/cypress/e2e/example38.cy.ts @@ -0,0 +1,206 @@ +describe('Example 38 - Infinite Scroll with OData', () => { + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example38`); + cy.get('h2').should('contain', 'Example 38: OData (v4) Backend Service with Infinite Scroll'); + }); + + describe('when "enableCount" is set', () => { + it('should have default OData query', () => { + cy.get('[data-test=alert-odata-query]').should('exist'); + cy.get('[data-test=alert-odata-query]').should('contain', 'OData Query'); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$count=true&$top=30`); + }); + }); + + it('should scroll to bottom of the grid and expect next batch of 30 items appended to current dataset for a total of 60 items', () => { + cy.get('[data-test="itemCount"]') + .should('have.text', '30'); + + cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left') + .scrollTo('bottom'); + + cy.get('[data-test="itemCount"]') + .should('have.text', '60'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$count=true&$top=30&$skip=30`); + }); + }); + + it('should scroll to bottom of the grid and expect next batch of 30 items appended to current dataset for a new total of 90 items', () => { + cy.get('[data-test="itemCount"]') + .should('have.text', '60'); + + cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left') + .scrollTo('bottom'); + + cy.get('[data-test="itemCount"]') + .should('have.text', '90'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$count=true&$top=30&$skip=60`); + }); + }); + + it('should do one last scroll to reach the end of the data and have a full total of 100 items', () => { + cy.get('[data-test="itemCount"]') + .should('have.text', '90'); + + cy.get('[data-test="data-loaded-tag"]') + .should('be.hidden'); + + cy.get('[data-test="data-loaded-tag"]') + .should('not.have.class', 'fully-loaded'); + + cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left') + .scrollTo('bottom'); + + cy.get('[data-test="itemCount"]') + .should('have.text', '100'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$count=true&$top=30&$skip=90`); + }); + + cy.get('[data-test="data-loaded-tag"]') + .should('be.visible'); + + cy.get('[data-test="data-loaded-tag"]') + .should('have.class', 'fully-loaded'); + }); + + it('should sort by Name column and expect dataset to restart at index zero and have a total of 30 items', () => { + cy.get('[data-test="data-loaded-tag"]') + .should('have.class', 'fully-loaded'); + + cy.get('[data-id="name"]') + .click(); + + cy.get('[data-test="itemCount"]') + .should('have.text', '30'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$count=true&$top=30&$orderby=Name asc`); + }); + + cy.get('[data-test="data-loaded-tag"]') + .should('not.have.class', 'fully-loaded'); + }); + + it('should scroll to bottom again and expect next batch of 30 items appended to current dataset for a total of 60 items', () => { + cy.get('[data-test="itemCount"]') + .should('have.text', '30'); + + cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left') + .scrollTo('bottom'); + + cy.get('[data-test="itemCount"]') + .should('have.text', '60'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$count=true&$top=30&$skip=30&$orderby=Name asc`); + }); + + cy.get('[data-test="data-loaded-tag"]') + .should('not.have.class', 'fully-loaded'); + }); + + it('should change Gender filter to "female" and expect dataset to restart at index zero and have a total of 30 items', () => { + cy.get('.ms-filter.filter-gender:visible').click(); + + cy.get('[data-name="filter-gender"].ms-drop') + .find('li:visible:nth(2)') + .contains('female') + .click(); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$count=true&$top=30&$orderby=Name asc&$filter=(Gender eq 'female')`); + }); + + cy.get('[data-test="data-loaded-tag"]') + .should('not.have.class', 'fully-loaded'); + }); + + it('should scroll to bottom again and expect next batch to be only 20 females appended to current dataset for a total of 50 items found in DB', () => { + cy.get('[data-test="itemCount"]') + .should('have.text', '30'); + + cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left') + .scrollTo('bottom'); + + cy.get('[data-test="itemCount"]') + .should('have.text', '50'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$count=true&$top=30&$skip=30&$orderby=Name asc&$filter=(Gender eq 'female')`); + }); + }); + + it('should "Group by Gender" and expect 30 items grouped', () => { + cy.get('[data-test="clear-filters-sorting"]').click(); + cy.get('[data-test="group-by-gender"]').click(); + + cy.get('[data-test="itemCount"]') + .should('have.text', '30'); + + cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left') + .scrollTo('top'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$count=true&$top=30`); + }); + + cy.get(`[style="top: 0px;"] > .slick-cell:nth(0) .slick-group-toggle.expanded`).should('have.length', 1); + cy.get(`[style="top: 0px;"] > .slick-cell:nth(0) .slick-group-title`).contains(/Gender: [female|male]/); + }); + + it('should scroll to the bottom "Group by Gender" and expect 30 more items for a total of 60 items grouped', () => { + cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left') + .scrollTo('bottom'); + + cy.get('[data-test="itemCount"]') + .should('have.text', '60'); + + cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left') + .scrollTo('top'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$count=true&$top=30&$skip=30`); + }); + + cy.get(`[style="top: 0px;"] > .slick-cell:nth(0) .slick-group-toggle.expanded`).should('have.length', 1); + cy.get(`[style="top: 0px;"] > .slick-cell:nth(0) .slick-group-title`).contains(/Gender: [female|male]/); + }); + + it('should sort by Name column again and expect dataset to restart at index zero and have a total of 30 items still having Group Gender', () => { + cy.get('[data-id="name"]') + .click(); + + cy.get('[data-test="itemCount"]') + .should('have.text', '30'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$count=true&$top=30&$orderby=Name asc`); + }); + + cy.get(`[style="top: 0px;"] > .slick-cell:nth(0) .slick-group-toggle.expanded`).should('have.length', 1); + cy.get(`[style="top: 0px;"] > .slick-cell:nth(0) .slick-group-title`).contains(/Gender: [female|male]/); + }); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example39.cy.ts b/demos/vue/test/cypress/e2e/example39.cy.ts new file mode 100644 index 000000000..1cd6f3e24 --- /dev/null +++ b/demos/vue/test/cypress/e2e/example39.cy.ts @@ -0,0 +1,172 @@ +import { removeWhitespaces } from '../plugins/utilities'; + +describe('Example 39 - Infinite Scroll with GraphQL', () => { + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example39`); + cy.get('h2').should('contain', 'Example 39: GraphQL Backend Service with Infinite Scroll'); + }); + + it('should use fake smaller server wait delay for faster E2E tests', () => { + cy.get('[data-test="server-delay"]') + .clear() + .type('20'); + }); + + it('should have default GraphQL query', () => { + cy.get('[data-test=alert-graphql-query]').should('exist'); + cy.get('[data-test=alert-graphql-query]').should('contain', 'GraphQL Query'); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + const text = removeWhitespaces($span.text()); // remove all white spaces + expect(text).to.eq(removeWhitespaces(`query { users (first:30,offset:0,locale:"en",userId:123) { totalCount, nodes { id,name,gender,company } } }`)); + }); + }); + + it('should scroll to bottom of the grid and expect next batch of 30 items appended to current dataset for a total of 60 items', () => { + cy.get('[data-test="itemCount"]') + .should('have.text', '30'); + + cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left') + .scrollTo('bottom'); + + cy.get('[data-test="itemCount"]') + .should('have.text', '60'); + + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + const text = removeWhitespaces($span.text()); // remove all white spaces + expect(text).to.eq(removeWhitespaces(`query { users (first:30,offset:30,locale:"en",userId:123) { totalCount, nodes { id,name,gender,company } } }`)); + }); + }); + + it('should scroll to bottom of the grid and expect next batch of 30 items appended to current dataset for a new total of 90 items', () => { + cy.get('[data-test="itemCount"]') + .should('have.text', '60'); + + cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left') + .scrollTo('bottom'); + + cy.get('[data-test="itemCount"]') + .should('have.text', '90'); + + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + const text = removeWhitespaces($span.text()); // remove all white spaces + expect(text).to.eq(removeWhitespaces(`query { users (first:30,offset:60,locale:"en",userId:123) { totalCount, nodes { id,name,gender,company } } }`)); + }); + }); + + it('should do one last scroll to reach the end of the data and have a full total of 100 items', () => { + cy.get('[data-test="itemCount"]') + .should('have.text', '90'); + + cy.get('[data-test="data-loaded-tag"]') + .should('be.hidden'); + + cy.get('[data-test="data-loaded-tag"]') + .should('not.have.class', 'fully-loaded'); + + cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left') + .scrollTo('bottom'); + + cy.get('[data-test="itemCount"]') + .should('have.text', '100'); + + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + const text = removeWhitespaces($span.text()); // remove all white spaces + expect(text).to.eq(removeWhitespaces(`query { users (first:30,offset:90,locale:"en",userId:123) { totalCount, nodes { id,name,gender,company } } }`)); + }); + + cy.get('[data-test="data-loaded-tag"]') + .should('be.visible'); + + cy.get('[data-test="data-loaded-tag"]') + .should('have.class', 'fully-loaded'); + }); + + it('should sort by Name column and expect dataset to restart at index zero and have a total of 30 items', () => { + cy.get('[data-test="data-loaded-tag"]') + .should('have.class', 'fully-loaded'); + + cy.get('[data-id="name"]') + .click(); + + cy.get('[data-test="itemCount"]') + .should('have.text', '30'); + + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + const text = removeWhitespaces($span.text()); // remove all white spaces + expect(text).to.eq(removeWhitespaces(`query { users (first:30,offset:0,orderBy:[{field:name,direction:ASC}],locale:"en",userId:123) { + totalCount, nodes { id,name,gender,company } } }`)); + }); + + cy.get('[data-test="data-loaded-tag"]') + .should('not.have.class', 'fully-loaded'); + }); + + it('should scroll to bottom again and expect next batch of 30 items appended to current dataset for a total of 60 items', () => { + cy.get('[data-test="itemCount"]') + .should('have.text', '30'); + + cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left') + .scrollTo('bottom'); + + cy.get('[data-test="itemCount"]') + .should('have.text', '60'); + + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + const text = removeWhitespaces($span.text()); // remove all white spaces + expect(text).to.eq(removeWhitespaces(`query { users (first:30,offset:30,orderBy:[{field:name,direction:ASC}],locale:"en",userId:123) { + totalCount, nodes { id,name,gender,company } } }`)); + }); + + cy.get('[data-test="data-loaded-tag"]') + .should('not.have.class', 'fully-loaded'); + }); + + it('should change Gender filter to "female" and expect dataset to restart at index zero and have a total of 30 items', () => { + cy.get('.ms-filter.filter-gender:visible').click(); + + cy.get('[data-name="filter-gender"].ms-drop') + .find('li:visible:nth(2)') + .contains('Female') + .click(); + + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + const text = removeWhitespaces($span.text()); // remove all white spaces + expect(text).to.eq(removeWhitespaces(`query { users (first:30,offset:0, + orderBy:[{field:name,direction:ASC}], + filterBy:[{field:gender,operator:EQ,value:"female"}],locale:"en",userId:123) { totalCount, nodes { id,name,gender,company } } }`)); + }); + + cy.get('[data-test="data-loaded-tag"]') + .should('not.have.class', 'fully-loaded'); + }); + + it('should scroll to bottom again and expect next batch to be only 20 females appended to current dataset for a total of 50 items found in DB', () => { + cy.get('[data-test="itemCount"]') + .should('have.text', '30'); + + cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left') + .scrollTo('bottom'); + + cy.get('[data-test="itemCount"]') + .should('have.text', '50'); + + cy.get('[data-test=graphql-query-result]') + .should(($span) => { + const text = removeWhitespaces($span.text()); // remove all white spaces + expect(text).to.eq(removeWhitespaces(`query { users (first:30,offset:30, + orderBy:[{field:name,direction:ASC}], + filterBy:[{field:gender,operator:EQ,value:"female"}],locale:"en",userId:123) { totalCount, nodes { id,name,gender,company } } }`)); + }); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example40.cy.ts b/demos/vue/test/cypress/e2e/example40.cy.ts new file mode 100644 index 000000000..cbc166bc7 --- /dev/null +++ b/demos/vue/test/cypress/e2e/example40.cy.ts @@ -0,0 +1,110 @@ +describe('Example 40 - Infinite Scroll from JSON data', () => { + const GRID_ROW_HEIGHT = 33; + const titles = ['Title', 'Duration (days)', '% Complete', 'Start', 'Finish', 'Effort Driven']; + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example40`); + cy.get('h2').should('contain', 'Example 40: Infinite Scroll from JSON data'); + }); + + it('should have exact Column Titles in the grid', () => { + cy.get('#grid40') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(titles[index])); + }); + + it('should expect first row to include "Task 0" and other specific properties', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 0'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).contains(/[0-9]/); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).contains(/[0-9]/); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).contains(/20[0-9]{2}-[0-9]{2}-[0-9]{2}/); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(4)`).contains(/20[0-9]{2}-[0-9]{2}-[0-9]{2}/); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(5)`).find('.mdi.mdi-check').should('have.length', 1); + }); + + it('should scroll to bottom of the grid and expect next batch of 50 items appended to current dataset for a total of 100 items', () => { + cy.get('[data-test="totalItemCount"]') + .should('have.text', '50'); + + cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left') + .scrollTo('bottom'); + + cy.get('[data-test="totalItemCount"]') + .should('have.text', '100'); + }); + + it('should scroll to bottom of the grid again and expect 50 more items for a total of now 150 items', () => { + cy.get('[data-test="totalItemCount"]') + .should('have.text', '100'); + + cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left') + .scrollTo('bottom'); + + cy.get('[data-test="totalItemCount"]') + .should('have.text', '150'); + }); + + it('should disable onSort for data reset and expect same dataset length of 150 items after sorting by Title', () => { + cy.get('[data-test="onsort-off"]').click(); + + cy.get('[data-id="title"]') + .click(); + + cy.get('[data-test="totalItemCount"]') + .should('have.text', '150'); + + cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left') + .scrollTo('top'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 0'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 1'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 10'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 100'); + }); + + it('should enable onSort for data reset and expect dataset to be reset to 50 items after sorting by Title', () => { + cy.get('[data-test="onsort-on"]').click(); + + cy.get('[data-id="title"]') + .click(); + + cy.get('[data-test="totalItemCount"]') + .should('have.text', '50'); + + cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left') + .scrollTo('top'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 9'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 8'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 7'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 6'); + }); + + it('should "Group by Duration" and expect 50 items grouped', () => { + cy.get('[data-test="group-by-duration"]').click(); + + cy.get('[data-test="totalItemCount"]') + .should('have.text', '50'); + + cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left') + .scrollTo('top'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-toggle.expanded`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-title`).contains(/Duration: [0-9]/); + }); + + it('should scroll to the bottom "Group by Duration" and expect 50 more items for a total of 100 items grouped', () => { + cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left') + .scrollTo('bottom'); + + cy.get('[data-test="totalItemCount"]') + .should('have.text', '100'); + + cy.get('.slick-viewport.slick-viewport-top.slick-viewport-left') + .scrollTo('top'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-toggle.expanded`).should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0) .slick-group-title`).contains(/Duration: [0-9]/); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example41.cy.ts b/demos/vue/test/cypress/e2e/example41.cy.ts new file mode 100644 index 000000000..b2dd557a0 --- /dev/null +++ b/demos/vue/test/cypress/e2e/example41.cy.ts @@ -0,0 +1,94 @@ +describe('Example 41 - Drag & Drop', () => { + const GRID_ROW_HEIGHT = 33; + const titles = ['', 'Name', 'Complete']; + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example41`); + cy.get('h2').should('contain', 'Example 41: Drag & Drop'); + }); + + it('should have exact Column Titles in the grid', () => { + cy.get('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(titles[index])); + }); + + it('should expect first row to include "Task 0" and other specific properties', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Make a list'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).find('.mdi.mdi-check').should('have.length', 1); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Check it twice'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', `Find out who's naughty`); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1)`).should('contain', `Find out who's nice`); + }); + + it('should drag 2nd row to 3rd position', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.cell-reorder`).as('moveIconTask1'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell.cell-reorder`).as('moveIconTask2'); + + cy.get('@moveIconTask2').should('have.length', 1); + + cy.get('@moveIconTask2') + .trigger('mousedown', { which: 1, force: true }) + .trigger('mousemove', 'bottomRight'); + + cy.get('@moveIconTask1') + .trigger('mousemove', 'bottomRight') + .trigger('mouseup', 'bottomRight', { which: 1, force: true }); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Make a list'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', `Find out who's naughty`); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Check it twice'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1)`).should('contain', `Find out who's nice`); + }); + + it('should drag a single row "Check it twice" to recycle bin', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`) + .contains(`Find out who's naughty`) + .trigger('mousedown', { which: 1, force: true }) + .trigger('mousemove', 'bottomRight'); + + cy.get('.recycle-bin.drag-dropzone').should('have.length', 1); + cy.get('.drag-message') + .contains('Drag to Recycle Bin to delete 1 selected row(s)'); + + cy.get('#dropzone').trigger('mousemove', 'center'); + cy.get('.recycle-bin.drag-hover').should('have.length', 1); + cy.get('#dropzone').trigger('mouseup', 'center', { which: 1, force: true }); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Make a list'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Check it twice'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', `Find out who's nice`); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Make a list'); + cy.get('.slick-row').should('have.length', 3); + cy.get('.recycle-bin.drag-dropzone').should('have.length', 0); + cy.get('.recycle-bin.drag-hover').should('have.length', 0); + }); + + it('should be able to drag 2 last rows to recycle bin', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`) + .contains(`Find out who's nice`) + .type('{ctrl}', { release: false }) + .click(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`) + .contains('Check it twice') + .click() + .trigger('mousedown', { which: 1, force: true }) + .trigger('mousemove', 'bottomRight'); + + cy.get('.recycle-bin.drag-dropzone').should('have.length', 1); + cy.get('.drag-message') + .contains('Drag to Recycle Bin to delete 2 selected row(s)'); + + cy.get('#dropzone').trigger('mousemove', 'center'); + + cy.get('.recycle-bin.drag-hover').should('have.length', 1); + cy.get('#dropzone').trigger('mouseup', 'center', { which: 1, force: true }); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Make a list'); + cy.get('.slick-row').should('have.length', 1); + cy.get('.recycle-bin.drag-dropzone').should('have.length', 0); + cy.get('.recycle-bin.drag-hover').should('have.length', 0); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example42.cy.ts b/demos/vue/test/cypress/e2e/example42.cy.ts new file mode 100644 index 000000000..6d09398c0 --- /dev/null +++ b/demos/vue/test/cypress/e2e/example42.cy.ts @@ -0,0 +1,80 @@ +describe('Example 42 - Custom Pagination', () => { + const GRID_ROW_HEIGHT = 40; + const titles = ['Title', 'Description', '% Complete', 'Start', 'Finish', 'Duration', 'Completed']; + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example42`); + cy.get('h2').should('contain', 'Example 42: Custom Pagination'); + }); + + it('should have exact Column Titles in the grid', () => { + cy.get('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(titles[index])); + }); + + it('should expect first row to be Task 0', () => { + cy.get('.seek-first').should('have.class', 'disabled'); + cy.get('.seek-prev').should('have.class', 'disabled'); + cy.get('.item-from').should('contain', 1); + cy.get('.item-to').should('contain', 50); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 0'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 1'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 2'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 3'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 4'); + }); + + it('should click on next page and expect top row to be Task 50', () => { + cy.get('.page-item.seek-next').click(); + + cy.get('.seek-first').should('not.have.class', 'disabled'); + cy.get('.seek-prev').should('not.have.class', 'disabled'); + cy.get('.item-from').should('contain', 51); + cy.get('.item-to').should('contain', 100); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 50'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 51'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 52'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 53'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 54'); + }); + + it('should click on goto last page and expect top row to be Task 50', () => { + cy.get('.page-item.seek-end').click(); + + cy.get('.seek-next').should('have.class', 'disabled'); + cy.get('.seek-end').should('have.class', 'disabled'); + cy.get('.item-from').should('contain', 4951); + cy.get('.item-to').should('contain', 5000); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 4950'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 4951'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 4952'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 4953'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 4954'); + }); + + it('should change page size and expect pagination to be updated', () => { + cy.get('[data-test="page-size-input"]').type('{backspace}{backspace}75'); + cy.get('.seek-first').should('have.class', 'disabled'); + cy.get('.seek-prev').should('have.class', 'disabled'); + cy.get('.item-from').should('contain', 1); + cy.get('.item-to').should('contain', 75); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 0'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 1'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 2'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 3'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 4'); + }); + + it('should be able to goto next page', () => { + cy.get('.page-item.seek-next').click(); + + cy.get('.item-from').should('contain', 76); + cy.get('.item-to').should('contain', 150); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 75'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 76'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 77'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 78'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell:nth(0)`).should('contain', 'Task 79'); + }); +}); diff --git a/demos/vue/test/cypress/e2e/home.cy.ts b/demos/vue/test/cypress/e2e/home.cy.ts new file mode 100644 index 000000000..fcff40e3a --- /dev/null +++ b/demos/vue/test/cypress/e2e/home.cy.ts @@ -0,0 +1,11 @@ +describe('Home Page', () => { + it('should display Home Page', () => { + cy.visit(`${Cypress.config('baseUrl')}/home`); + + cy.get('h2').should('contain', 'Slickgrid-Vue'); + + cy.get('h4').contains('Quick intro'); + + cy.get('h4').contains('Documentation'); + }); +}); diff --git a/demos/vue/test/cypress/fixtures/example.json b/demos/vue/test/cypress/fixtures/example.json new file mode 100644 index 000000000..da18d9352 --- /dev/null +++ b/demos/vue/test/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} \ No newline at end of file diff --git a/demos/vue/test/cypress/plugins/index.ts b/demos/vue/test/cypress/plugins/index.ts new file mode 100644 index 000000000..39d628b69 --- /dev/null +++ b/demos/vue/test/cypress/plugins/index.ts @@ -0,0 +1,18 @@ + +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +export default (_on: any, _config: any) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +}; diff --git a/demos/vue/test/cypress/plugins/utilities.ts b/demos/vue/test/cypress/plugins/utilities.ts new file mode 100644 index 000000000..9d2ef5e69 --- /dev/null +++ b/demos/vue/test/cypress/plugins/utilities.ts @@ -0,0 +1,26 @@ +export function changeTimezone(date: Date, tz: string) { + // suppose the date is 12:00 UTC + const invdate = new Date(date.toLocaleString('en-US', { + timeZone: tz + })); + + // then invdate will be 07:00 in Toronto + // and the diff is 5 hours + const diff = date.getTime() - invdate.getTime(); + + // so 12:00 in Toronto is 17:00 UTC + return new Date(date.getTime() + diff); +} + +export function removeExtraSpaces(text: string) { + return `${text}`.replace(/\s+/g, ' ').trim(); +} + +export function removeWhitespaces(text: string) { + return `${text}`.replace(/\s+/g, ''); +} + +export function zeroPadding(input: string | number) { + const number = parseInt(input as string, 10); + return number < 10 ? `0${number}` : number; +} diff --git a/demos/vue/test/cypress/support/commands.ts b/demos/vue/test/cypress/support/commands.ts new file mode 100644 index 000000000..c19248dac --- /dev/null +++ b/demos/vue/test/cypress/support/commands.ts @@ -0,0 +1,74 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add("login", (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +import '@4tw/cypress-drag-drop'; +import 'cypress-real-events'; + +import { convertPosition } from './common'; + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + interface Chainable { + // triggerHover: (elements: NodeListOf) => void; + convertPosition(viewport: string): Chainable | { x: string; y: string; }>; + getCell(row: number, col: number, viewport?: string, options?: { parentSelector?: string, rowHeight?: number; }): Chainable>; + getNthCell(row: number, nthCol: number, viewport?: string, options?: { parentSelector?: string, rowHeight?: number; }): Chainable>; + saveLocalStorage: () => void; + restoreLocalStorage: () => void; + } + } +} + +// convert position like 'topLeft' to the object { x: 'left|right', y: 'top|bottom' } +Cypress.Commands.add('convertPosition', (viewport = 'topLeft') => cy.wrap(convertPosition(viewport))); + +Cypress.Commands.add('getCell', (row, col, viewport = 'topLeft', { parentSelector = '', rowHeight = 35 } = {}) => { + const position = convertPosition(viewport); + const canvasSelectorX = position.x ? `.grid-canvas-${position.x}` : ''; + const canvasSelectorY = position.y ? `.grid-canvas-${position.y}` : ''; + + return cy.get(`${parentSelector} ${canvasSelectorX}${canvasSelectorY} [style="top: ${row * rowHeight}px;"] > .slick-cell.l${col}.r${col}`); +}); + +Cypress.Commands.add('getNthCell', (row, nthCol, viewport = 'topLeft', { parentSelector = '', rowHeight = 35 } = {}) => { + const position = convertPosition(viewport); + const canvasSelectorX = position.x ? `.grid-canvas-${position.x}` : ''; + const canvasSelectorY = position.y ? `.grid-canvas-${position.y}` : ''; + + return cy.get(`${parentSelector} ${canvasSelectorX}${canvasSelectorY} [style="top: ${row * rowHeight}px;"] > .slick-cell:nth(${nthCol})`); +}); +const LOCAL_STORAGE_MEMORY: any = {}; + +Cypress.Commands.add('saveLocalStorage', () => { + Object.keys(localStorage).forEach(key => { + LOCAL_STORAGE_MEMORY[key] = localStorage[key]; + }); +}); + +Cypress.Commands.add('restoreLocalStorage', () => { + Object.keys(LOCAL_STORAGE_MEMORY).forEach(key => { + localStorage.setItem(key, LOCAL_STORAGE_MEMORY[key]); + }); +}); diff --git a/demos/vue/test/cypress/support/common.ts b/demos/vue/test/cypress/support/common.ts new file mode 100644 index 000000000..143a7694b --- /dev/null +++ b/demos/vue/test/cypress/support/common.ts @@ -0,0 +1,47 @@ +/** + * @typedef {('left'|'right')} xAllowed + * @typedef {('top'|'bottom')} yAllowed + * + * Define allowed input position + * @typedef {(xAllowed|yAllowed|'topLeft'|'topRight'|'bottomLeft'|'bottomRight')} AllowedInputPosition + * + * Define position object + * @typedef {Object} Position + * @property {xAllowed} x - horizontal + * @property {yAllowed} y - vertical + */ + +/** + * Enum for position values + * @constant + * @enum {(xAllowed|yAllowed)} + */ +const POSITION = Object.freeze({ + TOP: 'top', + BOTTOM: 'bottom', + LEFT: 'left', + RIGHT: 'right' +}); + +/** + * Convert position string to object + * + * @param {AllowedInputPosition} position + * @returns {Position} + */ +export function convertPosition(position: string) { + let x = ''; + let y = ''; + const _position = position.toLowerCase(); + if (_position.includes(POSITION.LEFT)) { + x = POSITION.LEFT; + } else if (_position.includes(POSITION.RIGHT)) { + x = POSITION.RIGHT; + } + if (_position.includes(POSITION.TOP)) { + y = POSITION.TOP; + } else if (_position.includes(POSITION.BOTTOM)) { + y = POSITION.BOTTOM; + } + return { x, y }; +} diff --git a/demos/vue/test/cypress/support/drag.ts b/demos/vue/test/cypress/support/drag.ts new file mode 100644 index 000000000..ca28e66da --- /dev/null +++ b/demos/vue/test/cypress/support/drag.ts @@ -0,0 +1,82 @@ +import { convertPosition } from './common'; + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + interface Chainable { + // triggerHover: (elements: NodeListOf) => void; + dragOutside(viewport?: string, ms?: number, px?: number, options?: { parentSelector?: string, scrollbarDimension?: number; rowHeight?: number; }): Chainable; + dragStart(options?: { cellWidth?: number; cellHeight?: number; }): Chainable; + dragCell(addRow: number, addCell: number, options?: { cellWidth?: number; cellHeight?: number; }): Chainable; + dragEnd(gridSelector?: string): Chainable; + } + } +} +// @ts-ignore +Cypress.Commands.add('dragStart', { prevSubject: true }, (subject: HTMLElement, { cellWidth = 90, cellHeight = 35 } = {}) => { + return cy.wrap(subject) + .click({ force: true }) + .trigger('mousedown', { which: 1 } as any, { force: true }) + .trigger('mousemove', cellWidth / 3, cellHeight / 3); +}); + +// use a different command name than 'drag' so that it doesn't conflict with the '@4tw/cypress-drag-drop' lib +// @ts-ignore +Cypress.Commands.add('dragCell', { prevSubject: true }, (subject: HTMLElement, addRow: number, addCell: number, { cellWidth = 90, cellHeight = 35 } = {}) => { + return cy.wrap(subject).trigger('mousemove', cellWidth * (addCell + 0.5), cellHeight * (addRow + 0.5), { force: true }); +}); + +Cypress.Commands.add('dragOutside', (viewport = 'topLeft', ms = 0, px = 0, { parentSelector = 'div[class^="slickgrid_"]', scrollbarDimension = 17 } = {}) => { + const $parent = cy.$$(parentSelector); + const gridWidth = $parent.width() as number; + const gridHeight = $parent.height() as number; + let x = gridWidth / 2; + let y = gridHeight / 2; + const position = convertPosition(viewport); + if (position.x === 'left') { + x = -px; + } else if (position.x === 'right') { + x = gridWidth - scrollbarDimension + 3 + px; + } + if (position.y === 'top') { + y = -px; + } else if (position.y === 'bottom') { + y = gridHeight - scrollbarDimension + 3 + px; + } + + cy.get(parentSelector).trigger('mousemove', x, y, { force: true }); + if (ms) { + cy.wait(ms); + } + return; +}); + +Cypress.Commands.add('dragEnd', { prevSubject: 'optional' }, (_subject, gridSelector = 'div[class^="slickgrid_"]') => { + cy.get(gridSelector).trigger('mouseup', { force: true }); + return; +}); + +export function getScrollDistanceWhenDragOutsideGrid(selector: string, viewport: string, dragDirection: string, fromRow: number, fromCol: number, px = 140) { + return (cy as any).convertPosition(viewport).then((_viewportPosition: { x: number; y: number; }) => { + const viewportSelector = `${selector} .slick-viewport-${_viewportPosition.x}.slick-viewport-${_viewportPosition.y}`; + (cy as any).getNthCell(fromRow, fromCol, viewport, { parentSelector: selector }) + .dragStart(); + return cy.get(viewportSelector).then($viewport => { + const scrollTopBefore = $viewport.scrollTop(); + const scrollLeftBefore = $viewport.scrollLeft(); + cy.dragOutside(dragDirection, 300, px, { parentSelector: selector }); + return cy.get(viewportSelector).then($viewportAfter => { + cy.dragEnd(selector); + const scrollTopAfter = $viewportAfter.scrollTop(); + const scrollLeftAfter = $viewportAfter.scrollLeft(); + cy.get(viewportSelector).scrollTo(0, 0, { ensureScrollable: false }); + return cy.wrap({ + scrollTopBefore, + scrollLeftBefore, + scrollTopAfter, + scrollLeftAfter + }); + }); + }); + }); +} diff --git a/demos/vue/test/cypress/support/index.ts b/demos/vue/test/cypress/support/index.ts new file mode 100644 index 000000000..e4d70b3bc --- /dev/null +++ b/demos/vue/test/cypress/support/index.ts @@ -0,0 +1,25 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands'; + +// Alternatively you can use CommonJS syntax: +// require('./commands') + +// Cypress.Cookies.defaults({ +// preserve: 'serve-mode', +// set: 'cypress' +// }); diff --git a/demos/vue/test/cypress/tsconfig.json b/demos/vue/test/cypress/tsconfig.json new file mode 100644 index 000000000..171c56e50 --- /dev/null +++ b/demos/vue/test/cypress/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "es5", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "lib": ["es2021", "dom"], + "types": ["@4tw/cypress-drag-drop", "cypress", "node"] + }, + "include": ["**/*.ts"] +} diff --git a/demos/vue/test/tsconfig.json b/demos/vue/test/tsconfig.json new file mode 100644 index 000000000..b9b7b48f8 --- /dev/null +++ b/demos/vue/test/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "outDir": "../out-tsc/spec", + "baseUrl": "./", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "target": "es2018", + "module": "commonjs", + "lib": ["es2018"], + "types": ["cypress", "jest", "jest-extended", "node"], + "typeRoots": [ + "../node_modules/@types" + ] + }, + "include": ["**/*.cy.ts", "**/*.d.ts"] +} diff --git a/demos/vue/tsconfig.app.json b/demos/vue/tsconfig.app.json new file mode 100644 index 000000000..4052b06dd --- /dev/null +++ b/demos/vue/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2021", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2021", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "preserve", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/demos/vue/tsconfig.json b/demos/vue/tsconfig.json new file mode 100644 index 000000000..d32ff6820 --- /dev/null +++ b/demos/vue/tsconfig.json @@ -0,0 +1,4 @@ +{ + "files": [], + "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }] +} diff --git a/demos/vue/tsconfig.node.json b/demos/vue/tsconfig.node.json new file mode 100644 index 000000000..abcd7f0da --- /dev/null +++ b/demos/vue/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/demos/vue/vite.config.ts b/demos/vue/vite.config.ts new file mode 100644 index 000000000..0e551492f --- /dev/null +++ b/demos/vue/vite.config.ts @@ -0,0 +1,35 @@ +import dns from 'node:dns'; + +import vue from '@vitejs/plugin-vue'; +import { defineConfig } from 'vite'; + +dns.setDefaultResultOrder('verbatim'); + +// https://vitejs.dev/config/ +export default defineConfig({ + base: './', + build: { + chunkSizeWarningLimit: 3000, + emptyOutDir: true, + outDir: './dist', + }, + css: { + preprocessorOptions: { + scss: { + quietDeps: true, + }, + }, + }, + plugins: [vue()], + preview: { + port: 7000, + }, + server: { + port: 7000, + cors: true, + host: 'localhost', + hmr: { + clientPort: 7000, + }, + }, +}); diff --git a/frameworks/slickgrid-vue/index.html b/frameworks/slickgrid-vue/index.html new file mode 100644 index 000000000..506c54360 --- /dev/null +++ b/frameworks/slickgrid-vue/index.html @@ -0,0 +1,14 @@ + + + + + + + Slickgrid-Vue + + + +
+ + + diff --git a/frameworks/slickgrid-vue/package.json b/frameworks/slickgrid-vue/package.json new file mode 100644 index 000000000..885fe2cf1 --- /dev/null +++ b/frameworks/slickgrid-vue/package.json @@ -0,0 +1,68 @@ +{ + "name": "slickgrid-vue", + "version": "0.1.0", + "type": "module", + "description": "Slickgrid-Vue", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "./package.json": "./package.json" + }, + "files": [ + "dist", + "src" + ], + "keywords": [ + "vue", + "vue3", + "component", + "library" + ], + "publishConfig": { + "access": "public" + }, + "funding": { + "type": "ko_fi", + "url": "https://ko-fi.com/ghiscoding" + }, + "scripts": { + "clean": "rimraf dist", + "dev": "vite build --watch", + "dev:init": "vite build", + "build": "vue-tsc --p ./tsconfig.app.json && vite build --sourcemap", + "preview": "vite preview", + "type-check": "vue-tsc --build --force" + }, + "dependencies": { + "@formkit/tempo": "^0.1.2", + "@slickgrid-universal/common": "workspace:*", + "@slickgrid-universal/custom-footer-component": "workspace:*", + "@slickgrid-universal/empty-warning-component": "workspace:*", + "@slickgrid-universal/event-pub-sub": "workspace:*", + "@slickgrid-universal/pagination-component": "workspace:*", + "@slickgrid-universal/row-detail-view-plugin": "workspace:*", + "@slickgrid-universal/utils": "workspace:*", + "dequal": "^2.0.3", + "i18next": "^24.0.2", + "i18next-vue": "^5.0.0", + "sortablejs": "^1.15.6" + }, + "peerDependencies": { + "vue": ">=3.4.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "sass": "^1.81.0", + "typescript": "~5.6.2", + "vite": "^6.0.1", + "vite-plugin-dts": "^4.3.0", + "vue": "^3.5.13", + "vue-tsc": "^2.1.10" + } +} \ No newline at end of file diff --git a/frameworks/slickgrid-vue/public/vue.svg b/frameworks/slickgrid-vue/public/vue.svg new file mode 100644 index 000000000..770e9d333 --- /dev/null +++ b/frameworks/slickgrid-vue/public/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frameworks/slickgrid-vue/src/.npmignore b/frameworks/slickgrid-vue/src/.npmignore new file mode 100644 index 000000000..fc5d3a8eb --- /dev/null +++ b/frameworks/slickgrid-vue/src/.npmignore @@ -0,0 +1,2 @@ +**/__tests__/*.* +**/*.spec.ts diff --git a/frameworks/slickgrid-vue/src/assets/vue.svg b/frameworks/slickgrid-vue/src/assets/vue.svg new file mode 100644 index 000000000..770e9d333 --- /dev/null +++ b/frameworks/slickgrid-vue/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frameworks/slickgrid-vue/src/components/SlickgridVue.vue b/frameworks/slickgrid-vue/src/components/SlickgridVue.vue new file mode 100644 index 000000000..d365cc937 --- /dev/null +++ b/frameworks/slickgrid-vue/src/components/SlickgridVue.vue @@ -0,0 +1,1638 @@ + + + diff --git a/frameworks/slickgrid-vue/src/components/slickgridVueProps.interface.ts b/frameworks/slickgrid-vue/src/components/slickgridVueProps.interface.ts new file mode 100644 index 000000000..ef19ad0ef --- /dev/null +++ b/frameworks/slickgrid-vue/src/components/slickgridVueProps.interface.ts @@ -0,0 +1,150 @@ +import type { + DragRowMove, + ExtensionList, + OnActiveCellChangedEventArgs, + OnAddNewRowEventArgs, + OnAutosizeColumnsEventArgs, + OnBeforeAppendCellEventArgs, + OnBeforeCellEditorDestroyEventArgs, + OnBeforeColumnsResizeEventArgs, + OnBeforeEditCellEventArgs, + OnBeforeFooterRowCellDestroyEventArgs, + OnBeforeHeaderCellDestroyEventArgs, + OnBeforeHeaderRowCellDestroyEventArgs, + OnBeforeSetColumnsEventArgs, + OnCellChangeEventArgs, + OnCellCssStylesChangedEventArgs, + OnClickEventArgs, + OnColumnsDragEventArgs, + OnColumnsReorderedEventArgs, + OnColumnsResizeDblClickEventArgs, + OnColumnsResizedEventArgs, + OnCompositeEditorChangeEventArgs, + OnDblClickEventArgs, + OnFooterClickEventArgs, + OnFooterContextMenuEventArgs, + OnFooterRowCellRenderedEventArgs, + OnGroupCollapsedEventArgs, + OnGroupExpandedEventArgs, + OnHeaderCellRenderedEventArgs, + OnHeaderClickEventArgs, + OnHeaderContextMenuEventArgs, + OnHeaderMouseEventArgs, + OnHeaderRowCellRenderedEventArgs, + OnKeyDownEventArgs, + OnRenderedEventArgs, + OnRowCountChangedEventArgs, + OnRowsChangedEventArgs, + OnRowsOrCountChangedEventArgs, + OnScrollEventArgs, + OnSelectedRowsChangedEventArgs, + OnSetItemsCalledEventArgs, + OnSetOptionsEventArgs, + OnValidationErrorEventArgs, + PaginationChangedArgs, + PagingInfo, + SingleColumnSort, + SlickControlList, + SlickGrid, + SlickPluginList, +} from '@slickgrid-universal/common'; +import type { Slot } from 'vue'; + +import type { SlickgridVueInstance } from '../models/index.js'; + +export interface SlickgridVueProps { + header?: Slot; + footer?: Slot; + extensions?: ExtensionList; + gridId: string; + instances?: SlickgridVueInstance; + + // Custom Events list + // --------------------- + // NOTE: we need to add an extra "onOn" prefix to all events because of how VueJS handles events + // for example onOnClick can actually be used as "@onClick" event + + // Slick Grid events + onOnActiveCellChanged?: (e: CustomEvent<{ eventData: any; args: OnActiveCellChangedEventArgs; }>) => void; + onOnActiveCellPositionChanged?: (e: CustomEvent<{ eventData: any; args: { grid: SlickGrid; }; }>) => void; + onOnAddNewRow?: (e: CustomEvent<{ eventData: any; args: OnAddNewRowEventArgs; }>) => void; + onOnAutosizeColumns?: (e: CustomEvent<{ eventData: any; args: OnAutosizeColumnsEventArgs; }>) => void; + onOnBeforeAppendCell?: (e: CustomEvent<{ eventData: any; args: OnBeforeAppendCellEventArgs; }>) => void; + onOnBeforeSearchChange?: (e: CustomEvent<{ eventData: any; args: OnCellChangeEventArgs; }>) => void; + onOnBeforeCellEditorDestroy?: (e: CustomEvent<{ eventData: any; args: OnBeforeCellEditorDestroyEventArgs; }>) => void; + onOnBeforeColumnsResize?: (e: CustomEvent<{ eventData: any; args: OnBeforeColumnsResizeEventArgs; }>) => void; + onOnBeforeDestroy?: (e: CustomEvent<{ eventData: any; args: { grid: SlickGrid; }; }>) => void; + onOnBeforeEditCell?: (e: CustomEvent<{ eventData: any; args: OnBeforeEditCellEventArgs; }>) => void; + onOnBeforeHeaderCellDestroy?: (e: CustomEvent<{ eventData: any; args: OnBeforeHeaderCellDestroyEventArgs; }>) => void; + onOnBeforeHeaderRowCellDestroy?: (e: CustomEvent<{ eventData: any; args: OnBeforeHeaderRowCellDestroyEventArgs; }>) => void; + onOnBeforeFooterRowCellDestroy?: (e: CustomEvent<{ eventData: any; args: OnBeforeFooterRowCellDestroyEventArgs; }>) => void; + onOnBeforeSetColumns?: (e: CustomEvent<{ eventData: any; args: OnBeforeSetColumnsEventArgs; }>) => void; + onOnBeforeSort?: (e: CustomEvent<{ eventData: any; args: SingleColumnSort; }>) => void; + onOnCellChange?: (e: CustomEvent<{ eventData: any; args: OnCellChangeEventArgs; }>) => void; + onOnCellCssStylesChanged?: (e: CustomEvent<{ eventData: any; args: OnCellCssStylesChangedEventArgs; }>) => void; + onOnClick?: (e: CustomEvent<{ eventData: any; args: OnClickEventArgs; }>) => void; + onOnColumnsDrag?: (e: CustomEvent<{ eventData: any; args: OnColumnsDragEventArgs; }>) => void; + onOnColumnsReordered?: (e: CustomEvent<{ eventData: any; args: OnColumnsReorderedEventArgs; }>) => void; + onOnColumnsResized?: (e: CustomEvent<{ eventData: any; args: OnColumnsResizedEventArgs; }>) => void; + onOnColumnsResizeDblClick?: (e: CustomEvent<{ eventData: any; args: OnColumnsResizeDblClickEventArgs; }>) => void; + onOnCompositeEditorChange?: (e: CustomEvent<{ eventData: any; args: OnCompositeEditorChangeEventArgs; }>) => void; + onOnContextMenu?: (e: CustomEvent<{ eventData: any; args: { grid: SlickGrid; }; }>) => void; + onOnDrag?: (e: CustomEvent<{ eventData: any; args: DragRowMove; }>) => void; + onOnDragEnd?: (e: CustomEvent<{ eventData: any; args: DragRowMove; }>) => void; + onOnDragInit?: (e: CustomEvent<{ eventData: any; args: DragRowMove; }>) => void; + onOnDragStart?: (e: CustomEvent<{ eventData: any; args: DragRowMove; }>) => void; + onOnDblClick?: (e: CustomEvent<{ eventData: any; args: OnDblClickEventArgs; }>) => void; + onOnFooterContextMenu?: (e: CustomEvent<{ eventData: any; args: OnFooterContextMenuEventArgs; }>) => void; + onOnFooterRowCellRendered?: (e: CustomEvent<{ eventData: any; args: OnFooterRowCellRenderedEventArgs; }>) => void; + onOnHeaderCellRendered?: (e: CustomEvent<{ eventData: any; args: OnHeaderCellRenderedEventArgs; }>) => void; + onOnFooterClick?: (e: CustomEvent<{ eventData: any; args: OnFooterClickEventArgs; }>) => void; + onOnHeaderClick?: (e: CustomEvent<{ eventData: any; args: OnHeaderClickEventArgs; }>) => void; + onOnHeaderContextMenu?: (e: CustomEvent<{ eventData: any; args: OnHeaderContextMenuEventArgs; }>) => void; + onOnHeaderMouseEnter?: (e: CustomEvent<{ eventData: any; args: OnHeaderMouseEventArgs; }>) => void; + onOnHeaderMouseLeave?: (e: CustomEvent<{ eventData: any; args: OnHeaderMouseEventArgs; }>) => void; + onOnHeaderRowCellRendered?: (e: CustomEvent<{ eventData: any; args: OnHeaderRowCellRenderedEventArgs; }>) => void; + onOnHeaderRowMouseEnter?: (e: CustomEvent<{ eventData: any; args: OnHeaderMouseEventArgs; }>) => void; + onOnHeaderRowMouseLeave?: (e: CustomEvent<{ eventData: any; args: OnHeaderMouseEventArgs; }>) => void; + onOnKeyDown?: (e: CustomEvent<{ eventData: any; args: OnKeyDownEventArgs; }>) => void; + onOnMouseEnter?: (e: CustomEvent<{ eventData: any; args: { grid: SlickGrid; }; }>) => void; + onOnMouseLeave?: (e: CustomEvent<{ eventData: any; args: { grid: SlickGrid; }; }>) => void; + onOnValidationError?: (e: CustomEvent<{ eventData: any; args: OnValidationErrorEventArgs; }>) => void; + onOnViewportChanged?: (e: CustomEvent<{ eventData: any; args: { grid: SlickGrid; }; }>) => void; + onOnRendered?: (e: CustomEvent<{ eventData: any; args: OnRenderedEventArgs; }>) => void; + onOnSelectedRowsChanged?: (e: CustomEvent<{ eventData: any; args: OnSelectedRowsChangedEventArgs; }>) => void; + onOnSetOptions?: (e: CustomEvent<{ eventData: any; args: OnSetOptionsEventArgs; }>) => void; + onOnScroll?: (e: CustomEvent<{ eventData: any; args: OnScrollEventArgs; }>) => void; + onOnSort?: (e: CustomEvent<{ eventData: any; args: SingleColumnSort; }>) => void; + + // Slick DataView events + onOnBeforePagingInfoChanged?: (e: CustomEvent<{ eventData: any; args: PagingInfo; }>) => void; + onOnGroupExpanded?: (e: CustomEvent<{ eventData: any; args: OnGroupExpandedEventArgs; }>) => void; + onOnGroupCollapsed?: (e: CustomEvent<{ eventData: any; args: OnGroupCollapsedEventArgs; }>) => void; + onOnPagingInfoChanged?: (e: CustomEvent<{ eventData: any; args: PagingInfo; }>) => void; + onOnRowCountChanged?: (e: CustomEvent<{ eventData: any; args: OnRowCountChangedEventArgs; }>) => void; + onOnRowsChanged?: (e: CustomEvent<{ eventData: any; args: OnRowsChangedEventArgs; }>) => void; + onOnRowsOrCountChanged?: (e: CustomEvent<{ eventData: any; args: OnRowsOrCountChangedEventArgs; }>) => void; + onOnSetItemsCalled?: (e: CustomEvent<{ eventData: any; args: OnSetItemsCalledEventArgs; }>) => void; + + // Slickgrid-Vue events + onOnAfterExportToExcel?: (e: CustomEvent) => void; + onOnBeforePaginationChange?: (e: CustomEvent) => void; + onOnBeforeExportToExcel?: (e: CustomEvent) => void; + onOnBeforeFilterChange?: (e: CustomEvent) => void; + onOnBeforeFilterClear?: (e: CustomEvent) => void; + onOnBeforeSortChange?: (e: CustomEvent) => void; + onOnBeforeToggleTreeCollapse?: (e: CustomEvent) => void; + onOnFilterChanged?: (e: CustomEvent) => void; + onOnFilterCleared?: (e: CustomEvent) => void; + onOnItemDeleted?: (e: CustomEvent) => void; + onOnGridStateChanged?: (e: CustomEvent) => void; + onOnPaginationChanged?: (e: CustomEvent) => void; + onOnReactGridCreated?: (e: CustomEvent) => void; + onOnSelectedRowIdsChanged?: (e: CustomEvent) => void; + onOnSortChanged?: (e: CustomEvent) => void; + onOnToggleTreeCollapsed?: (e: CustomEvent) => void; + onOnTreeItemToggled?: (e: CustomEvent) => void; + onOnTreeFullToggleEnd?: (e: CustomEvent) => void; + onOnTreeFullToggleStart?: (e: CustomEvent) => void; + onVueGridCreated?: (e: CustomEvent) => void; +} diff --git a/frameworks/slickgrid-vue/src/constants.ts b/frameworks/slickgrid-vue/src/constants.ts new file mode 100644 index 000000000..138606ad9 --- /dev/null +++ b/frameworks/slickgrid-vue/src/constants.ts @@ -0,0 +1,95 @@ +import type { Locale } from '@slickgrid-universal/common'; + +export class Constants { + // English Locale texts when using only 1 Locale instead of I18N + static readonly locales: Locale = { + TEXT_ALL_SELECTED: 'All Selected', + TEXT_ALL_X_RECORDS_SELECTED: 'All {{x}} records selected', + TEXT_APPLY_MASS_UPDATE: 'Apply Mass Update', + TEXT_APPLY_TO_SELECTION: 'Update Selection', + TEXT_CANCEL: 'Cancel', + TEXT_CLEAR_ALL_FILTERS: 'Clear all Filters', + TEXT_CLEAR_ALL_GROUPING: 'Clear all Grouping', + TEXT_CLEAR_ALL_SORTING: 'Clear all Sorting', + TEXT_CLEAR_PINNING: 'Unfreeze Columns/Rows', + TEXT_CLONE: 'Clone', + TEXT_COLLAPSE_ALL_GROUPS: 'Collapse all Groups', + TEXT_CONTAINS: 'Contains', + TEXT_COLUMNS: 'Columns', + TEXT_COLUMN_RESIZE_BY_CONTENT: 'Resize by Content', + TEXT_COMMANDS: 'Commands', + TEXT_COPY: 'Copy', + TEXT_EQUALS: 'Equals', + TEXT_EQUAL_TO: 'Equal to', + TEXT_ENDS_WITH: 'Ends With', + TEXT_ERROR_EDITABLE_GRID_REQUIRED: 'Your grid must be editable in order to use the Composite Editor Modal.', + TEXT_ERROR_ENABLE_CELL_NAVIGATION_REQUIRED: + 'Composite Editor requires the flag "enableCellNavigation" to be set to True in your Grid Options.', + TEXT_ERROR_NO_CHANGES_DETECTED: 'Sorry we could not detect any changes.', + TEXT_ERROR_NO_EDITOR_FOUND: 'We could not find any Editor in your Column Definition.', + TEXT_ERROR_NO_RECORD_FOUND: 'No records selected for edit or clone operation.', + TEXT_ERROR_ROW_NOT_EDITABLE: 'Current row is not editable.', + TEXT_ERROR_ROW_SELECTION_REQUIRED: 'You must select some rows before trying to apply new value(s).', + TEXT_EXPAND_ALL_GROUPS: 'Expand all Groups', + TEXT_EXPORT_TO_CSV: 'Export in CSV format', + TEXT_EXPORT_TO_TEXT_FORMAT: 'Export in Text format (Tab delimited)', + TEXT_EXPORT_TO_EXCEL: 'Export to Excel', + TEXT_EXPORT_TO_TAB_DELIMITED: 'Export in Text format (Tab delimited)', + TEXT_FORCE_FIT_COLUMNS: 'Force fit columns', + TEXT_FREEZE_COLUMNS: 'Freeze Columns', + TEXT_GREATER_THAN: 'Greater than', + TEXT_GREATER_THAN_OR_EQUAL_TO: 'Greater than or equal to', + TEXT_GROUP_BY: 'Group By', + TEXT_HIDE_COLUMN: 'Hide Column', + TEXT_ITEMS: 'items', + TEXT_ITEMS_PER_PAGE: 'items per page', + TEXT_ITEMS_SELECTED: 'items selected', + TEXT_OF: 'of', + TEXT_OK: 'OK', + TEXT_LAST_UPDATE: 'Last Update', + TEXT_LESS_THAN: 'Less than', + TEXT_LESS_THAN_OR_EQUAL_TO: 'Less than or equal to', + TEXT_NO_ELEMENTS_FOUND: 'Aucun รฉlรฉment trouvรฉ', + TEXT_NOT_CONTAINS: 'Not contains', + TEXT_NOT_EQUAL_TO: 'Not equal to', + TEXT_PAGE: 'Page', + TEXT_REFRESH_DATASET: 'Refresh Dataset', + TEXT_REMOVE_FILTER: 'Remove Filter', + TEXT_REMOVE_SORT: 'Remove Sort', + TEXT_SAVE: 'Save', + TEXT_SELECT_ALL: 'Select All', + TEXT_SYNCHRONOUS_RESIZE: 'Synchronous resize', + TEXT_SORT_ASCENDING: 'Sort Ascending', + TEXT_SORT_DESCENDING: 'Sort Descending', + TEXT_STARTS_WITH: 'Starts With', + TEXT_TOGGLE_DARK_MODE: 'Toggle Dark Mode', + TEXT_TOGGLE_FILTER_ROW: 'Toggle Filter Row', + TEXT_TOGGLE_PRE_HEADER_ROW: 'Toggle Pre-Header Row', + TEXT_X_OF_Y_SELECTED: '# of % selected', + TEXT_X_OF_Y_MASS_SELECTED: '{{x}} of {{y}} selected', + }; + + static readonly VALIDATION_REQUIRED_FIELD = 'Field is required'; + static readonly VALIDATION_EDITOR_VALID_NUMBER = 'Please enter a valid number'; + static readonly VALIDATION_EDITOR_VALID_INTEGER = 'Please enter a valid integer number'; + static readonly VALIDATION_EDITOR_INTEGER_BETWEEN = 'Please enter a valid integer number between {{minValue}} and {{maxValue}}'; + static readonly VALIDATION_EDITOR_INTEGER_MAX = 'Please enter a valid integer number that is lower than {{maxValue}}'; + static readonly VALIDATION_EDITOR_INTEGER_MAX_INCLUSIVE = + 'Please enter a valid integer number that is lower than or equal to {{maxValue}}'; + static readonly VALIDATION_EDITOR_INTEGER_MIN = 'Please enter a valid integer number that is greater than {{minValue}}'; + static readonly VALIDATION_EDITOR_INTEGER_MIN_INCLUSIVE = + 'Please enter a valid integer number that is greater than or equal to {{minValue}}'; + static readonly VALIDATION_EDITOR_NUMBER_BETWEEN = 'Please enter a valid number between {{minValue}} and {{maxValue}}'; + static readonly VALIDATION_EDITOR_NUMBER_MAX = 'Please enter a valid number that is lower than {{maxValue}}'; + static readonly VALIDATION_EDITOR_NUMBER_MAX_INCLUSIVE = 'Please enter a valid number that is lower than or equal to {{maxValue}}'; + static readonly VALIDATION_EDITOR_NUMBER_MIN = 'Please enter a valid number that is greater than {{minValue}}'; + static readonly VALIDATION_EDITOR_NUMBER_MIN_INCLUSIVE = 'Please enter a valid number that is greater than or equal to {{minValue}}'; + static readonly VALIDATION_EDITOR_DECIMAL_BETWEEN = 'Please enter a valid number with a maximum of {{maxDecimal}} decimals'; + static readonly VALIDATION_EDITOR_TEXT_LENGTH_BETWEEN = + 'Please make sure your text length is between {{minLength}} and {{maxLength}} characters'; + static readonly VALIDATION_EDITOR_TEXT_MAX_LENGTH = 'Please make sure your text is less than {{maxLength}} characters'; + static readonly VALIDATION_EDITOR_TEXT_MAX_LENGTH_INCLUSIVE = + 'Please make sure your text is less than or equal to {{maxLength}} characters'; + static readonly VALIDATION_EDITOR_TEXT_MIN_LENGTH = 'Please make sure your text is more than {{minLength}} character(s)'; + static readonly VALIDATION_EDITOR_TEXT_MIN_LENGTH_INCLUSIVE = 'Please make sure your text is at least {{minLength}} character(s)'; +} diff --git a/frameworks/slickgrid-vue/src/extensions/slickRowDetailView.ts b/frameworks/slickgrid-vue/src/extensions/slickRowDetailView.ts new file mode 100644 index 000000000..06e1b9e94 --- /dev/null +++ b/frameworks/slickgrid-vue/src/extensions/slickRowDetailView.ts @@ -0,0 +1,416 @@ +import { + addToArrayWhenNotExists, + type EventSubscription, + type OnBeforeRowDetailToggleArgs, + type OnRowBackToViewportRangeArgs, + SlickEventData, + type SlickEventHandler, + type SlickGrid, + SlickRowSelectionModel, + unsubscribeAll, +} from '@slickgrid-universal/common'; +import { type EventPubSubService } from '@slickgrid-universal/event-pub-sub'; +import { SlickRowDetailView as UniversalSlickRowDetailView } from '@slickgrid-universal/row-detail-view-plugin'; +import { type App, type ComponentPublicInstance, createApp } from 'vue'; + +import type { GridOption, RowDetailView, ViewModelBindableInputData } from '../models/index.js'; + +const ROW_DETAIL_CONTAINER_PREFIX = 'container_'; +const PRELOAD_CONTAINER_PREFIX = 'container_loading'; + +type AppData = Record; +export interface CreatedView { + id: string | number; + dataContext: any; + app: App | null; + instance: ComponentPublicInstance | null; +} +// interface SRDV extends React.Component, UniversalSlickRowDetailView {}s + +export class SlickRowDetailView extends UniversalSlickRowDetailView { + protected _component?: any; + protected _preloadComponent?: any; + protected _views: CreatedView[] = []; + protected _subscriptions: EventSubscription[] = []; + protected _userProcessFn?: (item: any) => Promise; + protected gridContainerElement!: HTMLElement; + + constructor(private readonly eventPubSubService: EventPubSubService) { + super(eventPubSubService); + } + + get addonOptions() { + return this.getOptions(); + } + + protected get datasetIdPropName(): string { + return this.gridOptions.datasetIdPropertyName || 'id'; + } + + get eventHandler(): SlickEventHandler { + return this._eventHandler; + } + set eventHandler(eventHandler: SlickEventHandler) { + this._eventHandler = eventHandler; + } + + get gridOptions(): GridOption { + return (this._grid?.getOptions() || {}) as GridOption; + } + + get rowDetailViewOptions(): RowDetailView | undefined { + return this.gridOptions.rowDetailView; + } + + /** Dispose of the RowDetailView Extension */ + dispose() { + this.disposeAllViewComponents(); + unsubscribeAll(this._subscriptions); + super.dispose(); + } + + /** Dispose of all the opened Row Detail Panels Components */ + disposeAllViewComponents() { + if (Array.isArray(this._views)) { + this._views.forEach((view) => this.disposeViewComponent(view)); + } + this._views = []; + } + + /** Get the instance of the SlickGrid addon (control or plugin). */ + getAddonInstance(): SlickRowDetailView | null { + return this; + } + + init(grid: SlickGrid) { + this._grid = grid; + super.init(this._grid); + this.gridContainerElement = grid.getContainerNode(); + this.register(grid?.getSelectionModel() as SlickRowSelectionModel); + } + + /** + * Create the plugin before the Grid creation, else it will behave oddly. + * Mostly because the column definitions might change after the grid creation + */ + register(rowSelectionPlugin?: SlickRowSelectionModel) { + if (typeof this.gridOptions.rowDetailView?.process === 'function') { + // we need to keep the user "process" method and replace it with our own execution method + // we do this because when we get the item detail, we need to call "onAsyncResponse.notify" for the plugin to work + this._userProcessFn = this.gridOptions.rowDetailView.process as (item: any) => Promise; // keep user's process method + this.addonOptions.process = (item) => this.onProcessing(item); // replace process method & run our internal one + } else { + throw new Error('[Slickgrid-React] You need to provide a "process" function for the Row Detail Extension to work properly'); + } + + if (this._grid && this.gridOptions?.rowDetailView) { + // load the Preload & RowDetail Templates (could be straight HTML or React Components) + // when those are React Components, we need to create View Component & provide the html containers to the Plugin (preTemplate/postTemplate methods) + if (!this.gridOptions.rowDetailView.preTemplate) { + this._preloadComponent = this.gridOptions?.rowDetailView?.preloadComponent; + this.addonOptions.preTemplate = () => this._grid.sanitizeHtmlString(`
`) as string; + } + if (!this.gridOptions.rowDetailView.postTemplate) { + this._component = this.gridOptions?.rowDetailView?.viewComponent; + this.addonOptions.postTemplate = (itemDetail: any) => { + return this._grid.sanitizeHtmlString( + `
` + ) as string; + }; + } + + if (this._grid && this.gridOptions) { + // this also requires the Row Selection Model to be registered as well + if (!rowSelectionPlugin || !this._grid.getSelectionModel()) { + rowSelectionPlugin = new SlickRowSelectionModel(this.gridOptions.rowSelectionOptions || { selectActiveRow: true }); + this._grid.setSelectionModel(rowSelectionPlugin); + } + + // hook all events + if (this._grid && this.rowDetailViewOptions) { + if (this.rowDetailViewOptions.onExtensionRegistered) { + this.rowDetailViewOptions.onExtensionRegistered(this); + } + + if (this.onAsyncResponse) { + this._eventHandler.subscribe(this.onAsyncResponse, (event, args) => { + if (typeof this.rowDetailViewOptions?.onAsyncResponse === 'function') { + this.rowDetailViewOptions.onAsyncResponse(event, args); + } + }); + } + + if (this.onAsyncEndUpdate) { + this._eventHandler.subscribe(this.onAsyncEndUpdate, async (event, args) => { + // triggers after backend called "onAsyncResponse.notify()" + await this.renderViewModel(args?.item); + + if (typeof this.rowDetailViewOptions?.onAsyncEndUpdate === 'function') { + this.rowDetailViewOptions.onAsyncEndUpdate(event, args); + } + }); + } + + if (this.onAfterRowDetailToggle) { + this._eventHandler.subscribe(this.onAfterRowDetailToggle, async (event, args) => { + // display preload template & re-render all the other Detail Views after toggling + // the preload View will eventually go away once the data gets loaded after the "onAsyncEndUpdate" event + await this.renderPreloadView(args.item); + this.renderAllViewModels(); + + if (typeof this.rowDetailViewOptions?.onAfterRowDetailToggle === 'function') { + this.rowDetailViewOptions.onAfterRowDetailToggle(event, args); + } + }); + } + + if (this.onBeforeRowDetailToggle) { + this._eventHandler.subscribe(this.onBeforeRowDetailToggle, (event, args) => { + // before toggling row detail, we need to create View Component if it doesn't exist + this.handleOnBeforeRowDetailToggle(event, args); + + if (typeof this.rowDetailViewOptions?.onBeforeRowDetailToggle === 'function') { + return this.rowDetailViewOptions.onBeforeRowDetailToggle(event, args); + } + return true; + }); + } + + if (this.onRowBackToViewportRange) { + this._eventHandler.subscribe(this.onRowBackToViewportRange, async (event, args) => { + // when row is back to viewport range, we will re-render the View Component(s) + await this.handleOnRowBackToViewportRange(event, args); + + if (typeof this.rowDetailViewOptions?.onRowBackToViewportRange === 'function') { + this.rowDetailViewOptions.onRowBackToViewportRange(event, args); + } + }); + } + + if (this.onRowOutOfViewportRange) { + this._eventHandler.subscribe(this.onRowOutOfViewportRange, (event, args) => { + if (typeof this.rowDetailViewOptions?.onRowOutOfViewportRange === 'function') { + this.rowDetailViewOptions.onRowOutOfViewportRange(event, args); + } + }); + } + + // -- + // hook some events needed by the Plugin itself + + // we need to redraw the open detail views if we change column position (column reorder) + this.eventHandler.subscribe(this._grid.onColumnsReordered, this.redrawAllViewComponents.bind(this)); + + // on row selection changed, we also need to redraw + if (this.gridOptions.enableRowSelection || this.gridOptions.enableCheckboxSelector) { + this._eventHandler.subscribe(this._grid.onSelectedRowsChanged, this.redrawAllViewComponents.bind(this)); + } + + // on column sort/reorder, all row detail are collapsed so we can dispose of all the Views as well + this._eventHandler.subscribe(this._grid.onSort, this.disposeAllViewComponents.bind(this)); + + // on filter changed, we need to re-render all Views + this._subscriptions.push( + this.eventPubSubService?.subscribe( + ['onFilterChanged', 'onGridMenuColumnsChanged', 'onColumnPickerColumnsChanged'], + this.redrawAllViewComponents.bind(this) + ), + this.eventPubSubService?.subscribe(['onGridMenuClearAllFilters', 'onGridMenuClearAllSorting'], () => + window.setTimeout(() => this.redrawAllViewComponents()) + ) + ); + } + } + } + + return this; + } + + /** Redraw (re-render) all the expanded row detail View Components */ + async redrawAllViewComponents() { + await Promise.all(this._views.map(async (x) => this.redrawViewComponent(x))); + } + + /** Render all the expanded row detail View Components */ + async renderAllViewModels() { + await Promise.all(this._views.filter((x) => x?.dataContext).map(async (x) => this.renderViewModel(x.dataContext))); + } + + /** Redraw the necessary View Component */ + async redrawViewComponent(view: CreatedView) { + const containerElement = this.gridContainerElement.getElementsByClassName(`${ROW_DETAIL_CONTAINER_PREFIX}${view.id}`); + if (containerElement?.length >= 0) { + await this.renderViewModel(view.dataContext); + } + } + + /** Render (or re-render) the View Component (Row Detail) */ + async renderPreloadView(item: any) { + const containerElements = this.gridContainerElement.getElementsByClassName(`${PRELOAD_CONTAINER_PREFIX}`); + if (this._preloadComponent && containerElements?.length) { + const viewObj = this._views.find((obj) => obj.id === item[this.datasetIdPropName]); + const bindableData = { + model: item, + addon: this, + grid: this._grid, + dataView: this.dataView, + // @deprecated @use `parentRef` + parent: this.rowDetailViewOptions?.parent, + parentRef: this.rowDetailViewOptions?.parent, + } as AppData & ViewModelBindableInputData; + + const tmpDiv = document.createElement('div'); + const app = createApp(this._preloadComponent, bindableData); + const instance = app.mount(tmpDiv) as ComponentPublicInstance; + bindableData.parent = instance; + containerElements[containerElements.length - 1]!.appendChild(instance.$el); + + if (viewObj) { + viewObj.app = app; + viewObj.instance = instance; + } + } + } + + /** Render (or re-render) the View Component (Row Detail) */ + async renderViewModel(item: any) { + const containerElements = this.gridContainerElement.getElementsByClassName( + `${ROW_DETAIL_CONTAINER_PREFIX}${item[this.datasetIdPropName]}` + ); + if (this._component && containerElements?.length) { + const viewObj = this._views.find((obj) => obj.id === item[this.datasetIdPropName]); + const bindableData = { + model: item, + addon: this, + grid: this._grid, + dataView: this.dataView, + // @deprecated @use `parentRef` + parent: this.rowDetailViewOptions?.parent, + parentRef: this.rowDetailViewOptions?.parent, + } as AppData & ViewModelBindableInputData; + + // load our Row Detail React Component dynamically, typically we would want to use `root.render()` after the preload component (last argument below) + // BUT the root render doesn't seem to work and shows a blank element, so we'll use `createRoot()` every time even though it shows a console log in Dev + // that is the only way I got it working so let's use it anyway and console warnings are removed in production anyway + if (viewObj?.app) { + viewObj.app.unmount(); + } + const tmpDiv = document.createElement('div'); + const app = createApp(this._component, bindableData); + const instance = app.mount(tmpDiv) as ComponentPublicInstance; + bindableData.parent = app.component; + (containerElements[containerElements.length - 1] as HTMLElement).appendChild(instance.$el); + if (!viewObj) { + this.addViewInfoToViewsRef(item, app, instance); + } + } + } + + // -- + // protected functions + // ------------------ + + protected addViewInfoToViewsRef(item: any, app: App | null, instance: ComponentPublicInstance | null) { + const viewInfo: CreatedView = { + id: item[this.datasetIdPropName], + dataContext: item, + app, + instance, + }; + const idPropName = this.gridOptions.datasetIdPropertyName || 'id'; + addToArrayWhenNotExists(this._views, viewInfo, idPropName); + } + + protected disposeViewComponent(expandedView: CreatedView): CreatedView | void { + if (expandedView) { + if (expandedView?.instance) { + const container = this.gridContainerElement.getElementsByClassName(`${ROW_DETAIL_CONTAINER_PREFIX}${this._views[0].id}`); + if (container?.length) { + expandedView.app?.unmount(); + container[0].textContent = ''; + return expandedView; + } + } + } + } + + /** + * Just before the row get expanded or collapsed we will do the following + * First determine if the row is expanding or collapsing, + * if it's expanding we will add it to our View Components reference array if we don't already have it + * or if it's collapsing we will remove it from our View Components reference array + */ + protected handleOnBeforeRowDetailToggle(_e: SlickEventData, args: { grid: SlickGrid; item: any; }) { + // expanding + if (args?.item?.__collapsed) { + // expanding row detail + this.addViewInfoToViewsRef(args.item, null, null); + } else { + // collapsing, so dispose of the View + const foundViewIdx = this._views.findIndex((view: CreatedView) => view.id === args.item[this.datasetIdPropName]); + if (foundViewIdx >= 0 && this.disposeViewComponent(this._views[foundViewIdx])) { + this._views.splice(foundViewIdx, 1); + } + } + } + + /** When Row comes back to Viewport Range, we need to redraw the View */ + protected async handleOnRowBackToViewportRange( + _e: SlickEventData, + args: { + item: any; + rowId: string | number; + rowIndex: number; + expandedRows: (string | number)[]; + rowIdsOutOfViewport: (string | number)[]; + grid: SlickGrid; + } + ) { + if (args?.item) { + await this.redrawAllViewComponents(); + } + } + + /** + * notify the onAsyncResponse with the "args.item" (required property) + * the plugin will then use item to populate the row detail panel with the "postTemplate" + * @param item + */ + protected notifyTemplate(item: any) { + if (this.onAsyncResponse) { + this.onAsyncResponse.notify({ item, itemDetail: item }, new SlickEventData(), this); + } + } + + /** + * On Processing, we will notify the plugin with the new item detail once backend server call completes + * @param item + */ + protected async onProcessing(item: any) { + if (item && typeof this._userProcessFn === 'function') { + let awaitedItemDetail: any; + const userProcessFn = this._userProcessFn(item); + + // wait for the "userProcessFn", once resolved we will save it into the "collection" + const response: any | any[] = await userProcessFn; + + if (this.datasetIdPropName in response) { + awaitedItemDetail = response; // from Promise + } else if (response instanceof Response && typeof response['json'] === 'function') { + awaitedItemDetail = await response['json'](); // from Fetch + } else if (response && response['content']) { + awaitedItemDetail = response['content']; // from http-client + } + + if (!awaitedItemDetail || !(this.datasetIdPropName in awaitedItemDetail)) { + throw new Error( + '[Slickgrid-React] could not process the Row Detail, please make sure that your "process" callback ' + + '(a Promise or an HttpClient call returning an Observable) returns an item object that has an "${this.datasetIdPropName}" property' + ); + } + + // notify the plugin with the new item details + this.notifyTemplate(awaitedItemDetail || {}); + } + } +} diff --git a/frameworks/slickgrid-vue/src/global-grid-options.ts b/frameworks/slickgrid-vue/src/global-grid-options.ts new file mode 100644 index 000000000..30c928cd0 --- /dev/null +++ b/frameworks/slickgrid-vue/src/global-grid-options.ts @@ -0,0 +1,288 @@ +import { + type Column, + DelimiterType, + EventNamingStyle, + FileType, + Filters, + OperatorType, + type TreeDataOption, +} from '@slickgrid-universal/common'; + +import type { GridOption, RowDetailView } from './models/index.js'; + +/** + * Default Options that can be passed to the Aurelia-Slickgrid + */ +export const GlobalGridOptions: Partial = { + alwaysShowVerticalScroll: true, + autoEdit: false, + asyncEditorLoading: false, + autoFitColumnsOnFirstLoad: true, + autoResize: { + applyResizeToContainer: true, + calculateAvailableSizeBy: 'window', + bottomPadding: 20, + minHeight: 180, + minWidth: 300, + rightPadding: 0, + }, + cellHighlightCssClass: 'slick-cell-modified', + checkboxSelector: { + cssClass: 'slick-cell-checkboxsel', + width: 40, + }, + cellMenu: { + autoAdjustDrop: true, + autoAlignSide: true, + hideCloseButton: true, + hideCommandSection: false, + hideOptionSection: false, + }, + columnGroupSeparator: ' - ', + columnPicker: { + hideForceFitButton: false, + hideSyncResizeButton: true, + headerColumnValueExtractor: pickerHeaderColumnValueExtractor, + }, + compositeEditorOptions: { + labels: { + cancelButtonKey: 'CANCEL', + cloneButtonKey: 'CLONE', + resetEditorButtonTooltipKey: 'RESET_INPUT_VALUE', + resetFormButtonKey: 'RESET_FORM', + massSelectionButtonKey: 'APPLY_TO_SELECTION', + massSelectionStatusKey: 'X_OF_Y_MASS_SELECTED', + massUpdateButtonKey: 'APPLY_MASS_UPDATE', + massUpdateStatusKey: 'ALL_X_RECORDS_SELECTED', + saveButtonKey: 'SAVE', + }, + resetEditorButtonCssClass: 'mdi mdi-refresh mdi-15px', + resetFormButtonIconCssClass: 'mdi mdi-refresh mdi-16px mdi-flip-h', + }, + contextMenu: { + autoAdjustDrop: true, + autoAlignSide: true, + hideCloseButton: true, + hideClearAllGrouping: false, + hideCollapseAllGroups: false, + hideCommandSection: false, + hideCopyCellValueCommand: false, + hideExpandAllGroups: false, + hideExportCsvCommand: false, + hideExportExcelCommand: false, + hideExportTextDelimitedCommand: true, + hideMenuOnScroll: true, + hideOptionSection: false, + iconCollapseAllGroupsCommand: 'mdi mdi-arrow-collapse', + iconExpandAllGroupsCommand: 'mdi mdi-arrow-expand', + iconClearGroupingCommand: 'mdi mdi-close', + iconCopyCellValueCommand: 'mdi mdi-content-copy', + iconExportCsvCommand: 'mdi mdi-download', + iconExportExcelCommand: 'mdi mdi-file-excel-outline text-success', + iconExportTextDelimitedCommand: 'mdi mdi-download', + }, + customFooterOptions: { + dateFormat: 'YYYY-MM-DD, hh:mm a', + hideRowSelectionCount: false, + hideTotalItemCount: false, + hideLastUpdateTimestamp: true, + footerHeight: 25, + leftContainerClass: 'col-xs-12 col-sm-5', + rightContainerClass: 'col-xs-6 col-sm-7', + metricSeparator: '|', + metricTexts: { + items: 'items', + itemsKey: 'ITEMS', + of: 'of', + ofKey: 'OF', + itemsSelected: 'items selected', + itemsSelectedKey: 'ITEMS_SELECTED', + }, + }, + dataView: { + // when enabled, this will preserve the row selection even after filtering/sorting/grouping + syncGridSelection: { + preserveHidden: false, + preserveHiddenOnSelectionChange: true, + }, + syncGridSelectionWithBackendService: false, // but disable it when using backend services + }, + datasetIdPropertyName: 'id', + defaultFilter: Filters.input, + defaultBackendServiceFilterTypingDebounce: 500, + defaultColumnSortFieldId: 'id', + defaultFilterPlaceholder: '๐Ÿ”Ž๏ธŽ', // magnifying glass icon + defaultFilterRangeOperator: OperatorType.rangeInclusive, + editable: false, + editorTypingDebounce: 450, + filterTypingDebounce: 0, + enableEmptyDataWarningMessage: true, + enableFilterTrimWhiteSpace: false, // do we want to trim white spaces on all Filters? + emptyDataWarning: { + className: 'slick-empty-data-warning', + message: 'No data to display.', + messageKey: 'EMPTY_DATA_WARNING_MESSAGE', + hideFrozenLeftWarning: false, + hideFrozenRightWarning: false, + leftViewportMarginLeft: '40%', + rightViewportMarginLeft: '40%', + frozenLeftViewportMarginLeft: '0px', + frozenRightViewportMarginLeft: '40%', + }, + enableAutoResize: true, + enableAutoSizeColumns: true, + enableCellNavigation: false, + enableColumnPicker: true, + enableColumnReorder: true, + enableColumnResizeOnDoubleClick: true, + enableContextMenu: true, + enableExcelExport: false, + enableTextExport: false, + enableGridMenu: true, + enableHeaderMenu: true, + enableMouseHoverHighlightRow: true, + enableSorting: true, + enableTextSelectionOnCells: true, + eventNamingStyle: EventNamingStyle.camelCaseWithExtraOnPrefix, + explicitInitialization: true, + excelExportOptions: { + addGroupIndentation: true, + exportWithFormatter: false, + filename: 'export', + format: FileType.xlsx, + groupingColumnHeaderTitle: 'Group By', + groupCollapsedSymbol: 'โฎž', + groupExpandedSymbol: 'โฎŸ', + groupingAggregatorRowText: '', + sanitizeDataExport: false, + }, + textExportOptions: { + delimiter: DelimiterType.comma, + exportWithFormatter: false, + filename: 'export', + format: FileType.csv, + groupingColumnHeaderTitle: 'Group By', + groupingAggregatorRowText: '', + sanitizeDataExport: false, + useUtf8WithBom: true, + }, + forceFitColumns: false, + frozenHeaderWidthCalcDifferential: 1, + gridMenu: { + dropSide: 'left', + commandLabels: { + clearAllFiltersCommandKey: 'CLEAR_ALL_FILTERS', + clearAllSortingCommandKey: 'CLEAR_ALL_SORTING', + clearFrozenColumnsCommandKey: 'CLEAR_PINNING', + exportCsvCommandKey: 'EXPORT_TO_CSV', + exportExcelCommandKey: 'EXPORT_TO_EXCEL', + exportTextDelimitedCommandKey: 'EXPORT_TO_TAB_DELIMITED', + refreshDatasetCommandKey: 'REFRESH_DATASET', + toggleDarkModeCommandKey: 'TOGGLE_DARK_MODE', + toggleFilterCommandKey: 'TOGGLE_FILTER_ROW', + togglePreHeaderCommandKey: 'TOGGLE_PRE_HEADER_ROW', + }, + hideClearAllFiltersCommand: false, + hideClearAllSortingCommand: false, + hideClearFrozenColumnsCommand: true, // opt-in command + hideExportCsvCommand: false, + hideExportExcelCommand: false, + hideExportTextDelimitedCommand: true, + hideForceFitButton: false, + hideRefreshDatasetCommand: false, + hideSyncResizeButton: true, + hideToggleDarkModeCommand: true, + hideToggleFilterCommand: false, + hideTogglePreHeaderCommand: false, + iconCssClass: 'mdi mdi-menu', + iconClearAllFiltersCommand: 'mdi mdi-filter-remove-outline', + iconClearAllSortingCommand: 'mdi mdi-sort-variant-off', + iconClearFrozenColumnsCommand: 'mdi mdi-close', + iconExportCsvCommand: 'mdi mdi-download', + iconExportExcelCommand: 'mdi mdi-file-excel-outline', + iconExportTextDelimitedCommand: 'mdi mdi-download', + iconRefreshDatasetCommand: 'mdi mdi-sync', + iconToggleDarkModeCommand: 'mdi mdi-brightness-4 mdi mdi-brightness-4', + iconToggleFilterCommand: 'mdi mdi-flip-vertical', + iconTogglePreHeaderCommand: 'mdi mdi-flip-vertical', + menuWidth: 16, + resizeOnShowHeaderRow: true, + headerColumnValueExtractor: pickerHeaderColumnValueExtractor, + }, + headerMenu: { + autoAlign: true, + autoAlignOffset: 12, + minWidth: 140, + iconClearFilterCommand: 'mdi mdi-filter-remove-outline', + iconClearSortCommand: 'mdi mdi-sort-variant-off', + iconFreezeColumns: 'mdi mdi-pin-outline', + iconSortAscCommand: 'mdi mdi-sort-ascending', + iconSortDescCommand: 'mdi mdi-sort-descending', + iconColumnHideCommand: 'mdi mdi-close', + iconColumnResizeByContentCommand: 'mdi mdi-arrow-expand-horizontal', + hideColumnResizeByContentCommand: false, + hideColumnHideCommand: false, + hideClearFilterCommand: false, + hideClearSortCommand: false, + hideFreezeColumnsCommand: true, // opt-in command + hideSortCommands: false, + }, + multiColumnSort: true, + numberedMultiColumnSort: true, + tristateMultiColumnSort: false, + sortColNumberInSeparateSpan: true, + suppressActiveCellChangeOnEdit: false, + pagination: { + pageSizes: [10, 15, 20, 25, 30, 40, 50, 75, 100], + pageSize: 25, + totalItems: 0, + }, + rowDetailView: { + collapseAllOnSort: true, + cssClass: 'detail-view-toggle', + panelRows: 1, + keyPrefix: '__', + useRowClick: false, + useSimpleViewportCalc: true, + saveDetailViewOnScroll: false, + } as RowDetailView, + headerRowHeight: 35, + rowHeight: 35, + topPanelHeight: 30, + preHeaderPanelWidth: '100%', // mostly useful for Draggable Grouping dropzone to take full width + translationNamespaceSeparator: ':', + resetFilterSearchValueAfterOnBeforeCancellation: true, + resizeByContentOnlyOnFirstLoad: true, + resizeByContentOptions: { + alwaysRecalculateColumnWidth: false, + cellCharWidthInPx: 7.8, + cellPaddingWidthInPx: 14, + defaultRatioForStringType: 0.88, + formatterPaddingWidthInPx: 0, + maxItemToInspectCellContentWidth: 1000, + maxItemToInspectSingleColumnWidthByContent: 5000, + widthToRemoveFromExceededWidthReadjustment: 50, + }, + treeDataOptions: { + exportIndentMarginLeft: 5, + exportIndentationLeadingChar: 'อออออออออยท', + } as unknown as TreeDataOption, +}; + +/** + * Value Extractor for both ColumnPicker & GridMenu Picker + * when using Column Header Grouping, we'll prefix the column group title + * else we'll simply return the column name title + */ +function pickerHeaderColumnValueExtractor(column: Column, gridOptions?: GridOption) { + let colName = column?.columnPickerLabel ?? column?.name ?? ''; + if (colName instanceof HTMLElement || colName instanceof DocumentFragment) { + colName = colName.textContent || ''; + } + const headerGroup = column?.columnGroup || ''; + const columnGroupSeparator = gridOptions?.columnGroupSeparator ?? ' - '; + if (headerGroup) { + return headerGroup + columnGroupSeparator + colName; + } + return colName; +} diff --git a/frameworks/slickgrid-vue/src/index.ts b/frameworks/slickgrid-vue/src/index.ts new file mode 100644 index 000000000..ecc7c0b09 --- /dev/null +++ b/frameworks/slickgrid-vue/src/index.ts @@ -0,0 +1,23 @@ +import type { Column } from '@slickgrid-universal/common'; +import { Editors, Filters } from '@slickgrid-universal/common'; +export * from '@slickgrid-universal/common'; +import SlickgridVue from './components/SlickgridVue.vue'; +import { SlickRowDetailView } from './extensions/slickRowDetailView.js'; +import type { GridOption, RowDetailView, SlickgridVueInstance } from './models/index.js'; +import type { SlickgridConfig } from './slickgrid-config.js'; + +// expose all public classes +export type { SlickgridVueProps } from './components/slickgridVueProps.interface.js'; +export { disposeAllSubscriptions, TranslaterService } from './services/index.js'; + +export { + type Column, + Editors, + Filters, + type GridOption, + type RowDetailView, + SlickgridConfig, + SlickgridVue, + type SlickgridVueInstance, + SlickRowDetailView, +}; diff --git a/frameworks/slickgrid-vue/src/models/gridOption.interface.ts b/frameworks/slickgrid-vue/src/models/gridOption.interface.ts new file mode 100644 index 000000000..58cc98c01 --- /dev/null +++ b/frameworks/slickgrid-vue/src/models/gridOption.interface.ts @@ -0,0 +1,16 @@ +import type { BasePaginationModel, Column, GridOption as UniversalGridOption } from '@slickgrid-universal/common'; +import type * as i18next from 'i18next'; +import type { DefineComponent } from 'vue'; + +import type { RowDetailView } from './rowDetailView.interface.js'; + +export interface GridOption extends UniversalGridOption { + /** External Custom Pagination Component that can be provided by the user */ + customPaginationComponent?: DefineComponent; + + /** I18N translation service instance */ + i18n?: i18next.i18n; + + /** Row Detail View Plugin options & events (columnId, cssClass, toolTip, width) */ + rowDetailView?: RowDetailView; +} diff --git a/frameworks/slickgrid-vue/src/models/index.ts b/frameworks/slickgrid-vue/src/models/index.ts new file mode 100644 index 000000000..27528aa40 --- /dev/null +++ b/frameworks/slickgrid-vue/src/models/index.ts @@ -0,0 +1,5 @@ +export type * from './gridOption.interface.js'; +export type * from './rowDetailView.interface.js'; +export type * from './viewModelBindableData.interface.js'; +export type * from './viewModelBindableInputData.interface.js'; +export type * from './vueGridInstance.interface.js'; diff --git a/frameworks/slickgrid-vue/src/models/rowDetailView.interface.ts b/frameworks/slickgrid-vue/src/models/rowDetailView.interface.ts new file mode 100644 index 000000000..feecabbd8 --- /dev/null +++ b/frameworks/slickgrid-vue/src/models/rowDetailView.interface.ts @@ -0,0 +1,16 @@ +import type { RowDetailView as UniversalRowDetailView } from '@slickgrid-universal/common'; +import type { DefineComponent } from 'vue'; + +export interface RowDetailView extends UniversalRowDetailView { + /** + * Optionally pass your Parent Component reference to your Child Component (row detail component). + * note:: If anyone finds a better way of passing the parent to the row detail extension, please reach out and/or create a PR + */ + parent?: any; + + /** View Model of the preload template which shows after opening row detail & before row detail data shows up */ + preloadComponent?: DefineComponent; + + /** View Model template that will be loaded once the async function finishes */ + viewComponent?: DefineComponent; +} diff --git a/frameworks/slickgrid-vue/src/models/viewModelBindableData.interface.ts b/frameworks/slickgrid-vue/src/models/viewModelBindableData.interface.ts new file mode 100644 index 000000000..46c437738 --- /dev/null +++ b/frameworks/slickgrid-vue/src/models/viewModelBindableData.interface.ts @@ -0,0 +1,10 @@ +import type { SlickDataView, SlickGrid } from '@slickgrid-universal/common'; + +export interface ViewModelBindableData { + template: string; + model: any; + addon: any; + grid: SlickGrid; + dataView: SlickDataView; + parent?: any; +} diff --git a/frameworks/slickgrid-vue/src/models/viewModelBindableInputData.interface.ts b/frameworks/slickgrid-vue/src/models/viewModelBindableInputData.interface.ts new file mode 100644 index 000000000..ea165fad1 --- /dev/null +++ b/frameworks/slickgrid-vue/src/models/viewModelBindableInputData.interface.ts @@ -0,0 +1,9 @@ +import type { SlickDataView, SlickGrid } from '@slickgrid-universal/common'; + +export interface ViewModelBindableInputData { + model: any; + addon: any; + grid: SlickGrid; + dataView: SlickDataView; + parent?: any; +} diff --git a/frameworks/slickgrid-vue/src/models/vueGridInstance.interface.ts b/frameworks/slickgrid-vue/src/models/vueGridInstance.interface.ts new file mode 100644 index 000000000..3f8f6b969 --- /dev/null +++ b/frameworks/slickgrid-vue/src/models/vueGridInstance.interface.ts @@ -0,0 +1,77 @@ +import type { + BackendService, + BasePaginationComponent, + ExtensionService, + FilterService, + GridEventService, + GridService, + GridStateService, + HeaderGroupingService, + PaginationService, + ResizerService, + SlickDataView, + SlickGrid, + SortService, + TreeDataService, +} from '@slickgrid-universal/common'; +import type { EventPubSubService } from '@slickgrid-universal/event-pub-sub'; + +export interface SlickgridVueInstance { + element: HTMLDivElement; + + /** Slick DataView object */ + dataView: SlickDataView; + + /** Slick Grid object */ + slickGrid: SlickGrid; + + // -- + // Methods + /** Dispose of the grid and optionally empty the DOM element grid container as well */ + dispose: (emptyDomElementContainer?: boolean) => void; + + // -- + // Services + + /** Backend Service, when available */ + backendService?: BackendService; + + /** EventPubSub Service instance that is used internal by the lib and could be used externally to subscribe to Aurelia-Slickgrid events */ + eventPubSubService?: EventPubSubService; + + /** Extension (Plugins & Controls) Service */ + extensionService: ExtensionService; + + /** Filter Service */ + filterService: FilterService; + + /** Grid Service (grid extra functionalities) */ + gridService: GridService; + + /** Grid Events Service */ + gridEventService: GridEventService; + + /** Grid State Service */ + gridStateService: GridStateService; + + /** @deprecated @use `headerGroupingService` */ + groupingService: HeaderGroupingService; + + /** Grouping (and colspan) Service */ + headerGroupingService: HeaderGroupingService; + + /** Pagination Component */ + paginationComponent?: BasePaginationComponent; + + /** Pagination Service (allows you to programmatically go to first/last page, etc...) */ + paginationService?: PaginationService; + + /** Resizer Service (including auto-resize) */ + resizerService: ResizerService; + + /** Sort Service */ + sortService: SortService; + + /** Tree Data View Service */ + treeDataService: TreeDataService; +} diff --git a/frameworks/slickgrid-vue/src/services/container.service.ts b/frameworks/slickgrid-vue/src/services/container.service.ts new file mode 100644 index 000000000..514129b13 --- /dev/null +++ b/frameworks/slickgrid-vue/src/services/container.service.ts @@ -0,0 +1,13 @@ +import type { ContainerService as UniversalContainerService } from '@slickgrid-universal/common'; + +export class ContainerService implements UniversalContainerService { + private readonly container: { [key: string]: any } = {}; + + get(key: string): T | null { + return this.container[key]; + } + + registerInstance(key: string, instance: any) { + this.container[key] = instance; + } +} diff --git a/frameworks/slickgrid-vue/src/services/index.ts b/frameworks/slickgrid-vue/src/services/index.ts new file mode 100644 index 000000000..b80f5ee12 --- /dev/null +++ b/frameworks/slickgrid-vue/src/services/index.ts @@ -0,0 +1,3 @@ +export * from './container.service.js'; +export * from './translater.service.js'; +export * from './utilities.js'; diff --git a/frameworks/slickgrid-vue/src/services/translater.service.ts b/frameworks/slickgrid-vue/src/services/translater.service.ts new file mode 100644 index 000000000..cb9227ece --- /dev/null +++ b/frameworks/slickgrid-vue/src/services/translater.service.ts @@ -0,0 +1,41 @@ +import type { TranslaterService as UniversalTranslateService } from '@slickgrid-universal/common'; +import { type i18n } from 'i18next'; +import { useTranslation } from 'i18next-vue'; + +/** + * This is a Translate Service Wrapper for Slickgrid-Universal monorepo lib to work properly, + * it must implement Slickgrid-Universal TranslaterService interface to work properly + */ +export class TranslaterService implements UniversalTranslateService { + public i18n: i18n; + + constructor() { + this.i18n = useTranslation().i18next; + } + + /** + * Method to return the current language used by the App + * @return {string} current language + */ + getCurrentLanguage(): string { + return this.i18n.language; + } + + /** + * Method to set the language to use in the App and Translate Service + * @param {string} language + * @return {Promise} output + */ + async use(newLang: string): Promise { + return this.i18n.changeLanguage(newLang); + } + + /** + * Method which receives a translation key and returns the translated value assigned to that key + * @param {string} translation key + * @return {string} translated value + */ + translate(translationKey: string): string { + return this.i18n.t(translationKey); + } +} diff --git a/frameworks/slickgrid-vue/src/services/utilities.ts b/frameworks/slickgrid-vue/src/services/utilities.ts new file mode 100644 index 000000000..1cff89cff --- /dev/null +++ b/frameworks/slickgrid-vue/src/services/utilities.ts @@ -0,0 +1,18 @@ +import type { EventSubscription } from '@slickgrid-universal/common'; + +/** + * Loop through and dispose of all subscriptions when they are disposable + * @param subscriptions + * @return empty array + */ +export function disposeAllSubscriptions(subscriptions: Array): Array { + if (Array.isArray(subscriptions)) { + while (subscriptions.length > 0) { + const subscription = subscriptions.pop() as EventSubscription; + if ((subscription as EventSubscription)?.unsubscribe) { + (subscription as EventSubscription).unsubscribe!(); + } + } + } + return subscriptions; +} diff --git a/frameworks/slickgrid-vue/src/services/vueUtils.ts b/frameworks/slickgrid-vue/src/services/vueUtils.ts new file mode 100644 index 000000000..bf81d488c --- /dev/null +++ b/frameworks/slickgrid-vue/src/services/vueUtils.ts @@ -0,0 +1,26 @@ +import { createVNode, render, type VNode, type VNodeProps } from 'vue'; + +type Data = Record; +interface MountOptions { + props: (Data & VNodeProps) | null; + children: unknown; + element: HTMLElement | null; + app: any; +} + +export function mount(component: any, { props, children, element, app } = {} as Partial) { + let el = element; + + let vNode: VNode | null = createVNode(component, props, children); + if (app && app._context) vNode.appContext = app._context; + if (el) render(vNode, el); + else if (typeof document !== 'undefined') render(vNode, (el = document.createElement('div'))); + + const destroy = () => { + if (el) render(null, el); + el = null; + vNode = null; + }; + + return { vNode, destroy, el }; +} diff --git a/frameworks/slickgrid-vue/src/slickgrid-config.ts b/frameworks/slickgrid-vue/src/slickgrid-config.ts new file mode 100644 index 000000000..447d44bfa --- /dev/null +++ b/frameworks/slickgrid-vue/src/slickgrid-config.ts @@ -0,0 +1,10 @@ +import { GlobalGridOptions } from './global-grid-options.js'; +import type { GridOption } from './models/gridOption.interface.js'; + +export class SlickgridConfig { + options: Partial; + + constructor() { + this.options = GlobalGridOptions; + } +} diff --git a/frameworks/slickgrid-vue/src/vite-env.d.ts b/frameworks/slickgrid-vue/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/frameworks/slickgrid-vue/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/frameworks/slickgrid-vue/tsconfig.app.json b/frameworks/slickgrid-vue/tsconfig.app.json new file mode 100644 index 000000000..f04f3b932 --- /dev/null +++ b/frameworks/slickgrid-vue/tsconfig.app.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "preserve", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["src", "lib"] +} diff --git a/frameworks/slickgrid-vue/tsconfig.json b/frameworks/slickgrid-vue/tsconfig.json new file mode 100644 index 000000000..1ffef600d --- /dev/null +++ b/frameworks/slickgrid-vue/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/frameworks/slickgrid-vue/tsconfig.node.json b/frameworks/slickgrid-vue/tsconfig.node.json new file mode 100644 index 000000000..abcd7f0da --- /dev/null +++ b/frameworks/slickgrid-vue/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frameworks/slickgrid-vue/vite.config.ts b/frameworks/slickgrid-vue/vite.config.ts new file mode 100644 index 000000000..31ed55598 --- /dev/null +++ b/frameworks/slickgrid-vue/vite.config.ts @@ -0,0 +1,40 @@ +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import vue from '@vitejs/plugin-vue'; +import { defineConfig } from 'vite'; +import dts from 'vite-plugin-dts'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue(), dts({ rollupTypes: false, tsconfigPath: './tsconfig.app.json' })], + build: { + copyPublicDir: false, + lib: { + entry: resolve(__dirname, 'src/index.ts'), + formats: ['es'], + fileName: 'index', + }, + rollupOptions: { + // make sure to externalize deps that shouldn't be bundled + // into your library + external: [ + 'vue', + '@formkit/tempo', + '@slickgrid-universal/common', + '@slickgrid-universal/custom-footer-component', + '@slickgrid-universal/empty-warning-component', + '@slickgrid-universal/event-pub-sub', + '@slickgrid-universal/pagination-component', + '@slickgrid-universal/row-detail-view-plugin', + '@slickgrid-universal/utils', + 'dequal', + 'i18next', + 'i18next-vue', + 'sortablejs', + ], + }, + }, +}); diff --git a/package.json b/package.json index 0e7a11ea3..756424e8f 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,11 @@ "test:watch": "vitest --config ./test/vitest.config.mts", "test:ui": "vitest --ui --config ./test/vitest.config.mts", "prepare": "husky", - "commitlint": "commitlint --edit" + "commitlint": "commitlint --edit", + "vue:build": "pnpm -r --stream --filter=./demos/vue/** run build", + "vue:dev": "pnpm -r --stream --filter=./demos/vue/** run dev", + "vue:cypress": "pnpm -r --stream --filter=./demos/vue/** run cypress", + "vue:serve": "pnpm -r --stream --filter=./demos/vue/** run preview" }, "comments": { "new-version": "To create a new version with Lerna-Lite, simply run the following script (1) 'roll-new-release'.", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0df5b55b1..261c70f0e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -114,6 +114,109 @@ importers: specifier: ^3.6.20 version: 3.6.20 + demos/vue: + dependencies: + '@faker-js/faker': + specifier: ^9.2.0 + version: 9.2.0 + '@fnando/sparkline': + specifier: ^0.3.10 + version: 0.3.10 + '@formkit/tempo': + specifier: ^0.1.2 + version: 0.1.2 + '@popperjs/core': + specifier: ^2.11.8 + version: 2.11.8 + '@slickgrid-universal/common': + specifier: workspace:* + version: link:../../packages/common + '@slickgrid-universal/composite-editor-component': + specifier: workspace:* + version: link:../../packages/composite-editor-component + '@slickgrid-universal/custom-tooltip-plugin': + specifier: workspace:* + version: link:../../packages/custom-tooltip-plugin + '@slickgrid-universal/event-pub-sub': + specifier: ~5.10.2 + version: 5.10.2 + '@slickgrid-universal/excel-export': + specifier: workspace:* + version: link:../../packages/excel-export + '@slickgrid-universal/graphql': + specifier: workspace:* + version: link:../../packages/graphql + '@slickgrid-universal/odata': + specifier: workspace:* + version: link:../../packages/odata + '@slickgrid-universal/row-detail-view-plugin': + specifier: workspace:* + version: link:../../packages/row-detail-view-plugin + '@slickgrid-universal/rxjs-observable': + specifier: workspace:* + version: link:../../packages/rxjs-observable + '@slickgrid-universal/text-export': + specifier: workspace:* + version: link:../../packages/text-export + bootstrap: + specifier: ^5.3.3 + version: 5.3.3(@popperjs/core@2.11.8) + dompurify: + specifier: ^3.2.2 + version: 3.2.2 + i18next: + specifier: ^24.0.2 + version: 24.0.2(typescript@5.6.3) + i18next-http-backend: + specifier: ^3.0.1 + version: 3.0.1(encoding@0.1.13) + i18next-vue: + specifier: ^5.0.0 + version: 5.0.0(i18next@24.0.2(typescript@5.6.3))(vue@3.5.13(typescript@5.6.3)) + rxjs: + specifier: ^7.8.1 + version: 7.8.1 + slickgrid-vue: + specifier: workspace:* + version: link:../../frameworks/slickgrid-vue + vue: + specifier: ^3.5.13 + version: 3.5.13(typescript@5.6.3) + vue-router: + specifier: ^4.5.0 + version: 4.5.0(vue@3.5.13(typescript@5.6.3)) + devDependencies: + '@4tw/cypress-drag-drop': + specifier: ^2.2.5 + version: 2.2.5(cypress@13.16.0) + '@types/fnando__sparkline': + specifier: ^0.3.7 + version: 0.3.7 + '@vitejs/plugin-vue': + specifier: ^5.2.1 + version: 5.2.1(vite@6.0.1(@types/node@22.10.1)(jiti@1.21.6)(sass@1.81.0)(yaml@2.6.0))(vue@3.5.13(typescript@5.6.3)) + cypress: + specifier: ^13.16.0 + version: 13.16.0 + cypress-real-events: + specifier: ^1.13.0 + version: 1.13.0(cypress@13.16.0) + fetch-jsonp: + specifier: ^1.3.0 + version: 1.3.0 + sass: + specifier: ^1.81.0 + version: 1.81.0 + servor: + specifier: ^4.0.2 + version: 4.0.2 + typescript: + specifier: ~5.6.2 + version: 5.6.3 + vite: + specifier: ^6.0.1 + version: 6.0.1(@types/node@22.10.1)(jiti@1.21.6)(sass@1.81.0)(yaml@2.6.0) + examples/vite-demo-vanilla-bundle: dependencies: '@faker-js/faker': @@ -199,6 +302,67 @@ importers: specifier: ^6.0.0 version: 6.0.0(@types/node@22.7.9)(jiti@1.21.6)(sass@1.80.4)(yaml@2.6.0) + frameworks/slickgrid-vue: + dependencies: + '@formkit/tempo': + specifier: ^0.1.2 + version: 0.1.2 + '@slickgrid-universal/common': + specifier: workspace:* + version: link:../../packages/common + '@slickgrid-universal/custom-footer-component': + specifier: workspace:* + version: link:../../packages/custom-footer-component + '@slickgrid-universal/empty-warning-component': + specifier: workspace:* + version: link:../../packages/empty-warning-component + '@slickgrid-universal/event-pub-sub': + specifier: workspace:* + version: link:../../packages/event-pub-sub + '@slickgrid-universal/pagination-component': + specifier: workspace:* + version: link:../../packages/pagination-component + '@slickgrid-universal/row-detail-view-plugin': + specifier: workspace:* + version: link:../../packages/row-detail-view-plugin + '@slickgrid-universal/utils': + specifier: workspace:* + version: link:../../packages/utils + dequal: + specifier: ^2.0.3 + version: 2.0.3 + i18next: + specifier: ^24.0.2 + version: 24.0.2(typescript@5.6.3) + i18next-vue: + specifier: ^5.0.0 + version: 5.0.0(i18next@24.0.2(typescript@5.6.3))(vue@3.5.13(typescript@5.6.3)) + sortablejs: + specifier: ^1.15.6 + version: 1.15.6 + devDependencies: + '@vitejs/plugin-vue': + specifier: ^5.2.1 + version: 5.2.1(vite@6.0.1(@types/node@22.10.1)(jiti@1.21.6)(sass@1.81.0)(yaml@2.6.0))(vue@3.5.13(typescript@5.6.3)) + sass: + specifier: ^1.81.0 + version: 1.81.0 + typescript: + specifier: ~5.6.2 + version: 5.6.3 + vite: + specifier: ^6.0.1 + version: 6.0.1(@types/node@22.10.1)(jiti@1.21.6)(sass@1.81.0)(yaml@2.6.0) + vite-plugin-dts: + specifier: ^4.3.0 + version: 4.3.0(@types/node@22.10.1)(rollup@4.24.0)(typescript@5.6.3)(vite@6.0.1(@types/node@22.10.1)(jiti@1.21.6)(sass@1.81.0)(yaml@2.6.0)) + vue: + specifier: ^3.5.13 + version: 3.5.13(typescript@5.6.3) + vue-tsc: + specifier: ^2.1.10 + version: 2.1.10(typescript@5.6.3) + packages/binding: {} packages/common: @@ -534,6 +698,10 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/runtime@7.26.0': + resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} + engines: {node: '>=6.9.0'} + '@babel/types@7.25.9': resolution: {integrity: sha512-OwS2CM5KocvQ/k7dFJa8i5bNGJP0hXWfVCfDkqRFP1IreH1JDC7wG6eCYCi0+McbfT8OR/kNqsI0UU0xP9H6PQ==} engines: {node: '>=6.9.0'} @@ -806,6 +974,10 @@ packages: resolution: {integrity: sha512-lWrrK4QNlFSU+13PL9jMbMKLJYXDFu3tQfayBsMXX7KL/GiQeqfB1CzHkqD5UHBUtPAuPo6XwGbMFNdVMZObRA==} engines: {node: '>=18.0.0', npm: '>=9.0.0'} + '@faker-js/faker@9.2.0': + resolution: {integrity: sha512-ulqQu4KMr1/sTFIYvqSdegHT8NIkt66tFAkugGnHA+1WAfEn6hMzNR+svjXGFRVLnapxvej67Z/LwchFrnLBUg==} + engines: {node: '>=18.0.0', npm: '>=9.0.0'} + '@fnando/sparkline@0.3.10': resolution: {integrity: sha512-Rwz2swatdSU5F4sCOvYG8EOWdjtLgq5d8nmnqlZ3PXdWJI9Zq9BRUvJ/9ygjajJG8qOyNpMFX3GEVFjZIuB1Jg==} @@ -954,6 +1126,19 @@ packages: resolution: {integrity: sha512-Gc2pkHXZD8m5/df+vHVH6vyU4Ps19b6j5K8ezdgP28oOmspbgkrIoVogF7LxjwgQrcx5bXfqXsfNnRbDF+DWmw==} engines: {node: ^18.0.0 || >=20.0.0} + '@microsoft/api-extractor-model@7.30.0': + resolution: {integrity: sha512-26/LJZBrsWDKAkOWRiQbdVgcfd1F3nyJnAiJzsAgpouPk7LtOIj7PK9aJtBaw/pUXrkotEg27RrT+Jm/q0bbug==} + + '@microsoft/api-extractor@7.48.0': + resolution: {integrity: sha512-FMFgPjoilMUWeZXqYRlJ3gCVRhB7WU/HN88n8OLqEsmsG4zBdX/KQdtJfhq95LQTQ++zfu0Em1LLb73NqRCLYQ==} + hasBin: true + + '@microsoft/tsdoc-config@0.17.1': + resolution: {integrity: sha512-UtjIFe0C6oYgTnad4q1QP4qXwLhe6tIpNTRStJ2RZEPIkqQPREAwE5spzVxsdn9UaEMUqhh0AqSx3X4nWAKXWw==} + + '@microsoft/tsdoc@0.15.1': + resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1162,6 +1347,18 @@ packages: '@polka/url@1.0.0-next.28': resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + + '@rollup/pluginutils@5.1.3': + resolution: {integrity: sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rollup/rollup-android-arm-eabi@4.24.0': resolution: {integrity: sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==} cpu: [arm] @@ -1242,6 +1439,28 @@ packages: cpu: [x64] os: [win32] + '@rushstack/node-core-library@5.10.0': + resolution: {integrity: sha512-2pPLCuS/3x7DCd7liZkqOewGM0OzLyCacdvOe8j6Yrx9LkETGnxul1t7603bIaB8nUAooORcct9fFDOQMbWAgw==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/rig-package@0.5.3': + resolution: {integrity: sha512-olzSSjYrvCNxUFZowevC3uz8gvKr3WTpHQ7BkpjtRpA3wK+T0ybep/SRUMfr195gBzJm5gaXw0ZMgjIyHqJUow==} + + '@rushstack/terminal@0.14.3': + resolution: {integrity: sha512-csXbZsAdab/v8DbU1sz7WC2aNaKArcdS/FPmXMOXEj/JBBZMvDK0+1b4Qao0kkG0ciB1Qe86/Mb68GjH6/TnMw==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/ts-command-line@4.23.1': + resolution: {integrity: sha512-40jTmYoiu/xlIpkkRsVfENtBq4CW3R4azbL0Vmda+fMwHWqss6wwf/Cy/UJmMqIzpfYc2OTnjYP1ZLD3CmyeCA==} + '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} @@ -1273,6 +1492,12 @@ packages: resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} engines: {node: '>=18'} + '@slickgrid-universal/event-pub-sub@5.10.2': + resolution: {integrity: sha512-3l0rAZEf2CX2ApaXv4VCFVlExJSTu6VjDiWsNuZ9dh7+auWtiihsX4QZS8zskQIqEVrHtuJAlV8fq/l/XcwS+g==} + + '@slickgrid-universal/utils@5.10.2': + resolution: {integrity: sha512-cijV2/u3xKnfdUinaJeQNcoZuLy+9J4EgYSfUpNX22kanp948uKOICQxVrlb7Lj82Ki0U+vtuQ+fS59KQD3YOQ==} + '@trysound/sax@0.2.0': resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} @@ -1285,6 +1510,9 @@ packages: resolution: {integrity: sha512-92F7/SFyufn4DXsha9+QfKnN03JGqtMFMXgSHbZOo8JG59WkTni7UzAouNQDf7AuP9OAMxVOPQcqG3sB7w+kkg==} engines: {node: ^16.14.0 || >=18.0.0} + '@types/argparse@1.0.38': + resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} + '@types/conventional-commits-parser@5.0.0': resolution: {integrity: sha512-loB369iXNmAZglwWATL+WRe+CRMmmBPtpolYzIebFaX4YA3x+BEfLqhUAV9WanycKI3TG1IMr5bMJDajDKLlUQ==} @@ -1397,6 +1625,13 @@ packages: resolution: {integrity: sha512-pq19gbaMOmFE3CbL0ZB8J8BFCo2ckfHBfaIsaOZgBIF4EoISJIdLX5xRhd0FGB0LlHReNRuzoJoMGpTjq8F2CQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vitejs/plugin-vue@5.2.1': + resolution: {integrity: sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.2.25 + '@vitest/coverage-v8@2.1.6': resolution: {integrity: sha512-qItJVYDbG3MUFO68dOZUz+rWlqe9LMzotERXFXKg25s2A/kSVsyS9O0yNGrITfBd943GsnBeQZkBUu7Pc+zVeA==} peerDependencies: @@ -1453,6 +1688,66 @@ packages: '@vitest/utils@2.1.6': resolution: {integrity: sha512-ixNkFy3k4vokOUTU2blIUvOgKq/N2PW8vKIjZZYsGJCMX69MRa9J2sKqX5hY/k5O5Gty3YJChepkqZ3KM9LyIQ==} + '@volar/language-core@2.4.10': + resolution: {integrity: sha512-hG3Z13+nJmGaT+fnQzAkS0hjJRa2FCeqZt6Bd+oGNhUkQ+mTFsDETg5rqUTxyzIh5pSOGY7FHCWUS8G82AzLCA==} + + '@volar/source-map@2.4.10': + resolution: {integrity: sha512-OCV+b5ihV0RF3A7vEvNyHPi4G4kFa6ukPmyVocmqm5QzOd8r5yAtiNvaPEjl8dNvgC/lj4JPryeeHLdXd62rWA==} + + '@volar/typescript@2.4.10': + resolution: {integrity: sha512-F8ZtBMhSXyYKuBfGpYwqA5rsONnOwAVvjyE7KPYJ7wgZqo2roASqNWUnianOomJX5u1cxeRooHV59N0PhvEOgw==} + + '@vue/compiler-core@3.5.13': + resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==} + + '@vue/compiler-dom@3.5.13': + resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==} + + '@vue/compiler-sfc@3.5.13': + resolution: {integrity: sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==} + + '@vue/compiler-ssr@3.5.13': + resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==} + + '@vue/compiler-vue2@2.7.16': + resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/language-core@2.1.10': + resolution: {integrity: sha512-DAI289d0K3AB5TUG3xDp9OuQ71CnrujQwJrQnfuZDwo6eGNf0UoRlPuaVNO+Zrn65PC3j0oB2i7mNmVPggeGeQ==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/language-core@2.1.6': + resolution: {integrity: sha512-MW569cSky9R/ooKMh6xa2g1D0AtRKbL56k83dzus/bx//RDJk24RHWkMzbAlXjMdDNyxAaagKPRquBIxkxlCkg==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/reactivity@3.5.13': + resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==} + + '@vue/runtime-core@3.5.13': + resolution: {integrity: sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==} + + '@vue/runtime-dom@3.5.13': + resolution: {integrity: sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==} + + '@vue/server-renderer@3.5.13': + resolution: {integrity: sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==} + peerDependencies: + vue: 3.5.13 + + '@vue/shared@3.5.13': + resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} + JSONStream@1.3.5: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true @@ -1482,12 +1777,37 @@ packages: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} + ajv-draft-04@1.0.0: + resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} + peerDependencies: + ajv: ^8.5.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.12.0: + resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + + ajv@8.13.0: + resolution: {integrity: sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==} + ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + alien-signals@0.2.2: + resolution: {integrity: sha512-cZIRkbERILsBOXTQmMrxc9hgpxglstn69zm+F1ARf4aPAzdAFYd6sBq87ErO0Fj3DV94tglcyHG5kQz9nDC/8A==} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -1526,6 +1846,9 @@ packages: arch@2.2.0: resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1613,6 +1936,11 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + bootstrap@5.3.3: + resolution: {integrity: sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==} + peerDependencies: + '@popperjs/core': ^2.11.8 + brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -1802,9 +2130,18 @@ packages: compare-func@2.0.0: resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + compare-versions@6.1.1: + resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} + + computeds@0.0.1: + resolution: {integrity: sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} @@ -1878,6 +2215,9 @@ packages: engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} hasBin: true + cross-fetch@4.0.0: + resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} + cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -1938,6 +2278,9 @@ packages: resolution: {integrity: sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==} engines: {node: '>=18'} + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + cypress-real-events@1.13.0: resolution: {integrity: sha512-LoejtK+dyZ1jaT8wGT5oASTPfsNV8/ClRp99ruN60oPj8cBJYod80iJDyNwfPAu4GCxTXOhhAv9FO65Hpwt6Hg==} peerDependencies: @@ -1967,6 +2310,9 @@ packages: dayjs@1.11.13: resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -2053,6 +2399,9 @@ packages: dompurify@3.1.7: resolution: {integrity: sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==} + dompurify@3.2.2: + resolution: {integrity: sha512-YMM+erhdZ2nkZ4fTNRTSI94mb7VG7uVF5vj5Zde7tImgnhZE3R6YW/IACGIHb2ux+QkEXMhe591N+5jWOmL4Zw==} + domutils@3.1.0: resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} @@ -2202,6 +2551,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -2346,6 +2698,10 @@ packages: resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} engines: {node: '>=14.14'} + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + fs-extra@9.1.0: resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} engines: {node: '>=10'} @@ -2502,6 +2858,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + hosted-git-info@7.0.2: resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} engines: {node: ^16.14.0 || >=18.0.0} @@ -2541,6 +2901,23 @@ packages: engines: {node: '>=18'} hasBin: true + i18next-http-backend@3.0.1: + resolution: {integrity: sha512-XT2lYSkbAtDE55c6m7CtKxxrsfuRQO3rUfHzj8ZyRtY9CkIX3aRGwXGTkUhpGWce+J8n7sfu3J0f2wTzo7Lw0A==} + + i18next-vue@5.0.0: + resolution: {integrity: sha512-8jlctdGSKws9fcFlGlFOQRKCQQdUTKSs4D9pbZ6iitpzHQzQSVTdeDPvrjxLomsTjLK65W+MUGW6cwavAMGo9w==} + peerDependencies: + i18next: '>=23' + vue: ^3.4.38 + + i18next@24.0.2: + resolution: {integrity: sha512-D88xyIGcWAKwBTAs4RSqASi8NXR/NhCVSTM4LDbdoU8qb/5dcEZjNCLDhtQBB7Epw/Cp1w2vH/3ujoTbqLSs5g==} + peerDependencies: + typescript: ^5 + peerDependenciesMeta: + typescript: + optional: true + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -2566,6 +2943,10 @@ packages: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} + import-lazy@4.0.0: + resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} + engines: {node: '>=8'} + import-local@3.2.0: resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} engines: {node: '>=8'} @@ -2623,6 +3004,10 @@ packages: resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} hasBin: true + is-core-module@2.15.1: + resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} + engines: {node: '>= 0.4'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -2734,6 +3119,9 @@ packages: resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} hasBin: true + jju@1.4.0: + resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2798,6 +3186,9 @@ packages: engines: {node: '>=6'} hasBin: true + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} @@ -2825,6 +3216,9 @@ packages: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + lazy-ass@1.6.0: resolution: {integrity: sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==} engines: {node: '> 0.8'} @@ -2865,6 +3259,10 @@ packages: resolution: {integrity: sha512-Gnxj3ev3mB5TkVBGad0JM6dmLiQL+o0t23JPBZ9sd+yvSLk05mFoqKBw5N8gbbkU4TNXyqCgIrl/VM17OgUIgQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + local-pkg@0.5.1: + resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} + engines: {node: '>=14'} + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -2927,6 +3325,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + magic-string@0.30.12: resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==} @@ -2986,6 +3388,9 @@ packages: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} + minimatch@3.0.8: + resolution: {integrity: sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -3037,6 +3442,9 @@ packages: engines: {node: '>=10'} hasBin: true + mlly@1.7.3: + resolution: {integrity: sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==} + mrmime@2.0.0: resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} engines: {node: '>=10'} @@ -3044,6 +3452,9 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + multimatch@7.0.0: resolution: {integrity: sha512-SYU3HBAdF4psHEL/+jXDKHO95/m5P2RvboHT2Y0WtTttvJLP4H/2WS9WlQPFvF6C8d6SpLw8vjCnQOnVIVOSJQ==} engines: {node: '>=18'} @@ -3084,6 +3495,15 @@ packages: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + node-fetch@3.3.2: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -3283,6 +3703,9 @@ packages: parse5@7.2.0: resolution: {integrity: sha512-ZkDsAOcxsUMZ4Lz5fVciOehNcJ+Gb8gTzcA4yl3wnc273BAybYWrQ+Ks/OjCjSEpjvQkDSeZbybK9qj2VHHdGA==} + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -3303,6 +3726,9 @@ packages: resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} engines: {node: '>=12'} + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@1.11.1: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} @@ -3352,6 +3778,9 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} + pkg-types@1.2.1: + resolution: {integrity: sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==} + pnpm@9.14.4: resolution: {integrity: sha512-yBgLP75OS8oCyUI0cXiWtVKXQKbLrfGfp4JUJwQD6i8n1OHUagig9WyJtj3I6/0+5TMm2nICc3lOYgD88NGEqw==} engines: {node: '>=18.12'} @@ -3667,6 +4096,9 @@ packages: resolution: {integrity: sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==} engines: {node: '>= 14.16.0'} + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + request-progress@3.0.0: resolution: {integrity: sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==} @@ -3693,6 +4125,10 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + restore-cursor@3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} @@ -3752,6 +4188,11 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} + semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + semver@7.6.3: resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} engines: {node: '>=10'} @@ -3860,6 +4301,9 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + sprintf-js@1.1.3: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} @@ -3882,6 +4326,10 @@ packages: std-env@3.8.0: resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -3943,6 +4391,10 @@ packages: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} engines: {node: '>=10'} + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + svgo@3.3.2: resolution: {integrity: sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==} engines: {node: '>=14.0.0'} @@ -4035,6 +4487,9 @@ packages: resolution: {integrity: sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==} engines: {node: '>=16'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@5.0.0: resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} engines: {node: '>=18'} @@ -4096,6 +4551,11 @@ packages: typescript: optional: true + typescript@5.4.2: + resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==} + engines: {node: '>=14.17'} + hasBin: true + typescript@5.6.3: resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} engines: {node: '>=14.17'} @@ -4106,6 +4566,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + ufo@1.5.4: + resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} + uglify-js@3.19.3: resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} engines: {node: '>=0.8.0'} @@ -4135,6 +4598,10 @@ packages: universal-user-agent@7.0.2: resolution: {integrity: sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==} + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -4178,7 +4645,7 @@ packages: resolution: {integrity: sha512-0yqWqlvitfQSRqjyVVr613whIgp62qC1JHgXyLalcJkNkMRZXRqEr+QQQvRdQavB2PBgB4HW+GM6VU4KU0K3Ng==} verror@1.10.0: - resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} + resolution: {integrity: sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=} engines: {'0': node >=0.6.0} vite-node@2.1.6: @@ -4186,6 +4653,16 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true + vite-plugin-dts@4.3.0: + resolution: {integrity: sha512-LkBJh9IbLwL6/rxh0C1/bOurDrIEmRE7joC+jFdOEEciAFPbpEKOLSAr5nNh5R7CJ45cMbksTrFfy52szzC5eA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + typescript: '*' + vite: '*' + peerDependenciesMeta: + vite: + optional: true + vite@6.0.0: resolution: {integrity: sha512-Q2+5yQV79EdnpbNxjD3/QHVMCBaQ3Kpd4/uL51UGuh38bIIM+s4o3FqyCzRvTRwFb+cWIUeZvaWwS9y2LD2qeQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -4291,6 +4768,28 @@ packages: jsdom: optional: true + vscode-uri@3.0.8: + resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + + vue-router@4.5.0: + resolution: {integrity: sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==} + peerDependencies: + vue: ^3.2.0 + + vue-tsc@2.1.10: + resolution: {integrity: sha512-RBNSfaaRHcN5uqVqJSZh++Gy/YUzryuv9u1aFWhsammDJXNtUiJMNoJ747lZcQ68wUQFx6E73y4FY3D8E7FGMA==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.13: + resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + w3c-xmlserializer@5.0.0: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} @@ -4305,6 +4804,9 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} @@ -4331,6 +4833,9 @@ packages: resolution: {integrity: sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==} engines: {node: '>=18'} + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -4485,6 +4990,10 @@ snapshots: dependencies: '@babel/types': 7.25.9 + '@babel/runtime@7.26.0': + dependencies: + regenerator-runtime: 0.14.1 + '@babel/types@7.25.9': dependencies: '@babel/helper-string-parser': 7.25.9 @@ -4750,6 +5259,8 @@ snapshots: '@faker-js/faker@9.0.3': {} + '@faker-js/faker@9.2.0': {} + '@fnando/sparkline@0.3.10': {} '@formkit/tempo@0.1.2': {} @@ -5061,6 +5572,41 @@ snapshots: - supports-color - typescript + '@microsoft/api-extractor-model@7.30.0(@types/node@22.10.1)': + dependencies: + '@microsoft/tsdoc': 0.15.1 + '@microsoft/tsdoc-config': 0.17.1 + '@rushstack/node-core-library': 5.10.0(@types/node@22.10.1) + transitivePeerDependencies: + - '@types/node' + + '@microsoft/api-extractor@7.48.0(@types/node@22.10.1)': + dependencies: + '@microsoft/api-extractor-model': 7.30.0(@types/node@22.10.1) + '@microsoft/tsdoc': 0.15.1 + '@microsoft/tsdoc-config': 0.17.1 + '@rushstack/node-core-library': 5.10.0(@types/node@22.10.1) + '@rushstack/rig-package': 0.5.3 + '@rushstack/terminal': 0.14.3(@types/node@22.10.1) + '@rushstack/ts-command-line': 4.23.1(@types/node@22.10.1) + lodash: 4.17.21 + minimatch: 3.0.8 + resolve: 1.22.8 + semver: 7.5.4 + source-map: 0.6.1 + typescript: 5.4.2 + transitivePeerDependencies: + - '@types/node' + + '@microsoft/tsdoc-config@0.17.1': + dependencies: + '@microsoft/tsdoc': 0.15.1 + ajv: 8.12.0 + jju: 1.4.0 + resolve: 1.22.8 + + '@microsoft/tsdoc@0.15.1': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -5327,6 +5873,16 @@ snapshots: '@polka/url@1.0.0-next.28': {} + '@popperjs/core@2.11.8': {} + + '@rollup/pluginutils@5.1.3(rollup@4.24.0)': + dependencies: + '@types/estree': 1.0.6 + estree-walker: 2.0.2 + picomatch: 4.0.2 + optionalDependencies: + rollup: 4.24.0 + '@rollup/rollup-android-arm-eabi@4.24.0': optional: true @@ -5375,6 +5931,40 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.24.0': optional: true + '@rushstack/node-core-library@5.10.0(@types/node@22.10.1)': + dependencies: + ajv: 8.13.0 + ajv-draft-04: 1.0.0(ajv@8.13.0) + ajv-formats: 3.0.1(ajv@8.13.0) + fs-extra: 7.0.1 + import-lazy: 4.0.0 + jju: 1.4.0 + resolve: 1.22.8 + semver: 7.5.4 + optionalDependencies: + '@types/node': 22.10.1 + + '@rushstack/rig-package@0.5.3': + dependencies: + resolve: 1.22.8 + strip-json-comments: 3.1.1 + + '@rushstack/terminal@0.14.3(@types/node@22.10.1)': + dependencies: + '@rushstack/node-core-library': 5.10.0(@types/node@22.10.1) + supports-color: 8.1.1 + optionalDependencies: + '@types/node': 22.10.1 + + '@rushstack/ts-command-line@4.23.1(@types/node@22.10.1)': + dependencies: + '@rushstack/terminal': 0.14.3(@types/node@22.10.1) + '@types/argparse': 1.0.38 + argparse: 1.0.10 + string-argv: 0.3.2 + transitivePeerDependencies: + - '@types/node' + '@sec-ant/readable-stream@0.4.1': {} '@sigstore/bundle@2.3.2': @@ -5411,6 +6001,12 @@ snapshots: '@sindresorhus/merge-streams@2.3.0': {} + '@slickgrid-universal/event-pub-sub@5.10.2': + dependencies: + '@slickgrid-universal/utils': 5.10.2 + + '@slickgrid-universal/utils@5.10.2': {} + '@trysound/sax@0.2.0': {} '@tufjs/canonical-json@2.0.0': {} @@ -5420,6 +6016,8 @@ snapshots: '@tufjs/canonical-json': 2.0.0 minimatch: 9.0.5 + '@types/argparse@1.0.38': {} + '@types/conventional-commits-parser@5.0.0': dependencies: '@types/node': 22.10.1 @@ -5547,6 +6145,11 @@ snapshots: '@typescript-eslint/types': 8.16.0 eslint-visitor-keys: 4.2.0 + '@vitejs/plugin-vue@5.2.1(vite@6.0.1(@types/node@22.10.1)(jiti@1.21.6)(sass@1.81.0)(yaml@2.6.0))(vue@3.5.13(typescript@5.6.3))': + dependencies: + vite: 6.0.1(@types/node@22.10.1)(jiti@1.21.6)(sass@1.81.0)(yaml@2.6.0) + vue: 3.5.13(typescript@5.6.3) + '@vitest/coverage-v8@2.1.6(vitest@2.1.6)': dependencies: '@ampproject/remapping': 2.3.0 @@ -5624,6 +6227,105 @@ snapshots: loupe: 3.1.2 tinyrainbow: 1.2.0 + '@volar/language-core@2.4.10': + dependencies: + '@volar/source-map': 2.4.10 + + '@volar/source-map@2.4.10': {} + + '@volar/typescript@2.4.10': + dependencies: + '@volar/language-core': 2.4.10 + path-browserify: 1.0.1 + vscode-uri: 3.0.8 + + '@vue/compiler-core@3.5.13': + dependencies: + '@babel/parser': 7.25.9 + '@vue/shared': 3.5.13 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.13': + dependencies: + '@vue/compiler-core': 3.5.13 + '@vue/shared': 3.5.13 + + '@vue/compiler-sfc@3.5.13': + dependencies: + '@babel/parser': 7.25.9 + '@vue/compiler-core': 3.5.13 + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-ssr': 3.5.13 + '@vue/shared': 3.5.13 + estree-walker: 2.0.2 + magic-string: 0.30.12 + postcss: 8.4.49 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.13': + dependencies: + '@vue/compiler-dom': 3.5.13 + '@vue/shared': 3.5.13 + + '@vue/compiler-vue2@2.7.16': + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + '@vue/devtools-api@6.6.4': {} + + '@vue/language-core@2.1.10(typescript@5.6.3)': + dependencies: + '@volar/language-core': 2.4.10 + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.13 + alien-signals: 0.2.2 + minimatch: 9.0.5 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.6.3 + + '@vue/language-core@2.1.6(typescript@5.6.3)': + dependencies: + '@volar/language-core': 2.4.10 + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.13 + computeds: 0.0.1 + minimatch: 9.0.5 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.6.3 + + '@vue/reactivity@3.5.13': + dependencies: + '@vue/shared': 3.5.13 + + '@vue/runtime-core@3.5.13': + dependencies: + '@vue/reactivity': 3.5.13 + '@vue/shared': 3.5.13 + + '@vue/runtime-dom@3.5.13': + dependencies: + '@vue/reactivity': 3.5.13 + '@vue/runtime-core': 3.5.13 + '@vue/shared': 3.5.13 + csstype: 3.1.3 + + '@vue/server-renderer@3.5.13(vue@3.5.13(typescript@5.6.3))': + dependencies: + '@vue/compiler-ssr': 3.5.13 + '@vue/shared': 3.5.13 + vue: 3.5.13(typescript@5.6.3) + + '@vue/shared@3.5.13': {} + JSONStream@1.3.5: dependencies: jsonparse: 1.3.1 @@ -5650,6 +6352,14 @@ snapshots: clean-stack: 2.2.0 indent-string: 4.0.0 + ajv-draft-04@1.0.0(ajv@8.13.0): + optionalDependencies: + ajv: 8.13.0 + + ajv-formats@3.0.1(ajv@8.13.0): + optionalDependencies: + ajv: 8.13.0 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -5657,6 +6367,20 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.12.0: + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + + ajv@8.13.0: + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + ajv@8.17.1: dependencies: fast-deep-equal: 3.1.3 @@ -5664,6 +6388,8 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + alien-signals@0.2.2: {} + ansi-colors@4.1.3: {} ansi-escapes@4.3.2: @@ -5693,6 +6419,10 @@ snapshots: arch@2.2.0: {} + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + argparse@2.0.1: {} array-differ@4.0.0: {} @@ -5760,6 +6490,10 @@ snapshots: boolbase@1.0.0: {} + bootstrap@5.3.3(@popperjs/core@2.11.8): + dependencies: + '@popperjs/core': 2.11.8 + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -5959,8 +6693,14 @@ snapshots: array-ify: 1.0.0 dot-prop: 5.3.0 + compare-versions@6.1.1: {} + + computeds@0.0.1: {} + concat-map@0.0.1: {} + confbox@0.1.8: {} + config-chain@1.1.13: dependencies: ini: 1.3.8 @@ -6052,6 +6792,12 @@ snapshots: dependencies: cross-spawn: 7.0.3 + cross-fetch@4.0.0(encoding@0.1.13): + dependencies: + node-fetch: 2.7.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + cross-spawn@7.0.3: dependencies: path-key: 3.1.1 @@ -6142,6 +6888,8 @@ snapshots: dependencies: rrweb-cssom: 0.7.1 + csstype@3.1.3: {} + cypress-real-events@1.13.0(cypress@13.16.0): dependencies: cypress: 13.16.0 @@ -6207,6 +6955,8 @@ snapshots: dayjs@1.11.13: {} + de-indent@1.0.2: {} + debug@3.2.7(supports-color@8.1.1): dependencies: ms: 2.1.3 @@ -6267,6 +7017,10 @@ snapshots: dompurify@3.1.7: {} + dompurify@3.2.2: + optionalDependencies: + '@types/trusted-types': 2.0.7 + domutils@3.1.0: dependencies: dom-serializer: 2.0.0 @@ -6461,6 +7215,8 @@ snapshots: estraverse@5.3.0: {} + estree-walker@2.0.2: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.6 @@ -6477,7 +7233,7 @@ snapshots: execa@4.1.0: dependencies: - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 get-stream: 5.2.0 human-signals: 1.1.1 is-stream: 2.0.1 @@ -6623,6 +7379,12 @@ snapshots: jsonfile: 6.1.0 universalify: 2.0.1 + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + fs-extra@9.1.0: dependencies: at-least-node: 1.0.0 @@ -6790,6 +7552,8 @@ snapshots: dependencies: function-bind: 1.1.2 + he@1.2.0: {} + hosted-git-info@7.0.2: dependencies: lru-cache: 10.4.3 @@ -6828,6 +7592,23 @@ snapshots: husky@9.1.7: {} + i18next-http-backend@3.0.1(encoding@0.1.13): + dependencies: + cross-fetch: 4.0.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + + i18next-vue@5.0.0(i18next@24.0.2(typescript@5.6.3))(vue@3.5.13(typescript@5.6.3)): + dependencies: + i18next: 24.0.2(typescript@5.6.3) + vue: 3.5.13(typescript@5.6.3) + + i18next@24.0.2(typescript@5.6.3): + dependencies: + '@babel/runtime': 7.26.0 + optionalDependencies: + typescript: 5.6.3 + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -6849,6 +7630,8 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-lazy@4.0.0: {} + import-local@3.2.0: dependencies: pkg-dir: 4.2.0 @@ -6892,6 +7675,10 @@ snapshots: dependencies: ci-info: 3.9.0 + is-core-module@2.15.1: + dependencies: + hasown: 2.0.2 + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -6980,6 +7767,8 @@ snapshots: jiti@1.21.6: {} + jju@1.4.0: {} + js-tokens@4.0.0: {} js-yaml@4.1.0: @@ -7044,6 +7833,10 @@ snapshots: json5@2.2.3: {} + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + jsonfile@6.1.0: dependencies: universalify: 2.0.1 @@ -7071,6 +7864,8 @@ snapshots: kind-of@6.0.3: {} + kolorist@1.8.0: {} + lazy-ass@1.6.0: {} levn@0.4.1: @@ -7119,6 +7914,11 @@ snapshots: load-json-file@7.0.1: {} + local-pkg@0.5.1: + dependencies: + mlly: 1.7.3 + pkg-types: 1.2.1 + locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -7171,6 +7971,10 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + magic-string@0.30.12: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -7231,6 +8035,10 @@ snapshots: mimic-fn@4.0.0: {} + minimatch@3.0.8: + dependencies: + brace-expansion: 1.1.11 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -7280,10 +8088,19 @@ snapshots: mkdirp@1.0.4: {} + mlly@1.7.3: + dependencies: + acorn: 8.14.0 + pathe: 1.1.2 + pkg-types: 1.2.1 + ufo: 1.5.4 + mrmime@2.0.0: {} ms@2.1.3: {} + muggle-string@0.4.1: {} + multimatch@7.0.0: dependencies: array-differ: 4.0.0 @@ -7316,6 +8133,12 @@ snapshots: node-domexception@1.0.0: {} + node-fetch@2.7.0(encoding@0.1.13): + dependencies: + whatwg-url: 5.0.0 + optionalDependencies: + encoding: 0.1.13 + node-fetch@3.3.2: dependencies: data-uri-to-buffer: 4.0.1 @@ -7567,6 +8390,8 @@ snapshots: dependencies: entities: 4.5.0 + path-browserify@1.0.1: {} + path-exists@4.0.0: {} path-exists@5.0.0: {} @@ -7577,6 +8402,8 @@ snapshots: path-key@4.0.0: {} + path-parse@1.0.7: {} + path-scurry@1.11.1: dependencies: lru-cache: 10.4.3 @@ -7608,6 +8435,12 @@ snapshots: dependencies: find-up: 4.1.0 + pkg-types@1.2.1: + dependencies: + confbox: 0.1.8 + mlly: 1.7.3 + pathe: 1.1.2 + pnpm@9.14.4: {} postcss-calc@10.0.2(postcss@8.4.49): @@ -7909,6 +8742,8 @@ snapshots: readdirp@4.0.2: {} + regenerator-runtime@0.14.1: {} + request-progress@3.0.0: dependencies: throttleit: 1.0.1 @@ -7927,6 +8762,12 @@ snapshots: resolve-pkg-maps@1.0.0: {} + resolve@1.22.8: + dependencies: + is-core-module: 2.15.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + restore-cursor@3.1.0: dependencies: onetime: 5.1.2 @@ -8003,6 +8844,10 @@ snapshots: dependencies: xmlchars: 2.2.0 + semver@7.5.4: + dependencies: + lru-cache: 6.0.0 + semver@7.6.3: {} servor@4.0.2: {} @@ -8115,6 +8960,8 @@ snapshots: split2@4.2.0: {} + sprintf-js@1.0.3: {} + sprintf-js@1.1.3: {} sshpk@1.18.0: @@ -8141,6 +8988,8 @@ snapshots: std-env@3.8.0: {} + string-argv@0.3.2: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -8203,6 +9052,8 @@ snapshots: dependencies: has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} + svgo@3.3.2: dependencies: '@trysound/sax': 0.2.0 @@ -8291,6 +9142,8 @@ snapshots: dependencies: tldts: 6.1.55 + tr46@0.0.3: {} + tr46@5.0.0: dependencies: punycode: 2.3.1 @@ -8342,10 +9195,14 @@ snapshots: transitivePeerDependencies: - supports-color + typescript@5.4.2: {} + typescript@5.6.3: {} typescript@5.7.2: {} + ufo@1.5.4: {} + uglify-js@3.19.3: optional: true @@ -8367,6 +9224,8 @@ snapshots: universal-user-agent@7.0.2: {} + universalify@0.1.2: {} + universalify@2.0.1: {} untildify@4.0.0: {} @@ -8425,6 +9284,25 @@ snapshots: - tsx - yaml + vite-plugin-dts@4.3.0(@types/node@22.10.1)(rollup@4.24.0)(typescript@5.6.3)(vite@6.0.1(@types/node@22.10.1)(jiti@1.21.6)(sass@1.81.0)(yaml@2.6.0)): + dependencies: + '@microsoft/api-extractor': 7.48.0(@types/node@22.10.1) + '@rollup/pluginutils': 5.1.3(rollup@4.24.0) + '@volar/typescript': 2.4.10 + '@vue/language-core': 2.1.6(typescript@5.6.3) + compare-versions: 6.1.1 + debug: 4.3.7(supports-color@8.1.1) + kolorist: 1.8.0 + local-pkg: 0.5.1 + magic-string: 0.30.12 + typescript: 5.6.3 + optionalDependencies: + vite: 6.0.1(@types/node@22.10.1)(jiti@1.21.6)(sass@1.81.0)(yaml@2.6.0) + transitivePeerDependencies: + - '@types/node' + - rollup + - supports-color + vite@6.0.0(@types/node@22.7.9)(jiti@1.21.6)(sass@1.80.4)(yaml@2.6.0): dependencies: esbuild: 0.24.0 @@ -8490,6 +9368,30 @@ snapshots: - tsx - yaml + vscode-uri@3.0.8: {} + + vue-router@4.5.0(vue@3.5.13(typescript@5.6.3)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.13(typescript@5.6.3) + + vue-tsc@2.1.10(typescript@5.6.3): + dependencies: + '@volar/typescript': 2.4.10 + '@vue/language-core': 2.1.10(typescript@5.6.3) + semver: 7.6.3 + typescript: 5.6.3 + + vue@3.5.13(typescript@5.6.3): + dependencies: + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-sfc': 3.5.13 + '@vue/runtime-dom': 3.5.13 + '@vue/server-renderer': 3.5.13(vue@3.5.13(typescript@5.6.3)) + '@vue/shared': 3.5.13 + optionalDependencies: + typescript: 5.6.3 + w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 @@ -8502,6 +9404,8 @@ snapshots: web-streams-polyfill@3.3.3: {} + webidl-conversions@3.0.1: {} + webidl-conversions@7.0.0: {} whatwg-encoding@3.1.1: @@ -8524,6 +9428,11 @@ snapshots: tr46: 5.0.0 webidl-conversions: 7.0.0 + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + which@2.0.2: dependencies: isexe: 2.0.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2937eb6b8..56cd8431d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,5 @@ packages: + - 'demos/**' + - 'frameworks/**' - 'packages/**' - 'examples/**' \ No newline at end of file diff --git a/test/cypress/e2e/example12.cy.ts b/test/cypress/e2e/example12.cy.ts index 6cecb77bc..87a38fe98 100644 --- a/test/cypress/e2e/example12.cy.ts +++ b/test/cypress/e2e/example12.cy.ts @@ -208,8 +208,7 @@ describe('Example 12 - Composite Editor Modal', () => { it('should undo last edit and expect the date editor to NOT be opened when clicking undo last edit button', () => { cy.get('[data-test=undo-last-edit-btn]').click(); - cy.get('.vanilla-calendar') - .should('not.exist'); + cy.get('.vanilla-calendar:visible').should('not.exist'); cy.get('.unsaved-editable-field') .should('have.length', 11); From 2d325ef9caf8735b4d56674f6692e5e6fc2cea8b Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Mon, 2 Dec 2024 19:03:45 -0500 Subject: [PATCH 02/36] chore: skip eslint rule --- eslint.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 2008688ed..20be891f1 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -59,7 +59,7 @@ export default tseslint.config( 'cypress/no-assigning-return-values': 'off', 'cypress/unsafe-to-chain-command': 'off', 'object-shorthand': 'error', - 'n/file-extension-in-import': ['error', 'always', { ".cy.ts": "never" }], + // 'n/file-extension-in-import': ['error', 'always', { ".cy.ts": "never" }], 'no-async-promise-executor': 'off', 'no-case-declarations': 'off', 'no-prototype-builtins': 'off', From dacb8489bcd391943af31fc4c0ff23fcfe6f618c Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Mon, 2 Dec 2024 19:10:49 -0500 Subject: [PATCH 03/36] chore: add slickgrid-vue as references path --- tsconfig.packages.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tsconfig.packages.json b/tsconfig.packages.json index 547014127..54e2e6a48 100644 --- a/tsconfig.packages.json +++ b/tsconfig.packages.json @@ -1,6 +1,7 @@ { "files": [], "references": [ + { "path": "./frameworks/slickgrid-vue" }, { "path": "./packages/binding" }, { "path": "./packages/common" }, { "path": "./packages/composite-editor-component" }, From 7636aecbc51392737a954bc7c753835762d1ea0d Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Mon, 2 Dec 2024 19:26:34 -0500 Subject: [PATCH 04/36] chore: add all universal dependencies to the workspace --- package.json | 17 ++++++++++++++ pnpm-lock.yaml | 51 ++++++++++++++++++++++++++++++++++++++++++ tsconfig.packages.json | 1 - 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 756424e8f..6c33a0d79 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,23 @@ "@lerna-lite/publish": "^3.10.1", "@lerna-lite/run": "^3.10.1", "@lerna-lite/watch": "^3.10.1", + "@slickgrid-universal/binding": "workspace:*", + "@slickgrid-universal/common": "workspace:*", + "@slickgrid-universal/composite-editor-component": "workspace:*", + "@slickgrid-universal/custom-footer-component": "workspace:*", + "@slickgrid-universal/custom-tooltip-plugin": "workspace:*", + "@slickgrid-universal/empty-warning-component": "workspace:*", + "@slickgrid-universal/event-pub-sub": "workspace:*", + "@slickgrid-universal/excel-export": "workspace:*", + "@slickgrid-universal/graphql": "workspace:*", + "@slickgrid-universal/odata": "workspace:*", + "@slickgrid-universal/pagination-component": "workspace:*", + "@slickgrid-universal/row-detail-view-plugin": "workspace:*", + "@slickgrid-universal/rxjs-observable": "workspace:*", + "@slickgrid-universal/text-export": "workspace:*", + "@slickgrid-universal/utils": "workspace:*", + "@slickgrid-universal/vanilla-bundle": "workspace:*", + "@slickgrid-universal/vanilla-force-bundle": "workspace:*", "@types/node": "^22.10.1", "@vitest/coverage-v8": "^2.1.6", "@vitest/eslint-plugin": "^1.1.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 261c70f0e..d68a4d611 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,57 @@ importers: '@lerna-lite/watch': specifier: ^3.10.1 version: 3.10.1(@lerna-lite/publish@3.10.1)(@lerna-lite/run@3.10.1)(@types/node@22.10.1)(typescript@5.7.2) + '@slickgrid-universal/binding': + specifier: workspace:* + version: link:packages/binding + '@slickgrid-universal/common': + specifier: workspace:* + version: link:packages/common + '@slickgrid-universal/composite-editor-component': + specifier: workspace:* + version: link:packages/composite-editor-component + '@slickgrid-universal/custom-footer-component': + specifier: workspace:* + version: link:packages/custom-footer-component + '@slickgrid-universal/custom-tooltip-plugin': + specifier: workspace:* + version: link:packages/custom-tooltip-plugin + '@slickgrid-universal/empty-warning-component': + specifier: workspace:* + version: link:packages/empty-warning-component + '@slickgrid-universal/event-pub-sub': + specifier: workspace:* + version: link:packages/event-pub-sub + '@slickgrid-universal/excel-export': + specifier: workspace:* + version: link:packages/excel-export + '@slickgrid-universal/graphql': + specifier: workspace:* + version: link:packages/graphql + '@slickgrid-universal/odata': + specifier: workspace:* + version: link:packages/odata + '@slickgrid-universal/pagination-component': + specifier: workspace:* + version: link:packages/pagination-component + '@slickgrid-universal/row-detail-view-plugin': + specifier: workspace:* + version: link:packages/row-detail-view-plugin + '@slickgrid-universal/rxjs-observable': + specifier: workspace:* + version: link:packages/rxjs-observable + '@slickgrid-universal/text-export': + specifier: workspace:* + version: link:packages/text-export + '@slickgrid-universal/utils': + specifier: workspace:* + version: link:packages/utils + '@slickgrid-universal/vanilla-bundle': + specifier: workspace:* + version: link:packages/vanilla-bundle + '@slickgrid-universal/vanilla-force-bundle': + specifier: workspace:* + version: link:packages/vanilla-force-bundle '@types/node': specifier: ^22.10.1 version: 22.10.1 diff --git a/tsconfig.packages.json b/tsconfig.packages.json index 54e2e6a48..547014127 100644 --- a/tsconfig.packages.json +++ b/tsconfig.packages.json @@ -1,7 +1,6 @@ { "files": [], "references": [ - { "path": "./frameworks/slickgrid-vue" }, { "path": "./packages/binding" }, { "path": "./packages/common" }, { "path": "./packages/composite-editor-component" }, From 16c4a82813b2e44d498da8ea6bd7074dc63f8e9a Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Mon, 2 Dec 2024 20:25:35 -0500 Subject: [PATCH 05/36] chore: add CJS build and exports --- frameworks/slickgrid-vue/clone-dts.mjs | 3 +++ frameworks/slickgrid-vue/package.json | 20 ++++++++++++++------ frameworks/slickgrid-vue/src/index.ts | 1 + frameworks/slickgrid-vue/vite.config.ts | 11 ++++++++--- 4 files changed, 26 insertions(+), 9 deletions(-) create mode 100644 frameworks/slickgrid-vue/clone-dts.mjs diff --git a/frameworks/slickgrid-vue/clone-dts.mjs b/frameworks/slickgrid-vue/clone-dts.mjs new file mode 100644 index 000000000..aa937b35e --- /dev/null +++ b/frameworks/slickgrid-vue/clone-dts.mjs @@ -0,0 +1,3 @@ +import { readFileSync, writeFileSync } from 'node:fs'; + +writeFileSync('dist/index.d.cts', readFileSync('dist/index.d.ts')); diff --git a/frameworks/slickgrid-vue/package.json b/frameworks/slickgrid-vue/package.json index 885fe2cf1..6ebe4a29a 100644 --- a/frameworks/slickgrid-vue/package.json +++ b/frameworks/slickgrid-vue/package.json @@ -1,15 +1,19 @@ { "name": "slickgrid-vue", "version": "0.1.0", - "type": "module", "description": "Slickgrid-Vue", - "module": "./dist/index.js", + "type": "module", + "main": "./dist/index.cjs", "types": "./dist/index.d.ts", "exports": { ".": { "import": { "types": "./dist/index.d.ts", - "default": "./dist/index.js" + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" } }, "./package.json": "./package.json" @@ -21,8 +25,10 @@ "keywords": [ "vue", "vue3", - "component", - "library" + "plugin", + "datagrid", + "datatable", + "slickgrid" ], "publishConfig": { "access": "public" @@ -32,11 +38,13 @@ "url": "https://ko-fi.com/ghiscoding" }, "scripts": { + "are-types-wrong": "pnpx @arethetypeswrong/cli --pack .", "clean": "rimraf dist", "dev": "vite build --watch", "dev:init": "vite build", - "build": "vue-tsc --p ./tsconfig.app.json && vite build --sourcemap", + "build": "pnpm clean && vue-tsc --p ./tsconfig.app.json && vite build --sourcemap && pnpm clone:dts", "preview": "vite preview", + "clone:dts": "node clone-dts.mjs", "type-check": "vue-tsc --build --force" }, "dependencies": { diff --git a/frameworks/slickgrid-vue/src/index.ts b/frameworks/slickgrid-vue/src/index.ts index ecc7c0b09..8f5845821 100644 --- a/frameworks/slickgrid-vue/src/index.ts +++ b/frameworks/slickgrid-vue/src/index.ts @@ -1,6 +1,7 @@ import type { Column } from '@slickgrid-universal/common'; import { Editors, Filters } from '@slickgrid-universal/common'; export * from '@slickgrid-universal/common'; + import SlickgridVue from './components/SlickgridVue.vue'; import { SlickRowDetailView } from './extensions/slickRowDetailView.js'; import type { GridOption, RowDetailView, SlickgridVueInstance } from './models/index.js'; diff --git a/frameworks/slickgrid-vue/vite.config.ts b/frameworks/slickgrid-vue/vite.config.ts index 31ed55598..ad03db8a2 100644 --- a/frameworks/slickgrid-vue/vite.config.ts +++ b/frameworks/slickgrid-vue/vite.config.ts @@ -14,14 +14,13 @@ export default defineConfig({ copyPublicDir: false, lib: { entry: resolve(__dirname, 'src/index.ts'), - formats: ['es'], - fileName: 'index', + formats: ['es', 'cjs'], + fileName: format => format === 'cjs' ? 'index.cjs' : 'index.mjs', }, rollupOptions: { // make sure to externalize deps that shouldn't be bundled // into your library external: [ - 'vue', '@formkit/tempo', '@slickgrid-universal/common', '@slickgrid-universal/custom-footer-component', @@ -34,7 +33,13 @@ export default defineConfig({ 'i18next', 'i18next-vue', 'sortablejs', + 'vue', ], + output: { + globals: { + vue: 'Vue', + }, + }, }, }, }); From 6024a92b3f0392ffb40d50e48f711fec8cb0428e Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Mon, 2 Dec 2024 21:57:59 -0500 Subject: [PATCH 06/36] chore: cleanup code --- demos/vue/{vite.config.ts => vite.config.mts} | 0 frameworks/slickgrid-vue/index.html | 14 -------------- frameworks/slickgrid-vue/package.json | 1 - frameworks/slickgrid-vue/src/index.ts | 18 ++++++++++++++---- frameworks/slickgrid-vue/tsconfig.node.json | 2 +- .../{vite.config.ts => vite.config.mts} | 0 6 files changed, 15 insertions(+), 20 deletions(-) rename demos/vue/{vite.config.ts => vite.config.mts} (100%) delete mode 100644 frameworks/slickgrid-vue/index.html rename frameworks/slickgrid-vue/{vite.config.ts => vite.config.mts} (100%) diff --git a/demos/vue/vite.config.ts b/demos/vue/vite.config.mts similarity index 100% rename from demos/vue/vite.config.ts rename to demos/vue/vite.config.mts diff --git a/frameworks/slickgrid-vue/index.html b/frameworks/slickgrid-vue/index.html deleted file mode 100644 index 506c54360..000000000 --- a/frameworks/slickgrid-vue/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - Slickgrid-Vue - - - -
- - - diff --git a/frameworks/slickgrid-vue/package.json b/frameworks/slickgrid-vue/package.json index 6ebe4a29a..66d7ce5af 100644 --- a/frameworks/slickgrid-vue/package.json +++ b/frameworks/slickgrid-vue/package.json @@ -43,7 +43,6 @@ "dev": "vite build --watch", "dev:init": "vite build", "build": "pnpm clean && vue-tsc --p ./tsconfig.app.json && vite build --sourcemap && pnpm clone:dts", - "preview": "vite preview", "clone:dts": "node clone-dts.mjs", "type-check": "vue-tsc --build --force" }, diff --git a/frameworks/slickgrid-vue/src/index.ts b/frameworks/slickgrid-vue/src/index.ts index 8f5845821..26621a1b1 100644 --- a/frameworks/slickgrid-vue/src/index.ts +++ b/frameworks/slickgrid-vue/src/index.ts @@ -1,5 +1,6 @@ -import type { Column } from '@slickgrid-universal/common'; -import { Editors, Filters } from '@slickgrid-universal/common'; +import { Aggregators, type Column, type Editors, Enums, type Filters, Formatters, GroupTotalFormatters, SortComparers, Utilities } from '@slickgrid-universal/common'; +import { BindingService } from '@slickgrid-universal/binding'; +import { EventPubSubService } from '@slickgrid-universal/event-pub-sub'; export * from '@slickgrid-universal/common'; import SlickgridVue from './components/SlickgridVue.vue'; @@ -12,13 +13,22 @@ export type { SlickgridVueProps } from './components/slickgridVueProps.interface export { disposeAllSubscriptions, TranslaterService } from './services/index.js'; export { + Aggregators, type Column, - Editors, - Filters, + type Editors, + type Filters, + Enums, + EventPubSubService, + Formatters, type GridOption, + GroupTotalFormatters, type RowDetailView, SlickgridConfig, SlickgridVue, type SlickgridVueInstance, SlickRowDetailView, + SortComparers, + Utilities, }; + +export { BindingService }; \ No newline at end of file diff --git a/frameworks/slickgrid-vue/tsconfig.node.json b/frameworks/slickgrid-vue/tsconfig.node.json index abcd7f0da..915cf5274 100644 --- a/frameworks/slickgrid-vue/tsconfig.node.json +++ b/frameworks/slickgrid-vue/tsconfig.node.json @@ -20,5 +20,5 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, - "include": ["vite.config.ts"] + "include": ["vite.config.mts"] } diff --git a/frameworks/slickgrid-vue/vite.config.ts b/frameworks/slickgrid-vue/vite.config.mts similarity index 100% rename from frameworks/slickgrid-vue/vite.config.ts rename to frameworks/slickgrid-vue/vite.config.mts From c0b57451ee1604279d7dc7ea725ea9b327ae27a3 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Mon, 2 Dec 2024 22:30:57 -0500 Subject: [PATCH 07/36] chore: rollback universal dependencies in root package --- package.json | 17 ----------------- pnpm-lock.yaml | 51 -------------------------------------------------- 2 files changed, 68 deletions(-) diff --git a/package.json b/package.json index 6c33a0d79..756424e8f 100644 --- a/package.json +++ b/package.json @@ -71,23 +71,6 @@ "@lerna-lite/publish": "^3.10.1", "@lerna-lite/run": "^3.10.1", "@lerna-lite/watch": "^3.10.1", - "@slickgrid-universal/binding": "workspace:*", - "@slickgrid-universal/common": "workspace:*", - "@slickgrid-universal/composite-editor-component": "workspace:*", - "@slickgrid-universal/custom-footer-component": "workspace:*", - "@slickgrid-universal/custom-tooltip-plugin": "workspace:*", - "@slickgrid-universal/empty-warning-component": "workspace:*", - "@slickgrid-universal/event-pub-sub": "workspace:*", - "@slickgrid-universal/excel-export": "workspace:*", - "@slickgrid-universal/graphql": "workspace:*", - "@slickgrid-universal/odata": "workspace:*", - "@slickgrid-universal/pagination-component": "workspace:*", - "@slickgrid-universal/row-detail-view-plugin": "workspace:*", - "@slickgrid-universal/rxjs-observable": "workspace:*", - "@slickgrid-universal/text-export": "workspace:*", - "@slickgrid-universal/utils": "workspace:*", - "@slickgrid-universal/vanilla-bundle": "workspace:*", - "@slickgrid-universal/vanilla-force-bundle": "workspace:*", "@types/node": "^22.10.1", "@vitest/coverage-v8": "^2.1.6", "@vitest/eslint-plugin": "^1.1.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d68a4d611..261c70f0e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,57 +32,6 @@ importers: '@lerna-lite/watch': specifier: ^3.10.1 version: 3.10.1(@lerna-lite/publish@3.10.1)(@lerna-lite/run@3.10.1)(@types/node@22.10.1)(typescript@5.7.2) - '@slickgrid-universal/binding': - specifier: workspace:* - version: link:packages/binding - '@slickgrid-universal/common': - specifier: workspace:* - version: link:packages/common - '@slickgrid-universal/composite-editor-component': - specifier: workspace:* - version: link:packages/composite-editor-component - '@slickgrid-universal/custom-footer-component': - specifier: workspace:* - version: link:packages/custom-footer-component - '@slickgrid-universal/custom-tooltip-plugin': - specifier: workspace:* - version: link:packages/custom-tooltip-plugin - '@slickgrid-universal/empty-warning-component': - specifier: workspace:* - version: link:packages/empty-warning-component - '@slickgrid-universal/event-pub-sub': - specifier: workspace:* - version: link:packages/event-pub-sub - '@slickgrid-universal/excel-export': - specifier: workspace:* - version: link:packages/excel-export - '@slickgrid-universal/graphql': - specifier: workspace:* - version: link:packages/graphql - '@slickgrid-universal/odata': - specifier: workspace:* - version: link:packages/odata - '@slickgrid-universal/pagination-component': - specifier: workspace:* - version: link:packages/pagination-component - '@slickgrid-universal/row-detail-view-plugin': - specifier: workspace:* - version: link:packages/row-detail-view-plugin - '@slickgrid-universal/rxjs-observable': - specifier: workspace:* - version: link:packages/rxjs-observable - '@slickgrid-universal/text-export': - specifier: workspace:* - version: link:packages/text-export - '@slickgrid-universal/utils': - specifier: workspace:* - version: link:packages/utils - '@slickgrid-universal/vanilla-bundle': - specifier: workspace:* - version: link:packages/vanilla-bundle - '@slickgrid-universal/vanilla-force-bundle': - specifier: workspace:* - version: link:packages/vanilla-force-bundle '@types/node': specifier: ^22.10.1 version: 22.10.1 From a6d452ee807c2db5189f7ad337c38fe29ad47c40 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Mon, 2 Dec 2024 23:11:19 -0500 Subject: [PATCH 08/36] chore: try to use released universal version to see if it builds --- demos/vue/package.json | 20 +-- frameworks/slickgrid-vue/package.json | 16 +- pnpm-lock.yaml | 207 +++++++++++++++++++++----- 3 files changed, 191 insertions(+), 52 deletions(-) diff --git a/demos/vue/package.json b/demos/vue/package.json index 815b3df91..8ff0d13b5 100644 --- a/demos/vue/package.json +++ b/demos/vue/package.json @@ -19,23 +19,23 @@ "@fnando/sparkline": "^0.3.10", "@formkit/tempo": "^0.1.2", "@popperjs/core": "^2.11.8", - "@slickgrid-universal/common": "workspace:*", - "@slickgrid-universal/composite-editor-component": "workspace:*", - "@slickgrid-universal/custom-tooltip-plugin": "workspace:*", + "@slickgrid-universal/common": "~5.10.2", + "@slickgrid-universal/composite-editor-component": "~5.10.2", + "@slickgrid-universal/custom-tooltip-plugin": "~5.10.2", "@slickgrid-universal/event-pub-sub": "~5.10.2", - "@slickgrid-universal/excel-export": "workspace:*", - "@slickgrid-universal/graphql": "workspace:*", - "@slickgrid-universal/odata": "workspace:*", - "@slickgrid-universal/row-detail-view-plugin": "workspace:*", - "@slickgrid-universal/rxjs-observable": "workspace:*", - "@slickgrid-universal/text-export": "workspace:*", + "@slickgrid-universal/excel-export": "~5.10.2", + "@slickgrid-universal/graphql": "~5.10.2", + "@slickgrid-universal/odata": "~5.10.2", + "@slickgrid-universal/row-detail-view-plugin": "~5.10.2", + "@slickgrid-universal/rxjs-observable": "~5.10.2", + "@slickgrid-universal/text-export": "~5.10.2", "bootstrap": "^5.3.3", "dompurify": "^3.2.2", "i18next": "^24.0.2", "i18next-http-backend": "^3.0.1", "i18next-vue": "^5.0.0", "rxjs": "^7.8.1", - "slickgrid-vue": "workspace:*", + "slickgrid-vue": "0.1.1", "vue": "^3.5.13", "vue-router": "^4.5.0" }, diff --git a/frameworks/slickgrid-vue/package.json b/frameworks/slickgrid-vue/package.json index 66d7ce5af..7f5942bb6 100644 --- a/frameworks/slickgrid-vue/package.json +++ b/frameworks/slickgrid-vue/package.json @@ -1,6 +1,6 @@ { "name": "slickgrid-vue", - "version": "0.1.0", + "version": "0.1.1", "description": "Slickgrid-Vue", "type": "module", "main": "./dist/index.cjs", @@ -48,13 +48,13 @@ }, "dependencies": { "@formkit/tempo": "^0.1.2", - "@slickgrid-universal/common": "workspace:*", - "@slickgrid-universal/custom-footer-component": "workspace:*", - "@slickgrid-universal/empty-warning-component": "workspace:*", - "@slickgrid-universal/event-pub-sub": "workspace:*", - "@slickgrid-universal/pagination-component": "workspace:*", - "@slickgrid-universal/row-detail-view-plugin": "workspace:*", - "@slickgrid-universal/utils": "workspace:*", + "@slickgrid-universal/common": "^5.10.2", + "@slickgrid-universal/custom-footer-component": "^5.10.2", + "@slickgrid-universal/empty-warning-component": "^5.10.2", + "@slickgrid-universal/event-pub-sub": "^5.10.2", + "@slickgrid-universal/pagination-component": "^5.10.2", + "@slickgrid-universal/row-detail-view-plugin": "^5.10.2", + "@slickgrid-universal/utils": "^5.10.2", "dequal": "^2.0.3", "i18next": "^24.0.2", "i18next-vue": "^5.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 261c70f0e..dda493741 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -129,35 +129,35 @@ importers: specifier: ^2.11.8 version: 2.11.8 '@slickgrid-universal/common': - specifier: workspace:* - version: link:../../packages/common + specifier: ~5.10.2 + version: 5.10.2 '@slickgrid-universal/composite-editor-component': - specifier: workspace:* - version: link:../../packages/composite-editor-component + specifier: ~5.10.2 + version: 5.10.2 '@slickgrid-universal/custom-tooltip-plugin': - specifier: workspace:* - version: link:../../packages/custom-tooltip-plugin + specifier: ~5.10.2 + version: 5.10.2 '@slickgrid-universal/event-pub-sub': specifier: ~5.10.2 version: 5.10.2 '@slickgrid-universal/excel-export': - specifier: workspace:* - version: link:../../packages/excel-export + specifier: ~5.10.2 + version: 5.10.2 '@slickgrid-universal/graphql': - specifier: workspace:* - version: link:../../packages/graphql + specifier: ~5.10.2 + version: 5.10.2 '@slickgrid-universal/odata': - specifier: workspace:* - version: link:../../packages/odata + specifier: ~5.10.2 + version: 5.10.2 '@slickgrid-universal/row-detail-view-plugin': - specifier: workspace:* - version: link:../../packages/row-detail-view-plugin + specifier: ~5.10.2 + version: 5.10.2 '@slickgrid-universal/rxjs-observable': - specifier: workspace:* - version: link:../../packages/rxjs-observable + specifier: ~5.10.2 + version: 5.10.2 '@slickgrid-universal/text-export': - specifier: workspace:* - version: link:../../packages/text-export + specifier: ~5.10.2 + version: 5.10.2 bootstrap: specifier: ^5.3.3 version: 5.3.3(@popperjs/core@2.11.8) @@ -177,8 +177,8 @@ importers: specifier: ^7.8.1 version: 7.8.1 slickgrid-vue: - specifier: workspace:* - version: link:../../frameworks/slickgrid-vue + specifier: 0.1.1 + version: 0.1.1(typescript@5.6.3)(vue@3.5.13(typescript@5.6.3)) vue: specifier: ^3.5.13 version: 3.5.13(typescript@5.6.3) @@ -308,26 +308,26 @@ importers: specifier: ^0.1.2 version: 0.1.2 '@slickgrid-universal/common': - specifier: workspace:* - version: link:../../packages/common + specifier: ^5.10.2 + version: 5.10.2 '@slickgrid-universal/custom-footer-component': - specifier: workspace:* - version: link:../../packages/custom-footer-component + specifier: ^5.10.2 + version: 5.10.2 '@slickgrid-universal/empty-warning-component': - specifier: workspace:* - version: link:../../packages/empty-warning-component + specifier: ^5.10.2 + version: 5.10.2 '@slickgrid-universal/event-pub-sub': - specifier: workspace:* - version: link:../../packages/event-pub-sub + specifier: ^5.10.2 + version: 5.10.2 '@slickgrid-universal/pagination-component': - specifier: workspace:* - version: link:../../packages/pagination-component + specifier: ^5.10.2 + version: 5.10.2 '@slickgrid-universal/row-detail-view-plugin': - specifier: workspace:* - version: link:../../packages/row-detail-view-plugin + specifier: ^5.10.2 + version: 5.10.2 '@slickgrid-universal/utils': - specifier: workspace:* - version: link:../../packages/utils + specifier: ^5.10.2 + version: 5.10.2 dequal: specifier: ^2.0.3 version: 2.0.3 @@ -1492,9 +1492,49 @@ packages: resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} engines: {node: '>=18'} + '@slickgrid-universal/binding@5.10.2': + resolution: {integrity: sha512-Hfn6ooFE28W9JOCLAOPDqlnrBJ1bfIkkXmV04bToCZk3SyKhUr6uYkmJMaMzs3S4U3a2aEe7srILPpLjfOmfmw==} + + '@slickgrid-universal/common@5.10.2': + resolution: {integrity: sha512-yE6yw+23xC8LcA5+aPkcF/mexKFzX7qgWl4iGkDPDTA3RQaEYVWiv25XQWjwomti9CH9pnsBTRjLduEDm2NDDw==} + engines: {node: ^18.0.0 || >=20.0.0} + + '@slickgrid-universal/composite-editor-component@5.10.2': + resolution: {integrity: sha512-7pi3AmB2GiQzA0GHJnu+bp+sfB7i51Td2fUNrLlxxSrJu5YpoHhMye52MFKA6Tv/fTnePvnOBEyml+psrjIdLQ==} + + '@slickgrid-universal/custom-footer-component@5.10.2': + resolution: {integrity: sha512-MgQr7N6gzs6pomrzBpvCxptS+faq4rs5WNITppw/uHa/bjprJltr80/dzvxlsGQHsug+ikspOuK8lxkKESxC/A==} + + '@slickgrid-universal/custom-tooltip-plugin@5.10.2': + resolution: {integrity: sha512-xzN1fqAH5ma8vnmHIITGbx1C57famagvTZbW60Lh+Nqpd+WfseEAXSG9iwBGnZg7irdZLNzIwC6TadbYfNGTZQ==} + + '@slickgrid-universal/empty-warning-component@5.10.2': + resolution: {integrity: sha512-LSXzWcyypK6xirZZDTrKIxY2HS0jCr90CTL7V4c9Vb1B8Nv0CGte4L686cwNxg6MK8oT9SLgMqLgtjSSa7g1Dg==} + '@slickgrid-universal/event-pub-sub@5.10.2': resolution: {integrity: sha512-3l0rAZEf2CX2ApaXv4VCFVlExJSTu6VjDiWsNuZ9dh7+auWtiihsX4QZS8zskQIqEVrHtuJAlV8fq/l/XcwS+g==} + '@slickgrid-universal/excel-export@5.10.2': + resolution: {integrity: sha512-x6YtYpTdJcI9u98B4xU25L+DzVvOvk/8W63/qerLcap0hUUTjcTOotamglcGL41WI67UJ8vKAJEDLtErm8hQ+Q==} + + '@slickgrid-universal/graphql@5.10.2': + resolution: {integrity: sha512-06zVCdL3fWmtXkccVVUtvSmwcNgsJ4JASxUk0hLWRJ8mRR0MtU4AWRRynND8tlxbDSo/VWPkPxrx3tcbBCROrA==} + + '@slickgrid-universal/odata@5.10.2': + resolution: {integrity: sha512-yfWc2VJ7pwjTPG56cRefeDALHkxQSkMJ8YP6eq7ZWqg14/m30pCAkq2w7N15JMokw63+G2CXwP/KU2xFy/AH4g==} + + '@slickgrid-universal/pagination-component@5.10.2': + resolution: {integrity: sha512-rhspG1Lh5+ZUl96609p+NooDbAdzMMpkFsXxNW9jJWV9MnLQfg78FNDBG4zg/mLuVMW2SqNQ5p+N9TJS9TWbZg==} + + '@slickgrid-universal/row-detail-view-plugin@5.10.2': + resolution: {integrity: sha512-XKwaHun1hhShguadz9OgzxvqFKkUFHdA1rEFJXC5t60cuTa4J0j7xWe9HWrNcu4d+ywmXvhWZWW4QPyXEb7f7g==} + + '@slickgrid-universal/rxjs-observable@5.10.2': + resolution: {integrity: sha512-4D/XvaWYCj7kL8a2SNQaSIG99W7dzb/qjo4rYv6M3YfqMI22LxkcRW53gBBTl48QuGPM7W4uEH7Y4qqb5rxH8g==} + + '@slickgrid-universal/text-export@5.10.2': + resolution: {integrity: sha512-H+pyyiwArkBfFhMqPpSzqIzADlIZLkJuyQFVlSsxsegmTa9wHp9qGL+Y4ugfs77oQXZ5isRaUS53TW6ByRjtIg==} + '@slickgrid-universal/utils@5.10.2': resolution: {integrity: sha512-cijV2/u3xKnfdUinaJeQNcoZuLy+9J4EgYSfUpNX22kanp948uKOICQxVrlb7Lj82Ki0U+vtuQ+fS59KQD3YOQ==} @@ -4258,6 +4298,11 @@ packages: resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} engines: {node: '>=10'} + slickgrid-vue@0.1.1: + resolution: {integrity: sha512-TSmNFeie7yZ5UsVw1+DhZ8HTouy8lDITp8cSb7Etb4Qbx5pmdfIm+4n2gh7KRNhgf7Z2Z4IJI1mA0mWUApGrcw==} + peerDependencies: + vue: '>=3.4.0' + smart-buffer@4.2.0: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} @@ -6001,10 +6046,86 @@ snapshots: '@sindresorhus/merge-streams@2.3.0': {} + '@slickgrid-universal/binding@5.10.2': {} + + '@slickgrid-universal/common@5.10.2': + dependencies: + '@excel-builder-vanilla/types': 3.0.14 + '@formkit/tempo': 0.1.2 + '@slickgrid-universal/binding': 5.10.2 + '@slickgrid-universal/event-pub-sub': 5.10.2 + '@slickgrid-universal/utils': 5.10.2 + '@types/sortablejs': 1.15.8 + '@types/trusted-types': 2.0.7 + autocompleter: 9.3.2 + dequal: 2.0.3 + multiple-select-vanilla: 3.4.4 + sortablejs: 1.15.6 + un-flatten-tree: 2.0.12 + vanilla-calendar-pro: 2.9.10 + + '@slickgrid-universal/composite-editor-component@5.10.2': + dependencies: + '@slickgrid-universal/binding': 5.10.2 + '@slickgrid-universal/common': 5.10.2 + '@slickgrid-universal/utils': 5.10.2 + + '@slickgrid-universal/custom-footer-component@5.10.2': + dependencies: + '@formkit/tempo': 0.1.2 + '@slickgrid-universal/binding': 5.10.2 + '@slickgrid-universal/common': 5.10.2 + + '@slickgrid-universal/custom-tooltip-plugin@5.10.2': + dependencies: + '@slickgrid-universal/common': 5.10.2 + '@slickgrid-universal/utils': 5.10.2 + + '@slickgrid-universal/empty-warning-component@5.10.2': + dependencies: + '@slickgrid-universal/common': 5.10.2 + '@slickgrid-universal/event-pub-sub@5.10.2': dependencies: '@slickgrid-universal/utils': 5.10.2 + '@slickgrid-universal/excel-export@5.10.2': + dependencies: + '@slickgrid-universal/common': 5.10.2 + '@slickgrid-universal/utils': 5.10.2 + excel-builder-vanilla: 3.0.14 + + '@slickgrid-universal/graphql@5.10.2': + dependencies: + '@slickgrid-universal/common': 5.10.2 + '@slickgrid-universal/utils': 5.10.2 + + '@slickgrid-universal/odata@5.10.2': + dependencies: + '@slickgrid-universal/common': 5.10.2 + '@slickgrid-universal/utils': 5.10.2 + + '@slickgrid-universal/pagination-component@5.10.2': + dependencies: + '@slickgrid-universal/binding': 5.10.2 + '@slickgrid-universal/common': 5.10.2 + + '@slickgrid-universal/row-detail-view-plugin@5.10.2': + dependencies: + '@slickgrid-universal/common': 5.10.2 + '@slickgrid-universal/utils': 5.10.2 + + '@slickgrid-universal/rxjs-observable@5.10.2': + dependencies: + '@slickgrid-universal/common': 5.10.2 + rxjs: 7.8.1 + + '@slickgrid-universal/text-export@5.10.2': + dependencies: + '@slickgrid-universal/common': 5.10.2 + '@slickgrid-universal/utils': 5.10.2 + text-encoding-utf-8: 1.0.2 + '@slickgrid-universal/utils@5.10.2': {} '@trysound/sax@0.2.0': {} @@ -8919,6 +9040,24 @@ snapshots: astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 + slickgrid-vue@0.1.1(typescript@5.6.3)(vue@3.5.13(typescript@5.6.3)): + dependencies: + '@formkit/tempo': 0.1.2 + '@slickgrid-universal/common': 5.10.2 + '@slickgrid-universal/custom-footer-component': 5.10.2 + '@slickgrid-universal/empty-warning-component': 5.10.2 + '@slickgrid-universal/event-pub-sub': 5.10.2 + '@slickgrid-universal/pagination-component': 5.10.2 + '@slickgrid-universal/row-detail-view-plugin': 5.10.2 + '@slickgrid-universal/utils': 5.10.2 + dequal: 2.0.3 + i18next: 24.0.2(typescript@5.6.3) + i18next-vue: 5.0.0(i18next@24.0.2(typescript@5.6.3))(vue@3.5.13(typescript@5.6.3)) + sortablejs: 1.15.6 + vue: 3.5.13(typescript@5.6.3) + transitivePeerDependencies: + - typescript + smart-buffer@4.2.0: {} socks-proxy-agent@8.0.4: From 03deab4efc48644d07d2ee59b6e5f49626daaad5 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Mon, 2 Dec 2024 23:16:57 -0500 Subject: [PATCH 09/36] chore: add back workspace in slickgrid-vue --- .github/workflows/vue-cypress.yml | 2 +- frameworks/slickgrid-vue/package.json | 14 +++++++------- pnpm-lock.yaml | 28 +++++++++++++-------------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/vue-cypress.yml b/.github/workflows/vue-cypress.yml index e18da9d20..cbf99d389 100644 --- a/.github/workflows/vue-cypress.yml +++ b/.github/workflows/vue-cypress.yml @@ -77,7 +77,7 @@ jobs: with: install: false # working-directory: packages/dnd - start: pnpm vue:serve + # start: pnpm vue:serve # start: pnpm serve:vite wait-on: 'http://localhost:7000' config-file: test/cypress.config.ts diff --git a/frameworks/slickgrid-vue/package.json b/frameworks/slickgrid-vue/package.json index 7f5942bb6..345fc3b04 100644 --- a/frameworks/slickgrid-vue/package.json +++ b/frameworks/slickgrid-vue/package.json @@ -48,13 +48,13 @@ }, "dependencies": { "@formkit/tempo": "^0.1.2", - "@slickgrid-universal/common": "^5.10.2", - "@slickgrid-universal/custom-footer-component": "^5.10.2", - "@slickgrid-universal/empty-warning-component": "^5.10.2", - "@slickgrid-universal/event-pub-sub": "^5.10.2", - "@slickgrid-universal/pagination-component": "^5.10.2", - "@slickgrid-universal/row-detail-view-plugin": "^5.10.2", - "@slickgrid-universal/utils": "^5.10.2", + "@slickgrid-universal/common": "workspace:*", + "@slickgrid-universal/custom-footer-component": "workspace:*", + "@slickgrid-universal/empty-warning-component": "workspace:*", + "@slickgrid-universal/event-pub-sub": "workspace:*", + "@slickgrid-universal/pagination-component": "workspace:*", + "@slickgrid-universal/row-detail-view-plugin": "workspace:*", + "@slickgrid-universal/utils": "workspace:*", "dequal": "^2.0.3", "i18next": "^24.0.2", "i18next-vue": "^5.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dda493741..84e172e7b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -308,26 +308,26 @@ importers: specifier: ^0.1.2 version: 0.1.2 '@slickgrid-universal/common': - specifier: ^5.10.2 - version: 5.10.2 + specifier: workspace:* + version: link:../../packages/common '@slickgrid-universal/custom-footer-component': - specifier: ^5.10.2 - version: 5.10.2 + specifier: workspace:* + version: link:../../packages/custom-footer-component '@slickgrid-universal/empty-warning-component': - specifier: ^5.10.2 - version: 5.10.2 + specifier: workspace:* + version: link:../../packages/empty-warning-component '@slickgrid-universal/event-pub-sub': - specifier: ^5.10.2 - version: 5.10.2 + specifier: workspace:* + version: link:../../packages/event-pub-sub '@slickgrid-universal/pagination-component': - specifier: ^5.10.2 - version: 5.10.2 + specifier: workspace:* + version: link:../../packages/pagination-component '@slickgrid-universal/row-detail-view-plugin': - specifier: ^5.10.2 - version: 5.10.2 + specifier: workspace:* + version: link:../../packages/row-detail-view-plugin '@slickgrid-universal/utils': - specifier: ^5.10.2 - version: 5.10.2 + specifier: workspace:* + version: link:../../packages/utils dequal: specifier: ^2.0.3 version: 2.0.3 From 7bdf68aecdca24742a935fadb1deab22df4f065c Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Mon, 2 Dec 2024 23:22:34 -0500 Subject: [PATCH 10/36] chore: use Vue Cypress config not the vanilla one --- .github/workflows/vue-cypress.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/vue-cypress.yml b/.github/workflows/vue-cypress.yml index cbf99d389..f799ea18b 100644 --- a/.github/workflows/vue-cypress.yml +++ b/.github/workflows/vue-cypress.yml @@ -80,7 +80,7 @@ jobs: # start: pnpm vue:serve # start: pnpm serve:vite wait-on: 'http://localhost:7000' - config-file: test/cypress.config.ts + config-file: demos/vue/test/cypress.config.ts browser: chrome record: true env: From 879b7e6603c2c9b559035d698044e835e95e2446 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Mon, 2 Dec 2024 23:33:15 -0500 Subject: [PATCH 11/36] chore: use correct Cypress config filename --- .github/workflows/vue-cypress.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/vue-cypress.yml b/.github/workflows/vue-cypress.yml index f799ea18b..f170dbf8d 100644 --- a/.github/workflows/vue-cypress.yml +++ b/.github/workflows/vue-cypress.yml @@ -80,7 +80,7 @@ jobs: # start: pnpm vue:serve # start: pnpm serve:vite wait-on: 'http://localhost:7000' - config-file: demos/vue/test/cypress.config.ts + config-file: demos/vue/test/cypress.config.mjs browser: chrome record: true env: From 62f0c128cf6092278e3f5b9b653c6965218bba27 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Mon, 2 Dec 2024 23:36:18 -0500 Subject: [PATCH 12/36] chore: remove Cypress project recording key --- demos/vue/test/cypress.config.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/demos/vue/test/cypress.config.mjs b/demos/vue/test/cypress.config.mjs index c59a506f1..aeb73b04b 100644 --- a/demos/vue/test/cypress.config.mjs +++ b/demos/vue/test/cypress.config.mjs @@ -1,7 +1,6 @@ import { defineConfig } from 'cypress'; export default defineConfig({ - projectId: 'gtbpy4', video: false, viewportWidth: 1200, viewportHeight: 1020, From 22af5e1750d2ec4a41e2b687a969206c9eef066d Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Mon, 2 Dec 2024 23:38:15 -0500 Subject: [PATCH 13/36] chore: disable Cypress recording --- .github/workflows/vue-cypress.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/vue-cypress.yml b/.github/workflows/vue-cypress.yml index f170dbf8d..1bc110c87 100644 --- a/.github/workflows/vue-cypress.yml +++ b/.github/workflows/vue-cypress.yml @@ -82,7 +82,7 @@ jobs: wait-on: 'http://localhost:7000' config-file: demos/vue/test/cypress.config.mjs browser: chrome - record: true + record: false env: # pass the Dashboard record key as an environment variable CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} From 7fbc51c79aebcc55b416f7e0098f6f7e7a8e6488 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Mon, 2 Dec 2024 23:48:33 -0500 Subject: [PATCH 14/36] chore: servor to serve html --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 756424e8f..980f0fd26 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "vue:build": "pnpm -r --stream --filter=./demos/vue/** run build", "vue:dev": "pnpm -r --stream --filter=./demos/vue/** run dev", "vue:cypress": "pnpm -r --stream --filter=./demos/vue/** run cypress", - "vue:serve": "pnpm -r --stream --filter=./demos/vue/** run preview" + "vue:serve": "pnpm -r --stream --filter=./demos/vue/** run serve:demo" }, "comments": { "new-version": "To create a new version with Lerna-Lite, simply run the following script (1) 'roll-new-release'.", From 96f6555157e19e830ee2a2f5b38ae1914c6fc18c Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Mon, 2 Dec 2024 23:49:26 -0500 Subject: [PATCH 15/36] chore: use http base path --- demos/vue/test/cypress.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/vue/test/cypress.config.mjs b/demos/vue/test/cypress.config.mjs index aeb73b04b..d82101a73 100644 --- a/demos/vue/test/cypress.config.mjs +++ b/demos/vue/test/cypress.config.mjs @@ -21,7 +21,7 @@ export default defineConfig({ runMode: true, // run in CI }, e2e: { - baseUrl: 'http://localhost:7000/#', + baseUrl: 'http://localhost:7000', experimentalRunAllSpecs: true, supportFile: 'test/cypress/support/index.ts', specPattern: 'test/cypress/e2e/**/*.cy.ts', From 13e17d1f1ec38dcfdf3827ee287cd82b39d61937 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Mon, 2 Dec 2024 23:58:38 -0500 Subject: [PATCH 16/36] chore: add back universal workspace --- .github/workflows/vue-cypress.yml | 4 +- demos/vue/package.json | 20 +++---- pnpm-lock.yaml | 99 +++++++------------------------ 3 files changed, 32 insertions(+), 91 deletions(-) diff --git a/.github/workflows/vue-cypress.yml b/.github/workflows/vue-cypress.yml index 1bc110c87..a3abe591e 100644 --- a/.github/workflows/vue-cypress.yml +++ b/.github/workflows/vue-cypress.yml @@ -76,11 +76,11 @@ jobs: uses: cypress-io/github-action@v6 with: install: false - # working-directory: packages/dnd + working-directory: demos/vue # start: pnpm vue:serve # start: pnpm serve:vite wait-on: 'http://localhost:7000' - config-file: demos/vue/test/cypress.config.mjs + config-file: test/cypress.config.mjs browser: chrome record: false env: diff --git a/demos/vue/package.json b/demos/vue/package.json index 8ff0d13b5..a20cd84d7 100644 --- a/demos/vue/package.json +++ b/demos/vue/package.json @@ -19,16 +19,16 @@ "@fnando/sparkline": "^0.3.10", "@formkit/tempo": "^0.1.2", "@popperjs/core": "^2.11.8", - "@slickgrid-universal/common": "~5.10.2", - "@slickgrid-universal/composite-editor-component": "~5.10.2", - "@slickgrid-universal/custom-tooltip-plugin": "~5.10.2", - "@slickgrid-universal/event-pub-sub": "~5.10.2", - "@slickgrid-universal/excel-export": "~5.10.2", - "@slickgrid-universal/graphql": "~5.10.2", - "@slickgrid-universal/odata": "~5.10.2", - "@slickgrid-universal/row-detail-view-plugin": "~5.10.2", - "@slickgrid-universal/rxjs-observable": "~5.10.2", - "@slickgrid-universal/text-export": "~5.10.2", + "@slickgrid-universal/common": "workspace:*", + "@slickgrid-universal/composite-editor-component": "workspace:*", + "@slickgrid-universal/custom-tooltip-plugin": "workspace:*", + "@slickgrid-universal/event-pub-sub": "workspace:*", + "@slickgrid-universal/excel-export": "workspace:*", + "@slickgrid-universal/graphql": "workspace:*", + "@slickgrid-universal/odata": "workspace:*", + "@slickgrid-universal/row-detail-view-plugin": "workspace:*", + "@slickgrid-universal/rxjs-observable": "workspace:*", + "@slickgrid-universal/text-export": "workspace:*", "bootstrap": "^5.3.3", "dompurify": "^3.2.2", "i18next": "^24.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 84e172e7b..a63b04f53 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -129,35 +129,35 @@ importers: specifier: ^2.11.8 version: 2.11.8 '@slickgrid-universal/common': - specifier: ~5.10.2 - version: 5.10.2 + specifier: workspace:* + version: link:../../packages/common '@slickgrid-universal/composite-editor-component': - specifier: ~5.10.2 - version: 5.10.2 + specifier: workspace:* + version: link:../../packages/composite-editor-component '@slickgrid-universal/custom-tooltip-plugin': - specifier: ~5.10.2 - version: 5.10.2 + specifier: workspace:* + version: link:../../packages/custom-tooltip-plugin '@slickgrid-universal/event-pub-sub': - specifier: ~5.10.2 - version: 5.10.2 + specifier: workspace:* + version: link:../../packages/event-pub-sub '@slickgrid-universal/excel-export': - specifier: ~5.10.2 - version: 5.10.2 + specifier: workspace:* + version: link:../../packages/excel-export '@slickgrid-universal/graphql': - specifier: ~5.10.2 - version: 5.10.2 + specifier: workspace:* + version: link:../../packages/graphql '@slickgrid-universal/odata': - specifier: ~5.10.2 - version: 5.10.2 + specifier: workspace:* + version: link:../../packages/odata '@slickgrid-universal/row-detail-view-plugin': - specifier: ~5.10.2 - version: 5.10.2 + specifier: workspace:* + version: link:../../packages/row-detail-view-plugin '@slickgrid-universal/rxjs-observable': - specifier: ~5.10.2 - version: 5.10.2 + specifier: workspace:* + version: link:../../packages/rxjs-observable '@slickgrid-universal/text-export': - specifier: ~5.10.2 - version: 5.10.2 + specifier: workspace:* + version: link:../../packages/text-export bootstrap: specifier: ^5.3.3 version: 5.3.3(@popperjs/core@2.11.8) @@ -1499,42 +1499,21 @@ packages: resolution: {integrity: sha512-yE6yw+23xC8LcA5+aPkcF/mexKFzX7qgWl4iGkDPDTA3RQaEYVWiv25XQWjwomti9CH9pnsBTRjLduEDm2NDDw==} engines: {node: ^18.0.0 || >=20.0.0} - '@slickgrid-universal/composite-editor-component@5.10.2': - resolution: {integrity: sha512-7pi3AmB2GiQzA0GHJnu+bp+sfB7i51Td2fUNrLlxxSrJu5YpoHhMye52MFKA6Tv/fTnePvnOBEyml+psrjIdLQ==} - '@slickgrid-universal/custom-footer-component@5.10.2': resolution: {integrity: sha512-MgQr7N6gzs6pomrzBpvCxptS+faq4rs5WNITppw/uHa/bjprJltr80/dzvxlsGQHsug+ikspOuK8lxkKESxC/A==} - '@slickgrid-universal/custom-tooltip-plugin@5.10.2': - resolution: {integrity: sha512-xzN1fqAH5ma8vnmHIITGbx1C57famagvTZbW60Lh+Nqpd+WfseEAXSG9iwBGnZg7irdZLNzIwC6TadbYfNGTZQ==} - '@slickgrid-universal/empty-warning-component@5.10.2': resolution: {integrity: sha512-LSXzWcyypK6xirZZDTrKIxY2HS0jCr90CTL7V4c9Vb1B8Nv0CGte4L686cwNxg6MK8oT9SLgMqLgtjSSa7g1Dg==} '@slickgrid-universal/event-pub-sub@5.10.2': resolution: {integrity: sha512-3l0rAZEf2CX2ApaXv4VCFVlExJSTu6VjDiWsNuZ9dh7+auWtiihsX4QZS8zskQIqEVrHtuJAlV8fq/l/XcwS+g==} - '@slickgrid-universal/excel-export@5.10.2': - resolution: {integrity: sha512-x6YtYpTdJcI9u98B4xU25L+DzVvOvk/8W63/qerLcap0hUUTjcTOotamglcGL41WI67UJ8vKAJEDLtErm8hQ+Q==} - - '@slickgrid-universal/graphql@5.10.2': - resolution: {integrity: sha512-06zVCdL3fWmtXkccVVUtvSmwcNgsJ4JASxUk0hLWRJ8mRR0MtU4AWRRynND8tlxbDSo/VWPkPxrx3tcbBCROrA==} - - '@slickgrid-universal/odata@5.10.2': - resolution: {integrity: sha512-yfWc2VJ7pwjTPG56cRefeDALHkxQSkMJ8YP6eq7ZWqg14/m30pCAkq2w7N15JMokw63+G2CXwP/KU2xFy/AH4g==} - '@slickgrid-universal/pagination-component@5.10.2': resolution: {integrity: sha512-rhspG1Lh5+ZUl96609p+NooDbAdzMMpkFsXxNW9jJWV9MnLQfg78FNDBG4zg/mLuVMW2SqNQ5p+N9TJS9TWbZg==} '@slickgrid-universal/row-detail-view-plugin@5.10.2': resolution: {integrity: sha512-XKwaHun1hhShguadz9OgzxvqFKkUFHdA1rEFJXC5t60cuTa4J0j7xWe9HWrNcu4d+ywmXvhWZWW4QPyXEb7f7g==} - '@slickgrid-universal/rxjs-observable@5.10.2': - resolution: {integrity: sha512-4D/XvaWYCj7kL8a2SNQaSIG99W7dzb/qjo4rYv6M3YfqMI22LxkcRW53gBBTl48QuGPM7W4uEH7Y4qqb5rxH8g==} - - '@slickgrid-universal/text-export@5.10.2': - resolution: {integrity: sha512-H+pyyiwArkBfFhMqPpSzqIzADlIZLkJuyQFVlSsxsegmTa9wHp9qGL+Y4ugfs77oQXZ5isRaUS53TW6ByRjtIg==} - '@slickgrid-universal/utils@5.10.2': resolution: {integrity: sha512-cijV2/u3xKnfdUinaJeQNcoZuLy+9J4EgYSfUpNX22kanp948uKOICQxVrlb7Lj82Ki0U+vtuQ+fS59KQD3YOQ==} @@ -6064,23 +6043,12 @@ snapshots: un-flatten-tree: 2.0.12 vanilla-calendar-pro: 2.9.10 - '@slickgrid-universal/composite-editor-component@5.10.2': - dependencies: - '@slickgrid-universal/binding': 5.10.2 - '@slickgrid-universal/common': 5.10.2 - '@slickgrid-universal/utils': 5.10.2 - '@slickgrid-universal/custom-footer-component@5.10.2': dependencies: '@formkit/tempo': 0.1.2 '@slickgrid-universal/binding': 5.10.2 '@slickgrid-universal/common': 5.10.2 - '@slickgrid-universal/custom-tooltip-plugin@5.10.2': - dependencies: - '@slickgrid-universal/common': 5.10.2 - '@slickgrid-universal/utils': 5.10.2 - '@slickgrid-universal/empty-warning-component@5.10.2': dependencies: '@slickgrid-universal/common': 5.10.2 @@ -6089,22 +6057,6 @@ snapshots: dependencies: '@slickgrid-universal/utils': 5.10.2 - '@slickgrid-universal/excel-export@5.10.2': - dependencies: - '@slickgrid-universal/common': 5.10.2 - '@slickgrid-universal/utils': 5.10.2 - excel-builder-vanilla: 3.0.14 - - '@slickgrid-universal/graphql@5.10.2': - dependencies: - '@slickgrid-universal/common': 5.10.2 - '@slickgrid-universal/utils': 5.10.2 - - '@slickgrid-universal/odata@5.10.2': - dependencies: - '@slickgrid-universal/common': 5.10.2 - '@slickgrid-universal/utils': 5.10.2 - '@slickgrid-universal/pagination-component@5.10.2': dependencies: '@slickgrid-universal/binding': 5.10.2 @@ -6115,17 +6067,6 @@ snapshots: '@slickgrid-universal/common': 5.10.2 '@slickgrid-universal/utils': 5.10.2 - '@slickgrid-universal/rxjs-observable@5.10.2': - dependencies: - '@slickgrid-universal/common': 5.10.2 - rxjs: 7.8.1 - - '@slickgrid-universal/text-export@5.10.2': - dependencies: - '@slickgrid-universal/common': 5.10.2 - '@slickgrid-universal/utils': 5.10.2 - text-encoding-utf-8: 1.0.2 - '@slickgrid-universal/utils@5.10.2': {} '@trysound/sax@0.2.0': {} From 43f8fd141a047383ff146128be567735a55f7730 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Tue, 3 Dec 2024 00:04:48 -0500 Subject: [PATCH 17/36] chore: add back hashtag in Cypress base path --- demos/vue/test/cypress.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/vue/test/cypress.config.mjs b/demos/vue/test/cypress.config.mjs index d82101a73..aeb73b04b 100644 --- a/demos/vue/test/cypress.config.mjs +++ b/demos/vue/test/cypress.config.mjs @@ -21,7 +21,7 @@ export default defineConfig({ runMode: true, // run in CI }, e2e: { - baseUrl: 'http://localhost:7000', + baseUrl: 'http://localhost:7000/#', experimentalRunAllSpecs: true, supportFile: 'test/cypress/support/index.ts', specPattern: 'test/cypress/e2e/**/*.cy.ts', From 5eb27bdfe64e25f4791281116d84ffbbc59a2c0c Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Tue, 3 Dec 2024 00:18:14 -0500 Subject: [PATCH 18/36] chore: add back local dep workspace --- demos/vue/package.json | 2 +- demos/vue/tsconfig.json | 6 ++- pnpm-lock.yaml | 96 +---------------------------------------- 3 files changed, 8 insertions(+), 96 deletions(-) diff --git a/demos/vue/package.json b/demos/vue/package.json index a20cd84d7..6aa2142f3 100644 --- a/demos/vue/package.json +++ b/demos/vue/package.json @@ -35,7 +35,7 @@ "i18next-http-backend": "^3.0.1", "i18next-vue": "^5.0.0", "rxjs": "^7.8.1", - "slickgrid-vue": "0.1.1", + "slickgrid-vue": "workspace:*", "vue": "^3.5.13", "vue-router": "^4.5.0" }, diff --git a/demos/vue/tsconfig.json b/demos/vue/tsconfig.json index d32ff6820..81812d102 100644 --- a/demos/vue/tsconfig.json +++ b/demos/vue/tsconfig.json @@ -1,4 +1,8 @@ { "files": [], - "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }] + "references": [ + { "path": "../../frameworks/vue" }, + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a63b04f53..7c7d7e142 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -177,8 +177,8 @@ importers: specifier: ^7.8.1 version: 7.8.1 slickgrid-vue: - specifier: 0.1.1 - version: 0.1.1(typescript@5.6.3)(vue@3.5.13(typescript@5.6.3)) + specifier: workspace:* + version: link:../../frameworks/slickgrid-vue vue: specifier: ^3.5.13 version: 3.5.13(typescript@5.6.3) @@ -1492,31 +1492,6 @@ packages: resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} engines: {node: '>=18'} - '@slickgrid-universal/binding@5.10.2': - resolution: {integrity: sha512-Hfn6ooFE28W9JOCLAOPDqlnrBJ1bfIkkXmV04bToCZk3SyKhUr6uYkmJMaMzs3S4U3a2aEe7srILPpLjfOmfmw==} - - '@slickgrid-universal/common@5.10.2': - resolution: {integrity: sha512-yE6yw+23xC8LcA5+aPkcF/mexKFzX7qgWl4iGkDPDTA3RQaEYVWiv25XQWjwomti9CH9pnsBTRjLduEDm2NDDw==} - engines: {node: ^18.0.0 || >=20.0.0} - - '@slickgrid-universal/custom-footer-component@5.10.2': - resolution: {integrity: sha512-MgQr7N6gzs6pomrzBpvCxptS+faq4rs5WNITppw/uHa/bjprJltr80/dzvxlsGQHsug+ikspOuK8lxkKESxC/A==} - - '@slickgrid-universal/empty-warning-component@5.10.2': - resolution: {integrity: sha512-LSXzWcyypK6xirZZDTrKIxY2HS0jCr90CTL7V4c9Vb1B8Nv0CGte4L686cwNxg6MK8oT9SLgMqLgtjSSa7g1Dg==} - - '@slickgrid-universal/event-pub-sub@5.10.2': - resolution: {integrity: sha512-3l0rAZEf2CX2ApaXv4VCFVlExJSTu6VjDiWsNuZ9dh7+auWtiihsX4QZS8zskQIqEVrHtuJAlV8fq/l/XcwS+g==} - - '@slickgrid-universal/pagination-component@5.10.2': - resolution: {integrity: sha512-rhspG1Lh5+ZUl96609p+NooDbAdzMMpkFsXxNW9jJWV9MnLQfg78FNDBG4zg/mLuVMW2SqNQ5p+N9TJS9TWbZg==} - - '@slickgrid-universal/row-detail-view-plugin@5.10.2': - resolution: {integrity: sha512-XKwaHun1hhShguadz9OgzxvqFKkUFHdA1rEFJXC5t60cuTa4J0j7xWe9HWrNcu4d+ywmXvhWZWW4QPyXEb7f7g==} - - '@slickgrid-universal/utils@5.10.2': - resolution: {integrity: sha512-cijV2/u3xKnfdUinaJeQNcoZuLy+9J4EgYSfUpNX22kanp948uKOICQxVrlb7Lj82Ki0U+vtuQ+fS59KQD3YOQ==} - '@trysound/sax@0.2.0': resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} @@ -4277,11 +4252,6 @@ packages: resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} engines: {node: '>=10'} - slickgrid-vue@0.1.1: - resolution: {integrity: sha512-TSmNFeie7yZ5UsVw1+DhZ8HTouy8lDITp8cSb7Etb4Qbx5pmdfIm+4n2gh7KRNhgf7Z2Z4IJI1mA0mWUApGrcw==} - peerDependencies: - vue: '>=3.4.0' - smart-buffer@4.2.0: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} @@ -6025,50 +5995,6 @@ snapshots: '@sindresorhus/merge-streams@2.3.0': {} - '@slickgrid-universal/binding@5.10.2': {} - - '@slickgrid-universal/common@5.10.2': - dependencies: - '@excel-builder-vanilla/types': 3.0.14 - '@formkit/tempo': 0.1.2 - '@slickgrid-universal/binding': 5.10.2 - '@slickgrid-universal/event-pub-sub': 5.10.2 - '@slickgrid-universal/utils': 5.10.2 - '@types/sortablejs': 1.15.8 - '@types/trusted-types': 2.0.7 - autocompleter: 9.3.2 - dequal: 2.0.3 - multiple-select-vanilla: 3.4.4 - sortablejs: 1.15.6 - un-flatten-tree: 2.0.12 - vanilla-calendar-pro: 2.9.10 - - '@slickgrid-universal/custom-footer-component@5.10.2': - dependencies: - '@formkit/tempo': 0.1.2 - '@slickgrid-universal/binding': 5.10.2 - '@slickgrid-universal/common': 5.10.2 - - '@slickgrid-universal/empty-warning-component@5.10.2': - dependencies: - '@slickgrid-universal/common': 5.10.2 - - '@slickgrid-universal/event-pub-sub@5.10.2': - dependencies: - '@slickgrid-universal/utils': 5.10.2 - - '@slickgrid-universal/pagination-component@5.10.2': - dependencies: - '@slickgrid-universal/binding': 5.10.2 - '@slickgrid-universal/common': 5.10.2 - - '@slickgrid-universal/row-detail-view-plugin@5.10.2': - dependencies: - '@slickgrid-universal/common': 5.10.2 - '@slickgrid-universal/utils': 5.10.2 - - '@slickgrid-universal/utils@5.10.2': {} - '@trysound/sax@0.2.0': {} '@tufjs/canonical-json@2.0.0': {} @@ -8981,24 +8907,6 @@ snapshots: astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 - slickgrid-vue@0.1.1(typescript@5.6.3)(vue@3.5.13(typescript@5.6.3)): - dependencies: - '@formkit/tempo': 0.1.2 - '@slickgrid-universal/common': 5.10.2 - '@slickgrid-universal/custom-footer-component': 5.10.2 - '@slickgrid-universal/empty-warning-component': 5.10.2 - '@slickgrid-universal/event-pub-sub': 5.10.2 - '@slickgrid-universal/pagination-component': 5.10.2 - '@slickgrid-universal/row-detail-view-plugin': 5.10.2 - '@slickgrid-universal/utils': 5.10.2 - dequal: 2.0.3 - i18next: 24.0.2(typescript@5.6.3) - i18next-vue: 5.0.0(i18next@24.0.2(typescript@5.6.3))(vue@3.5.13(typescript@5.6.3)) - sortablejs: 1.15.6 - vue: 3.5.13(typescript@5.6.3) - transitivePeerDependencies: - - typescript - smart-buffer@4.2.0: {} socks-proxy-agent@8.0.4: From 6fe2bbf9abdc29ced5b450a53cb1cfa6d0882d53 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Tue, 3 Dec 2024 00:21:57 -0500 Subject: [PATCH 19/36] chore: fix reference path --- demos/vue/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/vue/tsconfig.json b/demos/vue/tsconfig.json index 81812d102..d1e039e9e 100644 --- a/demos/vue/tsconfig.json +++ b/demos/vue/tsconfig.json @@ -1,7 +1,7 @@ { "files": [], "references": [ - { "path": "../../frameworks/vue" }, + { "path": "../../frameworks/slickgrid-vue" }, { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" } ] From cd72a355e5bce3d6909c7089e774202155c833f6 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Tue, 3 Dec 2024 00:28:10 -0500 Subject: [PATCH 20/36] chore: test with link path instead of workspace --- demos/vue/package.json | 2 +- pnpm-lock.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/demos/vue/package.json b/demos/vue/package.json index 6aa2142f3..033dd64bb 100644 --- a/demos/vue/package.json +++ b/demos/vue/package.json @@ -35,7 +35,7 @@ "i18next-http-backend": "^3.0.1", "i18next-vue": "^5.0.0", "rxjs": "^7.8.1", - "slickgrid-vue": "workspace:*", + "slickgrid-vue": "link:../../frameworks/slickgrid-vue", "vue": "^3.5.13", "vue-router": "^4.5.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7c7d7e142..dfc11c277 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -177,7 +177,7 @@ importers: specifier: ^7.8.1 version: 7.8.1 slickgrid-vue: - specifier: workspace:* + specifier: link:../../frameworks/slickgrid-vue version: link:../../frameworks/slickgrid-vue vue: specifier: ^3.5.13 From a5fd3f19b75028510cacc0a60e26635123043f6d Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Tue, 3 Dec 2024 00:32:25 -0500 Subject: [PATCH 21/36] chore: test with link path instead of workspace --- demos/vue/package.json | 2 +- package.json | 1 + pnpm-lock.yaml | 5 ++++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/demos/vue/package.json b/demos/vue/package.json index 033dd64bb..6aa2142f3 100644 --- a/demos/vue/package.json +++ b/demos/vue/package.json @@ -35,7 +35,7 @@ "i18next-http-backend": "^3.0.1", "i18next-vue": "^5.0.0", "rxjs": "^7.8.1", - "slickgrid-vue": "link:../../frameworks/slickgrid-vue", + "slickgrid-vue": "workspace:*", "vue": "^3.5.13", "vue-router": "^4.5.0" }, diff --git a/package.json b/package.json index 980f0fd26..a813c71d1 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "rimraf": "^5.0.10", "rxjs": "^7.8.1", "servor": "^4.0.2", + "slickgrid-vue": "link:frameworks/slickgrid-vue", "sortablejs": "^1.15.6", "typescript": "^5.7.2", "typescript-eslint": "^8.16.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dfc11c277..be4c21369 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,6 +98,9 @@ importers: servor: specifier: ^4.0.2 version: 4.0.2 + slickgrid-vue: + specifier: link:frameworks/slickgrid-vue + version: link:frameworks/slickgrid-vue sortablejs: specifier: ^1.15.6 version: 1.15.6 @@ -177,7 +180,7 @@ importers: specifier: ^7.8.1 version: 7.8.1 slickgrid-vue: - specifier: link:../../frameworks/slickgrid-vue + specifier: workspace:* version: link:../../frameworks/slickgrid-vue vue: specifier: ^3.5.13 From ead8221db10bea48abb182e82e3ab62cc67581b1 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Tue, 3 Dec 2024 00:45:29 -0500 Subject: [PATCH 22/36] chore: add license and vite path resolve --- demos/vue/vite.config.mts | 9 +++++++++ frameworks/slickgrid-vue/LICENSE | 21 +++++++++++++++++++++ frameworks/slickgrid-vue/src/index.ts | 8 +++----- 3 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 frameworks/slickgrid-vue/LICENSE diff --git a/demos/vue/vite.config.mts b/demos/vue/vite.config.mts index 0e551492f..4b9f0b8e2 100644 --- a/demos/vue/vite.config.mts +++ b/demos/vue/vite.config.mts @@ -1,4 +1,5 @@ import dns from 'node:dns'; +import { resolve } from 'node:path'; import vue from '@vitejs/plugin-vue'; import { defineConfig } from 'vite'; @@ -32,4 +33,12 @@ export default defineConfig({ clientPort: 7000, }, }, + resolve: { + alias: [ + { + find: /slickgrid-vue/, + replacement: resolve(__dirname, '../../', 'frameworks', 'slickgrid-vue'), + }, + ], + }, }); diff --git a/frameworks/slickgrid-vue/LICENSE b/frameworks/slickgrid-vue/LICENSE new file mode 100644 index 000000000..3a2210686 --- /dev/null +++ b/frameworks/slickgrid-vue/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2024-present, Ghislain B. +https://github.com/ghiscoding/slickgrid-universal/frameworks/slickgrid-vue + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/frameworks/slickgrid-vue/src/index.ts b/frameworks/slickgrid-vue/src/index.ts index 26621a1b1..1932068f8 100644 --- a/frameworks/slickgrid-vue/src/index.ts +++ b/frameworks/slickgrid-vue/src/index.ts @@ -1,5 +1,4 @@ -import { Aggregators, type Column, type Editors, Enums, type Filters, Formatters, GroupTotalFormatters, SortComparers, Utilities } from '@slickgrid-universal/common'; -import { BindingService } from '@slickgrid-universal/binding'; +import { Aggregators, type Column, type Editors, Enums, type Filters, Formatters, GroupTotalFormatters, type RowDetailViewProps, SortComparers, Utilities } from '@slickgrid-universal/common'; import { EventPubSubService } from '@slickgrid-universal/event-pub-sub'; export * from '@slickgrid-universal/common'; @@ -23,12 +22,11 @@ export { type GridOption, GroupTotalFormatters, type RowDetailView, + type RowDetailViewProps, SlickgridConfig, SlickgridVue, type SlickgridVueInstance, SlickRowDetailView, SortComparers, Utilities, -}; - -export { BindingService }; \ No newline at end of file +}; \ No newline at end of file From 6b5e8b724b19e2e8aade4caae184b20284f759d4 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Tue, 3 Dec 2024 00:50:10 -0500 Subject: [PATCH 23/36] chore: add missing slickgrid-vue build in test workflow --- .github/workflows/vue-cypress.yml | 5 ++++- frameworks/slickgrid-vue/src/index.ts | 3 +-- package.json | 4 ++-- pnpm-lock.yaml | 3 --- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/vue-cypress.yml b/.github/workflows/vue-cypress.yml index a3abe591e..8a3550245 100644 --- a/.github/workflows/vue-cypress.yml +++ b/.github/workflows/vue-cypress.yml @@ -66,8 +66,11 @@ jobs: - name: TSC Build (esm) run: pnpm predev + - name: Slickgrid-Vue Framework Build + run: pnpm vue:framework:build + - name: Website Dev Build (served for Cypress) - run: pnpm vue:build + run: pnpm vue:demo:build - name: Start HTTP Server run: pnpm vue:serve & diff --git a/frameworks/slickgrid-vue/src/index.ts b/frameworks/slickgrid-vue/src/index.ts index 1932068f8..c56a72aab 100644 --- a/frameworks/slickgrid-vue/src/index.ts +++ b/frameworks/slickgrid-vue/src/index.ts @@ -1,4 +1,4 @@ -import { Aggregators, type Column, type Editors, Enums, type Filters, Formatters, GroupTotalFormatters, type RowDetailViewProps, SortComparers, Utilities } from '@slickgrid-universal/common'; +import { Aggregators, type Column, type Editors, Enums, type Filters, Formatters, GroupTotalFormatters, SortComparers, Utilities } from '@slickgrid-universal/common'; import { EventPubSubService } from '@slickgrid-universal/event-pub-sub'; export * from '@slickgrid-universal/common'; @@ -22,7 +22,6 @@ export { type GridOption, GroupTotalFormatters, type RowDetailView, - type RowDetailViewProps, SlickgridConfig, SlickgridVue, type SlickgridVueInstance, diff --git a/package.json b/package.json index a813c71d1..fed3a32db 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,8 @@ "test:ui": "vitest --ui --config ./test/vitest.config.mts", "prepare": "husky", "commitlint": "commitlint --edit", - "vue:build": "pnpm -r --stream --filter=./demos/vue/** run build", + "vue:framework:build": "pnpm -r --stream --filter=./demos/vue/** run build", + "vue:demo:build": "pnpm -r --stream --filter=./demos/vue/** run build", "vue:dev": "pnpm -r --stream --filter=./demos/vue/** run dev", "vue:cypress": "pnpm -r --stream --filter=./demos/vue/** run cypress", "vue:serve": "pnpm -r --stream --filter=./demos/vue/** run serve:demo" @@ -93,7 +94,6 @@ "rimraf": "^5.0.10", "rxjs": "^7.8.1", "servor": "^4.0.2", - "slickgrid-vue": "link:frameworks/slickgrid-vue", "sortablejs": "^1.15.6", "typescript": "^5.7.2", "typescript-eslint": "^8.16.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be4c21369..7c7d7e142 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,9 +98,6 @@ importers: servor: specifier: ^4.0.2 version: 4.0.2 - slickgrid-vue: - specifier: link:frameworks/slickgrid-vue - version: link:frameworks/slickgrid-vue sortablejs: specifier: ^1.15.6 version: 1.15.6 From 36d6798c94bc864d20b0d050ce48ba49421677b2 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Tue, 3 Dec 2024 00:52:36 -0500 Subject: [PATCH 24/36] chore: run correct framework build path --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fed3a32db..581e56f96 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "test:ui": "vitest --ui --config ./test/vitest.config.mts", "prepare": "husky", "commitlint": "commitlint --edit", - "vue:framework:build": "pnpm -r --stream --filter=./demos/vue/** run build", + "vue:framework:build": "pnpm -r --stream --filter=./frameworks/slickgrid-vue/** run build", "vue:demo:build": "pnpm -r --stream --filter=./demos/vue/** run build", "vue:dev": "pnpm -r --stream --filter=./demos/vue/** run dev", "vue:cypress": "pnpm -r --stream --filter=./demos/vue/** run cypress", From 0ac22ceb66f6648367ca70b3d2e808ed9e948699 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Tue, 3 Dec 2024 00:55:19 -0500 Subject: [PATCH 25/36] chore: remove unnecessary Vite path resolve --- demos/vue/vite.config.mts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/demos/vue/vite.config.mts b/demos/vue/vite.config.mts index 4b9f0b8e2..0e551492f 100644 --- a/demos/vue/vite.config.mts +++ b/demos/vue/vite.config.mts @@ -1,5 +1,4 @@ import dns from 'node:dns'; -import { resolve } from 'node:path'; import vue from '@vitejs/plugin-vue'; import { defineConfig } from 'vite'; @@ -33,12 +32,4 @@ export default defineConfig({ clientPort: 7000, }, }, - resolve: { - alias: [ - { - find: /slickgrid-vue/, - replacement: resolve(__dirname, '../../', 'frameworks', 'slickgrid-vue'), - }, - ], - }, }); From 7a7d0e86c61f5c8e8244c5c1167b6a0303c65257 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Tue, 3 Dec 2024 00:56:23 -0500 Subject: [PATCH 26/36] chore: use Vite preview to run Vue http server demo --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 581e56f96..ff3b6ee64 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "vue:demo:build": "pnpm -r --stream --filter=./demos/vue/** run build", "vue:dev": "pnpm -r --stream --filter=./demos/vue/** run dev", "vue:cypress": "pnpm -r --stream --filter=./demos/vue/** run cypress", - "vue:serve": "pnpm -r --stream --filter=./demos/vue/** run serve:demo" + "vue:serve": "pnpm -r --stream --filter=./demos/vue/** run preview" }, "comments": { "new-version": "To create a new version with Lerna-Lite, simply run the following script (1) 'roll-new-release'.", From b8c1581e674b4c0c710e03e8b6cba7c51bc710ef Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Tue, 3 Dec 2024 01:08:16 -0500 Subject: [PATCH 27/36] chore: remove servor from Vue and tweak all npm scripts --- .github/workflows/vue-cypress.yml | 5 ++--- demos/vue/package.json | 11 ++++------- frameworks/slickgrid-vue/package.json | 6 +++--- package.json | 11 ++++++----- pnpm-lock.yaml | 3 --- 5 files changed, 15 insertions(+), 21 deletions(-) diff --git a/.github/workflows/vue-cypress.yml b/.github/workflows/vue-cypress.yml index 8a3550245..cfb32b1c1 100644 --- a/.github/workflows/vue-cypress.yml +++ b/.github/workflows/vue-cypress.yml @@ -67,10 +67,10 @@ jobs: run: pnpm predev - name: Slickgrid-Vue Framework Build - run: pnpm vue:framework:build + run: pnpm vue:build:framework - name: Website Dev Build (served for Cypress) - run: pnpm vue:demo:build + run: pnpm vue:build:demo - name: Start HTTP Server run: pnpm vue:serve & @@ -81,7 +81,6 @@ jobs: install: false working-directory: demos/vue # start: pnpm vue:serve - # start: pnpm serve:vite wait-on: 'http://localhost:7000' config-file: test/cypress.config.mjs browser: chrome diff --git a/demos/vue/package.json b/demos/vue/package.json index 6aa2142f3..7c82195bd 100644 --- a/demos/vue/package.json +++ b/demos/vue/package.json @@ -7,12 +7,10 @@ "name": "Ghislain B." }, "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "preview": "vite preview", - "cypress": "cypress open --config-file test/cypress.config.mjs", - "cypress:ci": "cypress run --config-file test/cypress.config.mjs", - "serve:demo": "servor ./dist index.html 7000" + "vue:dev": "vite", + "vue:build": "tsc && vite build", + "vue:preview": "vite preview", + "vue:cypress": "cypress open --config-file test/cypress.config.mjs" }, "dependencies": { "@faker-js/faker": "^9.2.0", @@ -47,7 +45,6 @@ "cypress-real-events": "^1.13.0", "fetch-jsonp": "^1.3.0", "sass": "^1.81.0", - "servor": "^4.0.2", "typescript": "~5.6.2", "vite": "^6.0.1" } diff --git a/frameworks/slickgrid-vue/package.json b/frameworks/slickgrid-vue/package.json index 345fc3b04..853e9111a 100644 --- a/frameworks/slickgrid-vue/package.json +++ b/frameworks/slickgrid-vue/package.json @@ -40,9 +40,9 @@ "scripts": { "are-types-wrong": "pnpx @arethetypeswrong/cli --pack .", "clean": "rimraf dist", - "dev": "vite build --watch", - "dev:init": "vite build", - "build": "pnpm clean && vue-tsc --p ./tsconfig.app.json && vite build --sourcemap && pnpm clone:dts", + "vue:dev": "vite build --watch", + "vue:dev:init": "vite build", + "vue:build": "pnpm clean && vue-tsc --p ./tsconfig.app.json && vite build --sourcemap && pnpm clone:dts", "clone:dts": "node clone-dts.mjs", "type-check": "vue-tsc --build --force" }, diff --git a/package.json b/package.json index ff3b6ee64..e35fc410b 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "build:watch": "tsc --build ./tsconfig.packages.json --watch", "predev": "pnpm run build:esm && pnpm run -r --filter=./packages/common sass:copy", "dev": "run-p dev:watch vite:watch", + "dev:vue": "run-p dev:watch vue:watch", "dev:watch": "lerna watch --no-bail --file-delimiter=\",\" --glob=\"src/**/*.{ts,scss}\" --ignored=\"**/*.spec.ts\" -- cross-env-shell pnpm run -r --filter $LERNA_PACKAGE_NAME dev", "vite:watch": "pnpm -r --parallel run vite:dev", "preview:publish": "lerna publish from-package --dry-run --yes", @@ -49,11 +50,11 @@ "test:ui": "vitest --ui --config ./test/vitest.config.mts", "prepare": "husky", "commitlint": "commitlint --edit", - "vue:framework:build": "pnpm -r --stream --filter=./frameworks/slickgrid-vue/** run build", - "vue:demo:build": "pnpm -r --stream --filter=./demos/vue/** run build", - "vue:dev": "pnpm -r --stream --filter=./demos/vue/** run dev", - "vue:cypress": "pnpm -r --stream --filter=./demos/vue/** run cypress", - "vue:serve": "pnpm -r --stream --filter=./demos/vue/** run preview" + "vue:build:framework": "pnpm -r --stream --filter=./frameworks/slickgrid-vue/** run vue:build", + "vue:build:demo": "pnpm -r --stream --filter=./demos/vue/** run vue:build", + "vue:watch": "pnpm -r --stream --filter=./demos/vue/** run vue:dev", + "vue:cypress": "pnpm -r --stream --filter=./demos/vue/** run vue:cypress", + "vue:serve": "pnpm -r --stream --filter=./demos/vue/** run vue:preview" }, "comments": { "new-version": "To create a new version with Lerna-Lite, simply run the following script (1) 'roll-new-release'.", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7c7d7e142..92d933f41 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -207,9 +207,6 @@ importers: sass: specifier: ^1.81.0 version: 1.81.0 - servor: - specifier: ^4.0.2 - version: 4.0.2 typescript: specifier: ~5.6.2 version: 5.6.3 From 13c38cd72f14f8d9b721441502121eae19d34ec5 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Tue, 3 Dec 2024 01:24:30 -0500 Subject: [PATCH 28/36] chore: add dev watch for slickgrid-vue development --- frameworks/slickgrid-vue/src/components/SlickgridVue.vue | 6 +++--- package.json | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/frameworks/slickgrid-vue/src/components/SlickgridVue.vue b/frameworks/slickgrid-vue/src/components/SlickgridVue.vue index d365cc937..8a2f27440 100644 --- a/frameworks/slickgrid-vue/src/components/SlickgridVue.vue +++ b/frameworks/slickgrid-vue/src/components/SlickgridVue.vue @@ -1156,9 +1156,9 @@ function updateColumnDefinitionsList(newColumnDefinitions: Column[]) { * see docs https://docs.aurelia.io/components/bindable-properties#calling-a-change-function-when-bindable-is-modified */ // function observeColumnDefinitions() { -// // _columnDefinitionObserver?.unsubscribe(columnDefinitionsModel.valueSubscriber); -// // _columnDefinitionObserver = observerLocator.getArrayObserver(columnDefinitions); -// // _columnDefinitionObserver.subscribe(columnDefinitionsModel.valueSubscriber); +// _columnDefinitionObserver?.unsubscribe(columnDefinitionsModel.valueSubscriber); +// _columnDefinitionObserver = observerLocator.getArrayObserver(columnDefinitions); +// _columnDefinitionObserver.subscribe(columnDefinitionsModel.valueSubscriber); // } /** diff --git a/package.json b/package.json index e35fc410b..bdb2d18f8 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "build:watch": "tsc --build ./tsconfig.packages.json --watch", "predev": "pnpm run build:esm && pnpm run -r --filter=./packages/common sass:copy", "dev": "run-p dev:watch vite:watch", - "dev:vue": "run-p dev:watch vue:watch", + "dev:vue": "run-p dev:watch vue:watch:framework vue:watch:demo", "dev:watch": "lerna watch --no-bail --file-delimiter=\",\" --glob=\"src/**/*.{ts,scss}\" --ignored=\"**/*.spec.ts\" -- cross-env-shell pnpm run -r --filter $LERNA_PACKAGE_NAME dev", "vite:watch": "pnpm -r --parallel run vite:dev", "preview:publish": "lerna publish from-package --dry-run --yes", @@ -52,7 +52,8 @@ "commitlint": "commitlint --edit", "vue:build:framework": "pnpm -r --stream --filter=./frameworks/slickgrid-vue/** run vue:build", "vue:build:demo": "pnpm -r --stream --filter=./demos/vue/** run vue:build", - "vue:watch": "pnpm -r --stream --filter=./demos/vue/** run vue:dev", + "vue:watch:framework": "pnpm -r --stream --filter=./frameworks/slickgrid-vue/** run vue:dev", + "vue:watch:demo": "pnpm -r --stream --filter=./demos/vue/** run vue:dev", "vue:cypress": "pnpm -r --stream --filter=./demos/vue/** run vue:cypress", "vue:serve": "pnpm -r --stream --filter=./demos/vue/** run vue:preview" }, From fb32e5d08fa90c5479c6b8430b7daee186fcfdf0 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Tue, 3 Dec 2024 18:48:57 -0500 Subject: [PATCH 29/36] chore: add Renovate deps group for VueJS --- .github/renovate.json5 | 5 +++++ frameworks/slickgrid-vue/package.json | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 47af37bcc..01d718f5f 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -10,6 +10,11 @@ depTypeList: ['peerDependencies'], enabled: false, }, + { + description: 'Group all Vue framework/demo dependencies', + matchFileNames: ['frameworks/slickgrid-vue/**', 'demos/vue/**'], + groupName: 'Vue package' + }, // rimraf new major releases dropped support for Node 18, we'll have to wait our next major to upgrade them { packageNames: ['rimraf'], diff --git a/frameworks/slickgrid-vue/package.json b/frameworks/slickgrid-vue/package.json index 853e9111a..b3b782e82 100644 --- a/frameworks/slickgrid-vue/package.json +++ b/frameworks/slickgrid-vue/package.json @@ -30,6 +30,15 @@ "datatable", "slickgrid" ], + "homepage": "https://github.com/ghiscoding/slickgrid-universal/frameworks/slickgrid-vue", + "repository": { + "type": "git", + "url": "git+https://github.com/ghiscoding/slickgrid-universal.git", + "directory": "frameworks/slickgrid-vue" + }, + "bugs": { + "url": "https://github.com/ghiscoding/slickgrid-universal/issues" + }, "publishConfig": { "access": "public" }, From b1dc2a37a448966bc29b1b6e062f5c5911b05caf Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Tue, 3 Dec 2024 19:53:36 -0500 Subject: [PATCH 30/36] chore: reenable import file extensions & add Prettier - adding Prettier but only for the new Vue frameworks/demos folders for now --- .github/workflows/main.yml | 9 ++++++++- .prettierrc | 15 +++++++++++++++ demos/vue/test/cypress/e2e/example02.cy.ts | 1 + demos/vue/test/cypress/e2e/example04.cy.ts | 1 + demos/vue/test/cypress/e2e/example06.cy.ts | 1 + demos/vue/test/cypress/e2e/example12.cy.ts | 1 + demos/vue/test/cypress/e2e/example27.cy.ts | 2 +- demos/vue/test/cypress/e2e/example30.cy.ts | 1 + demos/vue/test/cypress/e2e/example39.cy.ts | 1 + demos/vue/test/cypress/support/commands.ts | 1 + demos/vue/test/cypress/support/drag.ts | 1 + demos/vue/test/cypress/support/index.ts | 1 + eslint.config.mjs | 2 +- package.json | 3 +++ pnpm-lock.yaml | 10 ++++++++++ 15 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 .prettierrc diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c2de4f73f..8df9dabc2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,5 +1,5 @@ # CI Build & Vitest Unit Tests (ship smaller name for CI badge) -name: Build & Tests +name: Build & Unit Tests on: # Trigger the workflow on push or pull request, # but only for the master branch on Push and any branches on PR @@ -63,6 +63,13 @@ jobs: - name: Run pnpm install dependencies run: pnpm install + - name: ESLint + run: pnpm lint + + - name: Prettier + description: only covering frameworks/demos folders for now + run: pnpm prettier:check + - name: TSC Full Bundle (all Bundler types) run: pnpm bundle diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..0f4f09ae3 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,15 @@ +{ + "useTabs": false, + "tabWidth": 2, + "printWidth": 130, + "singleQuote": true, + "trailingComma": "es5", + "overrides": [ + { + "files": ["**/*.spec.ts"], + "options": { + "printWidth": 160 + } + } + ] +} \ No newline at end of file diff --git a/demos/vue/test/cypress/e2e/example02.cy.ts b/demos/vue/test/cypress/e2e/example02.cy.ts index 654f3579f..7f5b8828f 100644 --- a/demos/vue/test/cypress/e2e/example02.cy.ts +++ b/demos/vue/test/cypress/e2e/example02.cy.ts @@ -1,3 +1,4 @@ +/* eslint-disable n/file-extension-in-import */ import { removeExtraSpaces } from '../plugins/utilities'; describe('Example 2 - Grid with Formatters', () => { diff --git a/demos/vue/test/cypress/e2e/example04.cy.ts b/demos/vue/test/cypress/e2e/example04.cy.ts index 7f5be80e6..420a50f4f 100644 --- a/demos/vue/test/cypress/e2e/example04.cy.ts +++ b/demos/vue/test/cypress/e2e/example04.cy.ts @@ -1,3 +1,4 @@ +/* eslint-disable n/file-extension-in-import */ import { isAfter, isBefore, isEqual, parse } from '@formkit/tempo'; import { removeExtraSpaces } from '../plugins/utilities'; diff --git a/demos/vue/test/cypress/e2e/example06.cy.ts b/demos/vue/test/cypress/e2e/example06.cy.ts index 98e345d25..218cdf27c 100644 --- a/demos/vue/test/cypress/e2e/example06.cy.ts +++ b/demos/vue/test/cypress/e2e/example06.cy.ts @@ -1,3 +1,4 @@ +/* eslint-disable n/file-extension-in-import */ import { addDay, format } from '@formkit/tempo'; import { removeWhitespaces } from '../plugins/utilities'; diff --git a/demos/vue/test/cypress/e2e/example12.cy.ts b/demos/vue/test/cypress/e2e/example12.cy.ts index d667c81ea..060fec97b 100644 --- a/demos/vue/test/cypress/e2e/example12.cy.ts +++ b/demos/vue/test/cypress/e2e/example12.cy.ts @@ -1,3 +1,4 @@ +/* eslint-disable n/file-extension-in-import */ import { format } from '@formkit/tempo'; import { removeExtraSpaces } from '../plugins/utilities'; diff --git a/demos/vue/test/cypress/e2e/example27.cy.ts b/demos/vue/test/cypress/e2e/example27.cy.ts index 3aeb58d0a..758cc0c01 100644 --- a/demos/vue/test/cypress/e2e/example27.cy.ts +++ b/demos/vue/test/cypress/e2e/example27.cy.ts @@ -1,4 +1,4 @@ - +/* eslint-disable n/file-extension-in-import */ import { changeTimezone, zeroPadding } from '../plugins/utilities'; function removeExtraSpaces(text) { diff --git a/demos/vue/test/cypress/e2e/example30.cy.ts b/demos/vue/test/cypress/e2e/example30.cy.ts index e531ad23a..77399735a 100644 --- a/demos/vue/test/cypress/e2e/example30.cy.ts +++ b/demos/vue/test/cypress/e2e/example30.cy.ts @@ -1,3 +1,4 @@ +/* eslint-disable n/file-extension-in-import */ import { changeTimezone, zeroPadding } from '../plugins/utilities'; describe('Example 30 Composite Editor Modal', () => { diff --git a/demos/vue/test/cypress/e2e/example39.cy.ts b/demos/vue/test/cypress/e2e/example39.cy.ts index 1cd6f3e24..4f1e8fa85 100644 --- a/demos/vue/test/cypress/e2e/example39.cy.ts +++ b/demos/vue/test/cypress/e2e/example39.cy.ts @@ -1,3 +1,4 @@ +/* eslint-disable n/file-extension-in-import */ import { removeWhitespaces } from '../plugins/utilities'; describe('Example 39 - Infinite Scroll with GraphQL', () => { diff --git a/demos/vue/test/cypress/support/commands.ts b/demos/vue/test/cypress/support/commands.ts index c19248dac..9c80f0628 100644 --- a/demos/vue/test/cypress/support/commands.ts +++ b/demos/vue/test/cypress/support/commands.ts @@ -1,3 +1,4 @@ +/* eslint-disable n/file-extension-in-import */ // *********************************************** // This example commands.js shows you how to // create various custom commands and overwrite diff --git a/demos/vue/test/cypress/support/drag.ts b/demos/vue/test/cypress/support/drag.ts index ca28e66da..b87339d42 100644 --- a/demos/vue/test/cypress/support/drag.ts +++ b/demos/vue/test/cypress/support/drag.ts @@ -1,3 +1,4 @@ +/* eslint-disable n/file-extension-in-import */ import { convertPosition } from './common'; declare global { diff --git a/demos/vue/test/cypress/support/index.ts b/demos/vue/test/cypress/support/index.ts index e4d70b3bc..744312ea2 100644 --- a/demos/vue/test/cypress/support/index.ts +++ b/demos/vue/test/cypress/support/index.ts @@ -1,3 +1,4 @@ +/* eslint-disable n/file-extension-in-import */ // *********************************************************** // This example support/index.js is processed and // loaded automatically before your test files. diff --git a/eslint.config.mjs b/eslint.config.mjs index 20be891f1..5796e5c8e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -59,7 +59,7 @@ export default tseslint.config( 'cypress/no-assigning-return-values': 'off', 'cypress/unsafe-to-chain-command': 'off', 'object-shorthand': 'error', - // 'n/file-extension-in-import': ['error', 'always', { ".cy.ts": "never" }], + 'n/file-extension-in-import': ['error', 'always'], 'no-async-promise-executor': 'off', 'no-case-declarations': 'off', 'no-prototype-builtins': 'off', diff --git a/package.json b/package.json index bdb2d18f8..8cb466b9b 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,8 @@ "lint": "eslint --cache .", "lint:fix": "eslint --fix .", "lint:no-cache": "eslint .", + "prettier:check": "prettier --check {frameworks,demos}/**/*.{js,ts,vue}", + "prettier:write": "prettier --write {frameworks,demos}/**/*.{js,ts,vue}", "test": "vitest --config ./test/vitest.config.mts", "test:coverage": "vitest --no-watch --coverage --config ./test/vitest.config.mts", "test:watch": "vitest --config ./test/vitest.config.mts", @@ -93,6 +95,7 @@ "jsdom-global": "^3.0.2", "npm-run-all2": "^7.0.1", "pnpm": "^9.14.4", + "prettier": "^3.4.1", "rimraf": "^5.0.10", "rxjs": "^7.8.1", "servor": "^4.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 92d933f41..c44e157aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,6 +89,9 @@ importers: pnpm: specifier: ^9.14.4 version: 9.14.4 + prettier: + specifier: ^3.4.1 + version: 3.4.1 rimraf: specifier: ^5.0.10 version: 5.0.10 @@ -3982,6 +3985,11 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier@3.4.1: + resolution: {integrity: sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg==} + engines: {node: '>=14'} + hasBin: true + pretty-bytes@5.6.0: resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} engines: {node: '>=6'} @@ -8625,6 +8633,8 @@ snapshots: prelude-ls@1.2.1: {} + prettier@3.4.1: {} + pretty-bytes@5.6.0: {} pretty-hrtime@1.0.3: {} From def1d8695da89a379240a1f59652bcfa9edeea70 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Tue, 3 Dec 2024 19:54:44 -0500 Subject: [PATCH 31/36] chore: remove invalid description prop in CI workflow --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8df9dabc2..b0fea8923 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -66,8 +66,8 @@ jobs: - name: ESLint run: pnpm lint + # only covering frameworks/demos folders for now - name: Prettier - description: only covering frameworks/demos folders for now run: pnpm prettier:check - name: TSC Full Bundle (all Bundler types) From 441f36980055670c5060c46a0da4616fe416f6c6 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Wed, 4 Dec 2024 01:09:20 -0500 Subject: [PATCH 32/36] docs: add Slickgrid-Vue docs --- docs/backend-services/GraphQL.md | 2 +- docs/backend-services/OData.md | 2 +- docs/grid-functionalities/infinite-scroll.md | 2 +- frameworks/slickgrid-vue/.gitbook.yaml | 5 + frameworks/slickgrid-vue/README.md | 64 ++ frameworks/slickgrid-vue/docs/README.md | 5 + frameworks/slickgrid-vue/docs/TOC.md | 96 +++ .../docs/backend-services/GraphQL.md | 294 ++++++++ .../docs/backend-services/OData.md | 250 +++++++ .../custom-backend-service.md | 68 ++ .../graphql/GraphQL-Filtering.md | 162 +++++ .../graphql/GraphQL-JSON-Result.md | 85 +++ .../graphql/GraphQL-Pagination.md | 77 ++ .../graphql/GraphQL-Sorting.md | 84 +++ .../docs/column-functionalities/cell-menu.md | 213 ++++++ .../docs/column-functionalities/editors.md | 626 ++++++++++++++++ .../editors/autocomplete-editor-kraaden.md | 477 ++++++++++++ .../editors/date-editor-flatpickr.md | 72 ++ .../editors/date-editor-vanilla-calendar.md | 99 +++ .../editors/longtext-editor-textarea.md | 92 +++ .../editors/select-dropdown-editor.md | 228 ++++++ .../filters/autocomplete-filter-kraaden.md | 175 +++++ .../filters/compound-filters.md | 233 ++++++ .../filters/custom-filter.md | 193 +++++ .../filters/filter-intro.md | 87 +++ .../filters/input-filter.md | 268 +++++++ .../filters/range-filters.md | 202 ++++++ .../filters/select-filter.md | 685 ++++++++++++++++++ .../filters/single-search-filter.md | 129 ++++ .../filters/styling-filled-filters.md | 45 ++ .../docs/column-functionalities/formatters.md | 306 ++++++++ .../docs/column-functionalities/sorting.md | 214 ++++++ .../docs/developer-guides/csp-compliance.md | 73 ++ .../docs/events/available-events.md | 229 ++++++ .../docs/events/grid-dataview-events.md | 143 ++++ .../docs/getting-started/quick-start.md | 179 +++++ .../grid-functionalities/Row-based-edit.md | 71 ++ .../add-update-highlight.md | 270 +++++++ .../grid-functionalities/column-picker.md | 22 + .../composite-editor-modal.md | 675 +++++++++++++++++ .../docs/grid-functionalities/context-menu.md | 226 ++++++ .../grid-functionalities/custom-footer.md | 80 ++ .../grid-functionalities/custom-tooltip.md | 265 +++++++ .../dynamic-item-metadata.md | 139 ++++ .../grid-functionalities/excel-copy-buffer.md | 149 ++++ .../grid-functionalities/export-to-excel.md | 509 +++++++++++++ .../export-to-text-file.md | 184 +++++ .../frozen-columns-rows.md | 168 +++++ .../grid-functionalities/global-options.md | 4 + .../grid-functionalities/grid-auto-resize.md | 253 +++++++ .../docs/grid-functionalities/grid-menu.md | 132 ++++ .../grid-functionalities/grid-state-preset.md | 239 ++++++ .../grouping-aggregators.md | 293 ++++++++ .../header-menu-header-buttons.md | 126 ++++ .../grid-functionalities/infinite-scroll.md | 185 +++++ .../providing-grid-data.md | 61 ++ .../resize-by-cell-content.md | 139 ++++ .../docs/grid-functionalities/row-detail.md | 416 +++++++++++ .../grid-functionalities/row-selection.md | 395 ++++++++++ .../grid-functionalities/tree-data-grid.md | 422 +++++++++++ .../localization-with-custom-locales.md | 46 ++ .../docs/localization/localization.md | 146 ++++ .../slickgrid-dataview-objects.md | 130 ++++ .../slickgrid-vue/docs/styling/dark-mode.md | 108 +++ .../slickgrid-vue/docs/styling/styling.md | 248 +++++++ .../docs/testing/testing-patterns.md | 38 + 66 files changed, 12300 insertions(+), 3 deletions(-) create mode 100644 frameworks/slickgrid-vue/.gitbook.yaml create mode 100644 frameworks/slickgrid-vue/README.md create mode 100644 frameworks/slickgrid-vue/docs/README.md create mode 100644 frameworks/slickgrid-vue/docs/TOC.md create mode 100644 frameworks/slickgrid-vue/docs/backend-services/GraphQL.md create mode 100644 frameworks/slickgrid-vue/docs/backend-services/OData.md create mode 100644 frameworks/slickgrid-vue/docs/backend-services/custom-backend-service.md create mode 100644 frameworks/slickgrid-vue/docs/backend-services/graphql/GraphQL-Filtering.md create mode 100644 frameworks/slickgrid-vue/docs/backend-services/graphql/GraphQL-JSON-Result.md create mode 100644 frameworks/slickgrid-vue/docs/backend-services/graphql/GraphQL-Pagination.md create mode 100644 frameworks/slickgrid-vue/docs/backend-services/graphql/GraphQL-Sorting.md create mode 100644 frameworks/slickgrid-vue/docs/column-functionalities/cell-menu.md create mode 100644 frameworks/slickgrid-vue/docs/column-functionalities/editors.md create mode 100644 frameworks/slickgrid-vue/docs/column-functionalities/editors/autocomplete-editor-kraaden.md create mode 100644 frameworks/slickgrid-vue/docs/column-functionalities/editors/date-editor-flatpickr.md create mode 100644 frameworks/slickgrid-vue/docs/column-functionalities/editors/date-editor-vanilla-calendar.md create mode 100644 frameworks/slickgrid-vue/docs/column-functionalities/editors/longtext-editor-textarea.md create mode 100644 frameworks/slickgrid-vue/docs/column-functionalities/editors/select-dropdown-editor.md create mode 100644 frameworks/slickgrid-vue/docs/column-functionalities/filters/autocomplete-filter-kraaden.md create mode 100644 frameworks/slickgrid-vue/docs/column-functionalities/filters/compound-filters.md create mode 100644 frameworks/slickgrid-vue/docs/column-functionalities/filters/custom-filter.md create mode 100644 frameworks/slickgrid-vue/docs/column-functionalities/filters/filter-intro.md create mode 100644 frameworks/slickgrid-vue/docs/column-functionalities/filters/input-filter.md create mode 100644 frameworks/slickgrid-vue/docs/column-functionalities/filters/range-filters.md create mode 100644 frameworks/slickgrid-vue/docs/column-functionalities/filters/select-filter.md create mode 100644 frameworks/slickgrid-vue/docs/column-functionalities/filters/single-search-filter.md create mode 100644 frameworks/slickgrid-vue/docs/column-functionalities/filters/styling-filled-filters.md create mode 100644 frameworks/slickgrid-vue/docs/column-functionalities/formatters.md create mode 100644 frameworks/slickgrid-vue/docs/column-functionalities/sorting.md create mode 100644 frameworks/slickgrid-vue/docs/developer-guides/csp-compliance.md create mode 100644 frameworks/slickgrid-vue/docs/events/available-events.md create mode 100644 frameworks/slickgrid-vue/docs/events/grid-dataview-events.md create mode 100644 frameworks/slickgrid-vue/docs/getting-started/quick-start.md create mode 100644 frameworks/slickgrid-vue/docs/grid-functionalities/Row-based-edit.md create mode 100644 frameworks/slickgrid-vue/docs/grid-functionalities/add-update-highlight.md create mode 100644 frameworks/slickgrid-vue/docs/grid-functionalities/column-picker.md create mode 100644 frameworks/slickgrid-vue/docs/grid-functionalities/composite-editor-modal.md create mode 100644 frameworks/slickgrid-vue/docs/grid-functionalities/context-menu.md create mode 100644 frameworks/slickgrid-vue/docs/grid-functionalities/custom-footer.md create mode 100644 frameworks/slickgrid-vue/docs/grid-functionalities/custom-tooltip.md create mode 100644 frameworks/slickgrid-vue/docs/grid-functionalities/dynamic-item-metadata.md create mode 100644 frameworks/slickgrid-vue/docs/grid-functionalities/excel-copy-buffer.md create mode 100644 frameworks/slickgrid-vue/docs/grid-functionalities/export-to-excel.md create mode 100644 frameworks/slickgrid-vue/docs/grid-functionalities/export-to-text-file.md create mode 100644 frameworks/slickgrid-vue/docs/grid-functionalities/frozen-columns-rows.md create mode 100644 frameworks/slickgrid-vue/docs/grid-functionalities/global-options.md create mode 100644 frameworks/slickgrid-vue/docs/grid-functionalities/grid-auto-resize.md create mode 100644 frameworks/slickgrid-vue/docs/grid-functionalities/grid-menu.md create mode 100644 frameworks/slickgrid-vue/docs/grid-functionalities/grid-state-preset.md create mode 100644 frameworks/slickgrid-vue/docs/grid-functionalities/grouping-aggregators.md create mode 100644 frameworks/slickgrid-vue/docs/grid-functionalities/header-menu-header-buttons.md create mode 100644 frameworks/slickgrid-vue/docs/grid-functionalities/infinite-scroll.md create mode 100644 frameworks/slickgrid-vue/docs/grid-functionalities/providing-grid-data.md create mode 100644 frameworks/slickgrid-vue/docs/grid-functionalities/resize-by-cell-content.md create mode 100644 frameworks/slickgrid-vue/docs/grid-functionalities/row-detail.md create mode 100644 frameworks/slickgrid-vue/docs/grid-functionalities/row-selection.md create mode 100644 frameworks/slickgrid-vue/docs/grid-functionalities/tree-data-grid.md create mode 100644 frameworks/slickgrid-vue/docs/localization/localization-with-custom-locales.md create mode 100644 frameworks/slickgrid-vue/docs/localization/localization.md create mode 100644 frameworks/slickgrid-vue/docs/slick-grid-dataview-objects/slickgrid-dataview-objects.md create mode 100644 frameworks/slickgrid-vue/docs/styling/dark-mode.md create mode 100644 frameworks/slickgrid-vue/docs/styling/styling.md create mode 100644 frameworks/slickgrid-vue/docs/testing/testing-patterns.md diff --git a/docs/backend-services/GraphQL.md b/docs/backend-services/GraphQL.md index 12448ca53..ee8f80c22 100644 --- a/docs/backend-services/GraphQL.md +++ b/docs/backend-services/GraphQL.md @@ -159,7 +159,7 @@ export class Example { // Web API call getAllCustomers(graphqlQuery) { // regular Http Client call - return this.http.createRequest(`/api/customers?${graphqlQuery}`).asGet().send().then(response => response.content); + return this.http.createRequest(`/api/customers?${graphqlQuery}`).then(response => response.json()); // or with Fetch Client // return this.http.fetch(`/api/customers?${graphqlQuery}`).then(response => response.json()); diff --git a/docs/backend-services/OData.md b/docs/backend-services/OData.md index cff2dc7c3..c0af03798 100644 --- a/docs/backend-services/OData.md +++ b/docs/backend-services/OData.md @@ -111,7 +111,7 @@ export class Example { // Web API call getCustomerApiCall(odataQuery) { // regular Http Client call - return this.http.createRequest(`/api/customers?${odataQuery}`).asGet().send().then(response => response.content); + return this.http.createRequest(`/api/customers?${odataQuery}`).then(response => response.json()); // or with Fetch Client // return this.http.fetch(`/api/customers?${odataQuery}`).then(response => response.json()); diff --git a/docs/grid-functionalities/infinite-scroll.md b/docs/grid-functionalities/infinite-scroll.md index 9cc49cf5f..0554a2f3d 100644 --- a/docs/grid-functionalities/infinite-scroll.md +++ b/docs/grid-functionalities/infinite-scroll.md @@ -113,7 +113,7 @@ export class Example { // Web API call getCustomerApiCall(odataQuery) { // regular Http Client call - return this.http.createRequest(`/api/customers?${odataQuery}`).asGet().send().then(response => response.content); + return this.http.createRequest(`/api/customers?${odataQuery}`).then(response => response.json()); // or with Fetch Client // return this.http.fetch(`/api/customers?${odataQuery}`).then(response => response.json()); diff --git a/frameworks/slickgrid-vue/.gitbook.yaml b/frameworks/slickgrid-vue/.gitbook.yaml new file mode 100644 index 000000000..cc617d426 --- /dev/null +++ b/frameworks/slickgrid-vue/.gitbook.yaml @@ -0,0 +1,5 @@ +root: ./docs/ + +structure: + readme: README.md + summary: TOC.md \ No newline at end of file diff --git a/frameworks/slickgrid-vue/README.md b/frameworks/slickgrid-vue/README.md new file mode 100644 index 000000000..ecf5add0c --- /dev/null +++ b/frameworks/slickgrid-vue/README.md @@ -0,0 +1,64 @@ +# Slickgrid-Vue + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) +[![Cypress.io](https://img.shields.io/badge/tested%20with-Cypress-04C38E.svg?logo=cypress)](https://www.cypress.io/) +[![NPM downloads](https://img.shields.io/npm/dy/slickgrid-vue)](https://npmjs.org/package/slickgrid-vue) +[![npm](https://img.shields.io/npm/v/slickgrid-vue.svg?logo=npm&logoColor=fff&label=npm)](https://www.npmjs.com/package/slickgrid-vue) +[![npm bundle size](https://img.shields.io/bundlephobia/minzip/slickgrid-vue?color=success&label=gzip)](https://bundlephobia.com/result?p=slickgrid-vue) +[![Actions Status](https://github.com/ghiscoding/slickgrid-vue/workflows/CI%20Build/badge.svg)](https://github.com/ghiscoding/slickgrid-vue/actions) + +### Brief introduction +One of the best JavasSript data grid [SlickGrid](https://github.com/mleibman/SlickGrid), which was originally developed by @mleibman, is now available to the Vue world. SlickGrid beats most other data grids in terms of features, customizability & performance (running smoothly with even a million rows). Slickgrid-Vue is a wrapper on top of [Slickgrid-Universal](https://github.com/ghiscoding/slickgrid-universal/) (which is a dependency). + +## Documentation +๐Ÿ“˜ [Documentation](https://ghiscoding.gitbook.io/slickgrid-vue/getting-started/quick-start) website powered by GitBook. + +## Installation +Available in Stackblitz (Codeflow) below, this can also be used to provide an issue repro. + +[![Open in Codeflow](https://developer.stackblitz.com/img/open_in_codeflow.svg)](https:///pr.new/ghiscoding/slickgrid-vue) + +Refer to the **[Docs - Quick Start](https://ghiscoding.gitbook.io/slickgrid-vue/getting-started/quick-start)** and/or clone the [Slickgrid-Vue-Demos](https://github.com/ghiscoding/slickgrid-vue-demos) repository. Please consult all documentation before opening new issues, also consider asking installation and/or general questions on [Stack Overflow](https://stackoverflow.com/search?tab=newest&q=slickgrid) unless you think there's a bug with the library. + +### NPM Package +[slickgrid-vue on NPM](https://www.npmjs.com/package/slickgrid-vue) + +### Styling Themes + +Multiple styling themes are available +- Bootstrap (see all Slickgrid-Vue [live demos](https://ghiscoding.github.io/slickgrid-vue/)) +- Material (see [Slickgrid-Universal](https://ghiscoding.github.io/slickgrid-universal/#/example07)) +- Salesforce (see [Slickgrid-Universal](https://ghiscoding.github.io/slickgrid-universal/#/example16)) + +Also note that all of these themes also have **Dark Theme** equivalent and even though Bootstrap if often used as the default, it also works well with any other UI framework like Bulma, Material, ... + +### Live Demo page +`Slickgrid-Vue` works with all `Bootstrap` versions, you can see a demo of each one below. It also works well with any other frameworks like Material or Bulma and there are also couple of extra styling themes based on Material & Salesforce which are also available. You can also use different SVG icons, you may want to look at the [Docs - SVG Icons](https://ghiscoding.gitbook.io/slickgrid-vue/styling/svg-icons) +- [Bootstrap 5 demo](https://ghiscoding.github.io/slickgrid-vue) / [examples repo](https://github.com/ghiscoding/slickgrid-vue-demos/tree/main/bootstrap5-i18n-demo) + +#### Working Demos +For a complete set of working demos (40+ examples), we strongly suggest you to clone the [Slickgrid-Vue Demos](https://github.com/ghiscoding/slickgrid-vue-demos) repository (instructions are provided in the demo repo). The repo provides multiple demos and they are updated every time a new version is out, so it is updated frequently and is also used as the GitHub live demo page. + +## License +[MIT License](LICENSE) + +## Latest News & Releases +Check out the [Releases](https://github.com/ghiscoding/slickgrid-vue/releases) section for all latest News & Releases. + +### Tested with [Cypress](https://www.cypress.io/) (E2E Tests) +Slickgrid-Universal has **100%** Unit Test Coverage and all Slickgrid-Vue Examples are tested with [Cypress](https://www.cypress.io/) as E2E tests. + +### Like it? โญ it +You like **Slickgrid-Vue**? Be sure to upvote โญ, and perhaps support me with caffeine [โ˜•](https://ko-fi.com/ghiscoding) and feel free to contribute. ๐Ÿ‘ท๐Ÿ‘ทโ€โ™€๏ธ + +Buy Me a Coffee at ko-fi.com + +## Sponsors + + diff --git a/frameworks/slickgrid-vue/docs/README.md b/frameworks/slickgrid-vue/docs/README.md new file mode 100644 index 000000000..203bc3620 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/README.md @@ -0,0 +1,5 @@ +# Documentation + +The [`docs`](https://github.com/ghiscoding/slickgrid-vue/tree/master/docs) folder of Slickgrid-Vue is the one-stop-shop for all project related documentation. + +Feel free to contribution documentation fixes by editing any of the markdown files in the [`docs`](https://github.com/ghiscoding/slickgrid-vue/tree/master/docs) folder. diff --git a/frameworks/slickgrid-vue/docs/TOC.md b/frameworks/slickgrid-vue/docs/TOC.md new file mode 100644 index 000000000..2be688396 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/TOC.md @@ -0,0 +1,96 @@ +# Table of contents + +* [Introduction](README.md) + +## Getting Started + +* [Quick start](getting-started/quick-start.md) + +## Styling + +* [Dark Mode](styling/dark-mode.md) +* [Styling CSS/SASS/Themes](styling/styling.md) + +## Column Functionalities + +* [Cell Menu (Action Menu)](column-functionalities/cell-menu.md) +* [Editors](column-functionalities/editors.md) + * [Autocomplete](column-functionalities/editors/autocomplete-editor-kraaden.md) + * [old Date Picker (flatpickr)](column-functionalities/editors/date-editor-flatpickr.md) + * [new Date Picker (vanilla-calendar)](column-functionalities/editors/date-editor-vanilla-calendar.md) + * [LongText (textarea)](column-functionalities/editors/longtext-editor-textarea.md) + * [Select Dropdown Editor (single/multiple)](column-functionalities/editors/select-dropdown-editor.md) +* [Filters](column-functionalities/filters/filter-intro.md) + * [Autocomplete](column-functionalities/filters/autocomplete-filter-kraaden.md) + * [Input Filter (default)](column-functionalities/filters/input-filter.md) + * [Select Filter (dropdown)](column-functionalities/filters/select-filter.md) + * [Compound Filters](column-functionalities/filters/compound-filters.md) + * [Range Filters](column-functionalities/filters/range-filters.md) + * [Custom Filter](column-functionalities/filters/custom-filter.md) + * [Styling Filled Filters](column-functionalities/filters/styling-filled-filters.md) + * [Single Search Filter](column-functionalities/filters/single-search-filter.md) +* [Formatters](column-functionalities/formatters.md) +* [Sorting](column-functionalities/sorting.md) + +## Events + +* [Available events](events/available-events.md) +* [On Events](events/grid-dataview-events.md) + +## Slick Grid/DataView Objects +* [Slick Grid/DataView Objects](slick-grid-dataview-objects/slickgrid-dataview-objects.md) + +## Grid Functionalities + +* [Auto-Resize / Resizer Service](grid-functionalities/grid-auto-resize.md) +* [Resize by Cell Content](grid-functionalities/resize-by-cell-content.md) +* [Column Picker](grid-functionalities/column-picker.md) +* [Composite Editor Modal](grid-functionalities/composite-editor-modal.md) +* [Custom Tooltip](grid-functionalities/custom-tooltip.md) +* [Add, Update or Highlight a Datagrid Item](grid-functionalities/add-update-highlight.md) +* [Dynamically Add CSS Classes to Item Rows](grid-functionalities/dynamic-item-metadata.md) +* [Context Menu](grid-functionalities/context-menu.md) +* [Custom Footer](grid-functionalities/custom-footer.md) +* [Excel Copy Buffer Plugin](grid-functionalities/excel-copy-buffer.md) +* [Export to Excel](grid-functionalities/export-to-excel.md) +* [Export to File (csv/txt)](grid-functionalities/export-to-text-file.md) +* [Grid Menu](grid-functionalities/grid-menu.md) +* [Grid State & Presets](grid-functionalities/grid-state-preset.md) +* [Grouping & Aggregators](grid-functionalities/grouping-aggregators.md) +* [Header Menu & Header Buttons](grid-functionalities/header-menu-header-buttons.md) +* [Infinite Scroll](grid-functionalities/infinite-scroll.md) +* [Pinning (frozen) of Columns/Rows](grid-functionalities/frozen-columns-rows.md) +* [Providing data to the grid](grid-functionalities/providing-grid-data.md) +* [Row Detail](grid-functionalities/row-detail.md) +* [Row Selection](grid-functionalities/row-selection.md) +* [Tree Data Grid](grid-functionalities/tree-data-grid.md) +* [Row Based Editing Plugin](grid-functionalities/Row-based-edit.md) + +## Developer Guides + +* [CSP Compliance](developer-guides/csp-compliance.md) + +## Localization + +* [with I18N](localization/localization.md) +* [with Custom Locales](localization/localization-with-custom-locales.md) + +## Backend Services + +* [Custom Backend Service](backend-services/custom-backend-service.md) +* [OData](backend-services/OData.md) +* [GraphQL](backend-services/GraphQL.md) + * [JSON Result Structure](backend-services/graphql/GraphQL-JSON-Result.md) + * [Filtering Schema](backend-services/graphql/GraphQL-Filtering.md) + * [Pagination Schema](backend-services/graphql/GraphQL-Pagination.md) + * [Sorting Schema](backend-services/graphql/GraphQL-Sorting.md) + +## Testing + +* [Testing Patterns](testing/testing-patterns.md) + +## Migrations + +* [Migration Guide to 3.x](migrations/migration-to-3.x.md) +* [Migration Guide to 4.x](migrations/migration-to-4.x.md) +* [Migration Guide to 5.x](migrations/migration-to-5.x.md) diff --git a/frameworks/slickgrid-vue/docs/backend-services/GraphQL.md b/frameworks/slickgrid-vue/docs/backend-services/GraphQL.md new file mode 100644 index 000000000..5b8bbcaed --- /dev/null +++ b/frameworks/slickgrid-vue/docs/backend-services/GraphQL.md @@ -0,0 +1,294 @@ +##### index +- [Extra Query Arguments](#extra-query-arguments) +- [Changing/Updating Options Dynamically](#changingupdating-options-dynamically) +- [GraphQL without Pagination](#graphql-without-pagination) +- [GraphQL Server Definitions](#graphql-server-definitions) + - [Pagination](graphql/GraphQL-Pagination.md) + - [Sorting](graphql/GraphQL-Sorting.md) + - [Filtering](graphql/GraphQL-Filtering.md) +- [Infinite Scroll](../grid-functionalities/infinite-scroll.md#infinite-scroll-with-backend-services) + +### Description +GraphQL Backend Service (for Pagination purposes) to get data from a backend server with the help of GraphQL. + +### Demo +[Demo Page](https://ghiscoding.github.io/slickgrid-vue/#/Example6) / [Demo ViewModel](https://github.com/ghiscoding/slickgrid-vue/blob/master/src/examples/slickgrid/Example6.tsx) + +### Note +You can use it when you need to support **Pagination** (though you could disable Pagination if you wish), that is when your dataset is rather large and has typically more than 5k rows, with a GraphQL endpoint. If your dataset is small (less than 5k rows), then you might be better off with [regular grid](https://ghiscoding.github.io/slickgrid-vue/#/Example1) with the "dataset.bind" property. SlickGrid can easily handle million of rows using a DataView object, but personally when the dataset is known to be large, I usually use a backend service (OData or GraphQL) and when it's small I go with a [regular grid](https://ghiscoding.github.io/slickgrid-vue/#/Example1). + +## Implementation +To connect a backend service into `Slickgrid-Universal`, you simply need to modify your `gridOptions` and add a declaration of `backendServiceApi`. See below for the signature and an example further down below. + +### TypeScript Signature +```ts +backendServiceApi: { + // On init (or on page load), what action to perform? + onInit?: (query: string) => Promise; + + // Before executing the query, what action to perform? For example, start a spinner + preProcess?: () => void; + + // On Processing, we get the query back from the service, and we need to provide a Promise. For example: http.get(myGraphqlUrl) + process: (query: string) => Promise; + + // After executing the query, what action to perform? For example, stop the spinner + postProcess: (response: any) => void; + + // Backend Service instance (could be OData or GraphQL Service) + service: BackendService; + + // Throttle the amount of requests sent to the backend. Default to 500ms + filterTypingDebounce?: number; +} +``` + +As you can see, you mainly need to define which service to use (GridODataService or GraphQLService) and finally add the `process` and `postProcess` callback, while all the rest are totally optional. + +### Typescript GraphQL Service Options +You can also pass certain options to the `backendServiceApi` through the `options` property. The list of options is the following + +```typescript +export interface GraphqlServiceOption extends BackendServiceOption { + /** + * When using Translation, we probably want to add locale in the query for the filterBy/orderBy to work + * ex.: users(first: 10, offset: 0, locale: "en-CA", filterBy: [{field: name, operator: EQ, value:"John"}]) { + */ + addLocaleIntoQuery?: boolean; + + /** Array of column ids that are included in the column definitions */ + columnIds?: string[]; + + /** What is the dataset, this is required for the GraphQL query to be built */ + datasetName?: string; + + /** Column definitions, you can pass this instead of "columnIds" */ + columnDefinitions?: Column[]; + + /** Used for defining the operation name when building the GraphQL query */ + operationName?: string; + + /** Use Pagination Cursor in the GraphQL Server. Note: previously named `isWithCursor */ + useCursor?: boolean; + + /** What are the pagination options? ex.: (first, last, offset) */ + paginationOptions?: GraphqlPaginationOption | GraphqlCursorPaginationOption; + + /** array of Filtering Options, ex.: { field: name, operator: EQ, value: "John" } */ + filteringOptions?: GraphqlFilteringOption[]; + + /** array of Filtering Options, ex.: { field: name, direction: DESC } */ + sortingOptions?: GraphqlSortingOption[]; + + /** + * Do we want to keep double quotes on field arguments of filterBy/sortBy (field: "name" instead of field: name) + * ex.: { field: "name", operator: EQ, value: "John" } + */ + keepArgumentFieldDoubleQuotes?: boolean; + + /** + * When false, searchTerms may be manipulated to be functional with certain filters eg: string only filters. + * When true, JSON.stringify is used on the searchTerms and used in the query "as-is". It is then the responsibility of the developer to sanitise the `searchTerms` property if necessary. + */ + useVerbatimSearchTerms?: boolean; +} +``` + +#### Grid Definition & call of `backendServiceApi` +##### Notes +- Pagination is optional and if not defined, it will use what is set in the [Slickgrid-Universal - Global Options](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/global-grid-options.ts) +- `onInit` is optional and is there to initialize the grid with data on first page load (typically the same call as `process`) + - you could load the grid yourself outside of the `gridOptions` which is why it's optional +- `filterTypingDebounce` is a timer (in milliseconds) that waits for user input pause before querying the backend server + - this is meant to throttle the amount of requests sent to the backend (we don't really want to query every keystroke) + - 700ms is the default when not provided + +##### Code +```vue + +``` + +### Extra Query Arguments +You can pass extra query arguments to the GraphQL query via the `extraQueryArguments` property defined in the `backendServiceApi.options`. For example let say you have a list of users and your GraphQL query accepts an optional `userId`, you can write it in code this way: +```vue + +``` + +The GraphQL query built with these options will be +```ts +// extraQueryArguments will change the userId with +{ + users(first: 20, offset: 0, userId: 567) { + totalCount, + nodes { + id, + name, + company + } + } +} +``` + +### Changing/Updating Options Dynamically +You might want to change certain options dynamically, for example passing new set of values to `extraQueryArguments`. For that you will have to first keep a reference to your `GraphqlService` instance and then you can call the `updateOptions` method. + +##### Code Example +```vue + +``` + +### GraphQL without Pagination +By default, the Pagination is enabled and will produce a GraphQL query which includes page related information but you could also use the GraphQL Service without Pagination if you wish by disabling the flag `enablePagination: false` in the Grid Options. However please note that the GraphQL Query will be totally different since it won't include any page related information. + +#### Code Example +```ts +gridOptions.value = { + enablePagination: false, + backendServiceApi: { + service: graphqlService, + // ... + } +}; +``` + +#### Query Change Example +If we take for example a GrahQL Query that includes Pagination versus without Pagination, you will see a much simpler query string. Also, note that the filtering and sorting won't be affected, they will remain as query input. + +##### with Pagination +1. `query{ users(first:20, offset:40){ totalCount, nodes{ id, field1, field2 }}}` +2. `query{ users(first:20, offset:40, filterBy: [{ field: field1, value: 'test', operator: StartsWith }]){ totalCount, nodes{ id, field1, field2 }}}` + +##### without Pagination +1. `query{ users{ id, field1, field2 }}` +2. `query{ users(filterBy: [{ field: field1, value: 'test', operator: StartsWith }]){ id, field1, field2 }}` + +## GraphQL Server Definitions +For the implementation of all 3 actions (filtering, sorting, pagination) with your GraphQL Server, please refer to the sections below to configure your GraphQL Schema accordingly. +- [Pagination](graphql/GraphQL-Pagination.md) +- [Sorting](graphql/GraphQL-Sorting.md) +- [Filtering](graphql/GraphQL-Filtering.md) diff --git a/frameworks/slickgrid-vue/docs/backend-services/OData.md b/frameworks/slickgrid-vue/docs/backend-services/OData.md new file mode 100644 index 000000000..d3b009330 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/backend-services/OData.md @@ -0,0 +1,250 @@ +##### index +- [TypeScript signature](#typescript-signature) +- [Usage](#grid-definition--call-of-backendserviceapi) +- [Passing Extra Arguments](#passing-extra-arguments-to-the-query) +- [OData options](#odata-options) +- [Override the filter query](#override-the-filter-query) +- [Infinite Scroll](../grid-functionalities/infinite-scroll.md#infinite-scroll-with-backend-services) + +### Description +OData Backend Service (for Pagination purposes) to get data from a backend server with the help of OData. + +### Demo +[Demo Page](https://ghiscoding.github.io/slickgrid-vue/#/Example5) / [Demo ViewModel](https://github.com/ghiscoding/slickgrid-vue/blob/master/src/examples/slickgrid/Example5.tsx) + +### Note +Use it when you need to support **Pagination** (that is when your dataset is rather large, more than 5k rows) with a OData endpoint. If your dataset is small (less than 5k rows), then go with a [regular grid](https://ghiscoding.github.io/slickgrid-vue/#/Example1) with the `[dataset]` binding property. SlickGrid can easily handle million of rows using a DataView object, but personally when the dataset is known to be large, I usually use a backend service (OData or GraphQL) and when it's small I go with a [regular grid](https://ghiscoding.github.io/slickgrid-vue/#/Example1). + +## Implementation +To connect a backend service into `Slickgrid-Universal`, you simply need to modify your `gridOptions` and add a declaration of `backendServiceApi` and pass it the `service`. See below for the signature and an example further down below. + +### IMPORTANT NOTE +All the code below assumes that your Backend Server (probably in C#) will return the data into an `items` property. You could return the array directly **but it is strongly discouraged to do that** because that will conflict with the `metrics` that you will see in the code below. The best approach is to return your data into a property, like `items` or any property name you wish to use, on your backend server side. Your result should have this kind of structure +```ts +{ + items: [ /* your data */ ] +} +``` + +### TypeScript Signature +```typescript +backendServiceApi: { + // Backend Service instance (could be OData or GraphQL Service) + service: BackendService; + + // add any options you might want to provide to the backend service + options: OdataOption | GraphqlServiceOption; + + // On init (or on page load), what action to perform? + onInit?: (query: string) => Promise; + + // Before executing the query, what action to perform? For example, start a spinner + preProcess?: () => void; + + // On Processing, we get the query back from the service, and we need to provide a Promise. For example: http.get(myGraphqlUrl) + process: (query: string) => Promise; + + // After executing the query, what action to perform? For example, stop the spinner + postProcess: (response: any) => void; + + // Throttle the amount of requests sent to the backend. Default to 500ms + filterTypingDebounce?: number; +} +``` +As you can see, you mainly need to define which service to use (GridODataService or GraphQLService) and finally add the `process` and `postProcess` callback. + +#### Grid Definition & call of `backendServiceApi` +##### Notes +- Pagination is optional and if not defined, it will use what is set in the [Slickgrid-Universal - Global Options](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/global-grid-options.ts) +- `onInit` is optional and is there to initialize (pre-populate) the grid with data on first page load (typically the same call as `process`) + - you could load the grid yourself outside of the `gridOptions` which is why it's optional +- `filterTypingDebounce` is a timer (in milliseconds) that waits for user input pause before querying the backend server + - this is meant to throttle the amount of requests sent to the backend (we don't really want to query every keystroke) + - 700ms is the default when not provided + +##### Component +```vue + +``` + +### Passing Extra Arguments to the Query +You might need to pass extra arguments to your OData query, for example passing a `userId`, you can do that simply by modifying the query you sent to your `process` callback method. For example +```ts +// Web API call +getCustomerApiCall(odataQuery) { + const finalQuery = `${odataQuery}$filter=(userId eq 12345)`; + return http.get(`/api/getCustomers?${finalQuery}`); +} +``` + +## OData options + +All options can be found here: [Slickgrid-Universal - OData Options](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/odata/src/interfaces/odataOption.interface.ts) + +Some are described in more detail below. + +### OData version + +By default the OData version is set to 2 because it was implemented with that version. If you wish to use version 4, then just change the `version: 4`, there are subtle differences. + +```ts +gridOptions.value = { + backendServiceApi: { + service: new GridOdataService(), + options: { + enableCount: true, // add the count in the OData query, which will return a property named "odata.count" (v2) or "@odata.count" (v4) + version: 4 // defaults to 2, the query string is slightly different between OData 2 and 4 + } as OdataOption, + process: (query) => getCustomerApiCall(query), + postProcess: (response) => { + metrics.value = response.metrics; + displaySpinner(false); + getCustomerCallback(response); + } + } as OdataServiceApi +}; +``` + +### Query total items count + +The total items count can be queried from the backend by: +```ts +const oDataOptions: OdataOption = { + enableCount: true; +} +``` + +When enabled that will add `$inlinecount=allpages` (v2/v3) or `$count=true` (v4) to the query. And the count from the backend's response is extracted and `pagination.totalItems` is updated with that count. The property in the response that is used depends on the oData version specified: `d.__count` for v2, `__count` for v3 and `@odata.count` for v4. If needed a custom extractor function can be set through `oDataOptions.countExtractor`. + +### Query only the grid column's fields + +Query only the grid column's fields from the backend by: +```ts +const oDataOptions: OdataOption = { + enableSelect: true; +} +``` + +For example `columns: [{ id: 'col1', field: 'field1' }, { id: 'col2', field: 'field2' }]` results in the query: `?$select=id,field1,field2`. + +A property `id` is always selected from the backend because the grid requires it. This property can be changed by setting `gridOptions.datasetIdPropertyName`. + +### Query related resources / expand navigation properties + +Specify that related resources (navigation properties) should be retrieved from the backend: +```ts +const oDataOptions: OdataOption = { + enableExpand: true; +} +``` + +A navigation property is identified as a field having `/` in it's name. For example `columns: [{ id: 'col1', field: 'nav1/field1' }, { id: 'col2', field: 'nav2/field1' }]` results in the query `?$expand=nav1,nav2` + +Often `enableSelect` and `enableExpand` are used in conjunction. And with oData v4 then also navigation properties are selected from the backend. For example `columns: [{ id: 'col1', field: 'nav1/field1' }, { id: 'col2', field: 'nav2/field1' }]` results in the query `?$select=id,$expand=nav1($select=field1),nav2($select=field2)` + +```ts +const oDataOptions: OdataOption = { + enableSelect: true; + enableExpand: true; + version: 4 +} +``` + +Navigations within navigations are also supported. For example `columns: [{ id: 'col1', field: 'nav1/subnav1/field1' }]`. + +The dataset from the backend is automatically extracted and navigation fields are flattened so the grid can display them and sort/filter just work. The exact property that is used as the dataset depends on the oData version: `d.results` for v2, `results` for v3 and `value` for v4. If needed a custom extractor function can be set through `oDataOptions.datasetExtractor`. +For example if the backend responds with `{ value: [{ id: 1, nav1: { field1: 'x' }, { nav2: { field2: 'y' } } ] }` this will be flattened to `{ value: [{ id: 1, 'nav1/field1': 'x', 'nav2/field2': 'y' } ] }`. + +### Override the filter query + +Column filters may have a `Custom` operator, that acts as a placeholder for you to define your own logic. To do so, the easiest way is to provide the `filterQueryOverride` callback in the OdataOptions. This method will be called with `BackendServiceFilterQueryOverrideArgs` to let you decide dynamically on how the filter should be assembled. + +E.g. you could listen for a specific column and the active OperatorType.custom in order to switch the filter to a matchesPattern SQL LIKE search: + +```ts +backendServiceApi: { + options: { + filterQueryOverride: ({ fieldName, columnDef, columnFilterOperator, searchValues }) => { + if (columnFilterOperator === OperatorType.custom && columnDef?.id === 'name') { + let matchesSearch = searchValues[0].replace(/\*/g, '.*'); + matchesSearch = matchesSearch.slice(0, 1) + '%5E' + matchesSearch.slice(1); + matchesSearch = matchesSearch.slice(0, -1) + '$\''; + + return `matchesPattern(${fieldName}, ${matchesSearch})`; + } + }, + } +} + +``` diff --git a/frameworks/slickgrid-vue/docs/backend-services/custom-backend-service.md b/frameworks/slickgrid-vue/docs/backend-services/custom-backend-service.md new file mode 100644 index 000000000..27a5e21c1 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/backend-services/custom-backend-service.md @@ -0,0 +1,68 @@ +### Intro +The lib currently supports OData and GraphQL with built-in Services, if you want to use and create a different and Custom Backend Service, then follow the steps below. + +### Instructions +To create your own Custom Backend Service, I suggest you take the code of the [GraphqlService](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/graphql/src/services/graphql.service.ts) and then rewrite the internal of each methods. The thing to remember is that you have to implement the `BackendService` as defined in the GraphqlService (`export class GraphqlService implements BackendService`). + +You typically want to implement your service following these TypeScript interfaces +- [backendService.interface.ts](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/interfaces/backendService.interface.ts) +- [backendServiceApi.interface.ts](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/interfaces/backendServiceApi.interface.ts) +- [backendServiceOption.interface.ts](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/interfaces/backendServiceOption.interface.ts) + +At the end of it, you'll have a Custom Backend Service that will be instantiated just like the GraphQL or OData that I've created, it should look similar to this (also note, try to avoid passing anything in the `constructor` of your Service to keep it usable by everyone) +```vue + +``` + +If you need to reference your Service for other purposes then you better instantiate it in a separate variable and then just pass it to the `service` property of the `backendServiceApi`. +```vue + +``` + +If your Service is for a well known DB or API framework, then it might be possible to add your Service to the lib itself, however it should be added to the new monorepo lib [Slickgrid-Universal](https://github.com/ghiscoding/slickgrid-universal) in the list of [slickgrid-universal/packages](https://github.com/ghiscoding/slickgrid-universal/tree/master/packages). Since that is a monorepo lib, users will have the ability to use and download only the package they need. diff --git a/frameworks/slickgrid-vue/docs/backend-services/graphql/GraphQL-Filtering.md b/frameworks/slickgrid-vue/docs/backend-services/graphql/GraphQL-Filtering.md new file mode 100644 index 000000000..02dd4e741 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/backend-services/graphql/GraphQL-Filtering.md @@ -0,0 +1,162 @@ +##### index +- [filterBy](#filterby) +- [Complex Objects](#complex-objects) +- [Extra Query Arguments](#extra-query-arguments) +- [Override the filter query](#override-the-filter-query) + +### Introduction +The implementation of a GraphQL Service requires a certain structure to follow for `Slickgrid-Universal` to work correctly (it will fail if your GraphQL Schema is any different than what is shown below). + +### Implementation +For the implementation in your code, refer to the [GraphQL Service](../GraphQL.md) section. + +### filterBy +The filtering uses `filterBy` with a structure which we think is flexible enough. The query will have a `filterBy` argument with an array of filter properties: +- `filterBy`: array of filter object(s) (see below) + - `field`: field name to filter + - `value`: search filter value + - `operator`: a GraphQL enum (server side) that can have 1 of these choices: + - `LT`, `LE`, `GT`, `GE`, `NE`, `EQ`, `Contains`, `Not_Contains`, `StartsWith`, `EndsWith`, `IN`, `NIN` + - `Contains` is the default and will be used (by the grid) when operator is not provided + - `IN` and `NIN` (alias to `NOT_IN`) are mainly used for multi-select filtering + +**Note:** the `filterBy` order is following the order of how the filter objects were entered in the array. + +For example, a filter that would search for a firstName that starts with "John" +- matches: "John", "Johnny", ... +```typescript +users (first: 20, offset: 10, filterBy: [{field: firstName, operator: StartsWith, value: 'John'}]) { + totalCount + nodes { + name + firstName + lastName + gender + } +} +``` + +### Complex Objects +Dealing with complex objects are a little bit more involving. Because of some limitation with our [GraphQL for .Net](https://github.com/graphql-dotnet/graphql-dotnet) implementation, we decided to leave `field` as regular strings and keep the dot notation within the string. For that behavior to work, a new `keepArgumentFieldDoubleQuotes` property was added that can be passed to the GraphQL `initOptions()` function. For example, given a complex object field (defined in the Column Definition) that is `field: "billing.street"` will give this GraphQL query (if you have `keepArgumentFieldDoubleQuotes` set to True). + +##### Grid Definition example +```vue + +``` + +##### GraphQL Query +```typescript +// the orderBy/filterBy fields will keep the dot notation while nodes are exploded +{ + users(first: 20, offset: 0, filterBy: [{field: "billing.address.street", operator: EQ, value: "123 Queens Street"}]) { + totalCount, + nodes { + name, + company, + billing { + address { + street, + zip + } + } + } + } +} +``` + +From the previous example, you can see that the `orderBy` keeps the (.) dot notation, while the `nodes` is exploded as it should `billing { street }}`. So keep this in mind while building your backend GraphQL service. + +### Extra Query Arguments +If you want to pass extra arguments outside of the `filterBy` argument, that will be used for the life of the grid. You can do so by using the `extraQueryArguments` which accept an array of field/value. For example, let say you want to load a grid of items that belongs to a particular user (with a `userId`). You can pass the `extraQueryArguments` to the `backendServiceApi` `options` property, like so + +##### Component +```typescript +gridOptions.value = { + backendServiceApi: { + service: new GraphqlService(), + process: (query) => userService.getAll(query), + options: { + columnDefinitions: columnDefinitions, + datasetName: 'users', + extraQueryArguments: [{ + field: 'userId', + value: 123 + }] + } + } +}; +``` + +##### GraphQL Query +```typescript +// the orderBy/filterBy fields will keep the dot notation while nodes are exploded +{ + users(first: 20, offset: 0, userId: 123) { + totalCount, + nodes { + name, + company, + billing { + address { + street, + zip + } + } + } + } +} +``` + +### Override the filter query + +Column filters may have a `Custom` Operator, that acts as a placeholder for you to define your own logic. To do so, the easiest way is to provide the `filterQueryOverride` callback in the GraphQL Options. This method will be called with `BackendServiceFilterQueryOverrideArgs` to let you decide dynamically on how the filter should be assembled. + +E.g. you could listen for a specific column and the active `OperatorType.custom` in order to switch the filter to an SQL LIKE search in GraphQL: + +> **Note** technically speaking GraphQL isn't a database query language like SQL, it's an application query language. Depending on your configuration, your GraphQL Server might already support regex querying (e.g. Hasura [_regex](https://hasura.io/docs/latest/queries/postgres/filters/text-search-operators/#_regex)) or you could add your own implementation (e.g. see this SO: https://stackoverflow.com/a/37981802/1212166). Just make sure that whatever custom operator that you want to use, is already included in your GraphQL Schema. +```ts +backendServiceApi: { + options: { + filterQueryOverride: ({ fieldName, columnDef, columnFilterOperator, searchValues }) => { + if (columnFilterOperator === OperatorType.custom && columnDef?.id === 'name') { + // the `operator` is a string, make sure to implement this new operator in your GraphQL Schema + return { field: fieldName, operator: 'Like', value: searchValues[0] }; + } + }, + } +} +``` diff --git a/frameworks/slickgrid-vue/docs/backend-services/graphql/GraphQL-JSON-Result.md b/frameworks/slickgrid-vue/docs/backend-services/graphql/GraphQL-JSON-Result.md new file mode 100644 index 000000000..694f014cf --- /dev/null +++ b/frameworks/slickgrid-vue/docs/backend-services/graphql/GraphQL-JSON-Result.md @@ -0,0 +1,85 @@ +The GraphQL JSON result will always follow a certain structure where only the dataset name and the `nodes` array will change. With that in mind, if we look at the `GraphqlResult` TypeScript interface, the JSON result will mostly follow this structure (except when Pagination is disabled if so continue reading): + +#### `GraphqlResult` TypeScript interface +The `datasetName` is the only dynamic portion of the structure and in our demo will be `users`. + +##### with Pagination +```ts +export interface GraphqlPaginatedResult { + data: { + [datasetName: string]: { + /** result set of data objects (array of data) */ + nodes: any[]; + + /** Total count of items in the table (needed for the Pagination to work) */ + totalCount: number; + } + }; + + /** Some metrics of the last executed query (startTime, endTime, executionTime, itemCount, totalItemCount) */ + metrics?: Metrics; +} +``` + +##### without Pagination +```ts +export interface GraphqlResult { + data: { + [datasetName: string]: any[]; + }; + + /** Some metrics of the last executed query (startTime, endTime, executionTime, itemCount, totalItemCount) */ + metrics?: Metrics; +} +``` + +### ResultSet +#### Users demo (with Pagination) +If we consider that we defined a grid of Users and we provided the `datasetName: 'users'` with 3 defined columns (firstName, lastName, email), note that `id` will **always** be included as it is a requirement from SlickGrid itself and it must be unique ids. The JSON result could look like the following: + +```ts +{ + "data": { + "users": { + "totalCount": 2, + "nodes": [ + { + "id": 0, + "firstName": "John", + "lastName": "Doe", + "email": "john@doe.com" + }, + { + "id": 1, + "firstName": "Jane", + "lastName": "Doe", + "email": "john@doe.com" + } + ] + } + } +} +``` + +#### Users demo (**without** Pagination) + +```ts +{ + "data": { + "users": [ + { + "id": 0, + "firstName": "John", + "lastName": "Doe", + "email": "john@doe.com" + }, + { + "id": 1, + "firstName": "Jane", + "lastName": "Doe", + "email": "john@doe.com" + } + ] + } +} +``` \ No newline at end of file diff --git a/frameworks/slickgrid-vue/docs/backend-services/graphql/GraphQL-Pagination.md b/frameworks/slickgrid-vue/docs/backend-services/graphql/GraphQL-Pagination.md new file mode 100644 index 000000000..a192ed9f1 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/backend-services/graphql/GraphQL-Pagination.md @@ -0,0 +1,77 @@ +The implementation of a GraphQL Service requires a certain structure to follow for `Slickgrid-Universal` to work correctly (it will fail if your GraphQL Schema is any different than what is shown below). + +### Implementation +For the implementation in your code, refer to the [GraphQL Service](../GraphQL.md) section. + +### Without Cursor (recommended) +Pagination without cursor, this is the simplest implementation and is what we use on our side. The query can have any of the 3 arguments: +- `first`: integer representing how many rows of data to get from the start of dataset +- `last`: integer representing how many rows of data to get from the end of dataset +- `offset`: integer representing how many to skip + +For example +```ts +users (first:20, offset: 10) { + totalCount + nodes { + name + gender + } +} +``` + +### With Cursor `useCursor` +Cursor Pagination is more generally used for real-time data scenarios. It usually reads sequentially from the head or tail of a list. It cannot navigate directly to the middle of a list. It conceptually treats the data similarly to a LinkedList as opposed to a Vector. + +Pagination with cursor, the query can have any of the 4 arguments: +- `first`: integer representing how many rows of data to get from the start of dataset +- `after`: pull data starting at `cursor` "x", where "x" is the last item `cursor` +- `last`: integer representing how many rows of data to get from the end of dataset +- `before`: pull data before a `cursor` "x", where "x" is the last item `cursor` + +For example +```ts +users (first:20, after:"YXJyYXljb25uZWN0aW9uOjM=") { + totalCount + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + node { + name + gender + } + } +} +``` + +To retrieve subsequent data, the `pageInfo.endCursor` property should be used as part of the next query. +eg: +```ts +users (first:20, after:"${pageInfo.endCursor}") +``` + +or when navigating backwards +```ts +users (last:20, before:"${pageInfo.startCursor}") +``` + +When using the `paginationService`, this is handled by calling `setCursorPageInfo(pageInfo)`. + +Also note the difference in behaviour between `relay` style pagination as it affects the returned `pageInfo` object. +eg +```ts +relay pagination - Infinte scrolling appending data + page1: {startCursor: A, endCursor: B } + page2: {startCursor: A, endCursor: C } + page3: {startCursor: A, endCursor: D } + +non-relay pagination - Getting page chunks + page1: {startCursor: A, endCursor: B } + page2: {startCursor: B, endCursor: C } + page3: {startCursor: C, endCursor: D } +``` \ No newline at end of file diff --git a/frameworks/slickgrid-vue/docs/backend-services/graphql/GraphQL-Sorting.md b/frameworks/slickgrid-vue/docs/backend-services/graphql/GraphQL-Sorting.md new file mode 100644 index 000000000..d027cf0e9 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/backend-services/graphql/GraphQL-Sorting.md @@ -0,0 +1,84 @@ +The implementation of a GraphQL Service requires a certain structure to follow for `Slickgrid-Universal` to work correctly (it will fail if your GraphQL Schema is any different than what is shown below). + +### Implementation +For the implementation in your code, refer to the [GraphQL Service](../GraphQL.md) section. + +### orderBy +The sorting uses `orderBy` as per this [GitHub Suggestion](https://github.com/graphql/graphql-relay-js/issues/20#issuecomment-220494222) of a Facebook employee. The query will have a `orderBy` argument with an array of filter properties: +- `orderBy`: array of sorting object(s) (see below) + - `field`: field name to sort + - `direction`: a GraphQL enum (server side) that can have 1 of these choices: + - `ASC`, `DESC` + +**Note:** the `orderBy` order is following the order of how the filter objects were entered in the array. + +For example +```ts +users (first: 20, offset: 10, orderBy: [{field: lastName, direction: ASC}, {field: firstName, direction: DESC}]) { + totalCount + nodes { + name + firstName + lastName + gender + } +} +``` + +### Complex Objects +Dealing with complex objects are a little bit more involving. Because of some limitation with our [GraphQL for .Net](https://github.com/graphql-dotnet/graphql-dotnet) implementation, we decided to leave `field` as regular strings and keep the dot notation within the string. For that behavior to work, a new `keepArgumentFieldDoubleQuotes` property was added that can be passed to the GraphQL `initOptions()` function. For example, given a complex object field (defined in the Column Definition) that is `field: "billing.street"` will give this GraphQL query (if you have `keepArgumentFieldDoubleQuotes` set to True). + +##### Grid Definition example +```vue + +``` + +##### GraphQL Query + +```ts +// the orderBy/filterBy fields will keep the dot notation while nodes are exploded +{ + users(first: 20, offset: 0, orderBy: [{field: "billing.address.street", direction: ASC}]) { + totalCount, + nodes { + name, + company, + billing { + address { + street, + zip + } + } + } + } +} +``` + +From the previous example, you can see that the `orderBy` keeps the (.) dot notation, while the `nodes` is exploded as it should `billing { street }}`. So keep this in mind while building your backend GraphQL service. diff --git a/frameworks/slickgrid-vue/docs/column-functionalities/cell-menu.md b/frameworks/slickgrid-vue/docs/column-functionalities/cell-menu.md new file mode 100644 index 000000000..321c86b06 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/column-functionalities/cell-menu.md @@ -0,0 +1,213 @@ +#### index +- [Default Usage](#default-usage) +- [Action Callback Methods](#action-callback-methods-preferred-approach) +- [Override Callback Methods](#override-callback-methods) +- [How to add Translations](#how-to-add-translations) +- [How to Disable Cell Menu](#how-to-disable-the-cell-menu) +- [UI Sample](#ui-sample) + +### Demo +[Demo Page](https://ghiscoding.github.io/slickgrid-vue/#/Example24) / [Demo ViewModel](https://github.com/ghiscoding/slickgrid-vue/blob/master/src/examples/slickgrid/Example24.tsx) + +### Description +A Cell Menu, most often used as an Action Menu and is more oriented on a row action (e.g. delete current row), it could be defined on 1 or more columns (defined in a column definition) and is triggered by a cell click or touch. The menu can show a list of Commands (to execute an action) and/or Options (to change the value of a field). Also note that the Commands list is following the same structure used in the [Context Menu](../grid-functionalities/Context-Menu.md), [Header Menu](../grid-functionalities/Header-Menu-&-Header-Buttons.md) & [Grid Menu](../grid-functionalities/Grid-Menu.md). The Cell Menu is very similar to the [Context Menu](../grid-functionalities/Context-Menu.md), both were create as SlickGrid plugins during the same period, their main difference is that they get triggered differently (cell click vs mouse right+click) and they serve different purposes. The Cell Menu is more oriented on a row action (e.g. delete current row) while the Context Menu is all about actions for the entire grid (e.g. export to Excel). + +This extensions is wrapped around the new SlickGrid Plugin **SlickCellMenu** + +### Default Usage +To use the Cell Menu, you will need to enable it in the Grid Options and also define its structure in the chose column. You can customize the menu with 2 different lists, Commands and/or Options, they can be used separately or at the same time (same as [Context Menu](../grid-functionalities/Context-Menu.md)). However please note that you will also need to use a Custom Formatter to display the Action button/text, it's easy enough as you can see below. Also note that even though the code shown below makes a separation between the Commands and Options, you can mix them in the same Cell Menu. + +#### with Commands +```ts +columnDefinitions.value = [ + { id: 'firstName', field: 'firstName', name: 'First Name' }, + { id: 'lastName', field: 'lastName', name: 'Last Name' }, + // ... more column defs + { + id: 'action', name: 'Action', field: 'action', width: 110, maxWidth: 200, + excludeFromExport: true, // you typically don't want this column exported + formatter: actionFormatter, // your Custom Formatter + cellMenu: { + commandTitle: 'Commands', // optional title + commandItems: [ + // array of command item objects, you can also use the "positionOrder" that will be used to sort the items in the list + { + command: 'command1', title: 'Command 1', positionOrder: 61, + // you can use the "action" callback and/or use "onCommand" callback from the grid options, they both have the same arguments + action: (e, args) => { + console.log(args.dataContext, args.column); // action callback.. do something + } + }, + { command: 'help', title: 'HELP', iconCssClass: 'mdi mdi-help-circle', positionOrder: 62 }, + // you can add sub-menus by adding nested `commandItems` + { + // we can also have multiple nested sub-menus + command: 'export', title: 'Exports', positionOrder: 99, + commandItems: [ + { command: 'exports-txt', title: 'Text (tab delimited)' }, + { + command: 'sub-menu', title: 'Excel', cssClass: 'green', subMenuTitle: 'available formats', subMenuTitleCssClass: 'text-italic orange', + commandItems: [ + { command: 'exports-csv', title: 'Excel (csv)' }, + { command: 'exports-xlsx', title: 'Excel (xlsx)' }, + ] + } + ] + }, + ], + } + } +]; +``` + +#### with Options +That is when you want to define a list of Options (only 1 list) that the user can choose from and once an option is selected we would do something (for example change the value of a cell in the grid). +```ts +columnDefinitions.value = [ + { id: 'firstName', field: 'firstName', name: 'First Name' }, + { id: 'lastName', field: 'lastName', name: 'Last Name' }, + // ... more column defs + { + id: 'action', name: 'Action', field: 'action', width: 110, maxWidth: 200, + excludeFromExport: true, // you typically don't want this column exported + formatter: actionFormatter, // your Custom Formatter + cellMenu: { + optionTitle: 'Change Effort Driven Flag', // optional, add title + optionItems: [ + { option: true, title: 'True', iconCssClass: 'mdi mdi-check-box-outline' }, + { option: false, title: 'False', iconCssClass: 'mdi mdi-checkbox-blank-outline' }, + { divider: true, command: '', positionOrder: 60 }, + ], + // subscribe to Context Menu onOptionSelected event (or use the "action" callback on each option) + action: (e, args) => { + console.log(args.dataContext, args.column); // action callback.. do something + } + } + } +]; +``` + +### Action Callback Methods (preferred approach) +There are 2 ways to execute an action after a Command is clicked (or an Option is selected), you could do it via the `action` callback or via the `onCommand` callback. You might be wondering why 2 and what's the difference? Well, the `action` would have to be defined on every single Command/Option while the `onCommand` (or `onOptionSelected`) is more of a global subscriber which gets triggered every time any of the Command/Option is clicked/selected, so for that, you would typically need to use `if/else` or a `switch/case`... hmm ok but I still don't understand when would I use the `onCommand`? Let say you combine the Cell Menu with the [Context Menu](../grid-functionalities/Context-Menu.md) and some of the commands are the same, well, in that case, it might be better to use the `onCommand` and centralize your commands in that callback, while in most other cases if you wish to do only 1 thing with a command, then using the `action` might be better. Also, note that they could also both be used if you wish. + +So if you decide to use the `action` callback, then your code would look like this +##### with `action` callback +```ts +columnDefinitions.value = [ + { id: 'action', field: 'action', name: 'Action', + cellMenu: { + commandItems: [ + { command: 'command1', title: 'Command 1', action: (e, args) => console.log(args) }, + { command: 'command2', title: 'Command 2', action: (e, args) => console.log(args) } + // ... + ] + } + } +]; +``` + +##### with `onCommand` callback +The `onCommand` (or `onOptionSelected`) **must** be defined in the Grid Options + +```ts +columnDefinitions.value = [ + { id: 'action', field: 'action', name: 'Action', + cellMenu: { + commandItems: [ + { command: 'command1', title: 'Command 1' }, + { command: 'command2', title: 'Command 2' } + // ... + ] + } + } +]; + +gridOptions.value = { + enableCellMenu: true, + cellMenu: { + onCommand(e, args) => { + const columnDef = args.columnDef; + const command = args.command; + const dataContext = args.dataContext; + + switch (command) { + case 'command1': alert('Command 1'); break; + case 'command2': alert('Command 2'); break; + default: break; + } + } + } +}; +``` + +### Override Callback Methods +What if you want to dynamically disable or hide a Command/Option or even disable the entire menu in certain circumstances? For these cases, you would use the override callback methods, the method must return a `boolean`. The list of override available are the following +- `menuUsabilityOverride` returning false would make the Cell Menu unavailable to the user +- `itemVisibilityOverride` returning false would hide the item (command/option) from the list +- `itemUsabilityOverride` return false would disabled the item (command/option) from the list + - **note** there is also a `disabled` property that you could use, however it is defined at the beginning while the override is meant to be used with certain logic dynamically. + +For example, say we want the Cell Menu to only be available on the first 20 rows of the grid, we could use the override this way +```ts +columnDefinitions.value = [ + { id: 'action', field: 'action', name: 'Action', + cellMenu: { + menuUsabilityOverride: (args) => { + const dataContext = args && args.dataContext; + return (dataContext.id < 21); // say we want to display the menu only from Task 0 to 20 + }, + } + } +]; +``` + +To give another example, with Options this time, we could say that we enable the `n/a` option only when the row is Completed. So we could do it this way +```ts +columnDefinitions.value = [ + { id: 'action', field: 'action', name: 'Action', + cellMenu: { + optionItems: [ + { + option: 0, title: 'n/a', textCssClass: 'italic', + // only enable this option when the task is Not Completed + itemUsabilityOverride: (args) => { + const dataContext = args && args.dataContext; + return !dataContext.completed; + } + }, + { option: 1, iconCssClass: 'mdi mdi-star-outline yellow', title: 'Low' }, + { option: 2, iconCssClass: 'mdi mdi-star orange', title: 'Medium' }, + { option: 3, iconCssClass: 'mdi mdi-star red', title: 'High' }, + ] + } + } +]; +``` + +### How to add Translations? +It works exactly like the rest of the library when `enableTranslate` is set, all we have to do is to provide translations with the `Key` suffix, so for example without translations, we would use `title` and that would become `titleKey` with translations, that;'s easy enough. So for example, a list of Options could be defined as follow: +```ts +columnDefinitions.value = [ + { id: 'action', field: 'action', name: 'Action', + cellMenu: { + optionTitleKey: 'COMMANDS', // optionally pass a title to show over the Options + optionItems: [ + { option: 1, titleKey: 'LOW', iconCssClass: 'mdi mdi-star-outline yellow' }, + { option: 2, titleKey: 'MEDIUM', iconCssClass: 'mdi mdi-star orange' }, + { option: 3, titleKey: 'HIGH', iconCssClass: 'mdi mdi-star red' }, + ] + } + } +]; +``` + +### How to Disable the Cell Menu? +You can disable the Cell Menu, by calling `enableCellMenu: false` from the Grid Options. +```ts +gridOptions.value = { + enableCellMenu: false +}; +``` + +### UI Sample +![image](https://user-images.githubusercontent.com/643976/71301668-3aead800-2370-11ea-8ae5-acd124aff054.png) diff --git a/frameworks/slickgrid-vue/docs/column-functionalities/editors.md b/frameworks/slickgrid-vue/docs/column-functionalities/editors.md new file mode 100644 index 000000000..7becf60e5 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/column-functionalities/editors.md @@ -0,0 +1,626 @@ +#### index +* [Inline Editors](#how-to-use-inline-editors) + * [Demo with Float Editor & Dollar Formatter](#demo-with-float-editor-and-dollar-currency-formatter) + * [Editor `outputType` and `saveOutputType`](#editor-output-type--save-output-type) + * [Custom Editor](#custom-inline-editor) +* [Perform an Action after Inline Edit](#perform-an-action-after-inline-edit) +* [How to prevent Editor from going to the next bottom cell](#how-to-prevent-editor-from-going-to-the-next-bottom-cell) +* [onClick Action Editor (icon click)](#onclick-action-editor-icon-click) +* [AutoComplete Editor](#autocomplete-editor) +* [Select (single/multi) Editors](#select-editors) + - [Editor Options (multipleSelectOption interface)](#editor-options-multipleselectoption-interface) + - [Collection Async Load](#collection-async-load) + - [Collection Label Prefix/Suffix](#collection-label-prefixsuffix) + - [Collection Label Render HTML](#collection-label-render-html) + - [`multiple-select.js` Options](#multiple-selectjs-options) +- [Editor Options](#editor-options) +- [Validators](#validators) + - [Custom Validator](#custom-validator) +- [Disabling specific cell Edit](#disabling-specific-cell-edit) +- [Editors on Mobile Phone](#editors-on-mobile-phone) +- [Dynamically Change Column Editor](#dynamically-change-column-editor) + +## Description +`slickgrid-vue` ships with a few default inline editors (checkbox, dateEditor, float, integer, text, longText). You can see the full list [here](/ghiscoding/slickgrid-vue/tree/master/src/slickgrid-vue/editors). + +**Note:** For the Float Editor, you can provide decimal places with `params: { decimalPlaces: 2 }` to your column definition else it will be 0 decimal places by default. + +### Required Grid Option +Editors won't work without these 2 flags `enableCellNavigation: true` and `editable: true` enabled in your Grid Options, so make sure to always to always defined them. Also note that you can toggle the grid to read only (not editable) via the `editable` grid option flag. + +### Demo +##### with plain javascript +[Demo Page](https://ghiscoding.github.io/slickgrid-vue/#/slickgrid/Example3) / [Demo ViewModel](https://github.com/ghiscoding/slickgrid-vue/blob/master/src/examples/slickgrid/Example3.tsx) + +##### with Vue Custom Components +[Demo](https://ghiscoding.github.io/slickgrid-vue/#/slickgrid) / [Demo ViewModel](https://github.com/ghiscoding/slickgrid-vue/blob/master/src/examples/slickgrid/Example26.tsx) + + +### How to use Inline Editors +Simply call the editor in your column definition with the `Editors` you want, as for example (`editor: { model: Editors.text }`). Here is an example with a full column definition: +```vue + +``` + +#### Demo with Float Editor and Dollar Currency Formatter +This probably comes often, so here's all the setting you would need for displaying & editing a dollar currency value with 2 decimal places. +```vue + +``` + +#### Editor Output Type & Save Output Type +You could also define an `outputType` and a `saveOutputType` to an inline editor. There is only 1 built-in Editor with this functionality for now which is the `dateEditor`. For example, on a date field, we can call this `outputType: FieldType.dateIso` (by default it uses `dateUtc` as the output): +```vue + +``` + +So to make it more clear, the `saveOutputType` is the format that will be sent to the `onCellChange` event, then the `outputType` is how the date will show up in the date picker (Vanilla-Calendar) and finally the `type` is basically the input format (coming from your dataset). Note however that each property are cascading, if 1 property is missing it will go to the next one until 1 is found... for example, on the `onCellChange` if you aren't defining `saveOutputType`, it will try to use `outputType`, if again none is provided it will try to use `type` and finally if none is provided it will use `FieldType.dateIso` as the default. + +## Perform an action After Inline Edit +#### Recommended way +What is ideal is to bind to a SlickGrid Event, for that you can take a look at this [Wiki - On Events](../events/grid-dataview-events.md) + +#### Not recommended +You could also, perform an action after the item changed event with `onCellChange`. However, this is not the recommended way, since it would require to add a `onCellChange` on every every single column definition. + +## Custom Inline Editor +To create a Custom Editor, you need to create a `class` that will extend the [`Editors` interface](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/interfaces/editor.interface.ts) and then use it in your grid with `editor: { model: myCustomEditor }` and that should be it. + +**To use dependency injection with an `Editor` make sure your Vue dependencies are before the `args` constructor parameter. `args` must be the last parameter in your constructor because we wrap all `Editors` in Vue's `Factory` resolver so DI can work with slickgrid `Editors`** + +Once you are done with the class, just reference it's class name as the `editor`, for example: + +##### Class implementing Editor +```ts +export class IntegerEditor implements Editor { + constructor(private args: any) { + init(); + } + + init(): void {} + destroy() {} + focus() {} + loadValue(item: any) {} + serializeValue() {} + applyValue(item: any, state: any) {} + isValueChanged() {} + validate() {} +} +``` + +##### Use it in your Column Definition +For Custom Editor class example, take a look at [custom-inputEditor.ts](https://github.com/ghiscoding/slickgrid-vue/blob/master/src/examples/slickgrid/custom-inputEditor.ts) + +```vue + +``` + +## How to prevent Editor from going to the next bottom cell? +The default behavior or SlickGrid is to go to the next cell at the bottom of the current cell that you are editing. You can change and remove this behavior by enabling `autoCommitEdit` which will save current editor and remain in the same cell + +```vue + +``` +## OnClick Action Editor (icon click) +Instead of an inline editor, you might want to simply click on an edit icon that could call a modal window, or a redirect URL, or whatever you wish to do. For that you can use the inline `onCellClick` event and define a callback function for the action (you could also create your own [Custom Formatter](../column-functionalities/formatters.md)). +- The `Formatters.editIcon` will give you a pen icon, while a `Formatters.deleteIcon` is an "x" icon +```vue + +``` + +The `args` returned to the `onCellClick` callback is of type `OnEventArgs` which is the following: + +```ts +export interface OnEventArgs { + row: number; + cell: number; + columnDef: Column; + dataContext: any; + dataView: any; + grid: any; + gridDefinition: GridOption; +} +``` + +## AutoComplete Editor +The AutoComplete Editor has the same configuration (except for the `model: Editors.autoComplete`) as the AutoComplete Filter, so you can refer to the [AutoComplete Filter - Docs](../column-functionalities/filters/autocomplete-filter-kraaden.md) for more info on how to use it. + +## Select Editors +The library ships with two select editors: [singleSelectEditor](https://github.com/ghiscoding/slickgrid-vue/blob/master/slickgrid-vue/src/slickgrid-vue/editors/singleSelectEditor.ts) and the [multipleSelectEditor](https://github.com/ghiscoding/slickgrid-vue/blob/master/slickgrid-vue/src/slickgrid-vue/editors/multipleSelectEditor.ts). Both support the [multiple-select](https://github.com/ghiscoding/slickgrid-vue/blob/master/slickgrid-vue/assets/lib/multiple-select/multiple-select.js) library, but fallback to the bootstrap form-control style if you decide to exclude this library from your build. These editors will work with a list of foreign key values (custom structure not supported) and can be displayed properly with the [collectionFormatter](https://github.com/ghiscoding/slickgrid-vue/blob/master/slickgrid-vue/src/slickgrid-vue/formatters/collectionEditorFormatter.ts). [example 3](https://ghiscoding.github.io/slickgrid-vue/#/slickgrid/Example3) has all the details for you to get started with these editors. + +Here's an example with a `collection`, `collectionFilterBy` and `collectionSortBy` + +```vue + +``` + +### Editor Options (`MultipleSelectOption` interface) +All the available options that can be provided as `editorOptions` to your column definitions can be found under this [multipleSelectOption interface](https://github.com/ghiscoding/slickgrid-vue/blob/master/src/slickgrid-vue/models/multipleSelectOption.interface.ts) and you should cast your `editorOptions` to that interface to make sure that you use only valid options of the `multiple-select.js` library. + +```ts +editor: { + model: Editors.SingleSelect, + editorOptions: { + maxHeight: 400 + } as MultipleSelectOption +} +``` + +### Collection Async Load +You can also load the collection asynchronously, but for that you will have to use the `collectionAsync` property, which expect a Promise to be passed (it actually accepts 3 types: `HttpClient`, `FetchClient` or regular `Promise`). + +#### Load the collection through an Http call + +```vue + +``` + +#### Modifying the collection afterward +If you want to modify the collection afterward, you simply need to find the associated Column reference from the Column Definition and modify the `collection` property (not `collectionAsync` since that is only meant to be used on page load). + +For example +```vue + +``` + +### Collection Label Prefix/Suffix +You can use `labelPrefix` and/or `labelSuffix` which will concatenate the multiple properties together (`labelPrefix` + `label` + `labelSuffix`) which will used by each Select Filter option label. You can also use the property `separatorBetweenTextLabels` to define a separator between prefix, label & suffix. + +**Note** +If `enableTranslateLabel` flag is set to `True`, it will also try to translate the Prefix / Suffix / OptionLabel texts. + +For example, say you have this collection + +```ts +const currencies = [ + { symbol: '$', currency: 'USD', country: 'USA' }, + { symbol: '$', currency: 'CAD', country: 'Canada' } +]; +``` + +You can display all of these properties inside your dropdown labels, say you want to show (symbol with abbreviation and country name). Now you can. + +So you can create the `multipleSelect` Filter with a `customStructure` by using the symbol as prefix, and country as suffix. That would make up something like this: +- $ USD USA +- $ CAD Canada + +with a `customStructure` defined as + +```ts +editor: { + collection: currencies, + customStructure: { + value: 'currency', + label: 'currency', + labelPrefix: 'symbol', + labelSuffix: 'country', + collectionOptions: { + separatorBetweenTextLabels: ' ', // add white space between each text + includePrefixSuffixToSelectedValues: true // should the selected value include the prefix/suffix in the output format + }, + model: Editors.multipleSelect +} +``` + +### Collection Label Render HTML +By default HTML is not rendered and the `label` will simply show HTML as text. But in some cases you might want to render it, you can do so by enabling the `enableRenderHtml` flag. + +**NOTE:** this is currently only used by the Editors that have a `collection` which are the `MultipleSelect` & `SingleSelect` Editors. + +```vue + +``` + +### `multiple-select` Options +You can use any options from [Multiple-Select-Vanilla](https://github.com/ghiscoding/multiple-select-vanilla) and add them to your `filterOptions` property. + +Couple of small options were added to suit slickgrid-vue needs, which is why it points to `slickgrid-vue/lib` folder (which is our customized version of the original). This lib is required if you plan to use `multipleSelect` or `singleSelect` Filters. What was customized to (compare to the original) is the following: +- `okButton` option was added to add an OK button for simpler closing of the dropdown after selecting multiple options. + - `okButtonText` was also added for locale (i18n) +- `offsetLeft` option was added to make it possible to offset the dropdown. By default it is set to 0 and is aligned to the left of the select element. This option is particularly helpful when used as the last right column, not to fall off the screen. +- `autoDropWidth` option was added to automatically resize the dropdown with the same width as the select filter element. +- `autoAdjustDropHeight` (defaults to true), when set will automatically adjust the drop (up or down) height +- `autoAdjustDropPosition` (defaults to true), when set will automatically calculate the area with the most available space and use best possible choise for the drop to show (up or down) +- `autoAdjustDropWidthByTextSize` (defaults to true), when set will automatically adjust the drop (up or down) width by the text size (it will use largest text width) +- to extend the previous 3 autoAdjustX flags, the following options can be helpful + - `minWidth` (defaults to null, to use when `autoAdjustDropWidthByTextSize` is enabled) + - `maxWidth` (defaults to 500, to use when `autoAdjustDropWidthByTextSize` is enabled) + - `adjustHeightPadding` (defaults to 10, to use when `autoAdjustDropHeight` is enabled), when using `autoAdjustDropHeight` we might want to add a bottom (or top) padding instead of taking the entire available space + - `maxHeight` (defaults to 275, to use when `autoAdjustDropHeight` is enabled) + +##### Code +```vue + +``` + +## Editor Options + +#### Column Editor `editorOptions` +Some of the Editors could receive extra options, which is mostly the case for Editors using external dependencies (e.g. `autocompleter`, `date`, `multipleSelect`, ...) you can provide options via the `editorOptions`, for example + +```ts +columnDefinitions.value = [{ + id: 'start', name: 'Start Date', field: 'start', + editor: { + model: Editors.date, + editorOptions: { range: { date: 'today' } } as VanillaCalendarOption + } +}]; +``` + +#### Grid Option `defaultEditorOptions +You could also define certain options as a global level (for the entire grid or even all grids) by taking advantage of the `defaultEditorOptions` Grid Option. Note that they are set via the editor type as a key name (`autocompleter`, `date`, ...) and then the content is the same as `editorOptions` (also note that each key is already typed with the correct editor option interface), for example + +```ts +gridOptions.value = { + defaultEditorOptions: { + autocompleter: { debounceWaitMs: 150 }, // typed as AutocompleterOption + date: { range: { date: 'today' } }, // typed as VanillaCalendarOption, + longText: { cols: 50, rows: 5 } + } +} +``` + +## Validators + +Each Editor needs to implement the `validate()` method which will be executed and validated before calling the `save()` method. Most Editor will simply validate that the value passed is correctly formed. The Float Editor is one of the more complex one and will first check if the number is a valid float then also check if `minValue` or `maxValue` was passed and if so validate against them. If any errors is found it will return an object of type `EditorValidatorOutput` (see the signature on top). + +### Custom Validator + +If you want more complex validation then you can implement your own Custom Validator as long as it implements the following signature. + +```ts +export type EditorValidator = (value: any, args?: EditorArgs) => EditorValidatorOutput; +``` + +So the `value` can be anything but the `args` is interesting since it provides multiple properties that you can hook into, which are the following + +```ts +export interface EditorArgs { + column: Column; + container: any; + grid: any; + gridPosition: ElementPosition; + item: any; + position: ElementPosition; + cancelChanges?: () => void; + commitChanges?: () => void; +} +``` + +And finally the Validator Output has the following signature + +```ts +export interface EditorValidatorOutput { + valid: boolean; + msg?: string | null; +} +``` + +So if we take all of these informations and we want to create our own Custom Editor to validate a Title field, we could create something like this: + +```ts +const myCustomTitleValidator: EditorValidator = (value: any, args: EditorArgs) => { + // you can get the Editor Args which can be helpful, e.g. we can get the Translate Service from it + const grid = args && args.grid; + const gridOptions = (grid && grid.getOptions) ? grid.getOptions() : {}; + const i18n = gridOptions.i18n; + + if (value == null || value === undefined || !value.length) { + return { valid: false, msg: 'This is a required field' }; + } else if (!/^Task\s\d+$/.test(value)) { + return { valid: false, msg: 'Your title is invalid, it must start with "Task" followed by a number' }; + // OR use the Translate Service with your custom message + // return { valid: false, msg: i18n.tr('YOUR_ERROR', { x: value }) }; + } else { + return { valid: true, msg: '' }; + } +}; +``` + +and use it in our Columns Definition like this: + +```ts +columnDefinition.value = [ + { + id: 'title', name: 'Title', field: 'title', + editor: { + model: Editors.longText, + validator: myCustomTitleValidator, // use our custom validator + }, + onCellChange: (e: Event, args: OnEventArgs) => { + // do something + console.log(args.dataContext.title); + } + } +]; +``` + +## Disabling specific cell edit +This can be answered by searching on Stack Overflow Stack Overflow and this is the best [answer](https://stackoverflow.com/questions/10491676/disabling-specific-cell-edit-in-slick-grid) found. + +More info can be found in this [Docs - Grid & DataView Events](../events/grid-dataview-events.md). + +With that in mind and the code from the SO answer, we end up with the following code. + +#### View + +```vue + + + +``` + +### Editors on Mobile Phone +If your grid uses the `autoResize` and you use Editors in your grid on a mobile phone, Android for example, you might have undesired behaviors. It might call a grid resize (and lose input focus) since the touch keyboard appears. This in term, is a bad user experience to your user, but there is a way to avoid this, you could use the `pauseResizer` + +##### Component +```vue + + + +``` + +## Turning individual rows into edit mode +Using the [Row Based Editing Plugin](../grid-functionalities/Row-based-edit.md) you can let the user toggle either one or multiple rows into edit mode, keep track of cell changes and either discard or save them on an individual basis using a custom `onBeforeRowUpdated` hook. + +## Dynamically change Column Editor + +You can dynamically change a column editor by taking advantage of the `onBeforeEditCell` event and change the editor just before the cell editor opens. However please note that the library keeps 2 references and you need to update both references as shown below. + +With the code sample shown below, we are using an input checkbox to toggle the Editor between `Editors.longText` to `Editors.text` and vice/versa + +```ts +function changeToInputTextEditor(checked: boolean) { + isInputTextEditor = checked; +} + +function handleOnBeforeEditCell(args: CustomEvent) { + const args = event?.detail?.args; + const { grid, column } = args; + column.editor.model = isInputTextEditor ? Editors.text : Editors.longText; + column.editorClass = column.editor.model; + return true; +} +``` diff --git a/frameworks/slickgrid-vue/docs/column-functionalities/editors/autocomplete-editor-kraaden.md b/frameworks/slickgrid-vue/docs/column-functionalities/editors/autocomplete-editor-kraaden.md new file mode 100644 index 000000000..f270dd444 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/column-functionalities/editors/autocomplete-editor-kraaden.md @@ -0,0 +1,477 @@ +#### Index +- [Using fixed `collection` or `collectionAsync`](#using-collection-or-collectionasync) +- [Editor Options (`AutocompleterOption` interface)](#editor-options-autocompleteroption-interface) +- [Using Remote API](#using-external-remote-api) + - [Basic Usage](#remote-api-basic) + - [Basic Usage with Object Result (**preferred way**)](#remote-api-basic-with-object-result) + - [with `renderItem` + custom Layout (`twoRows` or `fourCorners`)](#remote-api-renderitem-callback--custom-layout-tworows-or-fourcorners) + - [Custom Styling - SASS variables](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/styles/_variables.scss#L141) +- [Force User Input](#autocomplete---force-user-input) +- [How to change drop container dimensions?](#how-to-change-drop-container-dimensions) +- [Animated Gif Demo](#animated-gif-demo) + - See the [Editors - Wiki](../Editors.md) for more general info about Editors (validators, event handlers, ...) + +### Demo +[Demo Page](https://ghiscoding.github.io/slickgrid-vue/#/slickgrid/Example3) | [Demo Component](https://github.com/ghiscoding/slickgrid-vue/blob/master/src/examples/slickgrid/Example3.tsx) + +### Introduction +AutoComplete is a functionality that let the user start typing characters and the autocomplete will try to give suggestions according to the characters entered. The collection can be a fixed JSON files (collection of strings or objects) or can also be an external remote resource to an external API. For a demo of what that could look like, take a look at the [animated gif demo](#animated-gif-demo) below. + +We use an external lib named [Autocomplete](https://github.com/kraaden/autocomplete) (aka `autocompleter` on npm) by Kraaden. + +## Using `collection` or `collectionAsync` +If you want to pass the entire list to the AutoComplete (like a JSON file or a Web API call), you can do so using the `collection` or the `collectionAsync` (the latter will load it asynchronously). You can also see that the Editor and Filter have almost the exact same configuration (apart from the `model` that is obviously different). + +##### Component +```vue + +``` + +### Collection Label Render HTML +By default HTML is not rendered and the `label` will simply show HTML as text. But in some cases you might want to render it, you can do so by enabling the `enableRenderHtml` flag. + +**NOTE:** this is currently only used by the Editors that have a `collection` which are the `MultipleSelect` & `SingleSelect` Editors. + +```typescript +columnDefinitions.value = [ + { + id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', + formatter: Formatters.checkmarkMaterial, + type: FieldType.boolean, + editor: { + model: Editors.autocompleter, + placeholder: '🔍 search city', + type: FieldType.string, + + // example with a fixed Collection (or collectionAsync) + editorOptions: { + showOnFocus: true, // display the list on focus of the autocomplete (without the need to type anything) + } as AutocompleterOption, + enableRenderHtml: true, // this flag only works with a fixed Collection + // collectionAsync: http.get(URL_COUNTRIES_COLLECTION), + collection: [ + { value: '', label: '' }, + { value: true, label: 'True', labelPrefix: ` ` }, + { value: false, label: 'False', labelPrefix: ` ` } + ], + } + } +]; +``` + +### Editor Options (`AutocompleterOption` interface) +All the available options that can be provided as `editorOptions` to your column definitions can be found under this [AutocompleterOption interface](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/interfaces/autocompleterOption.interface.ts) and you should cast your `editorOptions` to that interface to make sure that you use only valid options of the autocomplete library. + +```ts +editor: { + model: Editors.autocompleter, + editorOptions: { + minLength: 3, + } as AutocompleterOption +} +``` + +#### Grid Option `defaultEditorOptions +You could also define certain options as a global level (for the entire grid or even all grids) by taking advantage of the `defaultEditorOptions` Grid Option. Note that they are set via the editor type as a key name (`autocompleter`, `date`, ...) and then the content is the same as `editorOptions` (also note that each key is already typed with the correct editor option interface), for example + +```ts +gridOptions.value = { + defaultEditorOptions: { + autocompleter: { debounceWaitMs: 150 }, // typed as AutocompleterOption + } +} +``` + +## Using External Remote API +You could also use external 3rd party Web API (can be JSONP query or regular JSON). This will make a much shorter result since it will only return a small subset of what will be displayed in the AutoComplete Editor or Filter. For example, we could use GeoBytes which provide a JSONP Query API for the cities of the world, you can imagine the entire list of cities would be way too big to download locally, so this is why we use such API. + +### Remote API (basic) +The basic functionality will use built-in 3rd party lib styling that is to display a label/value pair item result. + +##### Component +```vue + +``` + +### Remote API (basic with object result) +This is the preferred way of dealing with the AutoComplete, the main reason is because the AutoComplete uses an `` and that means we can only keep 1 value and if we do then we lose the text label and so using an Object Result makes more sense. Note however that you'll need a bit more code that is because we'll use the `FieldType.Object` and so we need to provide a custom `SortComparer` and also a custom `Formatters` and for them to work we also need to provide a `dataKey` (the value) and a `labelKey` (text label) as shown below. +```ts +columnDefinitions.value = [ + { + id: 'product', name: 'Product', field: 'product', + dataKey: 'id', + labelKey: 'name', // (id/name) pair to override default (value/label) pair + editor: { + model: Editors.autocompleter, + alwaysSaveOnEnterKey: true, + type: 'object', + sortComparer: SortComparers.objectString, + editorOptions: { + showOnFocus: true, + minLength: 1, + fetch: (searchText, updateCallback) => { + // assuming your API call returns a label/value pair + yourAsyncApiCall(searchText) // typically you'll want to return no more than 10 results + .then(result => updateCallback((results.length > 0) ? results : [{ label: 'No match found.', value: '' }]); }) + .catch(error => console.log('Error:', error); + }, + } as AutocompleterOption, + } +]; +``` + +### Remote API with `renderItem` + custom layout (`twoRows` or `fourCorners`) +#### See animated gif ([twoRows](#with-tworows-custom-layout-without-optional-left-icon) or [fourCorners](#with-fourcorners-custom-layout-with-extra-optional-left-icon)) +The lib comes with 2 built-in custom layouts, these 2 layouts also have SASS variables if anyone wants to style it differently. When using the `renderItem`, it will require the user to provide a `layout` (2 possible options `twoRows` or `fourCorners`) and also a `templateCallback` that will be executed when rendering the AutoComplete Search List Item. For example: + +##### Component +```vue + +``` + +### Remote API `renderItem` callback + custom layout (`twoRows` or `fourCorners`) +#### See animated gif ([twoRows](#with-tworows-custom-layout-without-optional-left-icon) or [fourCorners](#with-fourcorners-custom-layout-with-extra-optional-left-icon)) +The previous example can also be written using the `renderItem` callback and adding `classes`, this is actually what Slickgrid-Universal does internally, you can do it yourself if you wish to have more control on the render callback result. + +##### Component +```vue + +``` + +#### with JSONP +Example from an external remote API (geobytes) returning a JSONP response. + +##### Component +```vue + +``` + +## Autocomplete - force user input +If you want to add the autocomplete functionality but want the user to be able to input a new option, then follow the example below: + +```ts +columnDefinitions.value = [{ + id: 'area', + name: 'Area', + field: 'area', + type: FieldType.string, + editor: { + model: Editors.autocompleter, + editorOptions: { + minLength: 0, + forceUserInput: true, + fetch: (searchText, updateCallback) => { + updateCallback(areas); // add here the array + }, + } + } +}]; +``` +You can also use the `minLength` to limit the autocomplete text to `0` characters or more, the default number is `3`. + +### How to change drop container dimensions? +You might want to change the dimensions of the drop container, this 3rd party library has a `customize` method to deal with such a thing. Slickgrid-Universal itself is removing the width using this method, you can however override this method to change the drop container dimensions + +```ts +columnDefinitions.value = [{ + id: 'product', name: 'Product', field: 'product', filterable: true, + editor: { + model: Editors.autocompleter, + alwaysSaveOnEnterKey: true, + + // example with a Remote API call + editorOptions: { + minLength: 1, + fetch: (searchTerm, callback) => { + // ... + }, + customize: (_input, _inputRect, container) => { + // change drop container dimensions + container.style.width = '250px'; + container.style.height = '325px'; + }, + } as AutocompleterOption, + }, +}]; +``` + +## Animated Gif Demo +### Basic (default layout) +![](https://user-images.githubusercontent.com/643976/50624023-d5e16c80-0ee9-11e9-809c-f98967953ba4.gif) + +### with `twoRows` custom layout (without optional left icon) +![](https://i.imgur.com/V9XzVXS.gif) + +### with `fourCorners` custom layout (with extra optional left icon) +![](https://i.imgur.com/LirGZFm.gif) diff --git a/frameworks/slickgrid-vue/docs/column-functionalities/editors/date-editor-flatpickr.md b/frameworks/slickgrid-vue/docs/column-functionalities/editors/date-editor-flatpickr.md new file mode 100644 index 000000000..3555688b6 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/column-functionalities/editors/date-editor-flatpickr.md @@ -0,0 +1,72 @@ +##### index +- [Editor Options](#editor-options) +- [Custom Validator](#custom-validator) +- See the [Editors - Wiki](../Editors.md) for more general info about Editors (validators, event handlers, ...) + +### Information +The Date Editor is provided through an external library named [Flatpickr](https://flatpickr.js.org/examples/) and all options from that library can be added to your `editorOptions` (see below), so in order to add things like minimum date, disabling dates, ... just review all the [Flatpickr Examples](https://flatpickr.js.org/examples/) and then add them into `editorOptions`. Also just so you know, `editorOptions` is use by all other editors as well to expose external library like Flatpickr, Multiple-Select.js, etc... + +### Demo +[Demo Page](https://ghiscoding.github.io/slickgrid-vue/#/slickgrid/Example3) | [Demo Component](https://github.com/ghiscoding/slickgrid-vue/blob/master/src/examples/slickgrid/Example3.tsx) + +### Editor Options +You can use any of the Flatpickr [options](https://flatpickr.js.org/options/) by adding them to `editorOptions` as shown below. + +#### [FlatpickrOption](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/interfaces/flatpickrOption.interface.ts) Interface. + +```ts +function defineGrid() { + columnDefinitions.value = [ + { + id: 'title', name: 'Title', field: 'title', + editor: { + model: Editors.date, + editorOptions: { + minDate: 'today', + disable: [(date: Date) => isWeekendDay(date)], // disable weekend days (Sat, Sunday) + } as FlatpickrOption, + }, + } + ]; +} + +/** Returns true when it's a weekend day (Saturday, Sunday) */ +function isWeekendDay(date: Date): boolean { + return (date.getDay() === 0 || date.getDay() === 6); +} +``` + +#### Grid Option `defaultEditorOptions +You could also define certain options as a global level (for the entire grid or even all grids) by taking advantage of the `defaultEditorOptions` Grid Option. Note that they are set via the editor type as a key name (`autocompleter`, `date`, ...) and then the content is the same as `editorOptions` (also note that each key is already typed with the correct editor option interface), for example + +```ts +gridOptions.value = { + defaultEditorOptions: { + date: { minDate: 'today' }, // typed as FlatpickrOption + } +} +``` + +### Custom Validator +You can add a Custom Validator from an external function or inline (inline is shown below and comes from [Example 3](https://ghiscoding.github.io/slickgrid-vue/#/slickgrid/Example3)) + +```ts +function defineGrid() { + columnDefinitions.value = [ + { + id: 'title', name: 'Title', field: 'title', + editor: { + model: Editors.date, + required: true, + validator: (value, args) => { + const dataContext = args && args.item; + if (dataContext && (dataContext.completed && !value)) { + return { valid: false, msg: 'You must provide a "Finish" date when "Completed" is checked.' }; + } + return { valid: true, msg: '' }; + } + }, + }, + ]; +} +``` diff --git a/frameworks/slickgrid-vue/docs/column-functionalities/editors/date-editor-vanilla-calendar.md b/frameworks/slickgrid-vue/docs/column-functionalities/editors/date-editor-vanilla-calendar.md new file mode 100644 index 000000000..a5974a890 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/column-functionalities/editors/date-editor-vanilla-calendar.md @@ -0,0 +1,99 @@ +##### index +- [Editor Options](#editor-options) +- [Custom Validator](#custom-validator) +- [Date Format](#date-format) +- See the [Editors - Wiki](../Editors.md) for more general info about Editors (validators, event handlers, ...) + +### Information +The Date Editor is provided through an external library named [Vanilla-Calendar-Pro](https://vanilla-calendar.pro) and all options from that library can be added to your `editorOptions` (see below), so in order to add things like minimum date, disabling dates, ... just review all the [Vanilla-Calendar-Pro](https://vanilla-calendar.pro/docs/reference/additionally/settings) and then add them into `editorOptions`. We use [Tempo](https://tempo.formkit.com/) to parse and format Dates to the chosen format (when `type`, `outputType` and/or `saveType` are provided in your column definition) + +> **Note** Also just so you know, `editorOptions` is used by all other editors as well to expose external library like Autocompleter, Multiple-Select, etc... + +### Demo +[Demo Page](https://ghiscoding.github.io/slickgrid-vue/#/example30) | [Demo Component](https://github.com/ghiscoding/slickgrid-vue/blob/master/src/examples/slickgrid/Example30.tsx) + +### Editor Options +You can use any of the Vanilla-Calendar [settings](https://vanilla-calendar.pro/docs/reference/additionally/settings) by adding them to `editorOptions` as shown below. + +> **Note** for easier implementation, you should import `VanillaCalendarOption` from Slickgrid-Universal common package. + +```ts +import { type Column, Filters, Formatters, SlickgridVue, type VanillaCalendarOption } from 'slickgrid-vue'; +import { onBeforeMount } from 'vue'; + +const gridOptions = ref(); +const columnDefinitions = ref([]); +const dataset = ref([]); + +onBeforeMount(() => { + defineGrid(); +}); + +function defineGrid() { + columnDefinitions.value = [ + { + id: 'title', name: 'Title', field: 'title', + type: 'dateIso', // if your type has hours/minutes, then the date picker will include date+time + editor: { + model: Editors.date, + editorOptions: { + range: { + max: 'today', + disabled: ['2022-08-15', '2022-08-20'], + } + } as VanillaCalendarOption, + }, + }, + ]; +} +``` + +#### Grid Option `defaultEditorOptions +You could also define certain options as a global level (for the entire grid or even all grids) by taking advantage of the `defaultEditorOptions` Grid Option. Note that they are set via the editor type as a key name (`autocompleter`, `date`, ...) and then the content is the same as `editorOptions` (also note that each key is already typed with the correct editor option interface), for example + +```ts +gridOptions.value = { + defaultEditorOptions: { + date: { range: { min: 'today' } }, // typed as VanillaCalendarOption + } +} +``` + +### Custom Validator +You can add a Custom Validator from an external function or inline (inline is shown below and comes from [Example 12](https://ghiscoding.github.io/slickgrid-universal/#/example12)) +```ts +function initializeGrid() { + columnDefinitions.value = [ + { + id: 'title', name: 'Title', field: 'title', + editor: { + model: Editors.date, + required: true, + validator: (value, args) => { + const dataContext = args && args.item; + if (dataContext && (dataContext.completed && !value)) { + return { valid: false, msg: 'You must provide a "Finish" date when "Completed" is checked.' }; + } + return { valid: true, msg: '' }; + } + }, + }, + ]; +} +``` + +### Date Format +Your column definitions may include a `type` to tell Formatters how to formate your date, this `type` is also used by the Editor when saving. + +##### What if I want to use a different format when saving? +There are 3 types you can provide to inform the Editor on how to save: +1. `type` inform the entire column what its type is (used by Formatter, Filter, Editor, Export) +2. `outputType` what type to display in the Editor vs saving format. +3. `saveOutputType` the type to use when saving which is different than the one used on cell input (rarely used). + + +The `type` and `outputType` are often used when you want to save something different compare to what you show to the user (for example, show a date in the US Format but save it as ISO or UTC). + +The difference between `outputType` and `saveOutputType` when you wish to display a certain format in the date editor input (while editing), but wish to save in a different format. You will rarely need the `saveOutputType` and for most use cases, the use of both `type` and `outputType` should be enough. + +> **Note** the type detection when saving is the inverse of the list above, whichever comes first from 3 to 1. diff --git a/frameworks/slickgrid-vue/docs/column-functionalities/editors/longtext-editor-textarea.md b/frameworks/slickgrid-vue/docs/column-functionalities/editors/longtext-editor-textarea.md new file mode 100644 index 000000000..a1b02ecb1 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/column-functionalities/editors/longtext-editor-textarea.md @@ -0,0 +1,92 @@ +##### index +- [Editor Options](#editor-options) +- [Custom Validator](#custom-validator) +- See the [Editors - Wiki](../Editors.md) for more general info about Editors (validators, event handlers, ...) + +### Demo +[Demo Page](https://ghiscoding.github.io/slickgrid-vue/#/slickgrid/Example3) | [Demo Component](https://github.com/ghiscoding/slickgrid-vue/blob/master/src/examples/slickgrid/Example3.tsx) - ("Title" column to be more specific) + +### Editor Options +You can change button texts, textarea size (cols, rows) and also change position of the textarea (auto is the default which will try to automatically find best place to position the textarea). + +#### [LongTextEditorOption](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/interfaces/longTextEditorOption.interface.ts) Interface. + +```ts + +``` + +#### Grid Option `defaultEditorOptions +You could also define certain options as a global level (for the entire grid or even all grids) by taking advantage of the `defaultEditorOptions` Grid Option. Note that they are set via the editor type as a key name (`autocompleter`, `date`, ...) and then the content is the same as `editorOptions` (also note that each key is already typed with the correct editor option interface), for example + +```ts +gridOptions.value = { + defaultEditorOptions: { + longText: { cols: 50, rows: 5 }, // typed as LongTextEditorOption + } +} +``` + +### Custom Validator +You can add a Custom Validator, from an external function or inline. +```ts +// you can create custom validator to pass to an inline editor +const myCustomTitleValidator = (value, args) => { + if ((value === null || value === undefined || !value.length) && (args.compositeEditorOptions?.modalType === 'create' || args.compositeEditorOptions.modalType === 'edit')) { + // we will only check if the field is supplied when it's an inline editing OR a composite editor of type create/edit + return { valid: false, msg: 'This is a required field.' }; + } else if (!/^(task\s\d+)*$/i.test(value)) { + return { valid: false, msg: 'Your title is invalid, it must start with "Task" followed by a number.' }; + } + return { valid: true, msg: '' }; +}; + +function defineGrid() { + columnDefinitions.value = [ + { + id: 'title', name: 'Title', field: 'title', + editor: { + model: Editors.longText, + required: true, + validator: myCustomTitleValidator, + }, + }, + ]; +} +``` diff --git a/frameworks/slickgrid-vue/docs/column-functionalities/editors/select-dropdown-editor.md b/frameworks/slickgrid-vue/docs/column-functionalities/editors/select-dropdown-editor.md new file mode 100644 index 000000000..a7656bd4a --- /dev/null +++ b/frameworks/slickgrid-vue/docs/column-functionalities/editors/select-dropdown-editor.md @@ -0,0 +1,228 @@ +#### index + - [Editor Options (multipleSelectOption interface)](#editor-options-multipleselectoption-interface) + - [Complex Object](#complex-object) + - [Collection Async Load (same as Select Filter)](#collection-async-load) + - [Collection Override](#collection-override) + - [Collection Label Prefix/Suffix](#collection-label-prefixsuffix) + - [Collection Label Render HTML](#collection-label-render-html) + - [Collection Change Watch](#collection-watch) + - [`multiple-select.js` Options](#multiple-selectjs-options) + - See the [Editors - Wiki](../Editors.md) for more general info about Editors (validators, event handlers, ...) + +## Select Editors +The library ships with two select editors: `singleSelectEditor` and the `multipleSelectEditor`. Both support the [multiple-select](https://github.com/ghiscoding/multiple-select-adapted/blob/master/src/multiple-select.js) library, but fallback to the bootstrap form-control style if you decide to exclude this library from your build. These editors will work with a list of foreign key values (custom structure not supported) and can be displayed properly with the [collectionFormatter](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/formatters/collectionFormatter.ts). + +We use an external lib named [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla). + +Here's an example with a `collection`, `collectionFilterBy` and `collectionSortBy` + +```ts +columnDefinitions.value = [ + { + id: 'prerequisites', name: 'Prerequisites', field: 'prerequisites', + type: FieldType.string, + editor: { + model: Editors.multipleSelect, + collection: Array.from(Array(12).keys()).map(k => ({ value: `Task ${k}`, label: `Task ${k}` })), + collectionSortBy: { + property: 'label', + sortDesc: true + }, + collectionFilterBy: { + property: 'label', + value: 'Task 2' + } + } + } +]; +``` + +### Editor Options (`MultipleSelectOption` interface) +All the available options that can be provided as `editorOptions` to your column definitions can be found under this [multipleSelectOption interface](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/interfaces/multipleSelectOption.interface.ts) and you should cast your `editorOptions` to that interface to make sure that you use only valid options of the `multiple-select.js` library. + +```ts +editor: { + model: Editors.SingleSelect, + editorOptions: { + maxHeight: 400 + } as MultipleSelectOption +} +``` + +#### Grid Option `defaultEditorOptions +You could also define certain options as a global level (for the entire grid or even all grids) by taking advantage of the `defaultEditorOptions` Grid Option. Note that they are set via the editor type as a key name (`autocompleter`, `date`, ...) and then the content is the same as `editorOptions` (also note that each key is already typed with the correct editor option interface), for example + +```ts +gridOptions.value = { + defaultEditorOptions: { + // Note: that `select` combines both multipleSelect & singleSelect + select: { minHeight: 350 }, // typed as MultipleSelectOption + } +} +``` + +### Complex Object +If your `field` string has a dot (.) it will automatically assume that it is dealing with a complex object. There are however some options you can use with a complex object, the following options from the `ColumnEditor` might be useful to you +```ts +interface ColumnEditor { + /** + * When providing a dot (.) notation in the "field" property of a column definition, we might want to use a different path for the editable object itself + * For example if we provide a coldef = { field: 'user.name' } but we use a SingleSelect Editor with object values, we could override the path to simply 'user' + */ + complexObjectPath?: string; + + /** + * defaults to 'object', how do we want to serialize the editor value to the resulting dataContext object when using a complex object? + * For example, if keep default "object" format and the selected value is { value: 2, label: 'Two' } then the end value will remain as an object, so { value: 2, label: 'Two' }. + * On the other end, if we set "flat" format and the selected value is { value: 2, label: 'Two' } then the end value will be 2. + */ + serializeComplexValueFormat?: 'flat' | 'object'; +} +``` + +```ts +columnDefinitions.value = [{ + id: 'firstName', name: 'First Name', field: 'user.firstName', + formatter: Formatters.complexObject, // the complex formatter is necessary, unless you provide a custom formatter + editor: { + model: Editors.SingleSelect, + complexObjectPath: 'user.middleName', + serializeComplexValueFormat: 'flat' // (flat) will return 'Bob', (object) will return { label: 'Bob', value: 'Bob' } + } +}]; +``` + +### Collection Override +In some cases you might want to provide a custom collection based on the current item data context (or any other logic), you can do that via the collection override. Also note that this override is processed **after** `collectionFilterBy` and `collectionSortBy` but **before** the `customStructure` (if you have any), in other words make sure that the collection returned by the override does have the properties defined in the "customStructure". + +Let take this example, let say that we want to allow collection values lower than or greater than 50 depending on its item Id, we could do the following + +```ts +columnDefinitions.value = [ + { + id: 'prerequisites', name: 'Prerequisites', field: 'prerequisites', + type: FieldType.string, + editor: { + model: Editors.multipleSelect, + collection: Array.from(Array(12).keys()).map(k => ({ value: `Task ${k}`, label: `Task ${k}` })), + collectionOverride: (updatedCollection, args) => { + console.log(args); + return updatedCollection.filter((col) => args.dataContext.id % 2 ? col.value < 50 : col.value > 50); + }, + } + } +]; +``` + +### Collection Label Prefix/Suffix +You can use `labelPrefix` and/or `labelSuffix` which will concatenate the multiple properties together (`labelPrefix` + `label` + `labelSuffix`) which will used by each Select Filter option label. You can also use the property `separatorBetweenTextLabels` to define a separator between prefix, label & suffix. + +**Note** +If `enableTranslateLabel` flag is set to `True`, it will also try to translate the Prefix / Suffix / OptionLabel texts. + +For example, say you have this collection +```typescript +const currencies = [ + { symbol: '$', currency: 'USD', country: 'USA' }, + { symbol: '$', currency: 'CAD', country: 'Canada' } +]; +``` + +You can display all of these properties inside your dropdown labels, say you want to show (symbol with abbreviation and country name). Now you can. + +So you can create the `multipleSelect` Filter with a `customStructure` by using the symbol as prefix, and country as suffix. That would make up something like this: +- $ USD USA +- $ CAD Canada + +with a `customStructure` defined as +```typescript +editor: { + collection: currencies, + customStructure: { + value: 'currency', + label: 'currency', + labelPrefix: 'symbol', + labelSuffix: 'country', + collectionOptions: { + separatorBetweenTextLabels: ' ', // add white space between each text + includePrefixSuffixToSelectedValues: true // should the selected value include the prefix/suffix in the output format + }, + model: Editors.multipleSelect +} +``` + +### Collection Label Render HTML +By default HTML is not rendered and the `label` will simply show HTML as text. But in some cases you might want to render it, you can do so by enabling the `enableRenderHtml` flag. + +**NOTE:** this is currently only used by the Editors that have a `collection` which are the `MultipleSelect` & `SingleSelect` Editors. + +```typescript +columnDefinitions.value = [ + { + id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', + formatter: Formatters.checkmarkMaterial, + type: FieldType.boolean, + editor: { + // display checkmark icon when True + enableRenderHtml: true, + collection: [{ value: '', label: '' }, { value: true, label: 'True', labelPrefix: ` ` }, { value: false, label: 'False' }], + model: Editors.singleSelect + } + } +]; +``` + +### Collection Watch +Sometime you wish that whenever you change your filter collection, you'd like the filter to be updated, it won't do that by default but you could use `enableCollectionWatch` for that purpose to add collection observers and re-render the Filter DOM element whenever the collection changes. Also note that using `collectionAsync` will automatically watch for changes, so there's no need to enable this flag for that particular use case. + +```typescript +columnDefinitions.value = [ + { + id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', + formatter: Formatters.checkmarkMaterial, + type: FieldType.boolean, + editor: { + // watch for any changes in the collection and re-render when that happens + enableCollectionWatch: true, + collection: [{ value: '', label: '' }, { value: true, label: 'True' }, { value: false, label: 'False' }], + model: Editors.singleSelect + } + } +]; +``` + +### `multiple-select-vanilla.js` Options +You can use any options from [Multiple-Select.js](http://wenzhixin.net.cn/p/multiple-select) and add them to your `editorOptions` property. However please note that this is a customized version of the original (all original [lib options](http://wenzhixin.net.cn/p/multiple-select/docs/) are available so you can still consult the original site for all options). + +Couple of small options were added to suit SlickGrid-Universal needs, which is why it points to `slickgrid-universal/lib` folder (which is our customized version of the original). This lib is required if you plan to use `multipleSelect` or `singleSelect` Filters. What was customized to (compare to the original) is the following: +- `okButton` option was added to add an OK button for simpler closing of the dropdown after selecting multiple options. + - `okButtonText` was also added for locale (i18n) +- `offsetLeft` option was added to make it possible to offset the dropdown. By default it is set to 0 and is aligned to the left of the select element. This option is particularly helpful when used as the last right column, not to fall off the screen. +- `autoDropWidth` option was added to automatically resize the dropdown with the same width as the select filter element. +- `autoAdjustDropHeight` (defaults to true), when set will automatically adjust the drop (up or down) height +- `autoAdjustDropPosition` (defaults to true), when set will automatically calculate the area with the most available space and use best possible choise for the drop to show (up or down) +- `autoAdjustDropWidthByTextSize` (defaults to true), when set will automatically adjust the drop (up or down) width by the text size (it will use largest text width) +- to extend the previous 3 autoAdjustX flags, the following options can be helpful + - `minWidth` (defaults to null, to use when `autoAdjustDropWidthByTextSize` is enabled) + - `maxWidth` (defaults to 500, to use when `autoAdjustDropWidthByTextSize` is enabled) + - `adjustHeightPadding` (defaults to 10, to use when `autoAdjustDropHeight` is enabled), when using `autoAdjustDropHeight` we might want to add a bottom (or top) padding instead of taking the entire available space + - `maxHeight` (defaults to 275, to use when `autoAdjustDropHeight` is enabled) + +##### Code +```typescript +columnDefinitions.value = [ + { + id: 'isActive', name: 'Is Active', field: 'isActive', + filterable: true, + editor: { + collection: [{ value: '', label: '' }, { value: true, label: 'true' }, { value: false, label: 'false' }], + model: Editors.singleSelect, + elementOptions: { + // add any multiple-select.js options (from original or custom version) + autoAdjustDropPosition: false, // by default set to True, but you can disable it + position: 'top' + } + } + } +]; +``` diff --git a/frameworks/slickgrid-vue/docs/column-functionalities/filters/autocomplete-filter-kraaden.md b/frameworks/slickgrid-vue/docs/column-functionalities/filters/autocomplete-filter-kraaden.md new file mode 100644 index 000000000..f24f42b20 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/column-functionalities/filters/autocomplete-filter-kraaden.md @@ -0,0 +1,175 @@ +#### Index +- [Using `collection` or `collectionAsync`](#using-collection-or-collectionasync) +- [Filter Options (`AutocompleterOption` interface)](#filter-options-autocompleteroption-interface) +- [Using Remote API](#using-external-remote-api) +- [Force User Input](#autocomplete---force-user-input) +- [Update Filters Dynamically](../../column-functionalities/filters/input-filter.md#update-filters-dynamically) +- [Animated Gif Demo](#animated-gif-demo) + +### Demo +[Demo Page](https://ghiscoding.github.io/slickgrid-vue/#/slickgrid/Example3) | [Demo Component](https://github.com/ghiscoding/slickgrid-vue/blob/master/src/examples/slickgrid/Example3.tsx) + +### Introduction +AutoComplete is a functionality that let the user start typing characters and the autocomplete will try to give suggestions according to the characters entered. The collection can be a JSON files (collection of strings or objects) or can also be an external resource like a JSONP query to an external API. For a demo of what that could look like, take a look at the [animated gif demo](#animated-gif-demo) below. + +We use an external lib named [Autocomplete](https://github.com/kraaden/autocomplete) (aka `autocompleter` on npm) by Kraaden. + +## Using `collection` or `collectionAsync` +If you want to pass the entire list to the AutoComplete (like a JSON file or a Web API call), you can do so using the `collection` or the `collectionAsync` (the latter will load it asynchronously). You can also see that the Editor and Filter have almost the exact same configuration (apart from the `model` that is obviously different). + +##### Component +```vue + +``` + +### Filter Options (`AutocompleterOption` interface) +All the available options that can be provided as `filterOptions` to your column definitions can be found under this [AutocompleterOption interface](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/interfaces/autocompleterOption.interface.ts) and you should cast your `filterOptions` to that interface to make sure that you use only valid options of the autocomplete library. + +```ts +filter: { + model: Filters.autocompleter, + filterOptions: { + minLength: 3, + } as AutocompleterOption +} +``` + +## Using External Remote API +You could also use external 3rd party Web API (can be JSONP query or regular JSON). This will make a much shorter result since it will only return a small subset of what will be displayed in the AutoComplete Editor or Filter. For example, we could use GeoBytes which provide a JSONP Query API for the cities of the world, you can imagine the entire list of cities would be way too big to download locally, so this is why we use such API. + +##### Component +```vue + +``` + +## Autocomplete - force user input +If you want to add the autocomplete functionality but want the user to be able to input a new option, then follow the example below: + +```ts +columnDefinitions.value = [{ + id: 'area', + name: 'Area', + field: 'area', + type: FieldType.string, + editor: { + model: Editors.autocompleter, + editorOptions: { + minLength: 0, + forceUserInput: true, + fetch: (searchText, updateCallback) => { + updateCallback(areas); // add here the array + }, + } + }, +}]; +``` +You can also use the `minLength` to limit the autocomplete text to `0` characters or more, the default number is `3`. + +## Animated Gif Demo +![](https://user-images.githubusercontent.com/643976/50624023-d5e16c80-0ee9-11e9-809c-f98967953ba4.gif) diff --git a/frameworks/slickgrid-vue/docs/column-functionalities/filters/compound-filters.md b/frameworks/slickgrid-vue/docs/column-functionalities/filters/compound-filters.md new file mode 100644 index 000000000..072e7a414 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/column-functionalities/filters/compound-filters.md @@ -0,0 +1,233 @@ +#### index +- [Available Types](#available-types) +- [SASS Styling](#sass-styling) +- [Compound Input Filter](#how-to-use-compoundinput-filter) +- [Compound Date Filter](#how-to-use-compounddate-filter) +- [Compound Operator List (custom list)](#compound-operator-list-custom-list) +- [Compound Operator Alternate Texts](#compound-operator-alternate-texts) +- [Filter Complex Object](input-filter.md#how-to-filter-complex-objects) +- [Update Filters Dynamically](input-filter.md#update-filters-dynamically) +- [How to avoid filtering when only Operator dropdown is changed?](#how-to-avoid-filtering-when-only-operator-dropdown-is-changed) +- [Custom Filter Predicate](input-filter.md#custom-filter-predicate) +- [Filter Shortcuts](input-filter.md#filter-shortcuts) + +### Description +Compound filters are a combination of 2 elements (Operator Select + Input Filter) used as a filter on a column. This is very useful to make it obvious to the user that there are Operator available and even more useful with a date picker (`Vanilla-Calendar`). + +### Demo +[Demo Page](https://ghiscoding.github.io/slickgrid-vue/#/slickgrid/Example4) / [Demo Component](https://github.com/ghiscoding/slickgrid-vue/blob/master/src/examples/slickgrid/Example4.tsx) + +### Available Types +There are multiple types of compound filters available +1. `Filters.compoundInputText` adds an Operator combine to an Input of type `text` (alias to `Filters.compoundInput`). +2. `Filters.compoundInputNumber` adds an Operator combine to an Input of type `number`. +3. `Filters.compoundInputPassword` adds an Operator combine to an Input of type `password. +4. `Filters.compoundDate` adds an Operator combine to a Date Picker. +5. `Filters.compoundSlider` adds an Operator combine to a Slider Filter. + +### How to use CompoundInput Filter +Simply set the flag `filterable` to True and use the filter type `Filters.compoundInput`. Here is an example with a full column definition: +```ts +// define you columns, in this demo Effort Driven will use a Select Filter +columnDefinitions.value = [ + { id: 'title', name: 'Title', field: 'title' }, + { id: 'description', name: 'Description', field: 'description', filterable: true }, + { id: 'complete', name: '% Complete', field: 'percentComplete', + formatter: Formatters.percentCompleteBar, + type: 'number', + filterable: true, + filter: { model: Filters.compoundInput } + } +]; + +// you also need to enable the filters in the Grid Options +gridOptions.value = { + enableFiltering: true +}; +``` +#### Notes +The column definition `type` will affect the list of Operators shown, for example if you have `type: FieldType.string`, it will display the operators (`=`, `a*`, `*z`) where `a*` means StartsWith and `*z` means EndsWith. The current logic implemented is that any types that are not String, will display the list of Operators (` `, `=`, `<`, `<=`, `>`, `>=`, `<>`) + + +### How to use CompoundDate Filter +As any other columns, set the column definition flag `filterable: true` and use the filter type `Filters.compoundDate`. Here is an example with a full column definition: + +```ts +// define you columns, in this demo Effort Driven will use a Select Filter +columnDefinitions.value = [ + { id: 'title', name: 'Title', field: 'title' }, + { id: 'description', name: 'Description', field: 'description', filterable: true }, + { id: 'usDateShort', name: 'US Date Short', field: 'usDateShort', + type: 'dateUsShort', + filterable: true, + filter: { + model: Filters.compoundDate, + + // you can also add an optional placeholder + placeholder: 'filter by date' + } + } +]; + +// you also need to enable the filters in the Grid Options +gridOptions.value = { + enableFiltering: true +}; +``` + +> **Note** we use [Tempo](https://tempo.formkit.com/) to parse and format Dates to the chosen format via the `type` option when provided in your column definition. + +#### Dealing with different input/ouput dates (example: UTC) +What if your date input (from your dataset) has a different output on the screen (UI)? +In that case, you will most probably have a Formatter and type representing the input type, we also provided an `outputType` that can be used to deal with that case. + +For example, if we have an input date in UTC format and we want to display a Date ISO format to the screen (UI) in the date picker. + +```ts +// define you columns, in this demo Effort Driven will use a Select Filter +columnDefinitions.value = [ + { id: 'title', name: 'Title', field: 'title' }, + { id: 'description', name: 'Description', field: 'description', filterable: true }, + { id: 'utcDate', name: 'UTC Date', field: 'utcDate', + type: 'dateUtc', // format used in the dataset (input) + formatter: Formatters.dateTimeIso, // format to show in the cell on each row (formatter) + outputType: 'dateTimeIso', // format to show in the date picker + filterable: true, filter: { model: Filters.compoundDate } + } +]; + +// you also need to enable the filters in the Grid Options +gridOptions.value = { + enableFiltering: true +}; +``` + +#### Date and Time +The date picker will automatically detect if the `type` or `outputType` has time inside, if it does then it will add a time picker at the bottom of the date picker. + +For example, if we have an input date in UTC format and we want to display a Date ISO format with time to the screen (UI) and the date picker. + +```ts +// define you columns, in this demo Effort Driven will use a Select Filter +columnDefinitions.value = [ + { id: 'title', name: 'Title', field: 'title' }, + { id: 'description', name: 'Description', field: 'description', filterable: true }, + { id: 'utcDate', name: 'UTC Date', field: 'utcDate', // if your type has hours/minutes, then the date picker will include date+time + type: 'dateUtc', + formatter: Formatters.dateTimeIsoAmPm, + outputType: 'dateTimeIsoAmPm', + filterable: true, filter: { model: Filters.compoundDate } + } +]; + +// you also need to enable the filters in the Grid Options +gridOptions.value = { + enableFiltering: true +}; +``` + +#### Filter Options (`VanillaCalendarOption` interface) +All the available options that can be provided as `filterOptions` to your column definitions can be found under this [VanillaCalendarOption interface](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/interfaces/vanillaCalendarOption.interface.ts) and you should cast your `filterOptions` with the expected interface to make sure that you use only valid settings of the [Vanilla-Calendar](https://vanilla-calendar.pro/docs/reference/additionally/settings) library. + +```ts +filter: { + model: Filters.compoundDate, + filterOptions: { + range: { min: 'today' } + } as VanillaCalendarOption +} +``` + +#### Grid Option `defaultFilterOptions +You could also define certain options as a global level (for the entire grid or even all grids) by taking advantage of the `defaultFilterOptions` Grid Option. Note that they are set via the filter type as a key name (`autocompleter`, `date`, ...) and then the content is the same as `filterOptions` (also note that each key is already typed with the correct filter option interface), for example + +```ts +gridOptions.value = { + defaultFilterOptions: { + // Note: that `date`, `select` and `slider` are combining both compound & range filters together + date: { range: { min: 'today' } }, // typed as VanillaCalendarOption + select: { minHeight: 350 }, // typed as MultipleSelectOption + slider: { sliderStartValue: 10 } + } +} +``` + +### Compound Operator List (custom list) +Each Compound Filter will try to define the best possible Operator List depending on what Field Type you may have (for example we can have StartsWith Operator on a string but not on a number). If you want to provide your own custom Operator List to a Compound Filter, you can do that via the `compoundOperatorList` property (also note that your Operator must be a valid OperatorType/OperatorString). + +```ts +columnDefinitions.value = [ + { id: 'title', name: 'Title', field: 'title' }, + { id: 'description', name: 'Description', field: 'description', filterable: true }, + { id: 'utcDate', name: 'UTC Date', field: 'utcDate', + type: 'dateUtc', + formatter: Formatters.dateTimeIsoAmPm, + outputType: 'dateTimeIsoAmPm', + filterable: true, filter: { + model: Filters.compoundSlider, + // here is our custom list that will override default list + compoundOperatorList: [ + { operator: '', desc: '' }, + { operator: '=', desc: 'Equal to' }, + { operator: '<', desc: 'Less than' }, + { operator: '>', desc: 'Greater than' }, + ] + } + } +]; +``` + +### Compound Operator Alternate Texts +You can change any of the compound operator text or description shown in the select dropdown list by using `compoundOperatorAltTexts` to provide alternate texts. + +The texts are separated into 2 groups (`numeric` or `text`) so that the alternate texts can be applied to all assigned filters, hence the type will vary depending on which Filter you choose as shown below: +- `numeric` + - `Filters.compoundDate` + - `Filters.compoundInputNumber` + - `Filters.compoundSlider` +- `text` + - `Filters.compoundInput` + - `Filters.compoundInputPassword` + - `Filters.compoundInputText` + +> **Note** avoid using text with more than 2 or 3 characters for the operator text (which is roughly the width of the compound operator select dropdown), exceeding this limit will require CSS style changes. + +```ts +gridOptions.value = { + compoundOperatorAltTexts: { + // where '=' is any of the `OperatorString` type shown above + numeric: { '=': { operatorAlt: 'eq', descAlt: 'alternate numeric equal description' } }, + text: { '=': { operatorAlt: 'eq', descAlt: 'alternate text equal description' } } + }, +} +``` + +![image](https://github.com/ghiscoding/slickgrid-universal/assets/643976/8f5cb431-d148-4c78-92fc-f1e3e48e64c4) + +### How to avoid filtering when only Operator dropdown is changed? +Starting with version `>=2.1.x`, you can now enable `skipCompoundOperatorFilterWithNullInput` that can be provided to your Grid Options (or via global grid options) and/or your Column Definition. + +What will this option do really? +- skip filtering (in other words do nothing) will occur when: + - Operator select dropdown (left side) is changed without any value provided in the filter input (right). +- start filtering when: + - Operator select dropdown is changed **and** we have a value provided in the filter input, it will start filtering + - Operator select dropdown is empty **but** we have a value provided in the filter input, it will start filtering + +> **Note** the Compound Date Filter is the only filter that has this option enabled by default. + +###### Code +```ts +columnDefinitions.value = [{ + id: 'name', field: 'name', + filter: { + model: Filters.compoundInput, + skipCompoundOperatorFilterWithNullInput: true // change via column def, always has higher specificity over grid options + } +}]; + +// or change for all compound filters of the same grid +gridOptions.value = { + skipCompoundOperatorFilterWithNullInput: true, +}; +``` diff --git a/frameworks/slickgrid-vue/docs/column-functionalities/filters/custom-filter.md b/frameworks/slickgrid-vue/docs/column-functionalities/filters/custom-filter.md new file mode 100644 index 000000000..431e911c8 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/column-functionalities/filters/custom-filter.md @@ -0,0 +1,193 @@ +#### index +- [Filter Complex Object](input-filter.md#filter-complex-object) +- [Update Filters Dynamically](input-filter.md#update-filters-dynamically) + +### Demo +[Demo Page](https://ghiscoding.github.io/slickgrid-vue/#/slickgrid/Example4) / [Demo Client Component](https://github.com/ghiscoding/slickgrid-vue/blob/master/src/examples/slickgrid/Example4.tsx) / [Custom InputFilter.ts](https://github.com/ghiscoding/slickgrid-vue/blob/master/src/examples/slickgrid/custom-inputFilter.ts) + +### Description +You can also create your own Custom Filter with any html/css you want to use. Vue template (View) are not supported at this point, if you wish to contribute on that end then I certainly accept PR (Pull Request). + +#### Limitations +- as mentioned in the description, only html/css and/or JS libraries are supported. + - this mainly mean that Vue templates (Views) are not supported (feel free to contribute). +- SlickGrid uses `table-cell` as CSS for it to display a consistent height for each rows (this keeps the same row height/line-height to always be the same). + - all this to say that you might be in a situation were your filter shows in the back of the grid. The best approach to overcome this is to use a modal if you can or if the library support `append to body container`. For example, you can see that `multiple-select.js` support a `container` and is needed for the filter to work as can be seen in the [multipleSelectFilter.ts](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/filters/multipleSelectFilter.ts#L26) + +### How to use Custom Filter? +1. You first need to create a `class` using the [Filter interface](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/models/filter.interface.ts). Make sure to create all necessary public properties and functions. + - You can see a demo with a [custom-inputFilter.ts](https://github.com/ghiscoding/slickgrid-vue/blob/master/src/examples/slickgrid/custom-inputFilter.ts) that is used in the [demo - example 4](https://ghiscoding.github.io/slickgrid-vue/#/slickgrid/Example4) +2. There are two methods to use your custom filters on the grid. + 1. Simply set the `columnDefinition.filter.model` to your new custom Filter class and instantiate it with `new` (you can also use dependency injection in the constructor if you wish). Here is an example with a custom input filter: + +```vue + +``` + +2. Or register your filter with the `registerTransient` method on the Vue container in the startup file (see the demo [index.ts](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/index.ts). It is recommended to use `registerTransient`, though you could use whatever lifetime you want). This registration is usually in `main.ts` or `main.js`. Then in your view model pass your custom filter to `columnDefinition.filter.model` property and we will use Vue's container to instantiate your filter. Here is that example: + +**myCustomFilter.ts** + +```vue + +``` + +**my-view-model.ts** + +```vue + +``` + +### Default Filter Type +By default, the library uses the [inputFilter](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/filters/inputFilter.ts) when none is specified. However, you can override this value with any filter you like during the startup/configuration of your Vue application: + +**main.ts** + +### Default Search Term(s) +If you want to load the grid with certain default filter(s), you can use the following optional properties: +- `searchTerms` (array of values) + +For example, setting a default value into an `input` element, you can simply get the search term with `columnDef.filter.searchTerms` and set the default value with `filterElm.value = searchTerms;` + +### Collection +If you want to pass a `collection` to your filter (for example, a multiple-select needs a select list of options), you can then use it in your custom filter through `columnDef.filter.collection` + +#### `key/label` pair +By default a `collection` uses the `label/value` pair. You can loop through your `collection` and use the `label/value` properties. For example: + +```vue + +``` + +#### Custom Structure (key/label pair) +What if your `collection` have totally different value/label pair? In this case, you can use the `customStructure` to change the property name(s) to use. You can change the label and/or the value, they can be passed independently. +For example: +```vue + +``` + +### How to add Translation? + +#### LabelKey +By default a `collection` uses the `label/value` pair without translation or `labelKey/value` pair with translation usage. So if you want to use translations, then you can loop through your `collection` and use the `labelKey/value` properties. For example: +```vue + +``` + +### Custom Structure with Translation +What if you want to use `customStructure` and translate the labels? Simply pass the flag `enableTranslateLabel: true` + +For example: +```vue + +``` diff --git a/frameworks/slickgrid-vue/docs/column-functionalities/filters/filter-intro.md b/frameworks/slickgrid-vue/docs/column-functionalities/filters/filter-intro.md new file mode 100644 index 000000000..2ea2d5601 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/column-functionalities/filters/filter-intro.md @@ -0,0 +1,87 @@ +#### Index +- [How to use Filter?](#how-to-use-filter) +- [Filtering with Localization](input-filter.md#how-to-hide-filter-header-row) +- [Filtering with Localization](input-filter.md#filtering-with-localization-i18n) +- [Filter Complex Object](input-filter.md#how-to-filter-complex-objects) +- [Update Filters Dynamically](input-filter.md#update-filters-dynamically) +- [Query Different Field (Filter/Sort)](input-filter.md#query-different-field) +- [Dynamic Query Field](input-filter.md#dynamic-query-field) +- [Debounce/Throttle Text Search (wait for user to stop typing before filtering)](input-filter.md#debouncethrottle-text-search-wait-for-user-to-stop-typing-before-filtering) +- [Ignore Locale Accent in Text Filter/Sorting](input-filter.md#ignore-locale-accent-in-text-filtersorting) +- [Custom Filter Predicate](input-filter.md#custom-filter-predicate) +- [Filter Shortcuts](input-filter.md#filter-shortcuts) + +### Description + +Filtering is a big part of a data grid, Slickgrid-Universal provides a few built-in Filters that you can use in your grids. You need to tell the grid that you want to use Filtering (via Grid Options) and you also need to enable the filter for every column that you need filtering (via Column Definitions). + +### How to use Filter? +You simply need to set the flag `filterable` for each column that you want filtering and then also enable the filters in the Grid Options. Here is an example with a full column definitions: +```ts + +``` + +### How to hide Filter Header Row? +There are 2 ways to hide Filters from the user, you could disable it completely OR you could hide the Filter Header Row. + +##### You could disable the Filters completely, +```ts +let vueGrid: SlickgridVueInstance; + +function vueGridReady(vGrid: SlickgridVueInstance) { + vueGrid = vGrid; +} + +function disableFilters() { + gridOptions.value = { + enableFiltering: false + }; + + // you could re-enable it later + vueGrid.setOptions({ enableFiltering: true }); +} +``` + +##### You could also enable Filters but Hide them from the user in the UI +This can be useful for features that require Filtering but you wish to hide the filters for example Tree Data. + +```ts +gridOptions.value = { + enableFiltering: true, + showHeaderRow: false, +}; +``` + +Also, if you don't want to see the Grid Menu toggle filter row command, you should also hide it from the menu via + +```ts +let vueGrid: SlickgridVueInstance; + +function vueGridReady(vGrid: SlickgridVueInstance) { + vueGrid = vGrid; +} + +function showFilterRow() { + gridOptions.value = { + enableFiltering: true, + showHeaderRow: false, + gridMenu: { + hideToggleFilterCommand: true + }, + }; + + // you can show toggle the filter header row dynamically + vueGrid.setHeaderRowVisibility(true); +} +``` diff --git a/frameworks/slickgrid-vue/docs/column-functionalities/filters/input-filter.md b/frameworks/slickgrid-vue/docs/column-functionalities/filters/input-filter.md new file mode 100644 index 000000000..a3640d561 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/column-functionalities/filters/input-filter.md @@ -0,0 +1,268 @@ +#### Index +- [Usage](#ui-usage) +- [Filtering with Localization](#filtering-with-localization-i18n) +- [Filter Complex Object](#how-to-filter-complex-objects) +- [Update Filters Dynamically](#update-filters-dynamically) +- [Query Different Field (Filter/Sort)](#query-different-field) +- [Dynamic Query Field](#dynamic-query-field) +- [Debounce/Throttle Text Search (wait for user to stop typing before filtering)](#debouncethrottle-text-search-wait-for-user-to-stop-typing-before-filtering) +- [Ignore Locale Accent in Text Filter/Sorting](#ignore-locale-accent-in-text-filtersorting) +- [Custom Filter Predicate](#custom-filter-predicate) +- [Filter Shortcuts](#filter-shortcuts) + +### Description +Input filter is the default filter when enabling filters. + +### Demo +[Demo Page](https://ghiscoding.github.io/slickgrid-vue/#/slickgrid/Example4) / [Demo Component](https://github.com/ghiscoding/slickgrid-vue/blob/master/src/examples/slickgrid/Example4.tsx) + +### UI Usage +All column types support the following operators: (`>`, `>=`, `<`, `<=`, `<>`, `!=`, `=`, `==`, `*`), range filters can also have 1 of these options (`rangeInclusive` or `rangeExclusive`, the inclusive is default) +Example: +- Number type + - `>100` => bigger than 100 + - `<>100` => not include number 100 + - `15..44` => between 15 and 44 (you can also provide option `rangeInclusive` or `rangeExclusive`, inclusive is default) +- Date types + - `>=2001-01-01` => bigger or equal than date `2001-01-01` + - `<02/28/17` => smaller than date `02/28/17` + - `2001-01-01..2002-02-22` => between 2001-01-01 and 2002-02-22 +- String type + - `<>John` => not containing the sub-string `John` + - `!=John` => not equal to the text `John` (note that this is **not** equivalent to `<>`) + - `John*` => starts with the sub-string `John` + - `*Doe` => ends with the sub-string `Doe` + - `ab..ef` => anything included between "af" and "ef" + - refer to the ASCII table for each character assigned index + - `!= ` => get defined only data and exclude any `undefined`, `null` or empty string `''` + - notice the empty string in the search value `' '` + +Note that you could do the same functionality with a Compound Filter. + +#### Note +For filters to work properly (default is `string`), make sure to provide a `FieldType` (type is against the dataset, not the Formatter), for example on a Date Filters, we can set the `FieldType` of dateUtc/date (from dataset) can use an extra option of `filterSearchType` to let user filter more easily. For example, with a column having a "UTC Date" coming from the dataset but has a `formatter: Formatters.dateUs`, you can type a date in US format `>02/28/2017`, also when dealing with UTC you have to take the time difference in consideration. + +### How to use Input Filter +Simply set the flag `filterable` to True and and enable the filters in the Grid Options. Here is an example with a full column definition: +```ts +// define you columns, in this demo Effort Driven will use a Select Filter +columnDefinitions.value = [ + { id: 'title', name: 'Title', field: 'title' }, + { id: 'description', name: 'Description', field: 'description', filterable: true } +]; + +// you also need to enable the filters in the Grid Options +gridOptions.value = { + enableFiltering: true +}; +``` + +### Filtering with Localization (i18n) +When using a regular grid with a JSON dataset (that is without using Backend Service API), the filter might not working correctly on cell values that are translated (because it will try to filter against the translation key instead of the actual formatted value). So to bypass this problem, a new extra `params` was created to resolve this, you need to set `useFormatterOuputToFilter` to True and the filter will, has the name suggest, use the output of the Formatter to filter against. Example: +```ts +// define you columns, in this demo Effort Driven will use a Select Filter +columnDefinitions.value = [ + { id: 'title', name: 'Title', field: 'id', + headerKey: 'TITLE', + formatter: taskTranslateFormatter, // <-- this could be a custom Formatter or the built-in translateFormatter + filterable: true, + params: { useFormatterOuputToFilter: true } // <-- set this flag to True + }, + { id: 'description', name: 'Description', field: 'description', filterable: true } +]; + +// you also need to enable the filters in the Grid Options +gridOptions.value = { + enableFiltering: true +}; + +// using a custom translate Formatter OR translateFormatter +taskTranslateFormatter: Formatter = (row, cell, value, columnDef, dataContext) => { + return i18n.tr('TASK_X', { x: value }); +} +``` + +### How to Filter Complex Objects? +You can filter complex objects using the dot (.) notation inside the `field` property defined in your Columns Definition. + +For example, let say that we have this dataset +```ts +dataset.value = [ + { item: 'HP Desktop', buyer: { id: 1234, address: { street: '123 belleville', zip: 123456 }} }, + { item: 'Lenovo Mouse', buyer: { id: 456, address: { street: '456 hollywood blvd', zip: 789123 }} }, +]; +``` + +We can now filter the zip code from the buyer's address using this filter: +```ts +columnDefinitions.value = [ + { + // the zip is a property of a complex object which is under the "buyer" property + // it will use the "field" property to explode (from "." notation) and find the child value + id: 'zip', name: 'ZIP', field: 'buyer.address.zip', filterable: true + // id: 'street', ... + } +]; +``` + +### Update Filters Dynamically +You can update/change the Filters dynamically (on the fly) via the `updateFilters` method from the `FilterService`. Note that calling this method will override all filters and replace them with the new array of filters provided. For example, you could update the filters from a button click or a select dropdown list with predefined filter set. + +##### Component +```vue + + +``` + +#### Extra Arguments +The `updateFilters` method has 2 extra arguments: +- 2nd argument, defaults to true, is to emit a filter changed event (the GridStateService uses this event) + - optional and defaults to true `updateFilters([], true)` +- 3rd argument is to trigger a backend query (when using a Backend Service like OData/GraphQL), this could be useful when using updateFilters & updateSorting and you wish to only send the backend query once. + - optional and defaults to true `updateFilters([], true, true)` + +### Query Different Field +Sometime you want to display a certain column (let say `countryName`) but you want to filter from a different column (say `countryCode`), in such use case you can use 1 of these 4 optional +- `queryField`: this will affect both the Filter & Sort +- `queryFieldFilter`: this will affect only the Filter +- `queryFieldSorter`: this will affect only the Sort +- `queryFieldNameGetterFn`: dynamically change column to do Filter/Sort (see below) + +### Dynamic Query Field +What if you a field that you only know which field to query only at run time and depending on the item object (`dataContext`)? +We can defined a `queryFieldNameGetterFn` callback that will be executed on each row when Filtering and/or Sorting. +```ts +queryFieldNameGetterFn: (dataContext) => { + // do your logic and return the field name will be queried + // for example let say that we query "profitRatio" when we have a profit else we query "lossRatio" + return dataContext.profit > 0 ? 'profitRatio' : 'lossRatio'; +}, +``` + +### Debounce/Throttle Text Search (wait for user to stop typing before filtering) +When having a large dataset, it might be useful to add a debounce delay so that typing multiple character successively won't affect the search time, you can use the `filterTypingDebounce` grid option for that use case. What it will do is simply wait for the user to finish typing before executing the filter condition, you typically don't want to put this number too high and I find that between 250-500 is a good number. +```ts +gridOptions.value = { + filterTypingDebounce: 250, +}; +``` + +### Ignore Locale Accent in Text Filter/Sorting +You can ignore latin accent (or any other language accent) in text filter via the Grid Option `ignoreAccentOnStringFilterAndSort` flag (default is false) +```ts +gridOptions.value = { + ignoreAccentOnStringFilterAndSort: true, +}; +``` + +### Custom Filter Predicate +You can provide a custom predicate by using the `filterPredicate` when defining your `filter`, the callback will provide you with 2 arguments (`dataContext` and `searchFilterArgs`). The `searchFilterArgs` has a type of `SearchColumnFilter` interface which will provide you more info about the filter itself (like parsed operator, search terms, column definition, column id and type as well). You can see a live demo at [Example 14](https://ghiscoding.github.io/slickgrid-universal/#/example14) and the associated [lines](https://github.com/ghiscoding/slickgrid-universal/blob/1a2c2ff4b72ac3f51b30b1d3d101e84ed9ec9ece/examples/vite-demo-vanilla-bundle/src/examples/example14.ts#L153-L178) of code. + +```ts +columnDefinitions.value = [ + { + id: 'title', name: 'Title', field: 'title', sortable: true, + filterable: true, type: FieldType.string, + filter: { + model: Filters.inputText, + // you can use your own custom filter predicate when built-in filters aren't working for you + // for example the example below will function similarly to an SQL LIKE to answer this SO: https://stackoverflow.com/questions/78471412/angular-slickgrid-filter + filterPredicate: (dataContext, searchFilterArgs) => { + const searchVals = (searchFilterArgs.searchTerms || []) as SearchTerm[]; + if (searchVals?.length) { + const columnId = searchFilterArgs.columnId; + const searchVal = searchVals[0] as string; + const likeMatches = searchVal.split('%'); + if (likeMatches.length > 3) { + // for matches like "%Ta%10%" will return text that starts with "Ta" and ends with "10" (e.g. "Task 10", "Task 110", "Task 210") + const [_, start, end] = likeMatches; + return dataContext[columnId].startsWith(start) && dataContext[columnId].endsWith(end); + } else if (likeMatches.length > 2) { + // for matches like "%Ta%10" will return text that starts with "Ta" and contains "10" (e.g. "Task 10", "Task 100", "Task 101") + const [_, start, contain] = likeMatches; + return dataContext[columnId].startsWith(start) && dataContext[columnId].includes(contain); + } + // for anything else we'll simply expect a Contains + return dataContext[columnId].includes(searchVal); + } + // if we fall here then the value is not filtered out + return true; + }, + }, + }, +]; +``` + +The custom filter predicate above was to answer a Stack Overflow question and will work similarly to an SQL LIKE matcher (it's not perfect and probably requires more work but is enough to demo the usage of a custom filter predicate) + +![image](https://github.com/ghiscoding/slickgrid-universal/assets/643976/3e77774e-3a9f-4ca4-bca7-50a033a4b48d) + +### Filter Shortcuts + +User can declare some Filter Shortcuts, that will be added to the Header Menu of the Column it was assigned. These shortcuts are simply a list of filter search values (e.g. Filter the Blank/Non-Blanks Values), the end user can type the same search values themselves but the shortcuts are simply meant to be quicker without having to know what to type (e.g. Filter Current Year). + + The shortcuts can be declared via an array that must include at least a `title` (or `titleKey`) a `searchTerms` array and lastly an optional `operator` can also be provided. The available properties of these shortcut is a merge of Header Menu Item interface (except `command` and `action` which are reserved and assigned internally) and of course the 3 properties mentioned above. The declaration is very similar to how we use it when declaring Grid Presets as shown below + +```ts +columnDefinitions.value = [ + { + id: 'country', name: 'Country', field: 'country', + filter: { + model: Filters.inputText, + filterShortcuts: [ + { title: 'Blank Values', searchTerms: ['A'], operator: '<', iconCssClass: 'mdi mdi-filter-minus-outline', }, + { title: 'Non-Blank Values', searchTerms: ['A'], operator: '>', iconCssClass: 'mdi mdi-filter-plus-outline', }, + ] + }, + }, + { + id: 'finish', name: 'Finish', field: 'finish', + filter: { + model: Filters.dateRange, + filterShortcuts: [ + { + // using Locale translations & Tempo to calculate next 30 days + titleKey: 'NEXT_30_DAYS', + iconCssClass: 'mdi mdi-calendar', + searchTerms: [tempoFormat(new Date(), 'YYYY-MM-DD'), tempoFormat(addDay(new Date(), 30), 'YYYY-MM-DD')], + }, + ] + }, + }, +]; +``` diff --git a/frameworks/slickgrid-vue/docs/column-functionalities/filters/range-filters.md b/frameworks/slickgrid-vue/docs/column-functionalities/filters/range-filters.md new file mode 100644 index 000000000..2f6a85040 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/column-functionalities/filters/range-filters.md @@ -0,0 +1,202 @@ +#### Index +- [Using an Inclusive Range](#using-an-inclusive-range-default-is-exclusive) +- [Using 2 dots (..) notation](#using-2-dots--notation) +- [Using a Slider Range](#using-a-slider-range-filter) + - [Filter Options](#filter-options) +- [Using a Date Range](#using-a-date-range-filter) +- [Update Filters Dynamically](input-filter.md#update-filters-dynamically) +- [Custom Filter Predicate](input-filter.md#custom-filter-predicate) +- [Filter Shortcuts](input-filter.md#filter-shortcuts) + +### Introduction +Range filters allows you to search for a value between 2 min/max values, the 2 most common use case would be to filter between 2 numbers or dates, you can do that with the Slider & Date Range Filters. The range can also be defined as inclusive (`>= 0 and <= 10`) or exclusive (`> 0 and < 10`), the default is exclusive but you can change that, see below for more info. + +### Using an Inclusive Range (default is Exclusive) +By default all the range filters are with exclusive range, which mean between value `x` and `y` but without including them. If you wish to include the `x` and `y` values, you can change that through the `operator` property. + +For example +```ts +// your columns definition +columnDefinitions.value = [ + { + id: 'duration', field: 'duration', name: 'Duration', + filterable: true, + filter: { + model: Filters.input, + operator: OperatorType.rangeInclusive // defaults to exclusive + + // or use the string (case sensitive) + operator: 'RangeInclusive', // defaults to exclusive + } + }, +]; +``` + +## Using 2 dots (..) notation +You can use a regular input filter with the 2 dots (..) notation to represent a range, for example `5..90` would search between the value 5 and 90 (exclusive search unless specified). + +##### Component +```vue + +``` + +### Using a Slider Range Filter +The slider range filter is very useful if you can just want to use the mouse to drag/slide a cursor, you can also optionally show/hide the slider values on screen (hiding them would giving you more room without but without the precision). + +##### Component +```vue + +``` + +##### Filter Options +All the available options that can be provided as `filterOptions` to your column definitions and you should try to cast your `filterOptions` to the specific interface as much as possible to make sure that you use only valid options of allowed by the targeted filter + +```ts +filter: { + model: Filters.sliderRange, + filterOptions: { + sliderStartValue: 5 + } as SliderOption +} +``` + +#### Grid Option `defaultFilterOptions +You could also define certain options as a global level (for the entire grid or even all grids) by taking advantage of the `defaultFilterOptions` Grid Option. Note that they are set via the filter type as a key name (`autocompleter`, `date`, ...) and then the content is the same as `filterOptions` (also note that each key is already typed with the correct filter option interface), for example + +```ts +gridOptions.value = { + defaultFilterOptions: { + // Note: that `date`, `select` and `slider` are combining both compound & range filters together + date: { range: { min: 'today' } }, + select: { minHeight: 350 }, // typed as MultipleSelectOption + slider: { sliderStartValue: 10 } + } +} +``` + +### Using a Date Range Filter +The date range filter allows you to search data between 2 dates, it uses the [Vanilla-Calendar Range](https://vanilla-calendar.pro/) feature. + +> **Note** we use [Tempo](https://tempo.formkit.com/) to parse and format Dates to the chosen format via the `type` option when provided in your column definition. + +##### Component +import { Filters, Formatters, GridOption, OperatorType, VanillaCalendarOption } from '@slickgrid-universal/common'; + +```vue + +``` + +#### Filter Options (`VanillaCalendarOption` interface) +All the available options that can be provided as `filterOptions` to your column definitions can be found under this [VanillaCalendarOption interface](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/interfaces/vanillaCalendarOption.interface.ts) and you should cast your `filterOptions` with the expected interface to make sure that you use only valid settings of the [Vanilla-Calendar](https://vanilla-calendar.pro/docs/reference/additionally/settings) library. + +```ts +filter: { + model: Filters.compoundDate, + filterOptions: { + range: { min: 'today' } + } as VanillaCalendarOption +} +``` diff --git a/frameworks/slickgrid-vue/docs/column-functionalities/filters/select-filter.md b/frameworks/slickgrid-vue/docs/column-functionalities/filters/select-filter.md new file mode 100644 index 000000000..ca522fbc7 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/column-functionalities/filters/select-filter.md @@ -0,0 +1,685 @@ +### index +- [demo](#demo) +- [SASS styling](#sass-styling) +- [How to use Select Filter](#how-to-use-select-filter) +- [Default Search Terms](#default-search-terms) +- [How to add Translation](#how-to-add-translation) +- [How to filter empty values](#how-to-filter-empty-values) +- Collection Options + - [Add Blank Entry](#collection-add-blank-entry) + - [Add Custom Entry at Beginning/End of Collection](#collection-add-custom-entry-at-the-beginningend-of-the-collection) + - [Custom Structure](#custom-structure-keylabel-pair) + - [Custom Structure with Translation](#custom-structure-with-translation) + - [Collection filterBy/sortBy](#collection-filterbysortby) + - [Collection Label Prefix/Suffix](#collection-label-prefixsuffix) + - [Collection Label Render HTML](#collection-label-render-html) + - [Collection Async Load](#collection-async-load) + - [Collection Watch](#collection-watch) +- [`multiple-select.js` Options](#multiple-selectjs-options) + - [Filter Options (`MultipleSelectOption` interface)](#filter-options-multipleselectoption-interface) + - [Display shorter selected label text](#display-shorter-selected-label-text) +- [Query against a different field](#query-against-another-field-property) +- [Update Filters Dynamically](input-filter.md#update-filters-dynamically) +- [Custom Filter Predicate](input-filter.md#custom-filter-predicate) +- [Filter Shortcuts](input-filter.md#filter-shortcuts) + +### Demo +[Demo Page](https://ghiscoding.github.io/slickgrid-vue/#/slickgrid/Example4) / [Demo Component](https://github.com/ghiscoding/slickgrid-vue/blob/master/src/examples/slickgrid/Example4.tsx) + +##### Demo with Localization +[Demo Page](https://ghiscoding.github.io/slickgrid-vue/#/slickgrid/Example12) / [Demo Component](https://github.com/ghiscoding/slickgrid-vue/blob/master/src/examples/slickgrid/Example12.tsx) + +### Description +Multiple Select (dropdown) filter is useful when we want to filter the grid 1 or more search term value. + +We use an external lib named [multiple-select-vanilla](https://github.com/ghiscoding/multiple-select-vanilla). + +#### Note +For this filter to work you will need to add [Multiple-Select.js](http://wenzhixin.net.cn/p/multiple-select) to your project. This is a customized version of the original (thought all the original [lib options](http://wenzhixin.net.cn/p/multiple-select/docs/) are available so you can still consult the original site for all options). Couple of small options were added to suit SlickGrid-Universal needs, which is why it points to `slickgrid-universal/dist/lib` folder. This lib is required if you plan to use `multipleSelect` or `singleSelect` Filters. What was customized to (compare to the original) +- `okButton` option was added to add an OK button for simpler closing of the dropdown after selecting multiple options. + - `okButtonText` was also added for locale (i18n) +- `offsetLeft` option was added to make it possible to offset the dropdown. By default it is set to 0 and is aligned to the left of the select element. This option is particularly helpful when used as the last right column, not to fall off the screen. +- `autoDropWidth` option was added to automatically resize the dropdown with the same width as the select filter element. + +##### UI Sample +Scroll down below to see the [UI Print Screens](#ui-sample-1) + +### Types +There are 3 types of select filter +1. `Filters.singleSelect` which will filter the dataset with 1 value (uses `EQ` internally) with checkbox icons. +2. `Filters.multipleSelect` which will do a search with 1 or more values (uses `IN` internally) with radio icons. + +##### Less recommended +3. `Filters.select` which will filter the dataset with 1 value (uses `EQ` internally), same as `singleSelect` but uses styling from the browser setup. + - this one is less recommended, it is a simple and plain select dropdown. There are no styling applied and will be different in every browser. If you want a more consistent visual UI, it's suggested to use the other 2 filters (`multipleSelect` or `singleSelect`) + +### SASS Styling +You can change the `multipleSelect` and `singleSelect` styling with SASS [variables](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/styles/_variables.scss#L736) for styling. For more info on how to use SASS in your project, read the [Wiki - Styling](../../styling/styling.md) + +### How to use Select Filter +Simply set the flag `filterable` to True and and enable the filters in the Grid Options. Here is an example with a full column definition: +```ts +// define you columns, in this demo Effort Driven will use a Select Filter +columnDefinitions.value = [ + { id: 'title', name: 'Title', field: 'title' }, + { id: 'description', name: 'Description', field: 'description', filterable: true }, + { id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', + type: FieldType.boolean, + filterable: true, + filter: { + collection: [ { value: '', label: '' }, { value: true, label: 'true' }, { value: false, label: 'false' } ], + model: Filters.multipleSelect, + + // you can add "multiple-select" plugin options like styling the first row + filterOptions: { + offsetLeft: 14, + width: 100 + } as MultipleSelectOption, + + // you can also add an optional placeholder + placeholder: 'choose an option' + } + } +]; + +// you also need to enable the filters in the Grid Options +gridOptions.value = { + enableFiltering: true +}; +``` + +### Default Search Term(s) +If you want to load the grid with certain default filter(s), you can use the following optional property: +- `searchTerms` (array of values) + +#### Note +Even though the option of `searchTerms` it is much better to use the more powerful `presets` grid options, please refer to the [Grid State & Presets](../../grid-functionalities/Grid-State-&-Preset#grid-presets) for more info. + +**NOTE** +If you also have `presets` in the grid options, then your `searchTerms` will be ignored completely (even if it's a different column) since `presets` have higher priority over `searchTerms`. See [Grid State & Grid Presets](../../grid-functionalities/grid-state-preset.md) from more info. + +#### Sample +```ts +// define you columns, in this demo Effort Driven will use a Select Filter +columnDefinitions.value = [ + { id: 'title', name: 'Title', field: 'title' }, + { id: 'description', name: 'Description', field: 'description', filterable: true }, + { id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', + type: FieldType.boolean, + filterable: true, + filter: { + collection: [ { value: '', label: '' }, { value: true, label: 'true' }, { value: false, label: 'false' } ], + model: Filters.multipleSelect, + searchTerms: [true], + } +]; +``` + +### How to add Translation? +#### LabelKey +For the Select (dropdown) filter, you can fill in the "labelKey" property, if found it will translate it right away. If no `labelKey` is provided nothing will be translated (unless you have `enableTranslateLabel` set to true), else it will use "label" +```typescript +// define you columns, in this demo Effort Driven will use a Select Filter +columnDefinitions.value = [ + { id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', + formatter: Formatters.checkmarkMaterial, + type: FieldType.boolean, + filterable: true, + filter: { + collection: [ { value: '', label: '' }, { value: true, labelKey: 'TRUE' }, { value: false, label: 'FALSE' } ], + model: Filters.singleSelect, + } +]; +``` + +#### enableTranslateLabel +You could also use the `enableTranslateLabel` which will translate regardless of the label key name (so it could be used with `label`, `labelKey` or even a `customStructure` label). +```typescript +// define you columns, in this demo Effort Driven will use a Select Filter +columnDefinitions.value = [ + { id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', + formatter: Formatters.checkmarkMaterial, + type: FieldType.boolean, + filterable: true, + filter: { + collection: [ { value: '', label: '' }, { value: true, label: 'true' }, { value: false, label: 'false' } ], + model: Filters.singleSelect, + enableTranslateLabel: true + } + } +]; +``` + +### Custom Structure (key/label pair) +What if your select options (collection) have totally different value/label pair? In this case, you can use the `customStructure` to change the property name(s) to use. You can change the label and/or the value, they can be passed independently. +```typescript +// define you columns, in this demo Effort Driven will use a Select Filter +columnDefinitions.value = [ + { id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', + formatter: Formatters.checkmarkMaterial, + type: FieldType.boolean, + filterable: true, + filter: { + collection: [ + { customValue: '', customLabel: '' }, + { customValue: true, customLabel: 'true' }, + { customValue: false, customLabel: 'false' } + ], + customStructure: { + label: 'customLabel', + value: 'customValue' + }, + model: Filters.multipleSelect, + } + } +]; +``` + +### Custom Structure with Translation +What if you want to use `customStructure` and translate the labels? Simply pass the flag `enableTranslateLabel: true` + +```typescript +// define you columns, in this demo Effort Driven will use a Select Filter +columnDefinitions.value = [ + { id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', + formatter: Formatters.checkmarkMaterial, + type: FieldType.boolean, + filterable: true, + filter: { + collection: [ + { customValue: '', customLabel: '' }, + { customValue: true, customLabel: 'TRUE' }, + { customValue: false, customLabel: 'FALSE' } + ], + customStructure: { + label: 'customLabel', + value: 'customValue' + }, + enableTranslateLabel: true, + model: Filters.multipleSelect, + } + } +]; +``` + +### How to filter empty values? +By default you cannot filter empty dataset values (unless you use a `multipleSelect` Filter). You might be wondering, why though? By default an empty value in a `singleSelect` Filter is equal to returning **all values**. You could however use this option `emptySearchTermReturnAllValues` set to `false` to add the ability to really search only empty values. + +Note: the defaults for single & multiple select filters are different +- single select filter default is `emptySearchTermReturnAllValues: true` +- multiple select filter default is `emptySearchTermReturnAllValues: false` + +```ts +// define you columns, in this demo Effort Driven will use a Select Filter +columnDefinitions.value = [ + { id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', + formatter: Formatters.checkmarkMaterial, + type: FieldType.boolean, + filterable: true, + filter: { + collection: [ { value: '', label: '' }, { value: true, labelKey: 'TRUE' }, { value: false, label: 'FALSE' } ], + model: Filters.singleSelect, + emptySearchTermReturnAllValues: false, // False when we really want to filter empty values + } + } +]; +``` + +### Collection FilterBy/SortBy +You can also pre-sort or pre-filter the collection given to the multipleSelect/singleSelect Filters. Also note that if the `enableTranslateLabel` flag is set to `True`, it will use the translated value to filter or sort the collection. The supported filters are +1. `equal`: filters the collection for only the value provided in `collectionFilterBy.value` +2. `notEqual`: filters the collection for every value **EXCEPT** the value provided in `collectionFilterBy.value` +3. `in`: supports `collectionFilterBy.property` as a nested array, and if the `collectionFilterBy.value` exists in the nested array, the parent item will remain in the collection (**NOT** be filtered from the collection). For example: `columnDef.filter.collection: [{ foo: ['bar'] }, { foo: ['foo'] }]`, `collectionFilterBy.property: 'foo'`, `collectionFilterBy.value: 'bar'` will return the first item in the collection only +4. `notIn`:opposite of `in` +5. `contains`: assumes the `collectionFilterBy.value` is an array and will check if any of those values exists in the `collectionFilterBy.property`. For example: `collection: [{ foo: 'bar' }, { foo: 'foo' }]`, `collectionFilterBy.property: 'foo'`, `collectionFilterBy.value: [ 'bar', 'foo' ]` will return bot items + +Full example: +```typescript +// define you columns, in this demo Effort Driven will use a Select Filter +columnDefinitions.value = [ + { id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', + formatter: Formatters.checkmarkMaterial, + type: FieldType.boolean, + filterable: true, + filter: { + collection: [ + { value: '', label: '' }, + { value: true, label: 'true' }, + { value: false, label: 'false' }, + { value: undefined, label: 'undefined' } + ], + collectionFilterBy: { + property: 'effortDriven', + operator: OperatorType.notEqual, + value: undefined + }, + collectionSortBy: { + property: 'effortDriven', // will sort by translated value since "enableTranslateLabel" is true + sortDesc: false, // defaults to "false" when not provided + fieldType: FieldType.boolean // defaults to FieldType.string when not provided + }, + model: Filters.multipleSelect + } + } +]; +``` + +#### Multiple FilterBy/SortBy +You can also pass multiple `collectionFilterBy` or `collectionSortBy` simply by changing these object to array of objects. + +```typescript +// prepare a multiple-select array to filter with +const multiSelectFilterArray = []; +for (let i = 0; i < 365; i++) { + multiSelectFilterArray.push({ value: i, label: i, labelSuffix: ' days' }); +} + +columnDefinitions.value = [ + { id: 'duration', name: 'Duration', field: 'duration', + formatter: Formatters.checkmarkMaterial, + type: FieldType.boolean, + filterable: true, + filter: { + collection: multiSelectFilterArray, + collectionFilterBy: [{ + property: 'value', + operator: OperatorType.notEqual, // remove day 1 + value: 1 + }, { + property: 'value', + operator: OperatorType.notEqual, // remove day 365 + value: 365 + }], + model: Filters.multipleSelect + } + } +]; +``` +However please note that by default the `collectionFilterBy` will **not** merge the result after each pass, it will instead chain them and use the new returned collection after each pass (which means that if original collection is 100 items and 20 items are returned after 1st pass, then the 2nd pass will filter out of these 20 items and so on). + +What if you wanted to merge the results instead? Then in this case, you can change the `filterResultAfterEachPass` flag defined in `collectionOptions + +```typescript +columnDefinitions.value = [ + { id: 'duration', name: 'Duration', field: 'duration', + filter: { + collection: [yourCollection], + collectionFilterBy: [ + // ... + ], + collectionOptions: { + filterResultAfterEachPass: 'chain' // options are "merge" or "chain" (defaults to "chain") + }, + model: Filters.multipleSelect + } + } +]; +``` + +#### LabelPrefix / LabelSuffix +`labelPrefix` and `labelSuffix` were recently added, they are also supported by the `customStructure` and can also be overridden. See [Collection Label Prefix/Suffix](#collection-label-prefixsuffix) + +### Custom Structure with Translation +What if you want to use `customStructure` and translate the labels? Simply pass the flag `enableTranslateLabel: true` + +```typescript +// define you columns, in this demo Effort Driven will use a Select Filter +columnDefinitions.value = [ + { id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', + formatter: Formatters.checkmarkMaterial, + type: FieldType.boolean, + filterable: true, + filter: { + collection: [ + { customValue: '', customLabel: '' }, + { customValue: true, customLabel: 'TRUE' }, + { customValue: false, customLabel: 'FALSE' } + ], + customStructure: { + label: 'customLabel', + value: 'customValue' + }, + enableTranslateLabel: true, + model: Filters.multipleSelect, + } + } +]; +``` + +### Collection FilterBy/SortBy +You can also pre-sort or pre-filter the collection given to the multipleSelect/singleSelect Filters. Also note that if the `enableTranslateLabel` flag is set to `True`, it will use the translated value to filter or sort the collection. For example: +```typescript +// define you columns, in this demo Effort Driven will use a Select Filter +columnDefinitions.value = [ + { + id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', + formatter: Formatters.checkmarkMaterial, + type: FieldType.boolean, + filterable: true, + filter: { + collection: [ + { value: '', label: '' }, + { value: true, label: 'true' }, + { value: false, label: 'false' }, + { value: undefined, label: 'undefined' } + ], + collectionFilterBy: { + property: 'effortDriven', + operator: OperatorType.equal, // defaults to equal when not provided + value: undefined + }, + collectionSortBy: { + property: 'effortDriven', // will sort by translated value since "enableTranslateLabel" is true + sortDesc: false, // defaults to "false" when not provided + fieldType: FieldType.boolean // defaults to FieldType.string when not provided + }, + model: Filters.multipleSelect + } + } +]; +``` + +### Collection Label Prefix/Suffix +You can use `labelPrefix` and/or `labelSuffix` which will concatenate the multiple properties together (`labelPrefix` + `label` + `labelSuffix`) which will used by each Select Filter option label. You can also use the property `separatorBetweenTextLabels` to define a separator between prefix, label & suffix. + +**Note** +If `enableTranslateLabel` flag is set to `True`, it will also try to translate the Prefix / Suffix / OptionLabel texts. + +For example, say you have this collection +```typescript +const currencies = [ + { symbol: '$', currency: 'USD', country: 'USA' }, + { symbol: '$', currency: 'CAD', country: 'Canada' } +]; +``` + +You can display all of these properties inside your dropdown labels, say you want to show (symbol with abbreviation and country name). Now you can. + +So you can create the `multipleSelect` Filter with a `customStructure` by using the symbol as prefix, and country as suffix. That would make up something like this: +- $ USD USA +- $ CAD Canada + +with a `customStructure` defined as +```typescript +filter: { + collection: currencies, + customStructure: { + value: 'currency', + label: 'currency', + labelPrefix: 'symbol', + labelSuffix: 'country', + }, + collectionOptions: { + separatorBetweenTextLabels: ' ' // add white space between each text + }, + model: Filters.multipleSelect +} +``` + + +### Collection Label Render HTML +By default HTML is not rendered and the `label` will simply show HTML as text. But in some cases you might want to render it, you can do so by enabling the `enableRenderHtml` flag. + +**NOTE:** this is currently only used by the Filters that have a `collection` which are the `MultipleSelect` & `SingleSelect` Filters. + +```typescript +columnDefinitions.value = [ + { + id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', + formatter: Formatters.checkmarkMaterial, + type: FieldType.boolean, + filterable: true, + filter: { + // display checkmark icon when True + enableRenderHtml: true, + collection: [{ value: '', label: '' }, { value: true, label: 'True', labelPrefix: ` ` }, { value: false, label: 'False' }], + model: Filters.singleSelect + } + } +]; +``` + +### Collection Add Blank Entry +In some cases a blank entry at the beginning of the collection could be useful, the most common example for this is to use the first option as a blank entry to tell our Filter to show everything. So for that we can use the `addBlankEntry` flag in `collectionOptions + +```typescript +columnDefinitions.value = [ + { id: 'duration', name: 'Duration', field: 'duration', + filter: { + collection: [yourCollection], + collectionOptions: { + addBlankEntry: true + }, + model: Filters.multipleSelect + } + } +]; +``` + +### Collection Add Custom Entry at the Beginning/End of the Collection +We can optionally add a custom entry at the beginning of the collection, the most common example for this is to use the first option as a blank entry to tell our Filter to show everything. So for that we can use the `addCustomFirstEntry` or `addCustomLastEntry` flag in `collectionOptions + +```typescript +columnDefinitions.value = [ + { id: 'duration', name: 'Duration', field: 'duration', + filter: { + collection: [yourCollection], + collectionOptions: { + addCustomFirstEntry: { value: '', label: '--n/a--' } + + // or at the end + addCustomLastEntry: { value: 'end', label: 'end' } + }, + model: Filters.multipleSelect + } + } +]; +``` + +### Collection Async Load +You can also load the collection asynchronously, but for that you will have to use the `collectionAsync` property, which expect a Promise to be passed (it actually accepts 3 types: `HttpClient`, `FetchClient` or regular `Promise`). + +#### Load the collection through an Http call + +```ts +columnDefinitions.value = [ + { + id: 'prerequisites', name: 'Prerequisites', field: 'prerequisites', + filterable: true, + filter: { + collectionAsync: http.fetch('api/data/pre-requisites'), + model: Filters.multipleSelect, + } + } +]; +``` + +#### Using Async Load when Collection is inside an Object Property +What if my collection is nested under the response object? For that you can use `collectionInsideObjectProperty` to let the filter know how to get the collection. + +```ts +columnDefinitions.value = [ + { + id: 'prerequisites', name: 'Prerequisites', field: 'prerequisites', + filterable: true, + filter: { + model: Filters.multipleSelect, + + // this async call will return the collection inside the response object in this format + // { data: { myCollection: [ /*...*/ ] } } + collectionAsync: http.fetch('api/data/pre-requisites'), + collectionOptions: { + collectionInsideObjectProperty: 'data.myCollection' // with/without dot notation + } + } + } +]; +``` + +#### Modifying the collection afterward +If you want to modify the `collection` afterward, you simply need to find the associated Column reference from the Column Definition and modify the `collection` property (not `collectionAsync` since that is only meant to be used on page load). + +For example +```ts +function addItem() { + const lastRowIndex = dataset.length; + const newRows = mockData(1, lastRowIndex); + + // wrap into a timer to simulate a backend async call + setTimeout(() => { + const requisiteColumnDef = columnDefinitions.find((column: Column) => column.id === 'prerequisites'); + if (requisiteColumnDef) { + const filterCollection = requisiteColumnDef.filter.collection; + + if (Array.isArray(collectionFilterAsync)) { + // add the new row to the grid + vueGrid.gridService.addItemToDatagrid(newRows[0]); + + // then refresh the Filter "collection", we have 2 ways of doing it + + // 1- Push to the Filter "collection" + filterCollection.push({ value: lastRowIndex, label: lastRowIndex, prefix: 'Task' }); + + // or 2- replace the entire "collection" + filterCollection = [...collection, ...[{ value: lastRowIndex, label: lastRowIndex }]]; + } + } + }, 250); +} +``` + +### Collection Watch +We can enable the collection watch via the column filter `enableCollectionWatch` flag, or if you use a `collectionAsync` then this will be enabled by default. The collection watch will basically watch for any changes applied to the collection (any mutation changes like `push`, `pop`, `unshift`, ...) and will also watch for the `filter.collection` array replace, when any changes happens then it will re-render the Select Filter with the updated collection list. + +```ts +columnDefinitions.value = [ + { + id: 'title', name: 'Title', field: 'title', + filterable: true, + filter: { + collection: [ /* ... */ ], + model: Filters.singleSelect, + enableCollectionWatch: true, + } + } +]; +``` + +### Filter Options (`MultipleSelectOption` interface) +All the available options that can be provided as `filterOptions` to your column definitions can be found under this [multipleSelectOption interface](/ghiscoding/slickgrid-universal/tree/master/packages/common/src/interfaces/multipleSelectOption.interface.ts) and you should cast your `filterOptions` to that interface to make sure that you use only valid options of the `multiple-select.js` library. + +```ts +filter: { + model: Filters.singleSelect, + filterOptions: { + maxHeight: 400 + } as MultipleSelectOption +} +``` + +#### Grid Option `defaultFilterOptions +You could also define certain options as a global level (for the entire grid or even all grids) by taking advantage of the `defaultFilterOptions` Grid Option. Note that they are set via the filter type as a key name (`autocompleter`, `date`, ...) and then the content is the same as `filterOptions` (also note that each key is already typed with the correct filter option interface), for example +```ts +gridOptions.value = { + defaultFilterOptions: { + // Note: that `select` combines both multipleSelect & singleSelect + select: { minHeight: 350 }, // typed as MultipleSelectOption + } +} +``` + +### Multiple-select.js Options +You can use any options from [Multiple-Select.js](http://wenzhixin.net.cn/p/multiple-select) and add them to your `filterOptions` property. However please note that this is a customized version of the original (all original [lib options](http://wenzhixin.net.cn/p/multiple-select/docs/) are available so you can still consult the original site for all options). + +Couple of small options were added to suit SlickGrid-Universal needs, which is why we are using a fork [ghiscoding/multiple-select-modified](https://github.com/ghiscoding/multiple-select-modified) folder (which is our customized version of the original). This lib is required if you plan to use `multipleSelect` or `singleSelect` Filters. What was customized to (compare to the original) is the following: +- `okButton` option was added to add an OK button for simpler closing of the dropdown after selecting multiple options. + - `okButtonText` was also added for locale (i18n) +- `offsetLeft` option was added to make it possible to offset the dropdown. By default it is set to 0 and is aligned to the left of the select element. This option is particularly helpful when used as the last right column, not to fall off the screen. +- `autoDropWidth` option was added to automatically resize the dropdown with the same width as the select filter element. +- `autoAdjustDropHeight` (defaults to true), when set will automatically adjust the drop (up or down) height +- `autoAdjustDropPosition` (defaults to true), when set will automatically calculate the area with the most available space and use best possible choise for the drop to show (up or down) +- `autoAdjustDropWidthByTextSize` (defaults to true), when set will automatically adjust the drop (up or down) width by the text size (it will use largest text width) +- to extend the previous 3 autoAdjustX flags, the following options can be helpful + - `minWidth` (defaults to null, to use when `autoAdjustDropWidthByTextSize` is enabled) + - `maxWidth` (defaults to 500, to use when `autoAdjustDropWidthByTextSize` is enabled) + - `adjustHeightPadding` (defaults to 10, to use when `autoAdjustDropHeight` is enabled), when using `autoAdjustDropHeight` we might want to add a bottom (or top) padding instead of taking the entire available space + - `maxHeight` (defaults to 275, to use when `autoAdjustDropHeight` is enabled) +- useSelectOptionLabel` to show different selected label text (on the input select element itself) + - `useSelectOptionLabelToHtml` is also available if you wish to render label text as HTML for these to work, you have define the `optionLabel` in the `customStructure` + +##### Code +```typescript +columnDefinitions.value = [ + { + id: 'isActive', name: 'Is Active', field: 'isActive', + filterable: true, + filter: { + collection: [{ value: '', label: '' }, { value: true, label: 'true' }, { value: false, label: 'false' }], + model: Filters.singleSelect, + filterOptions: { + // add any multiple-select.js options (from original or custom version) + autoAdjustDropPosition: false, // by default set to True, but you can disable it + position: 'top' + } as MultipleSelectOption + } + } +]; +``` + +#### Display shorter selected label text +If we find that our text shown as selected text is too wide, we can choose change that by using `optionLabel` in Custom Structure. +```typescript +columnDefinitions.value = [ + { + id: 'isActive', name: 'Is Active', field: 'isActive', + filterable: true, + filter: { + collection: [ + { value: 1, label: '1', suffix: 'day' }, + { value: 2, label: '2', suffix: 'days' }, + { value: 3, label: '3', suffix: 'days' }, + // ... + ], + model: Filters.multipleSelect, + customStructure: { + label: 'label', + labelSuffix: 'suffix', + value: 'value', + optionLabel: 'value', // use value instead to show "1, 2" instead of "1 day, 2 days" + }, + filterOptions: { + // use different label to show as selected text + // please note the Custom Structure with optionLabel defined + // or use "useSelectOptionLabelToHtml" to render HTML + useSelectOptionLabel: true + } as MultipleSelectOption + } + } +]; +``` + +### Query against another field property +What if your grid is displaying a certain `field` but you wish to query against another field? Well you could do that with 1 of the following 3 options: +- `queryField` (query on a specific field for both the Filter and Sort) +- `queryFieldFilter` (query on a specific field for only the Filter) +- `queryFieldSorter` (query on a specific field for only the Sort) + +##### Example +```ts +columnDefinitions.value = [ + { + id: 'salesRepName', + field: 'salesRepName', // display in Grid the sales rep name with "field" + queryFieldFilter: 'salesRepId', // but query against a different field for our multi-select to work + filterable: true, + filter: { + collection: salesRepList, // an array of Sales Rep + model: Filters.multipleSelect, + customStructure: { + label: 'salesRepName', + value: 'salesRepId' + } + } + } +]; +``` diff --git a/frameworks/slickgrid-vue/docs/column-functionalities/filters/single-search-filter.md b/frameworks/slickgrid-vue/docs/column-functionalities/filters/single-search-filter.md new file mode 100644 index 000000000..4086d8c5f --- /dev/null +++ b/frameworks/slickgrid-vue/docs/column-functionalities/filters/single-search-filter.md @@ -0,0 +1,129 @@ +#### Index +- [Update Filters Dynamically](input-filter.md#update-filters-dynamically) +- [Custom Filter Predicate](input-filter.md#custom-filter-predicate) + +### Description +Some users might want to have 1 main single search for filtering the grid data instead of using multiple column filters. You can see a demo of that below + +### Demo +[Demo Page](https://ghiscoding.github.io/slickgrid-vue/#/slickgrid/Example21) / [Demo Component](https://github.com/ghiscoding/slickgrid-vue/blob/master/src/examples/slickgrid/Example21.tsx#L162) + +### Code Sample +##### Component +```vue + + + +``` + +## Sample +![2019-04-16_15-42-05](https://user-images.githubusercontent.com/643976/56239148-3b530680-605e-11e9-99a2-e9a163abdd0c.gif) diff --git a/frameworks/slickgrid-vue/docs/column-functionalities/filters/styling-filled-filters.md b/frameworks/slickgrid-vue/docs/column-functionalities/filters/styling-filled-filters.md new file mode 100644 index 000000000..96f49f92d --- /dev/null +++ b/frameworks/slickgrid-vue/docs/column-functionalities/filters/styling-filled-filters.md @@ -0,0 +1,45 @@ +You can style any (or all) of the Filter(s) when they have value, the lib will automatically add a `filled` CSS class which you can style as you see fit. There is no style by default, if you wish to add styling, you will be required to add your own. + +## Styled Example +![grid_filled_styling](https://user-images.githubusercontent.com/643976/51334569-14306d00-1a4e-11e9-816c-439796eb8a59.png) + +## Code example +For example, the print screen shown earlier was styled using this piece of SASS (`.scss`) code. You can customize the styling to your liking. + +```scss +$slick-filled-filter-color: $slick-form-control-focus-border-color; +$slick-filled-filter-border: $slick-form-control-border; +$slick-filled-filter-box-shadow: $slick-form-control-focus-border-color; +$slick-filled-filter-font-weight: 400; + +.slick-headerrow { + input.search-filter.filled, + .search-filter.filled input, + .search-filter.filled .date-picker input, + .search-filter.filled .input-group-addon.slider-value, + .search-filter.filled .input-group-addon.slider-range-value, + .search-filter.filled .input-group-addon select { + color: $slick-filled-filter-color; + font-weight: $slick-filled-filter-font-weight; + border: $slick-filled-filter-border; + box-shadow: $slick-filled-filter-box-shadow; + &.input-group-prepend { + border-right: 0; + } + &.input-group-append { + border-left: 0; + } + } + .search-filter.filled .input-group-prepend select { + border-right: 0; + } + .search-filter.filled .ms-choice { + box-shadow: $slick-filled-filter-box-shadow; + border:$slick-filled-filter-border; + span { + font-weight: $slick-filled-filter-font-weight; + color: $slick-filled-filter-color; + } + } +} +``` diff --git a/frameworks/slickgrid-vue/docs/column-functionalities/formatters.md b/frameworks/slickgrid-vue/docs/column-functionalities/formatters.md new file mode 100644 index 000000000..a4d0b3a9a --- /dev/null +++ b/frameworks/slickgrid-vue/docs/column-functionalities/formatters.md @@ -0,0 +1,306 @@ +# Formatters + +#### Index + +* [Built-in Formatter](#list-of-provided-formatters) +* [Extra Params/Arguments](#extra-argumentsparams) +* [Using Multiple Formatters](#using-multiple-formatters) +* [Custom Formatter](#custom-formatter) + - [Example of a Custom Formatter with HTML string](#example-of-a-custom-formatter-with-html-string) + - [Example with `FormatterResultObject` instead of a string](#example-with-formatterresultobject-instead-of-a-string) + - [Example of Custom Formatter with Native DOM Element](#example-of-custom-formatter-with-native-dom-element) +* [Common Formatter Options](#common-formatter-options) +* [PostRenderer Formatter](#postrender-formatter) +* [UI Sample](#ui-sample) + +### Demo +[Demo Page](https://ghiscoding.github.io/slickgrid-vue/#/slickgrid/Example2) / [Demo ViewModel](https://github.com/ghiscoding/slickgrid-vue/blob/master/src/examples/slickgrid/Example2.tsx) + +#### Definition + +`Formatters` are functions that can be used to change and format certain column(s) in the grid. Please note that it does not alter the input data, it simply changes the styling by formatting the data differently to the screen (what the user visually see). + +A good example of a `Formatter` could be a column name `isActive` which is a `boolean` field with input data as `True` or `False`. User would prefer to simply see a checkbox as a visual indication representing the `True` flag, for this behavior you can use `Formatters.checkmark` which will use Material Design icon of `mdi-check` when `True` or an empty string when `False`. + +For a [UI sample](#ui-sample), scroll down below. + +#### Provided Formatters + +`Slickgrid-Universal` ships with a few `Formatters` by default which helps with common fields, you can see the [entire list here](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/formatters/index.ts#L37). + +> **Note** you might not need a Formatter when a simple CSS style and class might be enough, think about using `cssClass` column property as much as possible since it has much better perf. + +**List of provided `Formatters`** + +* `arrayObjectToCsv`: Takes an array of complex objects converts it to a comma delimited string. +* `arrayToCsv` : takes an array of text and returns it as CSV string +* `checkmarkMaterial` use Material Design to display a checkmark icon +* `collection`: Looks up values from the columnDefinition.params.collection property and displays the label in CSV or string format +* `complexObject`: takes a complex object (with a `field` that has a `.` notation) and pull correct value, there are multiple ways to use it + 1. `{ id: 'firstName', field: 'user.firstName', formatter: Formatters.complexObject}`, will display the user's first name + 2. `{ id: 'firstName', field: 'user', labelKey: 'firstName', params: { complexField: 'user' }, ... }` + 3. `{ id: 'firstName', field: 'user', params: { complexField: 'user.firstName' }, ... }` +* `currency`: will help with currency other than dollar (ie `โ‚ฌ`), there are multiple `params` available for this formatter + * `currencyPrefix`, `currencySuffix`, `minDecimal`, `maxDecimal`, `numberPrefix`, `numberSuffix`, `decimalSeparator`, `thousandSeparator` and `displayNegativeNumberWithParentheses` + * the distinction between `numberPrefix` and `currencyPrefix` can be seen when using `displayNegativeNumberWithParentheses`, for example if we have a value of `-12.432` with the `Formatters.currency` and `params: { currencyPrefix: 'โ‚ฌ', numberPrefix: 'Price ', numberSuffix: 'EUR' }` the output will be `"Price (โ‚ฌ12.432) EUR"` +* `dateEuro`: Takes a Date object and displays it as an Euro Date format (DD/MM/YYYY) +* `dateTimeEuro`: Takes a Date object and displays it as an Euro Date+Time format (DD/MM/YYYY HH:mm:ss) +* `dateTimeShortEuro`: Takes a Date object and displays it as an Euro Date+Time (without seconds) format (DD/MM/YYYY HH:mm) +* `dateTimeEuroAmPm`: Takes a Date object and displays it as an Euro Date+Time+(am/pm) format (DD/MM/YYYY hh:mm:ss a) +* `dateIso` : Takes a Date object and displays it as an ISO Date format (YYYY-MM-DD) +* `dateTimeIso` : Takes a Date object and displays it as an ISO Date+Time format (YYYY-MM-DD HH:mm:ss) +* `dateTimeIsoAmPm` : Takes a Date object and displays it as an ISO Date+Time+(am/pm) format (YYYY-MM-DD h:mm:ss a) +* `dateTimeShortIso`: Takes a Date object and displays it as an ISO Date+Time (without seconds) format (YYYY-MM-DD HH:mm) +* `dateUs` : Takes a Date object and displays it as an US Date format (MM/DD/YYYY) +* `dateTimeUs` : Takes a Date object and displays it as an US Date+Time format (MM/DD/YYYY HH:mm:ss) +* `dateTimeShortUs`: Takes a Date object and displays it as an US Date+Time (without seconds) format (MM/DD/YYYY HH:mm:ss) +* `dateTimeUsAmPm` : Takes a Date object and displays it as an US Date+Time+(am/pm) format (MM/DD/YYYY hh:mm:ss a) +* `dateUtc` : Takes a Date object and displays it as a TZ format (YYYY-MM-DDThh:mm:ssZ) +* `decimal`: Display the value as x decimals formatted, defaults to 2 decimals. You can pass "minDecimal" and/or "maxDecimal" to the "params" property. +* `dollar`: Display the value as 2 decimals formatted with dollar sign '$' at the end of of the value. +* `dollarColored`: Display the value as 2 decimals formatted with dollar sign '$' at the end of of the value, change color of text to red/green on negative/positive value +* `dollarColoredBoldFormatter`: Display the value as 2 decimals formatted with dollar sign '$' at the end of of the value, change color of text to red/green on negative/positive value, show it in bold font weight as well +* `hyperlink`: takes a URL cell value and wraps it into a clickable hyperlink `value` + * the cell value **must contain** (`ftp://abc`, `http://abc` or `https://abc`), if it doesn't then use `fakeHyperlink` +* `hyperlinkUriPrefix`: format a URI prefix into an hyperlink +* `icon`: to display an icon with defined CSS class name, use `params` to pass a `cssClass` property +* `iconBoolean`: similar to `icon` but will only be displayed on a Boolean truthy value, use `params` to pass a `cssClass` property +* `mask`: to change the string output using a mask, use `params` to pass a `mask` property + * example: `{ field: 'phone', formatter: Formatters.mask, params: { mask: '(000) 000-0000' }}` +* `multiple`: pipe multiple formatters (executed in sequence), use `params` to pass the list of formatters. + * example: `{ field: 'title', formatter: Formatters.multiple, params: { formatters: [ Formatters.lowercase, Formatters.uppercase ] }` +* `percent`: Takes a cell value number (between 0.0-1.0) and displays a red (<50) or green (>=50) bar +* `percentComplete` : takes a percentage value (0-100%), displays a bar following this logic: + * `red`: < 30%, `grey`: < 70%, `green`: >= 70% +* `percentCompleteBar` : same as `percentComplete` but uses [Bootstrap - Progress Bar with label](https://getbootstrap.com/docs/3.3/components/#progress-label). +* `percentCompleteBarWithText`: Takes a cell value number (between 0-100) and displays SlickGrid custom "percent-complete-bar" with Text a red (<30), silver (>30 & <70) or green (>=70) bar +* `percentSymbol`: Takes a cell value number (between 0-100) and add the "%" after the number +* `progressBar`: Takes a cell value number (between 0-100) and displays Bootstrap "progress-bar" a red (<30), silver (>30 & <70) or green (>=70) bar +* `translate`: Takes a cell value and translates it (i18n). Requires an instance of the Translate Service:: \`i18n: translate +* `translateBoolean`: Takes a boolean value, cast it to upperCase string and finally translates it (i18n). +* `tree`: Formatter that must be used when the column is a Tree Data column + +> **Note:** The list is certainly not up to date (especially for Dates), please refer to the [Formatters export](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/formatters/index.ts#L37) to know exactly which formatters are available. + +> **Note** all Date formatters are formatted using [Tempo](https://tempo.formkit.com/#format-tokens). There are also many more Date formats not shown above, simply visit the [formatters.index](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/formatters/formatters.index.ts#L101) to see all available Date/Time formats. + +### Usage +To use any of them, you simply need to import `Formatters` from `Slickgrid-Universal` and add a `formatter: Formatters.xyz` (where `xyx` is the name of the built-in formatter) in your column definitions as shown below: + +```vue + +``` + +#### Extra Arguments/Params + +What if you want to pass extra arguments that you want to use within the Formatter? You can use `params` for that. For example, let say you have a custom formatter to build a select list (dropdown), you could do it this way: + +```ts +let optionList = ['True', 'False']; + +columnDefinitions.value = [ + { id: 'title', field: 'title', + headerTranslate: 'TITLE', + formatter: myCustomSelectFormatter, + params: { options: optionList } + }, +]; +``` + +### Using Multiple Formatters + +SlickGrid only has 1 `formatter` property but if you want to use more than 1 Formatter then you'll want to use the `Formatters.multiple` and pass every Formatters inside your column definition `params: { formatters: [] }` as shown below. + +**Note:** please note that combining multiple Formatters has the side effect of cascading the formatted `value` output to the next Formatter. So for example if you use the `complexObject` and `dollar` Formatters, you want to make sure to define them in the correct order in your `formatters: []` array as shown below. + +* what if you want to avoid overwriting the `value` with a Custom Formatter? + * in that case you can have your Formatter return a [FormatterResultObject](#formatterresultobject), see below. + +```ts +// Data Example:: +// data = [{ shipping: { cost: 123.22, address: { zip: 123456 } } }]; + +columnDefinitions.value = [ + { + id: 'shippingCost', field: 'shipping.cost', name: 'Shipping Cost', + formatter: Formatters.multiple, + params: { + // list of Formatters (the order is very important) + formatters: [Formatters.complexObject, Formatters.dollar], + } + }, +]; +``` + +### Custom Formatter + +You could also create your own custom `Formatter` by simply following the structure shown below. + +#### TypeScript function signature + +```ts +export type Formatter = (row: number, cell: number, value: any, columnDef: Column, dataContext: any, grid?: any) => string | FormatterResultObject; +``` + +#### FormatterResultObject + +The most common return result of a Formatter is a `string` but you could also use the `FormatterResultObject` which is an object with the interface signature shown below. The main difference is that it will allow to add CSS classes directly to the cell container instead of having to create an extra `
` container and since that is the main cell container, you can add multiple Formatters to add/remove multiple CSS classes on that same container. + +```ts +export interface FormatterResultObject { + addClasses?: string; + removeClasses?: string; + text: string; + toolTip?: string; +} +``` + +#### Example of a Custom Formatter with HTML string + +For example, we will use our optional SVG icons `.mdi` with a `boolean` as input data, and display a (fire) icon when `True` or a (snowflake) when `False`. This custom formatter is actually display in the [UI sample](#ui-sample) shown below. + +```ts +// create a custom Formatter with the Formatter type +const myCustomCheckboxFormatter: Formatter = (row: number, cell: number, value: any, columnDef: Column, dataContext: any, grid?: any) => + value ? { addClasses: 'mdi mdi-fire', text: '', tooltip: 'burning fire' } : ''; +``` + +#### Example with `FormatterResultObject` instead of a string + +Using this object return type will provide the user the same look and feel, it will actually be quite different. The major difference is that all of the options (`addClasses`, `tooltip`, ...) will be added the CSS container of the cell instead of the content of that container. For example a typically cell looks like the following `
Task 4
` and if use `addClasses: 'red'`, it will end up being `
Task 4
` while if we use a string output of let say `${value>`, then our final result of the cell will be `
Task 4
`. This can be useful if you plan to use multiple Formatters and don't want to lose or overwrite the previous Formatter result (we do that in our project). + +```ts +// create a custom Formatter and returning a string and/or an object of type FormatterResultObject +const myCustomCheckboxFormatter: Formatter = (row: number, cell: number, value: any, columnDef: Column, dataContext: any, grid?: any) => + value ? { addClasses: 'mdi mdi-fire', text: '', tooltip: 'burning fire' } : ''; +``` + +### Example of Custom Formatter with Native DOM Element +Since version 4.x, you can now also return native DOM element instead of an HTML string. There are 2 main reasons for going with this approach: +1. CSP Safe by default, since it's native it is 100% CSP Safe (CSP: Content Security Policy) +2. Performance (the reasons are similar to point 1.) + - since it's native it can be appended directly to the grid cell + - when it's an HTML string, it has to do 2 extra steps (which is an overhead process) + i. sanitize the string (when a sanitizer, for example [DOMPurify](https://github.com/cure53/DOMPurify)) + ii. SlickGrid then has to convert it to native element by using `innerHTML` on the grid cell + +Demo +```ts +export const iconFormatter: Formatter = (_row, _cell, _value, columnDef) => { + const iconElm = document.createElement('span'); + iconElm.className = 'mdi mdi-check'; + + return iconElm; +}; +``` + +> **Note** you could also use our helper `createDomElement` which allows to create a DOM element and pass properties like `className` in 1 liner (and it also works with intellisense). For example `createDomElement('span', { className: 'bold title', textContent: 'Hello World', title: 'some tooltip description' })` would equal to 4 lines of code. + +### More Complex Example +If you need to add more complex logic to a `Formatter`, you can take a look at the [percentCompleteBar](https://github.com/ghiscoding/slickgrid-vue/blob/master/slickgrid-vue/src/slickgrid-vue/formatters/percentCompleteBarFormatter.ts) `Formatter` for more inspiration. + +### Common Formatter Options +You can set some defined common Formatter Options in your Grid Options through the `formatterOptions` in the Grid Options (locally or globally) as seen below, and/or independently through the column definition `params` (the option names are the same in both locations) + +```ts +function loadGrid() { + columnDefinitions.value = [ + // through the column definition "params" + { id: 'price', field: 'price', params: { thousandSeparator: ',' } }, + ]; + + // or through grid options to make it global + gridOptions.value = { + autoResize: { + containerId: 'demo-container', + sidePadding: 15 + }, + enableAutoResize: true, + + // you customize the date separator through "formatterOptions" + formatterOptions: { + // What separator to use to display a Date, for example using "." it could be "2002.01.01" + dateSeparator: '/', // can be any of '/' | '-' | '.' | ' ' | '' + + // Defaults to dot ".", separator to use as the decimal separator, example $123.55 or $123,55 + decimalSeparator: ',', // can be any of '.' | ',' + + // Defaults to false, option to display negative numbers wrapped in parentheses, example: -$12.50 becomes ($12.50) + displayNegativeNumberWithParentheses: true, + + // Defaults to undefined, minimum number of decimals + minDecimal: 2, + + // Defaults to undefined, maximum number of decimals + maxDecimal: 4, + + // Defaults to undefined, add a prefix when using `Formatters.decimal` (only) which can be used for example to display a currency. + // output: โ‚ฌ 123.45' + numberPrefix: 'โ‚ฌ ', + + // Defaults to undefined, add a suffix when using `Formatters.decimal` (only) which can be used for example to display a currency. + // output: '123.45 โ‚ฌ' + numberSuffix: ' โ‚ฌ', + + // Defaults to empty string, thousand separator on a number. Example: 12345678 becomes 12,345,678 + thousandSeparator: '_', // can be any of ',' | '_' | ' ' | '' + }, + } +} +``` + +### PostRender Formatter +SlickGrid also support Post Render Formatter (asynchronously) via the Column property `asyncPostRender` (you will also need to enable in the grid options via `enableAsyncPostRender`). When would you want to use this? It's useful if your formatter is expected to take a while to render, like showing a graph with Sparklines, and you don't want to delay rendering your grid, the Post Render will happen after all the grid is loaded. + +To see it in action, from the 6pac samples, click [here](http://6pac.github.io/SlickGrid/examples/example10-async-post-render.html) +Code example: +```ts +const renderSparklineFormatter: Formatter = (row: number, cell: number, value: any, columnDef: Column, dataContext: any) => { + var vals = [ + dataContext["n1"], + dataContext["n2"], + dataContext["n3"], + dataContext["n4"], + dataContext["n5"] + ]; + $(cellNode).empty().sparkline(vals, {width: "100%"}); +} + +function defineGrid() { + columnDefinitions.value = [ + { id: 'title', name: 'Title', field: 'title' }, + { id: "chart", name: "Chart", sortable: false, width: 60, + formatter: waitingFormatter, + rerenderOnResize: true, + asyncPostRender: renderSparklineFormatter + } + ]; + } +``` + +A **Better Solution** is to use Custom Formatters **as much as possible** because using an Vue Components with `asyncPostRender` are **SLOW** (you are warned). They are slow because they require a full cycle, cannot be cached and are rendered **after** each rows are rendered (because of their asynchronous nature), while Custom Formatters are rendered at the same time as the row itself since they are synchronous in nature. + +## UI Sample +![Default Slickgrid Example](https://github.com/ghiscoding/slickgrid-vue/blob/master/screenshots/formatters.png) diff --git a/frameworks/slickgrid-vue/docs/column-functionalities/sorting.md b/frameworks/slickgrid-vue/docs/column-functionalities/sorting.md new file mode 100644 index 000000000..175822495 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/column-functionalities/sorting.md @@ -0,0 +1,214 @@ +#### Index +- [Usage](#usage) +- [Sorting Complex Objects](#how-to-sort-complex-objects) +- [Custom Sort Comparer](#custom-sort-comparer) +- [Update Sorting Dynamically](#update-sorting-dynamically) +- [Dynamic Query Field](#dynamic-query-field) +- [Pre-Parse Date Columns for better perf](#pre-parse-date-columns-for-better-perf) + +### Demo +[Demo Page](https://ghiscoding.github.io/slickgrid-vue/#/slickgrid/Example4) / [Demo ViewModel](https://github.com/ghiscoding/slickgrid-vue/blob/master/src/examples/slickgrid/Example4.tsx) + +### Description +Sorting on the client side is really easy, you simply need to enable `sortable` (when not provided, it is considered as disabled) on each columns you want to sort and it will sort as a type string. Oh but wait, sorting as string might not always be ideal, what if we want to sort by number or by date? The answer is to simply pass a `type` as shown below. + +### Usage +To use any of them, you can use the `FieldType` interface or enter a type via a string as shown below. Also please note that `FieldType.string` is the default and you don't necessarily need to define it, though you could if you wish to see it in your column definition. + +```vue + +``` + +### How to Sort Complex Objects? +You can sort complex objects using the dot (.) notation inside the `field` property defined in your Columns Definition. + +For example, let say that we have this dataset + +```ts +dataset.value = [ + { item: 'HP Desktop', buyer: { id: 1234, address: { street: '123 Belleville', zip: 123456 }} }, + { item: 'Lenovo Mouse', buyer: { id: 456, address: { street: '456 Hollywood blvd', zip: 789123 }} } +]; +``` + +We can now filter the zip code from the buyer's address using this filter: +```ts +columnDefinitions.value = [ + { + // the zip is a property of a complex object which is under the "buyer" property + // it will use the "field" property to explode (from "." notation) and find the child value + id: 'zip', name: 'Zip Code', field: 'buyer.address.zip', sortable: true + } + // { id: 'street', ... }, +]; +``` + +### Custom Sort Comparer +If the builtin sort comparer methods are not sufficient for your use case, you could add your own custom Sort Comparer in your Column Definitions as shown below. Note that we are only showing a simple numeric sort, just adjust it to your needs. + +```ts +columnDefinitions.value = [{ + id: 'myField', name: 'My Field', + sorter: (a, b) => a > b ? 1 : -1, +}]; +``` + +similarly with a complex object + +```ts +// data = { user: { firstName: 'John', lastName: 'Doe', fullName: 'John Doe' }, address: { zip: 123456 } }}; + +columnDefinitions.value = [{ + id: 'firstName', name: 'First Name', field: 'user.firstName', + sorter: (a, b) => a.fullName > b.fullName ? 1 : -1, +}]; +``` + +### Update Sorting Dynamically +You can update/change the Sorting dynamically (on the fly) via the `updateSorting` method from the `SortService`. Note that calling this method will override all sorting (sorters) and replace them with the new array of sorters provided. For example, you could update the sorting from a button click or a select dropdown list with predefined filter set. + +##### Component +```vue + + + +``` + +#### Extra Arguments +The `updateSorting` method has 2 extra arguments: +- 2nd argument, defaults to true, is to emit a sort changed event (the GridStateService uses this event) + - optional and defaults to true `updateSorting([], true)` +- 3rd argument is to trigger a backend query (when using a Backend Service like OData/GraphQL), this could be useful when using updateFilters & updateSorting and you wish to only send the backend query once. + - optional and defaults to true `updateSorting([], true, true)` + +### Dynamic Query Field +What if you a field that you only know which field to query only at run time and depending on the item object (`dataContext`)? +We can defined a `queryFieldNameGetterFn` callback that will be executed on each row when Filtering and/or Sorting. + +```ts +queryFieldNameGetterFn: (dataContext) => { + // do your logic and return the field name will be queried + // for example let say that we query "profitRatio" when we have a profit else we query "lossRatio" + return dataContext.profit > 0 ? 'profitRatio' : 'lossRatio'; +}, +``` + +### Pre-Parse Date Columns for better perf +##### requires v5.8.0 and higher + +Sorting very large dataset with dates can be extremely slow when dates formated date strings, the reason is because these strings need to first be parsed and converted to real JS Dates before the Sorting process can actually happen (i.e. US Date Format). However parsing a large dataset can be slow **and** to make it worst, a Sort will revisit the same items over and over which mean that the same date strings will have to be reparsed over and over (for example while trying to Sort a dataset of 100 items, I saw some items being revisit 10 times and I can only imagine that it is exponentially worst with a large dataset). + +So what can we do to make this faster with a more reasonable time? Well, we can simply pre-parse all date strings once and only once and convert them to JS Date objects. Then once we get Date objects, we'll simply read the UNIX timestamp which is what we need to Sort. The first pre-parse takes a bit of time and will be executed only on the first date column Sort (any sort afterward will read the pre-parsed Date objects). + +What perf do we get with pre-parsing versus regular non-parsing? The benchmark was pulled using 50K items with 2 date columns (with US date format) +- without non-parsing: ~15sec +- with pre-parsing: ~1.4sec (1st pre-parse) and any subsequent Date sort is about ~0.2sec => so about ~1.5sec total + +The summary, is that we get a 10x boost **but** not only that, we also get an extremely fast subsequent sort afterward (sorting Date objects is as fast as sorting Numbers). + +#### Usage + +You can use the `preParseDateColumns` grid option, it can be either set as either `boolean` or a `string` but there's big distinction between the 2 approaches (both approaches will mutate the dataset). +1. `string` (i.e. set to `"__"`, it will parse a `"start"` date string and assign it as a `Date` object to a new `"__start"` prop) +2. `boolean` (i.e. parse `"start"` date string and reassign it as a `Date` object on the same `"start"` prop) + +> **Note** this option **does not work** with Backend Services because it simply has no effect. + +For example if our dataset has 2 columns named "start" and "finish", then pre-parse the dataset, + +with the 1nd approach (`string`), let's use `"__"` (which is in reality a prefix) it will mutate the dataset by adding new props (where `Date` is a `Date` object) + +```diff +data = [ +- { id: 0, start: '02/28/24', finish: '03/02/24' }, +- { id: 1, start: '01/14/24', finish: '02/13/24' }, ++ { id: 0, start: '02/28/24', finish: '03/02/24', __start: Date, __finish: Date }, ++ { id: 1, start: '01/14/24', finish: '02/13/24', __start: Date, __finish: Date }, +] +``` + +with the 2nd approach (`boolean`), it will instead mutate the dataset by overwriting the same properties + +```diff +data = [ +- { id: 0, start: '02/28/24', finish: '03/02/24' }, +- { id: 1, start: '01/14/24', finish: '02/13/24' }, ++ { id: 0, start: Date, finish: Date }, ++ { id: 1, start: Date, finish: Date }, +] +``` + +Which approach to choose? Both have pros and cons, overwriting the same props might cause problems with the column `type` that you use, you will have to give it a try yoursel. On the other hand, with the other approach, it will duplicate all date properties and take a bit more memory usage and when changing cells we'll need to make sure to keep these props in sync, however you will likely have less `type` issues. + +What happens when we do any cell changes (for our use case, it would be Create/Update), for any Editors we simply subscribe to the `onCellChange` change event and we re-parse the date strings when detected. We also subscribe to certain CRUD functions as long as they come from the `GridService` then all is fine... However, if you use the DataView functions directly then we have no way of knowing when to parse because these functions from the DataView don't have any events. Lastly, if we overwrite the entire dataset, we will also detect this (via an internal flag) and the next time you sort a date then the pre-parse kicks in again. + +#### Can I call the pre-parse myself? + +Yes, if for example you want to pre-parse right after the grid is loaded, you could call the pre-parse yourself for either all items or a single item +- all item pre-parsing: `sgb.sortService.preParseAllDateItems();` + - the items will be read directly from the DataView +- a single item parsing: `sgb.sortService.preParseSingleDateItem(item);` diff --git a/frameworks/slickgrid-vue/docs/developer-guides/csp-compliance.md b/frameworks/slickgrid-vue/docs/developer-guides/csp-compliance.md new file mode 100644 index 000000000..6ab2ba5c4 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/developer-guides/csp-compliance.md @@ -0,0 +1,73 @@ +## CSP Compliance +The library is for the most part CSP (Content Security Policy) compliant since `v4.0` **but** only if you configure the `sanitizer` grid option. We were previously using `DOMPurify` internally in the project (in version <=4.x) but it was made optional in version 5 and higher. The main reason to make it optional was because some users who require SSR support could not use `dompuriy` but would rather need to use `isomorphic-dompurify`. You could also skip the `sanitizer` configuration, but that is not recommended. + +> **Note** even if the `sanitizer` is optional, we **strongly suggest** that you configure it as a global grid option to avoid possible XSS attacks from your data and also to be CSP compliant. Note that for Salesforce users, you do not have to configure it since Salesforce is already using DOMPurify internally for any HTML templates. + +As mentioned above, the project is mostly CSP compliant, however there are some exceptions to be aware of. When using any html string as template (for example with Custom Formatter returning an html string), you will not be fully compliant unless you return `TrustedHTML`. You can achieve this by using the `sanitizer` method in combo with [DOMPurify](https://github.com/cure53/DOMPurify) to return `TrustedHTML` as shown below and with that in place you should be CSP compliant. + +```ts +// prefer the global grid options if possible +this.gridOptions = { + sanitizer: (dirtyHtml) => DOMPurify.sanitize(dirtyHtml, { ADD_ATTR: ['level'], RETURN_TRUSTED_TYPE: true }) +}; +``` + +> **Note** If you're wondering about the `ADD_ATTR: ['level']`, well the "level" is a custom attribute used by SlickGrid Grouping/Draggable Grouping to track the grouping level depth and it must be kept. + +> **Note** the DataView is not CSP safe by default, it is opt-in via the `useCSPSafeFilter` option. + +```typescript +import DOMPurify from 'dompurify'; +import { Slicker, SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle'; + +function defineGrid() { + // DOM Purify is already configured in Slickgrid-Universal with the configuration shown below + gridOptions.value = { + sanitizer: (html) => DOMPurify.sanitize(html, { RETURN_TRUSTED_TYPE: true }), + // you could also optionally use the sanitizerOptions instead + // sanitizerOptions: { RETURN_TRUSTED_TYPE: true } + } +} +``` + +with this code in place, we can use the following CSP meta tag (which is what we use in the lib demo, ref: [index.html](https://github.com/ghiscoding/slickgrid-universal/blob/master/examples/vite-demo-vanilla-bundle/index.html#L8-L14)) + +```html + +``` + +#### DataView +Since we use the DataView, you will also need to enable a new `useCSPSafeFilter` flag to be CSP safe as the name suggest. This option is opt-in because it has a slight performance impact when enabling this option (it shouldn't be noticeable unless you use a very large dataset). + +```typescript + +``` + +### Custom Formatter using native HTML +We now also allow passing native HTML Element as a Custom Formatter instead of HTML string in order to avoid the use of `innerHTML` and stay CSP safe. We also have a new grid option named `enableHtmlRendering`, which is enabled by default and is allowing the use of `innerHTML` in the library (by Formatters and others), however when disabled it will totally restrict the use of `innerHTML` which will help to stay CSP safe. + +You can take a look at the original SlickGrid library with this new [Filtered DataView with HTML Formatter - CSP Header (Content Security Policy)](https://6pac.github.io/SlickGrid/examples/example4-model-html-formatters.html) example which uses this new approach. There was no new Example created in Slickgrid-Universal specifically for this but the approach is the same. diff --git a/frameworks/slickgrid-vue/docs/events/available-events.md b/frameworks/slickgrid-vue/docs/events/available-events.md new file mode 100644 index 000000000..d590a4b76 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/events/available-events.md @@ -0,0 +1,229 @@ +## Full list of Events by Categories + +(see below for full list of events) + +All the events are published with a data payload in a `CustomEvent`, so you will typically find the payload under the `detail` property of the `CustomEvent`. However please note that the events from `SlickGrid` and `SlickDataView`, shown at the bottom of the list, are published with a different structure which is also including the JS event that it was triggered with under the property `eventData` and the payload itself is under the `args` property (which follows original SlickGrid structure). To subscribe to all events, you can use your PubSub instance (if available) or add listeners on your grid container DOM element. + +#### `SlickGrid` and `SlickDataView` +```vue + + + +``` + +#### all other events +```vue + + + +``` + +--- + +#### CellExternalCopyManager (extension) + - `onCopyCells` + - `onCopyCancelled` + - `onPasteCells` + - `onBeforePasteCell` + +#### Context Menu / Cell Menu (extension) + - `onContextMenuClearGrouping` + - `onContextMenuCollapseAllGroups` + - `onContextMenuExpandAllGroups` + - **Slick Events** + - `onAfterMenuShow` + - `onBeforeMenuShow` + - `onBeforeMenuClose` + - `onCommand` + - `onOptionSelected` + +#### Column Picker (extension) + - `onColumnPickerColumnsChanged` + - **Slick Events** + - `onColumnsChanged` + +#### Grid Menu (extension) + - `onGridMenuMenuClose` + - `onGridMenuBeforeMenuShow` + - `onGridMenuAfterMenuShow` + - `onGridMenuClearAllPinning` + - `onGridMenuClearAllFilters` + - `onGridMenuClearAllSorting` + - `onGridMenuColumnsChanged` + - `onGridMenuCommand` + - **Slick Events** + - `onAfterMenuShow` + - `onBeforeMenuShow` + - `onBeforeMenuClose` + - `onColumnsChanged` + - `onMenuClose` + - `onCommand` + +#### Header Buttons (extension) + - `onHeaderButtonCommand` + +#### Header Menu (extension) + - `onHeaderMenuHideColumns` + - `onHeaderMenuCommand` + - `onHeaderMenuColumnResizeByContent` + - `onHeaderMenuBeforeMenuShow` + - `onHeaderMenuAfterMenuShow` + +#### Filter Service + - `onBeforeFilterClear` + - `onBeforeSearchChange` + - `onFilterCleared` + +#### Grid Service + - `onHeaderMenuHideColumns` + - `onItemAdded` + - `onItemDeleted` + - `onItemUpdated` + - `onItemUpserted` + +#### GridState Service + - `onFullResizeByContentRequested` + - `onGridStateChanged` + +#### Pagination Service + - `onBeforePaginationChange` + - `onPaginationChanged` + - `onPaginationRefreshed` + - `onPaginationPresetsInitialized` + - `onPaginationVisibilityChanged` + - `onPaginationSetCursorBased` (for GraphQL only) + +#### Resizer Service + - `onGridBeforeResize` + - `onGridAfterResize` + - `onBeforeResizeByContent` + - `onAfterResizeByContent` + +#### Sort Service + - `onSortCleared` + - `onSortChanged` + - `onBeforeSortChange` + +#### TreeData Service + - `onTreeFullToggleStart` + - `onTreeFullToggleEnd` + - `onTreeItemToggled` + +#### Slickgrid-Vue Component + - `onBeforeGridDestroy` + - `onAfterGridDestroyed` + - `onBeforeGridCreate` + - `onDataviewCreated` + - `onGridCreated` + - `onvueGridCreated` + - `onGridStateChanged` + +#### SlickGrid + - `onActiveCellChanged` + - `onActiveCellPositionChanged` + - `onAddNewRow` + - `onAfterSetColumns` + - `onAutosizeColumns` + - `onBeforeAppendCell` + - `onBeforeCellEditorDestroy` + - `onBeforeColumnsResize` + - `onBeforeDestroy` + - `onBeforeEditCell` + - `onBeforeFooterRowCellDestroy` + - `onBeforeHeaderCellDestroy` + - `onBeforeHeaderRowCellDestroy` + - `onBeforeSetColumns` + - `onBeforeSort` + - `onBeforeUpdateColumns` + - `onCellChange` + - `onCellCssStylesChanged` + - `onClick` + - `onColumnsReordered` + - `onColumnsDrag` + - `onColumnsResized` + - `onColumnsResizeDblClick` + - `onCompositeEditorChange` + - `onContextMenu` + - `onDblClick` + - `onDrag` + - `onDragInit` + - `onDragStart` + - `onDragEnd` + - `onFooterClick` + - `onFooterContextMenu` + - `onFooterRowCellRendered` + - `onHeaderCellRendered` + - `onHeaderClick` + - `onHeaderContextMenu` + - `onHeaderMouseEnter` + - `onHeaderMouseLeave` + - `onHeaderMouseOver` + - `onHeaderMouseOut` + - `onHeaderRowCellRendered` + - `onHeaderRowMouseEnter` + - `onHeaderRowMouseLeave` + - `onHeaderRowMouseOver` + - `onHeaderRowMouseOut` + - `onKeyDown` + - `onMouseEnter` + - `onMouseLeave` + - `onPreHeaderClick` + - `onPreHeaderContextMenu` + - `onRendered` + - `onScroll` + - `onSelectedRowsChanged` + - `onSetOptions` + - `onActivateChangedOptions` + - `onSort` + - `onValidationError` + - `onViewportChanged` + +#### SlickDataView + - `onBeforePagingInfoChanged` + - `onGroupExpanded` + - `onGroupCollapsed` + - `onPagingInfoChanged` + - `onRowCountChanged` + - `onRowsChanged` + - `onRowsOrCountChanged` + - `onSelectedRowIdsChanged` + - `onSetItemsCalled` diff --git a/frameworks/slickgrid-vue/docs/events/grid-dataview-events.md b/frameworks/slickgrid-vue/docs/events/grid-dataview-events.md new file mode 100644 index 000000000..ebee7dc36 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/events/grid-dataview-events.md @@ -0,0 +1,143 @@ +SlickGrid has a nice amount of events, see the full list of [Available Events](Available-Events.md), which you can use by simply hook a `subscribe` to them (the `subscribe` are a custom `SlickGrid Event`). There are 2 options to get access to all these events (For the first 2 you will have to get access to the `Grid` and the `DataView` objects which are exposed in `Slickgrid-Vue`): + +**From the list below, the number 1. is by far the easiest and preferred way** + +### Example event in the rendered template + +##### Component +Hook yourself to the Changed event of the bindable grid object. + +```vue + + + +``` + +#### How to use Grid/Dataview Events +Once the `Grid` and `DataView` are ready, see all [Available Events](../events/available-events.md). See below for the `gridChanged(grid)` and `dataviewChanged(dataview)` functions. +- The `GridExtraUtils` is to bring easy access to common functionality like getting a `column` from it's `row` and `cell` index. +- The example shown below is subscribing to `onClick` and ask the user to confirm a delete, then will delete it from the `DataView`. +- Technically, the `Grid` and `DataView` are created at the same time by `slickgrid-vue`, so it's ok to call the `dataViewObj` within some code of the `gridObjChanged()` function since `DataView` object will already be available at that time. + +**Note** The example below is demonstrated with `bind` with event `Changed` hook on the `grid` and `dataview` objects. However you can also use the `EventAggregator` as shown earlier. It's really up to you to choose the way you want to call these objects. + +##### Component +```vue + + + +``` diff --git a/frameworks/slickgrid-vue/docs/getting-started/quick-start.md b/frameworks/slickgrid-vue/docs/getting-started/quick-start.md new file mode 100644 index 000000000..1fef0d1f2 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/getting-started/quick-start.md @@ -0,0 +1,179 @@ +# Quick start + +> **NOTE** The Documentations shown on this website are meant for Slickgrid-Vue v4.x and higher, for older versions please refer to the project [Wikis](https://github.com/ghiscoding/slickgrid-vue/wiki) for earlier versions of the project. + +### Easiest Way to Get Started + +The easiest is to simply clone the [Slickgrid-Vue-Demos](https://github.com/ghiscoding/slickgrid-vue-demos) project and run it from there... or if you really wish to start from scratch then follow the steps below. + +### 1. Install NPM Package + +Install `Vue`, `Slickgrid-Vue`, `Bootstrap` (or other UI framework) +```bash +npm install --save slickgrid-vue bootstrap +# or with yarn add +``` + +_Note: `Bootstrap` is optional, you can use any other framework_ + +### 2. Import all necessary dependencies in `main.ts` +```vue + +``` + + +### 3. CSS / SASS Styles +Load the default Bootstrap theme style and/or customize it to your taste (customization requires SASS) + +#### CSS +Default compiled `css`. + +**Note:** If you are also using `Bootstrap-SASS`, then there is no need to include the `bootstrap.css` in the `styles: []` section. + +```vue + +``` + +#### SASS (scss) +You could also compile the SASS files with your own customization, for that simply take any of the [_variables.scss](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/styles/_variables.scss) (without the `!default` flag) variable file and make sure to import the Bootstrap Theme afterward. For example, you could modify your `style.scss` with the following changes: + +```scss +/* for example, let's change the mouse hover color */ +@use '@slickgrid-universal/common/dist/styles/sass/slickgrid-theme-bootstrap.scss' with ( + $cell-odd-background-color: lightyellow, + $row-mouse-hover-color: lightgreen +); +``` + +### 4. Install/Setup `I18N` for Localization (optional) +To provide locales other than English (default locale), you have 2 options that you can go with. If you only use English, there is nothing to do (you can still change some of the texts in the grid via option 1.) +1. Using [Custom Locale](../localization/localization-with-custom-locales.md), that is when you use **only 1** locale (other thank English)... this is a new feature starting from version `2.10.0` and up. +2. Using [Localization with I18N](../localization/localization.md), that is when you want to use multiple locales dynamically. + +### 5. Create a basic grid + +```vue + + + +``` + +### 6. Explore the Wiki page content +The last step is really to explore all the pages that are available in this Wiki, all the documentation will be place in here and so you should visit it often. For example a good starter is to look at the following + +- for all the `Grid Options`, take a look at [Wiki - Grid Options](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/interfaces/gridOption.interface.ts) +- [Formatters](../column-functionalities/formatters.md) +- [Editors](../column-functionalities/editors.md) +- [Filters](../column-functionalities/filters/select-filter.md) +- [Grid Menu](../grid-functionalities/grid-menu.md) +- ... and much more, just explorer the Wikis through the sidebar index (on your right) + - it gets updated very frequently, we usually mention any new/updated wikis in any new version release + +### 7. Get Started +The best way to get started is to clone the [Slickgrid-Vue-Demos](https://github.com/ghiscoding/slickgrid-vue-demos), it has multiple examples and it is also updated frequently since it is used for the GitHub Bootstrap 4 demo page. `Slickgrid-Vue` has 2 `Bootstrap` themes, you can see a demo of each one below. +- [Bootstrap 5 demo](https://ghiscoding.github.io/slickgrid-vue) / [examples repo](https://github.com/ghiscoding/slickgrid-vue-demos) (with `I18N` Service) + +##### All Live Demo Examples have links to the actual code +Like to see the code to a particular Example? Just click on the "see code" that is available in every live examples. + +### 8. CSP Compliance +The project supports Content Security Policy (CSP) as long as you provide an optional `sanitizer` in your grid options (we recommend DOMPurify). Review the [CSP Compliance](../developer-guides/csp-compliance.md) documentation for more info. + +### 9. Add Optional Feature like Excel Export +Starting with version 3.0.0, the Excel Export is now an optional package and if you want to use it then you'll need to install it via npm from the monorepo library with `npm install @slickgrid-universal/excel-export`. Refer to the [Excel Export - Docs](../grid-functionalities/export-to-excel.md) for more info. + +Here's a quick list of some of these optional packages +- [@slickgrid-universal/excel-export](https://github.com/ghiscoding/slickgrid-universal/tree/master/packages/excel-export) +- [@slickgrid-universal/text-export](https://github.com/ghiscoding/slickgrid-universal/tree/master/packages/text-export) +- [@slickgrid-universal/graphql](https://github.com/ghiscoding/slickgrid-universal/tree/master/packages/graphql) +- [@slickgrid-universal/odata](https://github.com/ghiscoding/slickgrid-universal/tree/master/packages/odata) + +### 10. Missing Features? (fear not) +What if `Slickgrid-Vue` is missing feature(s) versus the original `SlickGrid`? Fear not and directly use the `SlickGrid` and `DataView` objects that are expose from the start through Event Emitters. For more info continue reading on [Wiki - SlickGrid & DataView objects](../slick-grid-dataview-objects/slickgrid-dataview-objects.md) and [Wiki - Grid & DataView Events](../events/grid-dataview-events.md) + +### 11. Having some issues? +After reading all this HOW TO, what if you have an issue with the grid? +Please start by searching any related [issues](/ghiscoding/slickgrid-vue/issues). If you can't find anything in the issues log and you made sure to also look through the multiple [wiki](/ghiscoding/slickgrid-vue/wiki) pages as well, then go ahead and fill in a [new issue](/ghiscoding/slickgrid-vue/issues/new) and we'll try to help. + +### Final word +This project is Open Source and is, for the most part, mainly done in spare time. So please be respectful when creating issues (and fill in the issue template) and I will try to help you out. If you like my work, you can also [buy me a coffee](https://ko-fi.com/N4N679OT) โ˜•๏ธ, some part of the code happens when I'm at StarBucks... That is it, thank you and don't forget to โญ it if you like the lib ๐Ÿ˜‰ diff --git a/frameworks/slickgrid-vue/docs/grid-functionalities/Row-based-edit.md b/frameworks/slickgrid-vue/docs/grid-functionalities/Row-based-edit.md new file mode 100644 index 000000000..4d16aaca7 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/grid-functionalities/Row-based-edit.md @@ -0,0 +1,71 @@ +#### index +- [The action column](#the-action-column) +- [Multiple Row Selections](#multiple-row-selections) +- [Change Dynamically Single/Multiple Selections](#changing-dynamically-from-single-to-multiple-selections-and-vice-versa) +- [Mixing Single & Multiple Row Selections](#mixing-single--multiple-row-selections) +- [Disable Custom Rows Selections via `selectableOverride`](#disable-custom-rows-selections-via-selectableoverride) +- [Disable External Button when having Empty Selection](#disable-external-button-when-having-empty-selection) +- [Change Row Selections](#change-row-selections) +- Troubleshooting + - [Adding a Column dynamically is removing the Row Selection, why is that?](#adding-a-column-dynamically-is-removing-the-row-selection-why-is-that) + +### Description +The Row based editing plugin makes it possible to keep the grid readonly except for rows which the user explicitely toggles into edit mode. + +**Note:** This plugin enforces the use of the `autoEdit` option and will turn it on with a console warning if its not already. + +### Demo +[Demo](https://ghiscoding.github.io/slickgrid-vue/#/slickgrid/Example35) / [Demo Component](https://github.com/ghiscoding/slickgrid-vue/blob/master/src/examples/slickgrid/Example35.tsx) + +## The action column +A new column is rendered that shows an edit/delete button. If the user clicks on edit, a save and cancel button are shown instead and the row toggles into edit mode. By default as the last column but you can override it with the option `columnIndexPosition`. Additionally it's default column id can be overriden using the opiton `columnId`. Furthermore, you can also override the columns label via the `actionsColumnLabel` property. + +### Single or multiple editable rows +By default you can only toggle a single row into edit mode. If you set the option `allowMultipleRows` to `true` on the other hand, you can toggle as many as you want. + +### Configuring the action buttons +You can override the styling, the hover text as well as whether a prompt โ€” and with what text โ€” should be shown. It is done by overriding the `actionButtons` property of the [plugins options](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/interfaces/rowBasedEditOption.interface.ts). + +## Support for the Excel Copy Buffer Plugin +If the [Excel Copy Buffer Plugin](excel-copy-buffer.md) is configured, the Row based editing pluging will override it's behavior by denying pastes on all cells not within a edit mode row. Nevertheless, any existing `BeforePasteCellHandler` will be respected. + +## How the plugin works +The idea of the plugin is to focus the users editing experience on specific individual rows and and save them individually. This is achieved by letting the user toggle one or more rows into edit mode. +When a that happens a potentially registered `onBeforeEditMode` callback is executed to handle various preparation or cleanup tasks. Now changes can be made to those rows and will be highlighted and tracked. The user may cancel the edit mode at any time and revert all cells changes. If the save button is pressed on the other hand an `onBeforeRowUpdated` hook, which you define via plugin options, is called and expects a `Promise`. In that method you'd typically write the changes to your backend and return either true or false based on the operations outcome. If a negative boolean is returned the edit mode is kept, otherwise the row applies the changes and toggles back into readonly mode. That means, no modifications can be done on the grid. + +Here's the respective code shown in Example22: + +#### ViewModel +```ts +function onBeforeRowUpdated(args) { + const { effortDriven, percentComplete, finish, start, duration, title } = args.dataContext; + + if (duration > 40) { + alert('Sorry, 40 is the maximum allowed duration.'); + return Promise.resolve(false); + } + + return fakeFetch('your-backend-api/endpoint', { + method: 'POST', + body: JSON.stringify({ effortDriven, percentComplete, finish, start, duration, title }), + headers: { + 'Content-type': 'application/json; charset=UTF-8' + } + }).catch(err => { + console.error(err); + return false; + }) + .then(response => { + if (response === false) { // <---- the negative response, e.g validation failed, keep the row as is + return false; + } + if (typeof response === 'object') { + return response!.json(); + } + }) + .then(json => { + alert(json.message); + return true; // <--- all good, apply changes in grid and toggle row into readonly mode + }); +}, +``` diff --git a/frameworks/slickgrid-vue/docs/grid-functionalities/add-update-highlight.md b/frameworks/slickgrid-vue/docs/grid-functionalities/add-update-highlight.md new file mode 100644 index 000000000..69a847496 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/grid-functionalities/add-update-highlight.md @@ -0,0 +1,270 @@ +#### index +- CRUD Methods + - [Add Item](#add-an-item-row) + - [Delete Item](#delete-an-item-row) + - [Update Item](#update-an-item-row) + - [Upsert Item](#upsert-an-item-row) + - [Default Option Flags](#crud-default-option-flags) +- [Highlight a Row](#highlight-a-row-item) + +### Description +When working with the grid, you might want to Add / Update or Hightlight an item row from the Datagrid. + +**Note:** This is strictly a client side event, you still have to implement any backend change yourself. + +### Demo +[Demo Page](https://ghiscoding.github.io/slickgrid-vue/slickgrid/Example11) / [Demo Component](https://github.com/ghiscoding/slslickgrid-vueb/master/src/examples/slickgrid/Example11.tsx) + +## CRUD Methods +### Add an Item (row) +Please note that you need to provide the `id` by yourself and remember that it has to be **unique**, else the `Slickgrid DataView` will throw you an error in the console. + +##### Component +```vue + + + +``` + +#### Add Item Position (top/bottom) + +When adding an item, you can add it on top (default) of the grid or at the bottom of the grid. In order to change that, you can use the `position` property. + +```ts +// add the item to the end of grid +vueGrid.gridService.addItem(newItem, { position: 'bottom' }); +``` + +#### Change default flags +When adding an item, you have access to change any of the default flags through the second argument of `addItem` method. + +```ts +// add the item to the end of grid +vueGrid.gridService.addItem(newItem, { + // the defaults are shown below + highlightRow: true, // do we want to highlight the row after the insert + position: 'top', // which position of the grid to add the item + resortGrid: false, // do we want to resort the grid after the insert + selectRow: false, // do we want to select the row after the insert + triggerEvent: true // do we want to trigger an event after the insert +}); +``` + +### Delete an Item (row) +To delete a row, you can use `deleteItem(s)` and the pass the entire object(s) or use `deleteItemById(s) in which you need to provide the object `id` to delete and the method will find it in the grid and remove it from the grid. Also please note that it's only deleting from the grid (by removing it from the DataView), meaning that it won't remove it from your database. + +##### Component +```vue + +``` + +#### Change default flags +When adding an item, you have access to change any of the default flags through the second argument of `addItem` method. + +```ts +// add the item to the end of grid +vueGrid.gridService.deleteItemById(123, { + // the defaults are shown below + triggerEvent: true // do we want to trigger an event after the insert +}); +``` + +### Update an Item (row) +To update an item, you can use `updateItem(s)` and the pass the entire object(s) in this case it does not require you to know the row number, it will try to find the row by itself (it uses the "id" for that) and update the item. The other way would be to use `updateItemById` in which you need to provide the object `id` to update the item. + +##### Component + +```vue + +``` + +#### Change default flags +When adding an item, you have access to change any of the default flags through the second argument of `addItem` method. + +```ts +// add the item to the end of grid +vueGrid.gridService.updateItem(newItem, { + // the defaults are shown below + highlightRow: true, // do we want to highlight the row after the update + selectRow: false, // do we want to select the row after the update + scrollRowIntoView: false, // do we want to scroll the row into the viewport after the update + triggerEvent: true // do we want to trigger an event after the update +}); +``` + +### Upsert an Item (row) +Upsert will do an Insert when not found or update if it found the item already exist in the grid. + +##### Component + +```vue + +``` + +### CRUD Default Option Flags +All the CRUD methods have default option flags that can be changed which will do certain actions. The default option flags are the following for each of the CRUD method (`upsert` will use the flags of the insert or the update depending on which method it calls). +```ts +const GridServiceDeleteOptionDefaults: GridServiceDeleteOption = { + triggerEvent: true // do we want to trigger an event after the insert +}; + +const GridServiceInsertOptionDefaults: GridServiceInsertOption = { + highlightRow: true, // do we want to highlight the row after the insert + position: 'top', // which position of the grid to add the item + resortGrid: false, // do we want to resort the grid after the insert + selectRow: false, // do we want to select the row after the insert + triggerEvent: true // do we want to trigger an event after the insert +}; + +const GridServiceUpdateOptionDefaults: GridServiceUpdateOption = { + highlightRow: true, // do we want to highlight the row after the update + selectRow: false, // do we want to select the row after the update + scrollRowIntoView: false, // do we want to scroll the row into the viewport after the update + triggerEvent: true // do we want to trigger an event after the update +}; +``` + +## Highlight a row item +Highlighting a row is customizable with SASS, you can change the highlighted color and/or animation by changing the [SASS variables](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/styles/_variables.scss) `$row-highlight-background-color` and/or `$row-highlight-fade-animation` +Take a look at all the available [SASS variables](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/styles/_variables.scss). + +##### Component +```vue + +``` diff --git a/frameworks/slickgrid-vue/docs/grid-functionalities/column-picker.md b/frameworks/slickgrid-vue/docs/grid-functionalities/column-picker.md new file mode 100644 index 000000000..eeac92a13 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/grid-functionalities/column-picker.md @@ -0,0 +1,22 @@ +## Column Picker +Enable by default and provides the list of available fields by simply doing a `right+click` over any column header, you can then hide/show the column(s) you want. + +#### Grid Options +To enable/disable the Column Picker, simply call the `enableColumnPicker` flag in the Grid Options (enabled by default). + +```ts +gridOptions.value = { + enableColumnPicker: true, + + // you can also enable/disable options and also use event for it + columnPicker: { + hideForceFitButton: true, + hideSyncResizeButton: true, + onColumnsChanged: (e, args) => { + console.log('Column selection changed from Column Picker, visible columns: ', args.visibleColumns); + } + }, +} +``` +#### UI Sample +![image](https://user-images.githubusercontent.com/643976/71301681-6cfc3a00-2370-11ea-9c84-be880f345bcd.png) \ No newline at end of file diff --git a/frameworks/slickgrid-vue/docs/grid-functionalities/composite-editor-modal.md b/frameworks/slickgrid-vue/docs/grid-functionalities/composite-editor-modal.md new file mode 100644 index 000000000..6c6f32a41 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/grid-functionalities/composite-editor-modal.md @@ -0,0 +1,675 @@ +##### index +- Composite Editor Modal Window Actions/Types + - [Create Item](#create-new-item) + - [Edit Item](#edit-item) + - [Clone Item](#clone-item) + - [Mass Update](#mass-update) + - [Mass Selection](#mass-selection) _(similar to Mass Update but apply changes only to selected rows)_ +- Modal Options + - [Customize Text Labels](#customize-text-labels) + - [UI Options & Responsive Design](#ui-options--responsive-design) +- Callback Functions + - [onBeforeOpen](#onBeforeOpen) - allows the user to optionally execute something before opening the modal + - [onClose](#onclose) - allows to warn/confirm with the user when leaving the form with unsaved data + - [onError](#onerror) - allows you to customize what to do when the modal throws an error (show an alert or toast notification) + - [onSave](#onsave) - typically used with a Backend Service API +- [How to Skip a Mass Change](#how-to-skip-a-mass-change) +- Dynamic Events/Methods + - [Dynamically Change a Form Input](#dynamically-change-form-input) - for example when 1 input value affect 1 or more other input(s). + - [Dynamically Change `editorOptions`](#dynamically-change-editor-options-like-mindate-on-a-date-picker) - for example, date picker `minDate` based on other field values + - [Dynamically Update Select Editor Collection](#dynamically-update-select-editor-collection) +- [Disabling Form Inputs (readonly)](#disabling-form-inputs-readonly) + +### Demo +[Demo](https://ghiscoding.github.io/slickgrid-vue/#/slickgrid/Example30) / [Demo Component](https://github.com/ghiscoding/slickgrid-vue/blob/master/src/examples/slickgrid/Example30.tsx) + +### Description +The Composite Editor Modal allows you to (create, clone, edit, mass update & mass selection changes). I believe the SlickGrid author names it as Composite Editor because it loops through each editor of all the column definitions and displays them in one composed form, hence the name Composite Editors. Also note that each editor are pulled directly from the column definition itself (their column title as well), so for example if you use `Editors.longText` then you will have a text area input associated to that field with the appropriate input label. + +The following 5 modal types (6 if we include the `auto-mass`) are available (via `CompositeEditorModalType` interface): +- `create` - create a new row/item +- `clone` - clone allows to copy & possibly edit some inputs before cloning +- `edit` - edit a row/item +- `mass-update` - apply changes on the entire dataset +- `mass-selection` - apply changes on all the selected row (similar to mass update but only on the selection) +- `auto-mass` - will auto-detect if it should do a Mass Update (no row selected) or a Mass Selection Changes (with rows selected) + - most user find this one approach confusing, our users prefer to have 2 separate buttons (which is a lot less confusing) + +## Create New Item +You can Create a new row/item via the composite editor modal window, it will display all editors as blank input field. Also note that this feature requires the `enableAddRow` grid option to be enabled or else it will throw an error. + +Note: the new item will be added to the top of the grid by default, if you wish to change that you can use `insertOptions`. The options are the same as calling `addItem()` from the Grid Service, so if you wish to add the new item to the bottom then you use this `insertOptions: { position: 'bottom' }` + +##### with TypeScript + +```vue + +``` + +## Clone Item + +You can Clone an existing row/item via the composite editor modal window (basically allows you to copy a row but also to make edits before cloning it). The setup is nearly identical to the Create Item, just make sure to display appropriate modal title. Also note that this feature requires the `enableAddRow` grid option to be enabled or else it will throw an error. + +Refer to the [Create Item](#create-new-item) section for code sample + +Just a side note on the UI, a good way to use this composite editor feature is probably with a [Cell Menu](../column-functionalities/Cell-Menu.md) (aka Action Menu) + +![image](https://user-images.githubusercontent.com/643976/106016610-a03dec00-608d-11eb-80bd-1f6e0a404eb5.png) + + +## Edit Item +You can Edit an existing row/item via the composite editor modal window. The setup is nearly identical to the Create Item, just make sure to display appropriate modal title. + +Refer to the [Create Item](#create-new-item) section for code sample + +Similar to the Clone Item, a good way to use this composite editor feature is probably with a [Cell Menu](../column-functionalities/Cell-Menu.md) (aka Action Menu) + +## Mass Update +Mass Update allows you to apply changes (from the modal form) to the entire dataset, internally it will apply the changes to all the items in the grid via the DataView. However, you could also choose to refresh the grid yourself after calling the backend and if you choose to do that then you'll want to do that via the `onSave` async callback (once backend is done, refresh the grid). + +Note however that there is a subtle difference compare to the Create Item action, you need to specifically tag which column will show up in the Mass Update and you need to do that by adding `massUpdate: true` flag inside the `editor` property of each column definition that you wish to be included in the form. + +`auto-mass` option: If you decide to use Mass Update and Mass Selection and wish to only expose 1 button to do the action and let the system decide if it's doing a Mass Update or a Mass Selection change, you can use the modal type `auto-mass` (if it detect that some rows are selected it will use Mass Selection or else Mass Update). From our experience, user prefer to expose the 2 separate action buttons (less confusion), but this for you to decide, you have the option. + +##### with TypeScript + +```vue + +``` + +## Mass Selection +Similar to the Mass Update but apply changes only on the selected rows. The setup is nearly identical to the Mass Update, just make sure to display appropriate modal title. Also note that you also need to add `massUpdate: true` flag inside the `editor` property of each column definition that you wish to be included in the Mass Selection changes form. + +Refer to the [Mass Update](#mass-update) section for code sample. + +`auto-mass` option: If you decide to use Mass Update and Mass Selection and wish to only expose 1 button to do the action and let the system decide if it's doing a Mass Update or a Mass Selection change, you can use the modal type `auto-mass` (if it detect that some rows are selected it will use Mass Selection or else Mass Update). From our experience, user prefer to expose the 2 separate action buttons (less confusion), but this for you to decide, you have the option. + +## Callback Functions + +### onBeforeOpen +The `onBeforeOpen` callback function allows the user to optionally execute something before opening the modal. This is synchronous call and it won't wait until proceeding to opening the modal, it just allows you to possibly do something before opening the modal (for example cancel any batch edits, or change/reset some validations in column definitions). + +```ts +compositeEditorInstance?.openDetails({ + headerTitle: 'Create Item', + modalType: 'create', + onBeforeOpen: () => rollbackAllUnsavedEdits(), // for example if we have any unsaved editors in the grids, we can roll them back before doing a Mass Update +}); +``` + +### onClose +The `onClose` callback function allows you to show a warning or confirm dialog to the user if there's any form input that were left unsaved. For example, when the user opens the modal window and start changing a few inputs in the form but then decides to the form, this is when the `onClose` gets executed (and to be clear, it only gets executed when there's changes in the form and a close action is clicked, it won't execute when there's no changes). + +You can return a synchronous or asynchronous function (typically the latter), for example we could display an alert when leaving with unsaved data. + +```ts +compositeEditorInstance?.openDetails({ + headerTitle: 'Create Item', + modalType: 'create', + onClose: () => Promise.resolve(confirm('You have unsaved changes, are you sure you want to close this window?')), +}); +``` + +### onError +The `onError` callback function will execute anytime an error is thrown by the modal window. +You can return a synchronous or asynchronous function (typically the latter), for example we could display an alert when leaving with unsaved data. + +```ts +compositeEditorInstance?.openDetails({ + headerTitle: 'Create Item', + modalType: 'create', + onError: (error) => alert(error.message), +}); +``` + +The `onError` error follows the `OnErrorOption` which includes the interface shown below (and if you want to customize the text or use translation, you will want to use the `code`) + +```ts +export type OnErrorOption = { + code?: string; // Error code (typically an uppercase error code key like: "NO_RECORD_FOUND") + message: string; // Error Message + type: 'error' | 'info' | 'warning'; // Error Type (info, error, warning) +}; +``` + +The available error `code` are the following: +- `EDITABLE_GRID_REQUIRED`: with default text of `"Your grid must be editable in order to use the Composite Editor Modal."` +- `ENABLE_ADD_ROW_REQUIRED`: with default text of `"Composite Editor requires the flag "enableAddRow" to be set to True in your Grid Options when cloning/creating a new item."` +- `ENABLE_CELL_NAVIGATION_REQUIRED`: with default text of `"Composite Editor requires the flag "enableCellNavigation" to be set to True in your Grid Options."` +- `ITEM_ALREADY_EXIST`: with default text of `"The item object which you are trying to add already exist with the same Id:: ${newId}"` +- `NO_CHANGES_DETECTED`: with default text of `"Sorry we could not detect any changes."` +- `NO_EDITOR_FOUND`: with default text of `"We could not find any Editor in your Column Definition"` +- `NO_RECORD_FOUND`: with default text of `"No records selected for edit or clone operation."` +- `ROW_NOT_EDITABLE`: with default text of `"Current row is not editable."` +- `ROW_SELECTION_REQUIRED`: with default text of `"You must select some rows before trying to apply new value(s)."` + +### onSave +The `onSave` callback function is optional and is very useful whenever you have a backend API (which I assume is most of the time). This callback will provide you with 3 arguments `(formValues, selection, dataContext)` +1. `formValues`: all the input values changed in the modal window form +2. `selection`: selected rows (commonly used with Mass Update/Mass Selection) +3. `dataContext`: item data context object (commonly used with Create/Clone/Edit) + +**Note:** the `onSave` must return a `boolean` or a `Promise` and if the returned result is `true` it will apply the changes to the data in the grid. However if it returns `false` then it assumes that an error occurred and no changes be applied in the grid. + +You can return a synchronous or asynchronous function (typically the latter), we can take the example below when creating an item + +##### Create Item demo + +```ts +compositeEditorInstance.openDetails({ + headerTitle: 'Create Item', + modalType: 'create', + onSave: (formValues, selection, dataContext) => { + return new Promise(async (resolve, reject) => { + try { + const success = await createUser(dataContext); + resolve(success); + } catch (backendError) { + // when your backend API throws an error, we can reject the promise and that will show as a validation summary on top of the modal + reject(backendError); + } + }); + } +} +``` + +##### Mass Selection changes demo +Note that the `formValues` is an object with a very simple structure, the object properties are the column `id` with their new values. For example if we changed the column id of `percentCompleted` with a value of 100% and we also changed another column id `isCompleted` to `true`, then our `formValues` will be: + +`const formValues = { percentCompleted: 100, isCompleted: true };` + +```ts +compositeEditorInstance.openDetails({ + headerTitle: 'Update Selected Items', + modalType: 'mass-selection', + onSave: (formValues, selection, dataContext) => { + return new Promise(async (resolve, reject) => { + try { + const success = await updateUsers(selection.dataContextIds, formValues); + resolve(success); + } catch (backendError) { + // when your backend API throws an error, we can reject the promise and that will show as a validation summary on top of the modal + reject(backendError); + } + }); + } +} +``` + +#### onSave validation error/rejection +When adding a backend API to the `onSave` you can (and should) wrap your code in a try/catch and use the Promise rejection to send it back to the modal. If the modal finds any errors when saving, it will keep the modal window open and display the error as a validation summary on top of the modal as shown below (as you can see below the backend rejected the save because the value is below 50%) + +![image](https://user-images.githubusercontent.com/643976/106039582-477b4d00-60a7-11eb-88e1-269790a77852.png) + +## How to Skip a Mass Change +### Mass Change (Mass-Update / Mass-Selection) - Skipping according to certain condition(s) +The use case would be to skip a change, in silent without any errors shown, if another column or property has value(s) that do not match our condition expectaation. A possible use case could be found under [Example 12](https://github.com/ghiscoding/slickgrid-universal/blob/eb1d5069e10b8b2cb2f14ac964f2c6e2b8f006a9/examples/webpack-demo-vanilla-bundle/src/examples/example12.ts#L949-L956), the use case that we could do is the following: "Do not apply a mass change on the 'Duration' column that is below 5 days if its 'Complexity' column is set to 'Complex' or 'Very Complex'", the code do this use case is shown below. Also note that the 3rd argument of `onSave` (in our case `dataContextOrUpdatedDatasetPreview`) will have the updated dataset but without the change(s) that got skipped + +```ts +compositeEditorInstance.openDetails({ + headerTitle: 'My Modal', + modalType, + + // you can validate each row item dataContext before applying a Mass Update/Selection changes + // via this validation callback (returning false would skip the change) + validateMassUpdateChange: (fieldName, dataContext, formValues) => { + const levelComplex = complexityLevelList.find(level => level.label === 'Complex'); + if (fieldName === 'duration' && (dataContext.complexity === levelComplex?.value || formValues.complexity === levelComplex?.value) && formValues.duration < 5) { + // below expectation (it's "Complex" and it doesn't have at least 5 days of work (duration)) + return false; + } + return true; //expectation met, apply the mass change + }, + + // you can optionally provide an async callback method when dealing with a backend server + onSave: (formValues, selection, dataContextOrUpdatedDatasetPreview) => { + // simulate a backend server call which returns true (successful) after 30sec + return new Promise(resolve => setTimeout(() => resolve(true), 500)); + } +}); +``` +With that same use case, let say that we try changing the first 4 rows with a "Duration" of 4 days, it will apply the changes to all the rows except the first row where the change is skipped because its complexity is set to "Complex" and we don't want a duration to be below 5 days for our use case. + +![image](https://user-images.githubusercontent.com/643976/171494716-60d32059-c212-4b13-b90d-1342d0999e38.png) + +## Customize Text Labels +You can customize many of the text labels used in the modal window, they are all regrouped under the `labels` options + +#### Regular text labels (without translations) +- `cancelButton`: defaults to `"Cancel"`, override the Cancel button label +- `cloneButton`: defaults to `"Clone"`, override the Clone button label used by a modal type of "clone" +- `massSelectionButton`: defaults to `"Update Selection"`, override the Mass Selection button label +- `massSelectionStatus`: defaults to `"{{selectedRowCount}} of {{totalItems}} selected"`, override the Mass Selection status text on the footer left side +- `massUpdateButton`: defaults to `"Mass Update"`, override the Mass Update button label +- `massUpdateStatus`: defaults to `"all {{totalItems}} items"`, override the Mass Update status text on the footer left side +- `saveButton`: defaults to `"Save"`, override the Save button label used by a modal type of "create" or "edit" + +#### with a Translation Service (I18N) +As all other features using translation in this library, you can provide a translation key with the `Key` suffix and the available keys are the following +- `cancelButtonKey`: defaults to `"CANCEL"`, translation key used for the Cancel button label. +- `cloneButtonKey`: defaults to `"CLONE"`, translation key used for the Clone button label used by a modal type of "clone" +- `massSelectionButtonKey`: defaults to `"APPLY_TO_SELECTION"`, translation key used for the Mass Selection button label. +- `massSelectionStatusKey`: defaults to `"X_OF_Y_MASS_SELECTED"`, translation key used for the Mass Selection status text on the footer left side +- `massUpdateButtonKey`: defaults to `"APPLY_MASS_UPDATE"`, translation key used for the Mass Update button label. +- `massUpdateStatusKey`: defaults to `"ALL_X_RECORDS_SELECTED"`, translation key used for the Mass Update status text on the footer left side +- `saveButtonKey`: defaults to `"SAVE"`, translation key used for the Save button label used by a modal type of "create" or "edit" + +For example +```ts +compositeEditorInstance?.openDetails({ + headerTitle: 'Create New Item', + modalType: 'create', + labels: { + // without translations + cancelButton: 'Leave', + saveButton: 'Create Item', + + // with translations + cancelButtonKey: 'CANCEL', + saveButtonKey: 'SAVE' + }, + // ... +}); +``` + +## UI Options & Responsive Design +There are multiple options that you can change to change the UI design a bit, here's a lit of things you can change with their defaults +- `backdrop`: allows you add/remove the modal backdrop (options are `'static' | null`, default is `static`) +- `showCloseButtonOutside`: boolean value to show the close (icon) button inside or outside the modal window (defaults to `true`) + - note that the modal has some minimal responsive design styling and will automatically show the close icon inside the modal when available space is small +- `viewColumnLayout`: how many columns do we want to show in the view layout (options are `1 | 2 | 3 | 'auto'`, defaults to `auto`) + - for example if you wish to see your form split in a 2 columns layout (split view) then use `2` + - the `auto` mode will display a 1 column layout for 8 or less Editors, 2 columns layout for less than 15 Editors or 3 columns when more than 15 Editors + +## Dynamic Methods + +### Dynamically Change Form Input + +##### Component +```vue + +``` + +### Dynamically Change Editor Options (like `minDate` on a date picker) +For example, say that you have a Date1 and that when the user changes the Date1 to let say "2020-02-02" and you wish to use this new date as the `minDate` of the Date2, you can do it via the `changeFormEditorOption()` method as shown below. + +The example below shows code sample for all 3 supported editors AutoComplete, Date (picker), Single/Multiple Select (dropdown) + +##### Component +```vue + + + +``` + +### Dynamically Update Select Editor Collection +What if you need to change the collection array of a single/multiple select editor but based on another field input in the form? + +There are 2 ways to do it + +1. When you use `collectionOverride` (this will work in both the grid and the modal window) + - _the important thing to know is that the `collectionOverride` defined in the column definition below will return `finalCollection` and that is what we refer to as `editor.finalCollection` inside the `handleOnCompositeEditorChange` event handler_ + +##### Component +```vue + +``` + +2. When you simply want to replace the entire collection (this will NOT work in the grid, this will only work in the modal window) + - this is not recommended unless you only care about what happens in the modal window and not in the grid (editing), so option (1) with `collectionOverride` is preferable + +```vue + +``` + +### Disabling Form Inputs (readonly) +Disabling field(s) is done through the exact same way that you would do it in the grid, which is through the `onBeforeEditCell` SlickGrid event and you can find more in depth info at this other [Wiki - Disabling specific cell edit](../column-functionalities/editors.md#disabling-specific-cell-edit) + +```vue + +``` + +#### Disabling Form Inputs but only in Composite Editor +What if you want to disable certain form inputs but only in the Composite Editor, or use different logic in the grid. For that we added an extra `target` (`target` will return either "grid" or "composite") in the returned `args`, so you could apply different logic based on the target being the grid or the composite editor. For example: + +```ts +function handleOnBeforeEditCell(event) { + const eventData = event.detail.eventData; + const args = event && event.detail && event.detail.args; + const { column, item, grid, target } = args; + + if (column && item) { + if (!checkItemIsEditable(item, column, grid, target )) { + event.preventDefault(); // OR eventData.preventDefault(); + return false; + } + } + return false; +} + +function checkItemIsEditable(dataContext: any, columnDef: Column, grid: SlickGrid, target: 'grid' | 'composite') { + const gridOptions = grid?.getOptions(); + const hasEditor = columnDef.editor; + const isGridEditable = gridOptions.editable; + let isEditable = (isGridEditable && hasEditor); + + if (target === 'composite') { + // ... do composite checks + // isEditable = true; + } else { + // ... do grid checks + // isEditable = true; + } + + return isEditable; +} +``` diff --git a/frameworks/slickgrid-vue/docs/grid-functionalities/context-menu.md b/frameworks/slickgrid-vue/docs/grid-functionalities/context-menu.md new file mode 100644 index 000000000..2699c8268 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/grid-functionalities/context-menu.md @@ -0,0 +1,226 @@ +#### index +- [Default Usage](#default-usage) +- [Action Callback Methods](#action-callback-methods) +- [Override Callback Methods](#override-callback-methods) +- [How to add Translations](#how-to-add-translations) +- [Default Internal Commands](#default-internal-commands) +- [Show only over Certain Columns](https://github.com#show-menu-only-over-certain-columns) +- [How to Disable Context Menu](#how-to-disable-the-context-menu) +- [UI Sample](#ui-sample) + +### Demo + +#### Context Menu with Grouping +[Context Menu Demo](https://ghiscoding.github.io/slickgrid-vue/#/slickgrid/Example24) / [Demo Component](https://github.com/ghiscoding/slickgrid-vue/blob/master/src/examples/slickgrid/Example24.tsx) + +[Grouping Demo](https://ghiscoding.github.io/slickgrid-vue/#/slickgrid/Example13) / [Demo Component](https://github.com/ghiscoding/slickgrid-vue/blob/master/src/examples/slickgrid/Example13.tsx) + +### Description +A Context Menu is triggered by a mouse right+click and can show a list of Commands (to execute an action) and/or Options (to change the value of a field). The lib comes with a default list of custom commands (copy cell, export & grouping commands). Also note that the Commands list is following the same structure used in the [Cell Menu](../column-functionalities/Cell-Menu.md), [Header Menu](Header-Menu-&-Header-Buttons.md) & [Grid Menu](Grid-Menu.md). Very similar to the [Cell Menu](../column-functionalities/Cell-Menu.md), they were both created as SlickGrid plugins during the same period, their main difference is that they get triggered differently (mouse right+click vs cell click) and they serve different purposes. The Cell Menu is more oriented on a row action (e.g. delete current row) while the Context Menu is all about actions for the entire grid (e.g. export to Excel). + +This extensions is wrapped around the new SlickGrid Plugin **SlickContextMenu** + +### Default Usage +Technically, the Context Menu is enabled by default (copy, export) and so you don't have anything to do to enjoy it (you could disable it at any time). However, if you want to customize the content of the Context Menu, then continue reading. You can customize the menu with 2 different lists, Commands and/or Options, they can be used separately or at the same time. Also note that even though the code shown below makes a separation between the Commands and Options, you can mix them in the same Context Menu. + +#### with Commands + +```ts +gridOptions.value = { + enableFiltering: true, + enableContextMenu: true, // enabled by default + contextMenu: { + hideCloseButton: false, + commandTitle: 'Commands', // optional, add title + commandItems: [ + 'divider', + { divider: true, command: '', positionOrder: 60 }, + { + command: 'command1', title: 'Command 1', positionOrder: 61, + // you can use the "action" callback and/or use "onCommand" callback from the grid options, they both have the same arguments + action: (e, args) => { + console.log(args.dataContext, args.column); // action callback.. do something + } + }, + { command: 'help', title: 'HELP', iconCssClass: 'mdi mdi-help-circle', positionOrder: 62 }, + // you can add sub-menus by adding nested `commandItems` + { + // we can also have multiple nested sub-menus + command: 'export', title: 'Exports', positionOrder: 99, + commandItems: [ + { command: 'exports-txt', title: 'Text (tab delimited)' }, + { + command: 'sub-menu', title: 'Excel', cssClass: 'green', subMenuTitle: 'available formats', subMenuTitleCssClass: 'text-italic orange', + commandItems: [ + { command: 'exports-csv', title: 'Excel (csv)' }, + { command: 'exports-xlsx', title: 'Excel (xlsx)' }, + ] + } + ] + }, + ], + } +}; +``` + +#### with Options +That is when you want to define a list of Options (only 1 list) that the user can choose from and once is selected we would do something (for example change the value of a cell in the grid). + +```ts +gridOptions.value = { + contextMenu: { + hideCloseButton: false, + optionTitle: 'Change Effort Driven Flag', // optional, add title + optionItems: [ + { option: true, title: 'True', iconCssClass: 'mdi mdi-check-box-outline' }, + { option: false, title: 'False', iconCssClass: 'mdi mdi-checkbox-blank-outline' }, + { divider: true, command: '', positionOrder: 60 }, + ], + // subscribe to Context Menu onOptionSelected event (or use the "action" callback on each option) + onOptionSelected: (e, args) => { + // change Priority + const dataContext = args && args.dataContext; + if (dataContext && dataContext.hasOwnProperty('priority')) { + dataContext.priority = args.item.option; + sgb.gridService.updateItem(dataContext); + } + }, + } +}; +``` + +### Action Callback Methods +There are 2 ways to execute an action after a Command is clicked (or an Option is selected), you could do it via the `action` callback or via the `onCommand` callback. You might be wondering why 2 and what's the difference? Well the `action` would have to be defined on every single Command/Option while the `onCommand` (or `onOptionSelected`) is more of a global subscriber which gets triggered every time any of the Command/Option is clicked/selected, so for that, you would typically need to use `if/else` or a `switch/case`... hmm ok but I still don't understand when would I use the `onCommand`? Let say you combine the Context Menu with the (Action) [Cell Menu](Cell-Menu.md) and some of the commands are the same, well, in that case, it might be better to use the `onCommand` and centralize your commands in that callback, while in most other cases if you wish to do only 1 thing with a command, then using the `action` might be better. Also, note that they could also both be used at the same time if you wish. + +So if you decide to use the `action` callback, then your code would look like this +##### with `action` callback + +```ts +contextMenu: { + commandItems: [ + { command: 'command1', title: 'Command 1', action: (e, args) => console.log(args) } + { command: 'command2', title: 'Command 2', action: (e, args) => console.log(args) } + // ... + ] +} +``` + +##### with `onCommand` callback +```ts +contextMenu: { + commandItems: [ + { command: 'command1', title: 'Command 1' } + { command: 'command2', title: 'Command 2' } + // ... + ], + onCommand(e, args) => { + const columnDef = args.columnDef; + const command = args.command; + const dataContext = args.dataContext; + + switch (command) { + case 'command1': alert('Command 1'); break; + case 'command2': alert('Command 2'); break; + default: break; + } + } +} +``` + +### Override Callback Methods +What if you want to dynamically disable or hide a Command/Option or even disable the entire menu in certain circumstances? For these cases, you would use the override callback methods, the method must return a `boolean`. The list of override available are the following +- `menuUsabilityOverride` returning false would make the Context Menu unavailable to the user +- `itemVisibilityOverride` returning false would hide the item (command/option) from the list +- `itemUsabilityOverride` return false would disabled the item (command/option) from the list + - **note** there is also a `disabled` property that you could use, however it is defined at the beginning while the override is meant to be used with certain logic dynamically. + +For example, say we want the Context Menu to only be available on the first 20 rows of the grid, we could use the override this way + +```ts +contextMenu: { + menuUsabilityOverride: (args) => { + const dataContext = args && args.dataContext; + return (dataContext.id < 21); // say we want to display the menu only from Task 0 to 20 + }, +}, +``` + +To give another example, with Options this time, we could say that we enable the `n/a` option only when the row is Completed. So we could do it this way +```ts +contextMenu: { + optionItems: [ + { + option: 0, title: 'n/a', textCssClass: 'italic', + // only enable this option when the task is Not Completed + itemUsabilityOverride: (args) => { + const dataContext = args && args.dataContext; + return !dataContext.completed; + }, + }, + { option: 1, iconCssClass: 'mdi mdi-star-outline yellow', title: 'Low' }, + { option: 2, iconCssClass: 'mdi mdi-star orange', title: 'Medium' }, + { option: 3, iconCssClass: 'mdi mdi-star red', title: 'High' }, + ] +} +``` + +### How to add Translations? +It works exactly like the rest of the library when `enableTranslate` is set, all we have to do is to provide translations with the `Key` suffix, so for example without translations, we would use `title` and that would become `titleKey` with translations, that;'s easy enough. So for example, a list of Options could be defined as follow: +```ts +contextMenu: { + optionTitleKey: 'COMMANDS', // optionally pass a title to show over the Options + optionItems: [ + { option: 1, titleKey: 'LOW', iconCssClass: 'mdi mdi-star-outline yellow' }, + { option: 2, titleKey: 'MEDIUM', iconCssClass: 'mdi mdi-star orange' }, + { option: 3, titleKey: 'HIGH', iconCssClass: 'mdi mdi-star red' }, + ] +} +``` + +### Show Menu only over Certain Columns +Say you want to show the Context Menu only when the user is over certain columns of the grid. For that, you could use the `commandShownOverColumnIds` (or `optionShownOverColumnIds`) array, by default these arrays are empty and when that is the case then the menu will be accessible from any columns. So if we want to have the Context Menu available only over the first 2 columns, we would have an array of those 2 column ids. For example, the following would show the Context Menu everywhere except the last 2 columns (priority, action) since they are not part of the array. +```ts +cellMenu: { + commandShownOverColumnIds: ['title', 'percentComplete', 'start', 'finish', 'completed'. /* 'priority', 'action' */], +} +``` + +### Default Internal Commands +By defaults, the Context Menu will come with a few different preset Commands (copy, export). The Copy is straightforward, it allows you to copy the cell value, on the other hand, the export command(s) is dependent on the flags you have enabled in your Grid Options. For example, if you have only `enableExport` then you will get the `Export to CSV` and you might get as well `Export Tab-Delimited`, again that depends on which Grid Options you have enabled. Note that all internal commands have a `positionOrder` in the range of 50 to 60 (which is used to sort the Commands list), this allows you to append or prepend Commands to the list. + +Another set of possible Commands would be related to Grouping, so if you are using Grouping in your grid then you will get 3 extra Commands (clear grouping, collapse groups, expand groups). + +All of these internal commands, you can choose to hide them and/or change their icons, the default global options are the following and you can change any of them. +```ts +contextMenu: { + autoAdjustDrop: true, + autoAlignSide: true, + hideCloseButton: true, + hideClearAllGrouping: false, + hideCollapseAllGroups: false, + hideCommandSection: false, + hideCopyCellValueCommand: false, + hideExpandAllGroups: false, + hideExportCsvCommand: false, + hideExportExcelCommand: false, + hideExportTextDelimitedCommand: true, + hideMenuOnScroll: true, + hideOptionSection: false, + iconCopyCellValueCommand: 'mdi mdi-content-copy', + iconExportCsvCommand: 'mdi mdi-download', + iconExportExcelCommand: 'mdi mdi-file-excel-outline text-success', + iconExportTextDelimitedCommand: 'mdi mdi-download', + width: 200, +}, +``` + +### How to Disable the Context Menu? +You can disable the Context Menu, by calling `enableContextMenu: false` from the Grid Options. +```typescript +gridOptions.value = { + enableContextMenu: false +}; +``` + +### UI Sample +![image](https://user-images.githubusercontent.com/643976/71301652-024afe80-2370-11ea-909d-bb802d69edc1.png) diff --git a/frameworks/slickgrid-vue/docs/grid-functionalities/custom-footer.md b/frameworks/slickgrid-vue/docs/grid-functionalities/custom-footer.md new file mode 100644 index 000000000..e7d611c10 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/grid-functionalities/custom-footer.md @@ -0,0 +1,80 @@ +### Description +You can use and show the Custom Footer with 2 left/right containers and will by default display filtered item count & total count on the right side. Also if it detects that you use row selection, it will also show the row selection count on the left footer side. You can also override both left/right side texts. + +**NOTE:** The Custom Footer cannot be used in combination with Pagination, you can only show 1 or the other. + +### Demo +[Demo Page](https://ghiscoding.github.io/slickgrid-vue/#/Example2) / [Demo Component](https://github.com/ghiscoding/slickgrid-vue/blob/master/src/examples/slickgrid/Example2.tsx) + +### Usage + +```ts +function defineGrid() { + columnDefinitions.value = [ /*...*/ ]; + + gridOptions.value = { + // ... + showCustomFooter: true, // display some metrics in the bottom custom footer + customFooterOptions: { + // optionally display some text on the left footer container + leftFooterText: 'Grid created with Slickgrid-Universal', + hideMetrics: false, + hideTotalItemCount: false, + hideLastUpdateTimestamp: false + }, + }; +} +``` + +#### CustomFooterOption Interface +Below is the list of all options available with the Custom Footer, you can visit the [customFooterOption.interface.ts](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/interfaces/customFooterOption.interface.ts) to see latest code in case the code below is not up to date. +```ts +export interface CustomFooterOption { + /** Optionally provide some text to be displayed on the left side of the footer (in the "left-footer" css class) */ + leftFooterText?: string; + + /** CSS class used for the left container */ + leftContainerClass?: string; + + /** Date format used when showing the "Last Update" timestamp in the metrics section. */ + dateFormat?: string; + + /** Defaults to 25, height of the Custom Footer in pixels, it could be a number (25) or a string ("25px") but it has to be in pixels. It will be used by the auto-resizer calculations. */ + footerHeight?: number | string; + + /** + * Defaults to false, which will hide the selected rows count on the bottom left of the footer. + * NOTE: if users defined a `leftFooterText`, then the selected rows count will NOT show up. + */ + hideRowSelectionCount?: boolean; + + /** Defaults to false, do we want to hide the last update timestamp (endTime)? */ + hideLastUpdateTimestamp?: boolean; + + /** + * Defaults to false, do we want to hide the metrics (right section) when the footer is displayed? + * That could be used when we want to display only the left section with custom text + */ + hideMetrics?: boolean; + + /** Defaults to false, do we want to hide the total item count of the entire dataset (the count exclude any filtered data) */ + hideTotalItemCount?: boolean; + + /** Defaults to "|", separator between the timestamp and the total count */ + metricSeparator?: string; + + /** Text shown in the custom footer on the far right for the metrics */ + metricTexts?: MetricTexts; + + /** CSS class used for the right container */ + rightContainerClass?: string; + + /** Optionally provide some text to be displayed on the right side of the footer (in the "right-footer" css class) */ + rightFooterText?: string; +} +``` + +#### Screenshot Demo +Below is a print screen of the demo, you can see the full advantage of the custom footer with custom text on the left and filtered item count + timestamp on the right. + +![image](https://user-images.githubusercontent.com/643976/122082196-ca3e4380-cdcd-11eb-84ed-4d2f4eb8057b.png) diff --git a/frameworks/slickgrid-vue/docs/grid-functionalities/custom-tooltip.md b/frameworks/slickgrid-vue/docs/grid-functionalities/custom-tooltip.md new file mode 100644 index 000000000..86033bbe5 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/grid-functionalities/custom-tooltip.md @@ -0,0 +1,265 @@ +#### index +- [Column Definition - Custom Tooltip](#via-column-definition) +- [Grid Options - Custom Tooltip](#via-grid-options) +- [Alignment](#alignment) +- Tooltip Type + - [on Cell](#cell-custom-tooltip-with-formatter) with `formatter` + - [on Cell Async Tooltip](#cell-async-custom-tooltip-with-formatter-and-asyncpostformatter-async-api-call) (Async API call from Promise/Observable) + - [on Column Header (title)](#column-header-custom-tooltip-with-headerformatter) with `headerFormatter` + - [on Column Header row (filter)](#column-header-custom-tooltip-with-headerrowformatter) with `headerRowFormatter` + - [with regular `[title]` attribute](#regular-tooltip-with-a-title-attribute) + - [tooltip text length](#regular-tooltip-max-length) +- [How to delay the opening of a tooltip?](#how-to-delay-the-opening-of-a-tooltip) + - [delay a tooltip with Formatter](#delay-a-tooltip-with-formatter) + - [delay a Regular Tooltip](#delay-a-regular-tooltip) +- `customTooltip` options + - too many to list, consult the [CustomTooltipOption](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/interfaces/column.interface.ts) interface for all possible options +- [UI Sample](#ui-sample) + +### Description +A plugin to add Custom Tooltip when hovering a cell, it subscribes to the cell `onMouseEnter` and `onMouseLeave` events. +The `customTooltip` is defined in the Column Definition OR Grid Options (the first found will have priority over the second) +To specify a tooltip when hovering a cell + +**NOTE:** this is an opt-in plugin, you must import the necessary plugin from `@slickgrid-universal/custom-tooltip-plugin` and instantiate it in your grid options via `externalResources`, see multiple examples below. + +### Demo +[Demo Page](https://ghiscoding.github.io/slickgrid-vue/#/slickgrid/Example32) / [Demo Component](https://github.com/ghiscoding/slickgrid-vue/blob/master/src/examples/slickgrid/Example32.tsx) + +### via Column Definition +You can set or change option of an individual column definition custom tooltip. +```ts +import { SlickCustomTooltip } from '@slickgrid-universal/custom-tooltip-plugin'; + +function defineGrid() { + columnDefinitions.value = [{ + id: "title", name: "Title", field: "title", formatter: titleFormatter, + customTooltip: { + formatter: tooltipTaskFormatter, + // ... + } + }]; + + // make sure to register the plugin in your grid options + gridOptions.value = { + externalResources: [new SlickCustomTooltip()], + }; +} +``` + +### via Grid Options +You can set certain options for the entire grid, for example if you set `exportWithFormatter` it will evaluate the Formatter (when exist) output to export each cell. The Grid Menu also has the "Export to Excel" enabled by default. +```ts +import { SlickCustomTooltip } from '@slickgrid-universal/custom-tooltip-plugin'; + +function defineGrid() { + gridOptions.value = { + externalResources: [new SlickCustomTooltip()], + customTooltip: { + formatter: tooltipTaskFormatter, + + // optionally skip tooltip on some of the column(s) (like 1st column when using row selection) + usabilityOverride: (args) => (args.cell !== 0 && args?.column?.id !== 'action'), // disable on 1st and also "action" column + }, + }; +} +``` + +## Alignment +The default alignment is "auto" (which will align to the left by default or on the right when there's not enough room). You can change the alignment on any of the cell (or all of them via grid option) by simply providing a value to the `position`. + +The available position are: `'auto' | 'top' | 'bottom' | 'left-align' | 'right-align' | 'center'` (note that "center" was only added recently) + +```ts +// define your custom tooltip in a Column Definition OR Grid Options +customTooltip: { + position: 'left-align' +}, +``` + +## Tooltip Types +### Cell Custom Tooltip with `formatter` +You can create a Custom Tooltip which will show up when hovering a cell by simply providing a `formatter` [via a Column Definition](#via-column-definition) (per column) OR [via Grid Options](#via-grid-options) (all columns of the grid), the formatter is the same structure as a regular formatter and accepts html string. +```ts +// define your custom tooltip in a Column Definition OR Grid Options +customTooltip: { + formatter: tooltipFormatter, +}, +``` +here's a simple formatter (you can see the result in the [UI Sample](#ui-sample) gif below) + +```ts +function tooltipFormatter(row, cell, value, column, dataContext, grid) { + const tooltipTitle = 'Custom Tooltip'; + const effortDrivenHtml = Formatters.checkmarkMaterial(row, cell, dataContext.effortDriven, column, dataContext, grid); + + return `
${tooltipTitle}
+
Id:
${dataContext.id}
+
Title:
${dataContext.title}
+
Effort Driven:
${effortDrivenHtml}
+
Completion:
${dataContext.percentComplete}
`; +} +``` + +### Cell Async Custom Tooltip with `formatter` and `asyncPostFormatter` (Async API call) +You can create an Async Custom Tooltip which is a delayed tooltip (for example when you call an API to fetch some info), will show up when hovering a cell it will require a bit more setup. The `formatter` will be use to show any form of "loading..." and your final tooltip will be shown via the `asyncPostFormatter` both formatters use the same structure as a regular formatter and accepts html string. It will also require you to provide an `asyncProcess` of your API call (it could be a Promise or Observable), it also provides the same arguments as a regular formatter. +```ts +// define your custom tooltip in a Column Definition OR Grid Options +customTooltip: { + // 1- loading formatter + formatter: () => `loading...
`, + + // 2- post process formatter + asyncProcess: (row, cell, val, column, dataContext) => fetch(`/user/${dataContext.id}`), // could be a Promise/Observable + asyncPostFormatter: userFullDetailAsyncFormatter, +}, +``` + +here's the final post process async formatter + +```ts +function userFullDetailAsyncFormatter(row, cell, value, column, dataContext, grid) { + const tooltipTitle = 'User Detail - Async Tooltip'; + return `
${tooltipTitle}
+
Id:
${dataContext.id}
+
First Name:
${dataContext.firstName}
+
Last Name:
${dataContext.lastName}
+
Age:
${dataContext.age}
+
Gender:
${dataContext.gender}
+
Title:
${dataContext.title}
+
Seniority:
${dataContext.seniority}
`; +} +``` + +### Column Header Custom Tooltip with `headerFormatter` +You can create a Custom Tooltip which will show up when hovering a column header (title) by simply providing a `headerFormatter` [via a Column Definition](#via-column-definition) (per column) OR [via Grid Options](#via-grid-options) (all columns of the grid), the formatter is the same structure as a regular formatter and accepts html string. +```ts +// define your custom tooltip in a Column Definition OR Grid Options +customTooltip: { + headerFormatter: headerFormatter, +}, +``` +here's a simple formatter +```ts +function headerFormatter(row, cell, value, column) { + const tooltipTitle = 'Custom Tooltip - Header'; + return `
${tooltipTitle}
+
Column:
${column.name}
`; +} +``` + +### Column Header Custom Tooltip with `headerRowFormatter` +You can create a Custom Tooltip which will show up when hovering a column header (title) by simply providing a `headerRowFormatter` [via a Column Definition](#via-column-definition) (per column) OR [via Grid Options](#via-grid-options) (all columns of the grid), the formatter is the same structure as a regular formatter and accepts html string. + +```ts +// define your custom tooltip in a Column Definition OR Grid Options +customTooltip: { + headerRowFormatter: headerRowFormatter, +}, +``` + +here's a simple formatter + +```ts +function headerRowFormatter(row, cell, value, column) { + const tooltipTitle = 'Custom Tooltip - Header Row (filter)'; + return `
${tooltipTitle}
+
Column:
${column.field}
`; +} +``` + +### Regular Tooltip with a `[title]` attribute +You can create a regular tooltip simply by enabling `useRegularTooltip: true`, it will parse the regular cell formatter in search for a `title="..."` attribute (it won't work without a cell formatter, unless the cell text content is larger than the cell width when ellipsis shows up "some text..." and that will automatically create a tooltip, that could however be disabled if you wish). + +This feature is very useful so you probably want to enable this flag globally, but you could also still choose to add only [via a Column Definition](#via-column-definition) (per column) OR [via Grid Options](#via-grid-options) (all columns of the grid). + +NOTE: regular tooltip, as opposed to other type of custom tooltip, will be rendered as plain text. You could however change that by enabling this flag `renderRegularTooltipAsHtml: true` + +```ts +// define your custom tooltip in a Column Definition OR Grid Options +customTooltip: { + useRegularTooltip: true, // a regular tooltip will search for a "title" attribute in the cell formatter (it won't work without a cell formatter) + + // if you wish to disable auto-tooltip creation when ellipsis (...) shows up, can use this flag + // useRegularTooltipFromFormatterOnly: true, +}, +``` + +#### Regular tooltip max length +By default the custom tooltip text will be limited, and potentially truncated, to 650 characters in order to keep the tooltip with a size that is not too large. You could change the grid option setting with this + +```ts +gridOptions.value = { + customTooltip: { + tooltipTextMaxLength: 650, + }, +} +``` + +### How to delay the opening of a tooltip? +#### delay a Tooltip with Formatter +There are no built-in option to delay a custom tooltip because it would add too much code complexity to the codebase, however you can simply do that by taking advantage of the Async Custom Tooltip. The only thing you might want to do though is to have the first custom tooltip `formatter` to return an empty string (so it won't show a loading tooltip) and then use the `asyncPostFormatter` for the tooltip (note that it will **not** read the cell formatter, if you have requirement for that then simply combined formatter into an external formatter function, see 2nd examples below). +```ts +// define your custom tooltip in a Column Definition OR Grid Options +customTooltip: { + // 1- loading formatter + formatter: () => ``, // return empty so it won't show any pre-tooltip + + // 2- delay the opening by a simple Promise and `setTimeout` + asyncProcess: () => new Promise(resolve => { + setTimeout(() => resolve({}), 500); // delayed by half a second + }), + asyncPostFormatter: userFullDetailAsyncFormatter, +}, +``` +#### delay a Regular Tooltip +It is possible to also delay a regular tooltip (when using `useRegularTooltip`) even when using the optional `useRegularTooltipFromFormatterOnly` but it requires a bit of code change. For example, let say you want to parse the `title` from a formatter but delay it, you could do it as shown below but please note that it will read the `asyncPostFormatter`, not the cell `formatter`, and so you should probably create an external formatter function to make simpler code. + +##### tooltip text output will be: "show this tooltip title text" +```ts +// define your custom tooltip in a Column Definition OR Grid Options +columnDefinitions.value = [{ + id: 'firstName', field: 'firstName', name: 'First Name', + customTooltip: { + // 1- loading formatter + formatter: () => ``, // return empty so it won't show any pre-tooltip + + // 2- delay the opening by a simple Promise and `setTimeout` + asyncProcess: () => new Promise(resolve => setTimeout(() => resolve(null), 500)), // delayed by half a second + asyncPostFormatter: `cell value`, // this will be read as tooltip + }, + formatter: `cell value`, // this won't be read as tooltip +}]; +``` +the previous code could be refactored to have only 1 common formatter that is referenced in both cell `formatter` and tooltip `asyncPostFormatter` +##### tooltip text output will be: "show this tooltip title text" +```ts +const myFormatter = () => `cell value`; + +// define your custom tooltip in a Column Definition OR Grid Options +columnDefinitions.value = [{ + id: 'firstName', field: 'firstName', name: 'First Name', + customTooltip: { + // 1- loading formatter + formatter: () => ``, // return empty so it won't show any pre-tooltip + + // 2- delay the opening by a simple Promise and `setTimeout` + asyncProcess: () => new Promise(resolve => setTimeout(() => resolve(null), 500)), // delayed by half a second + asyncPostFormatter: myFormatter + }, + formatter: myFormatter +}]; +``` + +### UI Sample +The Export to Excel handles all characters quite well, from Latin, to Unicode and even Unicorn emoji, it all works on all browsers (`Chrome`, `Firefox`, even `IE11`, I don't have access to older versions). Here's a demo + +![image](https://user-images.githubusercontent.com/643976/138971279-b835b8f5-93f1-4e77-bd90-f86599e199e9.png) + +auto tooltip on large text, that is when ellipsis (...) shows up on large text + +![image](https://user-images.githubusercontent.com/643976/139088036-9168e632-1ae6-4c69-8302-f9df8510ec4b.png) + +Async Custom Tooltip (API call Promise/Observable) + +![ganSbcmm8v](https://user-images.githubusercontent.com/643976/139093922-987b953d-984f-4ec3-badb-941cc2ec78ec.gif) diff --git a/frameworks/slickgrid-vue/docs/grid-functionalities/dynamic-item-metadata.md b/frameworks/slickgrid-vue/docs/grid-functionalities/dynamic-item-metadata.md new file mode 100644 index 000000000..6936d41b8 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/grid-functionalities/dynamic-item-metadata.md @@ -0,0 +1,139 @@ +SlickGrid is very flexible and it allows you to change or add CSS Class(es) dynamically (or on page load) by changing it's `Item Metadata` (see [SlickGrid Wiki - Item Metadata](providing-grid-data.md)). There is also a Stack Overflow [answer](https://stackoverflow.com/a/19985148/1212166), which this code below is based from. + +### Demo +[Demo Page](https://ghiscoding.github.io/slickgrid-vue/#/slickgrid/Example11) / [Demo Component](https://github.com/ghiscoding/slickgrid-vue/blob/master/src/examples/slickgrid/Example11.tsx) + +### Dynamically Change CSS Classes +##### Component +```vue + + +