diff --git a/.ember-cli b/.ember-cli deleted file mode 100644 index ee64cfe..0000000 --- a/.ember-cli +++ /dev/null @@ -1,9 +0,0 @@ -{ - /** - Ember CLI sends analytics information by default. The data is completely - anonymous, but there are times when you might want to disable this behavior. - - Setting `disableAnalytics` to true will prevent any data from being sent. - */ - "disableAnalytics": false -} diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 74e37b8..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,52 +0,0 @@ -'use strict'; - -module.exports = { - root: true, - parser: 'babel-eslint', - parserOptions: { - ecmaVersion: 2018, - sourceType: 'module', - ecmaFeatures: { - legacyDecorators: true, - }, - }, - plugins: ['ember'], - extends: [ - 'eslint:recommended', - 'plugin:ember/recommended', - ], - env: { - browser: true, - }, - rules: {}, - overrides: [ - // node files - { - files: [ - './.eslintrc.js', - './.prettierrc.js', - './.template-lintrc.js', - './ember-cli-build.js', - './index.js', - './testem.js', - './blueprints/*/index.js', - './config/**/*.js', - './tests/dummy/config/**/*.js', - ], - parserOptions: { - sourceType: 'script', - }, - env: { - browser: false, - node: true, - }, - plugins: ['node'], - extends: ['plugin:node/recommended'], - }, - { - // Test files: - files: ['tests/**/*-test.{js,ts}'], - extends: ['plugin:qunit/recommended'], - }, - ], -}; diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..ec540de --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1 @@ +b3af786cd704cf42664d3064b140aaf21d53d283 diff --git a/.gitignore b/.gitignore index da389ad..b07c80a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,29 +1,5 @@ -# See http://help.github.com/ignore-files/ for more about ignoring files. +# you definitely want this: +node_modules -# compiled output -/dist/ -/tmp/ - -# dependencies -/bower_components/ -/node_modules/ - -# misc -/.env* -/.pnp* -/.sass-cache -/.eslintcache -/connect.lock -/coverage/ -/libpeerconnection.log -npm-debug.log -yarn-error.log -testem.log - -# ember-try -.node_modules.ember-try/ -bower.json.ember-try -package.json.ember-try - -*~ -package-lock.json +.DS_Store +addon/dist diff --git a/.prettierrc.js b/.prettierrc.js deleted file mode 100644 index 534e6d3..0000000 --- a/.prettierrc.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports = { - singleQuote: true, -}; diff --git a/.template-lintrc.js b/.template-lintrc.js deleted file mode 100644 index f35f61c..0000000 --- a/.template-lintrc.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports = { - extends: 'recommended', -}; diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9b3dfab..0000000 --- a/.travis.yml +++ /dev/null @@ -1,61 +0,0 @@ ---- -language: node_js -node_js: - # we recommend testing addons with the same minimum supported node version as Ember CLI - # so that your addon works for all apps - - "12" - -dist: xenial - -addons: - chrome: stable - -cache: - directories: - - $HOME/.npm - -env: - global: - # See https://git.io/vdao3 for details. - - JOBS=1 - -branches: - only: - - master - # npm version tags - - /^v\d+\.\d+\.\d+/ - -jobs: - fast_finish: true - allow_failures: - - env: EMBER_TRY_SCENARIO=ember-canary - - include: - # runs linting and tests with current locked deps - - stage: "Tests" - name: "Tests" - script: - - npm run lint - - npm run test:ember - - - stage: "Additional Tests" - name: "Floating Dependencies" - install: - - npm install --no-package-lock - script: - - npm run test:ember - - # we recommend new addons test the current and previous LTS - # as well as latest stable release (bonus points to beta/canary) - - env: EMBER_TRY_SCENARIO=ember-lts-3.24 - - env: EMBER_TRY_SCENARIO=ember-lts-3.28 - - env: EMBER_TRY_SCENARIO=ember-release - - env: EMBER_TRY_SCENARIO=ember-beta - - env: EMBER_TRY_SCENARIO=ember-canary - - env: EMBER_TRY_SCENARIO=ember-default-with-jquery - - env: EMBER_TRY_SCENARIO=ember-classic - - env: EMBER_TRY_SCENARIO=embroider-safe - - env: EMBER_TRY_SCENARIO=embroider-optimized - -script: - - node_modules/.bin/ember try:one $EMBER_TRY_SCENARIO diff --git a/.watchmanconfig b/.watchmanconfig deleted file mode 100644 index e7834e3..0000000 --- a/.watchmanconfig +++ /dev/null @@ -1,3 +0,0 @@ -{ - "ignore_dirs": ["tmp", "dist"] -} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0b36c4b..11723af 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,24 +2,24 @@ ## Installation -* `git clone ` -* `cd my-addon` -* `npm install` +- `git clone ` +- `cd ember-data-table` +- `npm install` ## Linting -* `npm run lint` -* `npm run lint:fix` +- `npm run lint` +- `npm run lint:fix` ## Running tests -* `ember test` – Runs the test suite on the current Ember version -* `ember test --server` – Runs the test suite in "watch mode" -* `ember try:each` – Runs the test suite against multiple Ember versions +- `npm run test` – Runs the test suite on the current Ember version +- `npm run test:ember -- --server` – Runs the test suite in "watch mode" +- `npm run test:ember-compatibility` – Runs the test suite against multiple Ember versions ## Running the dummy application -* `ember serve` -* Visit the dummy application at [http://localhost:4200](http://localhost:4200). +- `npm run start` +- Visit the dummy application at [http://localhost:4200](http://localhost:4200). -For more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/). +For more information on using ember-cli, visit [https://cli.emberjs.com/release/](https://cli.emberjs.com/release/). diff --git a/MODULE_REPORT.md b/MODULE_REPORT.md deleted file mode 100644 index cf399c9..0000000 --- a/MODULE_REPORT.md +++ /dev/null @@ -1,56 +0,0 @@ -## Module Report -### Unknown Global - -**Global**: `Ember.Logger` - -**Location**: `tests/dummy/app/controllers/application.js` at line 44 - -```js - actions: { - test(row) { - Ember.Logger.info("Hi, you reached the test action for row: " + JSON.stringify(row)); - }, - menuTest() { -``` - -### Unknown Global - -**Global**: `Ember.Logger` - -**Location**: `tests/dummy/app/controllers/application.js` at line 47 - -```js - }, - menuTest() { - Ember.Logger.info("Hi, you reached the general menu test action"); - }, - selectionTest(selection, datatable) { -``` - -### Unknown Global - -**Global**: `Ember.Logger` - -**Location**: `tests/dummy/app/controllers/application.js` at line 51 - -```js - selectionTest(selection, datatable) { - datatable.clearSelection(); - Ember.Logger.info("Hi, you reached the selection test action for selection: " + JSON.stringify(selection)); - selection.forEach(function(item) { - item.set('age', item.get('age') + 1); -``` - -### Unknown Global - -**Global**: `Ember.Logger` - -**Location**: `tests/dummy/app/controllers/application.js` at line 57 - -```js - }, - clickRow(row) { - Ember.Logger.info("Custom row click action on item " + JSON.stringify(row)); - } - } -``` diff --git a/README.md b/README.md index 1ac7da2..59bb045 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Data table for EmberJS ### Add basic Ember Data Table -Find an adaptation of Ember Data Table for the design framework of your choice or implement a custom variant for your application. This tutorial uses `RawDataTable`. +Find an adaptation of Ember Data Table for the design framework of your choice or implement a custom variant for your application. This tutorial uses `RawDataTable`. Alternatively look at the examples in [the dummy app](/addon/tests/dummy/app). Generate a route for products first: @@ -22,38 +22,86 @@ The tutorial assumes a model exists with `label` and `price` which you can gener ember g model product label:string price:number ``` -Next you'll fetch content from the back-end using standard model hooks and query parameters. Extending from the provided Route and Controller is the shortest form. - +Next you'll fetch content from the back-end using standard model hooks and query parameters. For the route stored in `/app/routes/products/index.js` write: ```javascript -import DataTableRoute from 'ember-data-table/route'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import Route from '@ember/routing/route'; + +export default class DataTableRoute extends Route { + @service store; -export default class ProductsIndexRoute extends DataTableRoute { modelName = 'product'; + + queryParams = { + filter: { refreshModel: true }, + page: { refreshModel: true }, + size: { refreshModel: true }, + sort: { refreshModel: true }, + }; + + model(params) { + const options = { + sort: params.sort, + page: { + number: params.page, + size: params.size, + }, + }; + if (params.filter) { + options['filter'] = params.filter; + } + return this.store.query(this.modelName, options); + } + + @action + loading(transition) { + // eslint-disable-next-line ember/no-controller-access-in-routes + let controller = this.controllerFor(this.routeName); + + if(controller) { + controller.isLoadingModel = true; + + transition.finally(function () { + controller.isLoadingModel = false; + }); + } + + return true; // bubble the loading event + } } ``` For the controller stored in `/app/controllers/product/index.js` write: ```javascript -import DataTableController from 'ember-data-table/controller'; +import { tracked } from '@glimmer/tracking'; +import Controller from '@ember/controller'; -export default class ProductsIndexController extends DataTableController {} +export default class DataTableController extends Controller { + queryParams = ['size', 'page', 'filter', 'sort']; + + @tracked size = 10; + @tracked page = 0; + @tracked filter = ''; + @tracked sort = ''; + @tracked isLoadingModel = false; +} ``` These steps are the same for any Ember Data Table flavour, the following visualizes `RawDataTable`: ```hbs +``` + ### Overwrite the header labels Supply column headers by adding extra properties to the fields attribute, split by a colon. A single `_` gets replaced by a space and two underscores get replaced by a single underscore -``` +```hbs ``` +Alternatively, pass the field (key `attribute`) and header (key `label`) via an object. This is only possible when passing the fields via an array. +```hbs + +``` + + ## Discussions ### Why one big template file @@ -150,6 +221,9 @@ The default implementation will be used most often, but the end-user receives an ## Reference +### Serializer +Ember Data Table expects meta (pagination) information in a specific format. The provided [serializer](/addon/serializer.js) can be used in case of a JSONAPI, or it can be used as an inspiration for a custom serializer. + ### Arguments to Ember Data Table These arguments should be supported by specific design implementations too. @@ -158,65 +232,78 @@ These arguments should be supported by specific design implementations too. The passing of data from route and controller, and moving data back up. -- `@content` :: Data to be rendered. In case this has a `meta` +- `@content` :: Data (an array of items) to be rendered. In case this has a `meta` property, this is used as default to derive amount of results and back-end pagination offset. -- `@page` and `@updatePage` :: Indicates the current page number and the +- `@page` and `@updatePage` :: Set the current page number and the function called to update the current page. -- `@size` and `@updatePageSize` :: Indicates the current page size and +- `@size` and `@updatePageSize` :: Set the current page size and the function called to update the current page size. -- `@sort` and `@updateSort` :: Returns current sorting for data table - and a function to update the sorting. -- `@filter` and `@updateFilter` :: Supplies the user filter string and - the function to call for updating that string. +- `@sort` and `@updateSort` :: Set the current sorting for data table + and the function called to update the sorting. +- `@filter` and `@updateFilter` :: Set the user search string and + the function called to update that string. - `@total` :: The total amount of results across all pages. If not set, `@meta.count` or `@content.meta.count` is tried. -- `@isLoading` :: Truthy if the Data Table is currently loading data. +- `@isLoading` :: Whether to show the Data Table in its loading state. - `@meta` :: Meta may be provided in `@content.meta` or it may be - provided in a separate property. If supplied, it may be used to + provided in this property. If supplied, it may be used to determine the back-end pagination offset from - `@meta.links.first.number` (often `0` but sometimes `1`) and - amount of results as alternative to `@total` from `@meta.count`. + `@meta.links.first.number` (often `0` but sometimes `1`), pagination in + `@meta.pagination` and + amount of results from `@meta.count` as an alternative to `@total`. #### Ember Data Table visualization configuration How to show different things in Ember Data Table -- `@fields` :: List of fields to render with extra options. The fields are split by spaces. Splitting a field with a colon (`:`) makes the first element be the attribute and the second be the label. Use an `_` to render a space in the label. E.g.: `@fields="label:Name priceInEuros:Euro_price"`. -- `@sortableFields` :: List of fields by which the user may sort. - Fields should use the attribute names of `@fields` and are split by - spaces. By default all fields are sortable. Set to an empty list to - disable sorting. +- `@fields` :: Array of objects/strings or space-separated string of fields to render (in given order) with extra options. Each field can consists of two parts, split by a colon (`:`) for string syntax. The first part is the attribute (key `attribute`, in string syntax `_` are rendered as spaces), the second an optional label (key `label`). If no label is provided, the attribute is used as the label. E.g.: `@fields="label:Name priceInEuros:Euro_price available"` or `@fields={{array "label:Name" (hash attribute="priceInEuros" label="Euro price") "available"}}`. +Other keys can also be passed when using the object syntax. These take precedence over other configurations to set up visualization logic: + - `attribute`: mandatory attribute this configuration is meant for + - `label`: label for the attribute, used in the column heading + - `isSortable`: set to false if this field should not be sortable. Default is true. Setting `sortParameters` or `@sortableFields` will take precedence. + - `sortParameters`: directly pass the sorting parameters, same form as return value of `@attributeToSortParams`. + - `hasCustomHeader`: set to true to render this field header via the `:data-header` named block, like `@customHeaders`. + - `customHeaderComponent`: Pass a component to use for rendering this header, like `@customHeaders`. + - `isCustom`: set to true to render this field via the `:data-cell` named block, like `@customFields`. + - `customFieldComponent`: Pass a component to use for rendering this field's cells, like `@customFields`. + +- `@sortableFields` :: Array or space-separated string of fields by which the user may sort. + Fields should use the attribute names of `@fields`. By default all fields are sortable. Set to an empty list or empty string to disable sorting. - `@noDataMessage` :: Custom message to show when no data is available. The `:no-data-message` block can be used as an alternative to provide styling. - `@enableSearch` :: Set to false to disable search in the table. +- `@searchPlaceholder` :: Custom placeholder text for the search input box. Defaults to 'Search input'. - `@enableLineNumbers` :: Set to truthy to show line numbers in the table. -- `@links` :: Each row may contain a number of links. Different links - are split by a space in the configuration. Each link consists of one - to three parts split by a colon. The first part is the route, the - second is the label, the third is an icon to use instead of the label - if supported (screen readers should see the label still). E.g.: - `@links="products.edit:edit:pencil products.show:open:file-earmark-richtext"`. +- `@sizes` :: Array or space-separated string of page size choices that should be shown in the pagination block. Defaults to `[5, 10, 25, 50, 100]`. Set to an empty list or empty string to hide. +- `@links` :: Array of objects/strings or a space-separated string of links with extra options. + Each row may contain a number of clickable links rendered in a separate column. Each link consists of one + to three parts, split by a colon for string syntax. The first part is the route (key `route`), the + second is the label (key `label`, in string syntax `_` are rendered as spaces), the third is an icon (key `icon`) to use instead of the label + if supported (screen readers should see the label still). E.g. (both equivalent): + `@links="products.edit:edit:pencil products.show:open:file-earmark-richtext"` + `@links={{array (hash route="products.edit" label="edit" icon="pencil") "products.show:open:file-earmark-richtext"}}`. Note that only the route is required in which case the label is - derived and no icon is shown. The link by default receives the `id` - of the item but this is configurable using the `@linksModelProperty` + derived and no icon is shown. By default the link receives the `id` + of the item, but is configurable using the `@linksModelProperty` attribute (see below). -- `@customHeaders` :: List of attributes for which a custom header will - be rendered through the `:data-header` named block. Each of the - attributes mentioned here won't render the default header but will - instead dispatch to the named block. Check which attribute is being - rendered in the named block to render the right label. Verify in the - implementation you override which actions are set on the columns how - to support sorting if needed. +- `@customHeaders` :: An object (hash) or an array/space-separated string of attributes. + When passing an array/space-separated string, the attributes will be rendered through the `:data-header` named block, instead of rendering the default header. + When passing an object, set as key the attribute and value the component to use for rendering. The component will receive in `@header` the same hash given to the `:data-header` block. If value is empty, the attribute will be rendered through the `:data-header` named block. + For the `:data-header` named block, check which attribute is being rendered to render the right label. + Check in the implementation you override how sorting is supported, if sorting is needed for this header. ```hbs <:data-header as |header|> {{#if (eq header.attribute "label")}} @@ -224,20 +311,21 @@ How to show different things in Ember Data Table {{else if (eq header.attribute "priceInEuros")}} Here is my price! {{/if}} - + ``` -- `@customFields` :: List of attributes for which the fields will - receive a custom rendering. This will render the individual cell - values based on the `:data-cell` custom block. You may use the - attribute name to verify which attribute the custom block is rendering - for. +- `@customFields` :: An object (hash) or an array/space-separated string of attributes. + When passing an array/space-separated string, the attributes will be rendered through the `:data-cell` named block. + When passing an object, set as key the attribute and value the component to use for rendering. The component will receive in `@cell` the same hash given to the `:data-cell` block. If value is empty, the attribute will be rendered through the `:data-cell` named block. + For the `:data-cell` named block, use the attribute name to verify which attribute the custom block is rendering for. ```hbs <:data-cell as |cell|> {{#if (eq cell.attribute "label")}} @@ -253,34 +341,36 @@ How to show different things in Ember Data Table - `@autoSearch` :: If truthy, search is automatically triggered without explicitly pressing search. If a number is provided, this is - the time in milliseconds before sending the request. If no number is - supplied a default is used. -- `@hasMenu` :: If not truthy, the component will show the supplied + the time in milliseconds to wait for input before sending the request (input douncing). + If not set, autosearch is enabled with a default wait of 2000ms. +- `@showMenu` :: If false, the component will hide the supplied menu. This allows controlling whether the menu should be shown dynamically. The menu may contain actions which act on the current selection. - `@enableSelection` :: Whether items should be selectable. Items are selectable across pages and may be acted on using the `:selection-menu-actions` or `:selection-menu` named blocks. +- `@initialSelection` :: The selection to use as long as the user has not changed the selection yet. +- `@selectionProperty` :: By default equality will be checked by direct comparison of objects, which works for e.g. ember-data records. If a specific property should be used for comparison (e.g. `uuid` when using mu-search), a property name or path can be supplied. +- `@selection` and `@updateSelection`: set the current selection and function that gets called when selection changes. This gives more control over handling selection logic. - `@linksModelProperty` :: When a link is clicked the row must supply information to the link to indicate which item was clicked. By - default the `id` property is used but another attribute may be + default the `id` property is used, but another property name or path may be supplied if desired (such as `uuid` when using mu-search). An empty string will provide the full object. -- `@attributeToSortParams` :: Function which translates an attribute to - its sort parameters. The sort parameters is currently a hash which - contains a key (default `'asc'` and `'desc'` to indicate sorting up - and down) and the corresponding sort key which should be sent out of - Ember Data Table (and used in the sort hash to the back-end). More - options than `'asc'` and `'desc'` can be provided if the back-end - understands different sorting strategies. -- `@onClickRow` :: Action to be triggered when the row is clicked. This - is an alternative for the row link but it triggers an action rather - than following a route. +- `@attributeToSortParams` :: Function which translates an attribute name to + its sort parameters. The sort parameters are a hash + with key the sort name to use in the table and value the corresponding sort key to sent out of Ember Data Table (and used in the sort hash to the back-end via `updateSort`). + By default, for input `attributeName`, it returns `{ 'asc': 'attribute-name', 'desc': '-attribute-name'}`. + More options can be provided if the back-end understands different sorting strategies. - `@rowLink` :: Link to be used when users click on the full row. This is an easier click target for users than an icon on the side. Ideally - the that target is provided too. `@onClickRow` may be provided to + that target is provided too. `@onClickRow` may be provided to call a function instead but this is less accessible. +- `@onClickRow` :: Callback to be triggered when the row is clicked. + Receives the clicked item (from `@content`) as its first argument. This + is an alternative for the row link but it triggers an action rather + than following a route. - `@rowLinkModelProperty` :: When `@rowLink` is used, the `id` property of the model rendered in the row will be supplied to the link. The property may be overridden by this property. Set to `uuid` when using @@ -289,39 +379,40 @@ How to show different things in Ember Data Table #### Overriding Ember Data Table parts using named blocks -Various named blocks are offered, check your Ember Data Table design implementation to see which part needs to be overridden. A list is provided here for reference. - -- `search` :: Overrides the full search component. Receives a search hash with properties: - - `filter` :: User's filter - - `placeholder` :: Placeholder for the text search - - `autoSearch` :: Value for autoSearch as supplied by the user - (subject to change) - - `submitForm` :: Action which can be used to submit the search form - and trigger search update - - `handleAutoInput` :: Action which can handle auto input by - debouncing and updating the search string - - `handleDirectInput` :: Action which handles the event where a user - types, gets value from `event.target.value`. - -- `menu` :: Overrides the full menu rendering. Receives three positional arguments: +Various named blocks are offered, check your Ember Data Table design implementation to see which part needs to be overridden. A list is provided here for reference as used in `raw-data-table.hbs`. + +- `:search` :: Overrides the full search block. Receives a hash containing: + - `filter` :: User's filter string + - `placeholder` :: Placeholder for the text search input + - `autoSearch` :: Value for autoSearch as supplied by the user (boolean or number). + - `submitSearch` :: Action which can be used to trigger a search string update (`@updateFilter`) + - `handleInput` :: Action which expects and event (with value in `event.target.value`) + and updates the search string immediately or after a debounce time, depending on `@autoSearch` value. + - `handleAutoInput` :: Like `handleInput`, but always uses a debounce time (even if `@autoSearch` is falsy). + - `handleDirectInput` :: Like `handleInput`, but always updates immediately (ignoring `@autoSearch` value). + + +- `:menu` :: Overrides the full menu block. Receives a hash containing: - `General` :: Component with information about the General menu which is rendered when nothing is selected. The block given to General - receives an argument which should be passed to `:general-menu`. + receives one block parameter which should be passed to `:general-menu`. + See `:general-menu` for the parameter details of `General`. - `Selected` :: Component with information on handling selected items. - The block given to Selected receives `selected` which should be - passed to `:selection-menu`. + The block given to Selected receives one block parameter which should be + passed to `:selection-menu`. + See `:selection-menu` for the parameter details of `Selected`. + - `enableSelection` :: Whether selection is enabled. -- `general-menu` :: Implements the menu with actions which is shown when - no items are selected. Receives a hash with two items: +- `:general-menu` :: Implements the menu with actions which is shown when + no items are selected. Receives a hash containing: - `dataTable` :: The main DataTable object on which actions can be called. - `selectionIsEmpty` :: Whether items are currently selected or not. -- `selection-menu` :: This menu is rendered only when items have been +- `:selection-menu` :: This menu is rendered only when items have been selected. It's the main wrapper which contains `:selection-menu-actions` (which you'd likely want to override - instead) as well as some visual information on the selected items. It - receives a hash with four elements: + instead) as well as some visual information on the selected items. Receives a hash containing: - `selectionIsEmpty` :: Whether the selection is currently empty. - `selectionCount` :: The amount of items which are selected at this point. - `clearSelection` :: An action to clear the whole selection. @@ -329,109 +420,102 @@ Various named blocks are offered, check your Ember Data Table design implementat - `dataTable` :: The DataTable object. - `selection-menu-actions` :: Contains the actions which can be applied - to a selection. This is likely custom for each use of the Ember Data + to a selection, rendered at the same time as `:selection-menu`. + This is likely custom for each use of the Ember Data Table (versus the template). Receives the same argument as `:selection-menu`. -- `content` :: This block is the full table but without search, actions - or pagination. It must render the table tag and everything in it. It - receives a hash with three elements. +- `:content` :: This block is the full table but without search, menu actions + or pagination. It must render the table tag and everything in it. + Receives a hash containing: - `Header` :: The Header logical component which contains information - to render the header row. Supplying a block to Header will yield - with the content for the `:header` named block. + to render the header row. Has the same block parameter hash as `:full-header` below. - `Body` :: The Body logical component which contains information to - render each of the body rows. Supplying a block to Body will yield - with the content for the `:body` named block. + render each of the body rows. Has the same block parameter hash as `:body` below. - `dataTable` :: The DataTable object. -- `full-header` :: This block should render the `` with the header row - inside of it. Receives a hash with the following items: - - `enableSelection` :: Whether or not selection is enabled. - - `enableLineNumbers` :: Whether or not line numbers are enabled. - - `sort` :: Sort parameter. - - `updateSort` :: Function to update sorting. +- `:full-header` :: This block should render the `` with the header row + inside of it. Receives a hash containing: + - `enableSelection` :: Whether selection is enabled. + - `enableLineNumbers` :: Whether line numbers are enabled. + - `sort` :: Current sort parameter. + - `updateSort` :: Function to update sorting (see `@updateSort`). - `hasLinks` :: Whether custom links are provided for this table (as per the `@links` argument to DataTable). - `customHeaders` :: Headers which should be rendered in a custom way - as an array or strings. - - `fields` :: A complex fields object containing the information about - each column to be rendered: + as an array of strings. + - `fields` :: An array of complex fields object containing the information about + each data column to be rendered: - `attribute` :: the attribute to be rendered - `label` :: the label of the header - `isSortable` :: whether this column is sortable or not - `sortParameters` :: hash which indicates in which ways this field - can be sorted (ascending, descending, something else). See + can be sorted (ascending, descending, something else). See output of `@attributeToSortParams`. - `hasCustomHeader` :: whether this column has a custom header or - not (meaning we should render it through the `:data-header` - named block) + not (meaning it should be rendered through the `:data-header` + named block). - `isCustom` :: whether the field rendering should be custom or not (meaning data cells should be rendered through `:data-cell`). + - `customFieldComponent` :: Available if the field rendering should use this custom component for rendering. + - `customHeaderComponent` :: Available if the column header should use this custom component for rendering. - `dataHeadersInfo` :: information for the data headers. Supplied to `:data-headers` named block. - `ThSortable` :: Contextual component. When calling this component - `@field` must be supplied (to generate info for a given field when + `@field` must be supplied (to generate info for the specific field when looping over `header.fields`) and `@hasCustomBlock` which should - indicate whether a `:data-header` is given. Supplying a block to - ThSortable will yield with the content for the `:data-header` - named block. The aforementioned content also has a + indicate whether a `:data-header` block is given. Has the same block parameter hash as `:data-header` below. This block parameter contains `renderCustomBlock` which can be used to detect whether a custom block should be rendered for this block or not. -- `data-headers` :: This is inside the `` of the `` and +- `:data-headers` :: This is inside the `` of the `` and should render all headers for the attributes. Thus ignoring the - headers for selection, numbers and actions. It receives a hash - containing the following elements: - - `fields` :: The fields to be rendered (see `fields` above for all - the attributes). + headers for selection, numbers and actions. Receives a hash + containing: + - `fields` :: The fields to be rendered (same `fields` as `:full-header`). - `customHeaders` :: Headers which should be rendered in a custom way - as an array or strings. + as an array of strings. - `sort` :: Sort parameter. - - `updateSort` :: Function to update sorting. -- `data-header` :: Renders a custom header which should handle sorting - etc. Receives a hash with the following elements: + - `updateSort` :: Function to update sorting (see `@updateSort`). +- `:data-header` :: Renders a custom header for headers specified in `@customHeaders`, which should handle sorting etc. Receives a hash containing: - `label` :: Label of the header. - `attribute` :: Attribute which will be rendered in this column. - `isSortable` :: Whether this column is sortable or not. - `isSorted` :: Whether sorting is applied to this header or not. - `toggleSort` :: Action which switches to the next sorting method - (e.g.: from `'asc'` to `'desc'` or from `'desc'` to nothing). + (e.g.: from `'asc'` to `'desc'` or from `'desc'` to nothing by default). - `nextSort` :: Next way of sorting. This is clear for - `["asc","desc",""]` but users may have provided other sorting - methods through `@attributeToSortParams`. - - `isAscending` :: Are we sorting ascending now? - - `isDescending` :: Are we sorting descending now? - - `sortDirection` :: What's the key on which we're sorting now (e.g.: `"desc"`) - - `renderCustomBlock` :: Should a custom block be rendered for this data header? - - `isCustom` :: Is the header explicitly marked to render custom? - - `hasCustomHeaders` :: Are there any custom headers to be rendered? - -- `actions-header` :: Header which will contain all actions. Receives no arguments. - -- `body` :: Renders the full body of the table, including the `` + `['asc','desc','']` but users may have provided other sorting + methods through `@attributeToSortParams`. The order is always alphabetically. + - `isAscending` :: Whether the current sorting is ascending (`'asc'`). + - `isDescending` :: Whether the current sorting is descending (`'desc'`). + - `sortDirection` :: What's the key on which we're sorting now (e.g.: `'desc'`) + - `renderCustomBlock` :: Whether a custom block should be rendered for this data header. + - `isCustom` :: Truthy if the header is explicitly marked to render custom. + - `hasCustomHeaders` :: Truthy if there are any custom headers to be rendered. + +- `:actions-header` :: Header which will contain all actions. Receives no arguments. + +- `:body` :: This block renders the full body of the table, and should include the `` tag. Receives a hash containing: - - `isLoading` :: Is the data being loaded at this point? Probably - need to render `:body-loading` named block then. - - `content` :: The actual content of this Data Table. - - `offset` :: The index of the first element in this data table. - - `wrappedItems` :: Rows of the data table in a way through which they - can be selected. + - `isLoading` :: Whether the data is being loaded. + Need to render `:body-loading` named block then. + - `content` :: The actual content of this Data Table (all items). + - `offset` :: The absolute index of the first element of the current page. - `enableLineNumbers` :: Whether line numbers are enabled or not. - `hasClickRowAction` :: Whether something needs to happen when the row is clicked. Either because there is an `@onClickRow` or because there is a `@rowLink`. - - `onClickRow` :: Action to be called when user clicked on a row, if - supplied by user of this Data Table. - `toggleSelected` :: Action which allows to toggle the selection - state of the current row. Should receive the an element from - `wrappedItems` as first element and the event that caused it (will - check `event.target.fetched`) as second argument. + state of the current row. Should receive the item to toggle from + `content` as first element and the event that caused it (will + check `event.target.checked`) as second argument. - `selection` :: Currently selected items. - `enableSelection` :: Whether selection of items is enabled. - `linkedRoutes` :: Array of objects describing each of the routes which should be linked as custom links per row. Each item is a hash with the following elements: - `route` :: The route to which we should link. - - `label` :: The human-readable label for the route if supplied. + - `label` :: The human-readable label for the route, if supplied. - `icon` :: The icon which should be rendered for the link, if supplied. - `linksModelProperty` :: The property of the model which should be supplied to the route (e.g.: `id` for the id or `""` if the whole @@ -447,88 +531,80 @@ Various named blocks are offered, check your Ember Data Table design implementat rendered. See `fields` higher up. - `Row` :: Contextual component handling the logic of an individual row. This has to be called for each row in the visible table and it - should receive `@wrapper` for the element of `wrappedItems` we're - rendering here, as well as the `@index` for the index we're looping - over here. The `@index` is a local index for this rendering - regardless of the page, so you can use `{{#each body.wrappedItems as - |wrapper index|}}...{{/each}}`. -- `body-loading` :: Renders a custom body loading message supplied in - this invocation of Ember Data Table. -- `row` :: Renders an individual row, including the `` tag. This is - the row with both the data elements as well as with the meta elements - such as selection of items and links. Receives a hash with the - following elements: - - `wrapper` :: An object containing the item and the selection status. + should receive in `@item` the element of `content` we're + rendering here, as well as the index we're looping + over here in `@index`. The `@index` is a local index for this rendering + regardless of the page, so you can use `{{#each body.content as + |item index|}}...{{/each}}`. +- `:body-loading` :: Block to show a custom loading message block. +- `:row` :: Renders an individual row, including the `` tag. This is + the row with both the data columns as well as the meta columns + such as selection of items and links. Receives a hash containing: - `item` :: Actual item to be rendered in this row. - `enableLineNumbers` :: See above. - `lineNumber` :: See above. - `enableSelection` :: See above. - - `selected` :: Whether this row is selected or not. - - `isSelected` :: Whether this item is selected or not (same as - selected). - - `toggleSelected` :: See above. + - `isSelected` :: Whether this item is selected or not. + - `toggleSelected` :: See above, but the row item is already passed. - `hasClickRowAction` :: See above. - - `onClickRow` :: See above. + - `rowClicked` :: Function to be called when user clicked on this row. - `linkedRoutes` :: A copy of `linkedRoutes` as mentioned above but adding the `model` key which contains the specific model to supply to the linked route for this row (e.g.: the `id`, `uuid` or the full `item`) + - `rowLink` :: The route which should be used when users click on the row itself. + - `rowLinkModel` :: Model to supply to the route specified by `rowLink` for this specific row. - `fields` :: See above. - `DataCells` :: Contextual component which provides information for - rendering the data cells of a row. Supplying a block to DataCells - will yield a block which is used for rendering the `:dataCells` named - block. -- `data-cells` :: Renders all the cells containing real data in a row. - This includes selection of the row and links. Receives a hash with - the following elements: - - `fields` :: See above. - - `firstColumn` :: The field of the first column to be rendered. Good + rendering the data cells of a row. Has the same block parameter hash as `:data-cells` below. +- `:data-cells` :: Renders all the cells containing real data (fields) in a row. + This excludes cells for meta columns (like selection and links). Receives a hash containing: + - `fields` :: All fields to be rendered. See above. + - `firstColumnField` :: The field of the first column to be rendered. Good for designs where the first column should receive different styling. - - `otherColumns` :: The fields of all columns but the first one to be + - `otherColumnFields` :: The fields of all columns but the first one to be rendered. Good for designs where the first column should receive different styling. - - `wrapper` :: See above. - `item` :: See above. - `rowLink` :: See above. - - `rowLinkModel` :: Model to supply to the route specified by `rowLink` for this specific row. # =@wrapper.rowLinkModel - - `fields` :: See above. + - `rowLinkModel` :: See above. + - `rowClicked` :: See above. - `DataCell` :: Contextual component which provides information for rendering an individual cell. Should receive `@column` with the - field to render and `@hasCustomBlock` with `{{has-block - "data-cell"}}` so we know whether a custom block was provided for + field to render and `@hasCustomBlock` with `{{has-block "data-cell"}}` + so we know whether a custom block was provided for the `data-cell` named slot. -- `data-cell` :: Renders a custom data cell regardless of whether it's - first or any other. Receives a hash with the following elements: - - `firstColumn` :: See above. - - `otherColumns` :: See above. +- `:data-cell` :: Renders a custom data cell regardless of whether it's + first or any other. Receives a hash containing: + - `fields` :: See above. + - `firstColumnField` :: See above. + - `otherColumnFields` :: See above. - `item` :: See above. - `rowLink` :: See above. - `rowLinkModel` :: See above. + - `rowClicked` :: See above. - `label` :: See above. - - `fields` :: See above. - - `isCustom` :: Is the cell explicitly marked to render custom? - - `hasCustomFields` :: Whether there are custom fields to be - rendered. + - `isCustom` :: Wether this cell is explicitly marked to render custom. + - `hasCustomFields` :: Whether there are any cells that are marked to render custom. - `attribute` :: The attribute which will be rendered. - `renderCustomBlock` :: Whether a custom block should be rendered - for this field. This is the named slot `:data-cell`. - - `value` :: The value which should be rendered. -- `first-data-cell` :: In designs which care about the first data cell + for this field. This block is the named slot `:data-cell`. + - `value` :: The data value which should be rendered. +- `:first-data-cell` :: In designs which care about the first data cell versus the others, this will render a custom design for the first data - column of the table. Receives the same arguments as `data-cell`. -- `rest-data-cell` :: In designs which care about the first data cell + column of the table. Receives the same block parameter hash as `:data-cell`. +- `:rest-data-cell` :: In designs which care about the first data cell versus the others, this will render a custom design for the other data - columns of the table. Receives the same arguments as `data-cell`. -- `actions` :: Renders the links next to each row specified through - `@links`. Receives the same arguments as `row`. -- `no-data-message` :: Rendered when no data was available in the data + columns of the table. Receives the block parameter hash as `:data-cell`. +- `:actions` :: Renders the links next to each row specified through + `@links`. Receives the same arguments as `:row`. +- `:no-data-message` :: Rendered when no data was available in the data cell. When no styling is needed, `@noDataMessage` can be used instead. -- `pagination` :: Renders everything needed to handle pagination. - Receives a hash with the following elements: - - `startItem` :: Number of the first item rendered on this page. - - `endItem` :: Number of the last item rendered on this page. +- `:pagination` :: This block contains everything needed to handle pagination. + Receives a hash containing: + - `startIndex` :: Absolute index of the first item rendered on this page. + - `endIndex` :: Absolute index of the last item rendered on this page. - `total` :: Total amount of items on all pages of this table. - `hasTotal` :: Whether the total amount of items is known. - `pageSize` :: Amount of items per page (though the last page may have fewer items). @@ -545,7 +621,7 @@ Various named blocks are offered, check your Ember Data Table design implementat - followed by up to three pages after the current page number, - followed by 'more' if empty spots follow, - followed by the last page number. - - `sizeOptions` :: The different sizes (as an array) for pages of this Data Table. + - `sizeOptions` :: The different sizes (as an array of numbers) for pages of this Data Table. `null` if size should not be changeable (defined by `@sizes`). - `firstPage` :: The first page number in this Data Table. - `lastPage` :: The last page number in this Data Table. - `nextPage` :: The next page number in this view, `undefined` if this @@ -553,11 +629,11 @@ Various named blocks are offered, check your Ember Data Table design implementat - `previousPage` :: The previous page number in this view, `undefined` if this is the first page. - `updatePage` :: Function which takes a back-end page number and - updates it (this is the raw function supplied to `DataTable`. - - `humanPage` :: Thu current page in human form. - - `updateHumanPage` :: Updates the human page number (this will call + updates it (this is the raw function supplied to `DataTable`). + - `humanPage` :: The current page in human form. + - `updateHumanPage` :: Updates the human page number. This will call `updatePage` after mapping the human page number through the back-end - page number offset). + page number offset. All page numbers defined here are human page numbers. - `selectSizeOption` :: Selects a new size option, takes `event` as input and gets the new value from `event.target.value`. - `setSizeOption` :: Selects a new size, takes the `size` as either @@ -574,3 +650,6 @@ Various named blocks are offered, check your Ember Data Table design implementat contain page links. - `backendPageOffset` :: The current back-end page offset (either calculated or guessed). + +## Development +There is a [testing app](/test-app/app) for example configurations to test changes. The dummy app can be run via `npm start`. When running [the addon](/addon) with `npm start`, the test-app will automatically rebuild if the addon gets rebuild. This way you can develop the addon in tandem with testing it via the test-app. diff --git a/addon/.gitignore b/addon/.gitignore new file mode 100644 index 0000000..e8bafed --- /dev/null +++ b/addon/.gitignore @@ -0,0 +1,3 @@ +# compiled output +dist/ +declarations/ \ No newline at end of file diff --git a/.eslintignore b/addon/.prettierignore similarity index 51% rename from .eslintignore rename to addon/.prettierignore index 701947e..9385391 100644 --- a/.eslintignore +++ b/addon/.prettierignore @@ -1,22 +1,13 @@ # unconventional js /blueprints/*/files/ -/vendor/ # compiled output /dist/ -/tmp/ - -# dependencies -/bower_components/ -/node_modules/ # misc /coverage/ !.* .*/ -.eslintcache # ember-try /.node_modules.ember-try/ -/bower.json.ember-try -/package.json.ember-try diff --git a/addon/.stylelintignore b/addon/.stylelintignore new file mode 100644 index 0000000..a0cf71c --- /dev/null +++ b/addon/.stylelintignore @@ -0,0 +1,8 @@ +# unconventional files +/blueprints/*/files/ + +# compiled output +/dist/ + +# addons +/.node_modules.ember-try/ diff --git a/addon/addon-main.js b/addon/addon-main.js new file mode 100644 index 0000000..76a4976 --- /dev/null +++ b/addon/addon-main.js @@ -0,0 +1,3 @@ +const { addonV1Shim } = require('@embroider/addon-shim'); + +module.exports = addonV1Shim(__dirname); diff --git a/addon/babel.config.cjs b/addon/babel.config.cjs new file mode 100644 index 0000000..f6e3498 --- /dev/null +++ b/addon/babel.config.cjs @@ -0,0 +1,32 @@ +module.exports = { + plugins: [ + [ + '@babel/plugin-transform-typescript', + { + allExtensions: true, + allowDeclareFields: true, + onlyRemoveTypeImports: true, + }, + ], + [ + 'babel-plugin-ember-template-compilation', + { + targetFormat: 'hbs', + transforms: [], + }, + ], + [ + 'module:decorator-transforms', + { + runtime: { + import: 'decorator-transforms/runtime-esm', + }, + }, + ], + ['ember-concurrency/async-arrow-task-transform'], + ], + + generatorOpts: { + compact: false, + }, +}; diff --git a/addon/components/data-table.hbs b/addon/components/data-table.hbs deleted file mode 100644 index c401c1e..0000000 --- a/addon/components/data-table.hbs +++ /dev/null @@ -1,41 +0,0 @@ -{{!-- TODO: supply both meta and @content.meta or supply @content.meta only when @meta is not supplied to be in line with readme --}} - -{{yield - (hash - Search=(component "data-table/text-search" - filter=this.filter - placeholder=this.searchPlaceholder - autoSearch=this.autoSearch - updateFilter=this.updateFilter - searchDebounceTime=this.searchDebounceTime) - Content=(component "data-table/data-table-content" - content=@content - noDataMessage=this.noDataMessage - enableSelection=@enableSelection - enableLineNumbers=@enableLineNumbers - onClickRow=@onClickRow - sort=this.sort - updateSort=this.updateSort - customHeaders=this.customHeaders - fields=this.fields - links=@links - linksModelProperty=this.linksModelProperty - rowLink=@rowLink - rowLinkModelProperty=this.rowLinkModelProperty - dataTable=this) - Pagination=(component "data-table/number-pagination" - page=this.page - size=this.size - itemsOnCurrentPage=@content.length - sizeOptions=this.sizeOptions - total=@total - meta=@content.meta - updatePage=this.updatePage - updateSize=this.updatePageSize - backendPageOffset=@backendPageOffset) - Menu=(component "data-table/data-table-menu" - enableSelection=@enableSelection - dataTable=this) - content=@content - enableSearch=this.enableSearch - dataTable=this)}} diff --git a/addon/components/data-table.js b/addon/components/data-table.js deleted file mode 100644 index ea157d3..0000000 --- a/addon/components/data-table.js +++ /dev/null @@ -1,251 +0,0 @@ -import { action } from '@ember/object'; -import { tracked } from '@glimmer/tracking'; -import Component from '@glimmer/component'; -import { typeOf } from '@ember/utils'; -import { toComponentSpecifications, splitDefinitions } from "../utils/string-specification-helpers"; -import attributeToSortParams from "../utils/attribute-to-sort-params"; - -export default class DataTable extends Component { - @tracked _selection = undefined; - - get filter() { - return this.args.filter !== undefined - ? this.args.filter - : this.args.view?.filter; - } - - get sort() { - return this.args.sort !== undefined - ? this.args.sort - : this.args.view?.sort; - } - - get selection() { - if (this._selection === undefined && this.args.selection === undefined) - return []; - else if (this._selection !== undefined) - return this._selection; - else - return this.args.selection; - } - - set selection(newSelection) { - this._selection = newSelection; // also trigers dependent properties - } - - get noDataMessage() { - return this.args.noDataMessage === undefined - ? 'No data' - : this.args.noDataMessage; - } - - get isLoading() { - return this.args.isLoading !== undefined - ? this.args.isLoading - : this.args.view?.isLoading; - } - - /** - * Calculates the search debounce time. - * - * If the user supplies searchDebounceTime, that is what we should - * use. A shorthand form is supported in which the user supplies a - * number to autoSearch in which case we use that. This would not - * work with 0 (which is a strange debounce time in itself) so this - * option exists for now. - */ - get searchDebounceTime() { - return this.args.searchDebounceTime === undefined - ? isNaN(this.args.autoSearch) ? 2000 : this.args.autoSearch - : this.args.searchDebounceTime; - } - - get enableSelection() { - return this.args.enableSelection; - } - - get selectionIsEmpty() { - return this.selection.length === 0; - } - - get enableSizes() { - return this.args.enableSizes === undefined ? true : this.args.enableSizes; - } - - get page() { - const page = this.args.page !== undefined - ? this.args.page - : this.args.view?.page; - return page || 0; - } - - get size() { - if ( this.args.size ) - return this.args.size; - else if ( this.args.view?.size ) - return this.args.view.size; - else - return 5; - } - - get sizeOptions() { - if (!this.enableSizes) { - return null; - } else { - const sizeOptions = this.args.sizes || [5, 10, 25, 50, 100]; - if (!sizeOptions.includes(this.size) && this.size) { - sizeOptions.push(this.size); - } - sizeOptions.sort((a, b) => a - b); - return sizeOptions; - } - } - - get enableSearch() { - return this.args.enableSearch === undefined - ? this.filter !== undefined - : this.args.enableSearch; - } - - get autoSearch() { - return this.args.autoSearch === undefined ? true : this.args.autoSearch; - } - - get linksModelProperty() { - return this.args.linksModelProperty === undefined - ? 'id' - : this.args.linksModelProperty; - } - - get rowLinkModelProperty() { - return this.args.rowLinkModelProperty === undefined - ? 'id' - : this.args.rowLinkModelProperty; - } - - get fieldsWithMeta() { - const fields = this.args.fields; - - if (typeOf(fields) === 'string') { - return toComponentSpecifications(fields, [{raw: "attribute"},{name: "label", default: "attribute"}]); - } else { - return fields || []; - } - } - - attributeToSortParams(attribute) { - if( this.args.attributeToSortParams ) { - return this.args.attributeToSortParams(attribute); - } else { - return attributeToSortParams(attribute); - } - } - - get fields() { - return this - .fieldsWithMeta - .map( ({ attribute, label, isSortable, hasCustomHeader, isCustom, sortParameters }) => ({ - attribute, - label, - sortParameters: sortParameters // custom format says it's sortable - || ( ( isSortable // custom format says it's sortable - || this.sortableFields === null // default: all fields are sortable - || this.sortableFields?.includes(attribute) ) // @sortableFields - && this.attributeToSortParams(attribute) ), - get isSortable() { return Object.keys( this.sortParameters || {} ).length >= 1; }, - hasCustomHeader: hasCustomHeader - || this.customHeaders.includes(attribute), - isCustom: isCustom - || this.customFields.includes(attribute) - })); - } - - get customHeaders() { - return splitDefinitions(this.args.customHeaders); - } - - get customFields() { - return splitDefinitions(this.args.customFields); - } - - get sortableFields() { - const sortableFields = this.args.sortableFields; - if (sortableFields || sortableFields === "") - return splitDefinitions(sortableFields); - else - // default: all fields are sortable - return null; - } - - get searchPlaceholder() { - return this.args.searchPlaceholder === undefined - ? 'Search input' - : this.args.searchPlaceholder; - } - - @action - updatePageSize(size) { - const updater = this.args.updatePageSize !== undefined - ? this.args.updatePageSize - : this.args.view?.updatePageSize; - - if( !updater ) { - console.error(`Could not update page size to ${size} because @updatePageSize was not supplied to data table`); - } else { - this.updatePage(0); - updater(size); - } - } - - @action - updateFilter(filter) { - const updater = this.args.updateFilter || this.args.view?.updateFilter; - - if( !updater ) { - console.error(`Could not update filter to '${filter}' because @updateFilter was not supplied to data table`); - } else { - this.updatePage(0); - updater(filter); - } - } - - @action - updateSort(sort) { - const updater = this.args.updateSort !== undefined - ? this.args.updateSort - : this.args.view?.updateSort; - - if( !updater ) { - console.error(`Could not update sorting to '${sort}' because @updateSort was not supplied to data table`); - } else { - this.updatePage(0); - updater(sort); - } - } - - @action - updatePage(page) { - const updater = this.args.updatePage !== undefined - ? this.args.updatePage - : this.args.view?.updatePage; - - if( !updater ) { - console.error(`Could not update page to ${page} because @updatePage was not supplied to data table`); - } else { - updater(page); - } - } - - @action - addItemToSelection(item) { - this.selection = [...new Set([item, ...this.selection])]; - } - @action - removeItemFromSelection(item) { - this.selection = this.selection.filter((x) => x !== item); - } - @action - clearSelection() { - this.selection = []; - } -} diff --git a/addon/components/data-table/data-cell.hbs b/addon/components/data-table/data-cell.hbs deleted file mode 100644 index 877b297..0000000 --- a/addon/components/data-table/data-cell.hbs +++ /dev/null @@ -1,14 +0,0 @@ -{{!-- Used in: data-table/data-cells --}} -{{yield (hash - firstColumn=@firstColumn - otherColumns=@otherColumns - item=@wrapper.item - rowLink=@wrapper.rowLink - rowLinkModel=@wrapper.rowLinkModel - label=@column.label - fields=@fields - isCustom=this.isCustom - hasCustomFields=this.hasCustomFields - attribute=@column.attribute - renderCustomBlock=this.renderCustomBlock - value=(get @wrapper.item @column.attribute))}} \ No newline at end of file diff --git a/addon/components/data-table/data-cell.js b/addon/components/data-table/data-cell.js deleted file mode 100644 index 136c5da..0000000 --- a/addon/components/data-table/data-cell.js +++ /dev/null @@ -1,15 +0,0 @@ -import Component from '@glimmer/component'; - -export default class DataTableDataCellComponent extends Component { - get isCustom() { - return this.args.column.isCustom; - } - - get hasCustomFields() { - return this.args.fields.find( ({isCustom}) => isCustom) || false; - } - - get renderCustomBlock() { - return this.args.hasCustomBlock && ( this.isCustom || !this.hasCustomFields ); - } -} diff --git a/addon/components/data-table/data-cells.hbs b/addon/components/data-table/data-cells.hbs deleted file mode 100644 index a0bfebb..0000000 --- a/addon/components/data-table/data-cells.hbs +++ /dev/null @@ -1,16 +0,0 @@ -{{!-- Used in: data-table/row --}} -{{yield (hash - fields=@dataTable.fields - firstColumn=this.firstColumn - otherColumns=this.otherColumns - wrapper=@wrapper - item=@wrapper.item - rowLink=@wrapper.rowLink - rowLinkModel=@wrapper.rowLinkModel - fields=@fields - DataCell=(component - "data-table/data-cell" - firstColumn=this.firstColumn - otherColumns=this.otherColumns - wrapper=@wrapper - fields=@fields))}} diff --git a/addon/components/data-table/data-cells.js b/addon/components/data-table/data-cells.js deleted file mode 100644 index 3b607d1..0000000 --- a/addon/components/data-table/data-cells.js +++ /dev/null @@ -1,16 +0,0 @@ -import Component from '@glimmer/component'; - -export default class DataTableDataCellsComponent extends Component { - get firstColumn() { - return this.args.fields?.[0] || null; - } - - get otherColumns() { - if (this.args.fields?.length) { - let [, ...fields] = this.args.fields; - return fields; - } else { - return []; - } - } -} diff --git a/addon/components/data-table/data-table-content-body.hbs b/addon/components/data-table/data-table-content-body.hbs deleted file mode 100644 index 5891161..0000000 --- a/addon/components/data-table/data-table-content-body.hbs +++ /dev/null @@ -1,28 +0,0 @@ -{{!-- Used in: data-table/data-table-content --}} -{{yield (hash - isLoading=@dataTable.isLoading - content=@content - offset=this.offset - wrappedItems=this.wrappedItems - enableLineNumbers=@enableLineNumbers - hasClickRowAction=(and (or @onClickRow @rowLink) true) - onClickRow=@onClickRow - toggleSelected=this.updateSelection - selection=@dataTable.selection - enableSelection=@enableSelection - linkedRoutes=@linkedRoutes - rowLink=@rowLink - rowLinkModelProperty=@rowLinkModelProperty - noDataMessage=@noDataMessage - fields=@fields - Row=(component "data-table/row" - dataTable=@dataTable - enableLineNumbers=@enableLineNumbers - enableSelection=@enableSelection - selection=@dataTable.selection - offset=this.offset - hasClickRowAction=(and (or @onClickRow @rowLink) true) - onClickRow=this.onClickRow - linkedRoutes=@linkedRoutes - fields=@fields - toggleSelected=this.updateSelection))}} diff --git a/addon/components/data-table/data-table-content-body.js b/addon/components/data-table/data-table-content-body.js deleted file mode 100644 index 3b27459..0000000 --- a/addon/components/data-table/data-table-content-body.js +++ /dev/null @@ -1,56 +0,0 @@ -import { cached } from '@glimmer/tracking'; -import { get } from '@ember/object'; -import { inject as service } from '@ember/service'; -import { action } from '@ember/object'; -import Component from '@glimmer/component'; - -export default class DataTableContentBodyComponent extends Component { - @service router; - - get offset() { - var offset = 1; //to avoid having 0. row - var page = this.args.dataTable.page; // TODO: pass on page directly? - var size = this.args.dataTable.size; // TODO: pass on size directly? - if (page && size) { - offset += page * size; - } - return offset; - } - - @cached - get wrappedItems() { - const selection = this.args.dataTable.selection || []; // TODO: should the dataTable ensure this is an array? - const content = this.args.content; - return content.map((item) => { - return { - item: item, - isSelected: selection.includes(item), - rowLink: this.args.rowLink, - rowLinkModel: this.rowLinkModel(item) - }; - }); - } - - rowLinkModel(row) { - return this.args.rowLinkModelProperty - ? get(row, this.args.rowLinkModelProperty) - : row; - } - - @action - updateSelection(selectedWrapper, event) { - if( event.target.checked ) - this.args.dataTable.addItemToSelection(selectedWrapper.item); - else - this.args.dataTable.removeItemFromSelection(selectedWrapper.item); - } - - @action - onClickRow(row) { - if ( this.args.onClickRow ) { - this.args.onClickRow(...arguments); - } else if ( this.args.rowLink ) { - this.router.transitionTo( this.args.rowLink, this.rowLinkModel(row) ); - } - } -} diff --git a/addon/components/data-table/data-table-content-header.hbs b/addon/components/data-table/data-table-content-header.hbs deleted file mode 100644 index d634aab..0000000 --- a/addon/components/data-table/data-table-content-header.hbs +++ /dev/null @@ -1,19 +0,0 @@ -{{!-- Used in: data-table/data-table-content --}} -{{yield (hash - enableSelection=@enableSelection - enableLineNumbers=@enableLineNumbers - sort=@sort - updateSort=@updateSort - hasLinks=@hasLinks - customHeaders=@customHeaders - fields=@fields - dataHeadersInfo=(hash - fields=@fields - customHeaders=@customHeaders - sort=@sort - updateSort=@updateSort) - ThSortable=(component - "data-table/th-sortable" - fields=@fields - sort=@sort - updateSort=@updateSort))}} diff --git a/addon/components/data-table/data-table-content-header.js b/addon/components/data-table/data-table-content-header.js deleted file mode 100644 index e84986c..0000000 --- a/addon/components/data-table/data-table-content-header.js +++ /dev/null @@ -1,3 +0,0 @@ -import Component from '@glimmer/component'; - -export default class DataTableContentHeaderComponent extends Component {} diff --git a/addon/components/data-table/data-table-content.hbs b/addon/components/data-table/data-table-content.hbs deleted file mode 100644 index 38570f9..0000000 --- a/addon/components/data-table/data-table-content.hbs +++ /dev/null @@ -1,24 +0,0 @@ -{{!-- Used in: data-table.hbs --}} -{{yield - (hash - Header=(component "data-table/data-table-content-header" - enableSelection=@enableSelection - enableLineNumbers=@enableLineNumbers - sort=@sort - updateSort=@updateSort - hasLinks=this.hasLinks - customHeaders=@customHeaders - dataTable=@dataTable - fields=@fields) - Body=(component "data-table/data-table-content-body" - content=@content - enableSelection=@enableSelection - enableLineNumbers=@enableLineNumbers - noDataMessage=@noDataMessage - onClickRow=@onClickRow - linkedRoutes=this.linkedRoutes - rowLink=@rowLink - rowLinkModelProperty=@rowLinkModelProperty - dataTable=@dataTable - fields=@fields) - dataTable=@dataTable)}} diff --git a/addon/components/data-table/data-table-content.js b/addon/components/data-table/data-table-content.js deleted file mode 100644 index b197317..0000000 --- a/addon/components/data-table/data-table-content.js +++ /dev/null @@ -1,32 +0,0 @@ -import Component from '@glimmer/component'; -import { toComponentSpecifications } from '../../utils/string-specification-helpers'; - -export default class DataTableContentComponent extends Component { - get hasLinks() { - return this.linkedRoutes.length > 0; - } - - /** - * Accepts and transforms definitions for linked routes. - * - * Implementations may transform this at will. The default - * transformation splits on `:` assuming the first part is the route - * and the second part is the label. If no label is given, it is - * passed as null. If a label is given, all underscores are - * transformed to spaces and double underscores are left as a single - * _. We split again on a third `:`, transforming in the same way for - * the suggested icon. - * - * Behaviour for `___` is undefined. - * - * Yields an array of objects to represent the linked routes. - * [ { route: "products.show", label: "Show product", icon: "show-icon" } ] - */ - get linkedRoutes() { - return toComponentSpecifications(this.args.links || "", [{ raw: "route" }, "label", "icon"]) - .map( (spec) => { - spec.linksModelProperty = this.args.linksModelProperty; - return spec; - } ); - } -} diff --git a/addon/components/data-table/data-table-menu-general.hbs b/addon/components/data-table/data-table-menu-general.hbs deleted file mode 100644 index aa4bd58..0000000 --- a/addon/components/data-table/data-table-menu-general.hbs +++ /dev/null @@ -1,5 +0,0 @@ -{{!-- Used in: data-table/data-table-menu --}} -{{yield (hash - dataTable=@dataTable - selectionIsEmpty=@dataTable.selectionIsEmpty) -}} diff --git a/addon/components/data-table/data-table-menu-general.js b/addon/components/data-table/data-table-menu-general.js deleted file mode 100644 index c985733..0000000 --- a/addon/components/data-table/data-table-menu-general.js +++ /dev/null @@ -1,3 +0,0 @@ -import Component from '@glimmer/component'; - -export default class DataTableMenuGeneralComponent extends Component {} diff --git a/addon/components/data-table/data-table-menu-selected.hbs b/addon/components/data-table/data-table-menu-selected.hbs deleted file mode 100644 index f232183..0000000 --- a/addon/components/data-table/data-table-menu-selected.hbs +++ /dev/null @@ -1,9 +0,0 @@ -{{!-- Used in: data-table/data-table-menu --}} -{{yield (hash - selectionIsEmpty=@dataTable.selectionIsEmpty - selectionCount=@dataTable.selection.length - clearSelection=@dataTable.clearSelection - selection=this.copiedSelection - dataTable=@dataTable)}} - -{{!-- TODO: must we pass the data table itself? It is shared with the consumers. --}} \ No newline at end of file diff --git a/addon/components/data-table/data-table-menu-selected.js b/addon/components/data-table/data-table-menu-selected.js deleted file mode 100644 index 29c343a..0000000 --- a/addon/components/data-table/data-table-menu-selected.js +++ /dev/null @@ -1,12 +0,0 @@ -import { action } from '@ember/object'; -import Component from '@glimmer/component'; - -export default class DataTableMenuSelectedComponent extends Component { - get selectionCount() { - return this.args.dataTable.selection.length; - } - - get copiedSelection() { - return [...this.args.dataTable.selection]; - } -} diff --git a/addon/components/data-table/data-table-menu.hbs b/addon/components/data-table/data-table-menu.hbs deleted file mode 100644 index 635a3ff..0000000 --- a/addon/components/data-table/data-table-menu.hbs +++ /dev/null @@ -1,7 +0,0 @@ -{{!-- Used in: data-table.hbs --}} -{{#let - (component "data-table/data-table-menu-general" dataTable=@dataTable) - (component "data-table/data-table-menu-selected" dataTable=@dataTable) - as |general selected|}} - {{yield general selected @dataTable.enableSelection}} -{{/let}} \ No newline at end of file diff --git a/addon/components/data-table/data-table-menu.js b/addon/components/data-table/data-table-menu.js deleted file mode 100644 index 9d407b4..0000000 --- a/addon/components/data-table/data-table-menu.js +++ /dev/null @@ -1,3 +0,0 @@ -import Component from '@glimmer/component'; - -export default class DataTableMenuComponent extends Component {} diff --git a/addon/components/data-table/number-pagination.hbs b/addon/components/data-table/number-pagination.hbs deleted file mode 100644 index 769b7e3..0000000 --- a/addon/components/data-table/number-pagination.hbs +++ /dev/null @@ -1,28 +0,0 @@ -{{!-- Used in: data-table.hbs --}} -{{yield (hash - startItem=this.startItem - endItem=this.endItem - total=this.total - hasTotal=this.hasTotal - pageSize=@size - pageNumber=this.humanPage - numberOfPages=this.numberOfPages - pageOptions=this.pageOptions - summarizedPageOptions=this.summarizedPageOptions - sizeOptions=@sizeOptions - firstPage=this.firstPage - lastPage=this.lastPage - nextPage=this.nextPage - previousPage=this.previousPage - updatePage=this.updatePage - humanPage=this.humanPage - updateHumanPage=this.updateHumanPage - selectSizeOption=this.selectSizeOption - setSizeOption=this.setSizeOption - hasMultiplePages=this.hasMultiplePages - isFirstPage=this.isFirstPage - isLastPage=this.isLastPage - hasPreviousPage=this.hasPreviousPage - hasNextPage=this.hasNextPage - meta=@meta - backendPageOffset=this.backendPageOffset)}} \ No newline at end of file diff --git a/addon/components/data-table/row.hbs b/addon/components/data-table/row.hbs deleted file mode 100644 index 60c2bc8..0000000 --- a/addon/components/data-table/row.hbs +++ /dev/null @@ -1,21 +0,0 @@ -{{!-- Used in: data-table/data-table-content-body --}} -{{!-- TODO: do we want both selected and isSelected? --}} -{{yield (hash - wrapper=@wrapper - item=@wrapper.item - enableLineNumbers=@enableLineNumbers - lineNumber=(add @index @offset) - enableSelection=@enableSelection - hasClickRowAction=@hasClickRowAction - onClickRow=(fn @onClickRow @wrapper.item) - isSelected=(includes @wrapper.item @selection) - selected=(includes @wrapper.item @selection) - toggleSelected=(fn @toggleSelected @wrapper) - linkedRoutes=this.linkedRoutes - fields=@fields - DataCells=(component - "data-table/data-cells" - fields=@fields - wrapper=@wrapper - linkedRoutes=this.linkedRoutes - dataTable=@dataTable))}} \ No newline at end of file diff --git a/addon/components/data-table/row.js b/addon/components/data-table/row.js deleted file mode 100644 index 6428da7..0000000 --- a/addon/components/data-table/row.js +++ /dev/null @@ -1,15 +0,0 @@ -import { get } from '@ember/object'; -import Component from '@glimmer/component'; - -export default class DataTableRowComponent extends Component { - get linkedRoutes() { - return this.args.linkedRoutes.map( (linkedRoute) => { - const model = this.args.wrapper.item; - return Object.assign( { - model: linkedRoute.linksModelProperty - ? get(model, linkedRoute.linksModelProperty) - : model - }, linkedRoute ); - } ); - } -} diff --git a/addon/components/data-table/text-search.hbs b/addon/components/data-table/text-search.hbs deleted file mode 100644 index a204a12..0000000 --- a/addon/components/data-table/text-search.hbs +++ /dev/null @@ -1,8 +0,0 @@ -{{!-- Used in data-table.hbs --}} -{{yield (hash - filter=@filter - placeholder=@placeholder - autoSearch=@autoSearch - submitForm=this.submitForm - handleAutoInput=this.handleAutoInput - handleDirectInput=this.handleDirectInput)}} diff --git a/addon/components/data-table/text-search.js b/addon/components/data-table/text-search.js deleted file mode 100644 index 8000fa0..0000000 --- a/addon/components/data-table/text-search.js +++ /dev/null @@ -1,38 +0,0 @@ -import { action } from '@ember/object'; -import { cancel, debounce } from '@ember/runloop'; -import Component from '@glimmer/component'; - -export default class TextSearchComponent extends Component { - enteredValue = undefined; - - autoDebouncePid = undefined; - - @action - handleAutoInput(event) { - this.enteredValue = event.target.value; - this.autoDebouncePid = debounce(this, this.submitCurrent, this.args.searchDebounceTime); - } - - submitCurrent() { - if (!this.isDestroying && !this.isDestroyed) { - this.args.updateFilter(this.enteredValue); - this.autoDebouncePid = undefined; - } - } - - willDestroy() { - super.willDestroy(...arguments); - cancel(this.autoDebouncePid); - } - - @action - handleDirectInput(event) { - this.enteredValue = event.target.value; - } - - @action - submitForm(event) { - event.preventDefault(); - this.submitCurrent(); - } -} diff --git a/addon/components/data-table/th-sortable.hbs b/addon/components/data-table/th-sortable.hbs deleted file mode 100644 index 15792f2..0000000 --- a/addon/components/data-table/th-sortable.hbs +++ /dev/null @@ -1,17 +0,0 @@ -{{!-- Used in: data-table/data-table-content-header --}} -{{yield (hash - label=@field.label - attribute=@field.attribute - - isSortable=@field.isSortable - isSorted=this.isSorted - toggleSort=this.toggleSort - nextSort=this.nextSort - - isAscending=this.isAscending - isDescending=this.isDescending - sortDirection=this.sortDirection - - renderCustomBlock=this.renderCustomBlock - isCustom=this.isCustom - hasCustom=this.hasCustom)}} \ No newline at end of file diff --git a/addon/components/raw-data-table.hbs b/addon/components/raw-data-table.hbs deleted file mode 100644 index 35deedc..0000000 --- a/addon/components/raw-data-table.hbs +++ /dev/null @@ -1,304 +0,0 @@ -{{!-- template-lint-disable no-inline-styles --}} - - {{!-- START: search --}} -
- {{#if dt.enableSearch}} - - {{#if (has-block "search")}} - {{yield search to="search"}} - {{else}} -
-
- -
-
- {{/if}} -
- {{/if}} - {{!-- END: search --}} - - {{!-- START: menu --}} - - {{#if (has-block "menu")}} - {{yield (hash General Selected enableSelection) to="menu"}} - {{else}} -
- {{!-- either we have a general block or we have to have a menu --}} - - {{!-- TODO: shouldn't this be rendered when the result is empty too? Update docs! --}} - {{#if general.selectionIsEmpty}} - {{yield general to="general-menu"}} - {{/if}} - - {{#if enableSelection}} - - {{#unless selected.selectionIsEmpty}} - {{#if (has-block "selection-menu")}} - {{yield selected to="selection-menu"}} - {{else}} - {{#if (has-block "selection-menu-actions")}} - {{selected.selectionCount}} item(s) selected - - {{yield selected to="selection-menu-actions"}} - {{/if}} - {{/if}} - {{/unless}} - - {{/if}} -
- {{/if}} -
- {{!-- END: menu --}} - - {{!-- START: content --}} - - {{#if (has-block "content")}} - {{yield content to="content"}} - {{else}} -
- - {{!-- START: headers --}} - - {{#if (has-block "full-header")}} - {{yield header to="full-header"}} - {{else}} - - - {{#if header.enableSelection}} - - {{/if}} - {{#if header.enableLineNumbers}} - - {{/if}} - {{#if (has-block "data-headers")}} - {{yield header.dataHeadersInfo to="data-headers"}} - {{else}} - {{#each header.fields as |field|}} - - {{#if dataHeader.renderCustomBlock}} - {{yield dataHeader to="data-header"}} - {{else}} - {{#if dataHeader.isSortable}} - - {{else}} - - {{/if}} - {{/if}} - - {{/each}} - {{/if}} - {{#if (has-block "actions-header")}} - {{yield to="actions-header"}} - {{else}} - {{#if (or (has-block "actions") header.hasLinks)}} - - {{/if}} - {{/if}} - - - {{/if}} - - {{!-- END: headers --}} - - {{!-- START: body --}} - - {{#if (has-block "body")}} - {{yield body to="body"}} - {{else}} - - {{#if body.isLoading}} - {{#if (has-block "body-loading")}} - {{yield to="body-loading"}} - {{else}} - - {{/if}} - {{else}} - {{#if body.content}} - {{#each body.wrappedItems as |wrapper index|}} - - {{#if (has-block "row")}} - {{yield row to="row"}} - {{else}} - - {{#if row.enableSelection}} - - {{/if}} - {{#if row.enableLineNumbers}} - - {{/if}} - - {{#if (has-block "data-cells")}} - {{yield dataCells to="data-cells"}} - {{else}} - {{!-- NOTE: you may drop this {{#if dataCells.firstColumn}}...{{/if}} when no custom first column styling is needed --}} - {{#if dataCells.firstColumn}} - - {{#if (has-block "first-data-cell")}} - {{yield cell to="first-data-cell"}} - {{else if cell.renderCustomBlock}} - {{yield cell to="data-cell"}} - {{else}} - {{!-- TODO: This should be based on the type of the field --}} - {{#if cell.rowLink}} - - {{else}} - - {{/if}} - {{/if}} - - {{/if}} - {{!-- NOTE: if you dropped custom styling for dataCells.firstColumn then use {{#each dataCells.fields as |column|}}...{{/each}} --}} - {{#each dataCells.otherColumns as |column|}} - - {{#if (has-block "rest-data-cell")}} - {{yield cell to="rest-data-cell"}} - {{else if cell.renderCustomBlock}} - {{yield cell to="data-cell"}} - {{else}} - {{!-- TODO: This should be based on the type of the field --}} - {{#if cell.rowLink}} - - {{else}} - - {{/if}} - {{/if}} - - {{/each}} - {{/if}} - - {{#if (has-block "actions")}} - {{yield row to="actions"}} - {{else}} - {{#if row.linkedRoutes}} - - {{/if}} - {{/if}} - - {{/if}} - - {{/each}} - {{else}} - {{#if (has-block "no-data-message")}} - {{yield to="no-data-message"}} - {{else}} - - {{/if}} - {{/if}} - {{/if}} - - {{/if}} - - {{!-- END: body --}} -
{{!-- Checkbox --}}{{!-- Linenumbers --}} - - {{#if dataHeader.isSorted}}[{{dataHeader.sortDirection}}]{{/if}} - {{dataHeader.label}} - - {{dataHeader.label}}
Loading...
- {{input type="checkbox" checked=row.isSelected click=row.toggleSelected}} - {{row.lineNumber}} - - {{cell.value}} - - {{cell.value}} - - {{cell.value}} - - {{cell.value}} - {{#each row.linkedRoutes as |linkedRoute|}} - - {{or linkedRoute.label linkedRoute.route}} - - {{/each}} -

{{@noDataMessage}}

-
- {{/if}} -
- {{!-- END: content --}} - - {{!-- START: pagination --}} - - {{#if (has-block "pagination")}} - {{yield pagination to="pagination"}} - {{else}} -
-
- Displaying {{pagination.startItem}}-{{pagination.endItem}} - {{#if pagination.hasTotal}} of {{pagination.total}}{{/if}} - {{#if pagination.sizeOptions}} - | - - {{/if}} -
- {{#if pagination.hasMultiplePages}} -
- - - - - -
- {{/if}} -
- {{/if}} -
- {{!-- END: pagination --}} -
-
\ No newline at end of file diff --git a/addon/controller.js b/addon/controller.js deleted file mode 100644 index a8063f4..0000000 --- a/addon/controller.js +++ /dev/null @@ -1,26 +0,0 @@ -import { tracked } from '@glimmer/tracking'; -import Controller from '@ember/controller'; - -export default class DataTableController extends Controller { - queryParams = ['size', 'page', 'filter', 'sort']; - - @tracked size = 10; - @tracked page = 0; - @tracked filter = ''; - @tracked sort = ''; // TODO: perhaps undefined would be a nicer default for consumers - @tracked isLoadingModel = false; - - get view() { - return { - size: this.size, - page: this.page, - filter: this.filter, - sort: this.sort, - isLoading: this.isLoadingModel, - updatePage: (page) => this.page = page, - updatePageSize: (size) => this.size = size, - updateFilter: (filter) => this.filter = filter, - updateSort: (sort) => this.sort = sort - } - } -} diff --git a/addon/eslint.config.js b/addon/eslint.config.js new file mode 100644 index 0000000..5d85bc9 --- /dev/null +++ b/addon/eslint.config.js @@ -0,0 +1,3 @@ +import { configs } from '@nullvoxpopuli/eslint-configs'; + +export default configs.ember(import.meta.dirname); \ No newline at end of file diff --git a/addon/jsconfig.json b/addon/jsconfig.json new file mode 100644 index 0000000..0aeaed4 --- /dev/null +++ b/addon/jsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "types": [ + "@glint/environment-ember-loose", + "ember-source/types" + ], + "noEmit": true + }, + "include": [ + "**/*.js", + "**/*.gjs" + ], + "glint": { + "environment": [ + "ember-loose", + "ember-template-imports" + ] + }, +} diff --git a/addon/mixins/default-query-params.js b/addon/mixins/default-query-params.js deleted file mode 100644 index 42e7737..0000000 --- a/addon/mixins/default-query-params.js +++ /dev/null @@ -1,7 +0,0 @@ -import Mixin from '@ember/object/mixin'; - -export default Mixin.create({ - page: 0, - size: 10, - filter: '', -}); diff --git a/addon/mixins/route.js b/addon/mixins/route.js deleted file mode 100644 index 34bd61f..0000000 --- a/addon/mixins/route.js +++ /dev/null @@ -1,43 +0,0 @@ -/*jshint unused:false */ -/* eslint-disable ember/no-new-mixins */ - -import Mixin from '@ember/object/mixin'; -import merge from 'lodash/merge'; - -export default Mixin.create({ - queryParams: { - filter: { refreshModel: true }, - page: { refreshModel: true }, - size: { refreshModel: true }, - sort: { refreshModel: true }, - }, - mergeQueryOptions() { - return {}; - }, - model(params) { - const options = { - sort: params.sort, - page: { - number: params.page, - size: params.size, - }, - }; - // TODO: sending an empty filter param to backend returns [] - if (params.filter) { - options['filter'] = params.filter; - } - merge(options, this.mergeQueryOptions(params)); - return this.store.query(this.modelName, options); - }, - actions: { - loading(transition) { - let controller = this.controllerFor(this.routeName); - controller.set('isLoadingModel', true); - transition.promise.finally(function () { - controller.set('isLoadingModel', false); - }); - - return true; // bubble the loading event - }, - }, -}); diff --git a/addon/package.json b/addon/package.json new file mode 100644 index 0000000..b029a46 --- /dev/null +++ b/addon/package.json @@ -0,0 +1,106 @@ +{ + "name": "ember-data-table", + "version": "2.0.1", + "description": "Data tables for Ember following Google Design specs", + "keywords": [ + "ember-addon", + "mu-semtech" + ], + "repository": { + "type": "git", + "url": "https://github.com/mu-semtech/ember-data-table" + }, + "license": "MIT", + "author": "Erika Pauwels", + "directories": { + "doc": "doc", + "test": "tests" + }, + "dependencies": { + "@embroider/addon-shim": "^1.8.7", + "decorator-transforms": "^2.2.2", + "ember-concurrency": "^4.0.4", + "ember-truth-helpers": "^4.0.3", + "lodash.upperfirst": "^4.3.1" + }, + "engines": { + "node": ">= 22" + }, + "ember": { + "edition": "octane" + }, + "ember-addon": { + "main": "addon-main.js", + "type": "addon", + "version": 2, + "app-js": { + "./components/data-table.js": "./dist/_app_/components/data-table.js", + "./components/data-table/content-body.js": "./dist/_app_/components/data-table/content-body.js", + "./components/data-table/content-header.js": "./dist/_app_/components/data-table/content-header.js", + "./components/data-table/content.js": "./dist/_app_/components/data-table/content.js", + "./components/data-table/data-cell.js": "./dist/_app_/components/data-table/data-cell.js", + "./components/data-table/data-cells.js": "./dist/_app_/components/data-table/data-cells.js", + "./components/data-table/menu-general.js": "./dist/_app_/components/data-table/menu-general.js", + "./components/data-table/menu-selected.js": "./dist/_app_/components/data-table/menu-selected.js", + "./components/data-table/menu.js": "./dist/_app_/components/data-table/menu.js", + "./components/data-table/number-pagination.js": "./dist/_app_/components/data-table/number-pagination.js", + "./components/data-table/row.js": "./dist/_app_/components/data-table/row.js", + "./components/data-table/text-search.js": "./dist/_app_/components/data-table/text-search.js", + "./components/data-table/th-sortable.js": "./dist/_app_/components/data-table/th-sortable.js", + "./components/raw-data-table.js": "./dist/_app_/components/raw-data-table.js", + "./helpers/includes-by.js": "./dist/_app_/helpers/includes-by.js" + }, + "app-styles": { + "./styles/ember-data-table.scss": "./dist/styles/ember-data-table.scss" + } + }, + "volta": { + "extends": "../package.json" + }, + "devDependencies": { + "@babel/core": "^7.27.1", + "@babel/plugin-proposal-decorators": "^7.27.1", + "@babel/plugin-transform-class-properties": "^7.27.1", + "@babel/plugin-transform-class-static-block": "^7.27.1", + "@embroider/addon-dev": "^8.0.1", + "@nullvoxpopuli/eslint-configs": "^5.1.2", + "@rollup/plugin-babel": "^6.0.4", + "ember-template-lint": "^6.0.0", + "eslint": "^9.28.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-ember": "^12.2.1", + "eslint-plugin-n": "^16.6.2", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-qunit": "^8.1.2", + "prettier": "^3.3.3", + "rollup": "^4.41.0", + "rollup-plugin-scss": "^4.0.1", + "stylelint": "^16.20.0", + "stylelint-config-standard": "^38.0.0", + "stylelint-prettier": "^5.0.3", + "@glint/core": "^1.5.2", + "@glint/environment-ember-loose": "^1.5.2", + "@glint/environment-ember-template-imports": "^1.5.2", + "@glint/template": "^1.5.2" + }, + "exports": { + ".": "./dist/index.js", + "./*": "./dist/*.js", + "./addon-main.js": "./addon-main.js" + }, + "files": [ + "addon-main.js", + "dist" + ], + "scripts": { + "build": "rollup --config", + "lint": "concurrently 'npm:lint:*(!fix)' --names 'lint:'", + "lint:fix": "concurrently 'npm:lint:*:fix' --names 'fix:'", + "lint:css": "stylelint \"**/*.css\" --allow-empty-input", + "lint:css:fix": "concurrently \"npm:lint:css -- --fix\"", + "lint:js": "eslint . --cache", + "lint:js:fix": "eslint . --fix", + "prepack": "rollup --config", + "start": "rollup --config --watch --environment development" + } +} diff --git a/addon/prettier.config.js b/addon/prettier.config.js new file mode 100644 index 0000000..a97ba62 --- /dev/null +++ b/addon/prettier.config.js @@ -0,0 +1,12 @@ +export default { + plugins: ['prettier-plugin-ember-template-tag'], + overrides: [ + { + files: '*.{js,ts,gjs}', + options: { + singleQuote: true, + templateSingleQuote: false, + }, + }, + ], +}; diff --git a/addon/rollup.config.mjs b/addon/rollup.config.mjs new file mode 100644 index 0000000..586162f --- /dev/null +++ b/addon/rollup.config.mjs @@ -0,0 +1,84 @@ +import { Addon } from '@embroider/addon-dev/rollup'; +import { dirname,resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { babel } from '@rollup/plugin-babel'; +import scss from 'rollup-plugin-scss'; + +const addon = new Addon({ + srcDir: 'src', + destDir: 'dist', +}); + +export default { + // This provides defaults that work well alongside `publicEntrypoints` below. + // You can augment this if you need to. + output: addon.output(), + + plugins: [ + // These are the modules that users should be able to import from your + // addon. Anything not listed here may get optimized away. + // By default all your JavaScript modules (**/*.js) will be importable. + // But you are encouraged to tweak this to only cover the modules that make + // up your addon's public API. Also make sure your package.json#exports + // is aligned to the config here. + // See https://github.com/embroider-build/embroider/blob/main/docs/v2-faq.md#how-can-i-define-the-public-exports-of-my-addon + addon.publicEntrypoints([ + "**/*.js", + "index.js", + "template-registry.js", + "styles/ember-data-table.scss", + ]), + + // These are the modules that should get reexported into the traditional + // "app" tree. Things in here should also be in publicEntrypoints above, but + // not everything in publicEntrypoints necessarily needs to go here. + addon.appReexports([ + "components/**/*.js", + "helpers/**/*.js", + "modifiers/**/*.js", + "services/**/*.js", + ]), + + scss({ + fileName: 'styles/ember-data-table.scss', + outputStyle: 'expanded', + sourceMap: false, + failOnError: true, + includePaths: ['src/styles'], + }), + + // Follow the V2 Addon rules about dependencies. Your code can import from + // `dependencies` and `peerDependencies` as well as standard Ember-provided + // package names. + addon.dependencies(), + + // This babel config should *not* apply presets or compile away ES modules. + // It exists only to provide development niceties for you, like automatic + // template colocation. + // + // By default, this will load the actual babel config from the file + // babel.config.json. + babel({ + extensions: [".js", ".gjs", ".ts", ".gts"], + babelHelpers: "bundled", + configFile: resolve( + dirname(fileURLToPath(import.meta.url)), + "./babel.config.cjs" + ), + }), + + // Ensure that standalone .hbs files are properly integrated as Javascript. + addon.hbs(), + + // Ensure that .gjs files are properly integrated as Javascript + addon.gjs(), + + // addons are allowed to contain imports of .css files, which we want rollup + // to leave alone and keep in the published output. + addon.keepAssets(["**/*.css"]), + + // Remove leftover build artifacts when starting a new build. + addon.clean(), + ], +}; \ No newline at end of file diff --git a/addon/src/components/data-table.gjs b/addon/src/components/data-table.gjs new file mode 100644 index 0000000..8c407b5 --- /dev/null +++ b/addon/src/components/data-table.gjs @@ -0,0 +1,378 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { assert } from '@ember/debug'; +import { hash } from '@ember/helper'; +import { action } from '@ember/object'; +import { isEmpty , typeOf } from '@ember/utils'; + +import { or } from 'ember-truth-helpers'; + +import attributeToSortParams from '../utils/attribute-to-sort-params'; +import get from '../utils/get'; +import { + definitionsToArray, + toComponentSpecification, +} from '../utils/string-specification-helpers'; +import DataTableDataTableContent from './data-table/content.gjs'; +import DataTableDataTableMenu from './data-table/menu.gjs'; +import DataTableNumberPagination from './data-table/number-pagination.gjs'; +import DataTableTextSearch from './data-table/text-search.gjs'; + +const DEFAULT_DEBOUNCE_TIME = 2000; + +export default class DataTable extends Component { + + @tracked _selection = undefined; + + get filter() { + return this.args.filter; + } + + get sort() { + return this.args.sort; + } + + get selection() { + if(this.args.selection !== undefined) { + return this.args.selection; + } + + if ( + this._selection === undefined && + this.args.initialSelection === undefined + ) + return []; + else if (this._selection !== undefined) return this._selection; + else return this.args.initialSelection; + } + + set selection(newSelection) { + if (this.args.selection !== undefined) { + const updater = this.args.updateSelection; + + if(!updater) { + assert( + `Could not update selection because @updateSelection was not supplied to data table, but @selection was.`, + ); + } else { + updater(newSelection); + } + } else { + this._selection = newSelection; + } + + } + + get noDataMessage() { + return this.args.noDataMessage === undefined + ? 'No data' + : this.args.noDataMessage; + } + + get isLoading() { + return this.args.isLoading; + } + + /** + * Calculates the search debounce time. + * + * A shorthand form is supported in which the user supplies a + * number to autoSearch in which case we use that. This would not + * work with 0 (which is a strange debounce time in itself) so this + * option exists for now. + */ + get searchDebounceTime() { + return isNaN(this.args.autoSearch) + ? DEFAULT_DEBOUNCE_TIME + : this.args.autoSearch; + } + + get enableSelection() { + return this.args.enableSelection; + } + + get selectionIsEmpty() { + return this.selection.length === 0; + } + + get page() { + return this.args.page || 0; + } + + get size() { + return this.args.size || 5; + } + + get sizeOptions() { + const sizeOptions = + this.args.sizes === undefined + ? [5, 10, 25, 50, 100] + : definitionsToArray(this.args.sizes).map((nrOrStr) => + parseInt(nrOrStr), + ); + + if (isEmpty(sizeOptions)) { + return null; + } else { + if (!sizeOptions.includes(this.size) && this.size) { + sizeOptions.push(this.size); + } + + sizeOptions.sort((a, b) => a - b); + + return sizeOptions; + } + } + + get enableSearch() { + return this.args.enableSearch === undefined + ? this.filter !== undefined + : this.args.enableSearch; + } + + get autoSearch() { + return this.args.autoSearch === undefined ? true : this.args.autoSearch; + } + + get linksModelProperty() { + return this.args.linksModelProperty === undefined + ? 'id' + : this.args.linksModelProperty; + } + + get rowLinkModelProperty() { + return this.args.rowLinkModelProperty === undefined + ? 'id' + : this.args.rowLinkModelProperty; + } + + attributeToSortParams(attribute) { + if (this.args.attributeToSortParams) { + return this.args.attributeToSortParams(attribute); + } else { + return attributeToSortParams(attribute); + } + } + + get fields() { + // this.args.fields can be: + // - a string => split up to array, use component specification logic to get the meta object + // - an array => map every value depending on its type: + // - if a string => use component specification logic + // - if an object => use the object as is, override `attribute` and `label` with component specification logic + // this always passing all parameters to `@fields` + const fields = definitionsToArray(this.args.fields); + const fieldsWithMeta = fields.map((field) => { + return { + ...(typeOf(field) === 'string' ? {} : field), + ...toComponentSpecification(field, [ + { raw: 'attribute' }, + { name: 'label', default: 'attribute' }, + ]), + }; + }); + + return fieldsWithMeta.map( + ({ + attribute, + label, + sortParameters, + isSortable, + hasCustomHeader, + isCustom, + customFieldComponent, + customHeaderComponent, + }) => ({ + attribute, + label, + sortParameters: + sortParameters || // custom format says it's sortable + ((isSortable || // custom format says it's sortable + this.sortableFields == null || // default: all fields are sortable + this.sortableFields?.includes(attribute)) && // @sortableFields + this.attributeToSortParams(attribute)), + get isSortable() { + return Object.keys(this.sortParameters || {}).length >= 1; + }, + hasCustomHeader: + hasCustomHeader || this.customHeaders.includes(attribute), + isCustom: isCustom || this.customFields.includes(attribute), + customFieldComponent: + customFieldComponent || this.customFieldComponents[attribute] || null, + customHeaderComponent: + customHeaderComponent || + this.customHeaderComponents[attribute] || + null, + }), + ); + } + + get customHeaders() { + const headers = this.args.customHeaders; + + if (typeOf(headers) === 'object') { + return Object.keys(headers).filter((attr) => isEmpty(headers[attr])); + } else { + return definitionsToArray(headers); + } + } + + get customFields() { + const fields = this.args.customFields; + + if (typeOf(fields) === 'object') { + return Object.keys(fields).filter((attr) => isEmpty(fields[attr])); + } else { + return definitionsToArray(fields); + } + } + + get customFieldComponents() { + const fields = this.args.customFields; + + return typeOf(fields) === 'object' ? fields : {}; + } + + get customHeaderComponents() { + const headers = this.args.customHeaders; + + return typeOf(headers) === 'object' ? headers : {}; + } + + get sortableFields() { + const sortableFields = this.args.sortableFields; + + if (sortableFields || sortableFields === '') + return definitionsToArray(sortableFields); + // default: all fields are sortable + else return null; + } + + get searchPlaceholder() { + return this.args.searchPlaceholder === undefined + ? 'Search input' + : this.args.searchPlaceholder; + } + + @action + updatePageSize(size) { + const updater = this.args.updatePageSize; + + if (!updater) { + assert( + `Could not update page size to ${size} because @updatePageSize was not supplied to data table`, + ); + } else { + this.updatePage(0); + updater(size); + } + } + + @action + updateFilter(filter) { + const updater = this.args.updateFilter; + + if (!updater) { + assert( + `Could not update filter to '${filter}' because @updateFilter was not supplied to data table`, + ); + } else { + this.updatePage(0); + updater(filter); + } + } + + @action + updateSort(sort) { + const updater = this.args.updateSort; + + if (!updater) { + assert( + `Could not update sorting to '${sort}' because @updateSort was not supplied to data table`, + ); + } else { + this.updatePage(0); + updater(sort); + } + } + + @action + updatePage(page) { + const updater = this.args.updatePage; + + if (!updater) { + console.error( + `Could not update page to ${page} because @updatePage was not supplied to data table`, + ); + } else { + updater(page); + } + } + + @action + addItemToSelection(item) { + this.removeItemFromSelection(item); // in case the item was already selected + this.selection = [...this.selection, item]; // create new array to trigger setter of `selection` + } + @action + removeItemFromSelection(item) { + const byPath = this.args.selectionProperty; + + this.selection = this.selection.filter( + (x) => get(x, byPath) !== get(item, byPath), + ); + } + @action + clearSelection() { + this.selection = []; + } +} diff --git a/addon/src/components/data-table/content-body.gjs b/addon/src/components/data-table/content-body.gjs new file mode 100644 index 0000000..262e451 --- /dev/null +++ b/addon/src/components/data-table/content-body.gjs @@ -0,0 +1,63 @@ +import Component from '@glimmer/component'; +import { hash } from '@ember/helper'; + +import and from 'ember-truth-helpers/helpers/and'; +import or from 'ember-truth-helpers/helpers/or'; + +import DataTableRow from './row.gjs'; + +/* Used in: data-table/content */ +export default class DataTableContentBodyComponent extends Component { + + + get offset() { + var offset = 1; //to avoid having 0. row + var page = this.args.dataTable.page; // TODO: pass on page directly? + var size = this.args.dataTable.size; // TODO: pass on size directly? + + if (page && size) { + offset += page * size; + } + + return offset; + } + + updateSelection = (item, event) => { + if (event.target.checked) this.args.dataTable.addItemToSelection(item); + else this.args.dataTable.removeItemFromSelection(item); + } +} diff --git a/addon/src/components/data-table/content-header.gjs b/addon/src/components/data-table/content-header.gjs new file mode 100644 index 0000000..132e7a4 --- /dev/null +++ b/addon/src/components/data-table/content-header.gjs @@ -0,0 +1,26 @@ +import { hash } from '@ember/helper'; + +import DataTableThSortable from './th-sortable.gjs'; +/* Used in: data-table/content */ + diff --git a/addon/src/components/data-table/content.gjs b/addon/src/components/data-table/content.gjs new file mode 100644 index 0000000..fa03bec --- /dev/null +++ b/addon/src/components/data-table/content.gjs @@ -0,0 +1,76 @@ +import Component from '@glimmer/component'; +import { hash } from '@ember/helper'; + +import { toComponentSpecifications } from '../../utils/string-specification-helpers'; +import DataTableDataTableContentBody from './content-body.gjs'; +import DataTableDataTableContentHeader from './content-header.gjs'; + +/* Used in: data-table.hbs */ +export default class DataTableContentComponent extends Component { + + get hasLinks() { + return this.linkedRoutes.length > 0; + } + + /** + * Accepts and transforms definitions for linked routes. + * + * Implementations may transform this at will. The default + * transformation splits on `:` assuming the first part is the route + * and the second part is the label. If no label is given, it is + * passed as null. If a label is given, all underscores are + * transformed to spaces and double underscores are left as a single + * _. We split again on a third `:`, transforming in the same way for + * the suggested icon. + * + * Behaviour for `___` is undefined. + * + * Can pass a space-separated string or an array. + * The array can already contain an object with the transformed link + * + * Yields an array of objects to represent the linked routes. + * [ { route: "products.show", label: "Show product", icon: "show-icon" } ] + */ + get linkedRoutes() { + return toComponentSpecifications(this.args.links || '', [ + { raw: 'route' }, + 'label', + 'icon', + ]).map((spec) => { + spec.linksModelProperty = this.args.linksModelProperty; + + return spec; + }); + } +} diff --git a/addon/src/components/data-table/data-cell.gjs b/addon/src/components/data-table/data-cell.gjs new file mode 100644 index 0000000..19d31ef --- /dev/null +++ b/addon/src/components/data-table/data-cell.gjs @@ -0,0 +1,36 @@ +import Component from '@glimmer/component'; +import { get,hash } from '@ember/helper'; + +/* Used in: data-table/data-cells */ +export default class DataTableDataCellComponent extends Component { + + get isCustom() { + return this.args.column.isCustom; + } + + get hasCustomFields() { + return this.args.fields.find(({ isCustom }) => isCustom) || false; + } + + get renderCustomBlock() { + return this.args.hasCustomBlock && this.isCustom; + } +} diff --git a/addon/src/components/data-table/data-cells.gjs b/addon/src/components/data-table/data-cells.gjs new file mode 100644 index 0000000..7c67bf6 --- /dev/null +++ b/addon/src/components/data-table/data-cells.gjs @@ -0,0 +1,44 @@ +import Component from '@glimmer/component'; +import { hash } from '@ember/helper'; + +import DataTableDataCell from './data-cell.gjs'; + +/* Used in: data-table/row */ +export default class DataTableDataCellsComponent extends Component { + + get firstColumnField() { + return this.args.fields?.[0] || null; + } + + get otherColumnFields() { + if (this.args.fields?.length) { + let [, ...fields] = this.args.fields; + + return fields; + } else { + return []; + } + } +} diff --git a/addon/src/components/data-table/menu-general.gjs b/addon/src/components/data-table/menu-general.gjs new file mode 100644 index 0000000..8017498 --- /dev/null +++ b/addon/src/components/data-table/menu-general.gjs @@ -0,0 +1,8 @@ +import { hash } from '@ember/helper'; + +/* Used in: data-table/menu */ + diff --git a/addon/src/components/data-table/menu-selected.gjs b/addon/src/components/data-table/menu-selected.gjs new file mode 100644 index 0000000..99e9d20 --- /dev/null +++ b/addon/src/components/data-table/menu-selected.gjs @@ -0,0 +1,26 @@ +import Component from '@glimmer/component'; +import { hash } from '@ember/helper'; + +/* Used in: data-table/menu */ +export default class DataTableMenuSelectedComponent extends Component { + + get selectionCount() { + return this.args.dataTable.selection.length; + } + + get copiedSelection() { + return [...this.args.dataTable.selection]; + } +} diff --git a/addon/src/components/data-table/menu.gjs b/addon/src/components/data-table/menu.gjs new file mode 100644 index 0000000..0f0b377 --- /dev/null +++ b/addon/src/components/data-table/menu.gjs @@ -0,0 +1,21 @@ +import { hash } from '@ember/helper'; + +import DataTableDataTableMenuGeneral from './menu-general.gjs'; +import DataTableDataTableMenuSelected from './menu-selected.gjs'; + +/* Used in: data-table.hbs */ + diff --git a/addon/components/data-table/number-pagination.js b/addon/src/components/data-table/number-pagination.gjs similarity index 66% rename from addon/components/data-table/number-pagination.js rename to addon/src/components/data-table/number-pagination.gjs index 61516b9..9aa5809 100644 --- a/addon/components/data-table/number-pagination.js +++ b/addon/src/components/data-table/number-pagination.gjs @@ -1,5 +1,6 @@ -import { action } from '@ember/object'; import Component from '@glimmer/component'; +import { hash } from '@ember/helper'; +import { action } from '@ember/object'; const humanPageOffset = 1; // humans assume the first page has number 1 @@ -33,6 +34,7 @@ function zeroToHumanBased(number) { return number + humanPageOffset; } +/* Used in: data-table.hbs */ /** * Helpers for pagination buttons. * @@ -46,10 +48,40 @@ function zeroToHumanBased(number) { * yielded block are what humans would understand. */ export default class NumberPaginationComponent extends Component { + get currentBackendPage() { - return this.args.page - ? parseInt(this.args.page) - : this.backendPageOffset; + return this.args.page ? parseInt(this.args.page) : this.backendPageOffset; } /** @@ -58,10 +90,10 @@ export default class NumberPaginationComponent extends Component { * hope. */ get backendPageOffset() { - if( this.args.backendPageOffset !== undefined ) { + if (this.args.backendPageOffset !== undefined) { // users may supply this return this.args.backendPageOffset; - } else if( this.args.meta?.links?.first?.number !== undefined ) { + } else if (this.args.meta?.links?.first?.number !== undefined) { // or we could derive from the backend return this.args.meta.links.first.number; } else { @@ -76,7 +108,10 @@ export default class NumberPaginationComponent extends Component { * what the API supplies. */ get humanPage() { - return backendToHuman(this.args.page || this.backendPageOffset, this.backendPageOffset); + return backendToHuman( + this.args.page || this.backendPageOffset, + this.backendPageOffset, + ); } set humanPage(number) { this.updatePage(humanToBackend(number || 0, this.backendPageOffset)); @@ -91,19 +126,15 @@ export default class NumberPaginationComponent extends Component { } get lastPage() { - return Math.ceil( (0.0 + this.total) / this.args.size); + return Math.ceil((0.0 + this.total) / this.args.size); } get previousPage() { - return this.isFirstPage - ? undefined - : this.humanPage - 1; + return this.isFirstPage ? undefined : this.humanPage - 1; } get nextPage() { - return this.isLastPage - ? undefined - : this.humanPage + 1; + return this.isLastPage ? undefined : this.humanPage + 1; } get isFirstPage() { @@ -126,26 +157,27 @@ export default class NumberPaginationComponent extends Component { return this.lastPage > this.firstPage; } - get startItem() { + get startIndex() { // note, you might want to use this.args.page instead, but given // that comes from the backend, it's *not* guaranteed to be // zero-based either. - if( this.args.itemsOnCurrentPage == 0 && this.isFirstPage ) + if (this.args.itemsOnCurrentPage == 0 && this.isFirstPage) // human probably expects to see 0-0 when no items exist. return 0; else - return zeroToHumanBased(this.args.size * humanToZeroBased( this.humanPage )); + return zeroToHumanBased( + this.args.size * humanToZeroBased(this.humanPage), + ); } - get endItem() { + get endIndex() { // this one is exactly the same number as humanPageOffset yet it has // a different meaning. When summing up lists, it's effectively // removing one regardless of the offset. - if( this.args.itemsOnCurrentPage == 0 && this.isFirstPage ) + if (this.args.itemsOnCurrentPage == 0 && this.isFirstPage) // human probably expects to see 0-0 when no items exist. return 0; - else - return this.startItem - 1 + this.args.itemsOnCurrentPage; + else return this.startIndex - 1 + this.args.itemsOnCurrentPage; } get numberOfPages() { @@ -158,7 +190,7 @@ export default class NumberPaginationComponent extends Component { get pageOptions() { return Array.from( new Array(this.numberOfPages), - (_val, index) => this.firstPage + index + (_val, index) => this.firstPage + index, ); } @@ -179,8 +211,13 @@ export default class NumberPaginationComponent extends Component { const x = this.firstPage; const leftWindow = [x, x + 1, x + 2].filter((i) => i <= this.lastPage); const y = this.lastPage; - const rightWindow = [y - 2, y - 1, y].filter((i) => i >= this.firstPage); - const pages = [...new Set([...leftWindow, ...rightWindow])].sort((a, b) => a - b); + const rightWindow = [y - 2, y - 1, y].filter( + (i) => i >= this.firstPage, + ); + const pages = [...new Set([...leftWindow, ...rightWindow])].sort( + (a, b) => a - b, + ); + if (pages.length == 6 && pages[2] < pages[3] - 1) { return [...leftWindow, more, ...rightWindow]; } else { @@ -189,20 +226,31 @@ export default class NumberPaginationComponent extends Component { } else { const x = this.humanPage; const currentPageWindow = [x - 2, x - 1, x, x + 1, x + 2].filter( - (i) => i >= this.firstPage && i <= this.lastPage + (i) => i >= this.firstPage && i <= this.lastPage, ); let prepend = []; let append = []; + if (currentPageWindow.length) { const first = currentPageWindow[0]; + if (first > this.firstPage) { - prepend = first == this.firstPage + 1 ? [this.firstPage] : [this.firstPage, more]; + prepend = + first == this.firstPage + 1 + ? [this.firstPage] + : [this.firstPage, more]; } + const last = currentPageWindow[currentPageWindow.length - 1]; + if (last < this.lastPage) { - append = last == this.lastPage - 1 ? [this.lastPage] : [more, this.lastPage]; + append = + last == this.lastPage - 1 + ? [this.lastPage] + : [more, this.lastPage]; } } + return [...prepend, ...currentPageWindow, ...append]; } } else { @@ -211,12 +259,9 @@ export default class NumberPaginationComponent extends Component { } get total() { - if( this.args.total !== undefined ) - return this.args.total; - else if( this.args.meta?.count !== undefined ) - return this.args.meta.count; - else - return undefined; + if (this.args.total !== undefined) return this.args.total; + else if (this.args.meta?.count !== undefined) return this.args.meta.count; + else return undefined; } get hasTotal() { diff --git a/addon/src/components/data-table/row.gjs b/addon/src/components/data-table/row.gjs new file mode 100644 index 0000000..e9df5a3 --- /dev/null +++ b/addon/src/components/data-table/row.gjs @@ -0,0 +1,73 @@ +import Component from '@glimmer/component'; +import { fn, hash } from '@ember/helper'; +import { action, get } from '@ember/object'; +import { service } from '@ember/service'; + +import includesBy from '../../helpers/includes-by.js'; +import DataTableDataCells from './data-cells.gjs'; + +/* Used in: data-table/content-body */ +export default class DataTableRowComponent extends Component { + + @service router; + + add = (a, b) => a + b; + + get linkedRoutes() { + return this.args.linkedRoutes.map((linkedRoute) => { + const model = this.args.item; + + return Object.assign( + { + model: linkedRoute.linksModelProperty + ? get(model, linkedRoute.linksModelProperty) + : model, + }, + linkedRoute, + ); + }); + } + + get rowLinkModel() { + const { item, rowLinkModelProperty } = this.args; + + return rowLinkModelProperty ? get(item, rowLinkModelProperty) : item; + } + + @action + rowClicked() { + if (this.args.onClickRow) { + this.args.onClickRow(...arguments); + } else if (this.args.rowLink) { + this.router.transitionTo(this.args.rowLink, this.rowLinkModel); + } + } +} diff --git a/addon/src/components/data-table/text-search.gjs b/addon/src/components/data-table/text-search.gjs new file mode 100644 index 0000000..e207f8f --- /dev/null +++ b/addon/src/components/data-table/text-search.gjs @@ -0,0 +1,57 @@ +import Component from '@glimmer/component'; +import { hash } from '@ember/helper'; +import { action } from '@ember/object'; +import { restartableTask, timeout } from 'ember-concurrency'; + +/* Used in: data-table.hbs */ +export default class TextSearchComponent extends Component { + + enteredValue = undefined; + + debouncedSubmit = restartableTask(async () => { + await timeout(this.args.searchDebounceTime); + this.args.updateFilter(this.enteredValue); + }); + + @action + handleAutoInput(event) { + this.enteredValue = event.target.value; + this.debouncedSubmit.perform(); + } + + @action + handleInput(event) { + this.enteredValue = event.target.value; + + if (this.args.autoSearch !== false) { + this.debouncedSubmit.perform(); + } + } + + submitCurrent() { + this.args.updateFilter(this.enteredValue); + } + + @action + handleDirectInput(event) { + this.enteredValue = event.target.value; + } + + @action + submitForm(event) { + event.preventDefault(); + this.submitCurrent(); + } +} diff --git a/addon/components/data-table/th-sortable.js b/addon/src/components/data-table/th-sortable.gjs similarity index 51% rename from addon/components/data-table/th-sortable.js rename to addon/src/components/data-table/th-sortable.gjs index abc00a3..90b9800 100644 --- a/addon/components/data-table/th-sortable.js +++ b/addon/src/components/data-table/th-sortable.gjs @@ -1,25 +1,44 @@ -import { action } from '@ember/object'; import Component from '@glimmer/component'; +import { hash } from '@ember/helper'; +import { action } from '@ember/object'; +/* Used in: data-table/content-header */ export default class ThSortableComponent extends Component { + get sortParameters() { return this.args.field.sortParameters; } get sortDirection() { - for ( const key in this.sortParameters ) - if( this.args.sort == this.sortParameters[key] ) - return key; + for (const key in this.sortParameters) + if (this.args.sort == this.sortParameters[key]) return key; return ''; } get isAscending() { - return this.sortDirection === "asc"; + return this.sortDirection === 'asc'; } get isDescending() { - return this.sortDirection === "desc"; + return this.sortDirection === 'desc'; } get isSorted() { @@ -27,13 +46,7 @@ export default class ThSortableComponent extends Component { } get renderCustomBlock() { - // render the custom block when this header is custom or when a - // custom block was given and no specific headers were supplied to - // be custom. - // - // Note: data table can't make this decision because it doesn't know - // whether a custom block was supplied. - return this.args.hasCustomBlock && (this.isCustom || !this.hasCustomHeaders); + return this.args.hasCustomBlock && this.isCustom; } get isCustom() { @@ -41,22 +54,26 @@ export default class ThSortableComponent extends Component { } get hasCustomHeaders() { - return this.args.fields.find(({hasCustomHeader}) => hasCustomHeader) || false; + return ( + this.args.fields.find(({ hasCustomHeader }) => hasCustomHeader) || false + ); } get availableSortOptions() { const options = []; - Object - .keys( this.sortParameters ) + + Object.keys(this.sortParameters) .sort() // for asc and desc, asc first then desc, the rest also sorted for now - .map( (key) => options.push(key) ); + .map((key) => options.push(key)); options.push(''); // no sorting + return options; } get nextSort() { // wrapping loop over availableSortOptions const opts = this.availableSortOptions; + return opts[(opts.indexOf(this.sortDirection) + 1) % opts.length]; } diff --git a/addon/src/components/raw-data-table.gjs b/addon/src/components/raw-data-table.gjs new file mode 100644 index 0000000..4be6e5d --- /dev/null +++ b/addon/src/components/raw-data-table.gjs @@ -0,0 +1,482 @@ +import { concat, fn } from '@ember/helper'; +import { on } from '@ember/modifier'; +import { LinkTo } from '@ember/routing'; + +import { and } from 'ember-truth-helpers'; +import eq from 'ember-truth-helpers/helpers/eq'; +import notEq from 'ember-truth-helpers/helpers/not-eq'; +import or from 'ember-truth-helpers/helpers/or'; + +import DataTable from './data-table.gjs'; + +const ClickableRow = + +export default diff --git a/addon/src/config/environment.js b/addon/src/config/environment.js new file mode 100644 index 0000000..683607e --- /dev/null +++ b/addon/src/config/environment.js @@ -0,0 +1,3 @@ +export default function (/* environment, appConfig */) { + return {}; +} \ No newline at end of file diff --git a/addon/src/helpers/includes-by.js b/addon/src/helpers/includes-by.js new file mode 100644 index 0000000..61786ad --- /dev/null +++ b/addon/src/helpers/includes-by.js @@ -0,0 +1,14 @@ +import { helper } from '@ember/component/helper'; + +import get from '../utils/get'; + +// todo: change to normal function helper, instead of separate function, in ember 4 +export function includesBy(array, obj, byPath) { + const valueByPath = get(obj, byPath); + + return !!array.find(a => get(a, byPath) === valueByPath); +} + +export default helper(function includesByHelper([array, obj, byPath]) { + return includesBy(array, obj, byPath); +}); diff --git a/app/styles/ember-data-table.scss b/addon/src/styles/ember-data-table.scss similarity index 100% rename from app/styles/ember-data-table.scss rename to addon/src/styles/ember-data-table.scss diff --git a/addon/utils/attribute-to-sort-params.js b/addon/src/utils/attribute-to-sort-params.js similarity index 99% rename from addon/utils/attribute-to-sort-params.js rename to addon/src/utils/attribute-to-sort-params.js index a77df92..1fede60 100644 --- a/addon/utils/attribute-to-sort-params.js +++ b/addon/src/utils/attribute-to-sort-params.js @@ -1,4 +1,5 @@ export default function attributeToSortParams(attribute) { const attr = attribute.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); + return { asc: attr, desc: `-${attr}` }; } diff --git a/addon/src/utils/get.js b/addon/src/utils/get.js new file mode 100644 index 0000000..4822e63 --- /dev/null +++ b/addon/src/utils/get.js @@ -0,0 +1,9 @@ +import { get as emberGet } from '@ember/object'; +import { isEmpty } from '@ember/utils'; + +// Same as the ember `get`, but returns the original object if the path does not exist. +export default function get(obj, path) { + if(isEmpty(path)) return obj; + + return emberGet(obj, path); +} diff --git a/addon/src/utils/string-specification-helpers.js b/addon/src/utils/string-specification-helpers.js new file mode 100644 index 0000000..dd1ed20 --- /dev/null +++ b/addon/src/utils/string-specification-helpers.js @@ -0,0 +1,122 @@ +import { typeOf } from "@ember/utils"; + +import upperFirst from "lodash.upperfirst"; + +/** + * Splits a string of definitions by space. + */ +export function splitDefinitions(string) { + return (string || "") + .split(" ") + .filter((x) => x !== ""); +} + +/** + * Splits a string of definitions by space, or returns the array directly. + */ +export function definitionsToArray(stringOrArray) { + if(typeOf(stringOrArray) === "array") { + return stringOrArray; + } else { + return splitDefinitions(stringOrArray); + } +} + +/** + * Transforms __ to _ and _ to space. + */ +export function deUnderscoreString(string) { + const arrString = []; + + // executing this with a regex turned out to be less clear + let idx = 0; + + while( idx < string.length ) { + let current = string[idx]; + let next = string[idx+1]; + + if( current === "_" && next === "_") { + arrString.push("_"); + idx = idx + 2; + } else if( current === "_" ) { + arrString.push(" "); + idx = idx + 1; + } else { + arrString.push(current); + idx = idx + 1; + } + } + + return arrString.join(""); +} + +/** + * Unpacks the components of a series of name/label specifications split + * by spaces (top-level) and : lower-level, including the unpacking of + * _. + * + * configuration is an array of components to be recognized. In case of + * a simple string, the item is placed under that key in the returned + * object and rawLabel is used to provide the unparsed value (without + * clearing _). An object may be supplied for further unpacking which + * may contain a combination of the following key/values: + * - `raw: "label"` : Store the unparsed value in `label`. Do not parse the value. + * - `name: "label"`: store the parsed value in `label` and unparsed value in `rawLabel`. + * - `default: "label"` : Use the previously stored value for label as the + * default value if no value was supplied or if an empty value was + * supplied. Must be used together with `raw` or `name`. + * + * Unspecified components will receive the value `null`. + * + * toComponentSpecifications( "number:Nr. location:Gemeente_en_straat land", [{raw: "attribute"},{name: "label", default: "attribute"}]) + * -> [{attribute:"number", label: "Nr.", rawLabel: "Nr."},{attribute:"location",label:"Gemeente en straat",rawLabel:"Gemeente_en_straat"},{attribute:"land",label:"land"}] + */ +export function toComponentSpecifications(spaceSeparatedSpecifications, configuration) { + return definitionsToArray(spaceSeparatedSpecifications) + .map( (specification) => toComponentSpecification(specification, configuration) ); +} + +/** + * see toComponentSpecifications. This also handles objects with already unpacked specifications. + */ +export function toComponentSpecification(specification, configuration) { + let obj = {}; + const component = (i, key, parser = (str) => str) => { + if(typeOf(specification) === 'string') { + const spec = specification.split(':')[i] || null; + + return spec && parser(spec) + }else { + return specification[key] || null; + } + } + + for (let i = 0; i < configuration.length; i++) { + let spec = configuration[i]; + + if (typeOf(spec) === 'string') { + spec = { name: spec }; + } + + if (!spec.name && !spec.raw && !spec.default) { + throw `Specification ${JSON.stringify(spec)} not understood`; + } + + if (spec.raw) { + obj[spec.raw] = component(i, spec.raw); + } + else if (spec.name) { + obj[spec.name] = component(i, spec.name, deUnderscoreString); + obj[`raw${upperFirst(spec.name)}`] = component(i, spec.name); + } + + if (spec.default && spec.raw && !obj[spec.raw]) { + obj[spec.raw] = obj[spec.default]; + } + else if (spec.default && spec.name && !obj[spec.name]) { + obj[spec.name] = obj[spec.default]; + } + } + + return obj; +} diff --git a/addon/stylelint.config.js b/addon/stylelint.config.js new file mode 100644 index 0000000..ac46be9 --- /dev/null +++ b/addon/stylelint.config.js @@ -0,0 +1,3 @@ +export default { + extends: ['stylelint-config-standard', 'stylelint-prettier/recommended'], +}; diff --git a/addon/utils/string-specification-helpers.js b/addon/utils/string-specification-helpers.js deleted file mode 100644 index 0818b45..0000000 --- a/addon/utils/string-specification-helpers.js +++ /dev/null @@ -1,88 +0,0 @@ -import { upperFirst } from "lodash"; - -/** - * Splits a string of defitinions by space. - */ -export function splitDefinitions(string) { - return (string || "") - .split(" ") - .filter((x) => x !== ""); -} - -/** - * Transforms __ to _ and _ to space. - */ -export function deUnderscoreString(string) { - const arrString = []; - - // executing this with a regex turned out to be less clear - let idx = 0; - while( idx < string.length ) { - let current = string[idx]; - let next = string[idx+1]; - - if( current === "_" && next === "_") { - arrString.push("_"); - idx = idx + 2; - } else if( current === "_" ) { - arrString.push(" "); - idx = idx + 1; - } else { - arrString.push(current); - idx = idx + 1; - } - } - return arrString.join(""); -} - -/** - * Unpacks the components of a series of name/label specifications split - * by spaces (top-level) and : lower-level, including the unpacking of - * _. - * - * configuration is an array of components to be recognized. In case of - * a simple string, the item is placed under that key in the returned - * object and rawLabel is used to provide the unparsed value (without - * clearing _). An object may be supplied for further unpacking which - * may contain the following key/values: - * - * - raw: label : Store the raw value as label, do not process further. - * - default: label : Use the previously parsed value for label as the * - * default value if no value was supplied or if an empty value was - * supplied, must also supply name as key. - * - * toComponentSpecifications( "number:Nr. location:Gemeente_en_straat land", [{raw: "attribute"},{name: "label", default: "attribute"}]) - * -> [{attribute:"number", label: "Nr."},{attribute:"location",label:"Gemeente en straat",rawLabel:"Gemeente_en_straat"},{attribute:"land",label:"land"}] - */ -export function toComponentSpecifications(string, configuration) { - return splitDefinitions(string) - .map( (specification) => { - let obj = {}; - let components = specification.split(":"); - for ( let i = 0; i < configuration.length; i++ ) { - const spec = configuration[i]; - const component = components[i]; - if ( typeof spec === "string" ) { - obj[`raw${upperFirst(spec)}`] = component; - obj[spec] = deUnderscoreString(component || ""); - } else { - // object specification - if (spec.raw) { - obj[spec.raw] = component; - } - if (spec.name) { - if (spec.default && !component) { - obj[spec.name] = obj[spec.default]; - } else { - obj[`raw${upperFirst(spec.name)}`] = component; - obj[spec.name] = deUnderscoreString(component || ""); - } - } - if (!spec.raw && !spec.default) { - throw `Specification ${JSON.stringify(spec)} not understood`; - } - } - } - return obj; - } ); -} diff --git a/app/components/data-table.js b/app/components/data-table.js deleted file mode 100644 index 3602cd5..0000000 --- a/app/components/data-table.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'ember-data-table/components/data-table'; diff --git a/app/components/data-table/data-cell.js b/app/components/data-table/data-cell.js deleted file mode 100644 index 11d7dae..0000000 --- a/app/components/data-table/data-cell.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'ember-data-table/components/data-table/data-cell'; diff --git a/app/components/data-table/data-cells.js b/app/components/data-table/data-cells.js deleted file mode 100644 index e68d66a..0000000 --- a/app/components/data-table/data-cells.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'ember-data-table/components/data-table/data-cells'; \ No newline at end of file diff --git a/app/components/data-table/data-table-content-body.js b/app/components/data-table/data-table-content-body.js deleted file mode 100644 index 8ce1b62..0000000 --- a/app/components/data-table/data-table-content-body.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'ember-data-table/components/data-table/data-table-content-body'; diff --git a/app/components/data-table/data-table-content-header.js b/app/components/data-table/data-table-content-header.js deleted file mode 100644 index ad8e187..0000000 --- a/app/components/data-table/data-table-content-header.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'ember-data-table/components/data-table/data-table-content-header'; diff --git a/app/components/data-table/data-table-content.js b/app/components/data-table/data-table-content.js deleted file mode 100644 index 672dc51..0000000 --- a/app/components/data-table/data-table-content.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'ember-data-table/components/data-table/data-table-content'; diff --git a/app/components/data-table/data-table-menu-general.js b/app/components/data-table/data-table-menu-general.js deleted file mode 100644 index ecb0364..0000000 --- a/app/components/data-table/data-table-menu-general.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'ember-data-table/components/data-table/data-table-menu-general'; diff --git a/app/components/data-table/data-table-menu-selected.js b/app/components/data-table/data-table-menu-selected.js deleted file mode 100644 index d216170..0000000 --- a/app/components/data-table/data-table-menu-selected.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'ember-data-table/components/data-table/data-table-menu-selected'; diff --git a/app/components/data-table/data-table-menu.js b/app/components/data-table/data-table-menu.js deleted file mode 100644 index 84a697d..0000000 --- a/app/components/data-table/data-table-menu.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'ember-data-table/components/data-table/data-table-menu'; diff --git a/app/components/data-table/number-pagination.js b/app/components/data-table/number-pagination.js deleted file mode 100644 index 3232643..0000000 --- a/app/components/data-table/number-pagination.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'ember-data-table/components/data-table/number-pagination'; diff --git a/app/components/data-table/row.js b/app/components/data-table/row.js deleted file mode 100644 index 2731d43..0000000 --- a/app/components/data-table/row.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'ember-data-table/components/data-table/row'; diff --git a/app/components/data-table/text-search.js b/app/components/data-table/text-search.js deleted file mode 100644 index 26875e3..0000000 --- a/app/components/data-table/text-search.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'ember-data-table/components/data-table/text-search'; diff --git a/app/components/data-table/th-sortable.js b/app/components/data-table/th-sortable.js deleted file mode 100644 index 057b751..0000000 --- a/app/components/data-table/th-sortable.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'ember-data-table/components/data-table/th-sortable'; diff --git a/app/components/raw-data-table.js b/app/components/raw-data-table.js deleted file mode 100644 index ea25044..0000000 --- a/app/components/raw-data-table.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'ember-data-table/components/raw-data-table'; diff --git a/app/utils/attribute-to-sort-params.js b/app/utils/attribute-to-sort-params.js deleted file mode 100644 index d9701d0..0000000 --- a/app/utils/attribute-to-sort-params.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'ember-data-table/utils/attribute-to-sort-params'; diff --git a/blueprints/ember-data-table/files/app/serializers/application.js b/blueprints/ember-data-table/files/app/serializers/application.js deleted file mode 100644 index 9e45377..0000000 --- a/blueprints/ember-data-table/files/app/serializers/application.js +++ /dev/null @@ -1,6 +0,0 @@ -import DS from 'ember-data'; -import DataTableSerializerMixin from 'ember-data-table/mixins/serializer'; - -export default DS.JSONAPISerializer.extend(DataTableSerializerMixin, { - -}); diff --git a/blueprints/ember-data-table/files/app/styles/app.scss b/blueprints/ember-data-table/files/app/styles/app.scss deleted file mode 100644 index dfc3dd8..0000000 --- a/blueprints/ember-data-table/files/app/styles/app.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'ember-data-table'; diff --git a/blueprints/ember-data-table/index.js b/blueprints/ember-data-table/index.js deleted file mode 100644 index 96ce404..0000000 --- a/blueprints/ember-data-table/index.js +++ /dev/null @@ -1,15 +0,0 @@ -/*jshint node:true*/ -module.exports = { - description: '', - - normalizeEntityName: function () {}, - - // locals: function(options) { - // // Return custom template variables here. - // return { - // foo: options.entity.options.foo - // }; - // } - - afterInstall: function () {}, -}; diff --git a/config/environment.js b/config/environment.js deleted file mode 100644 index 331ab30..0000000 --- a/config/environment.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports = function (/* environment, appConfig */) { - return {}; -}; diff --git a/ember-cli-build.js b/ember-cli-build.js deleted file mode 100644 index 39becfc..0000000 --- a/ember-cli-build.js +++ /dev/null @@ -1,25 +0,0 @@ -var EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); - -module.exports = function (defaults) { - let app = new EmberAddon(defaults, { - 'ember-cli-babel': { - includePolyfill: true, - }, - }); - - /* - This build file specifies the options for the dummy test app of this - addon, located in `/tests/dummy` - This build file does *not* influence how the addon or the app using it - behave. You most likely want to be modifying `./index.js` or app's build file - */ - - const { maybeEmbroider } = require('@embroider/test-setup'); - return maybeEmbroider(app, { - skipBabel: [ - { - package: 'qunit', - }, - ], - }); -}; diff --git a/index.js b/index.js deleted file mode 100644 index 0ca063d..0000000 --- a/index.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports = { - name: require('./package').name, -}; diff --git a/package.json b/package.json index e5f4664..0330963 100644 --- a/package.json +++ b/package.json @@ -1,93 +1,4 @@ { - "name": "ember-data-table", - "version": "2.0.1", - "description": "Data tables for Ember following Google Design specs", - "keywords": [ - "ember-addon", - "mu-semtech" - ], - "repository": { - "type": "git", - "url": "https://github.com/mu-semtech/ember-data-table" - }, - "license": "MIT", - "author": "Erika Pauwels", - "directories": { - "doc": "doc", - "test": "tests" - }, - "scripts": { - "build": "ember build --environment=production", - "lint": "npm-run-all --aggregate-output --continue-on-error --parallel \"lint:!(fix)\"", - "lint:fix": "npm-run-all --aggregate-output --continue-on-error --parallel lint:*:fix", - "lint:hbs": "ember-template-lint .", - "lint:hbs:fix": "ember-template-lint . --fix", - "lint:js": "eslint . --cache", - "lint:js:fix": "eslint . --fix", - "start": "ember serve", - "test": "npm-run-all lint test:*", - "test:ember": "ember test", - "test:ember-compatibility": "ember try:each" - }, - "dependencies": { - "ember-auto-import": "^2.0.0", - "ember-cli-babel": "^7.26.10", - "ember-cli-htmlbars": "^5.7.2", - "ember-composable-helpers": "^5.0.0", - "ember-data": ">=3.28.0", - "ember-math-helpers": "^2.18.0", - "ember-truth-helpers": "^3.0.0", - "lodash": "^4.17.21" - }, - "devDependencies": { - "@ember/jquery": "^0.6.0", - "@ember/optional-features": "^2.0.0", - "@ember/test-helpers": "^2.6.0", - "@embroider/test-setup": "^0.48.1", - "@glimmer/component": "^1.0.4", - "@glimmer/tracking": "^1.0.4", - "babel-eslint": "^10.1.0", - "broccoli-asset-rev": "^3.0.0", - "ember-cli": "~3.28.5", - "ember-cli-dependency-checker": "^3.2.0", - "ember-cli-inject-live-reload": "^2.1.0", - "ember-cli-release": "^1.0.0-beta.2", - "ember-cli-sass": "^10.0.1", - "ember-cli-sri": "^2.1.1", - "ember-cli-terser": "^4.0.2", - "ember-cli-update": "^0.33.2", - "ember-disable-prototype-extensions": "^1.1.3", - "ember-export-application-global": "^2.0.1", - "ember-load-initializers": "^2.1.2", - "ember-maybe-import-regenerator": "^1.0.0", - "ember-page-title": "^6.2.2", - "ember-qunit": "^5.1.5", - "ember-resolver": "^8.0.3", - "ember-source": "~3.28.8", - "ember-source-channel-url": "^3.0.0", - "ember-template-lint": "^3.15.0", - "ember-try": "^1.4.0", - "eslint": "^7.32.0", - "eslint-config-prettier": "^8.3.0", - "eslint-plugin-ember": "^10.5.8", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-prettier": "^3.4.1", - "eslint-plugin-qunit": "^6.2.0", - "loader.js": "^4.7.0", - "npm-run-all": "^4.1.5", - "phantomjs-prebuilt": "^2.1.16", - "prettier": "^2.5.1", - "qunit": "^2.17.2", - "qunit-dom": "^1.6.0", - "sass": "^1.48.0" - }, - "engines": { - "node": "12.* || 14.* || >= 16" - }, - "ember": { - "edition": "octane" - }, - "ember-addon": { - "configPath": "tests/dummy/config" - } -} + "private": true, + "workspaces": ["addon", "test-app"] +} \ No newline at end of file diff --git a/addon/serializer.js b/serializer_example.md similarity index 97% rename from addon/serializer.js rename to serializer_example.md index 2567f13..194f5a4 100644 --- a/addon/serializer.js +++ b/serializer_example.md @@ -1,3 +1,5 @@ +Code for a serializer to support page meta that ember-data-table understands. +```js import JSONAPISerializer from '@ember-data/serializer/json-api'; /** @@ -99,3 +101,4 @@ export default class ApplicationSerializer extends JSONAPISerializer { return super.normalizeQueryResponse(...arguments); } } +``` \ No newline at end of file diff --git a/.bowerrc b/test-app/.bowerrc similarity index 100% rename from .bowerrc rename to test-app/.bowerrc diff --git a/.editorconfig b/test-app/.editorconfig similarity index 100% rename from .editorconfig rename to test-app/.editorconfig diff --git a/test-app/.ember-cli b/test-app/.ember-cli new file mode 100644 index 0000000..465c405 --- /dev/null +++ b/test-app/.ember-cli @@ -0,0 +1,7 @@ +{ + /** + Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript + rather than JavaScript by default, when a TypeScript version of a given blueprint is available. + */ + "isTypeScriptProject": false +} diff --git a/test-app/.gitignore b/test-app/.gitignore new file mode 100644 index 0000000..48acbf6 --- /dev/null +++ b/test-app/.gitignore @@ -0,0 +1,33 @@ +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +/dist/ +/declarations/ + +# dependencies +/node_modules/ + +# misc +/.env* +/.pnp* +/.eslintcache +/coverage/ +/npm-debug.log* +/testem.log +/yarn-error.log + +# ember-try +.node_modules.ember-try/ +bower.json.ember-try +package.json.ember-try + +*~ +package-lock.json +/.node_modules.ember-try/ +/npm-shrinkwrap.json.ember-try +/package.json.ember-try +/package-lock.json.ember-try +/yarn.lock.ember-try + +# broccoli-debug +/DEBUG/ diff --git a/.jshintrc b/test-app/.jshintrc similarity index 100% rename from .jshintrc rename to test-app/.jshintrc diff --git a/.npmignore b/test-app/.npmignore similarity index 68% rename from .npmignore rename to test-app/.npmignore index f30effe..8c9fd97 100644 --- a/.npmignore +++ b/test-app/.npmignore @@ -2,11 +2,7 @@ /dist/ /tmp/ -# dependencies -/bower_components/ - # misc -/.bowerrc /.editorconfig /.ember-cli /.env* @@ -14,23 +10,28 @@ /.eslintignore /.eslintrc.js /.git/ +/.github/ /.gitignore /.prettierignore /.prettierrc.js +/.stylelintignore +/.stylelintrc.js /.template-lintrc.js /.travis.yml /.watchmanconfig -/bower.json -/config/ember-try.js /CONTRIBUTING.md /ember-cli-build.js /testem.js /tests/ +/tsconfig.declarations.json +/tsconfig.json /yarn-error.log /yarn.lock .gitkeep # ember-try /.node_modules.ember-try/ -/bower.json.ember-try +/npm-shrinkwrap.json.ember-try /package.json.ember-try +/package-lock.json.ember-try +/yarn.lock.ember-try diff --git a/.prettierignore b/test-app/.prettierignore similarity index 50% rename from .prettierignore rename to test-app/.prettierignore index 9221655..9385391 100644 --- a/.prettierignore +++ b/test-app/.prettierignore @@ -1,21 +1,13 @@ # unconventional js /blueprints/*/files/ -/vendor/ # compiled output /dist/ -/tmp/ - -# dependencies -/bower_components/ -/node_modules/ # misc /coverage/ !.* -.eslintcache +.*/ # ember-try /.node_modules.ember-try/ -/bower.json.ember-try -/package.json.ember-try diff --git a/test-app/.stylelintignore b/test-app/.stylelintignore new file mode 100644 index 0000000..a0cf71c --- /dev/null +++ b/test-app/.stylelintignore @@ -0,0 +1,8 @@ +# unconventional files +/blueprints/*/files/ + +# compiled output +/dist/ + +# addons +/.node_modules.ember-try/ diff --git a/test-app/.template-lintrc.js b/test-app/.template-lintrc.js new file mode 100644 index 0000000..f8be444 --- /dev/null +++ b/test-app/.template-lintrc.js @@ -0,0 +1,8 @@ +'use strict'; + +module.exports = { + extends: 'recommended', + rules: { + 'require-valid-named-block-naming-format': 'kebab-case', + } +}; diff --git a/test-app/.watchmanconfig b/test-app/.watchmanconfig new file mode 100644 index 0000000..f9c3d8f --- /dev/null +++ b/test-app/.watchmanconfig @@ -0,0 +1,3 @@ +{ + "ignore_dirs": ["dist"] +} diff --git a/_config.yml b/test-app/_config.yml similarity index 100% rename from _config.yml rename to test-app/_config.yml diff --git a/test-app/app/adapters/person.js b/test-app/app/adapters/person.js new file mode 100644 index 0000000..ab83007 --- /dev/null +++ b/test-app/app/adapters/person.js @@ -0,0 +1,10 @@ +import { inject as service } from '@ember/service'; +import Adapter from '@ember-data/adapter'; + +export default class LocalAdapter extends Adapter { + @service createData; + + async query(store, type, query) { + return this.createData.queryPeople(query); + } +} diff --git a/tests/dummy/app/app.js b/test-app/app/app.js similarity index 75% rename from tests/dummy/app/app.js rename to test-app/app/app.js index 523bad6..8e698d5 100644 --- a/tests/dummy/app/app.js +++ b/test-app/app/app.js @@ -1,7 +1,8 @@ import Application from '@ember/application'; -import Resolver from 'ember-resolver'; + import loadInitializers from 'ember-load-initializers'; -import config from 'dummy/config/environment'; +import Resolver from 'ember-resolver'; +import config from 'test-app/config/environment'; export default class App extends Application { modulePrefix = config.modulePrefix; @@ -9,4 +10,4 @@ export default class App extends Application { Resolver = Resolver; } -loadInitializers(App, config.modulePrefix); +loadInitializers(App, config.modulePrefix); \ No newline at end of file diff --git a/test-app/app/components/allcaps.gjs b/test-app/app/components/allcaps.gjs new file mode 100644 index 0000000..46a94b9 --- /dev/null +++ b/test-app/app/components/allcaps.gjs @@ -0,0 +1,5 @@ +import toUpperCase from '../helpers/toUpperCase'; + + diff --git a/test-app/app/controllers/adv-table-ed.js b/test-app/app/controllers/adv-table-ed.js new file mode 100644 index 0000000..0012574 --- /dev/null +++ b/test-app/app/controllers/adv-table-ed.js @@ -0,0 +1,12 @@ +import { tracked } from '@glimmer/tracking'; +import Controller from '@ember/controller'; + +export default class AdvTableEdController extends Controller { + queryParams = ['size', 'page', 'filter', 'sort']; + + @tracked size = 10; + @tracked page = 0; + @tracked filter = ''; + @tracked sort = ''; + @tracked isLoadingModel = false; +} diff --git a/test-app/app/controllers/adv-table-pojo.js b/test-app/app/controllers/adv-table-pojo.js new file mode 100644 index 0000000..eab3bbc --- /dev/null +++ b/test-app/app/controllers/adv-table-pojo.js @@ -0,0 +1,12 @@ +import { tracked } from '@glimmer/tracking'; +import Controller from '@ember/controller'; + +export default class AdvTablePojoController extends Controller { + queryParams = ['size', 'page', 'filter', 'sort']; + + @tracked size = 10; + @tracked page = 0; + @tracked filter = ''; + @tracked sort = ''; + @tracked isLoadingModel = false; +} \ No newline at end of file diff --git a/test-app/app/controllers/simple-table-ed.js b/test-app/app/controllers/simple-table-ed.js new file mode 100644 index 0000000..e9d545b --- /dev/null +++ b/test-app/app/controllers/simple-table-ed.js @@ -0,0 +1,12 @@ +import { tracked } from '@glimmer/tracking'; +import Controller from '@ember/controller'; + +export default class SimpleTableEdController extends Controller { + queryParams = ['size', 'page', 'filter', 'sort']; + + @tracked size = 10; + @tracked page = 0; + @tracked filter = ''; + @tracked sort = ''; + @tracked isLoadingModel = false; +} diff --git a/test-app/app/controllers/simple-table-pojo.js b/test-app/app/controllers/simple-table-pojo.js new file mode 100644 index 0000000..9a61053 --- /dev/null +++ b/test-app/app/controllers/simple-table-pojo.js @@ -0,0 +1,12 @@ +import { tracked } from '@glimmer/tracking'; +import Controller from '@ember/controller'; + +export default class SimpleTablePojoController extends Controller { + queryParams = ['size', 'page', 'filter', 'sort']; + + @tracked size = 10; + @tracked page = 0; + @tracked filter = ''; + @tracked sort = ''; + @tracked isLoadingModel = false; +} \ No newline at end of file diff --git a/test-app/app/helpers/lastItem.js b/test-app/app/helpers/lastItem.js new file mode 100644 index 0000000..d78bfd8 --- /dev/null +++ b/test-app/app/helpers/lastItem.js @@ -0,0 +1,5 @@ +import { helper } from '@ember/component/helper'; + +export default helper(function lastItemHelper([array]) { + return array.slice(-1).pop(); +}); diff --git a/test-app/app/helpers/toUpperCase.js b/test-app/app/helpers/toUpperCase.js new file mode 100644 index 0000000..05e16a4 --- /dev/null +++ b/test-app/app/helpers/toUpperCase.js @@ -0,0 +1,5 @@ +import { helper } from '@ember/component/helper'; + +export default helper(function lastItemHelper([str]) { + return str.toUpperCase(); +}); diff --git a/tests/dummy/app/index.html b/test-app/app/index.html similarity index 81% rename from tests/dummy/app/index.html rename to test-app/app/index.html index 61400b2..bbd2a2b 100644 --- a/tests/dummy/app/index.html +++ b/test-app/app/index.html @@ -2,7 +2,6 @@ - Dummy @@ -10,7 +9,7 @@ {{content-for "head"}} - + {{content-for "head-footer"}} @@ -18,7 +17,7 @@ {{content-for "body"}} - + {{content-for "body-footer"}} diff --git a/test-app/app/models/person.js b/test-app/app/models/person.js new file mode 100644 index 0000000..1e26310 --- /dev/null +++ b/test-app/app/models/person.js @@ -0,0 +1,9 @@ +import Model, { attr } from '@ember-data/model'; + +export default class PersonModel extends Model { + @attr('string') firstname; + @attr('string') lastname; + @attr('number') age; + @attr() created; + @attr() modified; +} diff --git a/test-app/app/router.js b/test-app/app/router.js new file mode 100644 index 0000000..f6244e6 --- /dev/null +++ b/test-app/app/router.js @@ -0,0 +1,17 @@ +import EmberRouter from '@ember/routing/router'; + +import config from 'test-app/config/environment'; + +export default class Router extends EmberRouter { + location = config.locationType; + rootURL = config.rootURL; +} + +Router.map(function () { + this.route('simple-table-ed'); + this.route('simple-table-pojo'); + this.route('adv-table-ed'); + this.route('adv-table-pojo'); + this.route('person', { path: '/people/:id/' }); + this.route('person-details', { path: '/people-details/:id/' }); +}); diff --git a/addon/route.js b/test-app/app/routes/adv-table-ed.js similarity index 66% rename from addon/route.js rename to test-app/app/routes/adv-table-ed.js index d670568..9c7898c 100644 --- a/addon/route.js +++ b/test-app/app/routes/adv-table-ed.js @@ -1,10 +1,10 @@ -import { inject as service } from '@ember/service'; import { action } from '@ember/object'; import Route from '@ember/routing/route'; -import merge from 'lodash/merge'; +import { inject as service } from '@ember/service'; -export default class DataTableRoute extends Route { +export default class SimpleTableEdRoute extends Route { @service store; + modelName = 'person'; queryParams = { filter: { refreshModel: true }, @@ -13,10 +13,6 @@ export default class DataTableRoute extends Route { sort: { refreshModel: true }, }; - mergeQueryOptions() { - return {}; - } - model(params) { const options = { sort: params.sort, @@ -25,22 +21,26 @@ export default class DataTableRoute extends Route { size: params.size, }, }; - // TODO: sending an empty filter param to backend returns [] + if (params.filter) { options['filter'] = params.filter; } - merge(options, this.mergeQueryOptions(params)); + return this.store.query(this.modelName, options); } @action loading(transition) { + // eslint-disable-next-line ember/no-controller-access-in-routes let controller = this.controllerFor(this.routeName); - controller.isLoadingModel = true; - transition.promise.finally(function () { - controller.isLoadingModel = false; - }); + if (controller) { + controller.isLoadingModel = true; + + transition.finally(function () { + controller.isLoadingModel = false; + }); + } return true; // bubble the loading event } diff --git a/test-app/app/routes/adv-table-pojo.js b/test-app/app/routes/adv-table-pojo.js new file mode 100644 index 0000000..2480d52 --- /dev/null +++ b/test-app/app/routes/adv-table-pojo.js @@ -0,0 +1,46 @@ +import { action } from '@ember/object'; +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; + +export default class AdvTablePojoRoute extends Route { + @service createData; + + queryParams = { + filter: { refreshModel: true }, + page: { refreshModel: true }, + size: { refreshModel: true }, + sort: { refreshModel: true }, + }; + + model(params) { + const query = { + sort: params.sort, + page: { + number: params.page, + size: params.size, + }, + }; + + if (params.filter) { + query['filter'] = params.filter; + } + + return this.createData.queryPeople(query); + } + + @action + loading(transition) { + // eslint-disable-next-line ember/no-controller-access-in-routes + let controller = this.controllerFor(this.routeName); + + if (controller) { + controller.isLoadingModel = true; + + transition.finally(function () { + controller.isLoadingModel = false; + }); + } + + return true; // bubble the loading event + } +} diff --git a/test-app/app/routes/application.js b/test-app/app/routes/application.js new file mode 100644 index 0000000..72f0d5e --- /dev/null +++ b/test-app/app/routes/application.js @@ -0,0 +1,3 @@ +import Route from '@ember/routing/route'; + +export default class ApplicationRoute extends Route {} diff --git a/test-app/app/routes/simple-table-ed.js b/test-app/app/routes/simple-table-ed.js new file mode 100644 index 0000000..004d3a3 --- /dev/null +++ b/test-app/app/routes/simple-table-ed.js @@ -0,0 +1,47 @@ +import { action } from '@ember/object'; +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; + +export default class AdvTableEdRoute extends Route { + @service store; + modelName = 'person'; + + queryParams = { + filter: { refreshModel: true }, + page: { refreshModel: true }, + size: { refreshModel: true }, + sort: { refreshModel: true }, + }; + + model(params) { + const options = { + sort: params.sort, + page: { + number: params.page, + size: params.size, + }, + }; + + if (params.filter) { + options['filter'] = params.filter; + } + + return this.store.query(this.modelName, options); + } + + @action + loading(transition) { + // eslint-disable-next-line ember/no-controller-access-in-routes + let controller = this.controllerFor(this.routeName); + + if (controller) { + controller.isLoadingModel = true; + + transition.finally(function () { + controller.isLoadingModel = false; + }); + } + + return true; // bubble the loading event + } +} diff --git a/test-app/app/routes/simple-table-pojo.js b/test-app/app/routes/simple-table-pojo.js new file mode 100644 index 0000000..b4482fa --- /dev/null +++ b/test-app/app/routes/simple-table-pojo.js @@ -0,0 +1,46 @@ +import { action } from '@ember/object'; +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; + +export default class SimpleTablePojoRoute extends Route { + @service createData; + + queryParams = { + filter: { refreshModel: true }, + page: { refreshModel: true }, + size: { refreshModel: true }, + sort: { refreshModel: true }, + }; + + model(params) { + const query = { + sort: params.sort, + page: { + number: params.page, + size: params.size, + }, + }; + + if (params.filter) { + query['filter'] = params.filter; + } + + return this.createData.queryPeople(query); + } + + @action + loading(transition) { + // eslint-disable-next-line ember/no-controller-access-in-routes + let controller = this.controllerFor(this.routeName); + + if (controller) { + controller.isLoadingModel = true; + + transition.finally(function () { + controller.isLoadingModel = false; + }); + } + + return true; // bubble the loading event + } +} diff --git a/test-app/app/serializers/application.js b/test-app/app/serializers/application.js new file mode 100644 index 0000000..c523e46 --- /dev/null +++ b/test-app/app/serializers/application.js @@ -0,0 +1,4 @@ +import JSONSerializer from '@ember-data/serializer/json'; + +export default class ApplicationSerializer extends JSONSerializer { +} diff --git a/test-app/app/services/create-data.js b/test-app/app/services/create-data.js new file mode 100644 index 0000000..3033e31 --- /dev/null +++ b/test-app/app/services/create-data.js @@ -0,0 +1,114 @@ +import Service from "@ember/service"; + +export default class CreateDataService extends Service { + generatedData = null; + generatedAmount = null; + + generatePeople(amount) { + if(this.generatedAmount === amount && this.generatedData) { + return this.generatedData; + } + + const firstNames = [ + 'John', + 'Jane', + 'Alice', + 'Bob', + 'Charlie', + 'David', + 'Eve', + 'Frank', + 'Grace', + ]; + const lastNames = [ + 'Doe', + 'Smith', + 'Johnson', + 'Brown', + 'Williams', + 'Jones', + 'Miller', + 'Davis', + 'Garcia', + 'Martinez', + ]; + const list = []; + + for (let i = 0; i < amount; i++) { + const firstName = firstNames[i % firstNames.length]; + const lastName = lastNames[i % lastNames.length]; + const age = 20 + (i % 30); + const created = new Date(Date.now() - 1000000000 * i); + const modified = new Date(Date.now() - 1000000000 * i); + + list.push({ + id: i, + firstname: firstName, + lastname: lastName, + age: age, + created: created, + modified: modified, + }); + } + + this.generatedAmount = amount; + this.generatedData = list; + + return this.generatedData; + } + + generatePaginationMeta(page, size, count) { + const pages = Math.floor(count / size); + + return { + count: count, + pagination: { + first: { number: 0, size: size }, + prev: { number: Math.max(0, page - 1), size: size }, + next: { number: Math.min(page + 1, pages), size: size }, + last: { number: pages }, + }, + }; + } + + compareAny(a, b) { + return a > b ? 1 : a < b ? -1 : 0; + } + + async queryPeople(query) { + let people = this.generatePeople(100); + + if (query.sort) { + const [sortOrder, sortType] = query.sort.startsWith('-') + ? [-1, query.sort.slice(1)] + : [1, query.sort]; + + people.sort( + (a, b) => sortOrder * this.compareAny(a[sortType], b[sortType]) + ); + } + + if (query.filter) { + people = people.filter((p) => + `${p.firstname} ${p.lastname}` + .toLowerCase() + .includes(query.filter.toLowerCase().trim()) + ); + } + + const count = people.length; + + if (query.page) { + const start = query.page.number * query.page.size; + + people = people.slice(start, start + query.page.size); + people.meta = this.generatePaginationMeta( + query.page.number, + query.page.size, + count + ); + } + + return Promise.resolve(people); + } +} diff --git a/test-app/app/services/store.js b/test-app/app/services/store.js new file mode 100644 index 0000000..50d79ca --- /dev/null +++ b/test-app/app/services/store.js @@ -0,0 +1,12 @@ + +import Store from 'ember-data/store'; + +export default class StoreService extends Store { + async query(modelName) { + if (modelName === 'person') { + await new Promise(r => setTimeout(r, 300)); + } + + return super.query(...arguments); + } +} diff --git a/tests/dummy/app/styles/app.scss b/test-app/app/styles/app.scss similarity index 81% rename from tests/dummy/app/styles/app.scss rename to test-app/app/styles/app.scss index 89f7987..f8eaa6b 100644 --- a/tests/dummy/app/styles/app.scss +++ b/test-app/app/styles/app.scss @@ -1,4 +1,4 @@ -@import 'ember-data-table'; +@use 'ember-data-table'; // Blockquote from MaterializeCSS blockquote { diff --git a/test-app/app/templates/adv-table-ed.gjs b/test-app/app/templates/adv-table-ed.gjs new file mode 100644 index 0000000..1331e28 --- /dev/null +++ b/test-app/app/templates/adv-table-ed.gjs @@ -0,0 +1,87 @@ +import { array, fn, hash } from '@ember/helper'; +import { on } from '@ember/modifier'; + +import RawDataTable from 'ember-data-table/components/raw-data-table'; +import pageTitle from 'ember-page-title/helpers/page-title'; +import eq from 'ember-truth-helpers/helpers/eq'; + +import Allcaps from '../components/allcaps'; +import lastItem from '../helpers/lastItem'; + + diff --git a/test-app/app/templates/adv-table-pojo.gjs b/test-app/app/templates/adv-table-pojo.gjs new file mode 100644 index 0000000..bdc5cd4 --- /dev/null +++ b/test-app/app/templates/adv-table-pojo.gjs @@ -0,0 +1,88 @@ +import { array, fn, hash } from '@ember/helper'; +import { on } from '@ember/modifier'; + +import RawDataTable from 'ember-data-table/components/raw-data-table'; +import pageTitle from 'ember-page-title/helpers/page-title'; +import eq from 'ember-truth-helpers/helpers/eq'; + +import Allcaps from '../components/allcaps'; +import lastItem from '../helpers/lastItem'; + + diff --git a/test-app/app/templates/application.gjs b/test-app/app/templates/application.gjs new file mode 100644 index 0000000..8a74ca1 --- /dev/null +++ b/test-app/app/templates/application.gjs @@ -0,0 +1,12 @@ +import { LinkTo } from '@ember/routing'; + + diff --git a/test-app/app/templates/person-details.gjs b/test-app/app/templates/person-details.gjs new file mode 100644 index 0000000..b38c1f4 --- /dev/null +++ b/test-app/app/templates/person-details.gjs @@ -0,0 +1,13 @@ +import { array, get } from '@ember/helper'; + + diff --git a/test-app/app/templates/person.gjs b/test-app/app/templates/person.gjs new file mode 100644 index 0000000..c2add47 --- /dev/null +++ b/test-app/app/templates/person.gjs @@ -0,0 +1,9 @@ + diff --git a/test-app/app/templates/simple-table-ed.gjs b/test-app/app/templates/simple-table-ed.gjs new file mode 100644 index 0000000..0332d5a --- /dev/null +++ b/test-app/app/templates/simple-table-ed.gjs @@ -0,0 +1,30 @@ +import { fn } from '@ember/helper'; + +import RawDataTable from 'ember-data-table/components/raw-data-table'; +import pageTitle from 'ember-page-title/helpers/page-title'; + + diff --git a/test-app/app/templates/simple-table-pojo.gjs b/test-app/app/templates/simple-table-pojo.gjs new file mode 100644 index 0000000..98d86ba --- /dev/null +++ b/test-app/app/templates/simple-table-pojo.gjs @@ -0,0 +1,30 @@ +import { fn } from '@ember/helper'; + +import RawDataTable from 'ember-data-table/components/raw-data-table'; +import pageTitle from 'ember-page-title/helpers/page-title'; + + diff --git a/config/ember-try.js b/test-app/config/ember-try.js similarity index 51% rename from config/ember-try.js rename to test-app/config/ember-try.js index 2612023..e3e6b94 100644 --- a/config/ember-try.js +++ b/test-app/config/ember-try.js @@ -7,18 +7,18 @@ module.exports = async function () { return { scenarios: [ { - name: 'ember-lts-3.24', + name: 'ember-lts-4.12', npm: { devDependencies: { - 'ember-source': '~3.24.3', + 'ember-source': '~4.12.0', }, }, }, { - name: 'ember-lts-3.28', + name: 'ember-lts-5.4', npm: { devDependencies: { - 'ember-source': '~3.28.0', + 'ember-source': '~5.4.0', }, }, }, @@ -46,37 +46,6 @@ module.exports = async function () { }, }, }, - { - name: 'ember-default-with-jquery', - env: { - EMBER_OPTIONAL_FEATURES: JSON.stringify({ - 'jquery-integration': true, - }), - }, - npm: { - devDependencies: { - '@ember/jquery': '^1.1.0', - }, - }, - }, - { - name: 'ember-classic', - env: { - EMBER_OPTIONAL_FEATURES: JSON.stringify({ - 'application-template-wrapper': true, - 'default-async-observers': false, - 'template-only-glimmer-components': false, - }), - }, - npm: { - devDependencies: { - 'ember-source': '~3.28.0', - }, - ember: { - edition: 'classic', - }, - }, - }, embroiderSafe(), embroiderOptimized(), ], diff --git a/tests/dummy/config/environment.js b/test-app/config/environment.js similarity index 84% rename from tests/dummy/config/environment.js rename to test-app/config/environment.js index ba4e8cb..7f282ab 100644 --- a/tests/dummy/config/environment.js +++ b/test-app/config/environment.js @@ -1,20 +1,17 @@ -'use strict'; + 'use strict'; module.exports = function (environment) { - let ENV = { - modulePrefix: 'dummy', + const ENV = { + modulePrefix: 'test-app', environment, rootURL: '/', - locationType: 'auto', + locationType: 'history', EmberENV: { + EXTEND_PROTOTYPES: false, FEATURES: { // Here you can enable experimental features on an ember canary build // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true }, - EXTEND_PROTOTYPES: { - // Prevent Ember Data from overriding Date.parse. - Date: false, - }, }, APP: { diff --git a/tests/dummy/config/optional-features.json b/test-app/config/optional-features.json similarity index 79% rename from tests/dummy/config/optional-features.json rename to test-app/config/optional-features.json index d0d7c47..b26286e 100644 --- a/tests/dummy/config/optional-features.json +++ b/test-app/config/optional-features.json @@ -1,6 +1,6 @@ { "application-template-wrapper": false, "default-async-observers": true, - "jquery-integration": true, + "jquery-integration": false, "template-only-glimmer-components": true } diff --git a/config/release.js b/test-app/config/release.js similarity index 100% rename from config/release.js rename to test-app/config/release.js diff --git a/test-app/config/targets.js b/test-app/config/targets.js new file mode 100644 index 0000000..1e48e05 --- /dev/null +++ b/test-app/config/targets.js @@ -0,0 +1,11 @@ +'use strict'; + +const browsers = [ + 'last 1 Chrome versions', + 'last 1 Firefox versions', + 'last 1 Safari versions', +]; + +module.exports = { + browsers, +}; diff --git a/test-app/ember-cli-build.js b/test-app/ember-cli-build.js new file mode 100644 index 0000000..ccbd8e1 --- /dev/null +++ b/test-app/ember-cli-build.js @@ -0,0 +1,40 @@ +'use strict'; + +const sideWatch = require('@embroider/broccoli-side-watch'); +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); + +module.exports = function (defaults) { + const app = new EmberApp(defaults, { + sassOptions: { + precision: 4, + includePaths: [ + '../node_modules/ember-data-table/dist/styles', + ], + }, + 'ember-cli-babel': { + includePolyfill: true, + }, + minifyCSS: { + options: { + advanced: false, + }, + }, + trees: { + // automatically watch the addon when running the test-app, so it gets rebuilt when the addon code is updated + app: sideWatch('app', { watching: [ + 'ember-data-table', // this will resolve the package by name and watch all its importable code + + ] }), + }, + }); + + const { maybeEmbroider } = require('@embroider/test-setup'); + + return maybeEmbroider(app, { + skipBabel: [ + { + package: 'qunit', + }, + ], + }); +}; diff --git a/test-app/eslint.config.mjs b/test-app/eslint.config.mjs new file mode 100644 index 0000000..5d85bc9 --- /dev/null +++ b/test-app/eslint.config.mjs @@ -0,0 +1,3 @@ +import { configs } from '@nullvoxpopuli/eslint-configs'; + +export default configs.ember(import.meta.dirname); \ No newline at end of file diff --git a/test-app/jsconfig.json b/test-app/jsconfig.json new file mode 100644 index 0000000..8e26dcb --- /dev/null +++ b/test-app/jsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "types": [ + "@glint/environment-ember-loose", + "ember-source/types" + ], + }, + "include": [ + "**/*.js", + "**/*.gjs" +, "ember-cli-build.js", "eslint.config.mjs", "stylelint.config.mjs" ], + "glint": { + "environment": [ + "ember-loose", + "ember-template-imports" + ] + }, +} diff --git a/test-app/package.json b/test-app/package.json new file mode 100644 index 0000000..5ab6537 --- /dev/null +++ b/test-app/package.json @@ -0,0 +1,111 @@ +{ + "name": "test-app", + "version": "3.0.0", + "description": "Data tables for Ember following Google Design specs", + "keywords": [ + "mu-semtech" + ], + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/mu-semtech/ember-data-table" + }, + "license": "MIT", + "author": "Erika Pauwels", + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "build": "ember build --environment=production", + "lint": "concurrently \"npm:lint:*(!fix)\" --names \"lint:\"", + "lint:css": "stylelint \"**/*.css\" --allow-empty-input", + "lint:css:fix": "concurrently \"npm:lint:css -- --fix\"", + "lint:fix": "concurrently \"npm:lint:*:fix\" --names \"fix:\"", + "lint:hbs": "ember-template-lint .", + "lint:hbs:fix": "ember-template-lint . --fix", + "lint:js": "eslint . --cache", + "lint:js:fix": "eslint . --fix", + "start": "ember serve", + "test": "concurrently \"npm:lint\" \"npm:test:*\" --names \"lint,test:\"", + "test:ember": "ember test", + "test:ember-compatibility": "ember try:each" + }, + "devDependencies": { + "@babel/eslint-parser": "^7.25.1", + "@babel/plugin-proposal-decorators": "^7.24.7", + "@ember/optional-features": "^2.1.0", + "@ember/test-helpers": "^5.2.2", + "@embroider/broccoli-side-watch": "^1.1.0", + "@embroider/test-setup": "^4.0.0", + "@glimmer/component": "^2.0.0", + "@glint/core": "^1.5.2", + "@glint/environment-ember-loose": "^1.5.2", + "@glint/environment-ember-template-imports": "^1.5.2", + "@glint/template": "^1.5.2", + "@nullvoxpopuli/eslint-configs": "^5.1.2", + "@types/qunit": "^2.19.12", + "broccoli-asset-rev": "^3.0.0", + "concurrently": "^8.2.2", + "ember-auto-import": "^2.8.1", + "ember-cli": "~5.12.0", + "ember-cli-babel": "^8.2.0", + "ember-cli-clean-css": "^3.0.0", + "ember-cli-dependency-checker": "^3.3.2", + "ember-cli-htmlbars": "^6.3.0", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-cli-release": "^1.0.0-beta.2", + "ember-cli-sass": "^11.0.1", + "ember-cli-sri": "^2.1.1", + "ember-cli-terser": "^4.0.2", + "ember-cli-update": "^0.33.2", + "ember-data": ">=5.3.13", + "ember-data-table": "2.0.1", + "ember-eslint-parser": "^0.5.9", + "ember-load-initializers": "^2.1.2", + "ember-page-title": "^8.2.3", + "ember-qunit": "^8.1.0", + "ember-resolver": "^12.0.1", + "ember-source": "~6.3.0", + "ember-source-channel-url": "^3.0.0", + "ember-template-imports": "^4.3.0", + "ember-template-lint": "^6.0.0", + "ember-try": "^3.0.0", + "eslint": "^9.28.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-ember": "^12.2.1", + "eslint-plugin-n": "^16.6.2", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-qunit": "^8.1.2", + "loader.js": "^4.7.0", + "prettier": "^3.3.3", + "prettier-plugin-ember-template-tag": "^2.0.5", + "qunit": "^2.22.0", + "qunit-dom": "^3.2.1", + "sass": "^1.89.0", + "stylelint": "^16.20.0", + "stylelint-config-standard": "^38.0.0", + "stylelint-prettier": "^5.0.3", + "webpack": "^5.95.0" + }, + "peerDependencies": { + "ember-source": ">= 4.0.0" + }, + "engines": { + "node": ">= 22" + }, + "ember": { + "edition": "octane" + }, + "ember-addon": { + "type": "app", + "version": 2 + }, + "volta": { + "extends": "../package.json" + }, + "exports": { + "./tests/*": "./tests/*", + "./*": "./app/*" + } +} diff --git a/test-app/prettier.config.js b/test-app/prettier.config.js new file mode 100644 index 0000000..a97ba62 --- /dev/null +++ b/test-app/prettier.config.js @@ -0,0 +1,12 @@ +export default { + plugins: ['prettier-plugin-ember-template-tag'], + overrides: [ + { + files: '*.{js,ts,gjs}', + options: { + singleQuote: true, + templateSingleQuote: false, + }, + }, + ], +}; diff --git a/test-app/stylelint.config.mjs b/test-app/stylelint.config.mjs new file mode 100644 index 0000000..ac46be9 --- /dev/null +++ b/test-app/stylelint.config.mjs @@ -0,0 +1,3 @@ +export default { + extends: ['stylelint-config-standard', 'stylelint-prettier/recommended'], +}; diff --git a/testem.js b/test-app/testem.js similarity index 100% rename from testem.js rename to test-app/testem.js diff --git a/test-app/tests/helpers/index.js b/test-app/tests/helpers/index.js new file mode 100644 index 0000000..c13826a --- /dev/null +++ b/test-app/tests/helpers/index.js @@ -0,0 +1,56 @@ +import { + setupApplicationTest as upstreamSetupApplicationTest, + setupRenderingTest as upstreamSetupRenderingTest, + setupTest as upstreamSetupTest, +} from 'ember-qunit'; + +// This file exists to provide wrappers around ember-qunit's +// test setup functions. This way, you can easily extend the setup that is +// needed per test type. + +function setupApplicationTest(hooks, options) { + upstreamSetupApplicationTest(hooks, options); + + // Additional setup for application tests can be done here. + // + // For example, if you need an authenticated session for each + // application test, you could do: + // + // hooks.beforeEach(async function () { + // await authenticateSession(); // ember-simple-auth + // }); + // + // This is also a good place to call test setup functions coming + // from other addons: + // + // setupIntl(hooks, 'en-us'); // ember-intl + // setupMirage(hooks); // ember-cli-mirage +} + +function setupRenderingTest(hooks, options) { + upstreamSetupRenderingTest(hooks, options); + + // Additional setup for rendering tests can be done here. +} + +function setupTest(hooks, options) { + upstreamSetupTest(hooks, options); + + // Additional setup for unit tests can be done here. +} + +function generatePaginationMeta(page, size, count) { + const pages = Math.floor(count / size); + + return { + count: count, + pagination: { + first: { number: 0, size: size }, + prev: { number: Math.max(0, page - 1), size: size }, + next: { number: Math.min(page + 1, pages), size: size }, + last: { number: pages }, + }, + }; +} + +export { generatePaginationMeta,setupApplicationTest, setupRenderingTest, setupTest }; diff --git a/tests/index.html b/test-app/tests/index.html similarity index 85% rename from tests/index.html rename to test-app/tests/index.html index 6d13069..2b0bf42 100644 --- a/tests/index.html +++ b/test-app/tests/index.html @@ -2,7 +2,6 @@ - Dummy Tests @@ -11,7 +10,7 @@ {{content-for "test-head"}} - + {{content-for "head-footer"}} @@ -31,7 +30,7 @@ - + {{content-for "body-footer"}} diff --git a/addon/.gitkeep b/test-app/tests/integration/.gitkeep similarity index 100% rename from addon/.gitkeep rename to test-app/tests/integration/.gitkeep diff --git a/test-app/tests/integration/components/data-cell-test.gjs b/test-app/tests/integration/components/data-cell-test.gjs new file mode 100644 index 0000000..616c323 --- /dev/null +++ b/test-app/tests/integration/components/data-cell-test.gjs @@ -0,0 +1,174 @@ +import { tracked } from '@glimmer/tracking'; +import { renderSettled } from '@ember/renderer'; +import { render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; + +import RawDataTable from 'ember-data-table/components/raw-data-table'; + +module('Integration | Component | data-table/data-cell', function (hooks) { + setupRenderingTest(hooks); + + const onePerson = [{ firstName: 'John', lastName: 'Doe', age: 20 }]; + const allFields = ['firstName', 'lastName', 'age']; + + test('it renders custom fields as custom', async function (assert) { + class Context { + @tracked customFields; + } + + const context = new Context(); + + context.customFields = 'age notExisting'; + await render( + , + ); + + assert + .dom('tbody>tr:first-child td') + .exists({ count: 3 }, 'displays 3 columns'); + assert + .dom('tbody>tr:first-child td:first-child') + .hasText('John', 'displays firstName in first column'); + assert + .dom('tbody>tr:first-child td:nth-child(2)') + .hasText('Doe', 'displays lastName in second column'); + assert + .dom('tbody>tr:first-child td:nth-child(3)') + .hasText('age', 'displays custom block in third column'); + assert + .dom('tbody') + .doesNotIncludeText('20', 'Only display custom block, not the age value'); + assert + .dom('tbody') + .doesNotIncludeText( + 'notExisting', + 'only show @customFields custom blocks for fields also passed to @fields', + ); + + context.customFields = 'firstName'; + await renderSettled(); + assert + .dom('tbody>tr:first-child td') + .exists({ count: 3 }, 'reactivity: displays 3 columns'); + assert + .dom('tbody>tr:first-child td:first-child') + .hasText( + 'firstName', + 'reactivity: displays custom block in first column', + ); + assert + .dom('tbody>tr:first-child td:nth-child(2)') + .hasText('Doe', 'reactivity: displays lastName in second column'); + assert + .dom('tbody>tr:first-child td:nth-child(3)') + .hasText('20', 'reactivity: displays age in third column'); + assert + .dom('tbody') + .doesNotIncludeText( + 'John', + 'reactivity: Only display custom block, not the firstName value', + ); + }); + + test('it renders custom fields as components', async function (assert) { + class Context { + @tracked customFields; + } + + const context = new Context(); + + context.customFields = { + firstName: , + age: '', + }; + await render( + , + ); + + assert + .dom('tbody>tr:first-child td') + .exists({ count: 3 }, 'displays 3 columns'); + assert + .dom('tbody>tr:first-child td:first-child') + .hasText( + 'customComponent:firstName', + 'displays firstName custom block in first column', + ); + assert + .dom('tbody>tr:first-child td:nth-child(2)') + .hasText('Doe', 'displays lastName in second column'); + assert + .dom('tbody>tr:first-child td:nth-child(3)') + .hasText('custom:age', 'displays custom component in third column'); + assert + .dom('tbody') + .doesNotIncludeText('20', 'Only display custom block, not the age value'); + assert + .dom('tbody') + .doesNotIncludeText( + 'John', + 'Only display custom component, not firstName value', + ); + + context.customFields = { + firstName: , + age: '', + }; + await renderSettled(); + assert + .dom('tbody>tr:first-child td') + .exists({ count: 3 }, 'reactivity: displays 3 columns'); + assert + .dom('tbody>tr:first-child td:first-child') + .hasText( + 'customComponentNew:firstName', + 'reactivity: displays firstName custom block in first column', + ); + assert + .dom('tbody>tr:first-child td:nth-child(2)') + .hasText('Doe', 'reactivity: displays lastName in second column'); + assert + .dom('tbody>tr:first-child td:nth-child(3)') + .hasText( + 'custom:age', + 'reactivity: displays custom component in third column', + ); + assert + .dom('tbody') + .doesNotIncludeText( + '20', + 'reactivity: Only display custom block, not the age value', + ); + assert + .dom('tbody') + .doesNotIncludeText( + 'John', + 'reactivity: Only display custom component, not firstName value', + ); + }); +}); diff --git a/test-app/tests/integration/components/data-table-content-body-test.gjs b/test-app/tests/integration/components/data-table-content-body-test.gjs new file mode 100644 index 0000000..93b5860 --- /dev/null +++ b/test-app/tests/integration/components/data-table-content-body-test.gjs @@ -0,0 +1,689 @@ +import { tracked } from '@glimmer/tracking'; +import { fn } from '@ember/helper'; +import { renderSettled } from '@ember/renderer'; +import { LinkTo } from '@ember/routing'; +import { click, render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; + +import RawDataTable from 'ember-data-table/components/raw-data-table'; +import { or } from 'ember-truth-helpers'; + +import { generatePaginationMeta } from '../../helpers'; + +module('Integration | Component | data table content body', function (hooks) { + setupRenderingTest(hooks); + + test('display rows', async function (assert) { + const content = [ + { firstName: 'John', lastName: 'Doe', age: 20 }, + { firstName: 'Jane', lastName: 'Doe', age: 21 }, + ]; + const fields = ['firstName', 'lastName', 'age']; + + await render( + , + ); + + assert.dom('tbody>tr').exists({ count: 2 }, 'displays 2 rows'); + assert + .dom('tbody>tr:first-child td') + .exists({ count: 3 }, 'displays 3 columns'); + assert + .dom('tbody>tr:first-child td:first-child') + .hasText('John', 'displays firstName in first column'); + assert + .dom('tbody>tr:first-child td:nth-child(2)') + .hasText('Doe', 'displays lastName in second column'); + assert + .dom('tbody>tr:first-child td:nth-child(3)') + .hasText('20', 'displays age in third column'); + }); + + test('add checkboxes for selection if enabled and allow initial selection', async function (assert) { + const john = { firstName: 'John', lastName: 'Doe', age: 20 }; + const jane = { firstName: 'Jane', lastName: 'Doe', age: 21 }; + const jeff = { firstName: 'Jeff', lastName: 'Doe', age: 22 }; + const content = [john, jane, jeff]; + const fields = ['firstName', 'lastName', 'age']; + const initialSelection = [jane]; + + class Context { + @tracked enableSelection; + } + + const context = new Context(); + + context.enableSelection = true; + + await render( + , + ); + + assert + .dom('tbody>tr:first-child td') + .exists({ count: 4 }, 'displays 4 columns'); + assert + .dom('tbody>tr.selected') + .exists({ count: 1 }, 'displays 1 selected row'); + assert + .dom('tbody>tr input[type="checkbox"]') + .exists({ count: 3 }, 'displays a checkbox on each row'); + assert + .dom('tbody>tr input[type="checkbox"]:checked') + .isChecked('displays 1 checked checkbox'); + + context.enableSelection = false; + await renderSettled(); + + assert + .dom('tbody>tr:first-child td') + .exists( + { count: 3 }, + 'reactivity: displays 3 columns when selection disabled', + ); + assert + .dom('tbody>tr.selected') + .doesNotExist('reactivity: no selected rows when selection disabled'); + assert + .dom('tbody>tr input[type="checkbox"]') + .doesNotExist('reactivity: no checkboxes when selection disabled'); + }); + + test('toggles selection if checkbox is clicked', async function (assert) { + const john = { firstName: 'John', lastName: 'Doe', age: 20 }; + const jane = { firstName: 'Jane', lastName: 'Doe', age: 21 }; + const jeff = { firstName: 'Jeff', lastName: 'Doe', age: 22 }; + const content = [john, jane, jeff]; + const fields = ['firstName', 'lastName', 'age']; + const selection = [jane]; + + await render( + , + ); + + assert + .dom('tbody>tr input[type="checkbox"]:checked') + .isChecked('displays 1 checked checkbox before selecting a row'); + await click('tbody>tr:first-child input[type="checkbox"]'); + assert + .dom('tbody>tr input[type="checkbox"]:checked') + .isChecked('displays 2 checked checkboxes after selecting a row'); + }); + + test('keep selection intact over page changes', async function (assert) { + const john = { firstName: 'John', lastName: 'Doe', age: 20 }; + const jane = { firstName: 'Jane', lastName: 'Doe', age: 21 }; + const jeff = { firstName: 'Jeff', lastName: 'Doe', age: 22 }; + const content = [john, jane, jeff]; + const fields = ['firstName', 'lastName', 'age']; + const selection = []; + const meta = generatePaginationMeta(0, 1, 3); + + class testContext { + @tracked page = 0; + @tracked size = 1; + @tracked content; + } + + const context = new testContext(); + + context.content = [john]; + + const changePage = (page) => { + context.page = page; + context.content = [content[page]]; + }; + + await render( + , + ); + + await click('tbody>tr:nth-child(1) input[type="checkbox"]'); + assert + .dom('tbody>tr input[type="checkbox"]:checked') + .isChecked('displays 1 checked checkbox after selecting a row'); + + await click('.data-table-pagination-right button:nth-child(3)'); // Next button + assert + .dom('tbody>tr input[type="checkbox"]') + .isNotChecked('no selection on next page'); + await click('.data-table-pagination-right button:nth-child(2)'); // Previous button + assert + .dom('tbody>tr input[type="checkbox"]:checked') + .isChecked('selection intact on first page after page change'); + }); + + test('add line numbers if enabled', async function (assert) { + const john = { firstName: 'John', lastName: 'Doe', age: 20 }; + const jane = { firstName: 'Jane', lastName: 'Doe', age: 21 }; + const jeff = { firstName: 'Jeff', lastName: 'Doe', age: 22 }; + const content = [john, jane, jeff]; + const fields = ['firstName', 'lastName', 'age']; + + class testContext { + @tracked enableLineNumbers; + } + + const context = new testContext(); + + context.enableLineNumbers = true; + + await render( + , + ); + + assert + .dom('tbody>tr:first-child td') + .exists({ count: 4 }, 'displays 4 columns'); + assert + .dom('tbody>tr:first-child td:first-child') + .hasText('1', 'displays offset 1 on the first row'); + assert + .dom('tbody>tr:nth-child(2) td:first-child') + .hasText('2', 'displays offset 2 on the second row'); + assert + .dom('tbody>tr:nth-child(3) td:first-child') + .hasText('3', 'displays offset 3 on the third row'); + context.enableLineNumbers = false; + await renderSettled(); + + assert + .dom('tbody>tr:first-child td') + .exists( + { count: 3 }, + 'reactivity: displays 3 columns (no line numbers column) when line numbers disabled', + ); + + context.enableLineNumbers = true; + + const page = 2; + const size = 5; + + await render( + , + ); + + assert + .dom('tbody>tr:first-child td') + .exists({ count: 4 }, 'displays 4 columns on page 3'); + assert + .dom('tbody>tr:first-child td:first-child') + .hasText('11', 'displays offset 11 on the first row on page 3'); + assert + .dom('tbody>tr:nth-child(2) td:first-child') + .hasText('12', 'displays offset 12 on the second row on page 3'); + assert + .dom('tbody>tr:nth-child(3) td:first-child') + .hasText('13', 'displays offset 13 on the third row of page 3'); + }); + + test('@selection and @updateSelection work', async function (assert) { + const john = { firstName: 'John', lastName: 'Doe', age: 20 }; + const jane = { firstName: 'Jane', lastName: 'Doe', age: 21 }; + const jeff = { firstName: 'Jeff', lastName: 'Doe', age: 22 }; + const content = [john, jane, jeff]; + const fields = ['firstName', 'lastName', 'age']; + + class Context { + @tracked selection; + } + + const context = new Context(); + + context.selection = [jane]; + + const updateSelection = (newSelection) => { + context.selection = newSelection; + }; + + await render( + , + ); + assert + .dom('tbody>tr:nth-child(2) input[type="checkbox"]:checked') + .isChecked( + 'displays 1 checked checkbox on second row before selecting another row', + ); + await click('tbody>tr:first-child input[type="checkbox"]'); + assert + .dom('tbody>tr:nth-child(1) input[type="checkbox"]:checked') + .isChecked( + 'displays 2 checked checkboxes after selecting a row (first row)', + ); + assert + .dom('tbody>tr:nth-child(2) input[type="checkbox"]:checked') + .isChecked( + 'displays 2 checked checkboxes after selecting a row (second row)', + ); + assert.propEqual(context.selection, [jane, john], 'selection is updated'); + await click('tbody>tr:first-child input[type="checkbox"]'); + assert.propEqual(context.selection, [jane], 'selection is updated'); + }); + + test('displays no data message if there is no data', async function (assert) { + const noDataMessage = 'No data'; + const dataTable = { + parsedFields: ['firstName', 'lastName', 'age'], + selection: [], + }; + + await render( + , + ); + assert + .dom('td.no-data-message') + .exists({ count: 1 }, 'displays a no data message if no content'); + assert + .dom('td.no-data-message') + .hasText('No data', 'displays message "No data" if no content'); + + const emptyContent = []; + + await render( + , + ); + assert + .dom('td.no-data-message') + .exists({ count: 1 }, 'displays a no data message if empty content'); + assert + .dom('td.no-data-message') + .hasText('No data', 'displays message "No data" if empty content'); + + const content = ['foo', 'bar']; + + await render( + , + ); + assert + .dom('td.no-data-message') + .doesNotExist('displays no message when there is content'); + }); + + test('@links property adds a links to every column', async function (assert) { + class testContext { + @tracked links; + } + + const context = new testContext(); + + context.links = [ + 'edit', + 'edit:Edit_Link', + 'edit:Edit_Link:icon-reference', + { route: 'edit' }, + { route: 'edit', label: 'Edit:Link' }, + { route: 'edit', label: 'Edit:Link', icon: 'icon:reference' }, + ]; + + const content = [ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' }, + ]; + + await render( + , + ); + + assert + .dom('tbody>tr:first-child td:nth-child(2) a:first-child') + .hasText('edit', 'string config: renders edit link'); + assert + .dom('tbody>tr:first-child td:nth-child(2) a:nth-child(2)') + .hasText('Edit Link', 'string config: renders edit link with label'); + assert + .dom('tbody>tr:first-child td:nth-child(2) a:nth-child(3)') + .hasText('Edit Link', 'string config: renders icon edit link with label'); + assert + .dom('tbody>tr:first-child td:nth-child(2) a:nth-child(4)') + .hasText('edit', 'object config: renders edit link'); + assert + .dom('tbody>tr:first-child td:nth-child(2) a:nth-child(5)') + .hasText('Edit:Link', 'object config: renders edit link with label'); + assert + .dom('tbody>tr:first-child td:nth-child(2) a:nth-child(6)') + .hasText('Edit:Link', 'object config: renders icon edit link with label'); + + assert + .dom('tbody>tr:nth-child(2) td:nth-child(2) a:first-child') + .hasText('edit', 'string config: renders edit link'); + assert + .dom('tbody>tr:nth-child(2) td:nth-child(2) a:nth-child(2)') + .hasText('Edit Link', 'string config: renders edit link with label'); + assert + .dom('tbody>tr:nth-child(2) td:nth-child(2) a:nth-child(3)') + .hasText('Edit Link', 'string config: renders icon edit link with label'); + assert + .dom('tbody>tr:nth-child(2) td:nth-child(2) a:nth-child(4)') + .hasText('edit', 'object config: renders edit link'); + assert + .dom('tbody>tr:nth-child(2) td:nth-child(2) a:nth-child(5)') + .hasText('Edit:Link', 'object config: renders edit link with label'); + assert + .dom('tbody>tr:nth-child(2) td:nth-child(2) a:nth-child(6)') + .hasText('Edit:Link', 'object config: renders icon edit link with label'); + + context.links = []; + await renderSettled(); + + assert + .dom('tbody>tr:first-child td:nth-child(2) a') + .doesNotExist('reactivity: no links when links is empty'); + }); + + test('@links property data is provided for :actions: block', async function (assert) { + const links = [ + 'edit', + 'edit:Edit_Link', + 'edit:Edit_Link:icon-reference', + { route: 'edit' }, + { route: 'edit', label: 'Edit:Link' }, + { route: 'edit', label: 'Edit:Link', icon: 'icon:reference' }, + ]; + + const content = [ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' }, + ]; + + await render( + , + ); + + assert + .dom('tbody>tr:first-child td a:first-child') + .hasText('model: 1 edit', 'string config: renders edit link'); + assert + .dom('tbody>tr:first-child td a:nth-child(2)') + .hasText( + 'model: 1 Edit Link', + 'string config: renders edit link with label', + ); + assert + .dom('tbody>tr:first-child td a:nth-child(3)') + .hasText( + 'model: 1 icon: icon-reference Edit Link', + 'string config: renders icon edit link with label', + ); + assert + .dom('tbody>tr:first-child td a:nth-child(4)') + .hasText('model: 1 edit', 'object config: renders edit link'); + assert + .dom('tbody>tr:first-child td a:nth-child(5)') + .hasText( + 'model: 1 Edit:Link', + 'object config: renders edit link with label', + ); + assert + .dom('tbody>tr:first-child td a:nth-child(6)') + .hasText( + 'model: 1 icon: icon:reference Edit:Link', + 'object config: renders icon edit link with label', + ); + + assert + .dom('tbody>tr:nth-child(2) td a:first-child') + .hasText('model: 2 edit', '2nd column: string config: renders edit link'); + assert + .dom('tbody>tr:nth-child(2) td a:nth-child(2)') + .hasText( + 'model: 2 Edit Link', + '2nd column: string config: renders edit link with label', + ); + assert + .dom('tbody>tr:nth-child(2) td a:nth-child(3)') + .hasText( + 'model: 2 icon: icon-reference Edit Link', + '2nd column: string config: renders icon edit link with label', + ); + assert + .dom('tbody>tr:nth-child(2) td a:nth-child(4)') + .hasText('model: 2 edit', '2nd column: object config: renders edit link'); + assert + .dom('tbody>tr:nth-child(2) td a:nth-child(5)') + .hasText( + 'model: 2 Edit:Link', + '2nd column: object config: renders edit link with label', + ); + assert + .dom('tbody>tr:nth-child(2) td a:nth-child(6)') + .hasText( + 'model: 2 icon: icon:reference Edit:Link', + '2nd column: object config: renders icon edit link with label', + ); + }); + + test('@linksModelProperty adds model to the link', async function (assert) { + const links = [ + 'edit', + 'edit:Edit_Link', + 'edit:Edit_Link:icon-reference', + { route: 'edit' }, + { route: 'edit', label: 'Edit:Link' }, + { route: 'edit', label: 'Edit:Link', icon: 'icon:reference' }, + ]; + + const content = [ + { id: 1, name: 'John' }, + { id: 2, name: 'Jane' }, + ]; + + await render( + , + ); + + assert + .dom('tbody>tr:first-child td a:first-child') + .hasText('model: John edit', 'string config: renders edit link'); + assert + .dom('tbody>tr:first-child td a:nth-child(2)') + .hasText( + 'model: John Edit Link', + 'string config: renders edit link with label', + ); + assert + .dom('tbody>tr:first-child td a:nth-child(3)') + .hasText( + 'model: John icon: icon-reference Edit Link', + 'string config: renders icon edit link with label', + ); + assert + .dom('tbody>tr:first-child td a:nth-child(4)') + .hasText('model: John edit', 'object config: renders edit link'); + assert + .dom('tbody>tr:first-child td a:nth-child(5)') + .hasText( + 'model: John Edit:Link', + 'object config: renders edit link with label', + ); + assert + .dom('tbody>tr:first-child td a:nth-child(6)') + .hasText( + 'model: John icon: icon:reference Edit:Link', + 'object config: renders icon edit link with label', + ); + + assert + .dom('tbody>tr:nth-child(2) td a:first-child') + .hasText( + 'model: Jane edit', + '2nd column: string config: renders edit link', + ); + assert + .dom('tbody>tr:nth-child(2) td a:nth-child(2)') + .hasText( + 'model: Jane Edit Link', + '2nd column: string config: renders edit link with label', + ); + assert + .dom('tbody>tr:nth-child(2) td a:nth-child(3)') + .hasText( + 'model: Jane icon: icon-reference Edit Link', + '2nd column: string config: renders icon edit link with label', + ); + assert + .dom('tbody>tr:nth-child(2) td a:nth-child(4)') + .hasText( + 'model: Jane edit', + '2nd column: object config: renders edit link', + ); + assert + .dom('tbody>tr:nth-child(2) td a:nth-child(5)') + .hasText( + 'model: Jane Edit:Link', + '2nd column: object config: renders edit link with label', + ); + assert + .dom('tbody>tr:nth-child(2) td a:nth-child(6)') + .hasText( + 'model: Jane icon: icon:reference Edit:Link', + '2nd column: object config: renders icon edit link with label', + ); + }); + + test('@onClickRow adds a click handler to the row', async function (assert) { + const content = [ + { id: 1, name: 'John' }, + { id: 2, name: 'Jane' }, + ]; + + const onClickRow = () => { + assert.step('clicked'); + }; + + await render( + , + ); + + await click('tbody>tr:first-child'); + assert.verifySteps(['clicked']); + await click('tbody>tr:nth-child(2)'); + assert.verifySteps(['clicked']); + }); +}); diff --git a/test-app/tests/integration/components/data-table-content-header-test.gjs b/test-app/tests/integration/components/data-table-content-header-test.gjs new file mode 100644 index 0000000..112f2b4 --- /dev/null +++ b/test-app/tests/integration/components/data-table-content-header-test.gjs @@ -0,0 +1,196 @@ +import { render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; + +import RawDataTable from 'ember-data-table/components/raw-data-table'; + +module('Integration | Component | data table content header', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + await render(); + assert.dom('thead').exists({ count: 1 }); + assert.dom('thead').hasText(''); + // Template block usage: + await render( + , + ); + assert.dom('thead').hasText('template block text'); + }); + + test('display column headers', async function (assert) { + const fields = ['firstName', 'lastName', 'age']; + + await render(); + assert.dom('thead tr').exists({ count: 1 }, 'displays 1 header row'); + assert.dom('thead tr th').exists({ count: 3 }, 'displays 3 column headers'); + assert + .dom('thead tr th:first-child') + .hasText('firstName', 'displays firstName as first header'); + assert + .dom('thead tr th:nth-child(2)') + .hasText('lastName', 'displays lastName as second column header'); + assert + .dom('thead tr th:nth-child(3)') + .hasText('age', 'displays age as third column header'); + }); + + test('add selection column header if enabled', async function (assert) { + const fields = ['firstName', 'lastName', 'age']; + + await render( + , + ); + assert.dom('thead tr').exists({ count: 1 }, 'displays 1 header row'); + assert.dom('thead tr th').exists({ count: 4 }, 'displays 4 column headers'); + assert + .dom('thead tr th:first-child') + .hasText('', 'displays selection as first header'); + }); + + test('add line number column header if enabled', async function (assert) { + const fields = ['firstName', 'lastName', 'age']; + + await render( + , + ); + assert.dom('thead tr').exists({ count: 1 }, 'displays 1 header row'); + assert.dom('thead tr th').exists({ count: 4 }, 'displays 4 column headers'); + assert + .dom('thead tr th:first-child') + .hasText('', 'displays line number as first header'); + }); + + test('display custom column headers defined via fields', async function (assert) { + const fields = [ + 'firstName', + 'lastName:Last_Name', + 'age:Some__Age', + { attribute: 'birthday', label: 'birth:__:day' }, + ]; + + await render(); + assert.dom('thead tr').exists({ count: 1 }, 'displays 1 header row'); + assert.dom('thead tr th').exists({ count: 4 }, 'displays 4 column headers'); + assert + .dom('thead tr th:first-child') + .hasText('firstName', 'displays firstName as first header'); + assert + .dom('thead tr th:nth-child(2)') + .hasText('Last Name', 'displays lastName as second column header'); + assert + .dom('thead tr th:nth-child(3)') + .hasText('Some_Age', 'displays age as third column header'); + assert + .dom('thead tr th:nth-child(4)') + .hasText('birth:__:day', 'displays age as third column header'); + }); + + test('display custom :data-header block via @customHeaders', async function (assert) { + const fields = ['firstName', 'lastName', 'age']; + + await render( + , + ); + assert.dom('thead tr').exists({ count: 1 }, 'displays 1 header row'); + assert.dom('thead tr th').exists({ count: 3 }, 'displays 3 column headers'); + assert + .dom('thead tr th:first-child') + .hasText( + 'custom:firstName', + 'displays custom block firstName as first header', + ); + assert + .dom('thead tr th:nth-child(2)') + .hasText( + 'custom:lastName', + 'displays custom blocklastName as second column header', + ); + assert + .dom('thead tr th:nth-child(3)') + .hasText('age', 'displays age as third column header'); + }); + + test('display custom template via @customHeaders', async function (assert) { + const fields = ['firstName', 'lastName', 'age']; + const customTemplate = ; + const customHeaders = { firstName: customTemplate, lastName: '' }; + + await render( + , + ); + + assert.dom('thead tr').exists({ count: 1 }, 'displays 1 header row'); + assert.dom('thead tr th').exists({ count: 3 }, 'displays 3 column headers'); + assert + .dom('thead tr th:first-child') + .hasText( + 'customTemplate:firstName', + 'displays custom template for firstName as first header', + ); + assert + .dom('thead tr th:nth-child(2)') + .hasText( + 'custom:lastName', + 'displays custom block lastName as second column header', + ); + assert + .dom('thead tr th:nth-child(3)') + .hasText('age', 'displays age as third column header'); + }); + + test('display normal column headers if @customHeaders is empty', async function (assert) { + const fields = ['firstName', 'lastName', 'age']; + const customHeaders = [null, '', {}]; + + for (const customHeader of customHeaders) { + await render( + , + ); + + assert.dom('thead tr').exists({ count: 1 }, 'displays 1 header row'); + assert + .dom('thead tr th') + .exists({ count: 3 }, 'displays 3 column headers'); + assert + .dom('thead tr th:first-child') + .hasText('firstName', 'displays firstName as first header'); + assert + .dom('thead tr th:nth-child(2)') + .hasText('lastName', 'displays lastName as second column header'); + assert + .dom('thead tr th:nth-child(3)') + .hasText('age', 'displays age as third column header'); + } + }); +}); diff --git a/test-app/tests/integration/components/data-table-content-test.gjs b/test-app/tests/integration/components/data-table-content-test.gjs new file mode 100644 index 0000000..116c5da --- /dev/null +++ b/test-app/tests/integration/components/data-table-content-test.gjs @@ -0,0 +1,29 @@ +import { render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; + +import RawDataTable from 'ember-data-table/components/raw-data-table'; + +module('Integration | Component | data table content', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + await render(); + assert + .dom('table.data-table') + .exists({ count: 1 }, 'displays 1 data table'); + + // Template block usage: + await render( + , + ); + + assert.dom('*').includesText('template block text'); + }); +}); diff --git a/test-app/tests/integration/components/data-table-menu-general-test.gjs b/test-app/tests/integration/components/data-table-menu-general-test.gjs new file mode 100644 index 0000000..65506a0 --- /dev/null +++ b/test-app/tests/integration/components/data-table-menu-general-test.gjs @@ -0,0 +1,61 @@ +import { render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; + +import RawDataTable from 'ember-data-table/components/raw-data-table'; + +module('Integration | Component | data table menu general', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + await render(); + + assert + .dom('.data-table-menu') + .exists({ count: 1 }, 'Menu container exists'); + }); + + test('it renders block only if data table selection is empty', async function (assert) { + const john = { firstName: 'John', lastName: 'Doe', age: 20 }; + const jane = { firstName: 'Jane', lastName: 'Doe', age: 21 }; + const jeff = { firstName: 'Jeff', lastName: 'Doe', age: 22 }; + const content = [john, jane, jeff]; + const fields = ['firstName', 'lastName', 'age']; + + // Test with empty selection + await render( + , + ); + assert.dom('.data-table-menu').containsText('template block text'); + + // Test with non-empty selection + const initialSelection = [jane]; + + await render( + , + ); + + assert.dom('.data-table-menu').doesNotContainText('template block text'); + }); +}); diff --git a/test-app/tests/integration/components/data-table-menu-selected-test.gjs b/test-app/tests/integration/components/data-table-menu-selected-test.gjs new file mode 100644 index 0000000..c026234 --- /dev/null +++ b/test-app/tests/integration/components/data-table-menu-selected-test.gjs @@ -0,0 +1,147 @@ +import { tracked } from '@glimmer/tracking'; +import { click,render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; + +import RawDataTable from 'ember-data-table/components/raw-data-table'; + +module('Integration | Component | data table menu selected', function (hooks) { + setupRenderingTest(hooks); + + test('it renders block only if data table selection is not empty', async function (assert) { + await render( + , + ); + assert.dom('.data-table-menu .item-count').doesNotExist(); + }); + + test('it renders selection count in raw-data-table', async function (assert) { + const john = { firstName: 'John', lastName: 'Doe', age: 20 }; + const jane = { firstName: 'Jane', lastName: 'Doe', age: 21 }; + const content = [john, jane]; + const fields = ['firstName', 'lastName', 'age']; + const selection = [john]; + + await render( + , + ); + assert + .dom('.data-table-menu .item-count') + .hasText('1 item(s) selected', 'item count 1'); + + const selection2 = [john, jane]; + + await render( + , + ); + assert + .dom('.data-table-menu .item-count') + .hasText('2 item(s) selected', 'item count 2'); + }); + + test('calls clearSelection on cancel button click', async function (assert) { + const john = { firstName: 'John', lastName: 'Doe', age: 20 }; + const jane = { firstName: 'Jane', lastName: 'Doe', age: 21 }; + const content = [john, jane]; + const fields = ['firstName', 'lastName', 'age']; + const selection = [john]; + + await render( + , + ); + assert + .dom('.data-table-menu button') + .hasText('Cancel', 'renders a cancel button'); + await click('.data-table-menu button'); + assert.dom('.data-table-menu .item-count').doesNotExist(); + }); + + test('sets empty selection for @updateSelection on cancel button click', async function (assert) { + const john = { firstName: 'John', lastName: 'Doe', age: 20 }; + const jane = { firstName: 'Jane', lastName: 'Doe', age: 21 }; + const content = [john, jane]; + const fields = ['firstName', 'lastName', 'age']; + + class Context { + @tracked selection; + } + + const context = new Context(); + + context.selection = [john]; + + const updateSelection = (newSelection) => { + assert.true(newSelection.length === 0, 'selection is empty'); + }; + + await render( + , + ); + assert + .dom('.data-table-menu button') + .hasText('Cancel', 'renders a cancel button'); + await click('.data-table-menu button'); + }); + + test('renders actions in selection-menu-actions block', async function (assert) { + const john = { firstName: 'John', lastName: 'Doe', age: 20 }; + const jane = { firstName: 'Jane', lastName: 'Doe', age: 21 }; + const content = [john, jane]; + const fields = ['firstName', 'lastName', 'age']; + const selection = [john]; + + await render( + , + ); + assert + .dom('.data-table-menu .item-count') + .hasText('1 item(s) selected', 'item count 1'); + assert.dom('.data-table-menu').containsText('template block text'); + }); +}); diff --git a/test-app/tests/integration/components/data-table-menu-test.gjs b/test-app/tests/integration/components/data-table-menu-test.gjs new file mode 100644 index 0000000..9b91796 --- /dev/null +++ b/test-app/tests/integration/components/data-table-menu-test.gjs @@ -0,0 +1,54 @@ +import { tracked } from '@glimmer/tracking'; +import { renderSettled } from '@ember/renderer'; +import { render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; + +import RawDataTable from 'ember-data-table/components/raw-data-table'; + +module('Integration | Component | data table menu', function (hooks) { + setupRenderingTest(hooks); + test('it renders', async function (assert) { + await render(); + + assert + .dom('.data-table-menu') + .exists({ count: 1 }, 'Menu container exists'); + + // Template block usage: + await render( + , + ); + + assert.dom('.raw-data-table').containsText('template block text'); + }); + + test('can toggle menu with @showMenu', async function (assert) { + class testContext { + @tracked showMenu; + } + + const context = new testContext(); + + context.showMenu = true; + await render( + , + ); + + assert + .dom('.data-table-menu') + .exists({ count: 1 }, 'Menu container exists'); + + context.showMenu = false; + await renderSettled(); + assert + .dom('.data-table-menu') + .doesNotExist('Menu container does not exist'); + }); +}); diff --git a/test-app/tests/integration/components/data-table-test.gjs b/test-app/tests/integration/components/data-table-test.gjs new file mode 100644 index 0000000..afeb500 --- /dev/null +++ b/test-app/tests/integration/components/data-table-test.gjs @@ -0,0 +1,104 @@ +import { tracked } from '@glimmer/tracking'; +import { renderSettled } from '@ember/renderer'; +import { render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; + +import RawDataTable from 'ember-data-table/components/raw-data-table'; + +module('Integration | Component | data table', function (hooks) { + setupRenderingTest(hooks); + test('it renders', async function (assert) { + const content = []; + const meta = { + pagination: { + first: { number: 1 }, + last: { number: 10 }, + }, + }; + + await render( + , + ); + + assert.dom('.raw-data-table').exists({ count: 1 }, 'renders a data table'); + assert + .dom('.raw-data-table .data-table-content') + .exists({ count: 1 }, 'renders table inside content container'); + }); + + test('content is reactive', async function (assert) { + class testContext { + @tracked content = []; + } + + const context = new testContext(); + + context.content = []; + + await render( + , + ); + + assert.dom('.raw-data-table').exists({ count: 1 }, 'renders a data table'); + + assert + .dom('.raw-data-table tbody tr') + .doesNotContainText( + 'new person', + 'do not render rows when content is empty', + ); + context.content = [{ id: 1, name: 'new person' }]; + await renderSettled(); + + assert + .dom('.raw-data-table tbody tr') + .exists({ count: 1 }, 'add row for new person'); + assert + .dom('.raw-data-table tbody tr:nth-child(1)') + .containsText('new person', 'renders new person'); + }); + + test('attribute @noDataMessage', async function (assert) { + const content = []; + const noDataMessage = 'No data'; + + await render( + , + ); + + assert + .dom('.raw-data-table .no-data-message') + .exists({ count: 1 }, 'No data message renders if content empty'); + assert + .dom('.raw-data-table .no-data-message') + .containsText(noDataMessage, 'renders @noDataMessage'); + }); + + test('no data message block', async function (assert) { + const content = []; + + await render( + , + ); + + assert + .dom('.raw-data-table .no-data-message-block') + .exists({ count: 1 }, 'No data message renders if content empty'); + assert + .dom('.raw-data-table .no-data-message-block') + .containsText('template block text', 'renders template block'); + }); +}); diff --git a/test-app/tests/integration/components/number-pagination-test.gjs b/test-app/tests/integration/components/number-pagination-test.gjs new file mode 100644 index 0000000..6079d75 --- /dev/null +++ b/test-app/tests/integration/components/number-pagination-test.gjs @@ -0,0 +1,141 @@ +import { tracked } from '@glimmer/tracking'; +import { fn } from '@ember/helper'; +import { click, fillIn, render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; + +import RawDataTable from 'ember-data-table/components/raw-data-table'; + +import { generatePaginationMeta } from '../../helpers'; + +module('Integration | Component | number pagination', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + const page = 0; + const meta = generatePaginationMeta(page, 10, 10); + + await render( + , + ); + + assert.dom('.raw-data-table .data-table-pagination').exists({ count: 1 }); + }); + + test('page and size are reactive', async function (assert) { + const content = [ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' }, + { id: 3, name: 'John Smith' }, + { id: 4, name: 'Jane Smith' }, + ]; + const meta = generatePaginationMeta(0, 1, 4); + + class testContext { + @tracked page = 0; + @tracked size = 1; + } + + const context = new testContext(); + + await render( + , + ); + + assert.equal(context.page, 0, 'initial page value is correct'); + await click('.data-table-pagination-right button:nth-child(3)'); // Next button + assert.equal(context.page, 1, 'page value changed reactively'); + await click('.data-table-pagination-right button:nth-child(2)'); // Previous button + assert.equal(context.page, 0, 'page value changed reactively back'); + await fillIn('.data-table-pagination-left select', '5'); + assert.equal(context.size, 5, 'size value changed reactively'); + }); + + test('set sizes via @sizes', async function (assert) { + const content = [ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' }, + { id: 3, name: 'John Smith' }, + { id: 4, name: 'Jane Smith' }, + ]; + const meta = generatePaginationMeta(0, 1, 4); + const sizes = [111, 222]; + const currentSize = 111; // the pagination will always render the current selected size too + + await render( + , + ); + + assert + .dom('.data-table-pagination-left select option') + .exists({ count: sizes.length }, 'does not render extra sizes'); + assert + .dom('.data-table-pagination-left select option') + .hasText('111', 'renders first size'); + assert + .dom('.data-table-pagination-left select') + .hasValue('111', 'renders first size'); + assert + .dom('.data-table-pagination-left select option:last-child') + .hasText('222', 'renders second size'); + }); + + test('allow to disable sizes via @sizes', async function (assert) { + const content = [ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' }, + { id: 3, name: 'John Smith' }, + { id: 4, name: 'Jane Smith' }, + ]; + const meta = generatePaginationMeta(0, 1, 4); + const sizes = null; + + await render( + , + ); + + assert + .dom('.data-table-pagination-left select') + .doesNotExist('does not render size select if null'); + + await render( + , + ); + + assert + .dom('.data-table-pagination-left select') + .doesNotExist('does not render size select if empty string'); + }); +}); diff --git a/test-app/tests/integration/components/text-search-test.gjs b/test-app/tests/integration/components/text-search-test.gjs new file mode 100644 index 0000000..c3780e5 --- /dev/null +++ b/test-app/tests/integration/components/text-search-test.gjs @@ -0,0 +1,191 @@ +import { fillIn, render, waitUntil } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; + +import RawDataTable from 'ember-data-table/components/raw-data-table'; + +module('Integration | Component | text search', function (hooks) { + setupRenderingTest(hooks); + + test('it renders by default', async function (assert) { + await render(); + assert.dom('.raw-data-table .data-table-search').exists({ count: 1 }); + }); + + test('it does not render if filter is not set', async function (assert) { + await render(); + assert.dom('.raw-data-table .data-table-search').doesNotExist(); + }); + + test('it does not render if enableSearch is false', async function (assert) { + await render( + , + ); + assert.dom('.raw-data-table .data-table-search').doesNotExist(); + }); + + test('@searchPlaceholder text is rendered', async function (assert) { + const searchPlaceholder = 'Search Placeholder Text'; + + await render( + , + ); + assert + .dom('.raw-data-table .data-table-search input') + .hasAttribute( + 'placeholder', + searchPlaceholder, + 'renders @searchPlaceholder', + ); + }); + test('@searchPlaceholder text can be empty', async function (assert) { + await render( + , + ); + assert + .dom('.raw-data-table .data-table-search input') + .hasAttribute('placeholder', '', 'renders empty @searchPlaceholder'); + }); + + test('@autoSearch calls @updateFilter after delay', async function (assert) { + let filterValue = ''; + const updateFilter = (value) => { + filterValue = value; + }; + + await render( + , + ); + + fillIn('.raw-data-table .data-table-search input', 'test search'); + assert.strictEqual( + filterValue, + '', + 'update of filter has a debounce delay', + ); + + await waitUntil(() => filterValue != '', { timeout: 2000 }); + assert.strictEqual( + filterValue, + 'test search', + 'filter updated after delay', + ); + }); + test('@autoSearch default: calls @updateFilter after delay', async function (assert) { + let filterValue = ''; + const updateFilter = (value) => { + filterValue = value; + }; + + await render( + , + ); + + fillIn('.raw-data-table .data-table-search input', 'test search'); + assert.strictEqual( + filterValue, + '', + 'update of filter has a debounce delay', + ); + + await waitUntil(() => filterValue != '', { timeout: 2000 }); + assert.strictEqual( + filterValue, + 'test search', + 'filter updated after delay', + ); + }); + + test('@autoSearch=true calls @updateFilter after delay', async function (assert) { + let filterValue = ''; + const updateFilter = (value) => { + filterValue = value; + }; + + await render( + , + ); + + fillIn('.raw-data-table .data-table-search input', 'test search'); + assert.strictEqual( + filterValue, + '', + 'update of filter has a debounce delay', + ); + + await waitUntil(() => filterValue != '', { timeout: 2000 }); + assert.strictEqual( + filterValue, + 'test search', + 'filter updated after delay', + ); + }); + + test('@autoSearch=number calls @updateFilter after small delay in ms', async function (assert) { + let filterValue = ''; + const updateFilter = (value) => { + filterValue = value; + }; + + await render( + , + ); + + fillIn('.raw-data-table .data-table-search input', 'test search'); + await waitUntil(() => filterValue != '', { timeout: 10 }); + assert.strictEqual( + filterValue, + 'test search', + 'update of filter has no debounce delay (10ms)', + ); + }); + + test('@autoSearch=number calls @updateFilter after higher delay in ms', async function (assert) { + let filterValue = ''; + const updateFilter = (value) => { + filterValue = value; + }; + + await render( + , + ); + + fillIn('.raw-data-table .data-table-search input', 'test search'); + await new Promise((resolve) => setTimeout(resolve, 2000)); + assert.strictEqual( + filterValue, + '', + 'update of filter has higher than 2000ms debounce delay (4000ms)', + ); + await waitUntil(() => filterValue != '', { timeout: 2000 }); + assert.strictEqual( + filterValue, + 'test search', + 'update of filter after given debounce delay', + ); + }); +}); diff --git a/test-app/tests/integration/components/th-sortable-test.gjs b/test-app/tests/integration/components/th-sortable-test.gjs new file mode 100644 index 0000000..d34f81d --- /dev/null +++ b/test-app/tests/integration/components/th-sortable-test.gjs @@ -0,0 +1,181 @@ +import { tracked } from '@glimmer/tracking'; +import { fn } from '@ember/helper'; +import { click, render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; + +import RawDataTable from 'ember-data-table/components/raw-data-table'; + +module('Integration | Component | th sortable', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + const fields = ['title']; + const sortableFields = ['title']; + + await render( + , + ); + + assert.dom('.data-table .sortable').exists({ count: 1 }); + }); + + test('sort is reactive', async function (assert) { + const content = [ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' }, + ]; + + class testContext { + @tracked sort = ''; + } + + const context = new testContext(); + + await render( + , + ); + + assert.equal(context.sort, '', 'initial sort value is correct'); + await click('.raw-data-table .sortable span'); + assert.equal(context.sort, 'name', 'sort value changed reactively'); + await click('.raw-data-table .sortable span'); + assert.equal( + context.sort, + '-name', + 'sort value changed reactively in correct order', + ); + await click('.raw-data-table .sortable span'); + assert.equal( + context.sort, + null, + 'sort value got cleared after three clicks', + ); + }); + + test('fields are sortable by default', async function (assert) { + const content = [{ id: 1, name: 'John Doe' }]; + + await render( + , + ); + + assert + .dom('.raw-data-table .sortable span') + .exists({ count: 1 }, 'sortable span exists'); + assert + .dom('.raw-data-table .sortable span') + .containsText('name', 'sortable span contains name'); + }); + + test('disable sorting with empty string or list', async function (assert) { + const content = [{ id: 1, name: 'John Doe' }]; + + await render( + , + ); + + assert + .dom('.raw-data-table .sortable span') + .doesNotExist('no sortable header if empty string'); + + const emptyList = []; + + await render( + , + ); + + assert + .dom('.raw-data-table .sortable span') + .doesNotExist('no sortable header if empty array'); + + await render( + , + ); + assert + .dom('.raw-data-table .sortable span') + .doesNotExist('no sortable header if inexistent field'); + }); + + test('set custom sorting params via @attributeToSortParams', async function (assert) { + const content = [{ id: 1, name: 'John Doe' }]; + + function customSort(attribute) { + return { + 'custom1 asc': `++${attribute}`, + 'custom2 desc': `--${attribute}`, + 'custom3 asc desc': `++--${attribute}`, + }; + } + + class Context { + @tracked sort = ''; + } + + const context = new Context(); + + await render( + , + ); + + assert + .dom('.raw-data-table .sortable span') + .exists({ count: 1 }, 'sortable span exists'); + assert + .dom('.raw-data-table .sortable span') + .containsText('name', 'sortable span contains name'); + assert.equal(context.sort, '', 'initial sort value is no sort'); + await click('.raw-data-table .sortable span'); + assert.equal(context.sort, '++name', 'sort value changed reactively'); + await click('.raw-data-table .sortable span'); + assert.equal( + context.sort, + '--name', + 'sort value changed reactively in correct order', + ); + await click('.raw-data-table .sortable span'); + assert.equal( + context.sort, + '++--name', + 'sort value changed reactively in correct order', + ); + await click('.raw-data-table .sortable span'); + assert.equal( + context.sort, + null, + 'sort value got cleared after four clicks', + ); + }); +}); diff --git a/tests/test-helper.js b/test-app/tests/test-helper.js similarity index 60% rename from tests/test-helper.js rename to test-app/tests/test-helper.js index 4efd6e5..b2dea0b 100644 --- a/tests/test-helper.js +++ b/test-app/tests/test-helper.js @@ -1,10 +1,14 @@ -import Application from 'dummy/app'; -import config from 'dummy/config/environment'; -import * as QUnit from 'qunit'; import { setApplication } from '@ember/test-helpers'; +import * as QUnit from 'qunit'; import { setup } from 'qunit-dom'; import { start } from 'ember-qunit'; +import Application from 'test-app/app'; +import config from 'test-app/config/environment'; + +QUnit.config.testTimeout = 60000; +QUnit.config.reorder = false; + setApplication(Application.create(config.APP)); setup(QUnit.assert); diff --git a/app/.gitkeep b/test-app/tests/unit/.gitkeep similarity index 100% rename from app/.gitkeep rename to test-app/tests/unit/.gitkeep diff --git a/test-app/tests/unit/utils/data-table-content-test.js b/test-app/tests/unit/utils/data-table-content-test.js new file mode 100644 index 0000000..d7ed277 --- /dev/null +++ b/test-app/tests/unit/utils/data-table-content-test.js @@ -0,0 +1,107 @@ +import { module, test } from 'qunit'; + +import { + deUnderscoreString, + splitDefinitions, + toComponentSpecification +} from 'ember-data-table/utils/string-specification-helpers'; + +module('Unit | Utils | string-specification-helpers', function () { + test('it strips underscores', function (assert) { + const checks = [ + ['one', 'one'], + ['one_two', 'one two'], + ['one_two_three', 'one two three'], + ['one__two', 'one_two'], + ['one__two_three', 'one_two three'], + ['__hello__', '_hello_'], + ]; + + checks.forEach(([input, output]) => { + assert.strictEqual(deUnderscoreString(input), output); + }); + }); + + test('it splits definitions', function (assert) { + assert.deepEqual(splitDefinitions('hello world'), ['hello', 'world']); + assert.deepEqual(splitDefinitions(null), []); + assert.deepEqual(splitDefinitions(undefined), []); + }); + + test('it creates definition objects', function (assert) { + function convertDefinition(string) { + return toComponentSpecification(string || '', [ + { raw: 'route' }, + 'label', + 'icon', + ]); + } + + const checks = [ + [ + { route: 'hello' }, + { + route: 'hello', + label: null, + icon: null, + rawLabel: null, + rawIcon: null, + }, + ], + [ + 'hello', + { + route: 'hello', + label: null, + icon: null, + rawLabel: null, + rawIcon: null, + }, + ], + [ + { route: 'hello.world', label: 'Hello World' }, + { + route: 'hello.world', + label: 'Hello World', + icon: null, + rawLabel: 'Hello World', + rawIcon: null, + }, + ], + [ + 'hello.world:Hello_World', + { + route: 'hello.world', + label: 'Hello World', + icon: null, + rawLabel: 'Hello_World', + rawIcon: null, + }, + ], + [ + 'hello.world:Hello_World:add-icon-thing', + { + route: 'hello.world', + label: 'Hello World', + icon: 'add-icon-thing', + rawLabel: 'Hello_World', + rawIcon: 'add-icon-thing', + }, + ], + [ + { route: 'hello.world', label: 'Hello World', icon: 'add-icon-thing' }, + { + route: 'hello.world', + label: 'Hello World', + icon: 'add-icon-thing', + rawLabel: 'Hello World', + rawIcon: 'add-icon-thing', + }, + ], + ]; + + checks.forEach(([input, output]) => + assert.deepEqual(convertDefinition(input), output), + ); + }); +}); diff --git a/tests/.jshintrc b/tests/.jshintrc deleted file mode 100644 index d2bd113..0000000 --- a/tests/.jshintrc +++ /dev/null @@ -1,52 +0,0 @@ -{ - "predef": [ - "document", - "window", - "location", - "setTimeout", - "$", - "-Promise", - "define", - "console", - "visit", - "exists", - "fillIn", - "click", - "keyEvent", - "triggerEvent", - "find", - "findWithAssert", - "wait", - "DS", - "andThen", - "currentURL", - "currentPath", - "currentRouteName" - ], - "node": false, - "browser": false, - "boss": true, - "curly": true, - "debug": false, - "devel": false, - "eqeqeq": true, - "evil": true, - "forin": false, - "immed": false, - "laxbreak": false, - "newcap": true, - "noarg": true, - "noempty": false, - "nonew": false, - "nomen": false, - "onevar": false, - "plusplus": false, - "regexp": false, - "undef": true, - "sub": true, - "strict": false, - "white": false, - "eqnull": true, - "esversion": 6, - "unused": true -} diff --git a/tests/dummy/app/components/.gitkeep b/tests/dummy/app/components/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/dummy/app/controllers/.gitkeep b/tests/dummy/app/controllers/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/dummy/app/controllers/application.js b/tests/dummy/app/controllers/application.js deleted file mode 100644 index 075e96d..0000000 --- a/tests/dummy/app/controllers/application.js +++ /dev/null @@ -1,78 +0,0 @@ -import EmberObject from '@ember/object'; -import ArrayProxy from '@ember/array/proxy'; -import Controller from '@ember/controller'; -import DefaultQueryParams from 'ember-data-table/mixins/default-query-params'; - -var ApplicationController = Controller.extend(DefaultQueryParams, { - model: ArrayProxy.create({ - content: [ - EmberObject.create({ - firstName: 'John', - lastName: 'Doe', - age: 20, - created: Date.now(), - modified: Date.now(), - }), - EmberObject.create({ - firstName: 'Jane', - lastName: 'Doe', - age: 25, - created: Date.now(), - modified: Date.now(), - }), - ], - meta: { - count: 63, - pagination: { - first: { - number: 0, - size: 5, - }, - prev: { - number: 1, - size: 5, - }, - self: { - number: 2, - size: 5, - }, - next: { - number: 3, - size: 5, - }, - last: { - number: 12, - size: 5, - }, - }, - }, - }), - page: 2, - size: 5, - sort: 'first-name', - actions: { - test(row) { - console.info( - 'Hi, you reached the test action for row: ' + JSON.stringify(row) - ); - }, - menuTest() { - console.info('Hi, you reached the general menu test action'); - }, - selectionTest(selection, datatable) { - datatable.clearSelection(); - console.info( - 'Hi, you reached the selection test action for selection: ' + - JSON.stringify(selection) - ); - selection.forEach(function (item) { - item.set('age', item.get('age') + 1); - }); - }, - clickRow(row) { - console.info('Custom row click action on item ' + JSON.stringify(row)); - }, - }, -}); - -export default ApplicationController; diff --git a/tests/dummy/app/helpers/.gitkeep b/tests/dummy/app/helpers/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/dummy/app/models/.gitkeep b/tests/dummy/app/models/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/dummy/app/router.js b/tests/dummy/app/router.js deleted file mode 100644 index 64e543a..0000000 --- a/tests/dummy/app/router.js +++ /dev/null @@ -1,9 +0,0 @@ -import EmberRouter from '@ember/routing/router'; -import config from 'dummy/config/environment'; - -export default class Router extends EmberRouter { - location = config.locationType; - rootURL = config.rootURL; -} - -Router.map(function () {}); diff --git a/tests/dummy/app/routes/.gitkeep b/tests/dummy/app/routes/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/dummy/app/templates/application.hbs b/tests/dummy/app/templates/application.hbs deleted file mode 100644 index bf06226..0000000 --- a/tests/dummy/app/templates/application.hbs +++ /dev/null @@ -1,72 +0,0 @@ -{{page-title "Dummy"}} - -
-
-

Ember Data Table demo

-

Simple table

-
Generated table header and body based on given fields
- {{data-table content=model fields="firstName lastName age created modified" sort=sort page=page size=size filter=filter}} - -

Semi-complex table

-
Customized table header and body
- - - - {{th-sortable field="firstName" currentSorting=sort label="First name"}} - {{th-sortable field="lastName" currentSorting=sort label="Last name"}} - Age - {{th-sortable field="created" currentSorting=sort label="Created"}} - Modified - - - {{row.firstName}} - {{row.lastName}} - {{row.age}} - {{row.created}} - {{row.modified}} - - - - -

Complex table

-
Customized table including an action menu on top
- - - - - - - - - - - - - {{th-sortable field="firstName" currentSorting=sort label="First name"}} - {{th-sortable field="lastName" currentSorting=sort label="Last name"}} - Age - {{th-sortable field="created" currentSorting=sort label="Created"}} - Modified - - - - - {{row.firstName}} - {{row.lastName}} - {{row.age}} - {{row.created}} - {{row.modified}} - - - - - -

Internal variables

-
    -
  • Sort: {{sort}}
  • -
  • Page: {{page}}
  • -
  • Size: {{size}}
  • -
  • Filter: {{filter}}
  • -
-
-
diff --git a/tests/dummy/config/ember-cli-update.json b/tests/dummy/config/ember-cli-update.json deleted file mode 100644 index a911ac0..0000000 --- a/tests/dummy/config/ember-cli-update.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "schemaVersion": "1.0.0", - "packages": [ - { - "name": "ember-cli", - "version": "3.28.5", - "blueprints": [ - { - "name": "addon", - "outputRepo": "https://github.com/ember-cli/ember-addon-output", - "codemodsSource": "ember-addon-codemods-manifest@1", - "isBaseBlueprint": true, - "options": [] - } - ] - } - ] -} diff --git a/tests/dummy/config/targets.js b/tests/dummy/config/targets.js deleted file mode 100644 index 3cd797a..0000000 --- a/tests/dummy/config/targets.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; - -const browsers = [ - 'last 1 Chrome versions', - 'last 1 Firefox versions', - 'last 1 Safari versions', -]; - -// Ember's browser support policy is changing, and IE11 support will end in -// v4.0 onwards. -// -// See https://deprecations.emberjs.com/v3.x#toc_3-0-browser-support-policy -// -// If you need IE11 support on a version of Ember that still offers support -// for it, uncomment the code block below. -// -// const isCI = Boolean(process.env.CI); -// const isProduction = process.env.EMBER_ENV === 'production'; -// -// if (isCI || isProduction) { -// browsers.push('ie 11'); -// } - -module.exports = { - browsers, -}; diff --git a/tests/dummy/public/robots.txt b/tests/dummy/public/robots.txt deleted file mode 100644 index f591645..0000000 --- a/tests/dummy/public/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -# http://www.robotstxt.org -User-agent: * -Disallow: diff --git a/tests/helpers/.gitkeep b/tests/helpers/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/helpers/module-for-acceptance.js b/tests/helpers/module-for-acceptance.js deleted file mode 100644 index 40784cd..0000000 --- a/tests/helpers/module-for-acceptance.js +++ /dev/null @@ -1,22 +0,0 @@ -import { resolve } from 'rsvp'; -import { module } from 'qunit'; -import startApp from '../helpers/start-app'; -import destroyApp from '../helpers/destroy-app'; - -export default function (name, options = {}) { - module(name, { - beforeEach() { - this.application = startApp(); - - if (options.beforeEach) { - return options.beforeEach.apply(this, arguments); - } - }, - - afterEach() { - let afterEach = - options.afterEach && options.afterEach.apply(this, arguments); - return resolve(afterEach).then(() => destroyApp(this.application)); - }, - }); -} diff --git a/tests/helpers/start-app.js b/tests/helpers/start-app.js deleted file mode 100644 index d359b69..0000000 --- a/tests/helpers/start-app.js +++ /dev/null @@ -1,17 +0,0 @@ -import { run } from '@ember/runloop'; -import { merge } from '@ember/polyfills'; -import Application from '../../app'; -import config from '../../config/environment'; - -export default function startApp(attrs) { - let attributes = merge({}, config.APP); - attributes.autoboot = true; - attributes = merge(attributes, attrs); // use defaults, but you can override; - - return run(() => { - let application = Application.create(attributes); - application.setupForTesting(); - application.injectTestHelpers(); - return application; - }); -} diff --git a/tests/integration/.gitkeep b/tests/integration/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/integration/components/data-table-content-body-test.no-js b/tests/integration/components/data-table-content-body-test.no-js deleted file mode 100644 index e02ae58..0000000 --- a/tests/integration/components/data-table-content-body-test.no-js +++ /dev/null @@ -1,192 +0,0 @@ -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; - -module('Integration | Component | data table content body', function (hooks) { - setupRenderingTest(hooks); - - test('it renders', async function (assert) { - // Set any properties with this.set('myProperty', 'value'); - // Handle any actions with this.on('myAction', function(val) { ... }); - await render(hbs`{{data-table/data-table-content-body}}`); - assert.dom('tbody').exists({ count: 1 }); - }); - - test('display rows', async function (assert) { - this.set('content', [ - { firstName: 'John', lastName: 'Doe', age: 20 }, - { firstName: 'Jane', lastName: 'Doe', age: 21 }, - ]); - this.set('dataTable', {}); - this.set('dataTable.parsedFields', ['firstName', 'lastName', 'age']); - this.set('dataTable.selection', []); - - await render( - hbs`{{data-table/data-table-content-body content=content dataTable=dataTable}}` - ); - - assert.dom('tr').exists({ count: 2 }, 'displays 2 rows'); - assert.equal(this.$('tr:first td').length, 3, 'displays 3 columns'); - assert.equal( - this.$('tr:first td:first').text().trim(), - 'John', - 'displays firstName is first column' - ); - assert.equal( - this.$('tr:first td:nth-child(2)').text().trim(), - 'Doe', - 'displays lastName in second column' - ); - assert.equal( - this.$('tr:first td:nth-child(3)').text().trim(), - '20', - 'displays age in third column' - ); - }); - - test('add checkboxes for selection if enabled', async function (assert) { - const john = { firstName: 'John', lastName: 'Doe', age: 20 }; - const jane = { firstName: 'Jane', lastName: 'Doe', age: 21 }; - const jeff = { firstName: 'Jeff', lastName: 'Doe', age: 22 }; - this.set('content', [john, jane, jeff]); - this.set('dataTable', {}); - this.set('dataTable.parsedFields', ['firstName', 'lastName', 'age']); - this.set('dataTable.selection', [jane]); - - await render( - hbs`{{data-table/data-table-content-body content=content dataTable=dataTable enableSelection=true}}` - ); - - assert.equal(this.$('tr:first td').length, 4, 'displays 4 columns'); - assert.dom('tr.selected').exists({ count: 1 }, 'displays 1 selected row'); - assert - .dom('tr input[type="checkbox"]') - .exists({ count: 3 }, 'displays a checkbox on each row'); - assert - .dom('tr input[type="checkbox"]:checked') - .isChecked('displays 1 checked checkbox'); - }); - - test('toggles selection if checkbox is clicked', async function (assert) { - const john = { firstName: 'John', lastName: 'Doe', age: 20 }; - const jane = { firstName: 'Jane', lastName: 'Doe', age: 21 }; - const jeff = { firstName: 'Jeff', lastName: 'Doe', age: 22 }; - this.set('content', [john, jane, jeff]); - this.set('dataTable', {}); - this.set('dataTable.parsedFields', ['firstName', 'lastName', 'age']); - this.set('dataTable.selection', [jane]); - this.set('dataTable.addItemToSelection', () => - this.set('dataTable.selection', [john, jane]) - ); // mock function - this.set('dataTable.removeItemFromSelection', function () {}); // mock function - - await render( - hbs`{{data-table/data-table-content-body content=content dataTable=dataTable enableSelection=true}}` - ); - - assert - .dom('tr input[type="checkbox"]:checked') - .isChecked('displays 1 checked checkbox before selecting a row'); - this.$('tr:first input[type="checkbox"]').click(); - assert - .dom('tr input[type="checkbox"]:checked') - .isChecked('displays 2 checked checkboxes after selecting a row'); - }); - - test('add line numbers if enabled', async function (assert) { - const john = { firstName: 'John', lastName: 'Doe', age: 20 }; - const jane = { firstName: 'Jane', lastName: 'Doe', age: 21 }; - const jeff = { firstName: 'Jeff', lastName: 'Doe', age: 22 }; - this.set('content', [john, jane, jeff]); - this.set('dataTable', {}); - this.set('dataTable.parsedFields', ['firstName', 'lastName', 'age']); - this.set('dataTable.selection', []); - - await render( - hbs`{{data-table/data-table-content-body content=content dataTable=dataTable enableLineNumbers=true}}` - ); - - assert.equal(this.$('tr:first td').length, 4, 'displays 4 columns'); - assert.equal( - this.$('tr:first td:first').text().trim(), - '1', - 'displays offset 1 on the first row' - ); - assert.equal( - this.$('tr:nth-child(2) td:first').text().trim(), - '2', - 'displays offset 2 on the second row' - ); - assert.equal( - this.$('tr:nth-child(3) td:first').text().trim(), - '3', - 'displays offset 3 on the third row' - ); - - this.set('dataTable.page', 2); - this.set('dataTable.size', 5); - await render( - hbs`{{data-table/data-table-content-body content=content dataTable=dataTable enableLineNumbers=true}}` - ); - - assert.equal( - this.$('tr:first td').length, - 4, - 'displays 4 columns on page 3' - ); - assert.equal( - this.$('tr:first td:first').text().trim(), - '11', - 'displays offset 11 on the first row on page 3' - ); - assert.equal( - this.$('tr:nth-child(2) td:first').text().trim(), - '12', - 'displays offset 12 on the second row on page 3' - ); - assert.equal( - this.$('tr:nth-child(3) td:first').text().trim(), - '13', - 'displays offset 13 on the third row of page 3' - ); - }); - - test('displays no data message if there is no data', async function (assert) { - // Set any properties with this.set('myProperty', 'value'); - // Handle any actions with this.on('myAction', function(val) { ... }); - this.set('noDataMessage', 'No data'); - this.set('dataTable', {}); - this.set('dataTable.parsedFields', ['firstName', 'lastName', 'age']); - this.set('dataTable.selection', []); - - await render( - hbs`{{data-table/data-table-content-body noDataMessage=noDataMessage dataTable=dataTable}}` - ); - assert - .dom('td.no-data-message') - .exists({ count: 1 }, 'displays a no data message if no content'); - assert - .dom('td.no-data-message') - .hasText('No data', 'displays message "No data" if no content'); - - this.set('content', []); - await render( - hbs`{{data-table/data-table-content-body content=content noDataMessage=noDataMessage dataTable=dataTable}}` - ); - assert - .dom('td.no-data-message') - .exists({ count: 1 }, 'displays a no data message if empty content'); - assert - .dom('td.no-data-message') - .hasText('No data', 'displays message "No data" if empty content'); - - this.set('content', ['foo', 'bar']); - await render( - hbs`{{data-table/data-table-content-body content=content noDataMessage=noDataMessage dataTable=dataTable}}` - ); - assert - .dom('td.no-data-message') - .doesNotExist('displays no message when there is content'); - }); -}); diff --git a/tests/integration/components/data-table-content-header-test.no-js b/tests/integration/components/data-table-content-header-test.no-js deleted file mode 100644 index 6ff2ee3..0000000 --- a/tests/integration/components/data-table-content-header-test.no-js +++ /dev/null @@ -1,86 +0,0 @@ -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; - -module('Integration | Component | data table content header', function (hooks) { - setupRenderingTest(hooks); - - test('it renders', async function (assert) { - // Set any properties with this.set('myProperty', 'value'); - // Handle any actions with this.on('myAction', function(val) { ... }); - - await render(hbs`{{data-table-content-header}}`); - assert.dom('thead').exists({ count: 1 }); - - assert.dom('*').hasText(''); - - // Template block usage: - await render(hbs` - {{#data-table-content-header}} - template block text - {{/data-table-content-header}} - `); - - assert.dom('*').hasText('template block text'); - }); - - test('display column headers', async function (assert) { - this.set('dataTable', {}); - this.set('dataTable.parsedFields', ['firstName', 'lastName', 'age']); - - await render(hbs`{{data-table-content-header dataTable=dataTable}}`); - - assert.dom('tr').exists({ count: 1 }, 'displays 1 header row'); - assert.equal(this.$('tr:first th').length, 3, 'displays 3 column headers'); - assert.equal( - this.$('tr:first th:first').text().trim(), - 'firstName', - 'displays firstName as first header' - ); - assert.equal( - this.$('tr:first th:nth-child(2)').text().trim(), - 'lastName', - 'displays lastName as second column header' - ); - assert.equal( - this.$('tr:first th:nth-child(3)').text().trim(), - 'age', - 'displays age as third column header' - ); - }); - - test('add selection column header if enabled', async function (assert) { - this.set('dataTable', {}); - this.set('dataTable.parsedFields', ['firstName', 'lastName', 'age']); - - await render( - hbs`{{data-table-content-header dataTable=dataTable enableSelection=true}}` - ); - - assert.dom('tr').exists({ count: 1 }, 'displays 1 header row'); - assert.equal(this.$('tr:first th').length, 4, 'displays 4 column headers'); - assert.equal( - this.$('tr:first th:first').text().trim(), - '', - 'displays selection as first header' - ); - }); - - test('add line number column header if enabled', async function (assert) { - this.set('dataTable', {}); - this.set('dataTable.parsedFields', ['firstName', 'lastName', 'age']); - - await render( - hbs`{{data-table-content-header dataTable=dataTable enableLineNumbers=true}}` - ); - - assert.dom('tr').exists({ count: 1 }, 'displays 1 header row'); - assert.equal(this.$('tr:first th').length, 4, 'displays 4 column headers'); - assert.equal( - this.$('tr:first th:first').text().trim(), - '', - 'displays line number as first header' - ); - }); -}); diff --git a/tests/integration/components/data-table-content-test.no-js b/tests/integration/components/data-table-content-test.no-js deleted file mode 100644 index a529f05..0000000 --- a/tests/integration/components/data-table-content-test.no-js +++ /dev/null @@ -1,29 +0,0 @@ -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; - -module('Integration | Component | data table content', function (hooks) { - setupRenderingTest(hooks); - - test('it renders', async function (assert) { - // Set any properties with this.set('myProperty', 'value'); - // Handle any actions with this.on('myAction', function(val) { ... }); - - await render(hbs`{{data-table-content}}`); - assert - .dom('table.data-table') - .exists({ count: 1 }, 'displays 1 data table'); - - assert.dom('*').hasText(''); - - // Template block usage: - await render(hbs` - {{#data-table-content}} - template block text - {{/data-table-content}} - `); - - assert.dom('*').hasText('template block text'); - }); -}); diff --git a/tests/integration/components/data-table-menu-general-test.no-js b/tests/integration/components/data-table-menu-general-test.no-js deleted file mode 100644 index 60c9fad..0000000 --- a/tests/integration/components/data-table-menu-general-test.no-js +++ /dev/null @@ -1,41 +0,0 @@ -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; - -module('Integration | Component | data table menu general', function (hooks) { - setupRenderingTest(hooks); - - test('it renders', async function (assert) { - // Set any properties with this.set('myProperty', 'value'); - // Handle any actions with this.on('myAction', function(val) { ... }); - - await render(hbs`{{data-table-menu-general}}`); - - assert.dom('*').hasText(''); - }); - - test('it renders block only if data table selection is empty', async function (assert) { - // Set any properties with this.set('myProperty', 'value'); - // Handle any actions with this.on('myAction', function(val) { ... }); - - this.set('data-table', { selectionIsEmpty: true }); - // Template block usage: - await render(hbs` - {{#data-table-menu-general dataTable=dataTable}} - template block text - {{/data-table-menu-general}} - `); - assert.dom('*').hasText('template block text'); - - this.set('dataTable', { selectionIsEmpty: false }); - // Template block usage: - await render(hbs` - {{#data-table-menu-general dataTable=dataTable}} - template block text - {{/data-table-menu-general}} - `); - - assert.dom('*').hasText(''); - }); -}); diff --git a/tests/integration/components/data-table-menu-selected-test.no-js b/tests/integration/components/data-table-menu-selected-test.no-js deleted file mode 100644 index 5ab4e5d..0000000 --- a/tests/integration/components/data-table-menu-selected-test.no-js +++ /dev/null @@ -1,62 +0,0 @@ -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render, click } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; - -module('Integration | Component | data table menu selected', function (hooks) { - setupRenderingTest(hooks); - - test('it renders block only if data table selection is not empty', async function (assert) { - this.set('dataTable', { selectionIsEmpty: true }); - // Template block usage: - await render(hbs` - {{#data-table-menu-selected dataTable=dataTable}} - template block text - {{/data-table-menu-selected}} - `); - assert.dom('*').hasText(''); - }); - - test('it renders selection count', async function (assert) { - this.set('dataTable', { selectionIsEmpty: false, selection: ['foo'] }); - // Template block usage: - await render(hbs` - {{#data-table-menu-selected dataTable=dataTable}} - template block text - {{/data-table-menu-selected}} - `); - - assert.dom('span.item-count').hasText('1 item(s) selected', 'item count 1'); - - this.set('dataTable', { - selectionIsEmpty: false, - selection: ['foo', 'bar'], - }); - // Template block usage: - await render(hbs` - {{#data-table-menu-selected dataTable=dataTable}} - template block text - {{/data-table-menu-selected}} - `); - - assert.dom('span.item-count').hasText('2 item(s) selected', 'item count 2'); - }); - - test('calls clearSelection on cancel button click', async function (assert) { - assert.expect(2); // 2 asserts in this test - - this.set('dataTable', { selectionIsEmpty: false, selection: ['foo'] }); - this.set('dataTable.clearSelection', function () { - assert.ok(true, 'dataTable.clearSelection gets called'); - }); - // Template block usage: - await render(hbs` - {{#data-table-menu-selected dataTable=dataTable}} - template block text - {{/data-table-menu-selected}} - `); - - assert.dom('button').hasText('Cancel', 'renders a cancel button'); - await click('button'); - }); -}); diff --git a/tests/integration/components/data-table-menu-test.no-js b/tests/integration/components/data-table-menu-test.no-js deleted file mode 100644 index 2219941..0000000 --- a/tests/integration/components/data-table-menu-test.no-js +++ /dev/null @@ -1,26 +0,0 @@ -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; - -module('Integration | Component | data table menu', function (hooks) { - setupRenderingTest(hooks); - - test('it renders', async function (assert) { - // Set any properties with this.set('myProperty', 'value'); - // Handle any actions with this.on('myAction', function(val) { ... }); - - await render(hbs`{{data-table-menu}}`); - - assert.dom('*').hasText(''); - - // Template block usage: - await render(hbs` - {{#data-table-menu}} - template block text - {{/data-table-menu}} - `); - - assert.dom('*').hasText('template block text'); - }); -}); diff --git a/tests/integration/components/data-table-test.no-js b/tests/integration/components/data-table-test.no-js deleted file mode 100644 index 71a4155..0000000 --- a/tests/integration/components/data-table-test.no-js +++ /dev/null @@ -1,25 +0,0 @@ -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; - -module('Integration | Component | data table', function (hooks) { - setupRenderingTest(hooks); - - test('it renders', async function (assert) { - // Set any properties with this.set('myProperty', 'value'); - // Handle any actions with this.on('myAction', function(val) { ... }); - - this.set('content', []); - this.set('content.meta', { - pagination: { - first: { number: 1 }, - last: { number: 10 }, - }, - }); - - await render(hbs`{{data-table content=content enableSizes=false}}`); - - assert.dom('.data-table-content').exists({ count: 1 }); - }); -}); diff --git a/tests/integration/components/data-table/data-cell-test.js b/tests/integration/components/data-table/data-cell-test.js deleted file mode 100644 index ff4306f..0000000 --- a/tests/integration/components/data-table/data-cell-test.js +++ /dev/null @@ -1,26 +0,0 @@ -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; - -module('Integration | Component | data-table/data-cell', function (hooks) { - setupRenderingTest(hooks); - - test('it renders', async function (assert) { - // Set any properties with this.set('myProperty', 'value'); - // Handle any actions with this.set('myAction', function(val) { ... }); - - await render(hbs``); - - assert.dom(this.element).hasText(''); - - // Template block usage: - await render(hbs` - - template block text - - `); - - assert.dom(this.element).hasText('template block text'); - }); -}); diff --git a/tests/integration/components/data-table/data-cells-test.js b/tests/integration/components/data-table/data-cells-test.js deleted file mode 100644 index bced827..0000000 --- a/tests/integration/components/data-table/data-cells-test.js +++ /dev/null @@ -1,26 +0,0 @@ -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; - -module('Integration | Component | data-table/data-cells', function(hooks) { - setupRenderingTest(hooks); - - test('it renders', async function(assert) { - // Set any properties with this.set('myProperty', 'value'); - // Handle any actions with this.set('myAction', function(val) { ... }); - - await render(hbs``); - - assert.dom(this.element).hasText(''); - - // Template block usage: - await render(hbs` - - template block text - - `); - - assert.dom(this.element).hasText('template block text'); - }); -}); diff --git a/tests/integration/components/data-table/row-test.js b/tests/integration/components/data-table/row-test.js deleted file mode 100644 index 4ff68d1..0000000 --- a/tests/integration/components/data-table/row-test.js +++ /dev/null @@ -1,29 +0,0 @@ -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; - -module('Integration | Component | data-table/row', function (hooks) { - setupRenderingTest(hooks); - - test('it renders', async function (assert) { - // Set any properties with this.set('myProperty', 'value'); - // Handle any actions with this.set('myAction', function(val) { ... }); - - this.set('onClickRow', () => undefined ); - this.set('toggleSelected', () => undefined ); - - await render(hbs``); - - assert.dom(this.element).hasText(''); - - // Template block usage: - await render(hbs` - - template block text - - `); - - assert.dom(this.element).hasText('template block text'); - }); -}); diff --git a/tests/integration/components/default-data-table-content-body-test.no-js b/tests/integration/components/default-data-table-content-body-test.no-js deleted file mode 100644 index 5966f01..0000000 --- a/tests/integration/components/default-data-table-content-body-test.no-js +++ /dev/null @@ -1,34 +0,0 @@ -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; - -module( - 'Integration | Component | default data table content body', - function (hooks) { - setupRenderingTest(hooks); - - test('it renders', async function (assert) { - // Set any properties with this.set('myProperty', 'value'); - // Handle any actions with this.on('myAction', function(val) { ... }); - - this.set('dataTable', { - parsedFields: ['firstName', 'lastName', 'age'], - }); - - await render( - hbs`{{default-data-table-content-body dataTable=dataTable}}` - ); - - assert.dom().hasText(''); - - // Template block usage: - await render(hbs` - - template block text - - `); - assert.dom().hasText('template block text'); - }); - } -); diff --git a/tests/integration/components/number-pagination-test.no-js b/tests/integration/components/number-pagination-test.no-js deleted file mode 100644 index 11cbcda..0000000 --- a/tests/integration/components/number-pagination-test.no-js +++ /dev/null @@ -1,22 +0,0 @@ -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; - -module('Integration | Component | number pagination', function (hooks) { - setupRenderingTest(hooks); - - test('it renders', async function (assert) { - // Set any properties with this.set('myProperty', 'value'); - // Handle any actions with this.on('myAction', function(val) { ... }); - - this.set('page', 0); - this.set('links', { - first: { number: 1 }, - last: { number: 10 }, - }); - await render(hbs`{{number-pagination page=page links=links}}`); - - assert.dom('.data-table-pagination').exists({ count: 1 }); - }); -}); diff --git a/tests/integration/components/raw-data-table-test.no-js b/tests/integration/components/raw-data-table-test.no-js deleted file mode 100644 index 6c72a99..0000000 --- a/tests/integration/components/raw-data-table-test.no-js +++ /dev/null @@ -1,26 +0,0 @@ -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; - -module('Integration | Component | raw-data-table', function (hooks) { - setupRenderingTest(hooks); - - test('it renders', async function (assert) { - // Set any properties with this.set('myProperty', 'value'); - // Handle any actions with this.set('myAction', function(val) { ... }); - - await render(hbs``); - - assert.dom(this.element).hasText(''); - - // Template block usage: - await render(hbs` - - template block text - - `); - - assert.dom(this.element).hasText('template block text'); - }); -}); diff --git a/tests/integration/components/text-search-test.no-js b/tests/integration/components/text-search-test.no-js deleted file mode 100644 index 75130f6..0000000 --- a/tests/integration/components/text-search-test.no-js +++ /dev/null @@ -1,17 +0,0 @@ -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; - -module('Integration | Component | text search', function (hooks) { - setupRenderingTest(hooks); - - test('it renders', async function (assert) { - // Set any properties with this.set('myProperty', 'value'); - // Handle any actions with this.on('myAction', function(val) { ... }); - - await render(hbs`{{text-search}}`); - - assert.dom('.data-table-search').exists({ count: 1 }); - }); -}); diff --git a/tests/integration/components/th-sortable-test.no-js b/tests/integration/components/th-sortable-test.no-js deleted file mode 100644 index 9ac87bb..0000000 --- a/tests/integration/components/th-sortable-test.no-js +++ /dev/null @@ -1,17 +0,0 @@ -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; - -module('Integration | Component | th sortable', function (hooks) { - setupRenderingTest(hooks); - - test('it renders', async function (assert) { - // Set any properties with this.set('myProperty', 'value'); - // Handle any actions with this.on('myAction', function(val) { ... }); - - await render(hbs`{{th-sortable field='title'}}`); - - assert.dom('.sortable').exists({ count: 1 }); - }); -}); diff --git a/tests/unit/.gitkeep b/tests/unit/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/unit/components/data-table/data-table-content-test.js b/tests/unit/components/data-table/data-table-content-test.js deleted file mode 100644 index 99cf300..0000000 --- a/tests/unit/components/data-table/data-table-content-test.js +++ /dev/null @@ -1,58 +0,0 @@ -import { module, test } from 'qunit'; -import { deUnderscoreString, splitDefinitions, toComponentSpecifications } from 'ember-data-table/utils/string-specification-helpers'; - -function convertDefinition(string) { - return toComponentSpecifications(string || "", [{ raw: "route" }, "label", "icon"]); -} - -module('Unit | Component | data-table-content', function() { - test('it strips underscores', function(assert) { - const checks = [["one", "one"], - ["one_two", "one two"], - ["one_two_three", "one two three"], - ["one__two", "one_two"], - ["one__two_three", "one_two three"], - ["__hello__", "_hello_"]]; - - assert.expect(checks.length); - - checks.forEach(([input, output]) => { - assert.strictEqual(deUnderscoreString(input), output); - }); - }); - - test('it splits definitions', function(assert) { - assert.deepEqual(splitDefinitions("hello world"), ["hello", "world"]); - assert.deepEqual(splitDefinitions(null), []); - assert.deepEqual(splitDefinitions(undefined), []); - }); - - test('it creates definition objects', function(assert) { - const checks = [ - ["hello", { - route: "hello", - label: null, - icon: null, - rawLabel: null, - rawIcon: null - }], - ["hello.world:Hello_World", { - route: "hello.world", - label: "Hello World", - icon: null, - rawLabel: "Hello_World", - rawIcon: null - }], - ["hello.world:Hello_World:add-icon-thing", { - route: "hello.world", - label: "Hello World", - icon: "add-icon-thing", - rawLabel: "Hello_World", - rawIcon: "add-icon-thing" - }]]; - - assert.expect(checks.length); - - checks.forEach(([input, output]) => assert.deepEqual(convertDefinition(input), output)); - }); -}); diff --git a/tests/unit/utils/string-specification-helpers-test.js b/tests/unit/utils/string-specification-helpers-test.js deleted file mode 100644 index e158f30..0000000 --- a/tests/unit/utils/string-specification-helpers-test.js +++ /dev/null @@ -1,10 +0,0 @@ -import stringSpecificationHelpers from 'dummy/utils/string-specification-helpers'; -import { module, test } from 'qunit'; - -module('Unit | Utility | string-specification-helpers', function () { - // TODO: Replace this with your real tests. - test('it works', function (assert) { - let result = stringSpecificationHelpers(); - assert.ok(result); - }); -}); diff --git a/vendor/.gitkeep b/vendor/.gitkeep deleted file mode 100644 index e69de29..0000000