Skip to content

Story/external plugin case widget - #873

Open
ivo-ritense wants to merge 3 commits into
feature/external-plugin-systemfrom
story/external-plugin-case-widget
Open

Story/external plugin case widget#873
ivo-ritense wants to merge 3 commits into
feature/external-plugin-systemfrom
story/external-plugin-case-widget

Conversation

@ivo-ritense

@ivo-ritense ivo-ritense commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Fixes: https://github.com/generiekzaakafhandelcomponent/atlas-internal/issues/491

A. Admin config — availability gate & configuration picker

  • A1 — Gate: type hidden when no bundle. With no activated configuration exposing a
    case-widget bundle (deactivate/remove them), open a WIDGETS tab → "add widget". The
    "External plugin" type is not offered.
  • A2 — Gate: type shown when a bundle exists. Activate a case-summary configuration, reopen
    "add widget" → "External plugin" now appears in the generic widgets group.
  • A3 — Config combo lists activated configs. Select the External plugin type → the CONTENT
    step shows a Plugin configuration combo box listing each activated config exposing a
    case-widget bundle, labelled "<config title> (Case Summary (0.1.0))".
  • A4 — Multi-bundle → bundle picker appears. Pick a config that exposes two case-widget
    bundles → a second Widget combo box appears offering "Case Summary Widget" and
    "Case Metrics Widget". The step stays invalid (can't proceed) until a bundle is chosen.
  • A5 — Single-bundle → no bundle picker. (If you also configure a plugin/config exposing only
    one case-widget bundle) selecting it auto-resolves the bundle and the second combo box does
    not appear; the step becomes valid immediately.
  • A6 — Title required. The widget title is required (CONTENT step invalid while blank).
  • A7 — Save persists selection. Choose config + "Case Metrics Widget", set a title, save.
    Re-open the widget (edit) → the config and bundle are pre-selected (prefill) and the title
    is retained.
  • A8 — Token-absent graceful state. (Optional / dev) If the config provider token is absent,
    the editor shows the "External plugins unavailable" message instead of the combos. (In a normal
    GZAC build this should not occur because of the A1 gate.)

B. Standard widget properties (full parity)

For an external-plugin widget, walk the generic wizard steps and confirm each works:

  • B1 — Width (1–4) applies to the card in the grid.
  • B2 — Appearance: icon, background color, and high-contrast all apply to the card chrome.
  • B3 — Density: default vs compact changes the card padding.
  • B4 — Display conditions: add a condition that resolves false → the widget is hidden on
    the case; a condition that resolves true → the widget is shown.

C. Rendering on the case detail page

  • C1 — Renders in a sandboxed iframe. Open a case of the definition → the widget card renders
    the plugin bundle inside <valtimo-external-plugin-iframe> (opaque-origin sandbox, no
    allow-same-origin).
  • C2 — Correct bundle renders. A summary-widget widget shows the Case Summary card; a
    metrics-widget widget shows the Case Metrics tiles (view count / currency / short doc id).
    Two widgets pointing at different bundles of the same config render differently side by side.
  • C3 — sdk.getPluginData works. The plugin-served data (view count, currency, message)
    loads via the parent-proxy /data route.
  • C4 — sdk.callValtimo works. The case-definition line loads via the downscoped user token
    (GZAC proxy, PBAC ∩ allowlist).
  • C5 — Loading / error / unavailable states. Loading spinner shows before ready; an
    unresolvable widget (see D-matrix) shows the "unavailable" message; a hard failure shows the
    error message.
  • C6 — PBAC. As a user without VIEW permission on the widget/tab (document context), the
    widget is not returned / not rendered; a user with permission sees it.

D. Export

  • D1 — Widget present in export. In Src, export the case definition. The
    config/case/<key>/<ver>/case/widget-tab/<key>.case-widget-tab.json file contains the
    external-plugin widget with properties.configurationId and the self-describing
    properties.pluginDefinitionKey (case-summary) + properties.pluginDefinitionVersion
    (0.1.0).
  • D2 — Non-external widgets unchanged. Other widget types in the same file serialize as before
    (no plugin-identity noise on them).

E. Import matrix — configuration id & plugin type: found / not found / later configured

The exported widget carries (a) the source configurationId and (b) the plugin
pluginDefinitionKey + pluginDefinitionVersion (self-describing). On import into Tgt the
import wizard's PLUGINS step lists the widget's referenced configuration and lets it be mapped.
An unmapped widget imports dangling (keeps the original, now-unresolvable id) and is repaired
later from the case-definition "missing plugin configurations" panel.

Matrix of what to test (Src exported a widget referencing config C of plugin case-summary@0.1.0):

# Config id C in Tgt Plugin case-summary installed in Tgt Action Expected
E1 Found Found import (no mapping needed) Widget resolves to C directly and renders
E2 Not found Found (≥1 config) map C → a Tgt config in PLUGINS step Widget re-pointed and renders
E3 Not found Found leave unmapped Imports dangling → issue raised → unavailable; repair later (E6)
E4 Not found Not found (no host/definition) import Row shows not-installed; cannot map; dangling/unavailable
E5 Not found Found but version mismatch (e.g. 0.2.0) map to a mismatched-version config Allowed behind an explicit warning; renders against the mismatched config
E6 (was E3/E4) later installed / config created repair from case-definition panel Widget re-pointed and renders

D-matrix — detailed steps

  • E1 — Config id found. Import the export into a Tgt where a configuration with the same
    id C
    exists (e.g. re-import into Src, or an environment that already has C). The PLUGINS
    step marks it as already present (no mapping required). After import, open a case → the widget
    renders (resolves C directly).
  • E2 — Config id not found, plugin type found, mapped. Import into a Tgt that has
    case-summary@0.1.0 installed with a different configuration id C2 (not C). The PLUGINS
    step lists the widget's configuration and offers C2 (label "<title> — Case Summary (0.1.0)").
    Map C → C2, finish import. Open a case → the widget renders against C2. Verify the
    widget's stored config id is now C2 (via /db on case_widget_tab_widget .external_plugin_configuration_id, or that it renders).
  • E3 — Config id not found, plugin type found, left unmapped. Same Tgt as E2 but do not map
    (leave the row unset) and finish import. Expected:
    - [ ] The "missing plugin configurations" panel on the case-definition detail becomes visible
    and lists a row for case-summary (0.1.0) (identified from the self-describing export).
    - [ ] Opening a case → the widget shows the "unavailable" state (bundleUrl null).
    - [ ] The widget kept its original (now-dangling) config id (not nulled) — confirm via /db:
    external_plugin_configuration_id = the source id, plugin_definition_key/version populated.
  • E4 — Config id not found, plugin type not found. Import into a Tgt that does not have
    case-summary installed at all (no host/definition). Expected:
    - [ ] The PLUGINS step / repair panel row shows status not-installed (the plugin is still
    identifiable by key/version from the self-describing export, but there is nothing to map to).
    - [ ] Widget imports danglingunavailable on the case.
  • E5 — Version mismatch (non-blocking warning). In a Tgt that has case-summary at a
    different version (e.g. 0.2.0) with a config, the repair/PLUGINS row offers that config
    behind an explicit version-mismatch warning. Mapping to it is allowed; after mapping the
    widget renders against the mismatched-version config.

F. Dangling repair (later configured) — closes E3 / E4

  • F1 — Repair after plugin becomes available (from E4). In the Tgt from E4, add the plugin
    host
    for case-summary (definition becomes AVAILABLE) and create a configuration C3.
    The case-definition "missing plugin configurations" panel row now shows status available
    with C3 selectable.
  • F2 — Repair maps and resolves (from E3 or F1). In the repair panel, select a target config
    for the case-summary (0.1.0) row and Save. Expected:
    - [ ] Success toast; the panel row disappears / the issue clears.
    - [ ] Opening a case → the previously-unavailable widget now renders.
    - [ ] The widget's stored external_plugin_configuration_id was rewritten to the mapped id
    (/db), and the external-plugin-case-widget configuration issue is resolved.
  • F3 — Repair groups by plugin identity. If several dangling widgets reference the same
    source config, one mapping repairs all of them (they share the source id in the row's
    sourcePluginConfigurationIds).
  • F4 — Mixed surfaces coexist. If the same case definition also has a dangling external-plugin
    tab or process link, each surface is repaired independently (repairing the widget
    does not clear the tab's issue, and vice-versa).

G. Delete guard

  • G1 — Config referenced by a widget can't be deleted. With a mapped/rendering widget
    referencing config C, attempt to delete C. Expected: blocked (HTTP 409), the read-only
    in-use modal lists the usage as a CASE parent with the widget's case definition + owning
    WIDGETS tab (tabKey/tabName, plus the widget key). No force override.
  • G2 — Host can't be deleted while a widget references one of its configs. Attempt to delete
    the plugin host → blocked, the modal includes the widget usage.
  • G3 — Delete succeeds after removal. Remove the widget (or the whole case definition), then
    deleting the configuration/host succeeds.

H. Multi-version coexistence

  • H1 — Two versions, two widgets. Install case-summary at two versions (e.g. 0.1.0 and a
    bumped version), each with its own configuration. Add one widget backed by each configuration on
    the same (or different) WIDGETS tab → both render, each pinned to its own plugin version.

I. Internationalisation

  • I1 — Widget type name/description localised. The "External plugin" type label + description
    in the "add widget" picker follow the UI language (EN/NL).
  • I2 — Config editor labels localised. "Plugin configuration" / "Widget" combo labels +
    placeholders switch with the language.
  • I3 — Live language switch. Switching language on the case-management widget editor relabels
    the type/description and combos live.
  • I4 — Bundle content localised. The rendered widget's own text (from the plugin's sdk.t)
    follows the UI locale (EN/NL) passed into the iframe.

J. Regression / safety

  • J1 — First-party widgets unaffected. Fields/table/collection/map/etc. still add, render,
    export, and import exactly as before (the shared WidgetComponentMap change is additive).
  • J2 — Byte-for-byte export for unaffected definitions. A case definition with no
    external-plugin widgets exports/imports unchanged.
  • J3 — Plain (non-wizard) re-import. Re-deploying a case definition (no interactive mapping,
    empty pluginConfigurationMappings) into the same environment keeps the original config ids
    and resolves normally (does not accidentally dangle widgets).
  • J4 — Dual-DB. Repeat the core add → render → export → import → repair → delete-guard loop on
    MySQL as well as PostgreSQL (the dedicated STI columns + queries are dual-DB).

@ivo-ritense
ivo-ritense requested review from a team as code owners July 31, 2026 13:25
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2ae305fe-ad5c-450a-859c-1c00aa063d14

📥 Commits

Reviewing files that changed from the base of the PR and between a49730e and 4827053.

📒 Files selected for processing (10)
  • backend/case/src/main/kotlin/com/ritense/case_/configuration/CaseWidgetAutoConfiguration.kt
  • frontend/projects/valtimo/case-management/src/lib/components/case-management-detail/tabs/case-management-tabs/widget-tab/case-management-widget-tab/case-management-widget-tab.component.ts
  • frontend/projects/valtimo/case/src/lib/components/case-detail/tab/widgets/widgets.component.ts
  • frontend/projects/valtimo/layout/src/lib/components/widget-management/management-content/index.ts
  • frontend/projects/valtimo/layout/src/lib/models/widget-content.model.ts
  • frontend/projects/valtimo/layout/src/lib/models/widget-editor.model.ts
  • frontend/projects/valtimo/layout/src/lib/models/widget-wizard.model.ts
  • frontend/projects/valtimo/layout/src/lib/models/widget.model.ts
  • frontend/projects/valtimo/shared/assets/core/en.json
  • frontend/projects/valtimo/shared/assets/core/nl.json
🚧 Files skipped from review as they are similar to previous changes (10)
  • frontend/projects/valtimo/layout/src/lib/components/widget-management/management-content/index.ts
  • frontend/projects/valtimo/layout/src/lib/models/widget-editor.model.ts
  • frontend/projects/valtimo/layout/src/lib/models/widget-wizard.model.ts
  • frontend/projects/valtimo/shared/assets/core/nl.json
  • frontend/projects/valtimo/shared/assets/core/en.json
  • frontend/projects/valtimo/case-management/src/lib/components/case-management-detail/tabs/case-management-tabs/widget-tab/case-management-widget-tab/case-management-widget-tab.component.ts
  • frontend/projects/valtimo/layout/src/lib/models/widget.model.ts
  • frontend/projects/valtimo/layout/src/lib/models/widget-content.model.ts
  • frontend/projects/valtimo/case/src/lib/components/case-detail/tab/widgets/widgets.component.ts
  • backend/case/src/main/kotlin/com/ritense/case_/configuration/CaseWidgetAutoConfiguration.kt

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added external-plugin widgets for case tabs, with configurable bundles and runtime iframe rendering.
    • Added widget configuration and bundle selection, including dynamic availability in widget management.
    • Added loading, error, and unavailable states for external widgets.
    • Added import/export support, configuration remapping, and import previews.
    • Added plugin usage tracking and deletion safeguards for referenced widgets.
    • Added sample case-summary and metrics widgets with English and Dutch translations.
  • Bug Fixes
    • Improved handling of missing or unresolved plugin configurations and bundles.
    • Added detection and reporting for missing widget configurations.

Walkthrough

Added external-plugin case widgets across the backend and frontend. The change persists widget configuration and bundle metadata, resolves plugin bundles, supports import/export remapping, reports configuration usage, and detects dangling references. The frontend adds widget configuration, dynamic availability, iframe rendering, lifecycle states, translations, and sample case-summary widgets.

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch story/external-plugin-case-widget

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ivo-ritense
ivo-ritense changed the base branch from next-minor to feature/external-plugin-system July 31, 2026 13:26
@mbritense mbritense self-assigned this Aug 4, 2026
@mbritense

Copy link
Copy Markdown
Contributor

Bugs

  • Unavailable/error widget alone on a tab → infinite spinner. CaseWidgetExternalPluginComponent only calls setWidgetDataLoaded on iframe-ready, so a WIDGETS tab (or divider group) containing only external-plugin widgets in the unavailable/error state never leaves the container's loading state — the "unavailable" message stays visibility: hidden behind a spinner forever. This is exactly the post-import dangling scenario (E3/E4). Fix: also call it in the error/unavailable handler (first-party widgets do this on 404), case-widget-external-plugin.component.ts:119.
  • Raw i18n keys shown. Translations were added under widgetTabManagement.type.externalPlugin.* (camelCase), but the wizard progress list and the admin widget table derive type.${enumValue}.titlewidgetTabManagement.type.external-plugin.title renders literally (every saved widget's Type column, plus the wizard step subtitle). Convention is enum-value keys (interactive-table, person-card) — rename the en/nl JSON key to external-plugin and update titleKey/descriptionKey in widget-wizard.model.ts.
  • In-use modal doesn't show widget usages. Backend PluginUsageDto.widgetKey is populated (verified in the 409 payloads for config and host delete), but PluginUsageModalComponent was never extended — widget usages render as indistinguishable "Zaaktabblad" rows without the widget key (G1 expects it).
  • Card chrome parity gaps. The required title and selected icon are never rendered on the case card (every first-party widget renders its own header section), and isCompact has no visual effect — while the wizard still collects title (required), icon and density for this type. Either render the standard header or drop icon/density for this type. Background color + high-contrast do apply.

Minor / cosmetic

  • Widget error state reuses the tab's copy: "Het externe plugin-tabblad kon niet worden geladen…".
  • 409 detail text says "process links or case tabs" — widgets missing from the sentence (payload itself is correct).
  • Stale KDoc on ExternalPluginCaseWidget, ExternalPluginWidgetProperties and the Liquibase changelog claims dangling widgets get a null config id — the importer (correctly) keeps the original id; recheckIssuesForCaseDefinition KDoc says "three" surfaces, now four.
  • Widget saved via management REST/JSON editor with an unresolvable config id raises no issue until an import/repair recheck (not reachable via the normal UI).

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
backend/external-plugin/src/main/kotlin/com/ritense/externalplugin/service/ExternalPluginConfigurationMappingResolver.kt (1)

130-140: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Recheck widget issues after every widget-tab mutation.

This method is invoked after import, but management REST and JSON editing can persist an unresolvable widget configuration without invoking it. The widget then remains unavailable and the missing-configuration flow does not appear.

After CaseWidgetService creates, updates, or deletes a widget tab, invoke recheckIssuesForCaseDefinition in the same persistence lifecycle.

backend/case/src/main/kotlin/com/ritense/case_/configuration/CaseWidgetAutoConfiguration.kt (1)

202-208: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Condition the default widget resolver on the SPI interface.

ExternalPluginAutoConfiguration only skips creating ExternalPluginCaseWidgetResolverImpl when another ExternalPluginCaseWidgetResolverImpl bean exists. If a supplier defines a different ExternalPluginCaseWidgetResolver, this bean is still created and the Optional<ExternalPluginCaseWidgetResolver> injections in CaseWidgetTabExporter and ExternalPluginCaseWidgetDataProvider get multiple Spring candidates. Use @ConditionalOnMissingBean(ExternalPluginCaseWidgetResolver::class) in both auto-configurations.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 89843bee-a57f-4b20-aca2-22023704448a

📥 Commits

Reviewing files that changed from the base of the PR and between e7ab845 and a49730e.

📒 Files selected for processing (65)
  • backend/case/src/main/kotlin/com/ritense/case_/configuration/CaseWidgetAutoConfiguration.kt
  • backend/case/src/main/kotlin/com/ritense/case_/repository/ExternalPluginCaseWidgetRepository.kt
  • backend/case/src/main/kotlin/com/ritense/case_/rest/dto/ExternalPluginWidgetContentDto.kt
  • backend/case/src/main/kotlin/com/ritense/case_/service/CaseExternalPluginWidgetService.kt
  • backend/case/src/main/kotlin/com/ritense/case_/service/CaseWidgetTabExporter.kt
  • backend/case/src/main/kotlin/com/ritense/case_/service/CaseWidgetTabImporter.kt
  • backend/case/src/main/kotlin/com/ritense/case_/service/ExternalPluginCaseWidgetResolver.kt
  • backend/case/src/main/kotlin/com/ritense/case_/widget/externalplugin/ExternalPluginCaseWidget.kt
  • backend/case/src/main/kotlin/com/ritense/case_/widget/externalplugin/ExternalPluginCaseWidgetDataProvider.kt
  • backend/case/src/main/kotlin/com/ritense/case_/widget/externalplugin/ExternalPluginCaseWidgetDto.kt
  • backend/case/src/main/kotlin/com/ritense/case_/widget/externalplugin/ExternalPluginCaseWidgetMapper.kt
  • backend/case/src/main/kotlin/com/ritense/case_/widget/externalplugin/ExternalPluginWidgetProperties.kt
  • backend/case/src/test/kotlin/com/ritense/case_/service/CaseWidgetTabExporterTest.kt
  • backend/case/src/test/kotlin/com/ritense/case_/service/CaseWidgetTabImporterExternalPluginTest.kt
  • backend/case/src/test/kotlin/com/ritense/case_/widget/externalplugin/ExternalPluginCaseWidgetDataProviderTest.kt
  • backend/case/src/test/kotlin/com/ritense/case_/widget/externalplugin/ExternalPluginCaseWidgetMapperTest.kt
  • backend/core/src/main/resources/config/liquibase/13-32-0/13-32-0-master.xml
  • backend/core/src/main/resources/config/liquibase/13-32-0/20260731-add-external-plugin-case-widget.xml
  • backend/external-plugin/src/main/kotlin/com/ritense/externalplugin/autoconfigure/ExternalPluginAutoConfiguration.kt
  • backend/external-plugin/src/main/kotlin/com/ritense/externalplugin/exception/ExternalPluginConfigurationInUseException.kt
  • backend/external-plugin/src/main/kotlin/com/ritense/externalplugin/exception/ExternalPluginHostInUseException.kt
  • backend/external-plugin/src/main/kotlin/com/ritense/externalplugin/preview/ExternalPluginImportPreviewContributor.kt
  • backend/external-plugin/src/main/kotlin/com/ritense/externalplugin/service/ExternalPluginCaseWidgetResolverImpl.kt
  • backend/external-plugin/src/main/kotlin/com/ritense/externalplugin/service/ExternalPluginConfigurationMappingResolver.kt
  • backend/external-plugin/src/main/kotlin/com/ritense/externalplugin/service/ExternalPluginHostService.kt
  • backend/external-plugin/src/main/kotlin/com/ritense/externalplugin/service/ExternalPluginHostUsageResolver.kt
  • backend/external-plugin/src/test/kotlin/com/ritense/externalplugin/exception/ExternalPluginHostInUseExceptionTest.kt
  • backend/external-plugin/src/test/kotlin/com/ritense/externalplugin/preview/ExternalPluginImportPreviewContributorTest.kt
  • backend/external-plugin/src/test/kotlin/com/ritense/externalplugin/service/ExternalPluginCaseWidgetResolverImplTest.kt
  • backend/external-plugin/src/test/kotlin/com/ritense/externalplugin/service/ExternalPluginConfigurationMappingResolverTest.kt
  • backend/external-plugin/src/test/kotlin/com/ritense/externalplugin/service/ExternalPluginHostUsageResolverTest.kt
  • backend/plugin/src/main/kotlin/com/ritense/plugin/web/rest/dto/PluginUsageDto.kt
  • frontend/projects/valtimo/case-management/src/lib/components/case-management-detail/tabs/case-management-general/components/case-management-missing-plugin-configurations/case-management-missing-plugin-configurations.component.ts
  • frontend/projects/valtimo/case-management/src/lib/components/case-management-detail/tabs/case-management-tabs/widget-tab/case-management-widget-tab/case-management-widget-tab.component.html
  • frontend/projects/valtimo/case-management/src/lib/components/case-management-detail/tabs/case-management-tabs/widget-tab/case-management-widget-tab/case-management-widget-tab.component.ts
  • frontend/projects/valtimo/case-management/src/lib/services/tab.service.ts
  • frontend/projects/valtimo/case/src/lib/components/case-detail/tab/widgets/components/external-plugin/case-widget-external-plugin.component.html
  • frontend/projects/valtimo/case/src/lib/components/case-detail/tab/widgets/components/external-plugin/case-widget-external-plugin.component.scss
  • frontend/projects/valtimo/case/src/lib/components/case-detail/tab/widgets/components/external-plugin/case-widget-external-plugin.component.ts
  • frontend/projects/valtimo/case/src/lib/components/case-detail/tab/widgets/widgets.component.ts
  • frontend/projects/valtimo/case/src/lib/models/external-plugin-widget.model.ts
  • frontend/projects/valtimo/case/src/lib/models/index.ts
  • frontend/projects/valtimo/layout/src/lib/components/widget-management/management-content/external-plugin/widget-management-external-plugin.component.html
  • frontend/projects/valtimo/layout/src/lib/components/widget-management/management-content/external-plugin/widget-management-external-plugin.component.scss
  • frontend/projects/valtimo/layout/src/lib/components/widget-management/management-content/external-plugin/widget-management-external-plugin.component.ts
  • frontend/projects/valtimo/layout/src/lib/components/widget-management/management-content/index.ts
  • frontend/projects/valtimo/layout/src/lib/constants/external-plugin-widget-token.ts
  • frontend/projects/valtimo/layout/src/lib/constants/index.ts
  • frontend/projects/valtimo/layout/src/lib/models/widget-content.model.ts
  • frontend/projects/valtimo/layout/src/lib/models/widget-editor.model.ts
  • frontend/projects/valtimo/layout/src/lib/models/widget-wizard.model.ts
  • frontend/projects/valtimo/layout/src/lib/models/widget.model.ts
  • frontend/projects/valtimo/layout/src/lib/services/widget-wizard.service.ts
  • frontend/projects/valtimo/plugin-management/src/lib/components/plugin-usage-modal/plugin-usage-modal.component.html
  • frontend/projects/valtimo/plugin-management/src/lib/components/plugin-usage-modal/plugin-usage-modal.component.ts
  • frontend/projects/valtimo/plugin/src/lib/models/external-plugin.model.ts
  • frontend/projects/valtimo/plugin/src/lib/services/external-plugin.service.ts
  • frontend/projects/valtimo/shared/assets/core/en.json
  • frontend/projects/valtimo/shared/assets/core/nl.json
  • plugin-host/docs/external-plugin-system-plan.md
  • plugin-host/sample-plugins/case-summary/frontend/case-widget-metrics.html
  • plugin-host/sample-plugins/case-summary/frontend/case-widget-metrics.tsx
  • plugin-host/sample-plugins/case-summary/frontend/case-widget.html
  • plugin-host/sample-plugins/case-summary/frontend/case-widget.tsx
  • plugin-host/sample-plugins/case-summary/manifest.json

Comment on lines +117 to +120
/**
* One external-plugin widget of a case definition: the configuration it references (`null` when it
* imported dangling) plus the design-time plugin identity that keeps it identifiable in the repair
* panel. Consumed by the external-plugin dangling-repair resolver.

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the dangling-ID description.

An imported dangling widget retains its original configuration UUID. CaseWidgetTabImporter.remapExternalPluginWidgets preserves an unmapped ID so the repair flow can map it later. State that configurationId is null only when no ID is configured.

Comment on lines +62 to +71
/**
* A case widget is not a process link, so it gets no detection from the process-link importer.
* Trigger an in-transaction recheck here — for external-plugin widgets this is what raises the
* configuration issue when a widget references a plugin configuration missing in this environment
* (mirrors [CaseTabImporter.afterImport]).
*/
override fun afterImport(request: ImportRequest) {
val caseDefinitionId = request.caseDefinitionId ?: return
pluginConfigurationMappingResolvers.forEach { it.recheckIssuesForCaseDefinition(caseDefinitionId) }
}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline backend/case/src/main/kotlin/com/ritense/case_/service/CaseExternalPluginWidgetService.kt --items all
rg -n -C 4 'save|update|create|recheckIssuesForCaseDefinition' \
  backend/case/src/main/kotlin/com/ritense/case_/service/CaseExternalPluginWidgetService.kt \
  backend/case/src/main/kotlin/com/ritense/case_/service/CaseWidgetService.kt

Repository: valtimo-platform/valtimo

Length of output: 6801


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## Candidate files and usages"
rg -n -C 5 'recheckIssuesForCaseDefinition|CaseExternalPluginWidgetService|CaseWidgetService|CaseTabImporter|ImportRequest|afterImport|pluginConfigurationMappingResolvers' \
  backend/case/src/main/kotlin \
  || true

echo
echo "## Service files snippets"
wc -l backend/case/src/main/kotlin/com/ritense/case_/service/CaseExternalPluginWidgetService.kt backend/case/src/main/kotlin/com/ritense/case_/service/CaseWidgetService.kt
sed -n '1,180p' backend/case/src/main/kotlin/com/ritense/case_/service/CaseExternalPluginWidgetService.kt
sed -n '1,180p' backend/case/src/main/kotlin/com/ritense/case_/service/CaseWidgetService.kt

Repository: valtimo-platform/valtimo

Length of output: 50380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## focused recheck usages"
rg -n -C 5 'recheckIssuesForCaseDefinition' backend/case/src/main/kotlin || true

echo
echo "## focused widget write/resource methods"
sed -n '70,220p' backend/case/src/main/kotlin/com/ritense/case_/rest/CaseWidgetTabResource.kt
sed -n '40,180p' backend/case/src/main/kotlin/com/ritense/case_/rest/CaseWidgetTabManagementResource.kt
sed -n '70,165p' backend/case/src/main/kotlin/com/ritense/case_/service/CaseWidgetService.kt
sed -n '50,140p' backend/case/src/main/kotlin/com/ritense/case_/service/CaseExternalPluginWidgetService.kt

echo
echo "## create/update identifiers in widgets and resources"
rg -n -C 3 'createWidget|updateWidget|saveWidgetTab|save\\(|external-plugin|externalPluginConfigurationId|widgetKey|key' \
  backend/case/src/main/kotlin/com/ritense/case_/rest backend/case/src/main/kotlin/com/ritense/case_/service || true

Repository: valtimo-platform/valtimo

Length of output: 13578


Trigger the same widget recheck from non-import write paths.

recheckIssuesForCaseDefinition is only called from importer afterImport, but updateWidgetTab and the importer deploy/copy paths can persist external-plugin widgets. Call the resolver recheck for the updated case definition after each non-import widget write, including remapConfiguration.

Comment on lines +48 to +52
override fun validate(caseDefinitionId: CaseDefinitionId) {
require(properties.configurationId != null) {
"External-plugin widget '$key' must reference a plugin configuration."
}
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect normal widget write paths and import-specific handling.
fd 'CaseExternalPluginWidgetService\.kt' backend/case
ast-grep outline backend/case/src/main/kotlin/com/ritense/case_/service/CaseExternalPluginWidgetService.kt --items all
rg -n -C 5 \
  'ExternalPluginCaseWidgetDto|ExternalPluginCaseWidgetResolver|configurationId|validate\(|dangling' \
  backend/case/src/main/kotlin

Repository: valtimo-platform/valtimo

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- candidate files ---\n'
fd -a 'ExternalPluginCaseWidgetDto\.kt|CaseWidgetTabDto\.kt|ExternalPluginCaseWidgetResolver\.kt|CaseWidgetService\.kt|CaseWidgetTabImporter\.kt|CaseWidgetTabExporter\.kt|CaseExternalPluginWidgetService\.kt' backend/case

printf '\n--- DTOs snippet ---\n'
cat -n backend/case/src/main/kotlin/com/ritense/case_/widget/externalplugin/ExternalPluginCaseWidgetDto.kt
printf '\n'
cat -n backend/case/src/main/kotlin/com/ritense/case_/rest/dto/CaseWidgetTabWidgetDto.kt
printf '\n'
cat -n backend/case/src/main/kotlin/com/ritense/case_/rest/dto/CaseWidgetTabDto.kt

printf '\n--- service/import/export snippets ---\n'
sed -n '1,240p' backend/case/src/main/kotlin/com/ritense/case_/service/CaseWidgetTabImporter.kt | cat -n
printf '\n--- CaseWidgetService methods relevant to update/create ---\n'
rg -n -A 8 -B 3 'fun .*Widget|updateWidget|createWidget|findById|validate' backend/case/src/main/kotlin/com/ritense/case_/service/CaseWidgetService.kt backend/case/src/main/kotlin/com/ritense/case_/service/CaseWidgetTabManagementService.kt

Repository: valtimo-platform/valtimo

Length of output: 253


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- candidate files ---'
fd 'ExternalPluginCaseWidgetDto\.kt|CaseWidgetTabDto\.kt|ExternalPluginCaseWidgetResolver\.kt|CaseWidgetService\.kt|CaseWidgetTabImporter\.kt|CaseWidgetTabExporter\.kt|CaseExternalPluginWidgetService\.kt' backend/case || true

printf '%s\n' ''
printf '%s\n' '--- ExternalPluginCaseWidgetDto.kt ---'
cat -n backend/case/src/main/kotlin/com/ritense/case_/widget/externalplugin/ExternalPluginCaseWidgetDto.kt

printf '%s\n' ''
printf '%s\n' '--- CaseWidgetTabWidgetDto.kt ---'
cat -n backend/case/src/main/kotlin/com/ritense/case_/rest/dto/CaseWidgetTabWidgetDto.kt

printf '%s\n' ''
printf '%s\n' '--- CaseWidgetTabDto.kt ---'
cat -n backend/case/src/main/kotlin/com/ritense/case_/rest/dto/CaseWidgetTabDto.kt

printf '%s\n' ''
printf '%s\n
' '--- CaseWidgetTabImporter.kt ---'
sed -n '1,240p' backend/case/src/main/kotlin/com/ritense/case_/service/CaseWidgetTabImporter.kt | cat -n

printf '%s\n' ''
printf '%s\n' '--- CaseWidgetService.kt relevant methods ---'
rg -n -A 8 -B 3 'fun .*Widget|updateWidget|createWidget|findById|validate' backend/case/src/main/kotlin/com/ritense/case_/service/CaseWidgetService.kt backend/case/src/main/kotlin/com/ritense/case_/service/CaseWidgetTabManagementService.kt || true

Repository: valtimo-platform/valtimo

Length of output: 35185


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- CaseWidgetTabManagerResource.kt ---'
fd 'CaseWidgetTabManagementResource\.kt' backend/case -x sh -c 'cat -n "$1"' sh {}

printf '%s\n' '--- full updateWidgetTab implementation ---'
sed -n '114,134p' backend/case/src/main/kotlin/com/ritense/case_/service/CaseWidgetService.kt | cat -n

printf '%s\n' '--- CaseDefinitionId.of validation ---'
rg -n -A 8 -B 3 'fun CaseDefinitionId\.of|data class CaseDefinitionId|class CaseDefinitionId|assertCanUpdateCaseDefinition' backend/case backend/valtimo-contract || true

printf '%s\n' '--- mapper for external plugin widgets ---'
fd 'ExternalPluginCaseWidgetMapper\.kt|ExternalPluginWidgetProperties\.kt|ExternalPluginCaseWidget\.kt' backend/case -x sh -c 'printf "\n--- $1 ---\n"; cat -n "$1"' sh {}

printf '%s\n' '--- external plugin configuration issue endpoints/repo/service references ---'
rg -n -A 4 -B 2 'dangling-plugin-configurations|plugin-configuration-mappings|recheckIssues|dangling' backend/valtimo-contract backend/case || true

Repository: valtimo-platform/valtimo

Length of output: 50380


Reject or immediately repair unresolved external-plugin configuration IDs on widget writes.

ExternalPluginCaseWidgetDto.validate only requires a non-null UUID, so /widget-tab updates and JSON-based writes can persist widgets with unresolvable references. Import mapping must still allow unmapped dangling IDs, but normal writes should resolve the referenced configuration or persist a repairable dangling-reference issue immediately.

Comment on lines +32 to 33
"One or more BPMN process links, case tabs or case widgets reference this configuration. " +
"Remove the references before deleting the configuration.",

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

List building-block mappings in both conflict details.

Both deletion guards can return building-block mapping usages. The messages name only process links, case tabs, and case widgets. When a building-block mapping is the only blocker, the detail gives an incorrect reason.

  • backend/external-plugin/src/main/kotlin/com/ritense/externalplugin/exception/ExternalPluginConfigurationInUseException.kt#L32-L33: Add building-block mappings to the configuration deletion detail.
  • backend/external-plugin/src/main/kotlin/com/ritense/externalplugin/exception/ExternalPluginHostInUseException.kt#L32-L33: Add building-block mappings to the host deletion detail.
📍 Affects 2 files
  • backend/external-plugin/src/main/kotlin/com/ritense/externalplugin/exception/ExternalPluginConfigurationInUseException.kt#L32-L33 (this comment)
  • backend/external-plugin/src/main/kotlin/com/ritense/externalplugin/exception/ExternalPluginHostInUseException.kt#L32-L33

assertThat(exception.title).isEqualTo("External plugin host is in use")
assertThat(exception.status).isEqualTo(Status.CONFLICT)
assertThat(exception.detail).contains("BPMN process links reference")
assertThat(exception.detail).contains("BPMN process links, case tabs or case widgets reference")

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Describe building-block mapping deletion blockers.

The host usage resolver includes building-block mapping usages. The exception detail still omits them. Update ExternalPluginHostInUseException and this assertion so a 409 identifies all supported reference types.

Proposed test update
-assertThat(exception.detail).contains("BPMN process links, case tabs or case widgets reference")
+assertThat(exception.detail).contains("case widgets or building-block mappings reference")

Comment on lines +48 to +71
[label]="'widgetTabManagement.content.externalPlugin.configLabel' | translate"
[placeholder]="'widgetTabManagement.content.externalPlugin.configPlaceholder' | translate"
(selected)="onConfigSelected($event)"
>
<cds-dropdown-list [cdsLayer]="1"></cds-dropdown-list>
</cds-combo-box>

<cds-combo-box
*ngIf="$showBundleSelect()"
[cdsLayer]="1"
[dropUp]="false"
[items]="$bundleItems()"
itemValueKey="bundleKey"
[label]="'widgetTabManagement.content.externalPlugin.bundleLabel' | translate"
[placeholder]="'widgetTabManagement.content.externalPlugin.bundlePlaceholder' | translate"
(selected)="onBundleSelected($event)"
>
<cds-dropdown-list [cdsLayer]="1"></cds-dropdown-list>
</cds-combo-box>
</ng-container>

<ng-template #pluginUnavailable>
<p class="valtimo-widget-management-external-plugin__unavailable">
{{ 'widgetTabManagement.content.externalPlugin.unavailableMessage' | translate }}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the hyphenated external-plugin translation-key segment.

externalPlugin does not match the enum-value translation keys. These labels render raw translation keys when the entries are not found. Replace externalPlugin with external-plugin for the configuration, bundle, and unavailable-message keys.

Comment on lines +22 to +24
::ng-deep .cds--text-input,
::ng-deep .cds--list-box,
::ng-deep .cds--list-box__field {

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Resolve the Stylelint errors for ::ng-deep.

The current Stylelint configuration reports each ::ng-deep selector as an error. Use a supported scoped styling method, or add a targeted project-approved Stylelint exception.

🧰 Tools
🪛 Stylelint (17.14.0)

[error] 22-22: Unknown pseudo-element selector "::ng-deep" (selector-pseudo-element-no-unknown)

(selector-pseudo-element-no-unknown)


[error] 23-23: Unknown pseudo-element selector "::ng-deep" (selector-pseudo-element-no-unknown)

(selector-pseudo-element-no-unknown)


[error] 24-24: Unknown pseudo-element selector "::ng-deep" (selector-pseudo-element-no-unknown)

(selector-pseudo-element-no-unknown)

Source: Linters/SAST tools

Comment on lines +119 to +123
effect(() =>
this.widgetWizardService.$widgetContentValid.set(
!!this._$selectedConfigId() && (!this.$showBundleSelect() || !!this._$selectedBundleKey())
)
);

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Require a resolved configuration before the wizard is valid.

A prefetched configuration ID can remain after getConfigOptions() filters that configuration out. In that state, $showBundleSelect() is false and this effect marks the widget content valid. The user can then save a widget with an unavailable configuration.

Require _$selectedConfig() to exist. Also validate the selected bundle against that configuration before setting $widgetContentValid.

"GLOBAL": "Global"
},
"caseTab": "Case tab",
"caseWidget": "Case widget",

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Include case widgets in all deletion-usage descriptions.

A configuration or host deletion can be blocked by a case widget. The current UI text lists only process links, case tabs, and building blocks. The system-plan text also describes only two usage payload shapes.

  • frontend/projects/valtimo/shared/assets/core/en.json#L2381-L2381: Add “case widgets” to hostInUseModal.description and configurationInUseModal.description.
  • frontend/projects/valtimo/shared/assets/core/nl.json#L2408-L2408: Add the Dutch case-widget equivalent to both deletion descriptions.
  • plugin-host/docs/external-plugin-system-plan.md#L912-L912: Update the host deletion row and PluginUsageDto description to include widget usages and widgetKey.
📍 Affects 3 files
  • frontend/projects/valtimo/shared/assets/core/en.json#L2381-L2381 (this comment)
  • frontend/projects/valtimo/shared/assets/core/nl.json#L2408-L2408
  • plugin-host/docs/external-plugin-system-plan.md#L912-L912

Comment on lines +1 to +16
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Case Summary — Metrics Widget</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'IBM Plex Sans', sans-serif; background: transparent; }
</style>
</head>
<body>
<div id="root"></div>
<script src="case-widget-metrics.bundle.js"></script>
</body>
</html>

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add the required license header.

Both new HTML files start with a doctype. Add the standard EUPL HTML comment header before it.

  • plugin-host/sample-plugins/case-summary/frontend/case-widget-metrics.html#L1-L16: Insert the standard HTML license header before <!DOCTYPE html>.
  • plugin-host/sample-plugins/case-summary/frontend/case-widget.html#L1-L16: Insert the standard HTML license header before <!DOCTYPE html>.

As per path instructions, “**/*.{kt,java,ts,html}: Please verify that each file starts with the standard license header.”

📍 Affects 2 files
  • plugin-host/sample-plugins/case-summary/frontend/case-widget-metrics.html#L1-L16 (this comment)
  • plugin-host/sample-plugins/case-summary/frontend/case-widget.html#L1-L16

Source: Path instructions

…into story/external-plugin-case-widget

# Conflicts:
#	frontend/projects/valtimo/case/src/lib/components/case-detail/tab/widgets/widgets.component.ts
#	frontend/projects/valtimo/layout/src/lib/models/widget-editor.model.ts
#	frontend/projects/valtimo/layout/src/lib/models/widget-wizard.model.ts
#	frontend/projects/valtimo/layout/src/lib/models/widget.model.ts
#	frontend/projects/valtimo/shared/assets/core/en.json
#	frontend/projects/valtimo/shared/assets/core/nl.json
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants