Skip to content
Merged
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
62 changes: 62 additions & 0 deletions ui/STYLEGUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,68 @@ To render smaller inputs in a dense layout, assign the CSS class `form-field-sma

You can also define an optional tooltip which is shown above the label.

#### Search Select

Use `sp-search-select` when users need to select one or more items from a searchable list, such as datasets, labels, sites, asset types, users, roles or groups.

Keep data loading, persistence and feature-specific actions outside the component. For example, actions such as `Manage Labels` or refresh buttons should remain in the parent view, usually in the surrounding `sp-form-field` actions.

Single-select example:

```html
<sp-search-select
[items]="availableMeasurements"
[(value)]="selectedMeasurement"
[placeholder]="'Search datasets' | translate"
>
</sp-search-select>
```

Multi-select example:

```html
<sp-search-select
[items]="labels"
[(value)]="selectedLabels"
[multiple]="true"
[placeholder]="'Add labels' | translate"
>
</sp-search-select>
```

The component keeps the selected value as the full object. It displays items by convention using `label`, `name`, `measureName`, `title`, `email`, `groupName`, `value`, `filename`, `assetName`, `_id` or `id`, and filters client-side by the displayed value.

Objects with a `color` property are rendered as small `sp-label` badges in both the selected value and the dropdown. This covers labels without adding label-specific inputs to the component:

```html
<sp-search-select
[items]="labels"
[(value)]="selectedLabels"
[multiple]="true"
[placeholder]="'Add labels' | translate"
>
</sp-search-select>
```

Use optional templates only when the default text or colored-label rendering is not enough:

```html
<sp-search-select
[items]="users"
[(value)]="selectedUsers"
[multiple]="true"
[placeholder]="'Add users' | translate"
>
<ng-template spSearchSelectOption let-user>
{{ user.email }} ({{ user.principalId }})
</ng-template>

<ng-template spSearchSelectChip let-user> {{ user.email }} </ng-template>
</sp-search-select>
```

Do not add feature-specific inputs such as label color keys, server-side search, action slots or custom comparison functions unless there is a concrete reusable need. Prefer the minimal API and keep feature behavior in the parent component.

#### Label

Use form labels to ensure a consistent layout of forms and labels.
Expand Down
2 changes: 2 additions & 0 deletions ui/deployment/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"Add node": "Knoten hinzufügen",
"Add pipeline element": "Pipeline-Element hinzufügen",
"Add role": "Rolle hinzufügen",
"Add service tag": "Service-Tag hinzufügen",
"Add the current dashboard to an existing asset": "Das aktuelle Dashboard zu einem bestehenden Asset hinzufügen",
"Add to Asset": "Zu Asset hinzufügen",
"Add user": "Benutzer hinzufügen",
Expand Down Expand Up @@ -749,6 +750,7 @@
"No more information": "Keine weiteren Informationen",
"No nodes selected yet": "Noch keine Knoten ausgewählt",
"No numeric or boolean fields available.": "Keine numerischen oder booleschen Felder verfügbar.",
"No results found": "Keine Ergebnisse gefunden",
"No running adapters": "Keine laufenden Adapter",
"No running pipeline elements": "Keine laufenden Pipeline-Elemente",
"No table rows match the current filter.": "Keine Tabellenzeilen entsprechen dem aktuellen Filter.",
Expand Down
2 changes: 2 additions & 0 deletions ui/deployment/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"Add node": null,
"Add pipeline element": null,
"Add role": null,
"Add service tag": null,
"Add the current dashboard to an existing asset": null,
"Add to Asset": null,
"Add user": null,
Expand Down Expand Up @@ -749,6 +750,7 @@
"No more information": null,
"No nodes selected yet": null,
"No numeric or boolean fields available.": null,
"No results found": null,
"No running adapters": null,
"No running pipeline elements": null,
"No table rows match the current filter.": null,
Expand Down
2 changes: 2 additions & 0 deletions ui/deployment/i18n/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"Add node": "Dodaj węzeł",
"Add pipeline element": "Dodaj element strumienia",
"Add role": "Dodaj rolę",
"Add service tag": "Dodaj tag usługi",
"Add the current dashboard to an existing asset": "Dodaj bieżący pulpit do istniejącego zasobu",
"Add to Asset": "Dodaj do zasobu",
"Add user": "Dodaj użytkownika",
Expand Down Expand Up @@ -749,6 +750,7 @@
"No more information": "Brak dalszych informacji",
"No nodes selected yet": "Nie wybrano jeszcze żadnych węzłów",
"No numeric or boolean fields available.": "Brak dostępnych pól liczbowych lub logicznych.",
"No results found": "Nie znaleziono wyników",
"No running adapters": "Brak uruchomionych adapterów",
"No running pipeline elements": "Brak uruchomionych elementów strumienia",
"No table rows match the current filter.": "Żaden wiersz tabeli nie pasuje do bieżącego filtra.",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

import { Directive, TemplateRef, inject } from '@angular/core';

@Directive({
selector: 'ng-template[spSearchSelectOption]',
})
export class SearchSelectOptionTemplateDirective<T = unknown> {
readonly templateRef = inject<TemplateRef<{ $implicit: T }>>(TemplateRef);
}

@Directive({
selector: 'ng-template[spSearchSelectChip]',
})
export class SearchSelectChipTemplateDirective<T = unknown> {
readonly templateRef = inject<TemplateRef<{ $implicit: T }>>(TemplateRef);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<!--
~ Licensed to the Apache Software Foundation (ASF) under one or more
~ contributor license agreements. See the NOTICE file distributed with
~ this work for additional information regarding copyright ownership.
~ The ASF licenses this file to You under the Apache License, Version 2.0
~ (the "License"); you may not use this file except in compliance with
~ the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
~
-->

<mat-form-field appearance="outline" subscriptSizing="dynamic">
<mat-icon matPrefix>search</mat-icon>

@if (multiple()) {
<mat-chip-grid
#chipGrid
[attr.aria-label]="placeholderText()"
[disabled]="disabled()"
>
@for (item of selectedItems(); track trackItem($index, item)) {
@let color = itemColor(item);
@let contrastColor = itemContrastColor(item);
<mat-chip-row
(removed)="removeItem(item)"
[class.search-select-chip-colored]="color"
[style.--sp-search-select-chip-bg]="color"
[style.--sp-search-select-chip-fg]="contrastColor"
>
@if (chipTemplate) {
<ng-container
[ngTemplateOutlet]="chipTemplate.templateRef"
[ngTemplateOutletContext]="{ $implicit: item }"
>
</ng-container>
} @else if (color) {
<sp-label
[labelText]="displayValue(item)"
size="small"
[color]="color"
>
</sp-label>
} @else {
{{ displayValue(item) }}
}
<button
matChipRemove
type="button"
[style.color]="contrastColor"
>
<mat-icon [style.color]="contrastColor">
cancel
</mat-icon>
</button>
</mat-chip-row>
}

<input
matInput
[placeholder]="placeholderText() | translate"
[value]="inputValue()"
(input)="onInput($any($event.target).value)"
[matAutocomplete]="searchSelectAutocomplete"
[matChipInputFor]="chipGrid"
[attr.data-cy]="dataCy()"
[disabled]="disabled()"
/>
</mat-chip-grid>
} @else {
<input
matInput
[placeholder]="placeholderText() | translate"
[value]="inputValue()"
(input)="onInput($any($event.target).value)"
[matAutocomplete]="searchSelectAutocomplete"
[attr.data-cy]="dataCy()"
[disabled]="disabled()"
/>
}

@if (selectedItems().length > 0 && !disabled()) {
<button
mat-icon-button
matSuffix
type="button"
[attr.aria-label]="'Clear search' | translate"
(click)="clearValue()"
>
<mat-icon>close</mat-icon>
</button>
}

<mat-autocomplete
#searchSelectAutocomplete="matAutocomplete"
autoActiveFirstOption
(opened)="onOpened()"
(closed)="onClosed()"
(optionSelected)="onSelected($event)"
[displayWith]="autocompleteDisplayWith"
>
@for (item of filteredItems(); track trackItem($index, item)) {
@let color = itemColor(item);
<mat-option [value]="item">
<mat-icon class="search-select-check-icon">
@if (isSelected(item)) {
check
}
</mat-icon>
@if (optionTemplate) {
<ng-container
[ngTemplateOutlet]="optionTemplate.templateRef"
[ngTemplateOutletContext]="{ $implicit: item }"
>
</ng-container>
} @else if (color) {
<sp-label
[labelText]="displayValue(item)"
size="small"
[color]="color"
>
</sp-label>
} @else {
{{ displayValue(item) }}
}
</mat-option>
}

@if (filteredItems().length === 0) {
<mat-option disabled>
{{ 'No results found' | translate }}
</mat-option>
}
</mat-autocomplete>
</mat-form-field>
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

:host {
display: block;
width: 100%;
}

mat-form-field {
width: 100%;
}

.search-select-check-icon {
margin-right: var(--space-xs);
width: 1.125rem;
}

.search-select-chip-colored {
background-color: transparent;
color: var(--sp-search-select-chip-fg);
min-height: auto;
padding: 0;
--mat-chip-container-height: auto;
--mat-chip-elevated-container-color: transparent;
--mat-chip-elevated-selected-container-color: transparent;
--mat-chip-flat-disabled-selected-container-color: transparent;
--mat-chip-outline-color: transparent;
--mat-chip-outline-width: 0;
--mat-chip-flat-selected-outline-width: 0;
--mat-chip-focus-outline-color: transparent;
--mat-chip-label-text-color: var(--sp-search-select-chip-fg);
--mat-chip-selected-label-text-color: var(--sp-search-select-chip-fg);
--mat-chip-with-trailing-icon-trailing-icon-color: var(
--sp-search-select-chip-fg
);
--mat-chip-selected-trailing-icon-color: var(--sp-search-select-chip-fg);
--mdc-chip-elevated-container-color: transparent;
--mdc-chip-outline-color: transparent;
--mdc-chip-outline-width: 0;
--mdc-chip-focus-outline-color: transparent;
--mdc-chip-label-text-color: var(--sp-search-select-chip-fg);
--mdc-chip-with-trailing-icon-trailing-icon-color: var(
--sp-search-select-chip-fg
);
--mdc-chip-with-trailing-icon-trailing-icon-hover-color: var(
--sp-search-select-chip-fg
);
--mdc-chip-with-trailing-icon-trailing-icon-focus-color: var(
--sp-search-select-chip-fg
);
}

// Angular Material renders chip outlines inside component internals. Colored
// items use sp-label for the visible badge, so the Material wrapper stays flat.
:host ::ng-deep .search-select-chip-colored {
.mat-mdc-chip-focus-overlay {
display: none;
}

.mdc-evolution-chip__cell,
.mdc-evolution-chip__action,
.mat-mdc-chip-action-label {
min-height: auto;
}

.mdc-evolution-chip__cell--primary,
.mdc-evolution-chip__action--primary {
padding-left: 0;
padding-right: 0;
}

.mdc-evolution-chip__action--primary::before {
border-color: transparent;
border-width: 0;
}

.mdc-evolution-chip__cell--trailing,
.mdc-evolution-chip__action--trailing {
padding-left: var(--space-xs);
padding-right: 0;
}

.mdc-evolution-chip__action,
.mdc-evolution-chip__text-label,
.mat-mdc-chip-action-label,
.mat-icon {
color: var(--sp-search-select-chip-fg);
}
}
Loading
Loading