From 86dd8a83f6b8d2fa7800af55f68167369c98ff94 Mon Sep 17 00:00:00 2001 From: Joshua B Date: Wed, 15 Dec 2021 09:41:39 -0700 Subject: [PATCH] Dev (#642) * update ky, fix ag to svelte example, RestDataApi will look for q and do stringify if its an object * Fixed quick search * Fixed search * refactor select2 so uses same fomratter for displayFields * comment out select tests since we dont support * get tests working for jest and ts. revamp truthy and empty * add jest to the checks * remove the setup for now * get jest tests working and move critical tests in, all tests are in __tests__ * Select fixes (#643) * Fixed select with complex dataApiParams * Lint fix * esm to get mockserver working with imports * reverted ts, to slow. * fix and cleanup the listctrl dep (#644) * fix and cleanup the listctrl dep * should have a property that we pass to GRidCtrl for contextMenuClickAction * pass everything through with the gridOpts * fixed showSearchForm * more clean up * cleanup to get rid of extendFilters * removed a bunch of console logs * add init search to config * Filtering refactoring Co-authored-by: alexeyzvegintcev * Grid remove listctrl dep (#645) * fix and cleanup the listctrl dep * should have a property that we pass to GRidCtrl for contextMenuClickAction * pass everything through with the gridOpts * fixed showSearchForm * more clean up * cleanup to get rid of extendFilters * removed a bunch of console logs * add init search to config * Filtering refactoring * get sort squared away * more lodash cleanup * fix for es6 * rename to restrictSearch * add start of the DataQuery class Co-authored-by: alexeyzvegintcev * Grid remove listctrl dep (#646) * fix and cleanup the listctrl dep * should have a property that we pass to GRidCtrl for contextMenuClickAction * pass everything through with the gridOpts * fixed showSearchForm * more clean up * cleanup to get rid of extendFilters * removed a bunch of console logs * add init search to config * Filtering refactoring * get sort squared away * more lodash cleanup * fix for es6 * rename to restrictSearch * add start of the DataQuery class Co-authored-by: alexeyzvegintcev * fixed foo bar * Grid fixes (#648) * Fixed cors issue for grids * Grid actions without listCtrl import * Custom toolbar actions * Fixed typo Co-authored-by: alexeyzvegintcev Co-authored-by: alexeyzvegintcev --- .circleci/config.yml | 1 + Makefile | 7 +- examples/demo/config.js | 2 +- examples/demo/mocker/index.js | 3 + examples/demo/public/data/CustomerConfig.yml | 2 +- examples/demo/public/data/Customers.json | 35 +++ examples/demo/src/app.config.js | 15 +- .../src/controls/select-rest/select-rest.html | 3 +- .../demo/src/controls/select/basic/comp.html | 31 ++- .../src/controls/select/selFromData/comp.html | 1 + .../src/grids/customGridList/list/list.html | 4 +- examples/demo/src/grids/routes.js | 1 + examples/demo/src/store/RestStoreApi.js | 6 +- examples/grails-demo/src/main.js | 2 +- jest.config.js | 10 +- package.json | 2 + src/dataApi/AppConfigApi.js | 4 +- src/dataApi/DataQuery.js | 28 +++ src/dataApi/ItemStore.js | 85 +++++++ src/dataApi/MemDataApi.js | 35 +-- src/dataApi/RestDataApi.js | 40 ++-- .../MemDataApi.spec.js} | 34 +-- src/dataApi/{kyClient.js => kyApi.js} | 11 +- src/gridz/GridCtrl.js | 92 +++++--- src/gridz/jq.formatters.js | 1 - src/gridz/jq.gridz.js | 117 +++++++--- .../formly-helpers.spec.js} | 3 +- src/ng/common/directives/agMaxLines.js | 2 +- src/ng/controls/AgBaseControl.js | 7 +- .../controls/ag-select-rest/select-rest.scss | 2 +- src/ng/controls/formly/helpers.js | 2 +- src/ng/controls/index.js | 3 - src/ng/controls/ui-select2/dataQuery.js | 91 +++++--- src/ng/controls/ui-select2/select2Setup.js | 99 ++++++++ src/ng/controls/ui-select2/ui.select2.js | 219 +++--------------- src/ng/filters/agCurrencyFilter.js | 4 +- src/ng/filters/agDateFilter.js | 2 +- src/ng/gridz/gridz-directive.js | 3 +- src/ng/gridz/list/BaseEditCtrl.js | 3 +- src/ng/gridz/list/BaseListCtrl.js | 58 +++-- src/ng/gridz/list/BulkUpdateModalCtrl.js | 1 - src/ng/gridz/list/ag-grid-list.js | 10 +- src/ng/gridz/toolbar/gridz-toolbar/index.js | 3 +- .../gridz/toolbar/gridz-toolbar/toolbar.html | 2 +- src/ng/uirouter/stateHelper.js | 2 - src/ng/uirouter/stateHelperInit.js | 4 +- src/styles/extra/components/_table.scss | 95 +------- src/tools/AppConfig.js | 9 +- src/utils/Log.js | 3 +- src/utils/__tests__/deepDiff.spec.js | 78 +++++++ src/utils/__tests__/deepPick.spec.js | 58 +++++ src/utils/__tests__/flattenObject.spec.js | 23 ++ src/utils/__tests__/isSomething.spec.js | 69 ++++++ src/utils/__tests__/nameUtils.spec.js | 24 ++ src/utils/__tests__/prune.spec.js | 40 ++++ src/utils/__tests__/truthy.spec.js | 107 +++++++++ src/utils/dash.js | 33 +++ src/utils/dateSupport.js | 2 +- src/utils/deepDiff.js | 5 +- src/{gridz => utils}/flattenObject.js | 3 +- src/utils/isFalsy.js | 13 -- src/utils/labelMaker.js | 17 -- src/utils/mapFunc.js | 10 + src/utils/nameUtils.js | 41 ++++ src/utils/prune.js | 22 ++ src/utils/stringFormUtils.js | 18 +- src/utils/stringUtils.js | 32 --- src/utils/truthy.js | 67 ++++++ svelte/Form/Choice.svelte | 13 +- svelte/Form/Form.svelte | 4 +- svelte/Form/Input.svelte | 2 +- svelte/Form/Select.svelte | 2 +- svelte/__tests__/Foo.spec.js | 20 -- svelte/utils/dash.js | 1 + tests/karma.webpack.js | 6 + tests/unit/angleGrinder/gridz/flattenSpec.js | 29 --- .../angleGrinder/select2/uiSelect2Spec.js | 81 +------ tests/unit/angleGrinder/utils/deepDiffSpec.js | 91 -------- tests/unit/angleGrinder/utils/deepPickSpec.js | 66 ------ tests/unit/angleGrinder/utils/isFalsy.spec.js | 42 ---- tsconfig.json | 5 +- webpack.config.js | 5 +- yarn.lock | 165 +++++++++++++ 83 files changed, 1437 insertions(+), 956 deletions(-) create mode 100644 src/dataApi/DataQuery.js create mode 100644 src/dataApi/ItemStore.js rename src/dataApi/{MemDataApi.test.js => __tests__/MemDataApi.spec.js} (82%) rename src/dataApi/{kyClient.js => kyApi.js} (81%) rename src/ng/{controls/formly/helpers.test.js => __tests__/formly-helpers.spec.js} (98%) create mode 100644 src/ng/controls/ui-select2/select2Setup.js create mode 100644 src/utils/__tests__/deepDiff.spec.js create mode 100644 src/utils/__tests__/deepPick.spec.js create mode 100644 src/utils/__tests__/flattenObject.spec.js create mode 100644 src/utils/__tests__/isSomething.spec.js create mode 100644 src/utils/__tests__/nameUtils.spec.js create mode 100644 src/utils/__tests__/prune.spec.js create mode 100644 src/utils/__tests__/truthy.spec.js create mode 100644 src/utils/dash.js rename src/{gridz => utils}/flattenObject.js (88%) delete mode 100644 src/utils/isFalsy.js delete mode 100644 src/utils/labelMaker.js create mode 100644 src/utils/mapFunc.js create mode 100644 src/utils/nameUtils.js create mode 100644 src/utils/prune.js delete mode 100644 src/utils/stringUtils.js create mode 100644 src/utils/truthy.js delete mode 100644 svelte/__tests__/Foo.spec.js create mode 100644 svelte/utils/dash.js delete mode 100644 tests/unit/angleGrinder/gridz/flattenSpec.js delete mode 100644 tests/unit/angleGrinder/utils/deepDiffSpec.js delete mode 100644 tests/unit/angleGrinder/utils/deepPickSpec.js delete mode 100644 tests/unit/angleGrinder/utils/isFalsy.spec.js diff --git a/.circleci/config.yml b/.circleci/config.yml index 325955277..cb82f89f7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -19,6 +19,7 @@ jobs: paths: [ './node_modules' ] - run: make lint + - run: make jest - run: make test.karma - run: make test.jasmine - run: make build.demo diff --git a/Makefile b/Makefile index f89c287b9..1ff40cef5 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,7 @@ include $(SHIPKIT_MAKEFILES)/circle.make export BOT_EMAIL ?= 9cibot@9ci.com karma.sh = npx karma lint.sh = npx eslint +jest.sh = npx jest # --- standard base build ---- @@ -26,7 +27,7 @@ install: node_modules check: install lint test ## runs both karma and jasmine tests -test: test.karma test.jasmine +test: jest test.karma test.jasmine ## runs karma tests test.karma: @@ -36,6 +37,10 @@ test.karma: test.jasmine: $(karma.sh) start tests/karma-jasmine.conf.js --single-run --no-auto-watch --no-sandbox $$* +## runs jasmine tests +jest: + $(jest.sh) + ## runs eslint lint: $(lint.sh) src/ diff --git a/examples/demo/config.js b/examples/demo/config.js index 61db82e71..617a1e3bf 100644 --- a/examples/demo/config.js +++ b/examples/demo/config.js @@ -1,4 +1,4 @@ var configData = { - base_url:"http://localhost:3002/", + base_url:"http://0.0.0.0:3002/", foo:"bar" } diff --git a/examples/demo/mocker/index.js b/examples/demo/mocker/index.js index 3cb5ce154..a27fd2ec5 100644 --- a/examples/demo/mocker/index.js +++ b/examples/demo/mocker/index.js @@ -1,3 +1,6 @@ +//esm allows imports to work with node +require = require("esm")(module); + const delay = require('mocker-api/utils/delay') // const _ = require('lodash') const yaml = require('js-yaml') diff --git a/examples/demo/public/data/CustomerConfig.yml b/examples/demo/public/data/CustomerConfig.yml index c83261925..6a4e4180d 100644 --- a/examples/demo/public/data/CustomerConfig.yml +++ b/examples/demo/public/data/CustomerConfig.yml @@ -4,7 +4,7 @@ gridOptions: - { name: num, width: 30 } - { name: name, formatter: editActionLink } - { name: city, width: 100 } - - { name: state, width: 60 } + - { name: location.state, width: 60 } - { name: postalCode, label: Zip, width: 60 } - { name: country, width: 60 } # sortname: name diff --git a/examples/demo/public/data/Customers.json b/examples/demo/public/data/Customers.json index 7c4c38b10..e226d6da0 100644 --- a/examples/demo/public/data/Customers.json +++ b/examples/demo/public/data/Customers.json @@ -3,6 +3,13 @@ "id": 1, "name": "Rhynoodle", "num": "RH0143", + "location": { + "street": "04828 Mcguire Alley", + "city": "Sacramento", + "state": "CA", + "postalCode": 95833, + "country": "US" + }, "street": "04828 Mcguire Alley", "city": "Sacramento", "state": "CA", @@ -14,6 +21,13 @@ "id": 2, "name": "Yodo", "num": "YO7612", + "location": { + "street": "405 Old Shore Avenue", + "city": "Merritt", + "state": "BC", + "postalCode": "V1K", + "country": "CA" + }, "street": "405 Old Shore Avenue", "city": "Merritt", "state": "BC", @@ -25,6 +39,13 @@ "id": 3, "name": "Omba", "num": "OM3054", + "location": { + "street": "8 Charing Cross Circle", + "city": "Niños Heroes", + "state": "OAX", + "postalCode": 68145, + "country": "MX" + }, "street": "8 Charing Cross Circle", "city": "Niños Heroes", "state": "OAX", @@ -36,6 +57,13 @@ "id": 4, "name": "Gabcube", "num": "GA8966", + "location": { + "street": "55 Warbler Street", + "city": "Colorado Springs", + "state": "CO", + "postalCode": 80945, + "country": "US" + }, "street": "55 Warbler Street", "city": "Colorado Springs", "state": "CO", @@ -47,6 +75,13 @@ "id": 5, "name": "Voonder", "num": "VO3537", + "location": { + "street": "4402 Summer Ridge Drive", + "city": "Maskinongé", + "state": "QC", + "postalCode": "T7A", + "country": "CA" + }, "street": "4402 Summer Ridge Drive", "city": "Maskinongé", "state": "QC", diff --git a/examples/demo/src/app.config.js b/examples/demo/src/app.config.js index 867ec60d0..555805b49 100644 --- a/examples/demo/src/app.config.js +++ b/examples/demo/src/app.config.js @@ -4,12 +4,25 @@ import appName from './app.module' import './config.router' import appState from 'angle-grinder/src/tools/AppState' import {setConfig} from 'angle-grinder/src/tools/AppConfig' -import {setClientConfig} from "angle-grinder/src/dataApi/kyClient"; +import {setClientConfig} from "angle-grinder/src/dataApi/kyApi"; const app = angular.module(appName) // export default app.name app.run(function($rootScope, $state, $stateParams) { + + // window.onbeforeunload = function(){ + // sessionStorage.setItem("origin", window.location.href); + // } + + // window.onload = function(){ + // console.log("************window.location.href", window.location.href) + // if(window.location.href == sessionStorage.getItem("origin")){ + // console.log("************clearing storage", window.location.href) + // sessionStorage.clear(); + // } + // } + 'ngInject'; // Set the ui-router state vars to global root to access them from any scope $rootScope.$state = $state diff --git a/examples/demo/src/controls/select-rest/select-rest.html b/examples/demo/src/controls/select-rest/select-rest.html index 25f7aa3ca..716a31620 100644 --- a/examples/demo/src/controls/select-rest/select-rest.html +++ b/examples/demo/src/controls/select-rest/select-rest.html @@ -1,6 +1,7 @@ +

LEGACY WAY OF DOING IT WITH AJAX, PREFER USING dataApiKey

- diff --git a/examples/demo/src/controls/select/basic/comp.html b/examples/demo/src/controls/select/basic/comp.html index 5009fc597..1f696b309 100644 --- a/examples/demo/src/controls/select/basic/comp.html +++ b/examples/demo/src/controls/select/basic/comp.html @@ -22,36 +22,31 @@ -
- Basic Select2 -
-
- -
-
- -
-
-
+

using ag-select component

rest data from dataApiKey

- - + + + -

rest data from promise

- diff --git a/examples/demo/src/controls/select/selFromData/comp.html b/examples/demo/src/controls/select/selFromData/comp.html index 34c1d935c..b99364b47 100644 --- a/examples/demo/src/controls/select/selFromData/comp.html +++ b/examples/demo/src/controls/select/selFromData/comp.html @@ -1,3 +1,4 @@ + diff --git a/examples/demo/src/grids/customGridList/list/list.html b/examples/demo/src/grids/customGridList/list/list.html index e606c3b07..869c386ec 100644 --- a/examples/demo/src/grids/customGridList/list/list.html +++ b/examples/demo/src/grids/customGridList/list/list.html @@ -5,7 +5,9 @@ + list-ctrl="$ctrl" + init-search="{name: 'yo*'}" + >
vm: {{$ctrl.vm | json}}
diff --git a/examples/demo/src/grids/routes.js b/examples/demo/src/grids/routes.js index 233596efe..70822ee0b 100644 --- a/examples/demo/src/grids/routes.js +++ b/examples/demo/src/grids/routes.js @@ -24,6 +24,7 @@ const gridsStates = { }], resolve: { apiKey: () => 'customer', + // gridOptions: () => ({multiSort: true}), notification: () => ({ class: 'is-primary is-light', text: 'Uses ui-router to send rest apiKey to generic agGridList component' diff --git a/examples/demo/src/store/RestStoreApi.js b/examples/demo/src/store/RestStoreApi.js index cea46acae..ab0507498 100644 --- a/examples/demo/src/store/RestStoreApi.js +++ b/examples/demo/src/store/RestStoreApi.js @@ -1,8 +1,8 @@ import RestDataApi from 'angle-grinder/src/dataApi/RestDataApi' -import ky from 'ky' +import kyApi from 'angle-grinder/src/dataApi/kyApi' function makeDataApi(endpoint){ - return new RestDataApi(`${configData.base_url}api/${endpoint}`) + return new RestDataApi(`api/${endpoint}`) } /** main holder for api */ @@ -23,7 +23,7 @@ export class RestStoreApi { get tag() { return makeDataApi('tag') } appConfig(configKey) { - return ky.get(`api/appConfig/${configKey}`).json() + return kyApi.ky.get(`api/appConfig/${configKey}`).json() } /** diff --git a/examples/grails-demo/src/main.js b/examples/grails-demo/src/main.js index 1130deac7..f77ff26a7 100644 --- a/examples/grails-demo/src/main.js +++ b/examples/grails-demo/src/main.js @@ -13,7 +13,7 @@ import './app.config' import './AppCtrl' import './grids' -import {setClientConfig} from 'angle-grinder/src/dataApi/kyClient' +import {setClientConfig} from 'angle-grinder/src/dataApi/kyApi' setClientConfig({prefixUrl: 'http://localhost:8080'}) $log.debugEnabled(true) diff --git a/jest.config.js b/jest.config.js index 6e6bf3159..26bbfbe58 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,10 +1,10 @@ module.exports = { - testMatch: [ "**/__tests__/**/*.[jt]s?(x)"], + testMatch: [ "**/__tests__/**/*.[jt]s?(x)"], // testMatch: ['/svelte/__tests__/specs/**/*.spec.js'], transform: { '^.+\\.m?(j|t)s$': 'babel-jest', - // '^.+\\.svelte$': ['svelte-jester', {preprocess: true}], - '^.+\\.svelte$': ['svelte-jester'], + '^.+\\.svelte$': ['svelte-jester', {preprocess: true}], + // '^.+\\.svelte$': ['svelte-jester'], /** * transform any svelte components in node_modules with svelte-jester */ @@ -17,6 +17,6 @@ module.exports = { }, setupFilesAfterEnv: [ '@testing-library/jest-dom/extend-expect', - "/jest.setup.js" + // "/jest.setup.js" ] -}; \ No newline at end of file +}; diff --git a/package.json b/package.json index fca564acd..6f159b31d 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "dayjs": "^1.10.6", "dequal": "^2.0.2", "dotenv": "^10.0.0", + "esm": "^3.2.25", "fast-safe-stringify": "^2.0.8", "fastclick": "^1.0.6", "feather-icons": "^4.28.0", @@ -86,6 +87,7 @@ "@babel/plugin-proposal-optional-chaining": "^7.7.5", "@babel/plugin-transform-react-jsx": "^7.14.9", "@babel/preset-env": "^7.15.6", + "@babel/preset-typescript": "^7.16.0", "@fullhuman/postcss-purgecss": "^1.3.0", "@testing-library/jest-dom": "^5.14.1", "angular-mocks": "1.6.10", diff --git a/src/dataApi/AppConfigApi.js b/src/dataApi/AppConfigApi.js index b86cb8b3d..e7a845f7c 100644 --- a/src/dataApi/AppConfigApi.js +++ b/src/dataApi/AppConfigApi.js @@ -1,4 +1,4 @@ -import kyApi from './kyClient' +import kyApi from './kyApi' class LocalCache { _values = {} @@ -45,7 +45,7 @@ export class AppConfigApi { if (_cache.contains(key)) { return _cache.get(key) } else { - const cfg = await kyApi.client.get(key).json() + const cfg = await kyApi.ky.get(key).json() _cache.set(key, cfg) return cfg } diff --git a/src/dataApi/DataQuery.js b/src/dataApi/DataQuery.js new file mode 100644 index 000000000..a448edd45 --- /dev/null +++ b/src/dataApi/DataQuery.js @@ -0,0 +1,28 @@ +/* eslint-disable no-unused-vars */ +import _ from 'lodash' + +export default class DataQuery { + //the api endpoint key + dataApi = "" + + // the q map or text + q + + //qSearch if both using the map criteria in q and a text fuzzy search + qSearch + + //initSearch is the initialSearch criteria + initSearch + + // when grid is for child or detail data, restrictSearch is what to filter it by, + // for example is showing invoices for customer then restrictSearch might be set to {custId:123} + restrictSearch + + //sort map + sort + + //page info + max = '20' + + page = '1' +} diff --git a/src/dataApi/ItemStore.js b/src/dataApi/ItemStore.js new file mode 100644 index 000000000..07c8972c1 --- /dev/null +++ b/src/dataApi/ItemStore.js @@ -0,0 +1,85 @@ +import cloneDeep from 'lodash/cloneDeep' + +/* + * Generic item store that can be extended and used for anything. + * loosely based on the Storage API but with errors + * https://developer.mozilla.org/en-US/docs/Web/API/Storage + */ + +const itemStoreState = function() { + return { + items: [], + activeItem: {}, + errors: [] // expects objects with at least a message [{ message: 'fubar' }] + } +} + +const ItemStore = { + state: itemStoreState(), + + setItems(items) { + this.state.items = items + }, + + addItem(changes) { + this.state.items.push(Object.assign({}, changes)) + return Promise.resolve() + }, + + updateItem(item, changes) { + Object.assign(item, changes) + return Promise.resolve() + }, + + updateAll(changes) { + for(const item of this.state.items) { + this.updateItem(item, changes) + } + }, + + removeItem(item) { + this.state.items.splice(this.state.items.indexOf(item), 1) + return Promise.resolve() + }, + + setActiveItem(item) { + this.state.activeItem = item + }, + //clears the items, active and errors + clear() { + this.items = [] + this.activeItem = {} + this.errors = [] + }, + + updateActiveItem(changes) { + this.updateItem(this.activeItem, changes) + }, + //sets the activeItem and then returns a clone for editing + editActiveItem(item) { + let aitem = (item === false) ? {} : item + this.setActiveItem(aitem) + return cloneDeep(this.state.activeItem) + }, + + setErrors(errors) { + this.state.errors = errors + }, + //clears and then sets a single error message into the errors array + setErrorMessage(message) { + this.state.errors = [{message: message}] + }, + //gets the first error message + getErrorMessage() { + if (this.state.errors[0] != null) { + return this.state.errors[0].message + } + }, + + clearErrors() { + this.state.errors = [{message: ''}] + } +} + +export default ItemStore + diff --git a/src/dataApi/MemDataApi.js b/src/dataApi/MemDataApi.js index 25ebef2f4..d685fb2ee 100644 --- a/src/dataApi/MemDataApi.js +++ b/src/dataApi/MemDataApi.js @@ -64,11 +64,12 @@ class MemDataApi { // console.log("search p", p) // console.log("search flds", flds) let list = await this.data() - const isSearch = p && (p._search === 'true' || p._search === true || p.q) + const isSearch = p && (p.q || p.qSearch) + if (isSearch) list = this.filter(list, p) if (p.sort) { const sortobj = p.sort.split(',').reduce((acc, item) => { - const sortar = item.trim().split(' ') + const sortar = item.trim().split(':') acc.sort.push(sortar[0]) acc.order.push(sortar[1]) return acc @@ -90,24 +91,26 @@ class MemDataApi { } filter(list, params) { - // console.log("filter params", params) + console.log("filter params", params) let flist = list - const filters = params.filters ? JSON.parse(params.filters) : null + // const filters = params.filters ? JSON.parse(params.filters) : null // const q = params.q ? JSON.parse(params.q) : null let q = params.q; - if (filters) { - // const filters = JSON.parse(params.filters) - if (filters.qSearch) { - flist = this.searchAny(list, filters.qSearch) - } else { - flist = this.qbe(list, filters) - } - } else if (q) { + + if (q) { if (q.startsWith('{') || q.startsWith('"') || q.startsWith('[')){ - q = JSON.parse(params.q) + let qParsed = JSON.parse(params.q) + if(qParsed['$qSearch']){ + flist = this.qSearch(list, qParsed['$qSearch']) + } else{ + flist = _.isPlainObject(qParsed) ? this.qbe(list, qParsed) : this.qSearch(list, qParsed) + } + } else { + flist = this.qSearch(list, q) } - flist = _.isPlainObject(q) ? this.qbe(list, q) : this.searchAny(list, q) - console.log("filter q flist", flist) + // console.log("filter q flist", flist) + } else if(params.qSearch){ + flist = this.qSearch(list, params.qSearch) } return flist } @@ -191,7 +194,7 @@ class MemDataApi { } } - searchAny(arr, searchKey) { + qSearch(arr, searchKey) { return arr.filter(obj => hasSomeDeep(obj, searchKey)) } diff --git a/src/dataApi/RestDataApi.js b/src/dataApi/RestDataApi.js index 77a3fac4b..c8286eec5 100644 --- a/src/dataApi/RestDataApi.js +++ b/src/dataApi/RestDataApi.js @@ -1,24 +1,27 @@ -import ky from 'ky' +import kyApi from './kyApi' +import prune from '../utils/prune'; /** - * This common wrapper around RESTful resource + * A common wrapper around RESTful resource */ export default class RestDataApi { /** * Creates a new SessionStorage object * * @param prefixUrl The endpoint url prefix ex: /api or http://foo.com/api + * @param customKyApi Allows to override with custom kyApi */ - constructor(endpoint, kyApi) { + constructor(endpoint, customKyApi) { // this.prefixUrl = prefixUrl this.endpoint = endpoint // this.api = ky.create({prefixUrl: prefixUrl}); this._idProp = 'id' - this.kyApi = kyApi + this.kyApi = customKyApi || kyApi } + // getter makes sure it always pull the current kyApi.ky get api(){ - return this.kyApi.client || ky + return this.kyApi.ky } /** @@ -27,27 +30,30 @@ export default class RestDataApi { * @param {*} params */ async search(params) { - //turn q into string if its an object - if(_.isObject(params.q)) params.q = JSON.stringify(params.q) - - const opts = { searchParams: params } - // console.log("query opts", opts) + let cleanParams = this.setupQ(params) + const opts = { searchParams: cleanParams } const data = await this.api.get(this.endpoint, opts).json() return data } - // async picklist(params) { - if(_.isObject(params.q)) params.q = JSON.stringify(params.q) - const opts = { searchParams: params } - // if (params) { - // opts = { searchParams: { q: params ? JSON.stringify(params) : '' } } - // } - // console.log("query opts", opts) + let cleanParams = this.setupQ(params) + const opts = { searchParams: cleanParams } const data = await this.api.get(`${this.endpoint}/picklist`, opts).json() return data } + // prunes params and stringifies the q param if exists + setupQ(params){ + let prunedParms = prune(params) + let q = prunedParms.q + if(_.isObject(q)) prunedParms.q = JSON.stringify(q) + //stringify sort and remove the + let sort = prunedParms.sort + if(_.isObject(sort)) prunedParms.sort = JSON.stringify(sort).replace(/{|}|"/g, '') + return prunedParms + } + /** Returns a promise for the item with the given identifier */ async get(id) { const item = await this.api.get(`${this.endpoint}/${id}`).json() diff --git a/src/dataApi/MemDataApi.test.js b/src/dataApi/__tests__/MemDataApi.spec.js similarity index 82% rename from src/dataApi/MemDataApi.test.js rename to src/dataApi/__tests__/MemDataApi.spec.js index f27d81fc6..d04ee6656 100644 --- a/src/dataApi/MemDataApi.test.js +++ b/src/dataApi/__tests__/MemDataApi.spec.js @@ -1,6 +1,6 @@ /* eslint-disable */ import _ from 'lodash' -import MemDataApi from './MemDataApi' +import MemDataApi from '../MemDataApi' describe('MemDataApi', () => { const dataJson = `[ @@ -13,13 +13,13 @@ describe('MemDataApi', () => { const api = new MemDataApi(JSON.parse(dataJson)) describe('qbe', function() { - it('query by example simple single', function() { + test('query by example simple single', function() { const result = api.qbe(data, {refnum: '762', amount: 3240.77}) // console.log("result", result) expect(result.length).toEqual(1) }) - it('playing with isMatchWith', function() { + test('playing with isMatchWith', function() { var match = {refnum: '762', amount: 3240.77, customer:{id:7}} @@ -38,13 +38,12 @@ describe('MemDataApi', () => { describe('searching', function() { - it('search', async function() { + test('search', async function() { const params = { max: 20, order: "asc", page: 1, sort: "id", - _search: false } const result = await api.search(params) // console.log("result", result) @@ -54,28 +53,27 @@ describe('MemDataApi', () => { expect(result.total).toEqual(1) }) - it('q search', async function() { + test('q is string', async function() { const params = { max: 20, order: "asc", page: 1, sort: "id", - _search: true, - filters: '{"qSearch":"762"}' + q: '762' } const result = await api.search(params) expect(result.data.length).toEqual(1) }) - it('filter qSearch', function() { + test('q with qSearch', function() { const params = { - filters: '{"qSearch":"762341"}' + q: '{"$qSearch":"762341"}' } const result = api.filter(data, params) expect(result.length).toEqual(1) }) - it('q search simple', async function() { + test('q search text', async function() { const params = { q: "762341" } @@ -83,14 +81,22 @@ describe('MemDataApi', () => { expect(result.data.length).toEqual(1) }) - it('searchAny function', function() { - const result = api.searchAny(data, "762341") + test('qSearch param', async function() { + const params = { + qSearch: "762341" + } + const result = await api.search(params) + expect(result.data.length).toEqual(1) + }) + + test('qSearch function', function() { + const result = api.qSearch(data, "762341") expect(result.length).toEqual(1) }) }) describe('picklist', function() { - it('should return paged data', async function() { + test('should return paged data', async function() { const result = await api.picklist() //console.log("result", result) expect(result.data.length).toEqual(4) diff --git a/src/dataApi/kyClient.js b/src/dataApi/kyApi.js similarity index 81% rename from src/dataApi/kyClient.js rename to src/dataApi/kyApi.js index d7a8de07a..6ad803024 100644 --- a/src/dataApi/kyClient.js +++ b/src/dataApi/kyApi.js @@ -4,10 +4,7 @@ import globalLoader from '../tools/globalLoader' // we wrap it like this so we can import the same instance // and when setClientConfig it will set it up and then every where its used -// can ref it with kyApi.client and it will get the same reference -const kyApi = { - client:{} -} +// can ref it with kyApi.ky and it will get the same reference const defaultKy = ky.extend({ hooks: { @@ -23,8 +20,12 @@ const defaultKy = ky.extend({ } }) +const kyApi = { + ky: defaultKy +} + export const setClientConfig = (config) => { - kyApi.client = defaultKy.extend(config) + kyApi.ky = defaultKy.extend(config) } export default kyApi diff --git a/src/gridz/GridCtrl.js b/src/gridz/GridCtrl.js index e3d4a4950..46f2564cd 100644 --- a/src/gridz/GridCtrl.js +++ b/src/gridz/GridCtrl.js @@ -1,7 +1,7 @@ /* eslint-disable no-unused-vars */ -import { makeLabel } from '../utils/labelMaker' +import { makeLabel } from '../utils/nameUtils' import { xlsData, csvData } from './excelExport' -import flattenObject from './flattenObject' +import flattenObject from '../utils/flattenObject' import toast from '../tools/growl' import _ from 'lodash' @@ -9,6 +9,7 @@ export default class GridCtrl { highlightClass = 'ui-state-highlight' systemColumns = ['cb', '-row_action_col'] isDense = false + showSearchForm = false defaultCtxMenuOptions = { edit: { @@ -28,7 +29,6 @@ export default class GridCtrl { const $jqGrid = $(jqGridElement) this.jqGridEl = $jqGrid - this.$gridWrapper = $(gridWrapper) if (!this.gridId && !_.isNil(opts.gridId)) { @@ -36,6 +36,14 @@ export default class GridCtrl { } $jqGrid.attr('id', this.gridId) + let optsToMerge = _.pick(opts, [ + 'showSearchForm', 'dataApi', 'initSearch', 'restrictSearch', 'contextMenuClick' + ]) + _.mergeWith(this, optsToMerge, (obj, optVal) => { + //dont merge val if its null + return optVal === null ? obj : undefined + }) + // pager ID setup if (opts.pager !== false) { const pagerId = `${this.gridId}-pager` @@ -59,10 +67,6 @@ export default class GridCtrl { opts.datatype = (params) => this.gridLoader(params) } - if (!_.isNil(opts.dataApi)) { - this.dataApi = opts.dataApi - } - this.setupColModel(opts) this.setupCtxMenu(opts) // this.setupDataLoader(gridOptions) @@ -73,7 +77,7 @@ export default class GridCtrl { //initialize the grid the jquery way initGridz(){ - console.log({opt: this.gridOptions}) + // console.log({opt: this.gridOptions}) this.jqGridEl.gridz(this.gridOptions) // setupFilterToolBar(options) } @@ -240,9 +244,11 @@ export default class GridCtrl { jqGridEl.jqGrid('hideCol', colSetup.hiddenColumns) } - contextMenuClick = (model, menuItem) => { - return this.listCtrl.fireRowAction(model, menuItem) - } + // contextMenuClick = (model, menuItem) => { + //listCtrl can pass the listener + // return this.contextMenuClickAction(model, menuItem) + //return this.listCtrl.fireRowAction(model, menuItem) + // } // Updates the values (using the data array) in the row with rowid. // The syntax of data array is: {name1:value1,name2: value2...} @@ -255,7 +261,7 @@ export default class GridCtrl { const prevData = this.getRowData(id) if (!_.isNil(prevData)) { // retrieve a list of removed keys - let diff = _.difference(_.keys(prevData), _.keys(flatData)) + let diff = _.difference(Object.keys(prevData), Object.keys(flatData)) // filter out restricted (private) columns like `-row_action_col` const restrictedColumns = key => !key.match(/^-/) @@ -382,11 +388,12 @@ export default class GridCtrl { search: this.hasSearchFilters(filters), postData: {} } - if (filters) params.postData.q = JSON.stringify(filters) - if (queryText || queryText === '') params.postData.q = queryText + if (filters) params.postData.q = filters + if (queryText || queryText === '') params.postData.qSearch = queryText this.setParam(params) await this.reload() } catch (er) { + //XXX should not swallow errors console.error('search error', er) } } @@ -397,7 +404,7 @@ export default class GridCtrl { if (_.isNil(value)) { continue } if (typeof value === 'string') { - if (_.trim(value) !== '') { return true } + if (value.trim() !== '') { return true } } else { return true } @@ -406,21 +413,42 @@ export default class GridCtrl { } /** + * The main loader for the grid. + * * @param {*} p the params to send to search - * @param {*} searchModel if passed in this will get converted to json string and override whats in q + * @param {*} searchModel if passed in this will get merged in whats in q */ - async gridLoader(p, searchModel={}) { + async gridLoader(p, searchModel) { this.toggleLoading(true) try { - // fix up sort - if (p.sort && p.order) p.sort = `${p.sort} ${p.order}` - if (!p.sort) delete p.sort - delete p.order + //we use the sortMap that constructed in jq.gridz so remove the sort and order + delete p.order; delete p.sort; + let sortMap = this.getParam('sortMap') + console.log('sortMap', sortMap) + if(sortMap){ + p.sort = sortMap + } + // to be able to set default filters on the first load - const q = p.q ? JSON.parse(p.q): {} - const permanentFilters = this.listCtrl?.permanentFilters || {} - const initSearch = this.listCtrl?.initSearch || {} - p.q = JSON.stringify({...initSearch, ...q, ...searchModel, ...permanentFilters}) + let q = p.q + if(_.isString(q) && !_.isEmpty(q)){ + if (q.trim().indexOf('{') === 0) { + q = JSON.parse(q) + } else { + q = {'$qSearch': q} + } + } + // when grid is for child or detail data, restrictSearch is what to filter it by, + // for example is showing invoices for customer then restrictSearch might be set to {custId:123} + const restrictSearch = this.restrictSearch || {} + const initSearch = this.initSearch || {} + const search = _.merge(initSearch, searchModel || {}) + q = {...search, ...q, ...restrictSearch} + + //now if its not empty set it back to p + if(!_.isEmpty(q)){ + p.q = q + } const data = await this.dataApi.search(p) this.addJSONData(data) } catch (er) { @@ -678,16 +706,6 @@ export default class GridCtrl { } } - setupDataLoader(options) { - // Log.debug(`[agGrid] initializing '${alias}' with`, options) - - // assign the url - if (!(!_.isNil(options.url)) && (!_.isNil(options.path))) { - options.url = this.pathWithContext(options.path) - } - - } - setupColModel(options) { options.colModel.forEach((col, i) => { if (!col.label) col.label = makeLabel(col.name) @@ -702,10 +720,10 @@ export default class GridCtrl { beforeSearch() { const postData = this.jqGridEl.jqGrid('getGridParam', 'postData') const defaultFilters = postData.defaultFilters || postData.filters - const filters = (_.extend(JSON.parse(defaultFilters), (_.pick(postData, (value, key) => !['page', 'filters', 'max', 'sort', 'order', 'nd', '_search'].includes(key))))) + const filters = (_.extend(JSON.parse(defaultFilters), (_.pick(postData, (value, key) => !['page', 'filters', 'max', 'sort', 'order'].includes(key))))) filters.firstLoad = false postData.defaultFilters = defaultFilters - postData.filters = JSON.stringify(filters) + postData.filters = filters } }) } diff --git a/src/gridz/jq.formatters.js b/src/gridz/jq.formatters.js index f01141f80..1f5cd9e71 100644 --- a/src/gridz/jq.formatters.js +++ b/src/gridz/jq.formatters.js @@ -1,5 +1,4 @@ /* eslint-disable no-unused-vars */ -import _ from 'lodash' import { isoDateToDisplay } from '../utils/dateSupport' // Extra formatters for jqGrid diff --git a/src/gridz/jq.gridz.js b/src/gridz/jq.gridz.js index 5e3d574b3..90e63022a 100644 --- a/src/gridz/jq.gridz.js +++ b/src/gridz/jq.gridz.js @@ -31,6 +31,16 @@ class Gridz { return this.responsiveResize() } + // Sets the given grid parameter + setGridParam(params, overwrite) { + this.gridEl.setGridParam(params, overwrite) + } + + // Sets the given grid parameter + getGridParam(name) { + this.gridEl.getGridParam(name) + } + getOptions(options) { options = $.extend({}, $.fn.gridz.defaults, options) @@ -67,37 +77,18 @@ class Gridz { if (this.options.multiSetSelection) { return this.memoizeSelectedRows() } }.bind(this) - // By default free-jqrid prepared sorting properties with next pattern - // sortName = columnName(id, name, etc) order(asc|desc), next column order of the last column name is in `order` parametr - // Example: if user first sorted by name and then by id sort params will be look like {sortName: 'name asc, id', order: 'asc'} - // Due to the fact that if id(or other unique) field is on the first place, the other sorting wont have any sense - // `sortLast` option is added to move unique column to the last place - // Example: if user first sorted by id and then by name sort params will be look like {sortName: 'name asc, id', order: 'asc'} - options.onSortCol = (sortname, x, order) => { - if (options.multiSort) { - // console.log('onSortCol sortname order', sortname, order) - const id = options.sortLast || 'id' - if (sortname.indexOf(id) > -1) { - sortname = sortname + ` ${order}` - const sortArray = sortname.split(',') - const res = [] - let sort = null - const idRegex = new RegExp(`(${id}[ ]+(asc|desc))`) - _.each(sortArray, function(it) { - it = it.trim() - if (_.isNil(idRegex.exec(it))) { - return res.push(it) - } else { - return sort = it.split(' ') - } - }) - if (sort) { res.push(sort[0]) } - sortname = res.join(',') - this.gridEl.jqGrid('setGridParam', { sortname }) - if (sort) { return this.gridEl.jqGrid('setGridParam', { order: sort[1] }) } - } - } - } + /** + * jqgrid default sort is a bit goofy on multiple. + * sortname will look like 'name asc, num' with the order for num in the order field. so last col will have its + * sort in the order prop and the others will be seperated by space in the sortname. + * on multiple sorts if id is in the list then other columns wont have any effect + * so we parse it out and remove it + * + * @param {*} sortname the sort name with the direction seperated by space when its multiSort + * @param {*} x the column index that was last clicked + * @param {*} order asc|desc for the last column sorted. + */ + options.onSortCol = this.onSortCol // If true - provides a possibility to select multiple sets of records with "shift" key. // Previously selected group(s) will not be unselected. @@ -111,6 +102,60 @@ class Gridz { return options } + /** + * jqgrid default sort is a bit goofy on multiple. + * sortname will look like 'name asc, num' with the order for num in the order field. so last col will have its + * sort in the order prop and the others will be seperated by space in the sortname. + * on multiple sorts if id is in the list then other columns wont have any effect + * so we parse it out and remove it + * + * @param {*} sortname the sort name with the direction seperated by space when its multiSort + * @param {*} x the column index that was last clicked + * @param {*} order asc|desc for the last column sorted. + */ + onSortCol = (sortname, x, order) => { + //dont do anything if its off + if(!sortname) { + this.setGridParam({ sortMap:{} }, true) + return + } + // console.log("grid sortname", sortname) + // console.log("grid sort x", x) + // console.log("grid order", order) + //no put it alltogether so we can split and make it a map form + let sortJoined = sortname + ` ${order}` + const sortMap = {} //will get populated at the end + const sortArray = sortJoined.split(', ') + const hasId = sortArray.find(el => el.startsWith('id')) + console.log("sortArray", sortArray) + + // see above notes, we remove the id sort so its doesn't cause problems when its part of multi + if (this.options.multiSort && sortArray.length > 1 && hasId) { + //will get populated without the id + const newSortArray = [] + sortJoined.split(',').forEach(sortCol => { + if(!sortCol.includes('id')) newSortArray.push(sortCol.trim()) + }) + sortArray = newSortArray + } + + sortArray.forEach(sortCol => { + let sortArray = sortCol.split(' ') + sortMap[sortArray[0]] = sortArray[1] + }) + + //now clean up the defaults that jqGrid expects using the order param + //remove last item and put order part back in sortorder + let lastItem = sortArray.pop().split(' ') + //put it back on with the order + sortArray.push(lastItem[0].trim()) + let sortorder = lastItem[1].trim() + //id back on to the end of it if its there + sortname = sortArray.join(', ') + this.setGridParam({ sortname, sortorder, sortMap }) + + } + /* stuff to do after the grid is completed loading and rendering */ @@ -132,7 +177,7 @@ class Gridz { } = this.gridEl[0] // get id of the previous selected row - const startId = this.gridEl.jqGrid('getGridParam', 'selrow') + const startId = this.getGridParam('selrow') const isCheckBox = $(e.target).hasClass('cbox') if (!e.ctrlKey && !e.shiftKey && !e.metaKey && !isCheckBox) { @@ -175,7 +220,7 @@ class Gridz { memoizeSelectedRows() { const selectedRows = this.selectedRowIds - return _.each(this.gridEl.jqGrid('getGridParam', 'selarrrow'), function(id) { + return _.each(this.getGridParam('selarrrow'), function(id) { if (!(Array.from(selectedRows).includes(id))) { return selectedRows.push(id) } }) } @@ -185,12 +230,12 @@ class Gridz { } onSelectRow(rowid, isChecked, e) { - if (this.gridEl.jqGrid('getGridParam', 'agRowNumber')) { + if (this.getGridParam('agRowNumber')) { // Add number of selected row in grid(nmber for all pages) const ids = this.gridEl.getDataIDs() let text = '' // check if only one row is selected - if (this.gridEl.jqGrid('getGridParam', 'selarrrow').length === 1) { + if (this.getGridParam('selarrrow').length === 1) { // add to the grid footer number of the row in total for all pages const rowNum = ((this.gridEl.jqGrid('getGridParam', 'page') - 1) * this.gridEl.jqGrid('getGridParam', 'rowNum')) + ids.indexOf(rowid) + 1 text = `Current row # ${rowNum} | ` @@ -213,7 +258,7 @@ class Gridz { grid.jqGrid('resetSelection') grid.jqGrid('setSelection', rowid) const selectedRows = this.selectedRowIds - const selected = grid.jqGrid('getGridParam', 'selarrrow') + const selected = this.getGridParam('selarrrow') _.each(selectedRows, function(id) { if (!(Array.from(selected).includes(id))) { return grid.jqGrid('setSelection', id) } }) diff --git a/src/ng/controls/formly/helpers.test.js b/src/ng/__tests__/formly-helpers.spec.js similarity index 98% rename from src/ng/controls/formly/helpers.test.js rename to src/ng/__tests__/formly-helpers.spec.js index 71327de23..a95b1a438 100644 --- a/src/ng/controls/formly/helpers.test.js +++ b/src/ng/__tests__/formly-helpers.spec.js @@ -1,7 +1,6 @@ /* eslint-disable */ -import agMod from '~/angle-grinder' import _ from 'lodash' -import {transformFields} from './helpers' +import {transformFields} from '../controls/formly/helpers' describe('transformFields', () => { let testOpts = { diff --git a/src/ng/common/directives/agMaxLines.js b/src/ng/common/directives/agMaxLines.js index 581daf3e7..26ee5493f 100644 --- a/src/ng/common/directives/agMaxLines.js +++ b/src/ng/common/directives/agMaxLines.js @@ -1,7 +1,7 @@ import angular from 'angular' import commonModule from '../commonModule' -import { isFalsy } from '../../../utils/isFalsy' +import { isFalsy } from '../../../utils/truthy' var app = angular.module(commonModule) // Validates text area to have not more then specified number of lines diff --git a/src/ng/controls/AgBaseControl.js b/src/ng/controls/AgBaseControl.js index 61fcc67fc..dd67d2037 100644 --- a/src/ng/controls/AgBaseControl.js +++ b/src/ng/controls/AgBaseControl.js @@ -1,4 +1,5 @@ -import stringUtils from '../../utils/stringFormUtils' +import * as nu from '../../utils/nameUtils' + // import Log from '../../utils/Log' import _ from 'lodash' @@ -31,9 +32,9 @@ export default class AgBaseControl { // passing in a blank string to label will not be undefined, and is how to blank it out if (typeof this.label === 'undefined') { - this.label = stringUtils.parseWords(this.modelKey) + this.label = nu.parseWords(this.modelKey) } - this.placeholder = this.placeholder || (this.label || stringUtils.parseWords(this.modelKey)) + this.placeholder = this.placeholder || (this.label || nu.parseWords(this.modelKey)) // if its not passed in then create a unique id for this component if (!this.elementId) { diff --git a/src/ng/controls/ag-select-rest/select-rest.scss b/src/ng/controls/ag-select-rest/select-rest.scss index b119dfc34..4921ed8df 100644 --- a/src/ng/controls/ag-select-rest/select-rest.scss +++ b/src/ng/controls/ag-select-rest/select-rest.scss @@ -6,7 +6,7 @@ table.select-rest-result { text-align: left; &:nth-child(1) { - width: 60px; + width: 80px; } } } diff --git a/src/ng/controls/formly/helpers.js b/src/ng/controls/formly/helpers.js index 730489b46..749a223a5 100644 --- a/src/ng/controls/formly/helpers.js +++ b/src/ng/controls/formly/helpers.js @@ -1,5 +1,5 @@ import _ from 'lodash' -import { makeLabel } from '../../../utils/labelMaker' +import { makeLabel } from '../../../utils/nameUtils' export function transformFields(fields, ctrl) { // if its a plain object and first key starts with column and its a columns layout diff --git a/src/ng/controls/index.js b/src/ng/controls/index.js index 9edd8e5ed..889abbd02 100644 --- a/src/ng/controls/index.js +++ b/src/ng/controls/index.js @@ -21,9 +21,6 @@ import agAmount from './ag-amount' import agOkCancel from './ag-ok-cancel' import agXeditable from './xeditable' -// import InputPasswordComponent from './ag-password/input-password.component' -// import StringUtility from './string-utility'; - const MOD_NAME = 'ag.form.controls' export default MOD_NAME diff --git a/src/ng/controls/ui-select2/dataQuery.js b/src/ng/controls/ui-select2/dataQuery.js index b26ea4a58..d7042c149 100644 --- a/src/ng/controls/ui-select2/dataQuery.js +++ b/src/ng/controls/ui-select2/dataQuery.js @@ -1,10 +1,17 @@ import _ from 'lodash' export function setupData(opts, dataStoreApi) { - if (opts.dataApiKey) { - const dataApiKey = opts.dataApiKey - const dataApiParams = opts.dataApiParams - opts.data = { results: () => dataStoreApi[dataApiKey].picklist(dataApiParams) } + if (opts.dataApiKey){ + //if minimumInputLength is not set then load the whole dataset + if(!opts.minimumInputLength) { + const dataApiKey = opts.dataApiKey + const dataApiParams = opts.dataApiParams + opts.data = { results: () => dataStoreApi[dataApiKey].picklist({q: dataApiParams}) } + } + else { + opts.data = {} + opts.query = dataMinCharsQuery(opts, dataStoreApi) + } } // setup defaults for data if (opts.data) { @@ -15,37 +22,34 @@ export function setupData(opts, dataStoreApi) { // console.log('results', results) opts.data = { results: results } } - if (opts.displayFields) { - opts.data.text = opts.displayFields[0] - } - // if data.text is not set then default it to name (select2 defaults it to 'text') - if (opts.data.text === undefined) { - opts.data.text = 'name' + if(!opts.minimumInputLength){ + // assign special query that can handle promises and the lazy data loading + opts.query = dataQuery(opts) } - - // assign special query that can handle promises - opts.query = dataQuery(opts) } + } // copied in from select2 source and modified so it works when data.results is a Promise export function dataQuery(opts) { + // console.log(`***** dataQuery for ${opts.wtf}`, opts) let data = opts.data // data elements let getText // function used to retrieve the text portion of a data item that is matched against the search + const displayFields = opts.displayFields - const textField = data.text - if (!$.isFunction(textField)) { - if (Array.isArray(opts.displayFields)) { - getText = (item) => opts.displayFields.map(text => item[text]).join(' ') - } else { - getText = (item) => item[textField] - } + if (displayFields.length > 1 ) { + getText = (item) => displayFields.map(text => item[text]).join(' ') + } else { + getText = (item) => opts.text(item) } - if (!$.isFunction(data.results)) { + + // make results a function if its not + if (!$.isFunction(data?.results)) { const dres = data.results data.results = () => dres } + // make data a function if its not if ($.isFunction(data) === false) { const tmp = data data = function() { return tmp } @@ -53,14 +57,13 @@ export function dataQuery(opts) { // cache the data.results // let dataResults - return function(query) { + var t = query.term; var filtered = { results: [] }; var process opts.dataResults = opts.dataResults || data().results() if (t === '') { Promise.resolve(opts.dataResults).then(res => { let dta = res - console.log('dta', dta) // if its an object then assume it pager object with data key if (_.isPlainObject(res)) dta = res.data // add the selectAll option if enabled @@ -74,10 +77,6 @@ export function dataQuery(opts) { var group //, attr if (datum.children) { group = {} - // does searching on any attributes too - // for (attr in datum) { - // if (datum.hasOwnProperty(attr)) group[attr] = datum[attr] - // } group.children = [] datum.children.forEach(childDatum => process(childDatum, group.children)) if (group.children.length || query.matcher(t, getText(group), datum)) { @@ -89,12 +88,16 @@ export function dataQuery(opts) { } } } + Promise.resolve(opts.dataResults).then(res => { let dta = res + if(!res) console.log("dataQuery empty promise resolved ") + // console.log("**** resolved dataQuery options", opts) // if its an object then assume it pager object with data key if (_.isPlainObject(res)) dta = res.data - - dta.forEach(datum => process(datum, filtered.results)) + if(dta){ + dta.forEach(datum => process(datum, filtered.results)) + } query.callback(filtered) }) } @@ -120,3 +123,33 @@ export function convertSelect2Data(strArray, textFieldKey = 'name') { } return dataArr || strArray } + +// if minimumInputLength > 0 then query api as they type +export function dataMinCharsQuery(opts, dataStoreApi) { + // console.log(`***** dataMinCharsQuery for ${opts.wtf}`, opts) + let timeout + let quietMillis = opts.quietMillis || 500 + + opts.initSelection = angular.noop + // cache the data.results + // let dataResults + return function(query) { + window.clearTimeout(timeout) + // quietMillis to wait until done typing, + timeout = window.setTimeout(function() { + let q = query.term + const dataApiKey = opts.dataApiKey + let picklistQuery = () => dataStoreApi[dataApiKey].picklist({q: q }) + let dataResults = picklistQuery() + + Promise.resolve(dataResults).then(response => { + var filtered = { more:false, results: [] } + // if its an object then assume it pager object with data key + if (_.isPlainObject(response) && response.data){ + response.data.forEach(datum => filtered.results.push(datum)) + } + query.callback(filtered) + }) + }, quietMillis) + } +} diff --git a/src/ng/controls/ui-select2/select2Setup.js b/src/ng/controls/ui-select2/select2Setup.js new file mode 100644 index 000000000..d25d74b1f --- /dev/null +++ b/src/ng/controls/ui-select2/select2Setup.js @@ -0,0 +1,99 @@ +import _ from 'lodash' +import { setupData } from './dataQuery' +/** + * configures the row formatter based on option settings + * + */ +export default function select2Setup(opts, dataStoreApi) { + let defaults = { + allowClear: true, + fields: { + id: 'id', + text: 'name' + }, + displayFields: ['name'], //first item in array is default + dataVar: 'val', + showSelectAll: false, + multiple: false + } + _.defaultsDeep(opts, defaults) + // select2 needs placeholder if allowClear=true. + if (opts.allowClear && !opts.placeholder) { + opts.placeholder = ' ' + } + + opts.text = (item) => item[opts.data.text] + + if (opts.multiple) { + //setup multiple defaults + if (_.isUndefined(opts.closeOnSelect)) opts.closeOnSelect = false + } + //setup data + setupData(opts, dataStoreApi) + + // if modelType is object then will use the elm.select2('data') and will store the selected + // object(s) in the model as objects instead of as just the ids + + // when its on and input and its set to multiple then we will use 'data' so it creates and array of obbjecgs for + // selection and not array of ids + if (opts.useDataObject === undefined && opts.multiple) { + opts.useDataObject = true + } + + // don't do initSelection with useDataObject, its screws it up and preruns the promise for rest + if (!opts.initSelection && opts.useDataObject) opts.initSelection = function(element, callback) { } + // if initSelection is a boolean true then remove it so the default in Select2 can take over + // useful to set true on single selects when you only id and want to get the name display from select2 + if (opts.initSelection === true) delete opts.initSelection + + if (opts.useDataObject) { + opts.dataVar = 'data' + } + + const showSelectAll = opts.showSelectAll + const displayFields = opts.displayFields + + opts.text = function(e) { + return e[opts.fields.text] + } + + //function to underline matching text with what was typed + let markMatch = function(text, term, escapeMarkup){ + var markup=[]; + Select2.util.markMatch(text, term, markup, escapeMarkup); + return markup.join("") + } + + let formatMultiColumns = function(item) { + let displayTds = '' + displayFields.forEach( it => displayTds = `${displayTds} ${item[it]}` ) + var markup = ` + + ${displayTds} +
+ ` + return markup; + } + + //main format function + opts.formatResult = function(result, container, query, escapeMarkup) { + if (showSelectAll && result.id === 'selectAll') return selectAllMenu() + + if(displayFields.length > 1){ + return formatMultiColumns(result) + }else{ + return markMatch(opts.text(result), query.term, escapeMarkup) + } + } +} + +function selectAllMenu(){ + return ` + + + Select All   | + x Clear All + + ` +} + diff --git a/src/ng/controls/ui-select2/ui.select2.js b/src/ng/controls/ui-select2/ui.select2.js index b3539b531..bf2572e59 100644 --- a/src/ng/controls/ui-select2/ui.select2.js +++ b/src/ng/controls/ui-select2/ui.select2.js @@ -1,13 +1,9 @@ import angular from 'angular' import _ from 'lodash' -import { setupData } from './dataQuery' +import select2Setup from './select2Setup' require('Select2/select2.js') -/** - * Copied from https://github.com/angular-ui/ui-select2 and modifed for es6 modules. - * TODO still need to fix failing tests - */ /** * Enhanced Select2 Dropmenus * @@ -24,22 +20,6 @@ angular.module('ui.select2', []) require: 'ngModel', priority: 1, compile: function(tElm, tAttrs) { - // if its populated from a select element and not from options.data - - var isMultiple = angular.isDefined(tAttrs.multiple) - // console.log("api-key attr", tAttrs.apiKey) - var isSelectElm = tElm.is('select') - var watchSelectOptionsEl - - // Enable watching of the options dataset if its an html select - if (isSelectElm) { - const repeatOption = tElm.find('optgroup[ng-repeat], optgroup[data-ng-repeat], option[ng-repeat], option[data-ng-repeat]') - - if (repeatOption.length) { - const repeatAttr = repeatOption.attr('ng-repeat') || repeatOption.attr('data-ng-repeat') - watchSelectOptionsEl = jQuery.trim(repeatAttr.split('|')[0]).split(' ').pop() - } - } // var elname = tElm.attr('name') // for logging const log = function(msg, val) { @@ -48,71 +28,19 @@ angular.module('ui.select2', []) return { pre: function(scope, elm, attrs, ngModelCtrl) { - // instance-specific options - var defaults = { - allowClear: true - } - var opts = angular.extend({}, defaults, scope.$eval(attrs.uiSelect2)) - // select2 needs placeholder if allowClear=true. - if (opts.allowClear && !attrs.placeholder && !opts.placeholder) { - opts.placeholder = ' ' - } - // if ui-select2-data attribute is set then assign it - const dataAttr = scope.$eval(attrs.uiSelect2Data) - if (dataAttr) { - opts.data = dataAttr - } - - if (opts.multiple) { - isMultiple = true - if (_.isUndefined(opts.closeOnSelect)) opts.closeOnSelect = false - } - const apiKey = attrs.apiKey - if (apiKey) opts.dataApiKey = apiKey - setupData(opts, dataStoreApi) - - // if(attrs.uiSelect2Data) opts.data = scope.$eval(attrs.uiSelect2Data) - // if modelType is object then will use the elm.select2('data') and will store the selected - // object(s) in the ng-model as objects instead of as just the ids - // let useDataObject = false - let dataVar = 'val' - const idProp = opts.idProp ? opts.idProp : 'id' - - // uses elm.select2('val') when its a select and we want the id in the model not the object. - // when its on and input and its set to multiple then we will use 'data' so it creates and array of obbjecgs for - // selection and not array of ids - if (opts.useDataObject === undefined && !isSelectElm && isMultiple) { - opts.useDataObject = true - } - - // don't do initSelection with useDataObject, its screws it up and preruns the promise for rest - if (!opts.initSelection && opts.useDataObject) opts.initSelection = function(element, callback) { } - // if initSelection is a boolean true then remove it so the default in Select2 can take over - // useful to set true on single selects when you only id and want to get the name display from select2 - if (opts.initSelection === true) delete opts.initSelection - - if (opts.useDataObject) { - // useDataObject = true - dataVar = 'data' - } - log(`isSelectElm: ${isSelectElm} , isMultiple: ${isMultiple}, dataVar: ${dataVar}`) - if (isSelectElm) { - // Use {option.title} {/if} {/each} - {#if get($touched, name) && get($errors, name)} -
{get($errors, name)}
+ {#if _.get($touched, name) && _.get($errors, name)} +
{_.get($errors, name)}
{/if} diff --git a/svelte/Form/Form.svelte b/svelte/Form/Form.svelte index 457755057..a5ba2d142 100644 --- a/svelte/Form/Form.svelte +++ b/svelte/Form/Form.svelte @@ -5,8 +5,8 @@