Fix view function source button in designer mode 36/143536/1
authorFiete Ostkamp <fiete.ostkamp@telekom.de>
Mon, 9 Mar 2026 06:31:37 +0000 (07:31 +0100)
committerFiete Ostkamp <fiete.ostkamp@telekom.de>
Mon, 9 Mar 2026 06:31:37 +0000 (07:31 +0100)
- add functionality for already existing button
- opens modal that displays json definition of function

Issue-ID: CCSDK-4179
Change-Id: I3f25d987c9c73478758faf40aa024d42ebff25b0
Signed-off-by: Fiete Ostkamp <fiete.ostkamp@telekom.de>
cds-ui/designer-client/src/app/modules/feature-modules/packages/designer/designer.component.css
cds-ui/designer-client/src/app/modules/feature-modules/packages/designer/designer.component.html
cds-ui/designer-client/src/app/modules/feature-modules/packages/designer/designer.component.ts
cds-ui/designer-client/src/app/modules/feature-modules/packages/package-creation/metadata-tab/metadata-tab.component.html
cds-ui/e2e-playwright/tests/designer.spec.ts

index d6e97b1..f270efc 100644 (file)
@@ -782,6 +782,17 @@ p.compType-4{
   color: #fff;
   font-size: 11px;
 }
+.source-json-pre{
+  background: #1B3E6F;
+  color: #E0E0E0;
+  padding: 16px;
+  border-radius: 4px;
+  font-size: 13px;
+  max-height: 60vh;
+  overflow: auto;
+  white-space: pre-wrap;
+  word-break: break-word;
+}
 .trash-item,
 .trash-item:hover{
   background: #fff;
index 1b47525..bcaad81 100644 (file)
                         <div class="col-3 pl-0">
                             <div class="btn-group" role="group" aria-label="Basic example">
                                 <button type="button" class="btn view-source" tooltip="View Action Source"
-                                    placement="bottom"><i class="icon-source"></i></button>
+                                    placement="bottom" (click)="viewActionSource()"><i class="icon-source" aria-hidden="true"></i></button>
                                 <button type="button" data-toggle="modal" data-target="#deleteActionModal"
                                     class="btn trash-item" tooltip="Delete Action" placement="bottom"><i
                                         class="icon-delete-sm" aria-hidden="true"></i></button>
                         <div class="col-3 pl-0">
                             <div class="btn-group" role="group" aria-label="Basic example">
                                 <button type="button" class="btn view-source" tooltip="View Function Source"
-                                    placement="bottom"><i class="icon-source"></i></button>
+                                    placement="bottom" (click)="viewFunctionSource()"><i class="icon-source"></i></button>
                                 <button type="button" class="btn trash-item" tooltip="Delete Function"
                                     placement="bottom"><i class="icon-delete-sm" type="button"
                                         aria-hidden="true"></i></button>
         </div>
     </div>
 </div>
+
+<!--View Source Modal-->
+<div class="modal fade" id="viewSourceModal" tabindex="-1" role="dialog"
+    aria-labelledby="viewSourceModalTitle" aria-hidden="true">
+    <div class="modal-dialog modal-lg modal-dialog-scrollable" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title" id="viewSourceModalTitle">{{viewSourceTitle}}</h5>
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                    <img src="assets/img/icon-close.svg" />
+                </button>
+            </div>
+            <div class="modal-body">
+                <pre class="source-json-pre">{{viewSourceContent}}</pre>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
+            </div>
+        </div>
+    </div>
+</div>
index ef28918..d9a4743 100644 (file)
@@ -97,6 +97,8 @@ export class DesignerComponent implements OnInit, OnDestroy {
     designerState: DesignerDashboardState;
     currentActionName: string;
     packageId: any;
+    viewSourceContent = '';
+    viewSourceTitle = '';
     private isDeletingAction = false;
     private suppressSidebarOpenUntil = 0;
     private deleteActionModalHiddenHandler = () => {
@@ -786,6 +788,30 @@ export class DesignerComponent implements OnInit, OnDestroy {
         this.designerStore.setCurrentFunction(currentStep['target']);
     }
 
+    viewFunctionSource() {
+        const funcName = this.designerState.functionName;
+        const nodeTemplate = funcName
+            ? this.designerState.template.node_templates[funcName]
+            : null;
+        this.viewSourceTitle = 'Function Source: ' + (funcName || '');
+        this.viewSourceContent = nodeTemplate
+            ? JSON.stringify(nodeTemplate, null, 2)
+            : '{ }';
+        ($('#viewSourceModal') as any).modal('show');
+    }
+
+    viewActionSource() {
+        const actionName = this.currentActionName;
+        const workflow = actionName
+            ? this.designerState.template.workflows[actionName]
+            : null;
+        this.viewSourceTitle = 'Action Source: ' + (actionName || '');
+        this.viewSourceContent = workflow
+            ? JSON.stringify(workflow, null, 2)
+            : '{ }';
+        ($('#viewSourceModal') as any).modal('show');
+    }
+
     getTarget(stepname) {
         try {
             //  console.log(this.currentActionName + " -- " + stepname)
index cca0dbc..7bb63fa 100644 (file)
@@ -3,7 +3,7 @@
     <div class="single-line-model">
         <label class="label-name">Mode</label>
         <div class="label-input">
-            <label name="trst" *ngFor="let mode of modes; let i = index" class="pl-0">
+            <label name="trst" *ngFor="let mode of modes; let i = index">
                 <input class="form-check-input" [(ngModel)]="modeType" type="radio" name="radioMode" id="radioMode"
                     [value]="mode.name">
 
index 4b3a227..48d8bb4 100644 (file)
@@ -840,3 +840,163 @@ test.describe('Designer – Action Deletion', () => {
         ).toHaveCount(1);
     });
 });
+
+// ── View Source ───────────────────────────────────────────────────────────────
+//
+// These tests verify that the "View Action Source" and "View Function Source"
+// buttons in the attribute side-panes open a modal containing the JSON source
+// of the respective action workflow or function node_template.
+//
+// Uses the real fixture blueprint (RT-resource-resolution) so the designer
+// store contains actual topology data:
+//   workflow:      resource-resolution
+//   node_template: resource-resolution  (step name on canvas: helloworld)
+
+test.describe('Designer – View Source', () => {
+
+    const sourceModal = '#viewSourceModal';
+
+    /** Returns the viewport center {x, y} of the first JointJS canvas element
+     *  whose #label tspan contains exactly the given text. */
+    async function getCanvasElementCenter(
+        page: import('@playwright/test').Page,
+        label: string,
+    ): Promise<{ x: number; y: number }> {
+        await expect(
+            page.locator('#board-paper tspan').filter({ hasText: label }).first(),
+        ).toBeAttached({ timeout: 30_000 });
+
+        const center = await page.evaluate((lbl: string) => {
+            const tspans = Array.from(
+                document.querySelectorAll<SVGTSpanElement>('#board-paper tspan[id="label"]'),
+            );
+            const match = tspans.find(t => t.textContent?.trim() === lbl);
+            if (!match) { return null; }
+            const r = match.getBoundingClientRect();
+            return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
+        }, label);
+
+        if (!center) { throw new Error(`Canvas element labeled "${label}" not found`); }
+        return center;
+    }
+
+    /** Click on the action node to open the Action Attributes pane. */
+    async function openActionPane(page: import('@playwright/test').Page) {
+        const { x, y } = await getCanvasElementCenter(page, 'resource-resolution');
+        await page.mouse.move(x, y);
+        await page.mouse.down();
+        await page.mouse.up();
+        await expect(
+            page.locator('h6:has-text("Action Attributes")'),
+        ).toBeInViewport({ timeout: 5_000 });
+    }
+
+    /** Click on the function node to open the Function Attributes pane. */
+    async function openFunctionPane(page: import('@playwright/test').Page) {
+        // We must first click the action to set currentActionName, then click
+        // the function.  The fixture has action "resource-resolution" with
+        // embedded function "helloworld".
+        const actionCenter = await getCanvasElementCenter(page, 'resource-resolution');
+        await page.mouse.move(actionCenter.x, actionCenter.y);
+        await page.mouse.down();
+        await page.mouse.up();
+        await expect(
+            page.locator('h6:has-text("Action Attributes")'),
+        ).toBeInViewport({ timeout: 5_000 });
+
+        const funcCenter = await getCanvasElementCenter(page, 'helloworld');
+        await page.mouse.move(funcCenter.x, funcCenter.y);
+        await page.mouse.down();
+        await page.mouse.up();
+        await expect(
+            page.locator('h6:has-text("Function Attributes")'),
+        ).toBeInViewport({ timeout: 5_000 });
+    }
+
+    test.beforeEach(async ({ page }) => {
+        await gotoDesigner(page);
+        await waitForBoardPaper(page);
+    });
+
+    // ── Action Source ────────────────────────────────────────────────────────
+
+    test('View Action Source button opens modal with workflow JSON', async ({ page }) => {
+        await openActionPane(page);
+
+        const viewBtn = page.locator('.attributesSideBar .btn.view-source').first();
+        await viewBtn.click();
+
+        const modal = page.locator(sourceModal);
+        await expect(modal).toBeVisible({ timeout: 5_000 });
+
+        // Title should identify the action.
+        await expect(modal.locator('.modal-title')).toContainText('Action Source');
+        await expect(modal.locator('.modal-title')).toContainText('resource-resolution');
+
+        // Body should contain workflow JSON with the step name.
+        const pre = modal.locator('pre.source-json-pre');
+        await expect(pre).toBeVisible();
+        const text = await pre.textContent() ?? '';
+        expect(text).toContain('"steps"');
+        expect(text).toContain('"helloworld"');
+    });
+
+    test('View Action Source modal can be closed', async ({ page }) => {
+        await openActionPane(page);
+
+        page.locator('.attributesSideBar .btn.view-source').first().click();
+        await expect(page.locator(sourceModal)).toBeVisible({ timeout: 5_000 });
+
+        await page.locator(`${sourceModal} .btn-secondary`).click();
+        await expect(page.locator(sourceModal)).not.toBeVisible({ timeout: 5_000 });
+    });
+
+    // ── Function Source ──────────────────────────────────────────────────────
+
+    test('View Function Source button opens modal with node_template JSON', async ({ page }) => {
+        await openFunctionPane(page);
+
+        // The function pane's view-source button is the second .btn.view-source
+        // in the DOM (first is from the action pane, which is hidden but still
+        // present).  Target the visible one.
+        const viewBtn = page.locator('.attributesSideBar .btn.view-source:visible').first();
+        await viewBtn.click();
+
+        const modal = page.locator(sourceModal);
+        await expect(modal).toBeVisible({ timeout: 5_000 });
+
+        await expect(modal.locator('.modal-title')).toContainText('Function Source');
+        await expect(modal.locator('.modal-title')).toContainText('resource-resolution');
+
+        const pre = modal.locator('pre.source-json-pre');
+        await expect(pre).toBeVisible();
+        const text = await pre.textContent() ?? '';
+        expect(text).toContain('"type"');
+        expect(text).toContain('component-resource-resolution');
+        expect(text).toContain('"interfaces"');
+    });
+
+    test('View Function Source modal can be closed', async ({ page }) => {
+        await openFunctionPane(page);
+
+        page.locator('.attributesSideBar .btn.view-source:visible').first().click();
+        await expect(page.locator(sourceModal)).toBeVisible({ timeout: 5_000 });
+
+        await page.locator(`${sourceModal} .btn-secondary`).click();
+        await expect(page.locator(sourceModal)).not.toBeVisible({ timeout: 5_000 });
+    });
+
+    test('View Function Source JSON is valid parseable JSON', async ({ page }) => {
+        await openFunctionPane(page);
+
+        page.locator('.attributesSideBar .btn.view-source:visible').first().click();
+        const modal = page.locator(sourceModal);
+        await expect(modal).toBeVisible({ timeout: 5_000 });
+
+        const text = await modal.locator('pre.source-json-pre').textContent() ?? '';
+        expect(() => JSON.parse(text)).not.toThrow();
+        const parsed = JSON.parse(text);
+        expect(parsed).toHaveProperty('type');
+        expect(parsed).toHaveProperty('interfaces');
+    });
+});