Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# next version

- Show UCP settings as read-only to viewers, let UCP editors save without sales-channel edit access, and require UCP view access for Agentic Commerce Insights.
- Keep disabled Agentic Files disabled when activating UCP or saving its settings.
Existing custom content is preserved, and missing default files are created.
- Serve `robots.txt` again on Shopware `6.7.1.0` to `6.7.4.x` instead of answering `500`.
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG_de-DE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# next version

- UCP-Einstellungen werden für Benutzer mit Leserechten schreibgeschützt angezeigt.
Benutzer mit UCP-Bearbeitungsrechten können sie ohne Bearbeitungsrechte für Verkaufskanäle speichern.
Agentic-Commerce-Insights erfordern UCP-Leserechte.
- Deaktivierte Agentic Files bleiben beim Aktivieren von UCP oder Speichern der UCP-Einstellungen deaktiviert.
Bestehende eigene Inhalte bleiben erhalten, und fehlende Standarddateien werden angelegt.
- Unter Shopware `6.7.1.0` bis `6.7.4.x` liefert der Shop `robots.txt` wieder aus, statt mit `500` zu antworten.
Expand Down
52 changes: 31 additions & 21 deletions docs/manual-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,11 +256,14 @@ Expected result:
- `SwagAgenticCommerce` administration assets exist under `public`
- `/admin` still renders after the build

The CI admin matrix runs Playwright automatically when `CI_ADMIN_BROWSER_VALIDATE=1` is set,
covering UCP module render, overview, detail, the authenticated admin API
list/detail/config/preview, and the signing-key lifecycle. The Playwright suite is the
release-confidence check for those flows; the manual browser steps below are for exploratory UX
review and for the ACL/UX matrix that CI cannot run (CI runs as a privileged user only).
The CI admin matrix runs the legacy `test:e2e:admin` Playwright suite when
`CI_ADMIN_BROWSER_VALIDATE=1` is set. It does not run the acceptance suite or the `@UcpAcl`
permission matrix. Run the gating acceptance suite, including the `UcpAcl` project, with
`npm run acceptance` from the plugin root after completing the acceptance setup described in
[the acceptance README](../tests/acceptance/README.md).

Use the manual browser steps below for exploratory UX review across the supported Administration
builds.

Install the local browser QA dependency before running it manually:

Expand Down Expand Up @@ -316,12 +319,14 @@ If needed, reset inside the lane:
bin/console user:change-password admin --password 'shopware'
```

The module render, overview, detail, save, signing-key actions, and profile preview are asserted by
the Playwright admin suite. Use the manual open for exploratory UX review:
The sales-channel detail, UCP configuration, and profile preview are asserted by the Playwright
Administration suite. Signing-key commands are verified through the console. Use the manual open for
exploratory UX review:

- settings page shows the UCP entry in the expected settings group for the lane
- direct route opens: `#/sw/settings/ucp/index`
- overview renders real sales channels and the native sales-channel shortcut
- Storefront and Headless channels show the UCP tab to users with `ucp.viewer`
- users with `ucp.viewer` but without `ucp.editor` see a read-only banner and disabled controls
- users with `ucp.editor` can save UCP configuration without `sales_channel.editor`
- Agentic Commerce Insights remain hidden without `ucp.viewer`, including direct navigation
- profile preview renders the lane-aware transports
- browser console has no UCP runtime error
- a Product Comparison and an Agentic Commerce channel show no UCP card, and saving their feed
Expand All @@ -331,6 +336,8 @@ the Playwright admin suite. Use the manual open for exploratory UX review:
is accepted
- `ucp:channels` lists no feed channel, and
`ucp:config:set --sales-channel <feed-channel-id> --active 1` exits non-zero
- signing keys are managed with `ucp:signing-keys:{generate,list,show-public,retire,delete}` through
`bin/console`; there is no Administration signing-key interface

## Storefront Build Validation

Expand Down Expand Up @@ -494,15 +501,18 @@ Steps:

### 4. Admin ACL / UX

CI runs the admin suite as a privileged user only. Verify the role matrix by logging in as users
that hold exactly one of the UCP ACL roles.

- `ucp.viewer`: save and signing-key actions are disabled; a read-only banner is shown.
- `ucp.key_rotator`: can create, retire, and delete signing keys, but cannot save config.
- `ucp.editor`: can save config.
The `UcpAcl` acceptance project verifies these role-specific behaviors. The gating acceptance suite
runs with `npm run acceptance` from the plugin root; `CI_ADMIN_BROWSER_VALIDATE` runs only the
legacy admin Playwright suite. Use manual accounts with the same role assignments when exploring the
UI:

Also visually verify the signing-key management UX: the algorithm select, the `kid` input, and the
retire/delete confirmation dialogs.
- `ucp.viewer` and `ucp.key_rotator`: configuration controls and Save are disabled, and the
read-only banner is shown.
- `ucp.editor` without `sales_channel.editor`: Save on the UCP tab persists only UCP configuration.
- Users without a UCP role do not see the UCP tab or list column, and cannot read or write UCP
configuration.
- Agentic Commerce Insights require `ucp.viewer`; product-comparison Insights keep their core
permission behavior.

Verify that Save applies pending changes across sales channel tabs:

Expand Down Expand Up @@ -601,7 +611,8 @@ A lane passes manual validation when all are true:

- plugin installs through Composer and activates
- admin build for the lane succeeds
- UCP administration module renders in browser (exploratory)
- Agentic Commerce sales-channel UCP settings render in browser (exploratory)
- Storefront and Headless channels show the UCP tab to users with `ucp.viewer`
- storefront build for the lane succeeds and the storefront UI renders
- real-browser cross-origin iframe enforcement behaves correctly: embed renders
- bridge works on an allowlisted origin, browser blocks the frame with a CSP violation on a
Expand All @@ -610,8 +621,7 @@ A lane passes manual validation when all are true:
(scenario 2)
- the remote-profile allowlist accepts an allowlisted external profile host and rejects a
non-allowlisted one (scenario 3)
- admin ACL/UX matrix is correct for `ucp.viewer`, `ucp.editor`, and `ucp.key_rotator`, and the
signing-key UX renders (scenario 4)
- admin ACL/UX matrix is correct for `ucp.viewer`, `ucp.editor`, and `ucp.key_rotator` (scenario 4)
- with a real tokenizing handler installed, the profile advertises it, `tokenize` returns a token,
and a paid order completes (scenario 5)
- concurrent `/complete` requests produce exactly one order with no duplicate and no `500`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,9 @@ Shopware.Service('privileges').addPrivilegeMappingEntry({
},
});

// UCP privileges (moved here from the removed sw-settings-ucp module). The
// Agentic Commerce tab gates editing on `ucp.editor` and signing-key actions on
// `ucp.key_rotator`; the parent is null since UCP config now lives on the sales
// channel rather than a dedicated Settings module.
// UCP config privileges. `ucp.key_rotator` currently adds only the viewer
// dependency; signing keys are managed through console commands, not the UI.
// The parent is null since UCP config lives on the sales channel.
Shopware.Service('privileges').addPrivilegeMappingEntry({
category: 'permissions',
parent: null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ import './view/sw-sales-channel-create-base';
import './view/sw-sales-channel-detail-agentic-commerce';
import './view/sw-sales-channel-detail-agentic-commerce-integration';
import './view/sw-sales-channel-detail-agentic-commerce-statistics';
import './view/sw-sales-channel-detail-product-export-insights';
import './view/sw-sales-channel-detail-product-comparison';
import './routes.init';
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import template from './sw-sales-channel-detail.html.twig';
import './sw-sales-channel-detail.scss';
import { coreShipsAgenticCommerce } from '../../../../core-feature';
import { mergeAgenticFileDrafts, saveAgenticFileConfigurations } from '../../agentic-file-persistence';
import { currentAdminVersion, isVersionAtLeast, useMtComponents } from '../../agentic-commerce/admin-version';
import {
defaultForm as ucpDefaultForm,
normalizeConfig as ucpNormalizeConfig,
Expand Down Expand Up @@ -103,13 +104,54 @@ export const swSalesChannelDetailOverride = {
: 'sw.sales.channel.detail.agenticCommerceStatistics';
},

canViewUcpInsights() {
return this.acl.can('ucp.viewer');
},

salesChannelDetailTabs() {
const tabs = this.$super('salesChannelDetailTabs');
if (!Array.isArray(tabs) || !this.isAgenticCommerce || this.canViewUcpInsights) {
return tabs;
}

return tabs.filter((tab) => tab.name !== 'sw.sales.channel.detail.productExportInsights');
},

shouldRenderAgenticCommerceTab() {
const typeId = this.salesChannel?.typeId ?? this.$route.params.typeId;

return this.acl.can('ucp.viewer')
&& (isTransactionalSalesChannelType(typeId) || this.ucpState.transactional);
},

isUcpConfigRoute() {
return this.$route.name === 'sw.sales.channel.detail.agenticCommerce';
},

usesMeteorComponents() {
return useMtComponents();
},

supportsVueRouterViewSlot() {
// Vue Router 4 (Shopware 6.6+) supports the scoped router-view slot.
return isVersionAtLeast(currentAdminVersion(), 6, 6);
},

shouldRenderAgenticCommerceContent() {
return (!this.isAgenticCommerce
|| this.$route.name !== 'sw.sales.channel.detail.productExportInsights'
|| this.canViewUcpInsights)
&& (this.$route.name !== 'sw.sales.channel.detail.agenticCommerce' || this.shouldRenderAgenticCommerceTab);
},

allowSaving() {
if (this.isUcpConfigRoute) {
return this.acl.can('ucp.editor') || this.acl.can('sales_channel.editor');
}

return this.$super('allowSaving');
},

// Widened to include AC channels so they reuse the product-export blocks.
// $super() is skipped — it is unreliable during the create flow in 6.5/6.6.
isProductComparison() {
Expand Down Expand Up @@ -289,7 +331,7 @@ export const swSalesChannelDetailOverride = {
// the page before this write, which replaces `ucpState.form` with the stored,
// not yet updated config.
async saveUcpState(salesChannelId, form) {
if (!form || !salesChannelId) {
if (!form || !salesChannelId || !this.acl.can('ucp.editor')) {
return true;
}

Expand Down Expand Up @@ -421,7 +463,8 @@ export const swSalesChannelDetailOverride = {
}

if (this.pendingAgenticFilesReload
&& this.pendingAgenticFilesReload === this.salesChannel?.id) {
&& this.pendingAgenticFilesReload === this.salesChannel?.id
&& this.acl.can('sales_channel.editor')) {
this.isLoading = true;
this.isSaveSuccessful = false;
try {
Expand All @@ -434,10 +477,40 @@ export const swSalesChannelDetailOverride = {
}
}

if (this.isUcpConfigRoute && !this.acl.can('sales_channel.editor')) {
if (!this.acl.can('ucp.editor') || !this.ucpState.loaded) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Save does nothing when the UCP config failed to load.

ucpState.loaded only turns true once getConfig has answered. After a failed request it stays false. allowSaving still enables Save for a ucp.editor on this tab. A click then returns here without a request, a notification or a spinner.

Could you keep Save disabled until ucpState.loaded is true on this route?

return;
}

const salesChannelId = this.salesChannel?.id;
const form = ucpNormalizeConfig(this.ucpState.form);
const hasUcpChanges = JSON.stringify(form) !== JSON.stringify(ucpNormalizeConfig(this.ucpState.savedForm));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (non-blocking): This adds a third copy of the UCP dirty check to onSave.

The same comparison is at lines 514 and 556. I suggest a hasUcpChanges(form) method that all three branches call.

this.isSavingUcp = true;
this.isLoading = true;
this.isSaveSuccessful = false;

try {
if (hasUcpChanges && !await this.saveUcpState(salesChannelId, form)) {
return;
}

this.isSaveSuccessful = true;
if (salesChannelId && hasUcpChanges) {
this.refreshUcpPreview(salesChannelId);
}
} finally {
this.isSavingUcp = false;
this.isLoading = false;
}

return;
}

if (!this.isAgenticCommerce) {
const salesChannelId = this.salesChannel?.id;
const form = this.ucpState.loaded ? ucpNormalizeConfig(this.ucpState.form) : null;
const hasUcpChanges = form
&& this.acl.can('ucp.editor')
&& JSON.stringify(form) !== JSON.stringify(ucpNormalizeConfig(this.ucpState.savedForm));

// Keep core validation and Storefront theme assignment in the save chain.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,42 @@ describe("sw-sales-channel-detail agenticCommerceStatisticsRoute", () => {
);
});
});

describe("sw-sales-channel-detail Insights ACL", () => {
const insight = { name: "sw.sales.channel.detail.productExportInsights" };
const base = { name: "sw.sales.channel.detail.base" };
const tabs = [base, insight];

it("hides core Insights for Agentic Commerce without ucp.viewer", () => {
const computed = loadComputed(true);
const actual = computed.salesChannelDetailTabs.call({
$super: () => tabs,
isAgenticCommerce: true,
canViewUcpInsights: false,
});

expect(actual).toEqual([base]);
});

it("preserves core Insights for Agentic Commerce with ucp.viewer", () => {
const computed = loadComputed(true);
const actual = computed.salesChannelDetailTabs.call({
$super: () => tabs,
isAgenticCommerce: true,
canViewUcpInsights: true,
});

expect(actual).toEqual(tabs);
});

it("leaves product-comparison Insights unchanged", () => {
const computed = loadComputed(true);
const actual = computed.salesChannelDetailTabs.call({
$super: () => tabs,
isAgenticCommerce: false,
canViewUcpInsights: false,
});

expect(actual).toBe(tabs);
});
});
Loading
Loading