diff --git a/README.md b/README.md
index d1c115f..e1b466e 100644
--- a/README.md
+++ b/README.md
@@ -106,6 +106,12 @@ when set to ``true`` removes the following UI elements for a leaner experience:
- Titles on embedded entities (since they are already shown in the entity list header)
- Action names (since the title already defines which action is which)
+### ``showSearch``
+
+default: ``true``
+
+when set to ``false`` removes the search box from the toolbar above the entity view, regardless of the user's general settings.
+
### ``autoFollowActionLocationOnSuccess``
default: ``false``
@@ -161,6 +167,11 @@ The following media types are supported for integrated preview:
- Configuration: `text/toml`, `text/yaml`
- Binary/Generic: `application/octet-stream` (can be manually rendered as text with syntax highlighting options).
+## Limitations
+
+- **Nullable objects in action parameters:** a nullable object can not be left unset.
+ It is always sent as an object, never as `null`, and its required properties must be filled before the action can be submitted.
+
## 💚 Many thanks to our dear sponsors
diff --git a/e2e/hypermedia-ui.spec.ts b/e2e/hypermedia-ui.spec.ts
index d83a989..943b72a 100644
--- a/e2e/hypermedia-ui.spec.ts
+++ b/e2e/hypermedia-ui.spec.ts
@@ -24,6 +24,14 @@ const entryPoint = {
class: ['Summary'],
title: 'Embedded summary',
properties: { status: 'ready' },
+ entities: [
+ {
+ rel: ['detail'],
+ class: ['Detail'],
+ title: 'Nested detail',
+ properties: { depth: 'nested-value' },
+ },
+ ],
},
{
rel: ['customer-reference'],
@@ -299,6 +307,114 @@ test('shows raw Siren data and embedded entities', async ({ page }) => {
await expect(page.getByText('Customer 42', { exact: true })).toBeVisible();
});
+test('navigates between top level embedded entities with keys, buttons and clicks', async ({ page }) => {
+ await openEntryPoint(page);
+ const navigation = page.getByRole('navigation', { name: 'Page navigation' });
+ const summary = page.locator('mat-expansion-panel', { hasText: 'Embedded summary' });
+ const linkedCustomer = page.locator('mat-card', { hasText: 'Linked customer' });
+ const current = page.locator('.embedded-nav-current');
+ await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur());
+
+ await expect(current).toHaveCount(0);
+ // collapsed to a handle until hovered
+ const buttonsWidth = async () => (await navigation.locator('.buttons').boundingBox())?.width ?? 0;
+ await expect.poll(buttonsWidth).toBe(0);
+ await navigation.hover();
+ await expect.poll(buttonsWidth).toBe(4 * 40 + 3 * 4);
+ const buttonsBox = await navigation.locator('.buttons').boundingBox();
+ const bottomButtonBox = await navigation.getByRole('button', { name: 'To bottom' }).boundingBox();
+ expect(bottomButtonBox!.x + bottomButtonBox!.width).toBeLessThanOrEqual(buttonsBox!.x + buttonsBox!.width + 0.5);
+ await page.mouse.move(0, 0);
+ await page.keyboard.press('ArrowDown');
+ await expect(summary).toHaveClass(/embedded-nav-current/);
+ const summaryHeader = summary.locator('mat-expansion-panel-header').first();
+ await page.keyboard.press('Enter');
+ await expect(summaryHeader).toHaveAttribute('aria-expanded', 'true');
+ await page.keyboard.press('Enter');
+ await expect(summaryHeader).toHaveAttribute('aria-expanded', 'false');
+ await summaryHeader.hover();
+ await expect(page.locator('.mat-mdc-tooltip')).toHaveText('Expand / collapse (Enter while selected)');
+ await page.mouse.move(0, 0);
+ await page.keyboard.press('ArrowDown');
+ await expect(linkedCustomer).toHaveClass(/embedded-nav-current/);
+ await expect(navigation.getByRole('button', { name: 'Next embedded entity' })).toBeDisabled();
+
+ await navigation.hover();
+ await navigation.getByRole('button', { name: 'Previous embedded entity' }).click();
+ await expect(summary).toHaveClass(/embedded-nav-current/);
+ // Enter toggles the current item instead of pressing the clicked footer button again
+ await page.keyboard.press('Enter');
+ await expect(summaryHeader).toHaveAttribute('aria-expanded', 'true');
+ await expect(summary).toHaveClass(/embedded-nav-current/);
+ await page.keyboard.press('Enter');
+ await expect(summaryHeader).toHaveAttribute('aria-expanded', 'false');
+
+ await linkedCustomer.getByText('Linked customer', { exact: true }).click();
+ await expect(linkedCustomer).toHaveClass(/embedded-nav-current/);
+ await expect(current).toHaveCount(1);
+
+ await page.getByText('welcome:', { exact: true }).click();
+ await expect(current).toHaveCount(0);
+
+ await navigation.hover();
+ await navigation.getByRole('button', { name: 'To bottom' }).click();
+ await expect(linkedCustomer).toHaveClass(/embedded-nav-current/);
+ await page.keyboard.press('Home');
+ await expect(summary).toHaveClass(/embedded-nav-current/);
+
+ await page.getByRole('link', { name: 'customer', exact: true }).click();
+ await expect(page.getByText('Customer 42', { exact: true })).toBeVisible();
+ await expect(navigation).toHaveCount(0);
+});
+
+test('expands and collapses all embedded entities of a level', async ({ page }) => {
+ await openEntryPoint(page);
+ const status = page.getByText('ready', { exact: true });
+ await page.getByRole('button', { name: 'Expand all embedded entities' }).click();
+ await expect(status).toBeVisible();
+ await page.getByRole('button', { name: 'Collapse all embedded entities' }).first().click();
+ await expect(status).toBeHidden();
+});
+
+test('leaves nested embedded entities alone when expanding or collapsing a level', async ({ page }) => {
+ await openEntryPoint(page);
+ const expandAll = page.getByRole('button', { name: 'Expand all embedded entities' });
+ const collapseAll = page.getByRole('button', { name: 'Collapse all embedded entities' });
+ const summaryHeader = page.locator('mat-expansion-panel-header', { hasText: 'Embedded summary' });
+ const nestedHeader = page.locator('mat-expansion-panel-header', { hasText: 'Nested detail' });
+
+ await expandAll.click();
+ await expect(summaryHeader).toHaveAttribute('aria-expanded', 'true');
+ await expect(nestedHeader).toHaveAttribute('aria-expanded', 'false');
+
+ await expandAll.last().click();
+ await expect(nestedHeader).toHaveAttribute('aria-expanded', 'true');
+
+ await collapseAll.first().click();
+ await expect(summaryHeader).toHaveAttribute('aria-expanded', 'false');
+ await expect(nestedHeader).toHaveAttribute('aria-expanded', 'true');
+ await expect(collapseAll).toHaveCount(1);
+});
+
+test('expands and collapses all top level embedded entities with + and -', async ({ page }) => {
+ await openEntryPoint(page);
+ const summaryHeader = page.locator('mat-expansion-panel-header', { hasText: 'Embedded summary' });
+ const nestedHeader = page.locator('mat-expansion-panel-header', { hasText: 'Nested detail' });
+
+ await page.keyboard.press('+');
+ await expect(summaryHeader).toHaveAttribute('aria-expanded', 'true');
+ await expect(nestedHeader).toHaveAttribute('aria-expanded', 'false');
+
+ // + needs Shift on a US layout
+ await page.keyboard.press('-');
+ await expect(summaryHeader).toHaveAttribute('aria-expanded', 'false');
+ await page.keyboard.press('Shift++');
+ await expect(summaryHeader).toHaveAttribute('aria-expanded', 'true');
+
+ await page.getByRole('button', { name: 'Expand all embedded entities' }).first().hover();
+ await expect(page.locator('.mat-mdc-tooltip')).toHaveText('Expand all embedded entities (+)');
+});
+
test('previews JSON, text, and image link content', async ({ page }) => {
await openEntryPoint(page);
await page.getByRole('link', { name: 'plain-json' }).click();
@@ -323,7 +439,6 @@ test('downloads non-Siren content', async ({ page }) => {
test('searches nested properties in the tree', async ({ page }) => {
await openEntryPoint(page);
- await page.getByRole('switch').check();
const search = page.getByPlaceholder('Search ...');
await search.fill('needle-value');
await expect(page.getByText('searchableProperty:', { exact: true })).toBeVisible();
@@ -331,6 +446,125 @@ test('searches nested properties in the tree', async ({ page }) => {
await expect(page.locator('.match-counter')).toHaveText('1/1');
});
+test('expands an embedded entity that contains a search hit', async ({ page }) => {
+ await openEntryPoint(page);
+ await page.getByPlaceholder('Search ...').fill('ready');
+ await expect(page.locator('mark', { hasText: 'ready' })).toBeVisible();
+ await expect(page.locator('.match-counter')).toHaveText('1/1');
+});
+
+test('steps through link relation hits with Page down and Page up', async ({ page }) => {
+ await openEntryPoint(page);
+ await page.getByRole('button', { name: 'Search options' }).click();
+ await page.getByRole('switch', { name: 'Link relations' }).check();
+ await page.keyboard.press('Escape');
+ await expect(page.getByRole('switch', { name: 'Link relations' })).toBeHidden();
+
+ const search = page.getByPlaceholder('Search ...');
+ await search.fill('plain');
+ await expect(page.locator('.match-counter')).toHaveText('1/2');
+ await search.press('PageDown');
+ await expect(page.locator('.match-counter')).toHaveText('2/2');
+ await expect(search).not.toBeFocused();
+ // outside the search box too
+ await page.keyboard.press('PageUp');
+ await expect(page.locator('.match-counter')).toHaveText('1/2');
+ await expect(page.locator('.link-group.search-current')).toContainText('plain-json');
+});
+
+test('continues the page navigation from the embedded entity of a search hit', async ({ page }) => {
+ await openEntryPoint(page);
+ const summary = page.locator('mat-expansion-panel', { hasText: 'Embedded summary' });
+ const linkedCustomer = page.locator('mat-card', { hasText: 'Linked customer' });
+ const search = page.getByPlaceholder('Search ...');
+
+ await page.keyboard.press('/');
+ await expect(search).toBeFocused();
+ await page.keyboard.type('ready');
+ // keys edit the query while in the search box
+ await page.keyboard.press('Home');
+ await page.keyboard.type('is ');
+ await expect(search).toHaveValue('is ready');
+ await page.keyboard.press('ArrowDown');
+ await expect(search).toBeFocused();
+
+ await page.keyboard.press('Control+a');
+ await page.keyboard.type('ready');
+ await page.keyboard.press('Enter');
+ await expect(search).not.toBeFocused();
+ await expect(summary).toHaveClass(/embedded-nav-current/);
+ await page.keyboard.press('ArrowDown');
+ await expect(linkedCustomer).toHaveClass(/embedded-nav-current/);
+
+ await search.focus();
+ await page.keyboard.press('Escape');
+ await expect(search).not.toBeFocused();
+ await expect(search).toHaveValue('ready');
+
+ await page.getByRole('button', { name: 'Next hit' }).hover();
+ await expect(page.locator('.mat-mdc-tooltip')).toHaveText('Next hit (Page down)');
+});
+
+test('remembers search options after a reload', async ({ page }) => {
+ await openEntryPoint(page);
+ await page.getByRole('button', { name: 'Search options' }).click();
+ await page.getByRole('switch', { name: 'Actions' }).check();
+ await page.keyboard.press('Escape');
+
+ await page.reload();
+ await expect(page.getByText('Demo API', { exact: true })).toBeVisible();
+ await page.getByRole('button', { name: 'Search options' }).click();
+ await expect(page.getByRole('switch', { name: 'Actions' })).toBeChecked();
+});
+
+test('opens the raw view 2 levels deep and expands or collapses it with buttons and keys', async ({ page }) => {
+ await openEntryPoint(page);
+ await page.getByRole('radio', { name: 'Raw' }).check();
+ const rawView = page.locator('app-raw-view');
+ // an opened node lists "key: value"; a collapsed one only shows a JSON preview of its content
+ const rootProperty = 'welcome: "Navigate using hypermedia"';
+ const embeddedProperty = 'status: "ready"';
+ await expect(rawView).toContainText(rootProperty);
+ await expect(rawView).not.toContainText(embeddedProperty);
+
+ // a clicked view toggle releases the focus, so the shortcuts work right away
+ await page.keyboard.press('+');
+ await expect(rawView).toContainText(embeddedProperty);
+ await page.keyboard.press('-');
+ await expect(rawView).not.toContainText(rootProperty);
+ await page.getByRole('button', { name: 'Expand 2 levels' }).click();
+ await expect(rawView).toContainText(rootProperty);
+ await expect(rawView).not.toContainText(embeddedProperty);
+ await page.getByRole('button', { name: 'Expand all' }).click();
+ await expect(rawView).toContainText(embeddedProperty);
+ await page.getByRole('button', { name: 'Collapse all' }).hover();
+ await expect(page.locator('.mat-mdc-tooltip')).toHaveText('Collapse all (-)');
+
+ const navigation = page.getByRole('navigation', { name: 'Page navigation' });
+ await expect(navigation.getByRole('button')).toHaveCount(2);
+ await expect(navigation.getByRole('button', { name: 'Back to top' })).toBeAttached();
+ await expect(navigation.getByRole('button', { name: 'To bottom' })).toBeAttached();
+});
+
+test('searches the raw view as plain text', async ({ page }) => {
+ await openEntryPoint(page);
+ await page.getByRole('radio', { name: 'Raw' }).check();
+ await page.getByPlaceholder('Search ...').fill('"class"');
+ await expect(page.locator('app-raw-view mark').first()).toHaveText('"class"');
+});
+
+test('hides the search when configured', async ({ page }) => {
+ await page.route('**/app.config.json', route => route.fulfill({
+ contentType: 'application/json',
+ body: JSON.stringify({
+ showSearch: false,
+ relationIconMapping: {}, httpMethodIconMapping: {}, actionPopupWarningConfigurations: [],
+ }),
+ }));
+ await openEntryPoint(page);
+ await expect(page.getByPlaceholder('Search ...')).toBeHidden();
+});
+
test('navigates with breadcrumbs and recovers from an API error', async ({ page }) => {
await openEntryPoint(page);
await page.getByRole('link', { name: 'customer' }).click();
@@ -442,3 +676,166 @@ test('sends credentials to an API backed by a BFF', async ({ page, context }) =>
await openEntryPoint(page);
expect(await (await entryPointRequest).headerValue('cookie')).toContain('bff-session=session-cookie');
});
+
+test('keeps a site settings panel open and the focus while editing it', async ({ page }) => {
+ await page.goto('/');
+ await page.getByRole('button', { name: 'Settings' }).click();
+ await page.getByRole('tab', { name: 'Sites' }).click();
+ await page.getByRole('button', { name: 'Add site' }).click();
+
+ const site = page.locator('app-site-settings').nth(1);
+ const header = site.locator('mat-expansion-panel-header');
+ await expect(header).toHaveAttribute('aria-expanded', 'true');
+ await site.getByRole('textbox', { name: 'Host' }).fill('api.test');
+ await site.locator('#headersTitle').click();
+ await expect(header).toHaveAttribute('aria-expanded', 'true');
+ await expect(header).toContainText('api.test');
+
+ await site.locator('#addHeaderButton').click();
+ await site.getByRole('textbox', { name: 'Key' }).fill('X-Tenant');
+ await page.keyboard.press('Tab');
+ await expect(site.getByRole('textbox', { name: 'Value' })).toBeFocused();
+ await page.keyboard.type('blue');
+ await site.locator('#headersTitle').click();
+ await expect(header).toHaveAttribute('aria-expanded', 'true');
+ await expect(site.getByRole('textbox', { name: 'Key' })).toHaveValue('X-Tenant');
+ await expect(site.getByRole('textbox', { name: 'Value' })).toHaveValue('blue');
+
+ await site.locator('.deleteHeader').click();
+ await expect(site.getByRole('textbox', { name: 'Key' })).toHaveCount(0);
+});
+
+async function openSiteSettings(page: import('@playwright/test').Page) {
+ await page.goto('/');
+ await page.getByRole('button', { name: 'Settings' }).click();
+ await page.getByRole('tab', { name: 'Sites' }).click();
+}
+
+async function addHeaderSetting(site: import('@playwright/test').Locator, key: string, value: string) {
+ await site.locator('#addHeaderButton').click();
+ await site.getByRole('textbox', { name: 'Key' }).last().fill(key);
+ await site.getByRole('textbox', { name: 'Value' }).last().fill(value);
+ await site.locator('#headersTitle').click();
+}
+
+test('sends global and site specific headers', async ({ page }) => {
+ await openSiteSettings(page);
+ const global = page.locator('app-site-settings').first();
+ await global.locator('mat-expansion-panel-header').click();
+ await addHeaderSetting(global, 'X-Global', 'g1');
+
+ await page.getByRole('button', { name: 'Add site' }).click();
+ const site = page.locator('app-site-settings').nth(1);
+ await expect(site.locator('mat-expansion-panel-header')).toHaveAttribute('aria-expanded', 'true');
+ await site.getByRole('textbox', { name: 'Host' }).fill('API.test');
+ await addHeaderSetting(site, 'X-Site', 's1');
+ await expect(page.locator('app-error-dialog')).toHaveCount(0);
+ await page.keyboard.press('Escape');
+
+ await expect(page.getByText('Settings saved.')).toBeVisible();
+ const request = page.waitForRequest(`${api}/entrypoint`);
+ await openEntryPoint(page);
+ const headers = (await request).headers();
+ expect(headers['x-global']).toBe('g1');
+ expect(headers['x-site']).toBe('s1');
+});
+
+test('accepts only a host for site specific settings', async ({ page }) => {
+ await openSiteSettings(page);
+ await page.getByRole('button', { name: 'Add site' }).click();
+ const site = page.locator('app-site-settings').nth(1);
+ const header = site.locator('mat-expansion-panel-header');
+ await expect(header).toHaveAttribute('aria-expanded', 'true');
+ const host = site.getByRole('textbox', { name: 'Host' });
+
+ await expect(host).toHaveAttribute('placeholder', 'Host or IP, optional port');
+ await host.pressSequentially('https://api.test/entrypoint');
+ // validated while typing
+ await expect(site.locator('mat-error')).toHaveText('Enter the host without scheme: api.test');
+ await site.locator('#headersTitle').click();
+ await expect(header).not.toContainText('api.test');
+
+ await host.fill('api.test/entrypoint');
+ await site.locator('#headersTitle').click();
+ await expect(site.locator('mat-error')).toHaveText('Enter the host without path, query or fragment: api.test');
+
+ await host.fill('api.test:8080');
+ await site.locator('#headersTitle').click();
+ await expect(site.locator('mat-error')).toHaveCount(0);
+ await expect(header).toContainText('api.test:8080');
+
+ // an invalid host is never stored, closing the settings discards it
+ await host.fill('https://other.test');
+ await site.locator('#headersTitle').click();
+ await page.keyboard.press('Escape');
+ await expect(page.getByText('Settings saved.')).toBeVisible();
+ expect(await page.evaluate(() => localStorage.getItem('appSettings'))).not.toContain('other.test');
+ await page.getByRole('button', { name: 'Settings' }).click();
+ await page.getByRole('tab', { name: 'Sites' }).click();
+ await page.locator('app-site-settings').nth(1).locator('mat-expansion-panel-header').click();
+ await expect(page.locator('app-site-settings').nth(1).getByRole('textbox', { name: 'Host' })).toHaveValue('api.test:8080');
+});
+
+test('opens a new site for entering its host and does not add a second one without host', async ({ page }) => {
+ await openSiteSettings(page);
+ await page.getByRole('button', { name: 'Add site' }).click();
+ const newSite = page.locator('app-site-settings').nth(1);
+ await expect(newSite.locator('mat-expansion-panel-header')).toHaveAttribute('aria-expanded', 'true');
+ await expect(newSite.getByRole('textbox', { name: 'Host' })).toBeFocused();
+
+ await newSite.locator('mat-expansion-panel-header').click();
+ await page.getByRole('button', { name: 'Add site' }).click();
+
+ await expect(page.locator('app-site-settings')).toHaveCount(2);
+ await expect(page.locator('app-error-dialog')).toHaveCount(0);
+ const site = page.locator('app-site-settings').nth(1);
+ await expect(site.locator('mat-expansion-panel-header')).toHaveAttribute('aria-expanded', 'true');
+ await expect(site.getByRole('textbox', { name: 'Host' })).toBeFocused();
+});
+
+test('keeps using headers stored before values could be hidden', async ({ page }) => {
+ await page.addInitScript(() => localStorage.setItem('appSettings', JSON.stringify({
+ SiteSettings: {
+ GlobalSiteSettings: { SiteUrl: 'Global', Headers: [{ Key: 'X-Global', Value: 'g1' }] },
+ SiteSpecificSettings: [{ SiteUrl: 'api.test', Headers: [{ Key: 'X-Site', Value: 's1' }] }],
+ },
+ })));
+
+ const request = page.waitForRequest(`${api}/entrypoint`);
+ await openEntryPoint(page);
+ const headers = (await request).headers();
+ expect(headers['x-global']).toBe('g1');
+ expect(headers['x-site']).toBe('s1');
+
+ await page.getByRole('button', { name: 'Settings' }).click();
+ await page.getByRole('tab', { name: 'Sites' }).click();
+ const site = page.locator('app-site-settings').nth(1);
+ await site.locator('mat-expansion-panel-header').click();
+ await expect(site.getByRole('button', { name: 'Hide value' })).toHaveAttribute('aria-pressed', 'false');
+ await expect(site.getByRole('textbox', { name: 'Value' })).not.toHaveClass(/maskedValue/);
+});
+
+test('hides a header value, also after renaming the header and reopening the settings', async ({ page }) => {
+ await openSiteSettings(page);
+ const global = page.locator('app-site-settings').first();
+ await global.locator('mat-expansion-panel-header').click();
+ await addHeaderSetting(global, 'X-Secret', 'token');
+ const value = global.getByRole('textbox', { name: 'Value' });
+ await expect(value).not.toHaveClass(/maskedValue/);
+
+ await global.getByRole('button', { name: 'Hide value' }).click();
+ await expect(value).toHaveClass(/maskedValue/);
+ await global.getByRole('textbox', { name: 'Key' }).fill('X-Renamed');
+ await global.locator('#headersTitle').click();
+ await page.keyboard.press('Escape');
+ await expect(page.getByText('Settings saved.')).toBeVisible();
+ expect(await page.evaluate(() => localStorage.getItem('appSettings')))
+ .toContain('{"Key":"X-Renamed","Value":"token","Hidden":true}');
+
+ await page.getByRole('button', { name: 'Settings' }).click();
+ await page.getByRole('tab', { name: 'Sites' }).click();
+ await global.locator('mat-expansion-panel-header').click();
+ await expect(global.getByRole('button', { name: 'Hide value' })).toHaveAttribute('aria-pressed', 'true');
+ await expect(value).toHaveClass(/maskedValue/);
+ await expect(value).toHaveValue('token');
+});
diff --git a/src/app.config.d.ts b/src/app.config.d.ts
index c906ece..49f15d3 100644
--- a/src/app.config.d.ts
+++ b/src/app.config.d.ts
@@ -2,6 +2,7 @@ declare namespace HypermediaUI {
export interface IAppConfig {
disableDeveloperControls: boolean;
reduceUiElements: boolean;
+ showSearch?: boolean;
configuredEntryPoints: IConfiguredEntryPointsItem[];
onlyAllowConfiguredEntryPoints: boolean;
relationIconMapping?: Record
;
diff --git a/src/app.config.json b/src/app.config.json
index 2d4849e..9840699 100644
--- a/src/app.config.json
+++ b/src/app.config.json
@@ -1,6 +1,7 @@
{
"disableDeveloperControls": false,
"reduceUiElements": false,
+ "showSearch": true,
"autoFollowActionLocationOnSuccess": true,
"configuredEntryPoints": [],
"onlyAllowConfiguredEntryPoints": false,
diff --git a/src/app.config.service.ts b/src/app.config.service.ts
index 0c1a8e7..b677ce9 100644
--- a/src/app.config.service.ts
+++ b/src/app.config.service.ts
@@ -19,6 +19,7 @@ export class AppConfigService implements HypermediaUI.IAppConfig {
public disableDeveloperControls: boolean = false;
public reduceUiElements: boolean = false;
public showActionsBeforeProperties: boolean = false;
+ public showSearch: boolean = true;
public autoFollowActionLocationOnSuccess: boolean = false;
public configuredEntryPoints: ConfiguredEntryPoint[] = [];
public onlyAllowConfiguredEntryPoints: boolean = false;
@@ -41,6 +42,7 @@ export class AppConfigService implements HypermediaUI.IAppConfig {
disableDeveloperControls: this.disableDeveloperControls,
reduceUiElements: this.reduceUiElements,
showActionsBeforeProperties: this.showActionsBeforeProperties,
+ showSearch: this.showSearch,
autoFollowActionLocationOnSuccess: this.autoFollowActionLocationOnSuccess,
configuredEntryPoints: this.configuredEntryPoints,
onlyAllowConfiguredEntryPoints: this.onlyAllowConfiguredEntryPoints,
@@ -61,6 +63,7 @@ export class AppConfig extends ImmutableJsRecord({
disableDeveloperControls: true,
reduceUiElements: true,
showActionsBeforeProperties: false,
+ showSearch: true,
autoFollowActionLocationOnSuccess: false,
configuredEntryPoints: [] as ConfiguredEntryPoint[],
onlyAllowConfiguredEntryPoints : false,
diff --git a/src/app/hypermedia-view/actions-view/actions-view.component.html b/src/app/hypermedia-view/actions-view/actions-view.component.html
index 0129a1a..2f2e890 100644
--- a/src/app/hypermedia-view/actions-view/actions-view.component.html
+++ b/src/app/hypermedia-view/actions-view/actions-view.component.html
@@ -1,15 +1,18 @@
- @for (action of actions; track action) {
-
+ @for (action of actions; track action; let idx = $index) {
+
@switch (action.actionType) {
@case (ActionType.NoParameters) {
-
+
}
@case ( ActionType.JsonObjectParameters) {
-
+
}
@case (ActionType.FileUpload) {
-
+
}
}
diff --git a/src/app/hypermedia-view/actions-view/actions-view.component.ts b/src/app/hypermedia-view/actions-view/actions-view.component.ts
index 3557b98..1b62a56 100644
--- a/src/app/hypermedia-view/actions-view/actions-view.component.ts
+++ b/src/app/hypermedia-view/actions-view/actions-view.component.ts
@@ -1,5 +1,7 @@
-import { Component, Input } from '@angular/core';
+import { Component, inject, Input } from '@angular/core';
import {ActionType, HypermediaAction} from '../siren-parser/hypermedia-action';
+import {actionTarget, rootEntityKey} from '../search/entity-search';
+import {EntitySearchService} from '../search/entity-search.service';
@Component({
selector: 'app-actions-view',
@@ -8,7 +10,11 @@ import {ActionType, HypermediaAction} from '../siren-parser/hypermedia-action';
standalone: false
})
export class ActionsViewComponent {
+ protected search = inject(EntitySearchService);
+
@Input() actions: HypermediaAction[] = [];
+ @Input() entityKey: string = rootEntityKey;
+ protected readonly actionTarget = actionTarget;
protected readonly ActionType = ActionType;
}
diff --git a/src/app/hypermedia-view/actions-view/file-upload-action/file-upload-action.component.html b/src/app/hypermedia-view/actions-view/file-upload-action/file-upload-action.component.html
index aff2c60..db37779 100644
--- a/src/app/hypermedia-view/actions-view/file-upload-action/file-upload-action.component.html
+++ b/src/app/hypermedia-view/actions-view/file-upload-action/file-upload-action.component.html
@@ -4,7 +4,7 @@