From 29b05f93d47c94751372c49c3d91afd00510f846 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Fri, 27 Feb 2026 12:23:32 +0800 Subject: [PATCH 1/2] Added tabbed user type sections to users management --- addon/controllers/users.js | 39 ++++++++ addon/controllers/users/customers.js | 3 + addon/controllers/users/drivers.js | 3 + addon/controllers/users/index.js | 98 +++++++++++-------- addon/routes.js | 6 +- addon/routes/users/customers.js | 22 +++++ addon/routes/users/drivers.js | 22 +++++ addon/routes/users/index.js | 4 +- addon/templates/users.hbs | 20 +++- addon/templates/users/customers.hbs | 14 +++ addon/templates/users/drivers.hbs | 14 +++ addon/templates/users/index.hbs | 27 ----- app/controllers/users.js | 1 + app/controllers/users/customers.js | 1 + app/controllers/users/drivers.js | 1 + app/routes/users/customers.js | 1 + app/routes/users/drivers.js | 1 + app/templates/users/customers.js | 1 + app/templates/users/drivers.js | 1 + package.json | 6 +- pnpm-lock.yaml | 49 +++++++--- tests/unit/controllers/users-test.js | 12 +++ .../unit/controllers/users/customers-test.js | 12 +++ tests/unit/controllers/users/drivers-test.js | 12 +++ tests/unit/routes/users/customers-test.js | 11 +++ tests/unit/routes/users/drivers-test.js | 11 +++ 26 files changed, 307 insertions(+), 85 deletions(-) create mode 100644 addon/controllers/users.js create mode 100644 addon/controllers/users/customers.js create mode 100644 addon/controllers/users/drivers.js create mode 100644 addon/routes/users/customers.js create mode 100644 addon/routes/users/drivers.js create mode 100644 addon/templates/users/customers.hbs create mode 100644 addon/templates/users/drivers.hbs create mode 100644 app/controllers/users.js create mode 100644 app/controllers/users/customers.js create mode 100644 app/controllers/users/drivers.js create mode 100644 app/routes/users/customers.js create mode 100644 app/routes/users/drivers.js create mode 100644 app/templates/users/customers.js create mode 100644 app/templates/users/drivers.js create mode 100644 tests/unit/controllers/users-test.js create mode 100644 tests/unit/controllers/users/customers-test.js create mode 100644 tests/unit/controllers/users/drivers-test.js create mode 100644 tests/unit/routes/users/customers-test.js create mode 100644 tests/unit/routes/users/drivers-test.js diff --git a/addon/controllers/users.js b/addon/controllers/users.js new file mode 100644 index 0000000..e4d2877 --- /dev/null +++ b/addon/controllers/users.js @@ -0,0 +1,39 @@ +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; +import { getOwner } from '@ember/application'; + +export default class UsersController extends Controller { + @service hostRouter; + + get tabs() { + return [ + { + route: 'users.index', + label: 'Users', + }, + { + route: 'users.drivers', + label: 'Drivers', + }, + { + route: 'users.customers', + label: 'Customers', + }, + ]; + } + + get childController() { + const owner = getOwner(this); + const fullRouteName = this.hostRouter.currentRouteName; + + // strip engine mount prefix once + const mount = owner.mountPoint; + let local = fullRouteName; + if (mount && local.startsWith(mount + '.')) { + local = local.slice(mount.length + 1); + } + + const childController = owner.lookup(`controller:${local}`); + return childController; + } +} diff --git a/addon/controllers/users/customers.js b/addon/controllers/users/customers.js new file mode 100644 index 0000000..e46e5d6 --- /dev/null +++ b/addon/controllers/users/customers.js @@ -0,0 +1,3 @@ +import UsersIndexController from './index'; + +export default class UsersCustomersController extends UsersIndexController {} diff --git a/addon/controllers/users/drivers.js b/addon/controllers/users/drivers.js new file mode 100644 index 0000000..70f1afa --- /dev/null +++ b/addon/controllers/users/drivers.js @@ -0,0 +1,3 @@ +import UsersIndexController from './index'; + +export default class UsersDriversController extends UsersIndexController {} diff --git a/addon/controllers/users/index.js b/addon/controllers/users/index.js index 6b4a266..42fafd0 100644 --- a/addon/controllers/users/index.js +++ b/addon/controllers/users/index.js @@ -16,40 +16,55 @@ export default class UsersIndexController extends Controller { @service fetch; @service abilities; @service filters; + @service tableContext; - /** - * Queryable parameters for this controller's model - * - * @var {Array} - */ - queryParams = ['page', 'limit', 'sort', 'query', 'type', 'created_by', 'updated_by', 'status', 'role', 'name']; + /** action buttons */ + get actionButtons() { + return [ + { + icon: 'refresh', + onClick: () => this.hostRouter.refresh(), + helpText: this.intl.t('common.refresh'), + }, + { + text: this.intl.t('common.new'), + type: 'primary', + icon: 'plus', + permission: 'iam create user', + onClick: this.createUser, + }, + { + text: this.intl.t('common.export'), + icon: 'long-arrow-up', + iconClass: 'rotate-icon-45', + wrapperClass: 'hidden md:flex', + permission: 'iam export user', + onClick: this.exportUsers, + }, + ]; + } - /** - * The current page of data being viewed - * - * @var {Integer} - */ - @tracked page = 1; + /** bulk actions */ + get bulkActions() { + const selected = this.tableContext.getSelectedRows(); - /** - * The maximum number of items to show per page - * - * @var {Integer} - */ - @tracked limit; + return [ + { + label: this.intl.t('common.delete-selected-count', { count: selected.length }), + class: 'text-red-500', + fn: this.bulkDeleteUsers, + }, + ]; + } - /** - * The search query param - * - * @var {Integer} - */ + queryParams = ['page', 'limit', 'sort', 'query', 'type', 'created_by', 'updated_by', 'status', 'role', 'name', 'phone', 'email']; + @tracked page = 1; + @tracked limit; @tracked query; - - /** - * The param to sort the data on, the param with prepended `-` is descending - * - * @var {String} - */ + @tracked name; + @tracked phone; + @tracked email; + @tracked role; @tracked sort = '-created_at'; /** @@ -59,9 +74,9 @@ export default class UsersIndexController extends Controller { */ @tracked columns = [ { + sticky: true, label: this.intl.t('iam.common.name'), valuePath: 'name', - width: '160px', cellComponent: 'table/cell/user-name', permission: 'iam view user', mediaPath: 'avatar_url', @@ -72,24 +87,28 @@ export default class UsersIndexController extends Controller { filterComponent: 'filter/string', }, { + sticky: true, label: this.intl.t('iam.common.email'), valuePath: 'email', cellComponent: 'click-to-copy', - sortable: false, - width: '12%', + resizable: true, + sortable: true, + filterable: true, + filterComponent: 'filter/string', }, { label: this.intl.t('iam.common.phone'), valuePath: 'phone', cellComponent: 'click-to-copy', - sortable: false, - width: '12%', + resizable: true, + sortable: true, + filterable: true, + filterComponent: 'filter/string', }, { label: this.intl.t('iam.common.role'), valuePath: 'role.name', sortable: false, - width: '10%', filterable: true, filterComponent: 'filter/model', filterComponentPlaceholder: 'Select role', @@ -100,17 +119,15 @@ export default class UsersIndexController extends Controller { label: this.intl.t('iam.common.status'), valuePath: 'session_status', sortable: false, - width: '12%', cellComponent: 'table/cell/status', filterable: true, filterComponent: 'filter/select', filterParam: 'status', - filterOptions: ['pending', 'active'], + filterOptions: ['pending', 'active', 'inactive'], }, { label: this.intl.t('iam.users.index.last-login'), valuePath: 'lastLogin', - width: '130px', resizable: true, sortable: false, filterable: false, @@ -120,7 +137,6 @@ export default class UsersIndexController extends Controller { label: this.intl.t('iam.users.index.created-at'), valuePath: 'createdAt', sortParam: 'created_at', - width: '140px', resizable: true, sortable: false, filterable: false, @@ -130,7 +146,6 @@ export default class UsersIndexController extends Controller { label: this.intl.t('iam.users.index.updated-at'), valuePath: 'updatedAt', sortParam: 'updated_at', - width: '130px', resizable: true, hidden: true, sortable: false, @@ -146,7 +161,8 @@ export default class UsersIndexController extends Controller { ddMenuLabel: this.intl.t('iam.users.index.user-actions'), cellClassNames: 'overflow-visible', wrapperClass: 'flex items-center justify-end mx-2', - width: '10%', + sticky: 'right', + width: 60, actions: [ { label: this.intl.t('iam.users.index.edit-user'), diff --git a/addon/routes.js b/addon/routes.js index ef1072f..2b1b75c 100644 --- a/addon/routes.js +++ b/addon/routes.js @@ -2,7 +2,11 @@ import buildRoutes from 'ember-engines/routes'; export default buildRoutes(function () { this.route('home', { path: '/' }, function () {}); - this.route('users', function () {}); + this.route('users', function () { + this.route('index', { path: '/' }); + this.route('drivers'); + this.route('customers'); + }); this.route('groups', function () {}); this.route('roles', function () {}); this.route('policies', function () {}); diff --git a/addon/routes/users/customers.js b/addon/routes/users/customers.js new file mode 100644 index 0000000..4287644 --- /dev/null +++ b/addon/routes/users/customers.js @@ -0,0 +1,22 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; + +export default class UsersCustomersRoute extends Route { + @service store; + + queryParams = { + page: { refreshModel: true }, + limit: { refreshModel: true }, + sort: { refreshModel: true }, + query: { refreshModel: true }, + status: { refreshModel: true }, + role: { refreshModel: true }, + name: { refreshModel: true }, + phone: { refreshModel: true }, + email: { refreshModel: true }, + }; + + model(params) { + return this.store.query('user', { ...params, is_customer: 1 }); + } +} diff --git a/addon/routes/users/drivers.js b/addon/routes/users/drivers.js new file mode 100644 index 0000000..fba268f --- /dev/null +++ b/addon/routes/users/drivers.js @@ -0,0 +1,22 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; + +export default class UsersDriversRoute extends Route { + @service store; + + queryParams = { + page: { refreshModel: true }, + limit: { refreshModel: true }, + sort: { refreshModel: true }, + query: { refreshModel: true }, + status: { refreshModel: true }, + role: { refreshModel: true }, + name: { refreshModel: true }, + phone: { refreshModel: true }, + email: { refreshModel: true }, + }; + + model(params) { + return this.store.query('user', { ...params, is_driver: 1 }); + } +} diff --git a/addon/routes/users/index.js b/addon/routes/users/index.js index 1d903d4..9ef6f54 100644 --- a/addon/routes/users/index.js +++ b/addon/routes/users/index.js @@ -12,9 +12,11 @@ export default class UsersIndexRoute extends Route { status: { refreshModel: true }, role: { refreshModel: true }, name: { refreshModel: true }, + phone: { refreshModel: true }, + email: { refreshModel: true }, }; model(params) { - return this.store.query('user', params); + return this.store.query('user', { ...params, is_user: 1 }); } } diff --git a/addon/templates/users.hbs b/addon/templates/users.hbs index e2147ca..ad4abb4 100644 --- a/addon/templates/users.hbs +++ b/addon/templates/users.hbs @@ -1 +1,19 @@ -{{outlet}} \ No newline at end of file + + <:actions> +
+ +
+ + <:default> + {{outlet}} + +
\ No newline at end of file diff --git a/addon/templates/users/customers.hbs b/addon/templates/users/customers.hbs new file mode 100644 index 0000000..9c9f736 --- /dev/null +++ b/addon/templates/users/customers.hbs @@ -0,0 +1,14 @@ + + + +{{outlet}} \ No newline at end of file diff --git a/addon/templates/users/drivers.hbs b/addon/templates/users/drivers.hbs new file mode 100644 index 0000000..9c9f736 --- /dev/null +++ b/addon/templates/users/drivers.hbs @@ -0,0 +1,14 @@ + +
+ +{{outlet}} \ No newline at end of file diff --git a/addon/templates/users/index.hbs b/addon/templates/users/index.hbs index b0afac0..9c9f736 100644 --- a/addon/templates/users/index.hbs +++ b/addon/templates/users/index.hbs @@ -1,29 +1,3 @@ - -
- {{outlet}} \ No newline at end of file diff --git a/app/controllers/users.js b/app/controllers/users.js new file mode 100644 index 0000000..7a136f3 --- /dev/null +++ b/app/controllers/users.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/iam-engine/controllers/users'; diff --git a/app/controllers/users/customers.js b/app/controllers/users/customers.js new file mode 100644 index 0000000..ca8c41b --- /dev/null +++ b/app/controllers/users/customers.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/iam-engine/controllers/users/customers'; diff --git a/app/controllers/users/drivers.js b/app/controllers/users/drivers.js new file mode 100644 index 0000000..9004faa --- /dev/null +++ b/app/controllers/users/drivers.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/iam-engine/controllers/users/drivers'; diff --git a/app/routes/users/customers.js b/app/routes/users/customers.js new file mode 100644 index 0000000..4e686b9 --- /dev/null +++ b/app/routes/users/customers.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/iam-engine/routes/users/customers'; diff --git a/app/routes/users/drivers.js b/app/routes/users/drivers.js new file mode 100644 index 0000000..135a440 --- /dev/null +++ b/app/routes/users/drivers.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/iam-engine/routes/users/drivers'; diff --git a/app/templates/users/customers.js b/app/templates/users/customers.js new file mode 100644 index 0000000..1fb0fd2 --- /dev/null +++ b/app/templates/users/customers.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/iam-engine/templates/users/customers'; diff --git a/app/templates/users/drivers.js b/app/templates/users/drivers.js new file mode 100644 index 0000000..26fc0bb --- /dev/null +++ b/app/templates/users/drivers.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/iam-engine/templates/users/drivers'; diff --git a/package.json b/package.json index 3a2f74a..1f39fcb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@fleetbase/iam-engine", - "version": "0.1.6", + "version": "0.1.7", "description": "Fleetbase IAM extension provides identity and access management module for managing users, permissions and policies.", "fleetbase": { "route": "iam" @@ -42,8 +42,8 @@ }, "dependencies": { "@babel/core": "^7.23.2", - "@fleetbase/ember-core": "^0.3.8", - "@fleetbase/ember-ui": "^0.3.14", + "@fleetbase/ember-core": "^0.3.12", + "@fleetbase/ember-ui": "^0.3.21", "@fortawesome/ember-fontawesome": "^2.0.0", "@fortawesome/fontawesome-svg-core": "6.4.0", "@fortawesome/free-brands-svg-icons": "6.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0ad0a4c..dae6dcb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,11 +15,11 @@ importers: specifier: ^7.23.2 version: 7.28.5 '@fleetbase/ember-core': - specifier: ^0.3.8 - version: 0.3.8(@ember/string@3.1.1)(@ember/test-helpers@3.3.1(@babel/core@7.28.5)(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0))(webpack@5.103.0))(ember-resolver@11.0.1(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0)))(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0))(eslint@8.57.1)(webpack@5.103.0) + specifier: ^0.3.12 + version: 0.3.12(@ember/string@3.1.1)(@ember/test-helpers@3.3.1(@babel/core@7.28.5)(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0))(webpack@5.103.0))(ember-resolver@11.0.1(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0)))(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0))(eslint@8.57.1)(webpack@5.103.0) '@fleetbase/ember-ui': - specifier: ^0.3.14 - version: 0.3.14(@ember/test-helpers@3.3.1(@babel/core@7.28.5)(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0))(webpack@5.103.0))(@glimmer/component@1.1.2(@babel/core@7.28.5))(@glimmer/tracking@1.1.2)(ember-resolver@11.0.1(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0)))(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0))(postcss@8.5.6)(rollup@2.79.2)(tracked-built-ins@3.4.0(@babel/core@7.28.5))(webpack@5.103.0) + specifier: ^0.3.21 + version: 0.3.21(@ember/test-helpers@3.3.1(@babel/core@7.28.5)(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0))(webpack@5.103.0))(@glimmer/component@1.1.2(@babel/core@7.28.5))(@glimmer/tracking@1.1.2)(ember-resolver@11.0.1(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0)))(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0))(postcss@8.5.6)(rollup@2.79.2)(tracked-built-ins@3.4.0(@babel/core@7.28.5))(webpack@5.103.0) '@fortawesome/ember-fontawesome': specifier: ^2.0.0 version: 2.0.0(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0))(rollup@2.79.2)(webpack@5.103.0) @@ -255,6 +255,10 @@ packages: resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} engines: {node: '>=6.9.0'} + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + '@babel/helper-remap-async-to-generator@7.27.1': resolution: {integrity: sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==} engines: {node: '>=6.9.0'} @@ -365,6 +369,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-decorators@7.28.6': + resolution: {integrity: sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-import-assertions@7.27.1': resolution: {integrity: sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==} engines: {node: '>=6.9.0'} @@ -1239,12 +1249,12 @@ packages: peerDependencies: ember-source: '>= 4.0.0' - '@fleetbase/ember-core@0.3.8': - resolution: {integrity: sha512-lakUs/gG8k4GbJprXlrz1KIZDr5254qjVfhXU5kcaKyS9VBzgsNsWKXIA8+821lcwufGIlioE12D4+qEb4GZNA==} + '@fleetbase/ember-core@0.3.12': + resolution: {integrity: sha512-XvX8ND36FOKvNPuXvo5usDlSrH1PkfpWrRGtlU4/bDkYZBcNUH3jJAu4Wfl8vfv1Tg232bMpHjgCYUgDlu1YUg==} engines: {node: '>= 18'} - '@fleetbase/ember-ui@0.3.14': - resolution: {integrity: sha512-w0IcDsANw3Vmi/ha1a6fHexIV2beb3wupB9j3dNs43lvQp6D+zoAtMrW9q6DD6ICNCbxikmD0AMUIbjO5gJYDg==} + '@fleetbase/ember-ui@0.3.21': + resolution: {integrity: sha512-HodjXkqg29/omCxmf4jwPlZcLhbjaLqDIVTprB9+c65Ekzsca2ag51dmpjjUIJisaZQ+OtHHNpOU0pVAYgaqKA==} engines: {node: '>= 18'} '@fleetbase/intl-lint@0.0.1': @@ -3444,6 +3454,9 @@ packages: decorator-transforms@2.3.0: resolution: {integrity: sha512-jo8c1ss9yFPudHuYYcrJ9jpkDZIoi+lOGvt+Uyp9B+dz32i50icRMx9Bfa8hEt7TnX1FyKWKkjV+cUdT/ep2kA==} + decorator-transforms@2.3.1: + resolution: {integrity: sha512-PDOk74Zqqy0946Lx4ckXxbgG6uhPScOICtrxL/pXmfznxchqNee0TaJISClGJQe6FeT8ohGqsOgdjfahm4FwEw==} + deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -8466,6 +8479,8 @@ snapshots: '@babel/helper-plugin-utils@7.27.1': {} + '@babel/helper-plugin-utils@7.28.6': {} + '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -8593,6 +8608,11 @@ snapshots: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-decorators@7.28.6(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -9727,7 +9747,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@fleetbase/ember-core@0.3.8(@ember/string@3.1.1)(@ember/test-helpers@3.3.1(@babel/core@7.28.5)(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0))(webpack@5.103.0))(ember-resolver@11.0.1(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0)))(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0))(eslint@8.57.1)(webpack@5.103.0)': + '@fleetbase/ember-core@0.3.12(@ember/string@3.1.1)(@ember/test-helpers@3.3.1(@babel/core@7.28.5)(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0))(webpack@5.103.0))(ember-resolver@11.0.1(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0)))(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0))(eslint@8.57.1)(webpack@5.103.0)': dependencies: '@babel/core': 7.28.5 compress-json: 3.4.0 @@ -9760,7 +9780,7 @@ snapshots: - utf-8-validate - webpack - '@fleetbase/ember-ui@0.3.14(@ember/test-helpers@3.3.1(@babel/core@7.28.5)(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0))(webpack@5.103.0))(@glimmer/component@1.1.2(@babel/core@7.28.5))(@glimmer/tracking@1.1.2)(ember-resolver@11.0.1(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0)))(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0))(postcss@8.5.6)(rollup@2.79.2)(tracked-built-ins@3.4.0(@babel/core@7.28.5))(webpack@5.103.0)': + '@fleetbase/ember-ui@0.3.21(@ember/test-helpers@3.3.1(@babel/core@7.28.5)(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0))(webpack@5.103.0))(@glimmer/component@1.1.2(@babel/core@7.28.5))(@glimmer/tracking@1.1.2)(ember-resolver@11.0.1(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0)))(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0))(postcss@8.5.6)(rollup@2.79.2)(tracked-built-ins@3.4.0(@babel/core@7.28.5))(webpack@5.103.0)': dependencies: '@babel/core': 7.28.5 '@ember/render-modifiers': 2.1.0(@babel/core@7.28.5)(ember-source@5.4.1(@babel/core@7.28.5)(@glimmer/component@1.1.2(@babel/core@7.28.5))(rsvp@4.8.5)(webpack@5.103.0)) @@ -12646,6 +12666,13 @@ snapshots: transitivePeerDependencies: - '@babel/core' + decorator-transforms@2.3.1(@babel/core@7.28.5): + dependencies: + '@babel/plugin-syntax-decorators': 7.28.6(@babel/core@7.28.5) + babel-import-util: 3.0.1 + transitivePeerDependencies: + - '@babel/core' + deep-extend@0.6.0: {} deep-is@0.1.4: {} @@ -18682,7 +18709,7 @@ snapshots: tracked-built-ins@3.4.0(@babel/core@7.28.5): dependencies: '@embroider/addon-shim': 1.10.2 - decorator-transforms: 2.3.0(@babel/core@7.28.5) + decorator-transforms: 2.3.1(@babel/core@7.28.5) ember-tracked-storage-polyfill: 1.0.0 transitivePeerDependencies: - '@babel/core' diff --git a/tests/unit/controllers/users-test.js b/tests/unit/controllers/users-test.js new file mode 100644 index 0000000..7f2879a --- /dev/null +++ b/tests/unit/controllers/users-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'dummy/tests/helpers'; + +module('Unit | Controller | users', function (hooks) { + setupTest(hooks); + + // TODO: Replace this with your real tests. + test('it exists', function (assert) { + let controller = this.owner.lookup('controller:users'); + assert.ok(controller); + }); +}); diff --git a/tests/unit/controllers/users/customers-test.js b/tests/unit/controllers/users/customers-test.js new file mode 100644 index 0000000..a06970d --- /dev/null +++ b/tests/unit/controllers/users/customers-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'dummy/tests/helpers'; + +module('Unit | Controller | users/customers', function (hooks) { + setupTest(hooks); + + // TODO: Replace this with your real tests. + test('it exists', function (assert) { + let controller = this.owner.lookup('controller:users/customers'); + assert.ok(controller); + }); +}); diff --git a/tests/unit/controllers/users/drivers-test.js b/tests/unit/controllers/users/drivers-test.js new file mode 100644 index 0000000..674f2b7 --- /dev/null +++ b/tests/unit/controllers/users/drivers-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'dummy/tests/helpers'; + +module('Unit | Controller | users/drivers', function (hooks) { + setupTest(hooks); + + // TODO: Replace this with your real tests. + test('it exists', function (assert) { + let controller = this.owner.lookup('controller:users/drivers'); + assert.ok(controller); + }); +}); diff --git a/tests/unit/routes/users/customers-test.js b/tests/unit/routes/users/customers-test.js new file mode 100644 index 0000000..4ffde7b --- /dev/null +++ b/tests/unit/routes/users/customers-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'dummy/tests/helpers'; + +module('Unit | Route | users/customers', function (hooks) { + setupTest(hooks); + + test('it exists', function (assert) { + let route = this.owner.lookup('route:users/customers'); + assert.ok(route); + }); +}); diff --git a/tests/unit/routes/users/drivers-test.js b/tests/unit/routes/users/drivers-test.js new file mode 100644 index 0000000..34c98c6 --- /dev/null +++ b/tests/unit/routes/users/drivers-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'dummy/tests/helpers'; + +module('Unit | Route | users/drivers', function (hooks) { + setupTest(hooks); + + test('it exists', function (assert) { + let route = this.owner.lookup('route:users/drivers'); + assert.ok(route); + }); +}); From 0fb1678d5612bf1ab5dfe520e5fd5a45a5c11502 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Fri, 27 Feb 2026 12:31:48 +0800 Subject: [PATCH 2/2] micro improvements to edit user and validation/error handling --- addon/controllers/users/index.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/addon/controllers/users/index.js b/addon/controllers/users/index.js index 42fafd0..ed7525a 100644 --- a/addon/controllers/users/index.js +++ b/addon/controllers/users/index.js @@ -340,6 +340,7 @@ export default class UsersIndexController extends Controller { acceptButtonIcon: 'save', acceptButtonDisabled: this.abilities.cannot(formPermission), acceptButtonHelpText: this.abilities.cannot(formPermission) ? this.intl.t('common.unauthorized') : null, + keepOpen: true, formPermission, user, uploadNewPhoto: (file) => { @@ -370,9 +371,16 @@ export default class UsersIndexController extends Controller { try { await user.save(); this.notifications.success(this.intl.t('iam.users.index.user-changes-saved-success')); - return this.hostRouter.refresh(); + this.hostRouter.refresh(); + modal.done(); } catch (error) { this.notifications.serverError(error); + + // If error is because email address was made empty rollback changes + if (error && typeof error.message === 'string' && error.message.includes('Email address cannot be empty')) { + user.rollbackAttributes(); + } + modal.stopLoading(); } }, @@ -395,6 +403,7 @@ export default class UsersIndexController extends Controller { body: this.intl.t('iam.users.index.data-assosciated-user-delete'), confirm: async (modal) => { modal.startLoading(); + try { await user.removeFromCurrentCompany(); this.notifications.success(this.intl.t('iam.users.index.delete-user-success-message', { userName: user.get('name') })); @@ -418,6 +427,7 @@ export default class UsersIndexController extends Controller { body: this.intl.t('iam.users.index.access-account-or-resources-unless-re-activated'), confirm: async (modal) => { modal.startLoading(); + try { await user.deactivate(); this.notifications.success(this.intl.t('iam.users.index.deactivate-user-success-message', { userName: user.get('name') })); @@ -441,6 +451,7 @@ export default class UsersIndexController extends Controller { body: this.intl.t('iam.users.index.this-user-will-regain-access-to-your-organization'), confirm: async (modal) => { modal.startLoading(); + try { await user.activate(); this.notifications.success(this.intl.t('iam.users.index.re-activate-user-success-message', { userName: user.get('name') })); @@ -464,6 +475,7 @@ export default class UsersIndexController extends Controller { body: this.intl.t('iam.users.index.verify-user-manually-prompt'), confirm: async (modal) => { modal.startLoading(); + try { await user.verify(); this.notifications.success(this.intl.t('iam.users.index.user-verified-success-message', { userName: user.get('name') })); @@ -499,6 +511,7 @@ export default class UsersIndexController extends Controller { body: this.intl.t('iam.users.index.confirming-fleetbase-will-re-send-invitation-for-user-to-join-your-organization'), confirm: async (modal) => { modal.startLoading(); + try { await user.resendInvite(); this.notifications.success(this.intl.t('iam.users.index.invitation-resent'));