From f28878cd9a052b1e7a6c066ca870e639c2d73c3d Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Fri, 12 Jun 2026 18:29:23 +0800 Subject: [PATCH 1/3] latest changes --- addon/components/customer-panel.hbs | 83 +---- addon/components/customer-panel.js | 33 +- .../customer-panel/panel-header.hbs | 33 ++ .../components/customer-panel/panel-header.js | 3 + addon/components/store-selector.hbs | 50 +-- addon/components/store-selector.js | 193 +++++++++++ .../storefront/product/collection.hbs | 2 +- addon/controllers/application.js | 260 +++++++++++++- addon/controllers/catalogs/index.js | 3 + addon/controllers/customers/index.js | 15 +- addon/controllers/food-trucks/index.js | 3 + addon/controllers/orders/index.js | 4 +- addon/controllers/settings/gateways.js | 4 + addon/controllers/settings/index.js | 3 + addon/controllers/settings/notifications.js | 4 + addon/helpers/avatar-url.js | 9 - addon/routes/application.js | 5 + addon/routes/catalogs/index.js | 4 + addon/routes/food-trucks/index.js | 4 + addon/routes/settings/gateways.js | 8 +- addon/routes/settings/index.js | 4 + addon/routes/settings/notifications.js | 8 +- addon/services/storefront.js | 7 +- addon/styles/storefront-engine.css | 8 + addon/templates/application.hbs | 59 +--- addon/templates/customers/index.hbs | 41 +-- app/components/customer-panel/panel-header.js | 1 + app/helpers/avatar-url.js | 1 - composer.json | 2 +- extension.json | 2 +- package.json | 2 +- .../src/Http/Controllers/SearchController.php | 318 +++++++++++++++++ server/src/routes.php | 2 + server/tests/Feature.php | 25 ++ .../components/customer-panel-test.js | 102 +++++- .../components/store-selector-test.js | 114 ++++++- tests/integration/helpers/avatar-url-test.js | 17 - tests/unit/controllers/application-test.js | 323 +++++++++++++++++- .../unit/controllers/customers/index-test.js | 10 +- tests/unit/controllers/orders/index-test.js | 10 +- tests/unit/services/storefront-test.js | 64 +++- 41 files changed, 1593 insertions(+), 250 deletions(-) create mode 100644 addon/components/customer-panel/panel-header.hbs create mode 100644 addon/components/customer-panel/panel-header.js delete mode 100644 addon/helpers/avatar-url.js create mode 100644 app/components/customer-panel/panel-header.js delete mode 100644 app/helpers/avatar-url.js create mode 100644 server/src/Http/Controllers/SearchController.php delete mode 100644 tests/integration/helpers/avatar-url-test.js diff --git a/addon/components/customer-panel.hbs b/addon/components/customer-panel.hbs index 735c983..92a1b94 100644 --- a/addon/components/customer-panel.hbs +++ b/addon/components/customer-panel.hbs @@ -1,70 +1,17 @@ - - -
- -
-
-
-
-
-
-
- {{this.customer.name}} - - - -
-
-

{{this.customer.name}}

-
-
- {{smart-humanize this.customer.type}} -
-
-
-
-
-
- -
- -
-
- -
-
- -
-
-
- {{component this.tab.component customer=this.customer tabOptions=this.tab options=this.tab.componentParams}} -
-
-
\ No newline at end of file + + {{component activeTab.component customer=this.customer tabOptions=activeTab options=activeTab.componentParams}} + + diff --git a/addon/components/customer-panel.js b/addon/components/customer-panel.js index 976e5f8..47ffe29 100644 --- a/addon/components/customer-panel.js +++ b/addon/components/customer-panel.js @@ -3,6 +3,7 @@ import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; import { inject as service } from '@ember/service'; import { isArray } from '@ember/array'; +import { dasherize } from '@ember/string'; import CustomerPanelDetailsComponent from './customer-panel/details'; import CustomerPanelOrdersComponent from './customer-panel/orders'; import contextComponentCallback from '@fleetbase/ember-core/utils/context-component-callback'; @@ -99,10 +100,21 @@ export default class CustomerPanelComponent extends Component { ]; if (isArray(registeredTabs)) { - return [...defaultTabs, ...registeredTabs]; + return [...defaultTabs, ...registeredTabs].map((tab) => this.normalizeTab(tab)); } - return defaultTabs; + return defaultTabs.map((tab) => this.normalizeTab(tab)); + } + + get actionButtons() { + return [ + { + icon: 'pencil', + helpText: 'Edit customer', + onClick: this.onEdit, + permission: 'storefront update customer', + }, + ]; } /** * Sets the overlay context. @@ -123,8 +135,8 @@ export default class CustomerPanelComponent extends Component { * @action */ @action onTabChanged(tab) { - this.tab = this.getTabUsingSlug(tab); - contextComponentCallback(this, 'onTabChanged', tab); + this.tab = tab; + contextComponentCallback(this, 'onTabChanged', tab?.slug ?? tab?.id); } /** @@ -164,9 +176,20 @@ export default class CustomerPanelComponent extends Component { */ getTabUsingSlug(tabSlug) { if (tabSlug) { - return this.tabs.find(({ slug }) => slug === tabSlug); + return this.tabs.find(({ slug, id }) => slug === tabSlug || id === tabSlug); } return this.tabs[0]; } + + normalizeTab(tab) { + const id = tab.id ?? tab.slug ?? dasherize(tab.title ?? tab.text ?? tab.label ?? 'tab'); + + return { + ...tab, + id, + slug: tab.slug ?? id, + label: tab.label ?? tab.title ?? tab.text, + }; + } } diff --git a/addon/components/customer-panel/panel-header.hbs b/addon/components/customer-panel/panel-header.hbs new file mode 100644 index 0000000..65036b9 --- /dev/null +++ b/addon/components/customer-panel/panel-header.hbs @@ -0,0 +1,33 @@ +
+
+
+
+ {{@resource.name}} + + + +
+
+

{{@resource.name}}

+
+ + +
+
+
+
+ + {{yield}} + +
+
+
diff --git a/addon/components/customer-panel/panel-header.js b/addon/components/customer-panel/panel-header.js new file mode 100644 index 0000000..5142682 --- /dev/null +++ b/addon/components/customer-panel/panel-header.js @@ -0,0 +1,3 @@ +import Component from '@glimmer/component'; + +export default class CustomerPanelPanelHeaderComponent extends Component {} diff --git a/addon/components/store-selector.hbs b/addon/components/store-selector.hbs index fbb9527..4b349ea 100644 --- a/addon/components/store-selector.hbs +++ b/addon/components/store-selector.hbs @@ -1,34 +1,18 @@ {{#if @activeStore}} - - - -{{/if}} \ No newline at end of file +
+
+{{else if (has-block)}} + {{yield}} +{{/if}} diff --git a/addon/components/store-selector.js b/addon/components/store-selector.js index 158be1d..bed362e 100644 --- a/addon/components/store-selector.js +++ b/addon/components/store-selector.js @@ -1,13 +1,204 @@ import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; export default class StoreSelectorComponent extends Component { + @service intl; + @tracked isOpen = false; + triggerElement; + menuElement; + teardownListeners = []; + animationFrame; + + get stores() { + return Array.from(this.args.stores ?? []); + } + + get hasStores() { + return this.stores.length > 0; + } + + willDestroy() { + super.willDestroy(...arguments); + this.close(); + } + + @action setupTrigger(element) { + this.triggerElement = element; + } + + @action toggle(event) { + event?.preventDefault?.(); + event?.stopPropagation?.(); + + if (this.isOpen) { + this.close(); + } else { + this.open(); + } + } + + open() { + if (!this.triggerElement || this.isOpen) { + return; + } + + this.isOpen = true; + this.menuElement = this.createMenuElement(); + document.body.appendChild(this.menuElement); + this.positionMenu(); + this.addListeners(); + } + + @action close() { + if (this.animationFrame) { + cancelAnimationFrame(this.animationFrame); + this.animationFrame = undefined; + } + + this.removeListeners(); + this.menuElement?.remove(); + this.menuElement = undefined; + this.isOpen = false; + } + + createMenuElement() { + const menu = document.createElement('div'); + menu.setAttribute('role', 'menu'); + menu.className = 'store-selector-dropdown-menu next-dd-menu py-1'; + menu.style.position = 'fixed'; + menu.style.zIndex = '900'; + menu.style.margin = '0'; + menu.style.height = 'auto'; + menu.style.minHeight = '0'; + menu.style.maxHeight = 'calc(100vh - 16px)'; + menu.style.overflowX = 'hidden'; + menu.style.overflowY = 'visible'; + + const storeList = document.createElement('div'); + storeList.setAttribute('role', 'group'); + storeList.className = 'px-1'; + storeList.style.maxHeight = '18rem'; + storeList.style.overflowY = 'auto'; + + if (this.hasStores) { + this.stores.forEach((store) => { + storeList.appendChild(this.createMenuItem(store?.name || '-', () => this.onSwitchStore(store))); + }); + } else { + const emptyItem = document.createElement('div'); + emptyItem.className = 'next-dd-item'; + emptyItem.setAttribute('role', 'menuitem'); + emptyItem.textContent = this.intl.t('storefront.component.store-selector.no-stores'); + storeList.appendChild(emptyItem); + } + + const footer = document.createElement('div'); + footer.className = 'px-1'; + + const separator = document.createElement('div'); + separator.className = 'next-dd-menu-seperator'; + + const footerGroup = document.createElement('div'); + footerGroup.setAttribute('role', 'group'); + footerGroup.className = 'px-1'; + footerGroup.appendChild(this.createMenuItem(this.intl.t('storefront.component.store-selector.create-storefront'), () => this.onCreateStore())); + + footer.append(separator, footerGroup); + menu.append(storeList, footer); + + return menu; + } + + createMenuItem(label, callback) { + const item = document.createElement('a'); + item.href = 'javascript:;'; + item.className = 'next-dd-item'; + item.setAttribute('role', 'menuitem'); + item.textContent = label; + item.addEventListener('click', (event) => { + event.preventDefault(); + event.stopPropagation(); + callback(); + }); + + return item; + } + + addListeners() { + this.addManagedListener(document, 'mousedown', this.handleDocumentMouseDown, true); + this.addManagedListener(document, 'keydown', this.handleDocumentKeydown); + this.addManagedListener(window, 'resize', this.schedulePositionMenu); + this.addManagedListener(window, 'scroll', this.schedulePositionMenu, true); + } + + addManagedListener(target, eventName, handler, options = false) { + target.addEventListener(eventName, handler, options); + this.teardownListeners.push(() => target.removeEventListener(eventName, handler, options)); + } + + removeListeners() { + this.teardownListeners.forEach((teardown) => teardown()); + this.teardownListeners = []; + } + + handleDocumentMouseDown = (event) => { + if (this.triggerElement?.contains(event.target) || this.menuElement?.contains(event.target)) { + return; + } + + this.close(); + }; + + handleDocumentKeydown = (event) => { + if (event.key === 'Escape') { + this.close(); + } + }; + + schedulePositionMenu = () => { + if (this.animationFrame) { + cancelAnimationFrame(this.animationFrame); + } + + this.animationFrame = requestAnimationFrame(() => { + this.animationFrame = undefined; + this.positionMenu(); + }); + }; + + positionMenu = () => { + if (!this.triggerElement || !this.menuElement) { + return; + } + + const rect = this.triggerElement.getBoundingClientRect(); + const viewportPadding = 8; + const width = Math.max(rect.width, 220); + const maxLeft = window.innerWidth - width - viewportPadding; + const left = Math.max(viewportPadding, Math.min(rect.left, maxLeft)); + let top = rect.bottom + 4; + + this.menuElement.style.width = `${width}px`; + + const menuHeight = this.menuElement.offsetHeight; + if (top + menuHeight > window.innerHeight - viewportPadding && rect.top - menuHeight - 4 > viewportPadding) { + top = rect.top - menuHeight - 4; + } + + this.menuElement.style.left = `${left}px`; + this.menuElement.style.top = `${Math.max(viewportPadding, top)}px`; + }; + @action onSwitchStore(store) { const { onSwitchStore } = this.args; if (typeof onSwitchStore === 'function') { onSwitchStore(store); } + + this.close(); } @action onCreateStore() { @@ -16,5 +207,7 @@ export default class StoreSelectorComponent extends Component { if (typeof onCreateStore === 'function') { onCreateStore(); } + + this.close(); } } diff --git a/addon/components/storefront/product/collection.hbs b/addon/components/storefront/product/collection.hbs index b7d6948..c5f4dfe 100644 --- a/addon/components/storefront/product/collection.hbs +++ b/addon/components/storefront/product/collection.hbs @@ -1,5 +1,5 @@
-
+
{{#if this.hasProducts}} {{#if this.isListView}}
diff --git a/addon/controllers/application.js b/addon/controllers/application.js index c41332c..e8e43b3 100644 --- a/addon/controllers/application.js +++ b/addon/controllers/application.js @@ -2,12 +2,232 @@ import Controller from '@ember/controller'; import { inject as service } from '@ember/service'; import { action } from '@ember/object'; import { alias } from '@ember/object/computed'; +import { tracked } from '@glimmer/tracking'; export default class ApplicationController extends Controller { @service storefront; @service hostRouter; @service loader; + @service fetch; + @service intl; + @service abilities; + @service store; @alias('storefront.activeStore') activeStore; + @tracked productCategories = []; + categoryLoadStoreUuid; + + get navigationItems() { + const hasActiveStore = Boolean(this.activeStore?.id); + + return [ + { + label: this.intl.t('storefront.sidebar.dashboard'), + description: 'Storefront dashboard and sales overview.', + icon: 'home', + route: 'console.storefront.home', + keywords: ['dashboard', 'overview', 'metrics', 'sales'], + }, + { + label: this.intl.t('storefront.sidebar.products'), + description: 'Manage products, categories, variants, and addons.', + icon: 'box', + permission: 'storefront list product', + visible: this.can('storefront see product'), + disabled: !hasActiveStore, + children: [ + { + label: 'All Products', + description: 'Browse and manage product inventory.', + icon: 'box', + route: 'console.storefront.products', + keywords: ['inventory', 'items', 'sku', 'create product', 'add product'], + }, + ...this.productCategoryItems, + ], + }, + { + label: this.intl.t('storefront.sidebar.catalogs'), + description: 'Create and manage storefront catalogs.', + icon: 'book-open', + route: 'console.storefront.catalogs', + permission: 'storefront list catalog', + visible: this.can('storefront see catalog'), + disabled: !hasActiveStore, + keywords: ['menus', 'catalogs', 'published catalog'], + }, + { + label: this.intl.t('storefront.sidebar.customers'), + description: 'Review storefront customers and order history.', + icon: 'users', + route: 'console.storefront.customers', + permission: 'storefront list user', + visible: this.can('storefront see user'), + disabled: !hasActiveStore, + keywords: ['contacts', 'buyers', 'users'], + }, + { + label: this.intl.t('storefront.sidebar.orders'), + description: 'Manage incoming storefront orders.', + icon: 'file-invoice-dollar', + route: 'console.storefront.orders', + permission: 'storefront list order', + visible: this.can('storefront see order'), + disabled: !hasActiveStore, + keywords: ['fulfillment', 'deliveries', 'checkout orders'], + }, + { + label: this.intl.t('storefront.sidebar.networks'), + description: 'Manage marketplace networks and participating stores.', + icon: 'network-wired', + route: 'console.storefront.networks', + permission: 'storefront list network', + visible: this.can('storefront see network'), + disabled: !hasActiveStore, + keywords: ['marketplace', 'network stores', 'partners'], + }, + { + label: this.intl.t('storefront.sidebar.food-trucks'), + description: 'Manage food truck locations and availability.', + icon: 'truck', + route: 'console.storefront.food-trucks', + permission: 'storefront list food-truck', + visible: this.can('storefront see food-truck'), + disabled: !hasActiveStore, + keywords: ['vehicles', 'mobile store', 'truck'], + }, + { + label: this.intl.t('storefront.sidebar.promotions'), + description: 'Send promotional notifications.', + icon: 'bullhorn', + permission: 'storefront view promotions', + visible: this.can('storefront see promotions'), + disabled: !hasActiveStore, + children: [ + { + label: 'Push Notifications', + description: 'Send push notifications to storefront customers.', + icon: 'bell', + route: 'console.storefront.promotions.push-notifications', + keywords: ['marketing', 'broadcast', 'campaigns'], + }, + ], + }, + { + label: this.intl.t('storefront.sidebar.settings'), + description: 'Configure storefront settings, locations, gateways, and notifications.', + icon: 'cogs', + permission: 'storefront view settings', + visible: this.can('storefront see settings'), + disabled: !hasActiveStore, + children: [ + { + label: this.intl.t('storefront.common.general'), + description: 'General storefront settings.', + icon: 'cog', + route: 'console.storefront.settings.index', + keywords: ['settings', 'general'], + }, + { + label: this.intl.t('storefront.common.location'), + description: 'Storefront pickup and service locations.', + icon: 'map-marker-alt', + route: 'console.storefront.settings.locations', + keywords: ['locations', 'places', 'pickup'], + }, + { + label: this.intl.t('storefront.common.gateways'), + description: 'Payment gateway settings.', + icon: 'cash-register', + route: 'console.storefront.settings.gateways', + keywords: ['payments', 'stripe', 'qpay', 'checkout'], + }, + { + label: this.intl.t('storefront.common.api'), + description: 'Storefront API settings.', + icon: 'code', + route: 'console.storefront.settings.api', + keywords: ['api', 'developer'], + }, + { + label: this.intl.t('storefront.common.notification'), + description: 'Notification channel settings.', + icon: 'bell-concierge', + route: 'console.storefront.settings.notifications', + keywords: ['push notifications', 'apn', 'fcm'], + }, + ], + }, + { + label: this.intl.t('storefront.sidebar.launch-app'), + description: 'Open the storefront application repository.', + icon: 'rocket', + url: 'https://github.com/fleetbase/storefront-app', + target: '_github', + keywords: ['launch app', 'storefront app', 'github'], + }, + ]; + } + + get activeStoreUuid() { + return this.activeStore?.id; + } + + get productCategoryItems() { + return this.productCategories + .filter((category) => category?.slug) + .map((category) => { + const description = category.description || category.slug; + + return { + label: category.name, + description, + icon: 'folder', + route: 'console.storefront.products.index.category', + models: [category.slug], + keywords: ['category', 'collection', category.slug, category.name, description].filter(Boolean), + }; + }); + } + + can(permission) { + try { + return this.abilities.can(permission); + } catch (_) { + return true; + } + } + + async loadProductCategories(storeUuid = this.activeStoreUuid) { + const ownerUuid = storeUuid; + this.categoryLoadStoreUuid = ownerUuid; + + if (!ownerUuid) { + this.productCategories = []; + return []; + } + + try { + const categories = await this.store.query('category', { + for: 'storefront_product', + owner_uuid: ownerUuid, + limit: -1, + }); + + if (this.categoryLoadStoreUuid !== ownerUuid) { + return this.productCategories; + } + + this.productCategories = categories?.toArray?.() ?? Array.from(categories ?? []); + return this.productCategories; + } catch (_) { + if (this.categoryLoadStoreUuid !== ownerUuid) { + return this.productCategories; + } + + this.productCategories = []; + return []; + } + } @action createNewStorefront() { return this.storefront.createNewStorefront({ @@ -16,6 +236,7 @@ export default class ApplicationController extends Controller { this.hostRouter.refresh().then(() => { this.notifyPropertyChange('activeStore'); + this.loadProductCategories(this.activeStoreUuid); this.loader.removeLoader(loader); }); }, @@ -25,9 +246,40 @@ export default class ApplicationController extends Controller { @action switchActiveStore(store) { const loader = this.loader.show({ loadingMessage: `Switching Storefront to ${store.name}...` }); this.storefront.setActiveStorefront(store); - this.hostRouter.refresh().then(() => { - this.notifyPropertyChange('activeStore'); - this.loader.removeLoader(loader); - }); + this.productCategories = []; + return this.hostRouter + .refresh() + .then(() => { + this.notifyPropertyChange('activeStore'); + return this.loadProductCategories(store.id); + }) + .finally(() => { + this.loader.removeLoader(loader); + }); + } + + @action + async searchNavigation({ query, limit = 12 }) { + const trimmedQuery = query?.trim(); + + if (!trimmedQuery || !this.activeStore?.public_id) { + return []; + } + + try { + const response = await this.fetch.get( + 'search', + { + query: trimmedQuery, + limit, + storefront: this.activeStore.public_id, + }, + { namespace: 'storefront/int/v1' } + ); + + return response.results ?? []; + } catch (_) { + return []; + } } } diff --git a/addon/controllers/catalogs/index.js b/addon/controllers/catalogs/index.js index 64ab8d1..67de1bf 100644 --- a/addon/controllers/catalogs/index.js +++ b/addon/controllers/catalogs/index.js @@ -11,6 +11,9 @@ export default class CatalogsIndexController extends Controller { @service notifications; @service crud; @service hostRouter; + queryParams = ['query']; + + @tracked query; @tracked statusOptions = ['draft', 'published']; @action createCatalog() { diff --git a/addon/controllers/customers/index.js b/addon/controllers/customers/index.js index feb58f2..d508964 100644 --- a/addon/controllers/customers/index.js +++ b/addon/controllers/customers/index.js @@ -118,6 +118,7 @@ export default class CustomersIndexController extends BaseController { */ @tracked columns = [ { + sticky: true, label: this.intl.t('storefront.common.name'), valuePath: 'name', width: '15%', @@ -222,7 +223,8 @@ export default class CustomersIndexController extends BaseController { ddMenuLabel: this.intl.t('storefront.customers.index.vendor-action'), cellClassNames: 'overflow-visible', wrapperClass: 'flex items-center justify-end mx-2', - width: '10%', + sticky: 'right', + width: 60, actions: [ { label: this.intl.t('storefront.customers.index.view-customer-details'), @@ -248,6 +250,17 @@ export default class CustomersIndexController extends BaseController { }, ]; + get actionButtons() { + return [ + { + icon: 'long-arrow-up', + iconClass: 'rotate-icon-45', + text: this.intl.t('storefront.common.export'), + permission: 'storefront export customer', + }, + ]; + } + @action viewCustomer(customer) { return this.transitionToRoute('customers.index.view', customer); } diff --git a/addon/controllers/food-trucks/index.js b/addon/controllers/food-trucks/index.js index e40a2f6..d0fe2c6 100644 --- a/addon/controllers/food-trucks/index.js +++ b/addon/controllers/food-trucks/index.js @@ -11,6 +11,9 @@ export default class FoodTrucksIndexController extends Controller { @service notifications; @service crud; @service hostRouter; + queryParams = ['query']; + + @tracked query; @tracked statusOptions = ['active', 'inactive']; @action createFoodTruck() { diff --git a/addon/controllers/orders/index.js b/addon/controllers/orders/index.js index fc85da5..8bf4552 100644 --- a/addon/controllers/orders/index.js +++ b/addon/controllers/orders/index.js @@ -54,6 +54,7 @@ export default class OrdersIndexController extends BaseController { @tracked columns = [ { + sticky: true, label: this.intl.t('storefront.common.id'), valuePath: 'public_id', width: '130px', @@ -248,7 +249,8 @@ export default class OrdersIndexController extends BaseController { ddMenuLabel: 'Order Actions', cellClassNames: 'overflow-visible', wrapperClass: 'flex items-center justify-end mx-2', - width: '90px', + sticky: 'right', + width: 60, actions: [ { label: this.intl.t('fleet-ops.operations.orders.index.view-order'), diff --git a/addon/controllers/settings/gateways.js b/addon/controllers/settings/gateways.js index 661f0d1..1e69358 100644 --- a/addon/controllers/settings/gateways.js +++ b/addon/controllers/settings/gateways.js @@ -3,6 +3,7 @@ import { inject as service } from '@ember/service'; import { alias } from '@ember/object/computed'; import { action, set } from '@ember/object'; import { capitalize } from '@ember/string'; +import { tracked } from '@glimmer/tracking'; import getGatewaySchemas from '../../utils/get-gateway-schemas'; export default class SettingsGatewaysController extends Controller { @@ -14,6 +15,9 @@ export default class SettingsGatewaysController extends Controller { @service crud; @service storefront; @alias('storefront.activeStore') activeStore; + queryParams = ['query']; + + @tracked query; @action createGateway() { const gateway = this.store.createRecord('gateway', { diff --git a/addon/controllers/settings/index.js b/addon/controllers/settings/index.js index c144e28..871606b 100644 --- a/addon/controllers/settings/index.js +++ b/addon/controllers/settings/index.js @@ -13,6 +13,9 @@ export default class SettingsIndexController extends Controller { @service intl; @alias('storefront.activeStore') activeStore; + queryParams = ['query']; + + @tracked query; @tracked podMethods = getPodMethods(); @tracked isLoading = false; @tracked uploadQueue = []; diff --git a/addon/controllers/settings/notifications.js b/addon/controllers/settings/notifications.js index 2159556..6579fac 100644 --- a/addon/controllers/settings/notifications.js +++ b/addon/controllers/settings/notifications.js @@ -3,6 +3,7 @@ import { inject as service } from '@ember/service'; import { alias } from '@ember/object/computed'; import { action, set } from '@ember/object'; import { capitalize } from '@ember/string'; +import { tracked } from '@glimmer/tracking'; import getNotificationSchemas from '../../utils/get-notification-schemas'; export default class SettingsNotificationsController extends Controller { @@ -14,6 +15,9 @@ export default class SettingsNotificationsController extends Controller { @service storefront; @service hostRouter; @alias('storefront.activeStore') activeStore; + queryParams = ['query']; + + @tracked query; @action createChannel() { const channel = this.store.createRecord('notification-channel', { diff --git a/addon/helpers/avatar-url.js b/addon/helpers/avatar-url.js deleted file mode 100644 index bb394fb..0000000 --- a/addon/helpers/avatar-url.js +++ /dev/null @@ -1,9 +0,0 @@ -import { helper } from '@ember/component/helper'; - -export default helper(function avatarUrl([url, defaultUrl = 'https://s3.ap-southeast-1.amazonaws.com/flb-assets/static/no-avatar.png']) { - if (typeof url === 'string') { - return url; - } - - return defaultUrl; -}); diff --git a/addon/routes/application.js b/addon/routes/application.js index e014893..5aca008 100644 --- a/addon/routes/application.js +++ b/addon/routes/application.js @@ -45,6 +45,11 @@ export default class ApplicationRoute extends Route { return this.store.query('store', { limit: 300, sort: '-updated_at' }); } + setupController(controller) { + super.setupController(...arguments); + controller.loadProductCategories(); + } + afterModel() { this.storefront.listenForIncomingOrders(); } diff --git a/addon/routes/catalogs/index.js b/addon/routes/catalogs/index.js index c3f5a9c..bd46637 100644 --- a/addon/routes/catalogs/index.js +++ b/addon/routes/catalogs/index.js @@ -9,6 +9,10 @@ export default class CatalogsIndexRoute extends Route { @service hostRouter; @service notifications; + queryParams = { + query: { refreshModel: true }, + }; + beforeModel() { if (this.abilities.cannot('storefront list catalog')) { this.notifications.warning(this.intl.t('common.unauthorized-access')); diff --git a/addon/routes/food-trucks/index.js b/addon/routes/food-trucks/index.js index ff184b6..800d7f8 100644 --- a/addon/routes/food-trucks/index.js +++ b/addon/routes/food-trucks/index.js @@ -9,6 +9,10 @@ export default class FoodTrucksIndexRoute extends Route { @service hostRouter; @service notifications; + queryParams = { + query: { refreshModel: true }, + }; + beforeModel() { if (this.abilities.cannot('storefront list food-truck')) { this.notifications.warning(this.intl.t('common.unauthorized-access')); diff --git a/addon/routes/settings/gateways.js b/addon/routes/settings/gateways.js index dc65d3e..1722e94 100644 --- a/addon/routes/settings/gateways.js +++ b/addon/routes/settings/gateways.js @@ -5,7 +5,11 @@ export default class SettingsGatewaysRoute extends Route { @service store; @service storefront; - model() { - return this.store.query('gateway', { owner_uuid: this.storefront?.activeStore?.id }); + queryParams = { + query: { refreshModel: true }, + }; + + model(params) { + return this.store.query('gateway', { ...params, owner_uuid: this.storefront?.activeStore?.id }); } } diff --git a/addon/routes/settings/index.js b/addon/routes/settings/index.js index 7ecbceb..c4d15fd 100644 --- a/addon/routes/settings/index.js +++ b/addon/routes/settings/index.js @@ -10,6 +10,10 @@ export default class SettingsIndexRoute extends Route { @service hostRouter; @service notifications; + queryParams = { + query: { refreshModel: false }, + }; + beforeModel() { if (this.abilities.cannot('storefront view settings')) { this.notifications.warning(this.intl.t('common.unauthorized-access')); diff --git a/addon/routes/settings/notifications.js b/addon/routes/settings/notifications.js index 2f492d4..b3e452a 100644 --- a/addon/routes/settings/notifications.js +++ b/addon/routes/settings/notifications.js @@ -5,7 +5,11 @@ export default class SettingsNotificationsRoute extends Route { @service store; @service storefront; - model() { - return this.store.query('notification-channel', { owner_uuid: this.storefront?.activeStore?.id }); + queryParams = { + query: { refreshModel: true }, + }; + + model(params) { + return this.store.query('notification-channel', { ...params, owner_uuid: this.storefront?.activeStore?.id }); } } diff --git a/addon/services/storefront.js b/addon/services/storefront.js index 0827ae4..9f2f8ea 100644 --- a/addon/services/storefront.js +++ b/addon/services/storefront.js @@ -3,6 +3,7 @@ import Evented from '@ember/object/evented'; import { inject as service } from '@ember/service'; import { get } from '@ember/object'; import { getOwner } from '@ember/application'; +import { tracked } from '@glimmer/tracking'; /** * Service to manage storefront operations. @@ -16,6 +17,7 @@ export default class StorefrontService extends Service.extend(Evented) { @service modalsManager; @service abilities; @service socket; + @tracked activeStoreId; get hostRouter() { const owner = getOwner(this); @@ -37,6 +39,7 @@ export default class StorefrontService extends Service.extend(Evented) { */ setActiveStorefront(store) { this.currentUser.setOption('activeStorefront', store.id); + this.activeStoreId = store.id; this.trigger('storefront.changed', store); } @@ -62,13 +65,14 @@ export default class StorefrontService extends Service.extend(Evented) { * @returns {Object|null} The active store object or null if not found. */ findActiveStore() { - const activeStoreId = this.currentUser.getOption('activeStorefront'); + const activeStoreId = this.activeStoreId ?? this.currentUser.getOption('activeStorefront'); if (!activeStoreId) { const stores = this.store.peekAll('store'); if (stores.firstObject) { this.currentUser.setOption('activeStorefront', stores.firstObject.id); + this.activeStoreId = stores.firstObject.id; } return stores.firstObject; @@ -78,6 +82,7 @@ export default class StorefrontService extends Service.extend(Evented) { if (!activeStore) { this.currentUser.setOption('activeStorefront', undefined); + this.activeStoreId = undefined; return this.findActiveStore(); } diff --git a/addon/styles/storefront-engine.css b/addon/styles/storefront-engine.css index 61f4766..1b4350c 100644 --- a/addon/styles/storefront-engine.css +++ b/addon/styles/storefront-engine.css @@ -290,6 +290,10 @@ body[data-theme='dark'] .storefront-product-view-toggle { padding: 0.85rem; } +.storefront-product-collection.is-empty { + grid-template-columns: auto; +} + .storefront-product-collection.is-list { display: block; height: 100%; @@ -300,6 +304,10 @@ body[data-theme='dark'] .storefront-product-view-toggle { overflow: auto; } +.storefront-product-collection.is-empty.is-list { + padding: 0.85rem; +} + .storefront-product-table { min-width: 960px; min-height: 100%; diff --git a/addon/templates/application.hbs b/addon/templates/application.hbs index 57c37e3..3d592ac 100644 --- a/addon/templates/application.hbs +++ b/addon/templates/application.hbs @@ -1,61 +1,6 @@ - - - {{t "storefront.sidebar.dashboard"}} - {{t "storefront.sidebar.products"}} - {{t "storefront.sidebar.catalogs"}} - {{t - "storefront.sidebar.customers" - }} - {{t "storefront.sidebar.orders"}} - {{t "storefront.sidebar.networks"}} - {{t "storefront.sidebar.food-trucks"}} - {{t "storefront.sidebar.promotions"}} - {{t "storefront.sidebar.settings"}} - - {{t "storefront.sidebar.launch-app"}} + + diff --git a/addon/templates/customers/index.hbs b/addon/templates/customers/index.hbs index 7410540..a61619e 100644 --- a/addon/templates/customers/index.hbs +++ b/addon/templates/customers/index.hbs @@ -1,28 +1,15 @@ - - - -