Skip to content

[Studio] Keep import execution available for read-only configurations - #684

Open
robertSt7 wants to merge 6 commits into
2026.2from
fix/pees-1290-importer-upload-permission
Open

[Studio] Keep import execution available for read-only configurations#684
robertSt7 wants to merge 6 commits into
2026.2from
fix/pees-1290-importer-upload-permission

Conversation

@robertSt7

Copy link
Copy Markdown
Contributor

Issue

https://github.com/pimcore/service-operations/issues/977

The "Upload File" button and the "Cancel" action of a running import were dimmed on instances that
do not run with APP_DEBUG=true / APP_ENV=dev, even for an admin user. Reproducible with
import configurations stored under symfony-config.

Root cause

Configuration::getConfiguration() (data-hub) exposes general.writeable, which comes from
LocationAwareConfigRepository::isWriteable():

if ($writeTarget === self::LOCATION_SYMFONY_CONFIG && !Pimcore::getKernel()->isDebug()) {
    return false;
}

So for configurations whose write target is the Symfony configuration, general.writeable is
false in every non-debug environment — that is the APP_DEBUG dependency from the report.

#642 ("Enforce per-config permissions in the data importer config UI") folded that flag into the
single isWriteable value in data-importer-detail-view.tsx:

-const isWriteable = configData?.userPermissions?.update ?? true
+const isWriteable = userPermissions.update === true && generalConfig.writeable !== false

That value is also passed to BaseDetailView disabled={ !isWriteable }, which ends up on the antd
Form as disabled. antd propagates it through DisabledContext to every control inside the
form, and antd's Button merges it as customDisabled ?? contextDisabled. Controls that pass an
explicit disabled boolean therefore keep working, while controls that pass none inherit it.

The only two controls in the data importer form without an explicit disabled were the
"Upload File" button (upload-loader-settings.tsx) and the "Cancel" button
(execution-status.tsx) — exactly the two actions named in the report. "Start Import" already
passed an explicit boolean, which is why it was not reported as dimmed.

The backend was never affected: ImportService::uploadImportFile(), startImport() and
cancelExecution() authorise against the per-configuration update permission
(Configuration::isAllowed('update')) and do not consult writeability at all. Verified still
present on 2026.2.

What changed

  • New utils/config-capabilities.ts with resolveConfigCapabilities(), which keeps the two
    aspects apart:
  • New ConfigCapabilitiesProvider context, so the upload loader settings — rendered through the
    dynamic type registry — can read the capabilities without changing
    DynamicTypeLoaderAbstract::renderSettings() (no BC break for custom loader types).
  • The "Upload File", "Cancel" and "Start Import" controls now always pass an explicit disabled
    boolean, so they reflect the execution permission instead of inheriting the read-only form state,
    and show a tooltip when the permission is missing.

No permission check was removed or widened. The UI gate is now exactly the backend gate. As a
side effect "Start Import" is now also gated on the update permission — previously it was enabled
for a read-only user and the request failed with 403.

Tests

The regression lives in the React layer, which had no test runner in this repository. Added Jest
(SWC transform, mirroring the studio UI bundle setup) and enabled the test step of the shared
reusable-studio-frontend-build.yaml workflow, plus
utils/config-capabilities.test.ts covering the regression and the surrounding permission matrix.

Verified locally in assets/studio:

  • npm ci — clean install from the regenerated lockfile (332 packages added, only 10 dev-only
    @babel/* patch bumps, nothing removed).
  • npm run test — 7 passed. The regression case fails against the pre-fix logic
    (Expected: true, Received: false for canRunImport on a non-writeable configuration) and
    passes after.
  • npm run check-types and npm run lint — clean.
  • npm run build — succeeds; the generated build output was reverted so CI produces the committed
    bundle.

Not verified locally: the PHP Codeception suite (no vendor/ in this workspace) — no PHP was
touched, so it relies on CI. The fix was not exercised against a running Pimcore instance; the
mechanism was established from the antd DisabledContext implementation and the compiled
BaseDetailView in the installed data-hub bundle rather than from a browser session.

🤖 Generated with Claude Code

robertSt7 and others added 2 commits August 27, 2026 06:30
The studio frontend had no test runner, so logic that lives only in the React
layer could not be covered. Adds Jest with the SWC transform, mirroring the
setup of the studio UI bundle, and enables the test step of the shared studio
frontend build workflow.

Co-Authored-By: Claude <noreply@anthropic.com>
Uploading an import file and cancelling a running import were unavailable
whenever the configuration itself could not be persisted. A configuration that
comes from the Symfony configuration is reported as not writeable unless the
instance runs in debug mode, so those actions were dimmed in production even
for users who are allowed to update the configuration.

The reason is that the writeability was folded into the single "is writeable"
flag that also puts the whole configuration form into a read-only state. Both
buttons had no explicit disabled state and therefore inherited it from the form.

The capability computation is extracted into resolveConfigCapabilities(), which
keeps the two aspects apart: editing the configuration requires the update
permission and a writeable storage, while executing an import only requires the
permission - the same authorisation the upload, start and cancel endpoints
apply. The execution controls now always pass an explicit disabled state, so
they reflect the permission instead of the read-only form. As a side effect the
start button is gated on the permission as well, which it was not before.

Co-Authored-By: Claude <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings August 27, 2026 06:31
@robertSt7

Copy link
Copy Markdown
Contributor Author

@copilot review

Copilot AI left a comment

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.

Pull request overview

Separates configuration writeability from import execution permissions so authorized users can run imports on read-only configurations.

Changes:

  • Adds centralized configuration capabilities and React context.
  • Explicitly gates upload, start, and cancel actions by update permission.
  • Adds Jest tests and CI execution.

Reviewed changes

Copilot reviewed 21 out of 41 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
.github/workflows/studio-frontend-build.yaml Enables frontend tests in CI.
assets/studio/eslint.config.mjs Adds Jest globals.
assets/studio/jest.config.ts Configures Jest with SWC.
assets/studio/js/src/modules/data-importer/components/config-capabilities-context.tsx Provides capabilities through React context.
assets/studio/js/src/modules/data-importer/components/data-importer-detail-view.tsx Resolves and distributes capabilities.
assets/studio/js/src/modules/data-importer/components/tabs/execution-tab/execution-status/execution-status.tsx Applies execution permission to start and cancel controls.
assets/studio/js/src/modules/data-importer/components/tabs/execution-tab/manual-execution-button/manual-execution-button.tsx Gates starting imports by permission.
assets/studio/js/src/modules/data-importer/dynamic-types/loader/upload/upload-loader-settings.tsx Gates file uploads by permission.
assets/studio/js/src/modules/data-importer/utils/config-capabilities.test.ts Tests the capability matrix.
assets/studio/js/src/modules/data-importer/utils/config-capabilities.ts Defines capability resolution.
assets/studio/package-lock.json Locks new test dependencies.
assets/studio/package.json Adds Jest tooling and test script.
assets/studio/tsconfig.json Adds Jest type definitions.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

{ /* Cancelling a run is an execution action, so `disabled` is passed
explicitly instead of inheriting the read-only form state. */ }
<Button
disabled={ !canRunImport }

Copilot AI commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

@copilot review

Here is the review of the PR changes:

Summary

The fix correctly separates two independent concerns that were incorrectly conflated:

  • Storage writeability (general.writeable) — whether the config file can be persisted (Symfony-config is read-only in non-debug environments)
  • User permission (userPermissions.update) — whether the user may perform actions

The backend endpoints for upload/start/cancel only check the user permission, so the UI should mirror that.

Code quality observations

✅ Correct

config-capabilities.ts — Logic is sound. isStorageWriteable = general?.writeable !== false correctly treats undefined as writeable (permissive default for missing data). The resolveConfigCapabilities() function is well-documented and correctly isolates the two concerns.

config-capabilities-context.tsx — Using deniedCapabilities as the context default is safe — nothing is permitted until the detail view provides resolved capabilities. Good defensive default.

data-importer-detail-view.tsx — Correctly computes capabilities via useMemo and wraps the tree with ConfigCapabilitiesProvider. The BaseDetailView disabled={!canSaveConfig} still correctly gates form editing on write capability, while execution actions bypass it via explicit props.

upload-loader-settings.tsx, execution-status.tsx, manual-execution-button.tsx — All three now pass disabled explicitly so they never inherit the form's disabled state. Tooltips are shown when permission is missing.

⚠️ Minor observations (no code changes needed)

  1. Cancel button has no tooltip when canRunImport is false — the Upload and Start buttons show a tooltip explaining why they're disabled, but the Cancel button in execution-status.tsx is silently disabled. This is a minor UX inconsistency. The Cancel button is only visible when an import is already running, so in practice the permission gate would have to be revoked mid-run to trigger this — low priority.

  2. saveDisabledTooltipKey when both permission and writeable are false — returns 'config_not_writeable' rather than the more informative 'data-hub.config.no-update-permission'. The storage message takes precedence. This is arguably correct (fixing writeability would not help either), but the opposite priority (permission message first) would be equally defensible. No regression here.

  3. Test coverage — 7 test cases cover the regression and the surrounding matrix well. The test for saveDisabledTooltipKey when both flags are false is not present, but the two individual cases are covered.

✅ CI status

The action_required workflow conclusions are for the centralised reusable workflows that require manual approval before running — they are not code failures. The Studio Frontend Build run for the previous commit (4c8ae66) completed with action_required for the same reason.

Overall: the implementation is correct and the approach is sound. The fix precisely mirrors the backend authorization model in the UI.

The upload and start controls already show why they are unavailable when the
user may not update the configuration. The cancel button was left without that
explanation, which is the situation this change set out to avoid.

Co-Authored-By: Claude <noreply@anthropic.com>
@robertSt7

Copy link
Copy Markdown
Contributor Author

@copilot review

Copilot AI commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

@copilot review

The tooltip was already added to the Cancel button in commit 859ee6b. All three execution controls (Upload File, Start Import, Cancel) now use the same Tooltip/span pattern and show data-hub.config.no-update-permission when canRunImport is false.

@robertSt7 robertSt7 self-assigned this Aug 27, 2026
@sonarqubecloud

Copy link
Copy Markdown

@robertSt7 robertSt7 added the Bug label Aug 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants