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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 173 additions & 3 deletions docs/declarative-table-card.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@ import {
(createSubmit)="onCreateSubmit($event, tableCard)"
(editSubmit)="onEditSubmit($event, tableCard)"
(deleteSubmit)="onDeleteSubmit($event, tableCard)"
(searchChanged)="onSearch($event)"
(searchChanged)="onSearchChanged($event)"
(searchSubmit)="onSearchSubmit($event)"
(scopeChanged)="onScopeChanged($event)"
/>
`,
})
Expand Down Expand Up @@ -164,6 +166,23 @@ export class MyComponent {
await this.deletePod(pod);
tableCard.closeDeleteDialog();
}

onSearchChanged(value: string | null): void {
// Debounced (~300 ms) — fires while the user types and on clear-icon click.
// The currently active scope is tracked separately via `scopeChanged`.
this.reloadPods({ query: value ?? '' });
}

onSearchSubmit(value: string | null): void {
// Synchronous — fired on Enter or the search icon. Useful for forcing
// an immediate refresh that bypasses the debounce.
this.reloadPods({ query: value ?? '' });
}

onScopeChanged(scope: Scope | undefined): void {
// Synchronous — re-fetch using the new scope.
this.reloadPods({ scope });
}
}
```

Expand All @@ -189,7 +208,9 @@ export class MyComponent {
| `createSubmit` | `Record<string, unknown>` | Fires when the create dialog Save button is clicked |
| `editSubmit` | `{ resource: T; value: Record<string, unknown> }` | Fires when the edit dialog Save button is clicked |
| `deleteSubmit` | `T` | Fires when the delete dialog Delete button is clicked |
| `searchChanged` | `string` | Emits 300 ms after the search input changes |
| `searchChanged` | `string \| null` | Emits ~300 ms after the search input changes (typing or clear icon click). The empty string is emitted immediately on clear. |
| `searchSubmit` | `string \| null` | Emits synchronously when the user submits the search (Enter or search icon) |
| `scopeChanged` | `Scope \| undefined` | Emits synchronously when the user picks a different scope from the dropdown. The full scope object is forwarded; `undefined` means "no scope selected". |
| `tableRowClicked` | `T` | Emits when a table row is clicked |
| `loadMoreResources` | - | Emits when the user triggers load more |
| `paginationLimitChanged` | `number` | Emits when the user changes page size |
Expand All @@ -211,15 +232,44 @@ Submit events do not close dialogs automatically. Close the dialog after success

```ts
interface TableCardConfig {
header: string;
header?: string;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I thought I was changing that already .... moght has been lost somewhere

headerTooltip?: string;
tableConfig: TableConfig;
buttonSettings?: TableCardButtonSettings;
searchConfig?: TableCardSearchConfig;
createResourceFormConfig?: ResourceFormConfig;
editResourceFormConfig?: ResourceFormConfig;
deleteResourceConfirmationConfig?: DeleteResourceConfirmationConfig;
}

/** One option in the `<ui5-search>` scopes dropdown. */
interface Scope {
/** Stable identifier used to match `initialScopeValue` and as `<ui5-search-scope value>`. */
id: string;
/** Visible label shown in the dropdown. */
label: string;
/** Logical value forwarded to the host (e.g. for filtering, URL query string). */
value: string;
/** Name of the property this scope filters by — used by the host to build the filter expression. */
property: string;
}

/** Configuration for the `<ui5-search>` element rendered in the table-card header. */
interface TableCardSearchConfig {
/** ARIA name for the search input. */
accessibleName?: string;
/** Placeholder text shown when the input is empty. */
placeholder?: string;
/** When `true`, the clear icon is shown inside the input. Default: `true`. */
showClearIcon?: boolean;
/** Initial / controlled scope — must be one of the entries in `scopes`. Matched against `scopes[].id`. */
initialScopeValue?: Scope;
Comment on lines +265 to +266

Copy link
Copy Markdown

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

Correct the initialScopeValue description.

The documentation claims initialScopeValue is "Matched against scopes[].id," but the component (see table-card-search.component.ts) directly assigns the object via this.activeScope.set(config.initialScopeValue) without matching by id. The id-based lookup only happens when the user changes the scope via the dropdown (onSearchScopeChange). Update the description to clarify that initialScopeValue should be a Scope object from scopes (or an equivalent object), not an id string to be matched.

-  /** Initial / controlled scope — must be one of the entries in `scopes`. Matched against `scopes[].id`. */
+  /** Initial / controlled scope object. Should reference one of the objects in `scopes` (or an equivalent `Scope` value). */
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/** Initial / controlled scope — must be one of the entries in `scopes`. Matched against `scopes[].id`. */
initialScopeValue?: Scope;
/** Initial / controlled scope object. Should reference one of the objects in `scopes` (or an equivalent `Scope` value). */
initialScopeValue?: Scope;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/declarative-table-card.md` around lines 265 - 266, The
`initialScopeValue` docs are inaccurate: it is not matched against `scopes[].id`
on init, because `table-card-search.component.ts` sets it directly via
`this.activeScope.set(config.initialScopeValue)`. Update the `initialScopeValue`
description in `declarative-table-card.md` to say it should be a `Scope` object
from `scopes` (or an equivalent `Scope`-shaped value), and reserve the
`id`-based lookup wording for the dropdown change path in `onSearchScopeChange`.

/** Initial / controlled search text value. */
value?: string;
/** Scope options shown in the scopes dropdown. Omit or leave empty to render the input without a scope dropdown. */
scopes?: Scope[];
}

interface TableConfig {
fields: TableFieldDefinition[];
totalItemsCount?: number;
Expand All @@ -244,6 +294,126 @@ interface TableCardFormState {

`ResourceFormConfig` is static. Keep runtime errors in `createFormState` / `editFormState`. The submit button is disabled when any entry in `fieldErrors` is truthy.

> `TableConfig` is declared in `declarative-ui/table` and re-exported from `declarative-ui/table-card`. Importing it from `@openmfp/webcomponents` (the public-api barrel) continues to work unchanged.

---

## Search & Scopes

When `searchConfig` is set on `TableCardConfig`, the card renders a [`<ui5-search>`](https://ui5.github.io/webcomponents/components/fiori/Search/) element inline in the toolbar. Omit `searchConfig` to hide the search entirely.

### Clearing the input

`showClearIcon` defaults to `true` — the user can click the inline X icon to empty the input. Clearing emits `searchChanged` with an empty string **immediately** (bypassing the typing debounce), so the host can refetch / un-filter without waiting.

### Scopes

`scopes` is an array of `Scope` objects shown in the dropdown next to the search input. Each scope is matched by its `id`; `initialScopeValue` (a full `Scope`) selects the initially-active scope. Omit `scopes` (or pass an empty array) to render the input without a scope dropdown.

The `value` and `property` fields on `Scope` are passed through verbatim in `scopeChanged` — the host decides how to use them (e.g. as `?<property>=<value>` in the URL, as an OpenSearch `filter=<property>=<value>` parameter, etc.).

### Event contract

The host owns data fetching and filtering. The card forwards user actions verbatim:

| Event | When | Payload |
| --------------- | ---- | ------- |
| `searchChanged` | ~300 ms after the input value changes while typing; **immediately** when the clear icon is clicked | `string \| null` — the current input value |
| `searchSubmit` | User presses Enter or clicks the search icon (synchronous) | `string \| null` — the current input value |
| `scopeChanged` | User picks a different scope from the dropdown (synchronous) | `Scope \| undefined` — the full scope object, or `undefined` when no scope is active |

The search text and the active scope are emitted on separate events. The host is responsible for keeping the most recent value of each and combining them when issuing the next request.

### Example — "My Contributions" / "All" scopes

```ts
import {
DeclarativeTableCard,
Scope,
TableCardConfig,
} from '@openmfp/webcomponents';

@Component({
imports: [DeclarativeTableCard],
template: `
<mfp-declarative-table-card
[config]="config"
[resources]="pods"
(searchChanged)="onSearchChanged($event)"
(searchSubmit)="onSearchSubmit($event)"
(scopeChanged)="onScopeChanged($event)"
/>
`,
})
export class MyComponent {
pods: Pod[] = [];

private currentQuery = '';
private currentScope: Scope | undefined;

private readonly ALL_SCOPE: Scope = {
id: 'all',
label: 'All',
value: '*',
property: 'owner',
};
private readonly MINE_SCOPE: Scope = {
id: 'mine',
label: 'My Contributions',
value: 'me',
property: 'owner',
};

config: TableCardConfig = {
header: 'Pods',
tableConfig: {
fields: [
{ label: 'Name', property: 'metadata.name' },
{ label: 'Namespace', property: 'metadata.namespace' },
],
},
searchConfig: {
placeholder: 'Search pods…',
accessibleName: 'Search pods',
scopes: [this.ALL_SCOPE, this.MINE_SCOPE],
initialScopeValue: this.ALL_SCOPE,
},
};

onSearchChanged(value: string | null): void {
// Debounced while typing; instant on clear. Combine with the current scope
// when reloading.
this.currentQuery = value ?? '';
this.reload();
}

onSearchSubmit(value: string | null): void {
// Synchronous — fired on Enter or the search icon. Useful for forcing
// an immediate refresh that bypasses the typing debounce.
this.currentQuery = value ?? '';
this.reload();
}

onScopeChanged(scope: Scope | undefined): void {
// Synchronous — re-fetch with the current in-flight search text and the
// newly-selected scope.
this.currentScope = scope;
this.reload();
}

private reload(): void {
this.reloadPods({
query: this.currentQuery,
filter: this.currentScope
? `${this.currentScope.property}=${this.currentScope.value}`
: undefined,
});
}
}
```

Omit `scopes` (or pass an empty array) to render the input without a scope dropdown.

---

## Actions column
Expand Down
7 changes: 7 additions & 0 deletions nodemon.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"watch": ["projects", "scripts"],
"ignore": ["dist", "public", ".angular", "node_modules", "documentation.json"],
"ext": "js,yml,yaml,ts,html,css,scss,json,md",
"delay": 1000,
"exec": "rimraf dist && npm run build:dev && npm run yalc:publish"
}
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
"type": "module",
"scripts": {
"build:ngx": "ng build ngx",
"build:ngx:dev": "ng build ngx --configuration=development",
"build:wc": "ng build webcomponents && ng build webcomponents-dashboard && node scripts/bundle-wc.mjs",
"build:wc:dev": "ng build webcomponents --configuration=development && ng build webcomponents-dashboard --configuration=development && node scripts/bundle-wc.mjs",
"build": "npm run build:ngx && npm run build:wc",
"build:watch": "node -e \"require('fs').mkdirSync('dist',{recursive:true})\" && nodemon --ignore dist --ignore public --ext js,yml,yaml,ts,html,css,scss,json,md --exec \"rimraf dist && npm run build && npm run yalc:publish\"",
"build:dev": "npm run build:ngx:dev && npm run build:wc:dev",
"build:watch": "node -e \"require('fs').mkdirSync('dist',{recursive:true})\" && nodemon",
"yalc:publish": "yalc publish dist/ngx --push --sig && yalc publish dist/webcomponents --push --sig",
"test": "ng test ngx --watch=false",
"test:watch": "ng test ngx",
Expand Down
1 change: 1 addition & 0 deletions projects/ngx/declarative-ui/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './resource';
export * from './ui-definition';
export type { TableFieldDefinition, ResourceFieldButtonClickEvent } from '../table/models/table-config';
35 changes: 8 additions & 27 deletions projects/ngx/declarative-ui/models/ui-definition.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { GenericResource } from './resource';

/** Text transformation applied to a field value before display. */
export type TransformType =
Expand All @@ -18,7 +17,14 @@ export interface PropertyField {

/** Appearance settings for tag chip rendering. */
export interface TagSettings {
design?: 'Neutral' | 'Positive' | 'Critical' | 'Negative' | 'Information' | 'Set1' | 'Set2';
design?:
| 'Neutral'
| 'Positive'
| 'Critical'
| 'Negative'
| 'Information'
| 'Set1'
| 'Set2';
colorScheme?: '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '10';
/** Delimiter used to split a plain-string value into individual tags. Default: `','`. */
valueSeparator?: string;
Expand Down Expand Up @@ -120,16 +126,6 @@ export interface ValueRule {
then: string;
}

/** Event payload emitted when a button inside a table cell is clicked. */
export interface ResourceFieldButtonClickEvent<T extends GenericResource> {
/** Original DOM click event. */
event: MouseEvent;
/** The field definition of the button cell that was clicked. */
field: TableFieldDefinition;
/** The data row associated with the clicked button. */
resource: T | undefined;
}

/** Base field definition shared by table columns and form fields. */
export interface FieldDefinition {
/** Column header / form label. */
Expand All @@ -145,18 +141,3 @@ export interface FieldDefinition {
/** Display and interaction configuration for this cell. */
uiSettings?: UiSettings;
}

/** Table column definition — extends `FieldDefinition` with optional column grouping. */
export interface TableFieldDefinition extends FieldDefinition {
/** Groups this column visually with adjacent columns that share the same `name`. */
group?: {
/** Logical group identifier. */
name: string;
/** Group header label shown above the grouped cells. */
label?: string;
/** Separator placed between values in the same group cell. */
delimiter?: string;
/** When `true`, each value is rendered on its own line. */
multiline?: boolean;
};
}
Loading