From 02f9ff8618507a7e4dc428e1c1e423526d9c8b98 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Fri, 9 Oct 2026 17:34:18 +0800 Subject: [PATCH 1/2] fix(i18n): keep the Russian multi-cart label on its own lines so the translations build again c8af413 inserted the split-tips label between the two lines of the enable-multi-cart-checkout value, which js-yaml reads as a bad mapping entry and the engine build fails. --- translations/ru-ru.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/translations/ru-ru.yaml b/translations/ru-ru.yaml index 20a565f1..d815e0f5 100644 --- a/translations/ru-ru.yaml +++ b/translations/ru-ru.yaml @@ -417,8 +417,8 @@ storefront: enable-tips: Включить чаевые enable-delivery-tips: Включить чаевые за доставку enable-multi-cart-checkout: Включить оформление заказа из нескольких - split-tips-across-stores: "Распределять чаевые между магазинами в заказах из нескольких магазинов" корзин + split-tips-across-stores: "Распределять чаевые между магазинами в заказах из нескольких магазинов" enable-user-reviews: Включить отзывы пользователей api-panel: panel-title: API From c833b5c8ad29481058d7d5418b3b22d40f3c8d63 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Fri, 9 Oct 2026 17:46:29 +0800 Subject: [PATCH 2/2] feat(identity): drivers, customers, vehicles and places render through the shared identity cells, pills and select options The orders, network orders, customers and network customers tables use table/cell/identity typed for the shared fleetops-data descriptors, reading the loaded relation or an identity stub named after the row, so a row shows the right tile and name before the relation loads. Clicking a customer opens the storefront customer page; a driver or place opens its FleetOps panel, loading the engine only then. The order details, fulfillment and customer panels, the incoming-order modal, the orders and customers widgets and the food-truck cards use Customer::Pill, Driver::Pill and Vehicle::Pill in place of hand-rolled avatar blocks, whose styles are removed. Driver, vehicle, customer, user and category selects render SelectOption rows with compact selected items. The fleetops-data range moves to ^0.2.3, the version the workspace links. --- addon/components/modals/assign-driver.hbs | 3 +- addon/components/modals/create-food-truck.hbs | 11 +-- .../modals/create-network-category.hbs | 2 +- addon/components/modals/import-products.hbs | 2 +- addon/components/modals/incoming-order.hbs | 24 ++----- .../modals/order-ready-assign-driver.hbs | 3 +- .../storefront/order/details/customer.hbs | 16 ++--- .../storefront/order/details/detail.hbs | 18 +---- .../storefront/order/details/fulfillment.hbs | 13 +--- addon/components/widget/customers.hbs | 11 +-- addon/components/widget/orders.hbs | 20 +++--- addon/controllers/customers/index.js | 3 +- .../networks/index/network/customers.js | 3 +- .../networks/index/network/orders.js | 38 ++++++++-- addon/controllers/orders/index.js | 38 ++++++++-- addon/styles/storefront-engine.css | 41 ----------- addon/templates/food-trucks/index.hbs | 7 +- .../networks/index/network/index.hbs | 3 +- .../promotions/push-notifications.hbs | 19 +---- addon/templates/settings/index.hbs | 3 +- package.json | 2 +- .../components/shared-identity-test.js | 71 +++++++++++++++++++ .../storefront/order/details-test.js | 26 +++++++ .../components/widget/customers-test.js | 2 +- .../unit/controllers/customers/index-test.js | 9 +++ .../networks/index/network/customers-test.js | 10 ++- .../networks/index/network/orders-test.js | 21 +++++- tests/unit/controllers/orders/index-test.js | 66 +++++++++++++++++ 28 files changed, 312 insertions(+), 173 deletions(-) create mode 100644 tests/integration/components/shared-identity-test.js diff --git a/addon/components/modals/assign-driver.hbs b/addon/components/modals/assign-driver.hbs index 02f6de6a..88b0947d 100644 --- a/addon/components/modals/assign-driver.hbs +++ b/addon/components/modals/assign-driver.hbs @@ -16,6 +16,7 @@ - {{model.name}} + diff --git a/addon/components/modals/create-food-truck.hbs b/addon/components/modals/create-food-truck.hbs index 41b9a9e9..3332b6b6 100644 --- a/addon/components/modals/create-food-truck.hbs +++ b/addon/components/modals/create-food-truck.hbs @@ -3,6 +3,7 @@ -
-
- -
-
-
{{model.display_name}}
-
{{model.plate_number}}
-
-
+
diff --git a/addon/components/modals/create-network-category.hbs b/addon/components/modals/create-network-category.hbs index 0a489992..832f5419 100644 --- a/addon/components/modals/create-network-category.hbs +++ b/addon/components/modals/create-network-category.hbs @@ -12,7 +12,7 @@ @onChange={{@options.setParentCategory}} as |model| > - {{model.name}} + - {{model.name}} + diff --git a/addon/components/modals/incoming-order.hbs b/addon/components/modals/incoming-order.hbs index 9accc241..fba4a8f3 100644 --- a/addon/components/modals/incoming-order.hbs +++ b/addon/components/modals/incoming-order.hbs @@ -34,16 +34,7 @@
{{t "storefront.component.modals.incoming-order.assigned"}}
{{#if @options.order.driver_assigned.id}} -
- {{@options.order.driver_assigned.name}} -
-
{{n-a @options.order.driver_assigned.displayName}}
-
{{n-a - @options.order.driver_assigned.phone - (t "storefront.component.modals.incoming-order.no-phone") - }}
-
-
+ {{else}}
{{t "storefront.component.modals.incoming-order.not-assigned"}}
@@ -72,16 +63,9 @@
{{t "storefront.common.customer"}}
-
-
- {{@options.order.customer.name}} -
-
-
{{@options.order.customer.name}}
-
{{@options.order.customer.email}}
-
{{@options.order.customer.phone}}
-
-
+ +
{{n-a @options.order.customer.email}}
+
{{n-a @options.order.customer.phone}}
{{#unless @options.order.meta.is_pickup}}
{{t "storefront.component.modals.incoming-order.address"}}
diff --git a/addon/components/modals/order-ready-assign-driver.hbs b/addon/components/modals/order-ready-assign-driver.hbs index 83e23bc6..9e313645 100644 --- a/addon/components/modals/order-ready-assign-driver.hbs +++ b/addon/components/modals/order-ready-assign-driver.hbs @@ -29,6 +29,7 @@ - {{model.name}} + {{/unless}} diff --git a/addon/components/storefront/order/details/customer.hbs b/addon/components/storefront/order/details/customer.hbs index d0eb04a3..59957374 100644 --- a/addon/components/storefront/order/details/customer.hbs +++ b/addon/components/storefront/order/details/customer.hbs @@ -1,17 +1,9 @@
-
- {{@resource.customer.name}} -
-
{{n-a @resource.customer.name "No customer"}}
-
{{n-a @resource.customer.email "No email"}}
-
{{n-a @resource.customer.phone "No phone"}}
-
+ +
+
Email{{n-a @resource.customer.email "No email"}}
+
Phone{{n-a @resource.customer.phone "No phone"}}
{{#unless @resource.meta.is_pickup}} diff --git a/addon/components/storefront/order/details/detail.hbs b/addon/components/storefront/order/details/detail.hbs index ca92fadc..9ffc644b 100644 --- a/addon/components/storefront/order/details/detail.hbs +++ b/addon/components/storefront/order/details/detail.hbs @@ -3,27 +3,13 @@
Customer
-
- {{@resource.customer.name}} -
-
{{n-a @resource.customer.name "No customer"}}
-
{{n-a (or @resource.customer.phone @resource.customer.email) "No contact details"}}
-
-
+
Driver
-
- {{@resource.driver_assigned.name}} -
-
- {{n-a (or @resource.driver_assigned.displayName @resource.driver_assigned.name) "No driver assigned"}} -
-
{{n-a @resource.driver_assigned.phone "No phone"}}
-
-
+
diff --git a/addon/components/storefront/order/details/fulfillment.hbs b/addon/components/storefront/order/details/fulfillment.hbs index 508ca07d..36e5b262 100644 --- a/addon/components/storefront/order/details/fulfillment.hbs +++ b/addon/components/storefront/order/details/fulfillment.hbs @@ -3,18 +3,7 @@
Driver
-
- {{@resource.driver_assigned.name}} -
-
{{n-a (or @resource.driver_assigned.displayName @resource.driver_assigned.name) "No driver assigned"}}
-
{{n-a @resource.driver_assigned.phone "No phone"}}
-
-
+
Schedule
diff --git a/addon/components/widget/customers.hbs b/addon/components/widget/customers.hbs index f9268eb5..afb75753 100644 --- a/addon/components/widget/customers.hbs +++ b/addon/components/widget/customers.hbs @@ -15,10 +15,8 @@ {{#each this.customers as |customer|}}
- {{customer.name}}
- {{n-a customer.name}} -
{{customer.public_id}}
+
{{n-a customer.orders 0}}
@@ -32,7 +30,7 @@ {{/each}}
{{else}} -
+
@@ -48,10 +46,7 @@ diff --git a/addon/components/widget/orders.hbs b/addon/components/widget/orders.hbs index 3ea48856..d256ad12 100644 --- a/addon/components/widget/orders.hbs +++ b/addon/components/widget/orders.hbs @@ -26,14 +26,12 @@
- {{n-a order.customer.name}} - - {{#if order.meta.is_pickup}} - {{t "storefront.component.widget.orders.pickup-order"}} - {{else}} - {{n-a order.driver_assigned.name}} - {{/if}} - + + {{#if order.meta.is_pickup}} + {{t "storefront.component.widget.orders.pickup-order"}} + {{else}} + + {{/if}}
@@ -46,7 +44,7 @@ {{/each}}
{{else}} -
+
{{customer.public_id}} -
- {{customer.name}} - {{n-a customer.name}} -
+
{{n-a customer.phone}} {{n-a customer.email}}
@@ -68,14 +66,14 @@ - + diff --git a/addon/controllers/customers/index.js b/addon/controllers/customers/index.js index 6242a732..72ffa14e 100644 --- a/addon/controllers/customers/index.js +++ b/addon/controllers/customers/index.js @@ -123,7 +123,8 @@ export default class CustomersIndexController extends BaseController { label: this.intl.t('storefront.common.name'), valuePath: 'name', width: '15%', - cellComponent: 'table/cell/media-name', + cellComponent: 'table/cell/identity', + resourceType: 'customer', action: this.viewCustomer, resizable: true, sortable: true, diff --git a/addon/controllers/networks/index/network/customers.js b/addon/controllers/networks/index/network/customers.js index 26776546..6d8f8dfc 100644 --- a/addon/controllers/networks/index/network/customers.js +++ b/addon/controllers/networks/index/network/customers.js @@ -15,7 +15,8 @@ export default class NetworksIndexNetworkCustomersController extends CustomersIn label: this.intl.t('storefront.common.name'), valuePath: 'name', width: '25%', - cellComponent: 'table/cell/media-name', + cellComponent: 'table/cell/identity', + resourceType: 'customer', action: this.viewCustomer, resizable: true, sortable: true, diff --git a/addon/controllers/networks/index/network/orders.js b/addon/controllers/networks/index/network/orders.js index eccfe065..2411a5d0 100644 --- a/addon/controllers/networks/index/network/orders.js +++ b/addon/controllers/networks/index/network/orders.js @@ -1,4 +1,6 @@ import BaseController from '@fleetbase/storefront-engine/controllers/base-controller'; +import { relationValue } from '@fleetbase/ember-ui/utils/resource-registry'; +import { buildIdentityStub } from '@fleetbase/fleetops-data/utils/identity-stub'; import { inject as service } from '@ember/service'; import { tracked, cached } from '@glimmer/tracking'; import { isBlank } from '@ember/utils'; @@ -11,6 +13,7 @@ export default class NetworksIndexNetworkOrdersController extends BaseController * * @var {Service} */ + @service store; @service notifications; /** @@ -101,7 +104,11 @@ export default class NetworksIndexNetworkOrdersController extends BaseController id: 'customer-name', label: this.intl.t('storefront.orders.index.customer'), valuePath: 'customer.name', - cellComponent: 'table/cell/base', + cellComponent: 'table/cell/identity', + resourceType: 'customer', + resourcePath: (order) => relationValue(order, 'customer') ?? buildIdentityStub(order, { type: order.customer_type ?? 'customer', nameKey: 'customer_name' }), + action: this.viewCustomer, + emptyText: 'No customer', width: '125px', resizable: true, sortable: true, @@ -116,7 +123,9 @@ export default class NetworksIndexNetworkOrdersController extends BaseController id: 'pickup-name', label: this.intl.t('storefront.common.pickup'), valuePath: 'pickupName', - cellComponent: 'table/cell/base', + cellComponent: 'table/cell/identity', + resourceType: 'place', + resourcePath: (order) => relationValue(relationValue(order, 'payload'), 'pickup') ?? buildIdentityStub(order, { type: 'place', nameKey: 'pickupName' }), width: '160px', resizable: true, sortable: true, @@ -130,7 +139,9 @@ export default class NetworksIndexNetworkOrdersController extends BaseController id: 'dropoff-name', label: this.intl.t('storefront.common.dropoff'), valuePath: 'dropoffName', - cellComponent: 'table/cell/base', + cellComponent: 'table/cell/identity', + resourceType: 'place', + resourcePath: (order) => relationValue(relationValue(order, 'payload'), 'dropoff') ?? buildIdentityStub(order, { type: 'place', nameKey: 'dropoffName' }), width: '160px', resizable: true, sortable: true, @@ -185,9 +196,13 @@ export default class NetworksIndexNetworkOrdersController extends BaseController { id: 'driver-assigned', label: this.intl.t('storefront.orders.index.driver-assigned'), - cellComponent: 'table/cell/driver-name', + cellComponent: 'table/cell/identity', + resourceType: 'driver', valuePath: 'driver_assigned', - modelPath: 'driver_assigned', + resourcePath: (order) => + relationValue(order, 'driver_assigned') ?? + buildIdentityStub(order, { type: 'driver', load: () => (order.driver_assigned_uuid ? this.store.findRecord('driver', order.driver_assigned_uuid) : null) }), + emptyText: 'No driver assigned', width: '170px', resizable: true, sortable: true, @@ -339,6 +354,19 @@ export default class NetworksIndexNetworkOrdersController extends BaseController this.query = value; } + /** + * Opens the storefront customer behind an order. A stub that only carries + * the customer's name has no page to open. + * @param {Object} customer + */ + @action viewCustomer(customer) { + if (!customer?.public_id) { + return; + } + + return this.transitionToRoute('customers.index.view', customer.public_id); + } + @action viewOrder(order) { return this.storefrontOrderActions.viewOrder(order); } diff --git a/addon/controllers/orders/index.js b/addon/controllers/orders/index.js index e5575279..1e77eb19 100644 --- a/addon/controllers/orders/index.js +++ b/addon/controllers/orders/index.js @@ -1,4 +1,6 @@ import BaseController from '@fleetbase/storefront-engine/controllers/base-controller'; +import { relationValue } from '@fleetbase/ember-ui/utils/resource-registry'; +import { buildIdentityStub } from '@fleetbase/fleetops-data/utils/identity-stub'; import { tracked } from '@glimmer/tracking'; import { action, get } from '@ember/object'; import { inject as service } from '@ember/service'; @@ -7,6 +9,7 @@ import { isArray } from '@ember/array'; import { timeout, task } from 'ember-concurrency'; export default class OrdersIndexController extends BaseController { + @service store; @service notifications; @service intl; @service modalsManager; @@ -131,7 +134,11 @@ export default class OrdersIndexController extends BaseController { id: 'customer-name', label: this.intl.t('storefront.orders.index.customer'), valuePath: 'customer.name', - cellComponent: 'table/cell/base', + cellComponent: 'table/cell/identity', + resourceType: 'customer', + resourcePath: (order) => relationValue(order, 'customer') ?? buildIdentityStub(order, { type: order.customer_type ?? 'customer', nameKey: 'customer_name' }), + action: this.viewCustomer, + emptyText: 'No customer', width: '100px', resizable: true, sortable: true, @@ -157,7 +164,9 @@ export default class OrdersIndexController extends BaseController { id: 'pickup-name', label: this.intl.t('storefront.common.pickup'), valuePath: 'pickupName', - cellComponent: 'table/cell/base', + cellComponent: 'table/cell/identity', + resourceType: 'place', + resourcePath: (order) => relationValue(relationValue(order, 'payload'), 'pickup') ?? buildIdentityStub(order, { type: 'place', nameKey: 'pickupName' }), width: '150px', resizable: true, sortable: true, @@ -171,7 +180,9 @@ export default class OrdersIndexController extends BaseController { id: 'dropoff-name', label: this.intl.t('storefront.common.dropoff'), valuePath: 'dropoffName', - cellComponent: 'table/cell/base', + cellComponent: 'table/cell/identity', + resourceType: 'place', + resourcePath: (order) => relationValue(relationValue(order, 'payload'), 'dropoff') ?? buildIdentityStub(order, { type: 'place', nameKey: 'dropoffName' }), width: '150px', resizable: true, sortable: true, @@ -184,9 +195,13 @@ export default class OrdersIndexController extends BaseController { { id: 'driver-assigned', label: this.intl.t('storefront.orders.index.driver-assigned'), - cellComponent: 'table/cell/driver-name', + cellComponent: 'table/cell/identity', + resourceType: 'driver', valuePath: 'driver_assigned', - modelPath: 'driver_assigned', + resourcePath: (order) => + relationValue(order, 'driver_assigned') ?? + buildIdentityStub(order, { type: 'driver', load: () => (order.driver_assigned_uuid ? this.store.findRecord('driver', order.driver_assigned_uuid) : null) }), + emptyText: 'No driver assigned', width: '150px', resizable: true, sortable: true, @@ -388,6 +403,19 @@ export default class OrdersIndexController extends BaseController { this.query = value; } + /** + * Opens the storefront customer behind an order. A stub that only carries + * the customer's name has no page to open. + * @param {Object} customer + */ + @action viewCustomer(customer) { + if (!customer?.public_id) { + return; + } + + return this.transitionToRoute('customers.index.view', customer.public_id); + } + @action viewOrder(order) { return this.transitionToRoute('orders.index.view', order); } diff --git a/addon/styles/storefront-engine.css b/addon/styles/storefront-engine.css index 1b4350cb..9034af9d 100644 --- a/addon/styles/storefront-engine.css +++ b/addon/styles/storefront-engine.css @@ -1912,22 +1912,6 @@ body[data-theme='dark'] .storefront-widget-count { background-color: #1d4ed8; } -.storefront-customer-cell { - display: flex; - min-width: 0; - align-items: center; - gap: 0.65rem; -} - -.storefront-customer-avatar { - width: 2rem; - height: 2rem; - flex: 0 0 auto; - border-radius: 0.35rem; - object-fit: cover; - box-shadow: 0 1px 2px rgb(15 23 42 / 16%); -} - .storefront-customer-name { display: block; overflow: hidden; @@ -2244,24 +2228,12 @@ body[data-theme='dark'] .storefront-order-totals strong { gap: 0.75rem; } -.storefront-order-person--compact { - align-items: flex-start; -} - .storefront-order-store { display: flex; align-items: flex-start; gap: 0.9rem; } -.storefront-order-person__avatar { - width: 2.75rem; - height: 2.75rem; - flex: 0 0 auto; - border-radius: 0.5rem; - object-fit: cover; -} - .storefront-order-person__name { overflow: hidden; color: #111827; @@ -2275,19 +2247,6 @@ body[data-theme='dark'] .storefront-order-person__name { color: #f8fafc; } -.storefront-order-person__meta { - overflow: hidden; - color: #64748b; - font-size: 0.75rem; - font-weight: 600; - text-overflow: ellipsis; - white-space: nowrap; -} - -body[data-theme='dark'] .storefront-order-person__meta { - color: #94a3b8; -} - .storefront-order-facts, .storefront-order-totals { display: flex; diff --git a/addon/templates/food-trucks/index.hbs b/addon/templates/food-trucks/index.hbs index b707b1dd..61cf0250 100644 --- a/addon/templates/food-trucks/index.hbs +++ b/addon/templates/food-trucks/index.hbs @@ -9,11 +9,8 @@ {{#each @model as |foodTruck|}}
-
- -
-
-
{{foodTruck.vehicle.display_name}}
+
+
{{n-a foodTruck.service_area.name}}
diff --git a/addon/templates/networks/index/network/index.hbs b/addon/templates/networks/index/network/index.hbs index 4666f10b..e0d28ae5 100644 --- a/addon/templates/networks/index/network/index.hbs +++ b/addon/templates/networks/index/network/index.hbs @@ -139,6 +139,7 @@ - {{model.name}} +
diff --git a/addon/templates/promotions/push-notifications.hbs b/addon/templates/promotions/push-notifications.hbs index b74e9994..35f04270 100644 --- a/addon/templates/promotions/push-notifications.hbs +++ b/addon/templates/promotions/push-notifications.hbs @@ -37,6 +37,7 @@ -
-
- -
-
-
{{model.name}}
-
{{n-a model.email}}
-
{{n-a model.phone}}
-
-
+
{{/unless}} diff --git a/addon/templates/settings/index.hbs b/addon/templates/settings/index.hbs index 581dec48..3e3ee253 100644 --- a/addon/templates/settings/index.hbs +++ b/addon/templates/settings/index.hbs @@ -217,6 +217,7 @@ - {{model.name}} +
diff --git a/package.json b/package.json index 43ea033e..344a57db 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "@babel/core": "^7.23.2", "@fleetbase/ember-core": "^0.3.25", "@fleetbase/ember-ui": "^0.4.5", - "@fleetbase/fleetops-data": "^0.1.37", + "@fleetbase/fleetops-data": "^0.2.3", "@fortawesome/ember-fontawesome": "^2.0.0", "@fortawesome/fontawesome-svg-core": "6.4.0", "@fortawesome/free-brands-svg-icons": "6.4.0", diff --git a/tests/integration/components/shared-identity-test.js b/tests/integration/components/shared-identity-test.js new file mode 100644 index 00000000..00bf135c --- /dev/null +++ b/tests/integration/components/shared-identity-test.js @@ -0,0 +1,71 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'dummy/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { initialize } from '@fleetbase/fleetops-data/instance-initializers/register-shared-resource-descriptors'; + +/** + * Storefront never loads the FleetOps engine to show a driver, customer or + * place: the descriptors and wrappers come from fleetops-data and the + * generic components from ember-ui, both of which this engine bundles. + */ +module('Integration | Component | shared identity components', function (hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function () { + initialize(this.owner); + }); + + test('a driver identity cell renders the name, status dot and vehicle badge from the shared descriptor', async function (assert) { + this.set('row', { driver_assigned: { resourceType: 'driver', name: 'Ada Driver', status: 'available', vehicle_name: 'Truck 10' } }); + this.set('column', { resourceType: 'driver', resourcePath: 'driver_assigned', popover: false }); + + await render(hbs``); + + assert.dom('[data-test-identity-label]').hasText('Ada Driver'); + assert.dom('[data-test-resource-identity-status-dot]').hasClass('text-green-500'); + assert.dom('[data-test-resource-identity-meta-badge][data-badge-key="vehicle"]').hasText('Truck 10'); + }); + + test('an empty relation renders the column empty text instead of a cell', async function (assert) { + this.set('row', {}); + this.set('column', { resourceType: 'driver', resourcePath: 'driver_assigned', emptyText: 'No driver assigned' }); + + await render(hbs``); + + assert.dom('[data-test-identity-empty-text]').hasText('No driver assigned'); + assert.dom('[data-test-identity-cell]').doesNotExist(); + }); + + test('the customer, driver, place and vehicle pills and select options resolve inside storefront', async function (assert) { + this.set('customer', { resourceType: 'customer', name: 'Ava Chen', customer_type: 'fleet-ops:contact' }); + this.set('driver', { resourceType: 'driver', name: 'Ada Driver', phone: '+15550100' }); + this.set('place', { resourceType: 'place', name: 'Depot', city: 'Singapore', country: 'SG' }); + this.set('vehicle', { resourceType: 'vehicle', displayName: 'Truck 10', plate_number: 'TRK-10' }); + + await render(hbs` +
+
+
+
+
+ `); + + assert.dom('[data-test-customer] [data-test-resource-pill-title]').hasText('Ava Chen'); + assert.dom('[data-test-customer] [data-test-resource-pill-subtitle]').hasText('Customer'); + assert.dom('[data-test-driver] [data-test-resource-pill-title]').hasText('Ada Driver'); + assert.dom('[data-test-driver] [data-test-resource-pill-subtitle]').hasText('+15550100'); + assert.dom('[data-test-place] [data-test-resource-pill-title]').hasText('Depot'); + assert.dom('[data-test-place] [data-test-resource-pill-subtitle]').hasText('Singapore, SG'); + assert.dom('[data-test-vehicle] [data-test-select-option-title]').hasText('Truck 10'); + assert.dom('[data-test-vehicle] .select-option').hasClass('select-option--compact'); + assert.dom('[data-test-driver-option] [data-test-select-option-title]').hasText('Ada Driver'); + }); + + test('a pill with no record shows its fallback title and does not open anything', async function (assert) { + await render(hbs``); + + assert.dom('[data-test-resource-pill-title]').hasText('No driver assigned'); + assert.dom('[data-test-resource-pill] a[href]').doesNotExist(); + }); +}); diff --git a/tests/integration/components/storefront/order/details-test.js b/tests/integration/components/storefront/order/details-test.js index 1d392fca..981b8944 100644 --- a/tests/integration/components/storefront/order/details-test.js +++ b/tests/integration/components/storefront/order/details-test.js @@ -2,10 +2,15 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'dummy/tests/helpers'; import { render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; +import { initialize } from '@fleetbase/fleetops-data/instance-initializers/register-shared-resource-descriptors'; module('Integration | Component | storefront/order/details', function (hooks) { setupRenderingTest(hooks); + hooks.beforeEach(function () { + initialize(this.owner); + }); + test('it renders dedicated storefront order detail panels', async function (assert) { this.set('order', { public_id: 'order_test', @@ -58,9 +63,30 @@ module('Integration | Component | storefront/order/details', function (hooks) { assert.dom('.next-content-panel-title-container').hasTextContaining('Route'); assert.dom('.next-content-panel-title-container').hasTextContaining('Metadata'); assert.dom('.storefront-order-person__name').hasTextContaining('Tasty Store'); + assert.dom('[data-test-order-driver-pill] [data-test-resource-pill-title]').hasText('No driver assigned', 'an unassigned driver is a static pill with the fallback title'); + assert.dom('[data-test-order-customer-pill] [data-test-resource-pill-title]').hasText('No customer'); assert.dom('.storefront-order-item__image').exists(); assert.dom('.storefront-order-item__price').exists(); assert.dom().doesNotContainText('Delivery Address'); assert.dom().doesNotContainText('Pickup Address'); }); + + test('a loaded customer and driver render as their pills', async function (assert) { + this.set('order', { + public_id: 'order_test', + meta: { total: 10, currency: 'USD', is_pickup: true }, + customer: { id: 'contact_1', resourceType: 'customer', name: 'Ava Chen', email: 'ava@example.test', phone: '+15550201' }, + driver_assigned: { id: 'driver_1', resourceType: 'driver', name: 'Ada Driver', phone: '+15550100' }, + payload: { pickup: {}, dropoff: {}, entities: [] }, + tracking_number: {}, + tracking_statuses: [], + files: [], + }); + + await render(hbs``); + + assert.dom('[data-test-order-customer-pill] [data-test-resource-pill-title]').hasText('Ava Chen'); + assert.dom('[data-test-order-driver-pill] [data-test-resource-pill-title]').hasText('Ada Driver'); + assert.dom('[data-test-order-driver-pill] [data-test-resource-pill-subtitle]').hasText('+15550100'); + }); }); diff --git a/tests/integration/components/widget/customers-test.js b/tests/integration/components/widget/customers-test.js index 9d51d239..4aa03639 100644 --- a/tests/integration/components/widget/customers-test.js +++ b/tests/integration/components/widget/customers-test.js @@ -57,7 +57,7 @@ module('Integration | Component | widget/customers', function (hooks) { assert.dom('.storefront-widget-count').hasText('1'); assert.dom('.storefront-customers-table').exists(); assert.dom('.storefront-customer-id').hasText('contact_1'); - assert.dom('.storefront-customers-table').includesText('Ava Chen'); + assert.dom('.storefront-customers-table [data-test-resource-pill-title]').hasText('Ava Chen', 'the customer renders as its pill'); assert.dom('.storefront-customers-table').includesText('ava.chen@example.test'); }); }); diff --git a/tests/unit/controllers/customers/index-test.js b/tests/unit/controllers/customers/index-test.js index 1acfcb49..d8189724 100644 --- a/tests/unit/controllers/customers/index-test.js +++ b/tests/unit/controllers/customers/index-test.js @@ -17,4 +17,13 @@ module('Unit | Controller | customers/index', function (hooks) { assert.true(nameColumn.sticky, 'name column is sticky'); assert.strictEqual(actionColumn.sticky, 'right', 'action column is sticky on the right'); }); + + test('the name column is a customer identity cell that opens the storefront customer', function (assert) { + let controller = this.owner.lookup('controller:customers/index'); + let [nameColumn] = controller.columns; + + assert.strictEqual(nameColumn.cellComponent, 'table/cell/identity'); + assert.strictEqual(nameColumn.resourceType, 'customer'); + assert.strictEqual(nameColumn.action, controller.viewCustomer); + }); }); diff --git a/tests/unit/controllers/networks/index/network/customers-test.js b/tests/unit/controllers/networks/index/network/customers-test.js index 844ec516..bd90c9fb 100644 --- a/tests/unit/controllers/networks/index/network/customers-test.js +++ b/tests/unit/controllers/networks/index/network/customers-test.js @@ -4,9 +4,17 @@ import { setupTest } from 'dummy/tests/helpers'; module('Unit | Controller | networks/index/network/customers', function (hooks) { setupTest(hooks); - // TODO: Replace this with your real tests. test('it exists', function (assert) { let controller = this.owner.lookup('controller:networks/index/network/customers'); assert.ok(controller); }); + + test('the name column is a customer identity cell that opens the customer panel', function (assert) { + let controller = this.owner.lookup('controller:networks/index/network/customers'); + let [nameColumn] = controller.columns; + + assert.strictEqual(nameColumn.cellComponent, 'table/cell/identity'); + assert.strictEqual(nameColumn.resourceType, 'customer'); + assert.strictEqual(nameColumn.action, controller.viewCustomer); + }); }); diff --git a/tests/unit/controllers/networks/index/network/orders-test.js b/tests/unit/controllers/networks/index/network/orders-test.js index 59ef20f3..141a91cc 100644 --- a/tests/unit/controllers/networks/index/network/orders-test.js +++ b/tests/unit/controllers/networks/index/network/orders-test.js @@ -4,9 +4,28 @@ import { setupTest } from 'dummy/tests/helpers'; module('Unit | Controller | networks/index/network/orders', function (hooks) { setupTest(hooks); - // TODO: Replace this with your real tests. test('it exists', function (assert) { let controller = this.owner.lookup('controller:networks/index/network/orders'); assert.ok(controller); }); + + test('customer, pickup, dropoff and driver are identity cells typed for the shared descriptors', function (assert) { + const controller = this.owner.lookup('controller:networks/index/network/orders'); + const byId = Object.fromEntries(controller.columns.map((column) => [column.id, column])); + + for (const [id, resourceType] of [ + ['customer-name', 'customer'], + ['pickup-name', 'place'], + ['dropoff-name', 'place'], + ['driver-assigned', 'driver'], + ]) { + assert.strictEqual(byId[id].cellComponent, 'table/cell/identity', `${id} is an identity cell`); + assert.strictEqual(byId[id].resourceType, resourceType, `${id} is a ${resourceType}`); + assert.strictEqual(typeof byId[id].resourcePath, 'function', `${id} resolves its resource`); + } + + const driver = { id: 'driver_1' }; + assert.strictEqual(byId['driver-assigned'].resourcePath({ driver_assigned: driver }), driver); + assert.strictEqual(byId['driver-assigned'].resourcePath({ driver_name: 'Bob' }).name, 'Bob'); + }); }); diff --git a/tests/unit/controllers/orders/index-test.js b/tests/unit/controllers/orders/index-test.js index b471c95b..6c9ca5d4 100644 --- a/tests/unit/controllers/orders/index-test.js +++ b/tests/unit/controllers/orders/index-test.js @@ -1,5 +1,6 @@ import { module, test } from 'qunit'; import { setupTest } from 'dummy/tests/helpers'; +import Service from '@ember/service'; module('Unit | Controller | orders/index', function (hooks) { setupTest(hooks); @@ -17,4 +18,69 @@ module('Unit | Controller | orders/index', function (hooks) { assert.true(idColumn.sticky, 'id column is sticky'); assert.strictEqual(actionColumn.sticky, 'right', 'action column is sticky on the right'); }); + + test('customer, pickup, dropoff and driver are identity cells typed for the shared descriptors', function (assert) { + const controller = this.owner.lookup('controller:orders/index'); + const byId = Object.fromEntries(controller.columns.map((column) => [column.id, column])); + + assert.strictEqual(byId['customer-name'].cellComponent, 'table/cell/identity'); + assert.strictEqual(byId['customer-name'].resourceType, 'customer'); + assert.strictEqual(byId['pickup-name'].cellComponent, 'table/cell/identity'); + assert.strictEqual(byId['pickup-name'].resourceType, 'place'); + assert.strictEqual(byId['dropoff-name'].cellComponent, 'table/cell/identity'); + assert.strictEqual(byId['dropoff-name'].resourceType, 'place'); + assert.strictEqual(byId['driver-assigned'].cellComponent, 'table/cell/identity'); + assert.strictEqual(byId['driver-assigned'].resourceType, 'driver'); + assert.strictEqual(byId['driver-assigned'].emptyText, 'No driver assigned'); + }); + + test('identity cells read the loaded relation, or a stub named after the row', async function (assert) { + const controller = this.owner.lookup('controller:orders/index'); + const byId = Object.fromEntries(controller.columns.map((column) => [column.id, column])); + const customer = { id: 'contact_1', name: 'Ada' }; + const driver = { id: 'driver_1', name: 'Bob' }; + const pickup = { id: 'place_1', name: 'Depot' }; + + assert.strictEqual(byId['customer-name'].resourcePath({ customer }), customer); + assert.strictEqual(byId['driver-assigned'].resourcePath({ driver_assigned: driver }), driver); + assert.strictEqual(byId['pickup-name'].resourcePath({ payload: { pickup } }), pickup); + + const customerStub = byId['customer-name'].resourcePath({ customer_name: 'Ada', customer_type: 'fleet-ops:contact' }); + assert.strictEqual(customerStub.name, 'Ada'); + assert.strictEqual(customerStub.resourceType, 'fleet-ops:contact'); + assert.true(customerStub.isIdentityStub); + + const placeStub = byId['dropoff-name'].resourcePath({ dropoffName: '1 Main St' }); + assert.strictEqual(placeStub.name, '1 Main St'); + assert.strictEqual(placeStub.resourceType, 'place'); + + const found = { id: 'driver_9' }; + const findRecord = (modelName, id) => (modelName === 'driver' && id === 'driver_9' ? Promise.resolve(found) : Promise.resolve(null)); + controller.store = { findRecord }; + const driverStub = byId['driver-assigned'].resourcePath({ driver_name: 'Bob', driver_assigned_uuid: 'driver_9' }); + assert.strictEqual(driverStub.name, 'Bob'); + assert.strictEqual(await driverStub.loadResource(), found, 'the stub loads the driver by uuid on demand'); + assert.strictEqual(await byId['driver-assigned'].resourcePath({ driver_name: 'Bob' }).loadResource(), null, 'no uuid, nothing to load'); + + assert.strictEqual(byId['driver-assigned'].resourcePath({}), null, 'no driver at all renders the empty text'); + }); + + test('clicking a customer opens the storefront customer page, and a name-only stub opens nothing', function (assert) { + const transitions = []; + this.owner.register( + 'service:hostRouter', + class extends Service { + transitionTo(...args) { + transitions.push(args); + } + } + ); + const controller = this.owner.lookup('controller:orders/index'); + + controller.viewCustomer({ public_id: 'contact_1', name: 'Ada' }); + controller.viewCustomer({ name: 'Ada', isIdentityStub: true }); + controller.viewCustomer(null); + + assert.deepEqual(transitions, [['console.storefront.customers.index.view', 'contact_1']]); + }); });
{{format-currency order.meta.total order.meta.currency}}{{n-a order.customer.name}} {{#if order.meta.is_pickup}} {{t "storefront.component.widget.orders.pickup-order" }} {{else}} - {{n-a order.driver_assigned.name}} + {{/if}}