From c1f4b13052c7ed7bb32eb594cfa4aa339b79cb77 Mon Sep 17 00:00:00 2001 From: Brian Hanson Date: Wed, 7 Oct 2026 13:40:01 -0500 Subject: [PATCH 1/7] Render product sources in the secondary nav --- resources/js/pages/products/Index.vue | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/resources/js/pages/products/Index.vue b/resources/js/pages/products/Index.vue index 7354087d4a..9ac9f66b78 100644 --- a/resources/js/pages/products/Index.vue +++ b/resources/js/pages/products/Index.vue @@ -2,6 +2,8 @@ import { appendIndexQuery, ElementIndexPage, + useAppLayout, + useCustomizeSources, type ElementIndexRoute, } from '@craftcms/cms/elements'; import NewProductButton, { @@ -14,8 +16,26 @@ creatableProductTypes: CreatableProductType[]; newProductLabel: string; newProductMenuLabel: string; + /** The product types, as the index's own view model described them. */ + sourceNavItems?: Array; + elementType?: string | null; + page?: string | null; + source?: {key?: string | null} | null; }>(); + const subnavActions = useCustomizeSources(() => ({ + elementType: props.elementType, + page: props.page, + sourceKey: props.source?.key, + })); + + // Products have no nav section of their own to hang their types off, so the + // index puts them in the shell's secondary nav itself. + useAppLayout(() => ({ + subnav: props.sourceNavItems ?? [], + subnavActions: subnavActions.value, + })); + // `indexUrl` carries the `site` param `Url::cpUrl()` adds, which the index // query is merged over. function indexUrlWith(path: string, query = {}): string { From b664e97e16ad34a9793dc7d78683d6b97ded7d49 Mon Sep 17 00:00:00 2001 From: Brian Hanson Date: Wed, 7 Oct 2026 14:06:53 -0500 Subject: [PATCH 2/7] Sources subnav --- src/Plugin.php | 31 ++++++++++--------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/src/Plugin.php b/src/Plugin.php index 4dd152661e..4d7b3240ef 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -442,30 +442,19 @@ public function getCpNavItem(): NavItem|array|null $item->add(new NavItem()->label(t('Orders', category: 'commerce'))->url('commerce/orders')); } - $viewableProductTypeIds = app(ProductTypes::class)->getViewableProductTypeIds(true); - - $productNavItems = []; - - if (count($viewableProductTypeIds) > 1) { - $productNavItems = app(Navigation::class)->sourceSubnav(Product::class, 'commerce/products'); - } elseif ($viewableProductTypeIds) { - $productNavItems[] = new NavItem()->label(t('Products', category: 'commerce'))->url('commerce/products'); + /** + * One entry, whatever the product types are. The index puts them in the + * secondary nav itself, so listing them here as well would say the same + * thing twice. + */ + if (app(ProductTypes::class)->getViewableProductTypeIds(true)) { + $item->add(new NavItem()->label(t('Products', category: 'commerce'))->url('commerce/products')); } if (currentUser()?->can('commerce-manageDonationSettings')) { - $donationsNavItem = new NavItem()->label(t('Donations', category: 'commerce'))->url('commerce/donations'); - $lastProductNavItem = end($productNavItems); - - // Listed directly beneath the product types, under their heading when they have one. - if ($lastProductNavItem instanceof NavItem && $lastProductNavItem->group && is_array($lastProductNavItem->subnav)) { - $lastProductNavItem->add($donationsNavItem); - } else { - $productNavItems[] = $donationsNavItem; - } - } - - foreach ($productNavItems as $productNavItem) { - $item->add($productNavItem); + $item->add( + new NavItem()->label(t('Donations', category: 'commerce'))->url('commerce/donations') + ); } $inventoryItems = []; From a938cda028814640dbf039e16fc73775a1355d7b Mon Sep 17 00:00:00 2001 From: Brian Hanson Date: Wed, 7 Oct 2026 14:14:56 -0500 Subject: [PATCH 3/7] Remove unufsed import --- src/Plugin.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Plugin.php b/src/Plugin.php index 4d7b3240ef..7c104cbdf3 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -12,7 +12,6 @@ use CraftCms\Cms\Auth\Events\ElementAuthorizing; use CraftCms\Cms\Cms; use CraftCms\Cms\Cp\Data\NavItem; -use CraftCms\Cms\Cp\Navigation; use CraftCms\Cms\Cp\Settings as CpSettings; use CraftCms\Cms\Element\Events\DefineDeletionBlockers; use CraftCms\Cms\Element\Events\ElementSaved; From 1c82e081bd0d3ecd37b1df73448fd4915eb56456 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Wed, 7 Oct 2026 21:29:14 +0100 Subject: [PATCH 4/7] Remove extra comments --- resources/js/pages/products/Index.vue | 3 --- src/Plugin.php | 5 ----- 2 files changed, 8 deletions(-) diff --git a/resources/js/pages/products/Index.vue b/resources/js/pages/products/Index.vue index 9ac9f66b78..aae8016a3e 100644 --- a/resources/js/pages/products/Index.vue +++ b/resources/js/pages/products/Index.vue @@ -16,7 +16,6 @@ creatableProductTypes: CreatableProductType[]; newProductLabel: string; newProductMenuLabel: string; - /** The product types, as the index's own view model described them. */ sourceNavItems?: Array; elementType?: string | null; page?: string | null; @@ -29,8 +28,6 @@ sourceKey: props.source?.key, })); - // Products have no nav section of their own to hang their types off, so the - // index puts them in the shell's secondary nav itself. useAppLayout(() => ({ subnav: props.sourceNavItems ?? [], subnavActions: subnavActions.value, diff --git a/src/Plugin.php b/src/Plugin.php index 7c104cbdf3..2797426739 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -441,11 +441,6 @@ public function getCpNavItem(): NavItem|array|null $item->add(new NavItem()->label(t('Orders', category: 'commerce'))->url('commerce/orders')); } - /** - * One entry, whatever the product types are. The index puts them in the - * secondary nav itself, so listing them here as well would say the same - * thing twice. - */ if (app(ProductTypes::class)->getViewableProductTypeIds(true)) { $item->add(new NavItem()->label(t('Products', category: 'commerce'))->url('commerce/products')); } From 7a78447d7c1ad4e7adea5cee34de3ee3bf387762 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Wed, 7 Oct 2026 22:07:13 +0100 Subject: [PATCH 5/7] Implement subnav for inventory transfers element index --- .../js/pages/inventory/transfers/Index.vue | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/resources/js/pages/inventory/transfers/Index.vue b/resources/js/pages/inventory/transfers/Index.vue index e6b74fe889..03cc2b3e8c 100644 --- a/resources/js/pages/inventory/transfers/Index.vue +++ b/resources/js/pages/inventory/transfers/Index.vue @@ -3,6 +3,8 @@ appendIndexQuery, CpButtonLink, ElementIndexPage, + useAppLayout, + useCustomizeSources, type ElementIndexRoute, } from '@craftcms/cms/elements'; @@ -10,8 +12,23 @@ indexUrl: string; newTransferUrl: string | null; newTransferLabel: string; + sourceNavItems?: Array; + elementType?: string | null; + page?: string | null; + source?: {key?: string | null} | null; }>(); + const subnavActions = useCustomizeSources(() => ({ + elementType: props.elementType, + page: props.page, + sourceKey: props.source?.key, + })); + + useAppLayout(() => ({ + subnav: props.sourceNavItems ?? [], + subnavActions: subnavActions.value, + })); + // `indexUrl` carries the `site` param `Url::cpUrl()` adds, which the index // query is merged over. const route: ElementIndexRoute = { From 2e00833f6211949ad68280dc99fd5d5743dc3898 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Fri, 9 Oct 2026 17:08:10 +0100 Subject: [PATCH 6/7] Fix phpstan / CI errors --- composer.lock | 30 +++++++------- resources/js/cms-elements.d.ts | 40 +++++++++++++++++-- .../js/pages/inventory/transfers/Index.vue | 3 +- resources/js/pages/products/Index.vue | 3 +- src/CatalogPricing/CatalogPricing.php | 8 +--- src/CatalogPricing/CatalogPricingRules.php | 4 +- .../Settings/LineItemStatusesController.php | 4 +- .../Settings/OrderStatusesController.php | 4 +- .../Controllers/Settings/StoresController.php | 2 +- .../StoreManagement/DiscountsController.php | 2 +- .../PaymentCurrenciesController.php | 10 ++--- .../ShippingRulesController.php | 7 ---- src/Plugin.php | 2 +- src/Promotion/Discounts.php | 4 +- .../Controllers/ProductsControllerTest.php | 5 ++- .../ShippingRulesControllerTest.php | 2 +- vite.config.mts | 2 + 17 files changed, 80 insertions(+), 52 deletions(-) diff --git a/composer.lock b/composer.lock index 8f43f70bc2..5066e9b3c5 100644 --- a/composer.lock +++ b/composer.lock @@ -417,12 +417,12 @@ "source": { "type": "git", "url": "https://github.com/craftcms/cms.git", - "reference": "2fa1cdc38bf0d3adf8d51bac1b617fe45a39d369" + "reference": "ae2fe8942244ea128b788f286923a849404a72cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/craftcms/cms/zipball/2fa1cdc38bf0d3adf8d51bac1b617fe45a39d369", - "reference": "2fa1cdc38bf0d3adf8d51bac1b617fe45a39d369", + "url": "https://api.github.com/repos/craftcms/cms/zipball/ae2fe8942244ea128b788f286923a849404a72cb", + "reference": "ae2fe8942244ea128b788f286923a849404a72cb", "shasum": "" }, "require": { @@ -485,11 +485,11 @@ "barryvdh/laravel-debugbar": "^4.2", "craftcms/test-plugin": "1.0.1", "dg/bypass-finals": "^1.9", - "driftingly/rector-laravel": "^2.1", + "driftingly/rector-laravel": "^2.6.2", "fakerphp/faker": "^1.19.0", "intervention/image-driver-vips": "^4.1", - "larastan/larastan": "^3.12", - "laravel/boost": "^2.6", + "larastan/larastan": "^3.13", + "laravel/boost": "^2.10.3", "laravel/pao": "^1.0", "laravel/pint": "^1.22", "laravel/socialite": "^5.23", @@ -603,7 +603,7 @@ "rss": "https://github.com/craftcms/cms/releases.atom", "source": "https://github.com/craftcms/cms" }, - "time": "2026-10-07T07:42:26+00:00" + "time": "2026-10-09T14:47:05+00:00" }, { "name": "craftcms/cms-assets", @@ -611,12 +611,12 @@ "source": { "type": "git", "url": "https://github.com/craftcms/cms-assets.git", - "reference": "00bad07191812d783bf86ec6d9ddf166e0919609" + "reference": "be93661e9a77ade658749c3d667631b68beec2d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/craftcms/cms-assets/zipball/00bad07191812d783bf86ec6d9ddf166e0919609", - "reference": "00bad07191812d783bf86ec6d9ddf166e0919609", + "url": "https://api.github.com/repos/craftcms/cms-assets/zipball/be93661e9a77ade658749c3d667631b68beec2d7", + "reference": "be93661e9a77ade658749c3d667631b68beec2d7", "shasum": "" }, "default-branch": true, @@ -629,7 +629,7 @@ "support": { "source": "https://github.com/craftcms/cms-assets/tree/6.x" }, - "time": "2026-10-07T08:25:32+00:00" + "time": "2026-10-09T14:58:47+00:00" }, { "name": "craftcms/laravel-aliases", @@ -12301,12 +12301,12 @@ "source": { "type": "git", "url": "https://github.com/craftcms/yii2-adapter.git", - "reference": "49a927354629904a876926946c211d018cc02a97" + "reference": "a94de44c90e1e090db34092763b2992645278d79" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/craftcms/yii2-adapter/zipball/49a927354629904a876926946c211d018cc02a97", - "reference": "49a927354629904a876926946c211d018cc02a97", + "url": "https://api.github.com/repos/craftcms/yii2-adapter/zipball/a94de44c90e1e090db34092763b2992645278d79", + "reference": "a94de44c90e1e090db34092763b2992645278d79", "shasum": "" }, "require": { @@ -12405,7 +12405,7 @@ "rss": "https://github.com/craftcms/cms/releases.atom", "source": "https://github.com/craftcms/cms" }, - "time": "2026-10-06T22:14:47+00:00" + "time": "2026-10-09T10:39:54+00:00" }, { "name": "creocoder/yii2-nested-sets", diff --git a/resources/js/cms-elements.d.ts b/resources/js/cms-elements.d.ts index 5a930eaa73..c77e2e32a2 100644 --- a/resources/js/cms-elements.d.ts +++ b/resources/js/cms-elements.d.ts @@ -4,10 +4,10 @@ * and `resources/js/elements.ts`). Hand-authored for the same reason as * `window.d.ts`; keep in sync with `cms`'s `ElementIndexPage.vue`, * `ElementEditor.vue`, `CpButtonLink.vue`, `ActionMenu.vue`, `common/types` and - * `useElementIndexVisits.ts`. + * `useElementIndexVisits.ts`, `useAppLayout.ts`, `useCustomizeSources.ts`. */ declare module '@craftcms/cms/elements' { - import type {DefineComponent} from 'vue'; + import type {ComputedRef, DefineComponent} from 'vue'; export type IndexQueryValue = | string @@ -56,7 +56,41 @@ declare module '@craftcms/cms/elements' { items: ActionItemLink[]; } - export type ActionItem = ActionItemLink | ActionItemGroup; + export interface ActionItemButton { + type?: 'button'; + label: string; + icon?: string; + onClick?: (event: Event) => void; + } + + export type ActionItem = ActionItemLink | ActionItemGroup | ActionItemButton; + + export interface NavItem { + label: string | null; + href: string | null; + selected: boolean; + group: boolean; + subnav: ActionItem[] | false; + } + + export interface UseAppLayoutOptions { + subnav?: NavItem[]; + subnavActions?: ActionItem[]; + } + + export function useAppLayout( + options: UseAppLayoutOptions | (() => UseAppLayoutOptions) + ): void; + + export interface CustomizeSourcesTarget { + elementType?: string | null; + page?: string | null; + sourceKey?: string | null; + } + + export function useCustomizeSources( + target: () => CustomizeSourcesTarget + ): ComputedRef; export const ActionMenu: DefineComponent<{ actions: ActionItem[]; diff --git a/resources/js/pages/inventory/transfers/Index.vue b/resources/js/pages/inventory/transfers/Index.vue index 03cc2b3e8c..d0bc696ed1 100644 --- a/resources/js/pages/inventory/transfers/Index.vue +++ b/resources/js/pages/inventory/transfers/Index.vue @@ -6,13 +6,14 @@ useAppLayout, useCustomizeSources, type ElementIndexRoute, + type NavItem, } from '@craftcms/cms/elements'; const props = defineProps<{ indexUrl: string; newTransferUrl: string | null; newTransferLabel: string; - sourceNavItems?: Array; + sourceNavItems?: NavItem[]; elementType?: string | null; page?: string | null; source?: {key?: string | null} | null; diff --git a/resources/js/pages/products/Index.vue b/resources/js/pages/products/Index.vue index aae8016a3e..1ce33a7d9f 100644 --- a/resources/js/pages/products/Index.vue +++ b/resources/js/pages/products/Index.vue @@ -5,6 +5,7 @@ useAppLayout, useCustomizeSources, type ElementIndexRoute, + type NavItem, } from '@craftcms/cms/elements'; import NewProductButton, { type CreatableProductType, @@ -16,7 +17,7 @@ creatableProductTypes: CreatableProductType[]; newProductLabel: string; newProductMenuLabel: string; - sourceNavItems?: Array; + sourceNavItems?: NavItem[]; elementType?: string | null; page?: string | null; source?: {key?: string | null} | null; diff --git a/src/CatalogPricing/CatalogPricing.php b/src/CatalogPricing/CatalogPricing.php index f5da521333..7d1153ff67 100644 --- a/src/CatalogPricing/CatalogPricing.php +++ b/src/CatalogPricing/CatalogPricing.php @@ -301,16 +301,10 @@ public function getCatalogPrices(int $storeId, ?CatalogPricingCondition $conditi public function getCatalogPricesPageInfo(int $storeId, ?CatalogPricingCondition $conditionBuilder = null, bool $includeBasePrices = true, ?string $searchText = null, int $limit = 100, int $offset = 0): array { - // getCountForPagination() wraps the query as-is and counts a column from its SELECT — - // but this query only ever selects price/promotionalPrice/salePrice aggregates, never - // purchasableId, so that column was never there to count. select()ing it first (like - // the pre-port Yii2 version did) replaces the aggregate select instead of layering atop - // it, giving a plain "one row per purchasableId" result to just count. $total = $this->buildCatalogPricesQuery($storeId, $conditionBuilder, $includeBasePrices, $searchText) ->select(['purchasableId']) ->groupBy('purchasableId') - ->get() - ->count(); + ->getCountForPagination(); return [ 'first' => $offset + 1, diff --git a/src/CatalogPricing/CatalogPricingRules.php b/src/CatalogPricing/CatalogPricingRules.php index f51cb209b2..8d781a8062 100644 --- a/src/CatalogPricing/CatalogPricingRules.php +++ b/src/CatalogPricing/CatalogPricingRules.php @@ -207,8 +207,8 @@ public function saveCatalogPricingRule(CatalogPricingRule $catalogPricingRule, b $record->apply = $catalogPricingRule->apply; $record->applyAmount = $catalogPricingRule->applyAmount; $record->applyPriceType = $catalogPricingRule->applyPriceType; - $record->dateFrom = Query::prepareDateForDb($catalogPricingRule->dateFrom); - $record->dateTo = Query::prepareDateForDb($catalogPricingRule->dateTo); + $record->setAttribute('dateFrom', Query::prepareDateForDb($catalogPricingRule->dateFrom)); + $record->setAttribute('dateTo', Query::prepareDateForDb($catalogPricingRule->dateTo)); $record->description = $catalogPricingRule->description; $record->enabled = $catalogPricingRule->enabled; $record->isPromotionalPrice = $catalogPricingRule->isPromotionalPrice; diff --git a/src/Http/Controllers/Settings/LineItemStatusesController.php b/src/Http/Controllers/Settings/LineItemStatusesController.php index f6ac223ed1..c5ab55931d 100644 --- a/src/Http/Controllers/Settings/LineItemStatusesController.php +++ b/src/Http/Controllers/Settings/LineItemStatusesController.php @@ -54,7 +54,7 @@ public function index(): CpScreenResponse // slot, so with more than one store, only the first store's table gets one — a single // combined "New line item status" menu covering every store, rather than one button apiece. $createMenuItems = $this->readOnly ? [] : $stores->map(fn(Store $store) => [ - 'label' => $store->name, + 'label' => $store->getName(), 'url' => cp_url("commerce/settings/orders/line-item-statuses/{$store->handle}/new"), ])->all(); $createMenuAssigned = false; @@ -62,7 +62,7 @@ public function index(): CpScreenResponse $nodes = []; $stores->each(function(Store $store) use (&$nodes, $isMultiStore, $createMenuItems, &$createMenuAssigned) { if ($isMultiStore) { - $nodes[] = Heading::make("{$store->handle}-heading", $store->name); + $nodes[] = Heading::make("{$store->handle}-heading", $store->getName()); } $rows = app(LineItemStatuses::class)->getAllLineItemStatuses($store->id) diff --git a/src/Http/Controllers/Settings/OrderStatusesController.php b/src/Http/Controllers/Settings/OrderStatusesController.php index ec1264d2b5..bf534c2694 100644 --- a/src/Http/Controllers/Settings/OrderStatusesController.php +++ b/src/Http/Controllers/Settings/OrderStatusesController.php @@ -60,7 +60,7 @@ public function index(): CpScreenResponse // slot, so with more than one store, only the first store's table gets one — a single // combined "New order status" menu covering every store, rather than one button apiece. $createMenuItems = $this->readOnly ? [] : $stores->map(fn(Store $store) => [ - 'label' => $store->name, + 'label' => $store->getName(), 'url' => cp_url("commerce/settings/orders/statuses/{$store->handle}/new"), ])->all(); $createMenuAssigned = false; @@ -68,7 +68,7 @@ public function index(): CpScreenResponse $nodes = []; $stores->each(function(Store $store) use (&$nodes, $isMultiStore, $createMenuItems, &$createMenuAssigned) { if ($isMultiStore) { - $nodes[] = Heading::make("{$store->handle}-heading", $store->name); + $nodes[] = Heading::make("{$store->handle}-heading", $store->getName()); } $rows = app(OrderStatuses::class)->getAllOrderStatuses($store->id) diff --git a/src/Http/Controllers/Settings/StoresController.php b/src/Http/Controllers/Settings/StoresController.php index b032563895..f4d77b78c9 100644 --- a/src/Http/Controllers/Settings/StoresController.php +++ b/src/Http/Controllers/Settings/StoresController.php @@ -514,7 +514,7 @@ public function editSiteStores(): CpScreenResponse foreach (Sites::getAllSites() as $site) { $siteStore = $sitesStores->count() > 0 ? $sitesStores->firstWhere('siteId', $site->id) : null; $siteName = t($site->name, category: 'site'); - $storeId = request()->old("siteStores.$site->id.storeId", $siteStore->storeId ?? $primaryStoreId); + $storeId = session()->getOldInput("siteStores.$site->id.storeId", $siteStore->storeId ?? $primaryStoreId); $values[$site->id] = ['storeId' => $storeId]; $rows[] = [ diff --git a/src/Http/Controllers/StoreManagement/DiscountsController.php b/src/Http/Controllers/StoreManagement/DiscountsController.php index 63a179cee4..405ddf7487 100644 --- a/src/Http/Controllers/StoreManagement/DiscountsController.php +++ b/src/Http/Controllers/StoreManagement/DiscountsController.php @@ -427,7 +427,7 @@ private function initialValues(Discount $discount, Store $store): array // negative internally (a discount subtracted from the total), shown to the editor as // a plain positive amount/percentage — flipped back in save(). 'perItemDiscount' => $discount->perItemDiscount < 0 ? $discount->perItemDiscount * -1 : $discount->perItemDiscount, - 'percentDiscount' => round(-($discount->percentDiscount ?? 0) * 100, 6), + 'percentDiscount' => round(-$discount->percentDiscount * 100, 6), 'percentageOffSubject' => $discount->percentageOffSubject, 'ignorePromotions' => $discount->ignorePromotions, 'baseDiscount' => $discount->baseDiscount < 0 ? $discount->baseDiscount * -1 : $discount->baseDiscount, diff --git a/src/Http/Controllers/StoreManagement/PaymentCurrenciesController.php b/src/Http/Controllers/StoreManagement/PaymentCurrenciesController.php index 439ca9ea05..9f22e6f62c 100644 --- a/src/Http/Controllers/StoreManagement/PaymentCurrenciesController.php +++ b/src/Http/Controllers/StoreManagement/PaymentCurrenciesController.php @@ -46,14 +46,14 @@ public function index(?string $storeHandle = null): CpScreenResponse // getName() is just the ISO code today (there's no display-name lookup yet) — // matches the "Code" column below exactly, which is a pre-existing redundancy, // not something introduced here. - 'name' => $currency->primary + 'name' => $currency->getPrimary() ? ['html' => Html::encode(t('{name} (Primary)', ['name' => t($currency->getName(), category: 'site')], category: 'commerce'))] : ['html' => Html::a(Html::encode(t($currency->getName(), category: 'site')), $currency->getCpEditUrl(), ['class' => 'cell-bold'])], 'handle' => ['html' => FormFields::copytextHtml(['value' => $currency->iso, 'monospace' => true])], - 'rate' => $currency->primary + 'rate' => $currency->getPrimary() ? ['html' => Html::tag('span', Html::encode(t('Base', category: 'commerce')), ['class' => 'token'])] : (string) $currency->rate, - '_deletable' => !$currency->primary, + '_deletable' => !$currency->getPrimary(), ]) ->values() ->all(); @@ -102,7 +102,7 @@ public function edit(?string $storeHandle = null, ?int $id = null): CpScreenResp $currencyOptions = app(Currencies::class)->getAllCurrenciesList(); $hasCompletedOrders = Order::find()->isCompleted(true)->exists(); - $isoLocked = $currency->id && $currency->primary && $hasCompletedOrders; + $isoLocked = $currency->id && $currency->getPrimary() && $hasCompletedOrders; $formatter = app(Formatter::class); $metaSidebarHtml = $currency->id ? app(ContentHtml::class)->metadataHtml([ @@ -136,7 +136,7 @@ public function edit(?string $storeHandle = null, ?int $id = null): CpScreenResp } $formNodes[] = Field::make(t('Conversion Rate', category: 'commerce'), Number::make('rate') - ->mode($currency->primary ? ControlMode::ReadOnly : ControlMode::Editable)) + ->mode($currency->getPrimary() ? ControlMode::ReadOnly : ControlMode::Editable)) ->instructions(t('The conversion rate that will be used when converting an amount to this currency. For example, if an item costs {amount1}, a conversion rate of {rate} would result in {amount2} in the alternate currency.', [ 'amount1' => 10, 'rate' => 1.5, diff --git a/src/Http/Controllers/StoreManagement/ShippingRulesController.php b/src/Http/Controllers/StoreManagement/ShippingRulesController.php index 317b5f7f20..8dc9945a10 100644 --- a/src/Http/Controllers/StoreManagement/ShippingRulesController.php +++ b/src/Http/Controllers/StoreManagement/ShippingRulesController.php @@ -7,12 +7,10 @@ use craft\helpers\Localization; use CraftCms\Cms\Cp\Html\ContentHtml; use CraftCms\Cms\Http\Responses\CpScreenResponse; -use CraftCms\Cms\Support\Facades\I18N; use CraftCms\Cms\Support\Html; use CraftCms\Cms\Support\Json; use CraftCms\Cms\Support\Money; use CraftCms\Cms\Translation\Formatter; -use CraftCms\Cms\Translation\Locale; use CraftCms\Cms\Ui\Controls\ConditionBuilder; use CraftCms\Cms\Ui\Controls\Lightswitch; use CraftCms\Cms\Ui\Controls\Money as MoneyControl; @@ -495,9 +493,4 @@ public function delete(Request $request): Response return $this->asSuccess(t('Shipping rule deleted.', category: 'commerce')); } - - private function percentSymbol(): string - { - return I18N::getFormattingLocale()->getNumberSymbol(Locale::SYMBOL_PERCENT); - } } diff --git a/src/Plugin.php b/src/Plugin.php index 92c58d7c9e..7cd6694caf 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -221,7 +221,7 @@ class Plugin extends BasePlugin * `vite.config.ts` builds under — the same entry-point path/convention `cms`'s own * `resources/js/cp.ts` uses. */ protected array $vite = [ - 'input' => ['resources/js/cp.ts'], + 'resources/js/cp.ts', ]; protected array $commands = [ diff --git a/src/Promotion/Discounts.php b/src/Promotion/Discounts.php index c71d10a003..b9f88726f5 100644 --- a/src/Promotion/Discounts.php +++ b/src/Promotion/Discounts.php @@ -543,8 +543,8 @@ public function saveDiscount(Discount $model, bool $runValidation = true): bool $record->storeId = $model->storeId; $record->name = $model->name; $record->description = $model->description; - $record->dateFrom = Query::prepareDateForDb($model->dateFrom); - $record->dateTo = Query::prepareDateForDb($model->dateTo); + $record->setAttribute('dateFrom', Query::prepareDateForDb($model->dateFrom)); + $record->setAttribute('dateTo', Query::prepareDateForDb($model->dateTo)); $record->enabled = $model->enabled; $record->stopProcessing = $model->stopProcessing; $record->orderCondition = $model->hasOrderCondition() ? $model->getOrderCondition()->getConfig() : null; diff --git a/tests/Feature/Http/Controllers/ProductsControllerTest.php b/tests/Feature/Http/Controllers/ProductsControllerTest.php index 9e03e557d8..7c710cc01a 100644 --- a/tests/Feature/Http/Controllers/ProductsControllerTest.php +++ b/tests/Feature/Http/Controllers/ProductsControllerTest.php @@ -78,7 +78,10 @@ function productsControllerTestProductType(string $handle, string $name): Produc ->where('productTypeHandle', 'hoodies') ->where('source.key', 'productType:' . $this->hoodies->uid) ->where('title', 'Hoodies') - ->where('crumbs.2.href', Url::cpUrl('commerce/products/hoodies')) + ->where('crumbs.2.label', 'Hoodies') + ->where('crumbs.2.href', null) + ->where('crumbs.2.items.1.items.0.href', Url::cpUrl('commerce/products/hoodies')) + ->where('crumbs.2.items.1.items.0.selected', true) ); }); diff --git a/tests/Feature/Http/Controllers/ShippingRulesControllerTest.php b/tests/Feature/Http/Controllers/ShippingRulesControllerTest.php index 86b1b7c128..df3a13ac58 100644 --- a/tests/Feature/Http/Controllers/ShippingRulesControllerTest.php +++ b/tests/Feature/Http/Controllers/ShippingRulesControllerTest.php @@ -119,7 +119,7 @@ function shippingRuleSaveBody(ShippingMethod $method, ShippingRule $rule, ?strin 'X-Requested-With' => 'XMLHttpRequest', ]) ->assertOk() - ->assertExactJson([]); + ->assertExactJson(['message' => 'Shipping rule deleted.']); expect(DB::table(Table::SHIPPINGRULES)->where('id', $usOnly->id)->exists())->toBeFalse(); }); diff --git a/vite.config.mts b/vite.config.mts index 29d2c2d942..47cb3f7975 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -36,6 +36,8 @@ const cpSharedModuleExports: Record = { 'CpButtonLink', 'ActionMenu', 'appendIndexQuery', + 'useAppLayout', + 'useCustomizeSources', ], }; const cpSharedModules = Object.keys(cpSharedModuleExports); From f201e9a1493afd83eb53f78442a252fab1b572ba Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Fri, 9 Oct 2026 17:51:06 +0100 Subject: [PATCH 7/7] minor fix and tests --- resources/js/cms-elements.d.ts | 2 +- .../Http/Controllers/ProductsControllerTest.php | 16 ++++++++++++++++ .../Http/Controllers/TransfersControllerTest.php | 15 +++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/resources/js/cms-elements.d.ts b/resources/js/cms-elements.d.ts index c77e2e32a2..7a978d0bbf 100644 --- a/resources/js/cms-elements.d.ts +++ b/resources/js/cms-elements.d.ts @@ -70,7 +70,7 @@ declare module '@craftcms/cms/elements' { href: string | null; selected: boolean; group: boolean; - subnav: ActionItem[] | false; + subnav: NavItem[] | false; } export interface UseAppLayoutOptions { diff --git a/tests/Feature/Http/Controllers/ProductsControllerTest.php b/tests/Feature/Http/Controllers/ProductsControllerTest.php index 7c710cc01a..063dbf2247 100644 --- a/tests/Feature/Http/Controllers/ProductsControllerTest.php +++ b/tests/Feature/Http/Controllers/ProductsControllerTest.php @@ -58,6 +58,19 @@ function productsControllerTestProductType(string $handle, string $name): Produc ->where('indexUrl', Url::cpUrl('commerce/products')) ->where('productTypeHandle', '') ->where('source.key', '*') + ->has('sourceNavItems', 2) + ->where('sourceNavItems.0.label', 'All products') + ->where('sourceNavItems.0.href', Url::cpUrl('commerce/products')) + ->where('sourceNavItems.0.selected', true) + ->where('sourceNavItems.1.label', 'Product Types') + ->where('sourceNavItems.1.group', true) + ->has('sourceNavItems.1.subnav', 2) + ->where('sourceNavItems.1.subnav.0.label', 'Hoodies') + ->where('sourceNavItems.1.subnav.0.href', Url::cpUrl('commerce/products/hoodies')) + ->where('sourceNavItems.1.subnav.0.selected', false) + ->where('sourceNavItems.1.subnav.1.label', 'T-Shirts') + ->where('sourceNavItems.1.subnav.1.href', Url::cpUrl('commerce/products/tShirts')) + ->where('sourceNavItems.1.subnav.1.selected', false) ->where('crumbs.0.label', 'Commerce') ->where('creatableProductTypes.0.handle', 'hoodies') ->where('creatableProductTypes.0.newUrl', Url::cpUrl('commerce/products/hoodies/new')) @@ -78,6 +91,9 @@ function productsControllerTestProductType(string $handle, string $name): Produc ->where('productTypeHandle', 'hoodies') ->where('source.key', 'productType:' . $this->hoodies->uid) ->where('title', 'Hoodies') + ->where('sourceNavItems.0.selected', false) + ->where('sourceNavItems.1.subnav.0.selected', true) + ->where('sourceNavItems.1.subnav.1.selected', false) ->where('crumbs.2.label', 'Hoodies') ->where('crumbs.2.href', null) ->where('crumbs.2.items.1.items.0.href', Url::cpUrl('commerce/products/hoodies')) diff --git a/tests/Feature/Http/Controllers/TransfersControllerTest.php b/tests/Feature/Http/Controllers/TransfersControllerTest.php index 2a0b5daae6..04d9ee1bb7 100644 --- a/tests/Feature/Http/Controllers/TransfersControllerTest.php +++ b/tests/Feature/Http/Controllers/TransfersControllerTest.php @@ -33,6 +33,16 @@ ->where('elementType', Transfer::class) ->where('indexUrl', Url::cpUrl('commerce/inventory/transfers')) ->where('source.key', '*') + ->has('sourceNavItems', 2) + ->where('sourceNavItems.0.label', 'All Transfers') + ->where('sourceNavItems.0.href', Url::cpUrl('commerce/inventory/transfers')) + ->where('sourceNavItems.0.selected', true) + ->where('sourceNavItems.1.label', 'Transfer Status') + ->where('sourceNavItems.1.group', true) + ->has('sourceNavItems.1.subnav', 4) + ->where('sourceNavItems.1.subnav.1.label', 'Pending') + ->where('sourceNavItems.1.subnav.1.href', Url::cpUrl('commerce/inventory/transfers', ['source' => 'pending'])) + ->where('sourceNavItems.1.subnav.1.selected', false) ->where('newTransferLabel', 'New transfer') ->where('newTransferUrl', Url::actionUrl('commerce/transfers/create')) ->where('crumbs.0.label', 'Commerce') @@ -47,6 +57,11 @@ ->assertInertia(fn(AssertableInertia $page) => $page ->where('source.key', 'pending') ->where('title', 'Pending') + ->where('sourceNavItems.0.selected', false) + ->where('sourceNavItems.1.subnav.0.selected', false) + ->where('sourceNavItems.1.subnav.1.selected', true) + ->where('sourceNavItems.1.subnav.2.selected', false) + ->where('sourceNavItems.1.subnav.3.selected', false) ); });