@trismegisto/spine 3.3.3 → 3.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +15 -2
  2. package/fesm2022/trismegisto-spine-audit.mjs +6 -6
  3. package/fesm2022/trismegisto-spine-audit.mjs.map +1 -1
  4. package/fesm2022/{trismegisto-spine-auto-service-auto-service-client.component-BAfeLyTB.mjs → trismegisto-spine-auto-service-auto-service-client.component-kAXaCM5W.mjs} +8 -8
  5. package/fesm2022/{trismegisto-spine-auto-service-auto-service-client.component-BAfeLyTB.mjs.map → trismegisto-spine-auto-service-auto-service-client.component-kAXaCM5W.mjs.map} +1 -1
  6. package/fesm2022/{trismegisto-spine-auto-service-auto-service-groups.component-BYjIQ9ID.mjs → trismegisto-spine-auto-service-auto-service-groups.component-DE2QIGRH.mjs} +251 -85
  7. package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-DE2QIGRH.mjs.map +1 -0
  8. package/fesm2022/{trismegisto-spine-auto-service-auto-service-news-page.component-CBemH4hr.mjs → trismegisto-spine-auto-service-auto-service-news-page.component-BW7BJ0oD.mjs} +2 -2
  9. package/fesm2022/{trismegisto-spine-auto-service-auto-service-news-page.component-CBemH4hr.mjs.map → trismegisto-spine-auto-service-auto-service-news-page.component-BW7BJ0oD.mjs.map} +1 -1
  10. package/fesm2022/{trismegisto-spine-auto-service-auto-service-ratings-page.component-C_THGTkw.mjs → trismegisto-spine-auto-service-auto-service-ratings-page.component-CHPU-QLJ.mjs} +2 -2
  11. package/fesm2022/{trismegisto-spine-auto-service-auto-service-ratings-page.component-C_THGTkw.mjs.map → trismegisto-spine-auto-service-auto-service-ratings-page.component-CHPU-QLJ.mjs.map} +1 -1
  12. package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-CkiCTmXD.mjs +3733 -0
  13. package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-CkiCTmXD.mjs.map +1 -0
  14. package/fesm2022/{trismegisto-spine-auto-service-auto-service.component-QsIQ61w2.mjs → trismegisto-spine-auto-service-auto-service.component-D58Ht9PH.mjs} +7 -7
  15. package/fesm2022/{trismegisto-spine-auto-service-auto-service.component-QsIQ61w2.mjs.map → trismegisto-spine-auto-service-auto-service.component-D58Ht9PH.mjs.map} +1 -1
  16. package/fesm2022/{trismegisto-spine-auto-service-groups.technical-_jQrg1Qh.mjs → trismegisto-spine-auto-service-groups.technical-CB1D3ANC.mjs} +12 -3
  17. package/fesm2022/trismegisto-spine-auto-service-groups.technical-CB1D3ANC.mjs.map +1 -0
  18. package/fesm2022/{trismegisto-spine-auto-service-groups.usage-OvWjNeeT.mjs → trismegisto-spine-auto-service-groups.usage-De2MhlE7.mjs} +7 -2
  19. package/fesm2022/{trismegisto-spine-auto-service-groups.usage-OvWjNeeT.mjs.map → trismegisto-spine-auto-service-groups.usage-De2MhlE7.mjs.map} +1 -1
  20. package/fesm2022/{trismegisto-spine-auto-service-suggestions.technical-BtUK6sKD.mjs → trismegisto-spine-auto-service-suggestions.technical-CGBBzAR8.mjs} +14 -1
  21. package/fesm2022/{trismegisto-spine-auto-service-suggestions.technical-BtUK6sKD.mjs.map → trismegisto-spine-auto-service-suggestions.technical-CGBBzAR8.mjs.map} +1 -1
  22. package/fesm2022/{trismegisto-spine-auto-service-suggestions.usage-BwO9Hjsx.mjs → trismegisto-spine-auto-service-suggestions.usage-DnAJS-Z0.mjs} +26 -3
  23. package/fesm2022/trismegisto-spine-auto-service-suggestions.usage-DnAJS-Z0.mjs.map +1 -0
  24. package/fesm2022/trismegisto-spine-auto-service.mjs +24 -11
  25. package/fesm2022/trismegisto-spine-auto-service.mjs.map +1 -1
  26. package/fesm2022/{trismegisto-spine-layout-suggestions-page.component-xI2gDMok.mjs → trismegisto-spine-layout-suggestions-page.component-CEKMqera.mjs} +3 -3
  27. package/fesm2022/{trismegisto-spine-layout-suggestions-page.component-xI2gDMok.mjs.map → trismegisto-spine-layout-suggestions-page.component-CEKMqera.mjs.map} +1 -1
  28. package/fesm2022/{trismegisto-spine-layout-trismegisto-spine-layout-Clphrsgc.mjs → trismegisto-spine-layout-trismegisto-spine-layout-DiAfxIGP.mjs} +428 -93
  29. package/fesm2022/trismegisto-spine-layout-trismegisto-spine-layout-DiAfxIGP.mjs.map +1 -0
  30. package/fesm2022/trismegisto-spine-layout.mjs +1 -1
  31. package/fesm2022/trismegisto-spine-side-panel.mjs +1 -1
  32. package/fesm2022/trismegisto-spine-side-panel.mjs.map +1 -1
  33. package/fesm2022/trismegisto-spine.mjs +528 -10
  34. package/fesm2022/trismegisto-spine.mjs.map +1 -1
  35. package/package.json +6 -6
  36. package/types/trismegisto-spine-auto-service.d.ts +20 -4
  37. package/types/trismegisto-spine-auto-service.d.ts.map +1 -1
  38. package/types/trismegisto-spine-layout.d.ts +327 -63
  39. package/types/trismegisto-spine-layout.d.ts.map +1 -1
  40. package/types/trismegisto-spine.d.ts +221 -3
  41. package/types/trismegisto-spine.d.ts.map +1 -1
  42. package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-BYjIQ9ID.mjs.map +0 -1
  43. package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-mMjE3JOL.mjs +0 -2179
  44. package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-mMjE3JOL.mjs.map +0 -1
  45. package/fesm2022/trismegisto-spine-auto-service-groups.technical-_jQrg1Qh.mjs.map +0 -1
  46. package/fesm2022/trismegisto-spine-auto-service-suggestions.usage-BwO9Hjsx.mjs.map +0 -1
  47. package/fesm2022/trismegisto-spine-layout-trismegisto-spine-layout-Clphrsgc.mjs.map +0 -1
@@ -1,8 +1,8 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, ChangeDetectorRef, EventEmitter, Output, Input, ChangeDetectionStrategy, Component, computed } from '@angular/core';
2
+ import { inject, ChangeDetectorRef, EventEmitter, Output, Input, ChangeDetectionStrategy, Component, signal, computed, DestroyRef } from '@angular/core';
3
3
  import * as i2 from '@angular/common';
4
4
  import { CommonModule } from '@angular/common';
5
- import { AlertComponent, ButtonComponent, CheckboxComponent, ComboboxComponent, InputComponent, ModalComponent, TextareaComponent, TagComponent, AccountAvatarComponent, DataGridComponent, DataGridCellDirective, HERMES_MESSAGES, EMPTY_SELECTION, BadgeComponent, IconButtonComponent, IconComponent, HermesDialogService, CardComponent } from '@trismegisto/angular';
5
+ import { AlertComponent, ButtonComponent, CheckboxComponent, ComboboxComponent, InputComponent, ModalComponent, TextareaComponent, HermesI18nService, TagComponent, AccountAvatarComponent, DataGridComponent, DataGridCellDirective, HERMES_MESSAGES, EMPTY_SELECTION, BadgeComponent, IconButtonComponent, IconComponent, HermesDialogService, CardComponent } from '@trismegisto/angular';
6
6
  import { formatMessage } from '@trismegisto/primitives';
7
7
  import { SPINE_MESSAGES } from '@trismegisto/spine';
8
8
  import { a as autoServiceErrorText, A as AutoServiceDeleteFeedback } from './trismegisto-spine-auto-service-auto-service-delete-CV_t9vxm.mjs';
@@ -11,6 +11,7 @@ import { A as AutoServiceClientPickerComponent } from './trismegisto-spine-auto-
11
11
  import * as i2$1 from '@angular/forms';
12
12
  import { FormControl, Validators, ReactiveFormsModule, FormsModule } from '@angular/forms';
13
13
  import { o as optionalIdValidator, a as optionalIdBody } from './trismegisto-spine-auto-service-optional-id.validator-0JPeqhjm.mjs';
14
+ import { Subscription, tap, map } from 'rxjs';
14
15
 
15
16
  /** Limite do nome no servidor (1–100). */
16
17
  const GROUP_NAME_MAX = 100;
@@ -102,7 +103,7 @@ class AutoServiceGroupModalComponent {
102
103
  }
103
104
  }
104
105
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceGroupModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
105
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: AutoServiceGroupModalComponent, isStandalone: true, selector: "spine-auto-service-group-modal", inputs: { group: "group", clientId: "clientId", groupTypes: "groupTypes", open: "open" }, outputs: { openChange: "openChange", saved: "saved" }, usesOnChanges: true, ngImport: i0, template: "<hm-modal [open]=\"open\" (openChange)=\"onModalOpenChange($event)\" [heading]=\"isEdit ? text.editGroup : text.newGroup\" (closed)=\"close()\">\n <div class=\"form-stack\">\n <hm-alert *ngIf=\"saveError\" variant=\"error\">{{ saveError }}</hm-alert>\n @if (!isEdit) {\n <hm-input [formControl]=\"newId\" [label]=\"text.optionalId\" [placeholder]=\"text.optionalIdPlaceholder\" [hint]=\"text.optionalIdHint\" [messages]=\"{ optionalId: text.optionalIdInvalid }\" />\n }\n <hm-input [formControl]=\"name\" [label]=\"text.groupName\" [maxLength]=\"nameMax\" />\n <hm-combobox [formControl]=\"typeId\" [options]=\"typeOptions\" [label]=\"text.groupType\" [placeholder]=\"text.groupTypePlaceholder\" />\n <hm-textarea [formControl]=\"description\" [label]=\"text.groupDescription\" />\n <hm-checkbox *ngIf=\"isEdit\" [formControl]=\"enabled\" [label]=\"text.active\" />\n </div>\n\n <ng-container footer>\n <hm-button emphasis=\"medium\" (clicked)=\"close()\">{{ text.cancel }}</hm-button>\n <hm-button [disabled]=\"!canSave\" [loading]=\"saving\" (clicked)=\"save()\">{{ isEdit ? text.save : text.create }}</hm-button>\n </ng-container>\n</hm-modal>\n", styles: [".form-stack{display:flex;flex-direction:column;gap:var(--hm-space-sm)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: AlertComponent, selector: "hm-alert", inputs: ["variant", "appearance", "heading", "closable", "size", "showIcon", "link", "linkLabel", "linkTarget", "dismissKey", "duration"], outputs: ["closed"] }, { kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "component", type: CheckboxComponent, selector: "hm-checkbox", inputs: ["label", "labelHidden", "checked", "indeterminate"], outputs: ["checkedChange"] }, { kind: "component", type: ComboboxComponent, selector: "hm-combobox", inputs: ["label", "options", "value", "placeholder", "icon", "hint", "size", "labelHidden", "clearable", "loading", "allowCustomValue"], outputs: ["valueChange"] }, { kind: "component", type: InputComponent, selector: "hm-input", inputs: ["value", "label", "type", "placeholder", "icon", "labelHidden", "hint", "prefix", "suffix", "prefixIcon", "mask", "maskValue", "min", "max", "step", "stepButtons", "maxLength", "autocomplete", "inputMode"], outputs: ["valueChange"] }, { kind: "component", type: ModalComponent, selector: "hm-modal", inputs: ["open", "heading", "size", "closeLabel", "closable", "beforeClose"], outputs: ["openChange", "closed"] }, { kind: "component", type: TextareaComponent, selector: "hm-textarea", inputs: ["value", "label", "hint", "placeholder", "maxLength", "rows", "autoGrow", "labelHidden", "loading", "maxRows", "submitKey"], outputs: ["valueChange", "submitted"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
106
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: AutoServiceGroupModalComponent, isStandalone: true, selector: "spine-auto-service-group-modal", inputs: { group: "group", clientId: "clientId", groupTypes: "groupTypes", open: "open" }, outputs: { openChange: "openChange", saved: "saved" }, usesOnChanges: true, ngImport: i0, template: "<hm-modal [open]=\"open\" (openChange)=\"onModalOpenChange($event)\" [heading]=\"isEdit ? text.editGroup : text.newGroup\" (closed)=\"close()\">\n <div class=\"form-stack\">\n <hm-alert *ngIf=\"saveError\" variant=\"error\">{{ saveError }}</hm-alert>\n @if (!isEdit) {\n <hm-input [formControl]=\"newId\" [label]=\"text.optionalId\" [placeholder]=\"text.optionalIdPlaceholder\" [hint]=\"text.optionalIdHint\" [messages]=\"{ optionalId: text.optionalIdInvalid }\" />\n }\n <hm-input [formControl]=\"name\" [label]=\"text.groupName\" [maxLength]=\"nameMax\" />\n <hm-combobox [formControl]=\"typeId\" [options]=\"typeOptions\" [label]=\"text.groupType\" [placeholder]=\"text.groupTypePlaceholder\" />\n <hm-textarea [formControl]=\"description\" [label]=\"text.groupDescription\" />\n <hm-checkbox *ngIf=\"isEdit\" [formControl]=\"enabled\" [label]=\"text.active\" />\n </div>\n\n <ng-container footer>\n <hm-button emphasis=\"medium\" (clicked)=\"close()\">{{ text.cancel }}</hm-button>\n <hm-button [disabled]=\"!canSave\" [loading]=\"saving\" (clicked)=\"save()\">{{ isEdit ? text.save : text.create }}</hm-button>\n </ng-container>\n</hm-modal>\n", styles: [".form-stack{display:flex;flex-direction:column;gap:var(--hm-space-sm)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: AlertComponent, selector: "hm-alert", inputs: ["variant", "appearance", "heading", "closable", "size", "showIcon", "link", "linkLabel", "linkTarget", "dismissKey", "duration"], outputs: ["closed"] }, { kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "component", type: CheckboxComponent, selector: "hm-checkbox", inputs: ["label", "labelHidden", "checked", "indeterminate"], outputs: ["checkedChange"] }, { kind: "component", type: ComboboxComponent, selector: "hm-combobox", inputs: ["label", "options", "value", "placeholder", "icon", "hint", "size", "labelHidden", "clearable", "loading", "allowCustomValue"], outputs: ["valueChange"] }, { kind: "component", type: InputComponent, selector: "hm-input", inputs: ["value", "label", "type", "placeholder", "icon", "labelHidden", "hint", "prefix", "suffix", "prefixIcon", "mask", "maskValue", "min", "max", "step", "stepButtons", "maxLength", "autocomplete", "inputMode"], outputs: ["valueChange"] }, { kind: "component", type: ModalComponent, selector: "hm-modal", inputs: ["open", "heading", "size", "closeLabel", "closable", "beforeClose"], outputs: ["openChange", "closed"] }, { kind: "component", type: TextareaComponent, selector: "hm-textarea", inputs: ["value", "label", "hint", "placeholder", "maxLength", "rows", "autoGrow", "labelHidden", "loading", "maxRows", "submitKey", "mentionCandidates", "mentionTrigger", "texts"], outputs: ["valueChange", "submitted", "mentioned"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
106
107
  }
107
108
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceGroupModalComponent, decorators: [{
108
109
  type: Component,
@@ -124,9 +125,118 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
124
125
  type: Output
125
126
  }] } });
126
127
 
128
+ // Mesma peça de argos admin/ui src/shared/members-loader.ts (o painel central usa a dele). Interna deste ponto de
129
+ // entrada: não vai no public-api.
130
+ /** Membros por página pedidos ao servidor: o teto das rotas `GET …/members` do argos-admin. */
131
+ const MEMBERS_PAGE_LIMIT = 200;
132
+ /**
133
+ * Lista inteira de membros carregada aos poucos: o servidor pagina (nunca monta o grupo inteiro em memória) e a tela
134
+ * pede as páginas uma depois da outra, acrescentando conforme chegam, até ter o `total` (ou vir uma página vazia). A
135
+ * lista já aparece e funciona com a primeira página; `loading` diz que ainda há páginas a caminho.
136
+ *
137
+ * `cancel()` desfaz a inscrição da requisição pendente (o HttpClient a cancela); chame ao fechar a tela ou trocar de
138
+ * grupo. Falha numa página para em `error`, com o que já chegou intacto; `retry()` continua da página que falhou.
139
+ */
140
+ class MembersLoader {
141
+ constructor(fetchPage, idOf) {
142
+ this.fetchPage = fetchPage;
143
+ this.idOf = idOf;
144
+ this.items = signal([], ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
145
+ /** Total do servidor (o grupo inteiro). */
146
+ this.total = signal(0, ...(ngDevMode ? [{ debugName: "total" }] : /* istanbul ignore next */ []));
147
+ /** Ainda pedindo páginas. */
148
+ this.loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
149
+ /** A primeira página já chegou (antes dela a tela mostra o estado de carregando). */
150
+ this.ready = signal(false, ...(ngDevMode ? [{ debugName: "ready" }] : /* istanbul ignore next */ []));
151
+ this.error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
152
+ this.loaded = computed(() => this.items().length, ...(ngDevMode ? [{ debugName: "loaded" }] : /* istanbul ignore next */ []));
153
+ this.current = null;
154
+ this.nextPage = 1;
155
+ this.ids = new Set();
156
+ }
157
+ /** Recomeça do zero (cancela o que estava a caminho). */
158
+ start() {
159
+ this.cancel();
160
+ this.items.set([]);
161
+ this.ids.clear();
162
+ this.total.set(0);
163
+ this.ready.set(false);
164
+ this.error.set(null);
165
+ this.nextPage = 1;
166
+ this.fetchNext();
167
+ }
168
+ /** Depois de uma falha: continua da página que falhou, sem perder o que já chegou. */
169
+ retry() {
170
+ if (this.loading())
171
+ return;
172
+ this.error.set(null);
173
+ this.fetchNext();
174
+ }
175
+ cancel() {
176
+ this.current?.unsubscribe();
177
+ this.current = null;
178
+ this.loading.set(false);
179
+ }
180
+ /**
181
+ * Mudança feita pela própria tela depois de gravar (incluir, remover, trocar função), sem reler: `totalDelta` mantém o
182
+ * total coerente com a lista.
183
+ */
184
+ patch(change, totalDelta = 0) {
185
+ const next = change(this.items());
186
+ this.ids.clear();
187
+ for (const item of next)
188
+ this.ids.add(this.idOf(item));
189
+ this.items.set(next);
190
+ this.total.update((n) => Math.max(next.length, n + totalDelta));
191
+ }
192
+ fetchNext() {
193
+ this.loading.set(true);
194
+ // A continuação (próxima página) roda no `complete`; com uma resposta síncrona ela começa antes do `subscribe`
195
+ // devolver, por isso a inscrição vai num contêiner criado antes.
196
+ const holder = new Subscription();
197
+ this.current = holder;
198
+ let more = false;
199
+ holder.add(this.fetchPage(this.nextPage).subscribe({
200
+ next: (res) => {
201
+ const page = res.members ?? [];
202
+ const fresh = page.filter((item) => !this.ids.has(this.idOf(item)));
203
+ for (const item of fresh)
204
+ this.ids.add(this.idOf(item));
205
+ if (fresh.length)
206
+ this.items.update((list) => [...list, ...fresh]);
207
+ this.total.set(Math.max(res.total ?? 0, this.items().length));
208
+ this.ready.set(true);
209
+ this.nextPage += 1;
210
+ // Página vazia, ou só com quem já chegou (o servidor mudou no meio), encerra: sem isso a carga não teria fim.
211
+ more = fresh.length > 0 && this.items().length < this.total();
212
+ },
213
+ error: (err) => {
214
+ if (this.current !== holder)
215
+ return;
216
+ this.current = null;
217
+ this.error.set(err);
218
+ this.ready.set(true);
219
+ this.loading.set(false);
220
+ },
221
+ complete: () => {
222
+ if (this.current !== holder)
223
+ return;
224
+ if (more) {
225
+ this.fetchNext();
226
+ }
227
+ else {
228
+ this.current = null;
229
+ this.loading.set(false);
230
+ }
231
+ },
232
+ }));
233
+ }
234
+ }
235
+
127
236
  // Mirror de admin/ui/.../groups/members/group-members.component.ts (argos-admin),
128
237
  // adaptado pra falar com /auto-service/:clientId/groups/:groupId/members em vez de
129
- // /admin/groups/:groupId/members.
238
+ // /admin/groups/:groupId/members. Todos os membros na grade: o servidor pagina e a tela pede as páginas até o fim
239
+ // (`MembersLoader`); busca local e instantânea, rolagem virtual, sem paginador.
130
240
  class AutoServiceGroupMembersComponent {
131
241
  /** Data e hora curtas no idioma ativo. */
132
242
  formatDate(value) {
@@ -135,55 +245,58 @@ class AutoServiceGroupMembersComponent {
135
245
  constructor(service) {
136
246
  this.service = service;
137
247
  this.catalog = inject(SPINE_MESSAGES);
248
+ this.i18n = inject(HermesI18nService);
138
249
  this.text = inject(SPINE_MESSAGES).autoService;
139
- /** Telas com estado em campo comum, preenchido depois de `await`: marca a view ao terminar (app sem zone.js). */
140
- this.cdr = inject(ChangeDetectorRef);
141
250
  this.clientId = '';
142
251
  this.groupId = '';
143
252
  this.refreshTrigger = 0;
253
+ /** Quantos membros o grupo tem (o total do servidor). */
144
254
  this.countChange = new EventEmitter();
145
255
  this.manageMembers = new EventEmitter();
146
- this.members = [];
147
- this.loading = false;
256
+ this.loader = new MembersLoader((page) => this.service.getGroupMembers(this.clientId, this.groupId, { page, limit: MEMBERS_PAGE_LIMIT }).pipe(tap((res) => this.countChange.emit(res.total ?? 0))), (m) => m.user_id);
148
257
  this.rowId = (m) => m.user_id;
149
258
  this.columns = computed(() => [
150
- { key: 'username', title: this.text.colUser, value: (m) => m.name || m.username, sortable: true },
259
+ // A busca da grade procura nome, login e e-mail; a ordem é pelo nome.
260
+ { key: 'username', title: this.text.colUser, value: (m) => m.name || m.username, raw: (m) => [m.name, m.username, m.email].filter(Boolean).join(' '), sortable: true },
151
261
  {
152
262
  key: 'joined_at',
153
263
  title: this.text.colMemberSince,
154
264
  value: (m) => (m.joined_at ? this.formatDate(m.joined_at) : '—'),
155
265
  sortable: true,
266
+ searchable: false,
156
267
  },
157
268
  ], ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
269
+ /** "Carregando membros… 400 de 1.250". */
270
+ this.progressLabel = computed(() => {
271
+ const number = new Intl.NumberFormat(this.i18n.formatLocale());
272
+ return formatMessage(this.text.membersLoadingProgress, { loaded: number.format(this.loader.loaded()), total: number.format(this.loader.total()) });
273
+ }, ...(ngDevMode ? [{ debugName: "progressLabel" }] : /* istanbul ignore next */ []));
274
+ this.incompleteLabel = computed(() => {
275
+ const number = new Intl.NumberFormat(this.i18n.formatLocale());
276
+ return formatMessage(this.text.membersLoadIncomplete, { loaded: number.format(this.loader.loaded()), total: number.format(this.loader.total()) });
277
+ }, ...(ngDevMode ? [{ debugName: "incompleteLabel" }] : /* istanbul ignore next */ []));
278
+ inject(DestroyRef).onDestroy(() => this.loader.cancel());
158
279
  }
159
280
  ngOnChanges(changes) {
160
- if (changes['groupId'] || changes['refreshTrigger']) {
281
+ if (changes['groupId'] || changes['refreshTrigger'])
161
282
  this.load();
162
- }
163
283
  }
164
- async load() {
284
+ /** Recomeça do zero (outro grupo, ou depois de salvar no modal). */
285
+ load() {
165
286
  if (!this.clientId || !this.groupId) {
166
- this.members = [];
287
+ this.loader.cancel();
288
+ this.loader.patch(() => [], -this.loader.total());
167
289
  this.countChange.emit(0);
168
290
  return;
169
291
  }
170
- this.loading = true;
171
- try {
172
- const res = await this.service.getGroupMembers(this.clientId, this.groupId);
173
- this.members = res.members;
174
- this.countChange.emit(this.members.length);
175
- }
176
- finally {
177
- this.loading = false;
178
- this.cdr.markForCheck();
179
- }
292
+ this.loader.start();
180
293
  }
181
294
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceGroupMembersComponent, deps: [{ token: AutoServiceService }], target: i0.ɵɵFactoryTarget.Component }); }
182
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.24", type: AutoServiceGroupMembersComponent, isStandalone: true, selector: "spine-auto-service-group-members", inputs: { clientId: "clientId", groupId: "groupId", refreshTrigger: "refreshTrigger" }, outputs: { countChange: "countChange", manageMembers: "manageMembers" }, usesOnChanges: true, ngImport: i0, template: "<hm-data-grid\n [columns]=\"columns()\"\n [rows]=\"members\"\n [rowId]=\"rowId\"\n [caption]=\"text.membersCaption\"\n [loading]=\"loading\"\n [emptyMessage]=\"text.noMembers\"\n>\n <hm-button dataGridActions icon=\"employee-group\" (clicked)=\"manageMembers.emit()\">{{ text.manageMembers }}</hm-button>\n <ng-template hmCell=\"username\" let-m>\n <div class=\"user-cell\">\n <hm-account-avatar class=\"user-cell__avatar\" [name]=\"m.name || m.username\" [imageUrl]=\"m.picture\"></hm-account-avatar>\n <div class=\"user-cell__body\">\n <div class=\"user-cell__name\">\n <b>{{ m.name || m.username }}</b>\n <hm-tag [name]=\"m.enabled ? text.active : text.inactive\" [color]=\"m.enabled ? 'success' : 'danger'\" />\n </div>\n <div class=\"user-cell__email\">\n <ng-container *ngIf=\"m.name\">{{ m.username }} \u00B7 </ng-container>{{ m.email }}\n </div>\n </div>\n </div>\n </ng-template>\n</hm-data-grid>\n", styles: [".as-stack{display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.as-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.as-field-row{display:flex;flex-wrap:wrap;align-items:flex-end;gap:var(--hm-space-sm)}.as-field-row>hm-combobox{flex:1 1 16rem;min-inline-size:0}.as-summary{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.as-summary__info{display:flex;flex:1 1 16rem;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.as-summary__title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs);margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold)}.as-summary__detail{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.muted{color:var(--hm-muted)}.as-empty{margin:var(--hm-space-xs) 0 0;font-size:var(--hm-font-size-sm)}.as-secret{display:grid;gap:var(--hm-space-micro)}.as-secret__label{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-secret__row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.as-secret__value{flex:1 1 240px;min-width:0;border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:var(--hm-radius-control);padding:var(--hm-space-xs) var(--hm-space-sm);background:var(--hm-surface-muted);color:var(--hm-text);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere;-webkit-user-select:all;user-select:all}:host{display:block}.user-cell{display:flex;align-items:center;gap:var(--hm-space-xs)}.user-cell__avatar{flex-shrink:0}.user-cell__body{min-width:0}.user-cell__name{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.user-cell__email{margin-top:var(--hm-space-micro);color:var(--hm-muted);font-size:var(--hm-font-size-xs)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "component", type: TagComponent, selector: "hm-tag", inputs: ["name", "color", "compact"] }, { kind: "component", type: AccountAvatarComponent, selector: "hm-account-avatar", inputs: ["name", "imageUrl", "size", "loading"] }, { kind: "component", type: DataGridComponent, selector: "hm-data-grid", inputs: ["columns", "rows", "rowId", "caption", "rowActions", "loading", "error", "emptyMessage", "noResultsMessage", "filterable", "paginated", "pageState", "pagination", "totalItems", "sort", "selectable", "selectionMode", "selection", "rowSelectable", "rowSelectableReason", "rowDetailDisabled", "rowDetailHidden", "virtual", "rowHeight", "pinnedColumns", "columnState", "pageSizes", "fillHeight", "maxHeight", "bordered", "rowState", "rowClickable", "remoteSearch", "remoteSort", "itemsLabel", "pageSizeLabel", "exportable", "exportFileName", "groupBy", "aggregates", "aggregateFormatter", "persistKey", "persistStorage", "advancedFilter", "filterQuery", "columnManager", "columnOrder", "searchTerm"], outputs: ["pageStateChange", "sortChange", "selectionChange", "pinnedColumnsChange", "columnStateChange", "actionTriggered", "detailToggled", "rowClicked", "clearFiltersRequested", "filteredChange", "searchTermChanged", "groupByChange", "filterQueryChange", "columnOrderChange", "searchTermChange", "detailPaneToggled"] }, { kind: "directive", type: DataGridCellDirective, selector: "ng-template[hmCell]", inputs: ["hmCell"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
295
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: AutoServiceGroupMembersComponent, isStandalone: true, selector: "spine-auto-service-group-members", inputs: { clientId: "clientId", groupId: "groupId", refreshTrigger: "refreshTrigger" }, outputs: { countChange: "countChange", manageMembers: "manageMembers" }, usesOnChanges: true, ngImport: i0, template: "@if (loader.error()) {\n <!-- Falha numa p\u00E1gina: o que j\u00E1 chegou continua na grade; \"Tentar de novo\" segue de onde parou. -->\n <div class=\"members-status\">\n <span class=\"members-status__error\">{{ incompleteLabel() }}</span>\n <hm-button emphasis=\"medium\" size=\"small\" icon=\"refresh\" (clicked)=\"loader.retry()\">{{ text.retryLoad }}</hm-button>\n </div>\n} @else if (loader.loading() && loader.ready()) {\n <p class=\"members-status members-status--progress\" role=\"status\">{{ progressLabel() }} \u00B7 {{ text.membersPartialResults }}</p>\n}\n\n<!-- Todos os membros na grade (carregados aos poucos): busca local, rolagem virtual, sem paginador. -->\n<hm-data-grid\n [columns]=\"columns()\"\n [rows]=\"loader.items()\"\n [rowId]=\"rowId\"\n [caption]=\"text.membersCaption\"\n [loading]=\"!loader.ready()\"\n [emptyMessage]=\"text.noMembers\"\n [noResultsMessage]=\"text.noMembersMatch\"\n [paginated]=\"false\"\n [pagination]=\"false\"\n [virtual]=\"true\"\n maxHeight=\"32rem\"\n>\n <hm-button dataGridActions icon=\"employee-group\" (clicked)=\"manageMembers.emit()\">{{ text.manageMembers }}</hm-button>\n <ng-template hmCell=\"username\" let-m>\n <div class=\"user-cell\">\n <hm-account-avatar class=\"user-cell__avatar\" [name]=\"m.name || m.username\" [imageUrl]=\"m.picture\"></hm-account-avatar>\n <div class=\"user-cell__body\">\n <div class=\"user-cell__name\">\n <b>{{ m.name || m.username }}</b>\n <hm-tag [name]=\"m.enabled ? text.active : text.inactive\" [color]=\"m.enabled ? 'success' : 'danger'\" />\n </div>\n <div class=\"user-cell__email\">\n <ng-container *ngIf=\"m.name\">{{ m.username }} \u00B7 </ng-container>{{ m.email }}\n </div>\n </div>\n </div>\n </ng-template>\n</hm-data-grid>\n", styles: [".as-stack{display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.as-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.as-field-row{display:flex;flex-wrap:wrap;align-items:flex-end;gap:var(--hm-space-sm)}.as-field-row>hm-combobox{flex:1 1 16rem;min-inline-size:0}.as-summary{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.as-summary__info{display:flex;flex:1 1 16rem;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.as-summary__title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs);margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold)}.as-summary__detail{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.muted{color:var(--hm-muted)}.as-empty{margin:var(--hm-space-xs) 0 0;font-size:var(--hm-font-size-sm)}.as-secret{display:grid;gap:var(--hm-space-micro)}.as-secret__label{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-secret__row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.as-secret__value{flex:1 1 240px;min-width:0;border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:var(--hm-radius-control);padding:var(--hm-space-xs) var(--hm-space-sm);background:var(--hm-surface-muted);color:var(--hm-text);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere;-webkit-user-select:all;user-select:all}:host{display:block}.members-status{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs);margin:0 0 var(--hm-space-sm);font-size:var(--hm-font-size-sm)}.members-status--progress{color:var(--hm-muted)}.members-status__error{color:var(--hm-danger)}.user-cell{display:flex;align-items:center;gap:var(--hm-space-xs)}.user-cell__avatar{flex-shrink:0}.user-cell__body{min-width:0}.user-cell__name{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.user-cell__email{margin-top:var(--hm-space-micro);color:var(--hm-muted);font-size:var(--hm-font-size-xs)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "component", type: TagComponent, selector: "hm-tag", inputs: ["name", "color", "compact"] }, { kind: "component", type: AccountAvatarComponent, selector: "hm-account-avatar", inputs: ["name", "imageUrl", "size", "loading", "presence", "texts"] }, { kind: "component", type: DataGridComponent, selector: "hm-data-grid", inputs: ["columns", "rows", "rowId", "caption", "rowActions", "loading", "error", "emptyMessage", "noResultsMessage", "filterable", "paginated", "pageState", "pagination", "totalItems", "sort", "selectable", "selectionMode", "selection", "rowSelectable", "rowSelectableReason", "rowDetailDisabled", "rowDetailHidden", "virtual", "rowHeight", "pinnedColumns", "columnState", "pageSizes", "fillHeight", "maxHeight", "bordered", "rowState", "rowClickable", "remoteSearch", "remoteSort", "itemsLabel", "pageSizeLabel", "exportable", "exportFileName", "groupBy", "aggregates", "aggregateFormatter", "persistKey", "persistStorage", "advancedFilter", "filterQuery", "columnManager", "columnOrder", "striped", "columnGroups", "editMode", "editBatch", "texts", "treeChildren", "treeHasChildren", "treeExpanded", "searchTerm"], outputs: ["pageStateChange", "sortChange", "selectionChange", "pinnedColumnsChange", "columnStateChange", "actionTriggered", "detailToggled", "rowClicked", "clearFiltersRequested", "filteredChange", "searchTermChanged", "groupByChange", "filterQueryChange", "columnOrderChange", "editSaved", "editStarted", "editCanceled", "treeExpandedChange", "treeToggled", "searchTermChange", "detailPaneToggled"] }, { kind: "directive", type: DataGridCellDirective, selector: "ng-template[hmCell]", inputs: ["hmCell"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
183
296
  }
184
297
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceGroupMembersComponent, decorators: [{
185
298
  type: Component,
186
- args: [{ selector: 'spine-auto-service-group-members', standalone: true, imports: [CommonModule, ButtonComponent, TagComponent, AccountAvatarComponent, DataGridComponent, DataGridCellDirective], changeDetection: ChangeDetectionStrategy.OnPush, template: "<hm-data-grid\n [columns]=\"columns()\"\n [rows]=\"members\"\n [rowId]=\"rowId\"\n [caption]=\"text.membersCaption\"\n [loading]=\"loading\"\n [emptyMessage]=\"text.noMembers\"\n>\n <hm-button dataGridActions icon=\"employee-group\" (clicked)=\"manageMembers.emit()\">{{ text.manageMembers }}</hm-button>\n <ng-template hmCell=\"username\" let-m>\n <div class=\"user-cell\">\n <hm-account-avatar class=\"user-cell__avatar\" [name]=\"m.name || m.username\" [imageUrl]=\"m.picture\"></hm-account-avatar>\n <div class=\"user-cell__body\">\n <div class=\"user-cell__name\">\n <b>{{ m.name || m.username }}</b>\n <hm-tag [name]=\"m.enabled ? text.active : text.inactive\" [color]=\"m.enabled ? 'success' : 'danger'\" />\n </div>\n <div class=\"user-cell__email\">\n <ng-container *ngIf=\"m.name\">{{ m.username }} \u00B7 </ng-container>{{ m.email }}\n </div>\n </div>\n </div>\n </ng-template>\n</hm-data-grid>\n", styles: [".as-stack{display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.as-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.as-field-row{display:flex;flex-wrap:wrap;align-items:flex-end;gap:var(--hm-space-sm)}.as-field-row>hm-combobox{flex:1 1 16rem;min-inline-size:0}.as-summary{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.as-summary__info{display:flex;flex:1 1 16rem;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.as-summary__title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs);margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold)}.as-summary__detail{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.muted{color:var(--hm-muted)}.as-empty{margin:var(--hm-space-xs) 0 0;font-size:var(--hm-font-size-sm)}.as-secret{display:grid;gap:var(--hm-space-micro)}.as-secret__label{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-secret__row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.as-secret__value{flex:1 1 240px;min-width:0;border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:var(--hm-radius-control);padding:var(--hm-space-xs) var(--hm-space-sm);background:var(--hm-surface-muted);color:var(--hm-text);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere;-webkit-user-select:all;user-select:all}:host{display:block}.user-cell{display:flex;align-items:center;gap:var(--hm-space-xs)}.user-cell__avatar{flex-shrink:0}.user-cell__body{min-width:0}.user-cell__name{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.user-cell__email{margin-top:var(--hm-space-micro);color:var(--hm-muted);font-size:var(--hm-font-size-xs)}\n"] }]
299
+ args: [{ selector: 'spine-auto-service-group-members', standalone: true, imports: [CommonModule, ButtonComponent, TagComponent, AccountAvatarComponent, DataGridComponent, DataGridCellDirective], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (loader.error()) {\n <!-- Falha numa p\u00E1gina: o que j\u00E1 chegou continua na grade; \"Tentar de novo\" segue de onde parou. -->\n <div class=\"members-status\">\n <span class=\"members-status__error\">{{ incompleteLabel() }}</span>\n <hm-button emphasis=\"medium\" size=\"small\" icon=\"refresh\" (clicked)=\"loader.retry()\">{{ text.retryLoad }}</hm-button>\n </div>\n} @else if (loader.loading() && loader.ready()) {\n <p class=\"members-status members-status--progress\" role=\"status\">{{ progressLabel() }} \u00B7 {{ text.membersPartialResults }}</p>\n}\n\n<!-- Todos os membros na grade (carregados aos poucos): busca local, rolagem virtual, sem paginador. -->\n<hm-data-grid\n [columns]=\"columns()\"\n [rows]=\"loader.items()\"\n [rowId]=\"rowId\"\n [caption]=\"text.membersCaption\"\n [loading]=\"!loader.ready()\"\n [emptyMessage]=\"text.noMembers\"\n [noResultsMessage]=\"text.noMembersMatch\"\n [paginated]=\"false\"\n [pagination]=\"false\"\n [virtual]=\"true\"\n maxHeight=\"32rem\"\n>\n <hm-button dataGridActions icon=\"employee-group\" (clicked)=\"manageMembers.emit()\">{{ text.manageMembers }}</hm-button>\n <ng-template hmCell=\"username\" let-m>\n <div class=\"user-cell\">\n <hm-account-avatar class=\"user-cell__avatar\" [name]=\"m.name || m.username\" [imageUrl]=\"m.picture\"></hm-account-avatar>\n <div class=\"user-cell__body\">\n <div class=\"user-cell__name\">\n <b>{{ m.name || m.username }}</b>\n <hm-tag [name]=\"m.enabled ? text.active : text.inactive\" [color]=\"m.enabled ? 'success' : 'danger'\" />\n </div>\n <div class=\"user-cell__email\">\n <ng-container *ngIf=\"m.name\">{{ m.username }} \u00B7 </ng-container>{{ m.email }}\n </div>\n </div>\n </div>\n </ng-template>\n</hm-data-grid>\n", styles: [".as-stack{display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.as-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.as-field-row{display:flex;flex-wrap:wrap;align-items:flex-end;gap:var(--hm-space-sm)}.as-field-row>hm-combobox{flex:1 1 16rem;min-inline-size:0}.as-summary{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.as-summary__info{display:flex;flex:1 1 16rem;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.as-summary__title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs);margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold)}.as-summary__detail{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.muted{color:var(--hm-muted)}.as-empty{margin:var(--hm-space-xs) 0 0;font-size:var(--hm-font-size-sm)}.as-secret{display:grid;gap:var(--hm-space-micro)}.as-secret__label{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-secret__row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.as-secret__value{flex:1 1 240px;min-width:0;border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:var(--hm-radius-control);padding:var(--hm-space-xs) var(--hm-space-sm);background:var(--hm-surface-muted);color:var(--hm-text);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere;-webkit-user-select:all;user-select:all}:host{display:block}.members-status{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs);margin:0 0 var(--hm-space-sm);font-size:var(--hm-font-size-sm)}.members-status--progress{color:var(--hm-muted)}.members-status__error{color:var(--hm-danger)}.user-cell{display:flex;align-items:center;gap:var(--hm-space-xs)}.user-cell__avatar{flex-shrink:0}.user-cell__body{min-width:0}.user-cell__name{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.user-cell__email{margin-top:var(--hm-space-micro);color:var(--hm-muted);font-size:var(--hm-font-size-xs)}\n"] }]
187
300
  }], ctorParameters: () => [{ type: AutoServiceService }], propDecorators: { clientId: [{
188
301
  type: Input
189
302
  }], groupId: [{
@@ -201,6 +314,17 @@ const ROLE_OPTIONS = ['Administrador', 'Editor'];
201
314
  /** Sugestões mostradas: quem já está em outros grupos deste sistema (o servidor ordena pelos mais conectados). */
202
315
  const SUGGESTIONS = 8;
203
316
  let searchSequence = 0;
317
+ function toStaged(m) {
318
+ return {
319
+ user_id: m.user_id, username: m.username, email: m.email, name: m.name ?? null,
320
+ picture: m.picture ?? null, enabled: m.enabled, role: m.role ?? null,
321
+ joined_at: m.joined_at, added_by_username: m.added_by_username ?? null,
322
+ };
323
+ }
324
+ /**
325
+ * Membros do grupo, com as alterações guardadas até o Salvar. A lista inteira vem do servidor em páginas, uma depois
326
+ * da outra (`MembersLoader`), e já funciona com a primeira; a busca é local e instantânea sobre o que já chegou.
327
+ */
204
328
  class AutoServiceGroupMembersModalComponent {
205
329
  /** Data e hora curtas no idioma ativo. */
206
330
  formatDate(value) {
@@ -209,6 +333,7 @@ class AutoServiceGroupMembersModalComponent {
209
333
  constructor(service) {
210
334
  this.service = service;
211
335
  this.catalog = inject(SPINE_MESSAGES);
336
+ this.i18n = inject(HermesI18nService);
212
337
  this.text = inject(SPINE_MESSAGES).autoService;
213
338
  /** Telas com estado em campo comum, preenchido depois de `await`: marca a view ao terminar (app sem zone.js). */
214
339
  this.cdr = inject(ChangeDetectorRef);
@@ -229,16 +354,45 @@ class AutoServiceGroupMembersModalComponent {
229
354
  this.searchId = `spine-members-search-${++searchSequence}`;
230
355
  this.titleId = `${this.searchId}-title`;
231
356
  this.clearLabel = inject(HERMES_MESSAGES).searchField.clear;
232
- /** Filtro da lista de membros (nome, username ou e-mail). */
233
- this.memberSearch = '';
234
- this.loading = false;
357
+ /** Busca na lista de membros (nome, username ou e-mail): local e instantânea. */
358
+ this.memberSearch = signal('', ...(ngDevMode ? [{ debugName: "memberSearch" }] : /* istanbul ignore next */ []));
235
359
  this.saving = false;
236
360
  /** Falha ao gravar (no idioma ativo); o que já tinha sido gravado fica — a lista é recarregada. */
237
361
  this.saveError = null;
238
- this.members = [];
362
+ /** Os membros gravados, carregados aos poucos. Página que traz quem estava como "novo" o tira das inclusões. */
363
+ this.loader = new MembersLoader((page) => this.service.getGroupMembers(this.clientId, this.groupId, { page, limit: MEMBERS_PAGE_LIMIT }).pipe(map((res) => ({ total: res.total, members: (res.members ?? []).map(toStaged) })), tap((res) => this.dropStagedExisting(res.members))), (m) => m.user_id);
364
+ /** Inclusões ainda não salvas, a mais recente primeiro. */
365
+ this.added = signal([], ...(ngDevMode ? [{ debugName: "added" }] : /* istanbul ignore next */ []));
366
+ /** Trocas de função dos que já são membros (id -> função nova; null = sem função). */
367
+ this.roleChanges = signal(new Map(), ...(ngDevMode ? [{ debugName: "roleChanges" }] : /* istanbul ignore next */ []));
368
+ /** A lista inteira: as inclusões no topo, depois os gravados com as trocas de função aplicadas. */
369
+ this.members = computed(() => {
370
+ const changes = this.roleChanges();
371
+ return [
372
+ ...this.added(),
373
+ ...this.loader.items().map((m) => (changes.has(m.user_id) ? { ...m, role: changes.get(m.user_id) ?? null } : m)),
374
+ ];
375
+ }, ...(ngDevMode ? [{ debugName: "members" }] : /* istanbul ignore next */ []));
376
+ this.filteredMembers = computed(() => {
377
+ const term = this.memberSearch().trim().toLowerCase();
378
+ const all = this.members();
379
+ if (!term)
380
+ return all;
381
+ return all.filter((m) => [m.name, m.username, m.email].some((v) => (v ?? '').toLowerCase().includes(term)));
382
+ }, ...(ngDevMode ? [{ debugName: "filteredMembers" }] : /* istanbul ignore next */ []));
383
+ /** Número no título: o grupo inteiro (total do servidor) mais as inclusões ainda não salvas. */
384
+ this.memberCount = computed(() => this.loader.total() + this.added().length, ...(ngDevMode ? [{ debugName: "memberCount" }] : /* istanbul ignore next */ []));
385
+ /** "Carregando membros… 400 de 1.250". */
386
+ this.progressLabel = computed(() => {
387
+ const number = new Intl.NumberFormat(this.i18n.formatLocale());
388
+ return formatMessage(this.text.membersLoadingProgress, { loaded: number.format(this.loader.loaded()), total: number.format(this.loader.total()) });
389
+ }, ...(ngDevMode ? [{ debugName: "progressLabel" }] : /* istanbul ignore next */ []));
390
+ this.incompleteLabel = computed(() => {
391
+ const number = new Intl.NumberFormat(this.i18n.formatLocale());
392
+ return formatMessage(this.text.membersLoadIncomplete, { loaded: number.format(this.loader.loaded()), total: number.format(this.loader.total()) });
393
+ }, ...(ngDevMode ? [{ debugName: "incompleteLabel" }] : /* istanbul ignore next */ []));
239
394
  /** Ids marcados pra remoção — o grid seleciona por id (não por objeto, ver `GridSelection`). */
240
395
  this.checkedForRemovalIds = EMPTY_SELECTION;
241
- this.originalRoles = new Map();
242
396
  this.addSearch = '';
243
397
  this.addSearchResults = [];
244
398
  this.selectedToAdd = null;
@@ -247,12 +401,7 @@ class AutoServiceGroupMembersModalComponent {
247
401
  this.suggestions = [];
248
402
  this.rowId = (m) => m.user_id;
249
403
  this.columns = computed(() => [
250
- {
251
- key: 'username',
252
- title: this.text.colUser,
253
- value: (m) => [m.name, m.username, m.email].filter(Boolean).join(' '),
254
- sortable: true,
255
- },
404
+ { key: 'username', title: this.text.colUser, value: (m) => [m.name, m.username, m.email].filter(Boolean).join(' '), sortable: true },
256
405
  { key: 'role', title: this.text.colRole, value: (m) => m.role ?? '—' },
257
406
  {
258
407
  key: 'joined_at',
@@ -261,22 +410,23 @@ class AutoServiceGroupMembersModalComponent {
261
410
  sortable: true,
262
411
  },
263
412
  ], ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
413
+ inject(DestroyRef).onDestroy(() => this.loader.cancel());
264
414
  }
265
415
  ngOnChanges(changes) { if (changes['open'] && this.open) {
266
416
  this.resetAndLoad();
267
417
  } }
268
- onModalOpenChange(value) { this.open = value; this.openChange.emit(value); }
418
+ onModalOpenChange(value) {
419
+ this.open = value;
420
+ // Fechado, nada mais vem do servidor (a página pendente é cancelada).
421
+ if (!value)
422
+ this.loader.cancel();
423
+ this.openChange.emit(value);
424
+ }
269
425
  close() { this.onModalOpenChange(false); }
270
426
  get checkedForRemoval() {
271
- return this.members.filter((m) => this.checkedForRemovalIds.has(m.user_id));
427
+ return this.loader.items().filter((m) => this.checkedForRemovalIds.has(m.user_id));
272
428
  }
273
429
  get removalCount() { return this.checkedForRemovalIds.size; }
274
- get filteredMembers() {
275
- const term = this.memberSearch.trim().toLowerCase();
276
- if (!term)
277
- return this.members;
278
- return this.members.filter((m) => [m.name, m.username, m.email].some((v) => (v ?? '').toLowerCase().includes(term)));
279
- }
280
430
  /** Segunda linha da pessoa: "username · e-mail" (só o e-mail quando o nome já é o username). */
281
431
  metaOf(u) {
282
432
  return [u.name ? u.username : null, u.email].filter(Boolean).join(' · ');
@@ -286,44 +436,45 @@ class AutoServiceGroupMembersModalComponent {
286
436
  return [this.metaOf(u), u.groups?.length ? formatMessage(this.text.suggestionGroups, { groups: u.groups.join(', ') }) : '']
287
437
  .filter(Boolean).join('\n');
288
438
  }
289
- async resetAndLoad() {
439
+ resetAndLoad() {
290
440
  this.addSearch = '';
291
441
  this.addSearchResults = [];
292
442
  this.selectedToAdd = null;
293
443
  this.selectedRole = null;
294
- this.memberSearch = '';
444
+ this.memberSearch.set('');
295
445
  this.checkedForRemovalIds = EMPTY_SELECTION;
296
446
  this.saveError = null;
297
- this.loading = true;
298
- try {
299
- const res = await this.service.getGroupMembers(this.clientId, this.groupId);
300
- this.members = (res.members ?? []).map((m) => ({
301
- user_id: m.user_id, username: m.username, email: m.email, name: m.name ?? null,
302
- picture: m.picture ?? null, enabled: m.enabled, role: m.role ?? null,
303
- joined_at: m.joined_at, added_by_username: m.added_by_username ?? null,
304
- }));
305
- this.originalRoles = new Map(this.members.map(m => [m.user_id, m.role ?? null]));
306
- this.cdr.markForCheck();
307
- await this.loadSuggestions();
308
- }
309
- finally {
310
- this.loading = false;
311
- this.cdr.markForCheck();
312
- }
313
- }
314
- /** Sugestões do servidor; falha (Argos sem a rota, rede) só deixa sem sugestões. */
447
+ this.added.set([]);
448
+ this.roleChanges.set(new Map());
449
+ this.loader.start();
450
+ void this.loadSuggestions();
451
+ }
452
+ /** Quem foi incluído como "novo" mas a página que chegou mostra que já é membro: sai das inclusões. */
453
+ dropStagedExisting(page) {
454
+ const ids = new Set(page.map((m) => m.user_id));
455
+ if (this.added().some((m) => ids.has(m.user_id)))
456
+ this.added.update((list) => list.filter((m) => !ids.has(m.user_id)));
457
+ }
458
+ /** Ids que não podem entrar de novo: os membros já carregados e as inclusões. */
459
+ takenIds() {
460
+ return new Set([...this.loader.items().map((m) => m.user_id), ...this.added().map((m) => m.user_id)]);
461
+ }
462
+ /** Sugestões do servidor (ele já tira quem é membro); falha (Argos sem a rota, rede) só deixa sem sugestões. */
315
463
  async loadSuggestions() {
316
464
  this.suggestions = [];
317
465
  if (!this.clientId || !this.groupId)
318
466
  return;
319
467
  try {
320
468
  const res = await this.service.getMemberSuggestions(this.clientId, this.groupId, SUGGESTIONS);
321
- const existingIds = new Set(this.members.map((m) => m.user_id));
322
- this.suggestions = (res.users ?? []).filter((u) => !existingIds.has(u.id)).slice(0, SUGGESTIONS);
469
+ const taken = this.takenIds();
470
+ this.suggestions = (res.users ?? []).filter((u) => !taken.has(u.id)).slice(0, SUGGESTIONS);
323
471
  }
324
472
  catch {
325
473
  this.suggestions = [];
326
474
  }
475
+ finally {
476
+ this.cdr.markForCheck();
477
+ }
327
478
  }
328
479
  /** Chip de sugestão: inclui direto (com a função escolhida ao lado), sem passar pela busca. */
329
480
  addSuggestion(user) { this.stageMember(user); }
@@ -337,8 +488,8 @@ class AutoServiceGroupMembersModalComponent {
337
488
  }
338
489
  this.addSearchTimer = setTimeout(async () => {
339
490
  const res = await this.service.searchUsers(this.clientId, { search: value.trim(), limit: 10 });
340
- const existingIds = new Set(this.members.map(m => m.user_id));
341
- this.addSearchResults = (res.users ?? []).filter((u) => !existingIds.has(u.id));
491
+ const taken = this.takenIds();
492
+ this.addSearchResults = (res.users ?? []).filter((u) => !taken.has(u.id));
342
493
  this.cdr.markForCheck();
343
494
  }, 300);
344
495
  }
@@ -349,22 +500,36 @@ class AutoServiceGroupMembersModalComponent {
349
500
  this.stageMember(this.selectedToAdd);
350
501
  }
351
502
  stageMember(user) {
352
- if (this.members.some((m) => m.user_id === user.id))
503
+ if (this.takenIds().has(user.id))
353
504
  return;
354
- this.members = [{
505
+ this.added.update((list) => [{
355
506
  user_id: user.id, username: user.username, email: user.email, name: user.name ?? null, picture: user.picture ?? null, enabled: user.enabled,
356
507
  role: this.selectedRole, joined_at: null, added_by_username: null, new: true,
357
- }, ...this.members];
508
+ }, ...list]);
358
509
  this.suggestions = this.suggestions.filter((u) => u.id !== user.id);
359
510
  this.clearSelectedToAdd();
360
511
  this.selectedRole = null;
361
512
  }
362
- removeStagedNew(member) { this.members = this.members.filter(m => m.user_id !== member.user_id); }
513
+ removeStagedNew(member) { this.added.update((list) => list.filter((m) => m.user_id !== member.user_id)); }
514
+ /** Função escolhida na linha: inclusão guarda nela; membro guarda como troca (ou deixa de ser, se voltou à gravada). */
515
+ setRole(member, value) {
516
+ const role = value || null;
517
+ if (member.new) {
518
+ this.added.update((list) => list.map((m) => (m.user_id === member.user_id ? { ...m, role } : m)));
519
+ return;
520
+ }
521
+ const stored = this.loader.items().find((m) => m.user_id === member.user_id);
522
+ const next = new Map(this.roleChanges());
523
+ if (stored && role === (stored.role ?? null))
524
+ next.delete(member.user_id);
525
+ else
526
+ next.set(member.user_id, role);
527
+ this.roleChanges.set(next);
528
+ }
363
529
  /** `[(selection)]` do grid: separa o que ainda é staged (nunca existiu no servidor — remove
364
530
  * na hora, não "marca pra remover") do que já existe (fica marcado, removido só no Salvar). */
365
531
  onSelectionChange(ids) {
366
- const cleaned = this.members.filter((m) => ids.has(m.user_id));
367
- const stillNew = cleaned.filter((m) => m.new);
532
+ const stillNew = this.added().filter((m) => ids.has(m.user_id));
368
533
  for (const m of stillNew)
369
534
  this.removeStagedNew(m);
370
535
  this.checkedForRemovalIds = new Set([...ids].filter((id) => !stillNew.some((m) => m.user_id === id)));
@@ -379,17 +544,18 @@ class AutoServiceGroupMembersModalComponent {
379
544
  this.saving = true;
380
545
  this.saveError = null;
381
546
  try {
382
- const toAdd = this.members.filter(m => m.new);
383
- for (const m of toAdd) {
547
+ for (const m of this.added()) {
384
548
  await this.service.addGroupMember(this.clientId, this.groupId, m.user_id, m.role ?? null);
385
549
  }
386
- for (const m of this.checkedForRemoval) {
550
+ const removed = this.checkedForRemoval;
551
+ for (const m of removed) {
387
552
  await this.service.removeGroupMember(this.clientId, this.groupId, m.user_id);
388
553
  }
389
- const removedIds = this.checkedForRemovalIds;
390
- const roleChanges = this.members.filter(m => !m.new && !removedIds.has(m.user_id) && (m.role ?? null) !== (this.originalRoles.get(m.user_id) ?? null));
391
- for (const m of roleChanges) {
392
- await this.service.updateGroupMemberRole(this.clientId, this.groupId, m.user_id, m.role ?? null);
554
+ const removedIds = new Set(removed.map((m) => m.user_id));
555
+ for (const [userId, role] of this.roleChanges()) {
556
+ if (removedIds.has(userId))
557
+ continue;
558
+ await this.service.updateGroupMemberRole(this.clientId, this.groupId, userId, role);
393
559
  }
394
560
  this.saved.emit();
395
561
  this.close();
@@ -399,7 +565,7 @@ class AutoServiceGroupMembersModalComponent {
399
565
  this.saveError = autoServiceErrorText(err, this.text, this.text.membersSaveError);
400
566
  this.saved.emit();
401
567
  const error = this.saveError;
402
- await this.resetAndLoad().catch(() => undefined);
568
+ this.resetAndLoad();
403
569
  this.saveError = error;
404
570
  }
405
571
  finally {
@@ -408,14 +574,14 @@ class AutoServiceGroupMembersModalComponent {
408
574
  }
409
575
  }
410
576
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceGroupMembersModalComponent, deps: [{ token: AutoServiceService }], target: i0.ɵɵFactoryTarget.Component }); }
411
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: AutoServiceGroupMembersModalComponent, isStandalone: true, selector: "spine-auto-service-group-members-modal", inputs: { clientId: "clientId", groupId: "groupId", groupName: "groupName", open: "open" }, outputs: { openChange: "openChange", saved: "saved" }, usesOnChanges: true, ngImport: i0, template: "<hm-modal [open]=\"open\" (openChange)=\"onModalOpenChange($event)\" [heading]=\"fmt(text.membersHeading, { group: groupName })\" size=\"large\" (closed)=\"close()\">\n <div class=\"gm\">\n @if (saveError) {\n <hm-alert variant=\"error\">{{ saveError }}</hm-alert>\n }\n\n <!-- Adicionar: busca (candidatos da API a cada tecla) + fun\u00E7\u00E3o + Adicionar; sugest\u00F5es em chips. -->\n <section class=\"gm-add\" [attr.aria-label]=\"text.addUser\">\n <div class=\"gm-add__row\">\n <div class=\"gm-field gm-field--grow\">\n <label class=\"gm-label\" [attr.for]=\"searchId\">{{ text.addUser }}</label>\n <div class=\"gm-search\">\n <hm-icon shape=\"search\" size=\"sm\" />\n <input type=\"text\" [id]=\"searchId\" [placeholder]=\"text.userSearch\" autocomplete=\"off\"\n [ngModel]=\"addSearch\" (ngModelChange)=\"onAddSearchChange($event)\" />\n @if (addSearch) {\n <button type=\"button\" class=\"gm-search__clear\" [attr.aria-label]=\"clearLabel\" (click)=\"clearSelectedToAdd()\">\n <hm-icon shape=\"times\" size=\"sm\" />\n </button>\n }\n @if (addSearchResults.length) {\n <ul class=\"gm-search__results\">\n @for (u of addSearchResults; track u.id) {\n <li>\n <button type=\"button\" class=\"gm-search__result\" (click)=\"selectCandidate(u)\">\n <span class=\"gm-user\">\n <hm-account-avatar size=\"small\" [name]=\"u.name || u.username\" [imageUrl]=\"u.picture\" />\n <span class=\"gm-user__body\">\n <span class=\"gm-user__name\"><span>{{ u.name || u.username }}</span></span>\n <span class=\"gm-user__meta\">{{ metaOf(u) }}</span>\n </span>\n </span>\n </button>\n </li>\n }\n </ul>\n }\n </div>\n </div>\n\n <hm-combobox class=\"gm-field--role\" [label]=\"text.role\" [options]=\"roleCombo()\" [value]=\"selectedRole ?? ''\" (valueChange)=\"selectedRole = $event || null\" />\n\n <hm-button icon=\"plus\" [disabled]=\"!selectedToAdd\" (clicked)=\"addSelectedMember()\">{{ text.add }}</hm-button>\n </div>\n\n @if (suggestions.length) {\n <div class=\"gm-suggestions\">\n <span class=\"gm-suggestions__title\" [title]=\"text.suggestionsHint\">{{ text.suggestions }}</span>\n <ul class=\"gm-suggestions__list\" [attr.aria-label]=\"text.suggestionsHint\">\n @for (u of suggestions; track u.id) {\n <li>\n <button type=\"button\" class=\"gm-chip\" [title]=\"suggestionTitle(u)\"\n [attr.aria-label]=\"fmt(text.addSuggestion, { name: u.name || u.username })\" (click)=\"addSuggestion(u)\">\n <hm-account-avatar [name]=\"u.name || u.username\" [imageUrl]=\"u.picture\" />\n <span class=\"gm-chip__name\">{{ u.name || u.username }}</span>\n <hm-icon class=\"gm-chip__plus\" shape=\"plus\" size=\"xs\" />\n </button>\n </li>\n }\n </ul>\n </div>\n }\n </section>\n\n <div class=\"gm-split\">\n <section class=\"gm-members\" [attr.aria-labelledby]=\"titleId\">\n <div class=\"gm-members__head\">\n <h3 class=\"gm-members__title\" [id]=\"titleId\">{{ text.membersTitle }} <hm-badge [value]=\"members.length\" intent=\"primary\" [max]=\"999\" /></h3>\n <div class=\"gm-search gm-members__filter\">\n <hm-icon shape=\"search\" size=\"sm\" />\n <input type=\"text\" [placeholder]=\"text.memberSearch\" [attr.aria-label]=\"text.memberSearch\" [ngModel]=\"memberSearch\" (ngModelChange)=\"memberSearch = $event\" />\n </div>\n </div>\n\n <hm-data-grid\n [columns]=\"columns()\"\n [rows]=\"filteredMembers\"\n [rowId]=\"rowId\"\n [caption]=\"fmt(text.membersOf, { group: groupName })\"\n [loading]=\"loading\"\n [selectable]=\"true\"\n [selection]=\"checkedForRemovalIds\"\n [pageSizes]=\"[10, 25, 50]\"\n (selectionChange)=\"onSelectionChange($event)\"\n [emptyMessage]=\"text.noMembersShort\"\n >\n <ng-template hmCell=\"username\" let-m>\n <span class=\"gm-user\">\n <hm-account-avatar [name]=\"m.name || m.username\" [imageUrl]=\"m.picture\" />\n <span class=\"gm-user__body\">\n <span class=\"gm-user__name\">\n <span>{{ m.name || m.username }}</span>\n @if (m.new) { <hm-tag [name]=\"text.newMember\" color=\"info\" /> }\n </span>\n <span class=\"gm-user__meta\">{{ metaOf(m) }}</span>\n </span>\n </span>\n </ng-template>\n\n <ng-template hmCell=\"role\" let-m>\n <hm-combobox class=\"gm-role\" [label]=\"text.colRole\" labelHidden [options]=\"roleCombo()\" [disabled]=\"isMarkedForRemoval(m)\"\n [value]=\"m.role ?? ''\" (valueChange)=\"m.role = $event || null\" />\n </ng-template>\n\n <ng-template hmCell=\"joined_at\" let-m>\n @if (m.joined_at) {\n {{ formatDate(m.joined_at) }}\n @if (m.added_by_username) {\n <div class=\"gm-joined-by\">{{ fmt(text.addedBy, { user: m.added_by_username }) }}</div>\n }\n } @else {\n \u2014\n }\n </ng-template>\n </hm-data-grid>\n </section>\n\n <!-- Remo\u00E7\u00E3o: sempre ao lado; vazia, s\u00F3 a dica. A remo\u00E7\u00E3o s\u00F3 vale ao salvar. -->\n <aside class=\"gm-removal\" [attr.aria-label]=\"text.removal\">\n <div class=\"gm-removal__head\">\n <hm-icon shape=\"trash\" size=\"sm\" /> {{ text.removal }}\n @if (removalCount) { <hm-badge [value]=\"removalCount\" intent=\"danger\" /> }\n </div>\n @if (removalCount === 0) {\n <div class=\"gm-removal__empty\">\n <hm-icon shape=\"employee-group\" size=\"md\" />\n <p>{{ text.removalEmpty }}</p>\n </div>\n } @else {\n <ul class=\"gm-removal__list\">\n @for (m of checkedForRemoval; track m.user_id) {\n <li class=\"gm-removal__item\">\n <span class=\"gm-user\">\n <hm-account-avatar size=\"small\" [name]=\"m.name || m.username\" [imageUrl]=\"m.picture\" />\n <span class=\"gm-user__body\">\n <span class=\"gm-user__name\"><span>{{ m.name || m.username }}</span></span>\n <span class=\"gm-user__meta\">{{ metaOf(m) }}</span>\n </span>\n </span>\n <hm-icon-button emphasis=\"subtle\" icon=\"undo\" [label]=\"text.undoRemoval\" (clicked)=\"undoRemoval(m)\" />\n </li>\n }\n </ul>\n }\n </aside>\n </div>\n </div>\n\n <ng-container footer>\n <hm-button emphasis=\"medium\" (clicked)=\"close()\">{{ text.cancel }}</hm-button>\n <hm-button [loading]=\"saving\" [disabled]=\"saving\" (clicked)=\"save()\" icon=\"floppy\">{{ text.saveChanges }}</hm-button>\n </ng-container>\n</hm-modal>\n", styles: ["@charset \"UTF-8\";:host{display:contents}.gm{display:flex;flex-direction:column;gap:var(--hm-space-md);min-width:0}.gm-add{display:flex;flex-direction:column;gap:var(--hm-space-sm);padding:var(--hm-space-md);border:1px solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface-raised);min-width:0}.gm-add__row{display:flex;align-items:flex-end;gap:var(--hm-space-sm);flex-wrap:wrap;min-width:0}.gm-field{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0}.gm-field--grow{flex:1 1 18rem}.gm-field--role{flex:0 1 14rem}.gm-label{font-size:var(--hm-font-size-sm);font-weight:600}.gm-search{position:relative;display:flex;align-items:center;gap:var(--hm-space-xs);height:var(--hm-control-height);padding:0 var(--hm-space-sm);border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised);color:var(--hm-muted)}.gm-search:focus-within{outline:2px solid var(--hm-focus-color);outline-offset:1px}.gm-search input{flex:1;min-width:0;height:100%;border:0;outline:none;background:transparent;font:inherit;font-size:var(--hm-font-size-sm);color:var(--hm-text)}.gm-search input::placeholder{color:var(--hm-muted)}.gm-search__clear{display:inline-grid;place-items:center;flex:none;padding:0;border:0;background:none;color:inherit;cursor:pointer;line-height:0}.gm-search__results{position:absolute;inset-block-start:calc(100% + 4px);inset-inline:0;z-index:20;max-height:16rem;margin:0;padding:var(--hm-space-xs) 0;overflow-y:auto;list-style:none;background:var(--hm-surface-overlay);border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);box-shadow:var(--hm-shadow-overlay)}.gm-search__result{display:flex;align-items:center;gap:var(--hm-space-sm);width:100%;padding:var(--hm-space-xs) var(--hm-space-sm);border:0;background:none;color:var(--hm-text);font:inherit;text-align:start;cursor:pointer}.gm-search__result:hover,.gm-search__result:focus-visible{background:var(--hm-hover)}.gm-search__result:focus-visible{outline:2px solid var(--hm-focus-color);outline-offset:-2px}.gm-suggestions{display:flex;align-items:flex-start;gap:var(--hm-space-sm);min-width:0}.gm-suggestions__title{flex:none;display:inline-flex;align-items:center;min-height:2rem;font-size:var(--hm-font-size-xs);font-weight:600;color:var(--hm-muted)}.gm-suggestions__list{display:flex;flex-wrap:wrap;gap:var(--hm-space-xs);min-width:0;margin:0;padding:0;list-style:none}.gm-chip{display:inline-flex;align-items:center;gap:var(--hm-space-xs);max-width:16rem;min-height:2rem;padding:2px var(--hm-space-sm) 2px 2px;border:1px solid var(--hm-border);border-radius:var(--hm-radius-pill);background:var(--hm-surface-raised);color:var(--hm-text);font:inherit;font-size:var(--hm-font-size-sm);cursor:pointer}.gm-chip hm-account-avatar{--hm-avatar-size: 1.625rem;flex:none}.gm-chip:hover{background:var(--hm-hover);border-color:var(--hm-accent)}.gm-chip:focus-visible{outline:2px solid var(--hm-focus-color);outline-offset:1px}.gm-chip__name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.gm-chip__plus{flex:none;color:var(--hm-accent)}.gm-split{display:grid;grid-template-columns:minmax(0,1fr) minmax(15rem,18rem);gap:var(--hm-space-md);align-items:start;min-width:0}@media(max-width:960px){.gm-split{grid-template-columns:minmax(0,1fr)}}.gm-members{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-width:0}.gm-members__head{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-sm);flex-wrap:wrap}.gm-members__title{display:inline-flex;align-items:center;gap:var(--hm-space-xs);margin:0;font-size:var(--hm-font-size-md);font-weight:600}.gm-members__filter{flex:0 1 16rem;height:2rem}.gm-user{display:flex;align-items:center;gap:var(--hm-space-sm);min-width:0}.gm-user hm-account-avatar{flex:none}.gm-user__body{display:flex;flex-direction:column;min-width:0}.gm-user__name{display:flex;align-items:center;gap:var(--hm-space-xs);min-width:0;font-weight:600}.gm-user__name>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.gm-user__meta{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.gm-role{display:block;min-width:11rem}.gm-joined-by{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.gm-removal{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-width:0;padding:var(--hm-space-md);border:1px solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.gm-removal__head{display:flex;align-items:center;gap:var(--hm-space-xs);font-weight:600;color:var(--hm-danger)}.gm-removal__empty{display:flex;align-items:flex-start;gap:var(--hm-space-sm);padding:var(--hm-space-sm);border-radius:var(--hm-radius-control);background:var(--hm-surface-muted);color:var(--hm-muted);font-size:var(--hm-font-size-sm);line-height:1.45}.gm-removal__empty hm-icon{flex:none;margin-top:2px}.gm-removal__empty p{margin:0}.gm-removal__list{display:flex;flex-direction:column;gap:var(--hm-space-xs);margin:0;padding:0;list-style:none}.gm-removal__item{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-xs);min-width:0}.gm-removal__item .gm-user{flex:1}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: AccountAvatarComponent, selector: "hm-account-avatar", inputs: ["name", "imageUrl", "size", "loading"] }, { kind: "component", type: AlertComponent, selector: "hm-alert", inputs: ["variant", "appearance", "heading", "closable", "size", "showIcon", "link", "linkLabel", "linkTarget", "dismissKey", "duration"], outputs: ["closed"] }, { kind: "component", type: BadgeComponent, selector: "hm-badge", inputs: ["value", "max", "dot", "intent", "label"] }, { kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "component", type: ComboboxComponent, selector: "hm-combobox", inputs: ["label", "options", "value", "placeholder", "icon", "hint", "size", "labelHidden", "clearable", "loading", "allowCustomValue"], outputs: ["valueChange"] }, { kind: "component", type: DataGridComponent, selector: "hm-data-grid", inputs: ["columns", "rows", "rowId", "caption", "rowActions", "loading", "error", "emptyMessage", "noResultsMessage", "filterable", "paginated", "pageState", "pagination", "totalItems", "sort", "selectable", "selectionMode", "selection", "rowSelectable", "rowSelectableReason", "rowDetailDisabled", "rowDetailHidden", "virtual", "rowHeight", "pinnedColumns", "columnState", "pageSizes", "fillHeight", "maxHeight", "bordered", "rowState", "rowClickable", "remoteSearch", "remoteSort", "itemsLabel", "pageSizeLabel", "exportable", "exportFileName", "groupBy", "aggregates", "aggregateFormatter", "persistKey", "persistStorage", "advancedFilter", "filterQuery", "columnManager", "columnOrder", "searchTerm"], outputs: ["pageStateChange", "sortChange", "selectionChange", "pinnedColumnsChange", "columnStateChange", "actionTriggered", "detailToggled", "rowClicked", "clearFiltersRequested", "filteredChange", "searchTermChanged", "groupByChange", "filterQueryChange", "columnOrderChange", "searchTermChange", "detailPaneToggled"] }, { kind: "directive", type: DataGridCellDirective, selector: "ng-template[hmCell]", inputs: ["hmCell"] }, { kind: "component", type: IconButtonComponent, selector: "hm-icon-button", inputs: ["label", "icon", "solid", "iconDirection", "emphasis", "intent", "size", "disabled", "toggle", "pressed", "keyShortcuts"], outputs: ["pressedChange", "clicked"] }, { kind: "component", type: IconComponent, selector: "hm-icon", inputs: ["shape", "direction", "size", "solid", "badge", "status", "flip", "inverse", "label"] }, { kind: "component", type: ModalComponent, selector: "hm-modal", inputs: ["open", "heading", "size", "closeLabel", "closable", "beforeClose"], outputs: ["openChange", "closed"] }, { kind: "component", type: TagComponent, selector: "hm-tag", inputs: ["name", "color", "compact"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
577
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: AutoServiceGroupMembersModalComponent, isStandalone: true, selector: "spine-auto-service-group-members-modal", inputs: { clientId: "clientId", groupId: "groupId", groupName: "groupName", open: "open" }, outputs: { openChange: "openChange", saved: "saved" }, usesOnChanges: true, ngImport: i0, template: "<hm-modal [open]=\"open\" (openChange)=\"onModalOpenChange($event)\" [heading]=\"fmt(text.membersHeading, { group: groupName })\" size=\"large\" (closed)=\"close()\">\n <div class=\"gm\">\n @if (saveError) {\n <hm-alert variant=\"error\">{{ saveError }}</hm-alert>\n }\n\n <!-- Adicionar: busca (candidatos da API a cada tecla) + fun\u00E7\u00E3o + Adicionar; sugest\u00F5es em chips. -->\n <section class=\"gm-add\" [attr.aria-label]=\"text.addUser\">\n <div class=\"gm-add__row\">\n <div class=\"gm-field gm-field--grow\">\n <label class=\"gm-label\" [attr.for]=\"searchId\">{{ text.addUser }}</label>\n <div class=\"gm-search\">\n <hm-icon shape=\"search\" size=\"sm\" />\n <input type=\"text\" [id]=\"searchId\" [placeholder]=\"text.userSearch\" autocomplete=\"off\"\n [ngModel]=\"addSearch\" (ngModelChange)=\"onAddSearchChange($event)\" />\n @if (addSearch) {\n <button type=\"button\" class=\"gm-search__clear\" [attr.aria-label]=\"clearLabel\" (click)=\"clearSelectedToAdd()\">\n <hm-icon shape=\"times\" size=\"sm\" />\n </button>\n }\n @if (addSearchResults.length) {\n <ul class=\"gm-search__results\">\n @for (u of addSearchResults; track u.id) {\n <li>\n <button type=\"button\" class=\"gm-search__result\" (click)=\"selectCandidate(u)\">\n <span class=\"gm-user\">\n <hm-account-avatar size=\"small\" [name]=\"u.name || u.username\" [imageUrl]=\"u.picture\" />\n <span class=\"gm-user__body\">\n <span class=\"gm-user__name\"><span>{{ u.name || u.username }}</span></span>\n <span class=\"gm-user__meta\">{{ metaOf(u) }}</span>\n </span>\n </span>\n </button>\n </li>\n }\n </ul>\n }\n </div>\n </div>\n\n <hm-combobox class=\"gm-field--role\" [label]=\"text.role\" [options]=\"roleCombo()\" [value]=\"selectedRole ?? ''\" (valueChange)=\"selectedRole = $event || null\" />\n\n <hm-button icon=\"plus\" [disabled]=\"!selectedToAdd\" (clicked)=\"addSelectedMember()\">{{ text.add }}</hm-button>\n </div>\n\n @if (suggestions.length) {\n <div class=\"gm-suggestions\">\n <span class=\"gm-suggestions__title\" [title]=\"text.suggestionsHint\">{{ text.suggestions }}</span>\n <ul class=\"gm-suggestions__list\" [attr.aria-label]=\"text.suggestionsHint\">\n @for (u of suggestions; track u.id) {\n <li>\n <button type=\"button\" class=\"gm-chip\" [title]=\"suggestionTitle(u)\"\n [attr.aria-label]=\"fmt(text.addSuggestion, { name: u.name || u.username })\" (click)=\"addSuggestion(u)\">\n <hm-account-avatar [name]=\"u.name || u.username\" [imageUrl]=\"u.picture\" />\n <span class=\"gm-chip__name\">{{ u.name || u.username }}</span>\n <hm-icon class=\"gm-chip__plus\" shape=\"plus\" size=\"xs\" />\n </button>\n </li>\n }\n </ul>\n </div>\n }\n </section>\n\n <div class=\"gm-split\">\n <section class=\"gm-members\" [attr.aria-labelledby]=\"titleId\">\n <div class=\"gm-members__head\">\n <h3 class=\"gm-members__title\" [id]=\"titleId\">{{ text.membersTitle }} <hm-badge [value]=\"memberCount()\" intent=\"primary\" [max]=\"9999\" /></h3>\n <div class=\"gm-search gm-members__filter\">\n <hm-icon shape=\"search\" size=\"sm\" />\n <input type=\"text\" [placeholder]=\"text.memberSearch\" [attr.aria-label]=\"text.memberSearch\" [ngModel]=\"memberSearch()\" (ngModelChange)=\"memberSearch.set($event)\" />\n </div>\n </div>\n\n @if (loader.error()) {\n <div class=\"gm-status\">\n <span class=\"gm-status__error\">{{ incompleteLabel() }}</span>\n <hm-button emphasis=\"medium\" size=\"small\" icon=\"refresh\" (clicked)=\"loader.retry()\">{{ text.retryLoad }}</hm-button>\n </div>\n } @else if (loader.loading() && loader.ready()) {\n <p class=\"gm-status gm-status--progress\" role=\"status\">\n {{ progressLabel() }}@if (memberSearch().trim()) { \u00B7 {{ text.membersPartialResults }} }\n </p>\n }\n\n <!-- Todos os membros (carregados aos poucos), os novos no topo; busca local, rolagem virtual, sem paginador. -->\n <hm-data-grid\n [columns]=\"columns()\"\n [rows]=\"filteredMembers()\"\n [rowId]=\"rowId\"\n [caption]=\"fmt(text.membersOf, { group: groupName })\"\n [loading]=\"!loader.ready()\"\n [filterable]=\"false\"\n [paginated]=\"false\"\n [pagination]=\"false\"\n [virtual]=\"true\"\n maxHeight=\"24rem\"\n [selectable]=\"true\"\n [selection]=\"checkedForRemovalIds\"\n (selectionChange)=\"onSelectionChange($event)\"\n [emptyMessage]=\"memberSearch().trim() ? text.noMembersMatch : text.noMembersShort\"\n >\n <ng-template hmCell=\"username\" let-m>\n <span class=\"gm-user\">\n <hm-account-avatar [name]=\"m.name || m.username\" [imageUrl]=\"m.picture\" />\n <span class=\"gm-user__body\">\n <span class=\"gm-user__name\">\n <span>{{ m.name || m.username }}</span>\n @if (m.new) { <hm-tag [name]=\"text.newMember\" color=\"info\" /> }\n </span>\n <span class=\"gm-user__meta\">{{ metaOf(m) }}</span>\n </span>\n </span>\n </ng-template>\n\n <ng-template hmCell=\"role\" let-m>\n <hm-combobox class=\"gm-role\" [label]=\"text.colRole\" labelHidden [options]=\"roleCombo()\" [disabled]=\"isMarkedForRemoval(m)\"\n [value]=\"m.role ?? ''\" (valueChange)=\"setRole(m, $event)\" />\n </ng-template>\n\n <ng-template hmCell=\"joined_at\" let-m>\n @if (m.joined_at) {\n {{ formatDate(m.joined_at) }}\n @if (m.added_by_username) {\n <div class=\"gm-joined-by\">{{ fmt(text.addedBy, { user: m.added_by_username }) }}</div>\n }\n } @else {\n \u2014\n }\n </ng-template>\n </hm-data-grid>\n </section>\n\n <!-- Remo\u00E7\u00E3o: sempre ao lado; vazia, s\u00F3 a dica. A remo\u00E7\u00E3o s\u00F3 vale ao salvar. -->\n <aside class=\"gm-removal\" [attr.aria-label]=\"text.removal\">\n <div class=\"gm-removal__head\">\n <hm-icon shape=\"trash\" size=\"sm\" /> {{ text.removal }}\n @if (removalCount) { <hm-badge [value]=\"removalCount\" intent=\"danger\" /> }\n </div>\n @if (removalCount === 0) {\n <div class=\"gm-removal__empty\">\n <hm-icon shape=\"employee-group\" size=\"md\" />\n <p>{{ text.removalEmpty }}</p>\n </div>\n } @else {\n <ul class=\"gm-removal__list\">\n @for (m of checkedForRemoval; track m.user_id) {\n <li class=\"gm-removal__item\">\n <span class=\"gm-user\">\n <hm-account-avatar size=\"small\" [name]=\"m.name || m.username\" [imageUrl]=\"m.picture\" />\n <span class=\"gm-user__body\">\n <span class=\"gm-user__name\"><span>{{ m.name || m.username }}</span></span>\n <span class=\"gm-user__meta\">{{ metaOf(m) }}</span>\n </span>\n </span>\n <hm-icon-button emphasis=\"subtle\" icon=\"undo\" [label]=\"text.undoRemoval\" (clicked)=\"undoRemoval(m)\" />\n </li>\n }\n </ul>\n }\n </aside>\n </div>\n </div>\n\n <ng-container footer>\n <hm-button emphasis=\"medium\" (clicked)=\"close()\">{{ text.cancel }}</hm-button>\n <hm-button [loading]=\"saving\" [disabled]=\"saving\" (clicked)=\"save()\" icon=\"floppy\">{{ text.saveChanges }}</hm-button>\n </ng-container>\n</hm-modal>\n", styles: ["@charset \"UTF-8\";:host{display:contents}.gm{display:flex;flex-direction:column;gap:var(--hm-space-md);min-width:0}.gm-add{display:flex;flex-direction:column;gap:var(--hm-space-sm);padding:var(--hm-space-md);border:1px solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface-raised);min-width:0}.gm-add__row{display:flex;align-items:flex-end;gap:var(--hm-space-sm);flex-wrap:wrap;min-width:0}.gm-field{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0}.gm-field--grow{flex:1 1 18rem}.gm-field--role{flex:0 1 14rem}.gm-label{font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.gm-search{position:relative;display:flex;align-items:center;gap:var(--hm-space-xs);height:var(--hm-control-height);padding:0 var(--hm-space-sm);border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised);color:var(--hm-muted)}.gm-search:focus-within{outline:2px solid var(--hm-focus-color);outline-offset:1px}.gm-search input{flex:1;min-width:0;height:100%;border:0;outline:none;background:transparent;font:inherit;font-size:var(--hm-font-size-sm);color:var(--hm-text)}.gm-search input::placeholder{color:var(--hm-muted)}.gm-search__clear{display:inline-grid;place-items:center;flex:none;padding:0;border:0;background:none;color:inherit;cursor:pointer;line-height:0}.gm-search__results{position:absolute;inset-block-start:calc(100% + 4px);inset-inline:0;z-index:20;max-height:16rem;margin:0;padding:var(--hm-space-xs) 0;overflow-y:auto;list-style:none;background:var(--hm-surface-overlay);border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);box-shadow:var(--hm-shadow-overlay)}.gm-search__result{display:flex;align-items:center;gap:var(--hm-space-sm);width:100%;padding:var(--hm-space-xs) var(--hm-space-sm);border:0;background:none;color:var(--hm-text);font:inherit;font-size:var(--hm-font-size-sm);text-align:start;cursor:pointer}.gm-search__result:hover,.gm-search__result:focus-visible{background:var(--hm-hover)}.gm-search__result:focus-visible{outline:2px solid var(--hm-focus-color);outline-offset:-2px}.gm-suggestions{display:flex;align-items:flex-start;gap:var(--hm-space-sm);min-width:0}.gm-suggestions__title{flex:none;display:inline-flex;align-items:center;min-height:2rem;font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);color:var(--hm-muted)}.gm-suggestions__list{display:flex;flex-wrap:wrap;gap:var(--hm-space-xs);min-width:0;margin:0;padding:0;list-style:none}.gm-chip{display:inline-flex;align-items:center;gap:var(--hm-space-xs);max-width:16rem;min-height:2rem;padding:2px var(--hm-space-sm) 2px 2px;border:1px solid var(--hm-border);border-radius:var(--hm-radius-pill);background:var(--hm-surface-raised);color:var(--hm-text);font:inherit;font-size:var(--hm-font-size-sm);cursor:pointer}.gm-chip hm-account-avatar{--hm-avatar-size: 1.625rem;flex:none}.gm-chip:hover{background:var(--hm-hover);border-color:var(--hm-accent)}.gm-chip:focus-visible{outline:2px solid var(--hm-focus-color);outline-offset:1px}.gm-chip__name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.gm-chip__plus{flex:none;color:var(--hm-accent)}.gm-split{display:grid;grid-template-columns:minmax(0,1fr) minmax(15rem,18rem);gap:var(--hm-space-md);align-items:start;min-width:0}@media(max-width:960px){.gm-split{grid-template-columns:minmax(0,1fr)}}.gm-members{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-width:0}.gm-members__head{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-sm);flex-wrap:wrap}.gm-members__title{display:inline-flex;align-items:center;gap:var(--hm-space-xs);margin:0;font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold)}.gm-members__filter{flex:0 1 16rem;height:2rem}.gm-user{display:flex;align-items:center;gap:var(--hm-space-sm);min-width:0}.gm-user hm-account-avatar{flex:none}.gm-user__body{display:flex;flex-direction:column;min-width:0}.gm-user__name{display:flex;align-items:center;gap:var(--hm-space-xs);min-width:0;font-weight:var(--hm-font-weight-semibold)}.gm-user__name>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.gm-user__meta{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.gm-role{display:block;min-width:11rem}.gm-joined-by{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.gm-status{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs);margin:0;font-size:var(--hm-font-size-sm)}.gm-status--progress{color:var(--hm-muted)}.gm-status__error{color:var(--hm-danger)}.gm-removal{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-width:0;padding:var(--hm-space-md);border:1px solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.gm-removal__head{display:flex;align-items:center;gap:var(--hm-space-xs);font-weight:var(--hm-font-weight-semibold);color:var(--hm-danger)}.gm-removal__empty{display:flex;align-items:flex-start;gap:var(--hm-space-sm);padding:var(--hm-space-sm);border-radius:var(--hm-radius-control);background:var(--hm-surface-muted);color:var(--hm-muted);font-size:var(--hm-font-size-sm);line-height:1.45}.gm-removal__empty hm-icon{flex:none;margin-top:2px}.gm-removal__empty p{margin:0}.gm-removal__list{display:flex;flex-direction:column;gap:var(--hm-space-xs);margin:0;padding:0;list-style:none}.gm-removal__item{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-xs);min-width:0}.gm-removal__item .gm-user{flex:1}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: AccountAvatarComponent, selector: "hm-account-avatar", inputs: ["name", "imageUrl", "size", "loading", "presence", "texts"] }, { kind: "component", type: AlertComponent, selector: "hm-alert", inputs: ["variant", "appearance", "heading", "closable", "size", "showIcon", "link", "linkLabel", "linkTarget", "dismissKey", "duration"], outputs: ["closed"] }, { kind: "component", type: BadgeComponent, selector: "hm-badge", inputs: ["value", "max", "dot", "intent", "label"] }, { kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "component", type: ComboboxComponent, selector: "hm-combobox", inputs: ["label", "options", "value", "placeholder", "icon", "hint", "size", "labelHidden", "clearable", "loading", "allowCustomValue"], outputs: ["valueChange"] }, { kind: "component", type: DataGridComponent, selector: "hm-data-grid", inputs: ["columns", "rows", "rowId", "caption", "rowActions", "loading", "error", "emptyMessage", "noResultsMessage", "filterable", "paginated", "pageState", "pagination", "totalItems", "sort", "selectable", "selectionMode", "selection", "rowSelectable", "rowSelectableReason", "rowDetailDisabled", "rowDetailHidden", "virtual", "rowHeight", "pinnedColumns", "columnState", "pageSizes", "fillHeight", "maxHeight", "bordered", "rowState", "rowClickable", "remoteSearch", "remoteSort", "itemsLabel", "pageSizeLabel", "exportable", "exportFileName", "groupBy", "aggregates", "aggregateFormatter", "persistKey", "persistStorage", "advancedFilter", "filterQuery", "columnManager", "columnOrder", "striped", "columnGroups", "editMode", "editBatch", "texts", "treeChildren", "treeHasChildren", "treeExpanded", "searchTerm"], outputs: ["pageStateChange", "sortChange", "selectionChange", "pinnedColumnsChange", "columnStateChange", "actionTriggered", "detailToggled", "rowClicked", "clearFiltersRequested", "filteredChange", "searchTermChanged", "groupByChange", "filterQueryChange", "columnOrderChange", "editSaved", "editStarted", "editCanceled", "treeExpandedChange", "treeToggled", "searchTermChange", "detailPaneToggled"] }, { kind: "directive", type: DataGridCellDirective, selector: "ng-template[hmCell]", inputs: ["hmCell"] }, { kind: "component", type: IconButtonComponent, selector: "hm-icon-button", inputs: ["label", "icon", "solid", "iconDirection", "emphasis", "intent", "size", "disabled", "toggle", "pressed", "keyShortcuts"], outputs: ["pressedChange", "clicked"] }, { kind: "component", type: IconComponent, selector: "hm-icon", inputs: ["shape", "direction", "size", "solid", "badge", "status", "flip", "inverse", "label"] }, { kind: "component", type: ModalComponent, selector: "hm-modal", inputs: ["open", "heading", "size", "closeLabel", "closable", "beforeClose"], outputs: ["openChange", "closed"] }, { kind: "component", type: TagComponent, selector: "hm-tag", inputs: ["name", "color", "compact"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
412
578
  }
413
579
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceGroupMembersModalComponent, decorators: [{
414
580
  type: Component,
415
581
  args: [{ selector: 'spine-auto-service-group-members-modal', standalone: true, imports: [
416
582
  CommonModule, FormsModule, AccountAvatarComponent, AlertComponent, BadgeComponent, ButtonComponent, ComboboxComponent, DataGridComponent,
417
583
  DataGridCellDirective, IconButtonComponent, IconComponent, ModalComponent, TagComponent,
418
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<hm-modal [open]=\"open\" (openChange)=\"onModalOpenChange($event)\" [heading]=\"fmt(text.membersHeading, { group: groupName })\" size=\"large\" (closed)=\"close()\">\n <div class=\"gm\">\n @if (saveError) {\n <hm-alert variant=\"error\">{{ saveError }}</hm-alert>\n }\n\n <!-- Adicionar: busca (candidatos da API a cada tecla) + fun\u00E7\u00E3o + Adicionar; sugest\u00F5es em chips. -->\n <section class=\"gm-add\" [attr.aria-label]=\"text.addUser\">\n <div class=\"gm-add__row\">\n <div class=\"gm-field gm-field--grow\">\n <label class=\"gm-label\" [attr.for]=\"searchId\">{{ text.addUser }}</label>\n <div class=\"gm-search\">\n <hm-icon shape=\"search\" size=\"sm\" />\n <input type=\"text\" [id]=\"searchId\" [placeholder]=\"text.userSearch\" autocomplete=\"off\"\n [ngModel]=\"addSearch\" (ngModelChange)=\"onAddSearchChange($event)\" />\n @if (addSearch) {\n <button type=\"button\" class=\"gm-search__clear\" [attr.aria-label]=\"clearLabel\" (click)=\"clearSelectedToAdd()\">\n <hm-icon shape=\"times\" size=\"sm\" />\n </button>\n }\n @if (addSearchResults.length) {\n <ul class=\"gm-search__results\">\n @for (u of addSearchResults; track u.id) {\n <li>\n <button type=\"button\" class=\"gm-search__result\" (click)=\"selectCandidate(u)\">\n <span class=\"gm-user\">\n <hm-account-avatar size=\"small\" [name]=\"u.name || u.username\" [imageUrl]=\"u.picture\" />\n <span class=\"gm-user__body\">\n <span class=\"gm-user__name\"><span>{{ u.name || u.username }}</span></span>\n <span class=\"gm-user__meta\">{{ metaOf(u) }}</span>\n </span>\n </span>\n </button>\n </li>\n }\n </ul>\n }\n </div>\n </div>\n\n <hm-combobox class=\"gm-field--role\" [label]=\"text.role\" [options]=\"roleCombo()\" [value]=\"selectedRole ?? ''\" (valueChange)=\"selectedRole = $event || null\" />\n\n <hm-button icon=\"plus\" [disabled]=\"!selectedToAdd\" (clicked)=\"addSelectedMember()\">{{ text.add }}</hm-button>\n </div>\n\n @if (suggestions.length) {\n <div class=\"gm-suggestions\">\n <span class=\"gm-suggestions__title\" [title]=\"text.suggestionsHint\">{{ text.suggestions }}</span>\n <ul class=\"gm-suggestions__list\" [attr.aria-label]=\"text.suggestionsHint\">\n @for (u of suggestions; track u.id) {\n <li>\n <button type=\"button\" class=\"gm-chip\" [title]=\"suggestionTitle(u)\"\n [attr.aria-label]=\"fmt(text.addSuggestion, { name: u.name || u.username })\" (click)=\"addSuggestion(u)\">\n <hm-account-avatar [name]=\"u.name || u.username\" [imageUrl]=\"u.picture\" />\n <span class=\"gm-chip__name\">{{ u.name || u.username }}</span>\n <hm-icon class=\"gm-chip__plus\" shape=\"plus\" size=\"xs\" />\n </button>\n </li>\n }\n </ul>\n </div>\n }\n </section>\n\n <div class=\"gm-split\">\n <section class=\"gm-members\" [attr.aria-labelledby]=\"titleId\">\n <div class=\"gm-members__head\">\n <h3 class=\"gm-members__title\" [id]=\"titleId\">{{ text.membersTitle }} <hm-badge [value]=\"members.length\" intent=\"primary\" [max]=\"999\" /></h3>\n <div class=\"gm-search gm-members__filter\">\n <hm-icon shape=\"search\" size=\"sm\" />\n <input type=\"text\" [placeholder]=\"text.memberSearch\" [attr.aria-label]=\"text.memberSearch\" [ngModel]=\"memberSearch\" (ngModelChange)=\"memberSearch = $event\" />\n </div>\n </div>\n\n <hm-data-grid\n [columns]=\"columns()\"\n [rows]=\"filteredMembers\"\n [rowId]=\"rowId\"\n [caption]=\"fmt(text.membersOf, { group: groupName })\"\n [loading]=\"loading\"\n [selectable]=\"true\"\n [selection]=\"checkedForRemovalIds\"\n [pageSizes]=\"[10, 25, 50]\"\n (selectionChange)=\"onSelectionChange($event)\"\n [emptyMessage]=\"text.noMembersShort\"\n >\n <ng-template hmCell=\"username\" let-m>\n <span class=\"gm-user\">\n <hm-account-avatar [name]=\"m.name || m.username\" [imageUrl]=\"m.picture\" />\n <span class=\"gm-user__body\">\n <span class=\"gm-user__name\">\n <span>{{ m.name || m.username }}</span>\n @if (m.new) { <hm-tag [name]=\"text.newMember\" color=\"info\" /> }\n </span>\n <span class=\"gm-user__meta\">{{ metaOf(m) }}</span>\n </span>\n </span>\n </ng-template>\n\n <ng-template hmCell=\"role\" let-m>\n <hm-combobox class=\"gm-role\" [label]=\"text.colRole\" labelHidden [options]=\"roleCombo()\" [disabled]=\"isMarkedForRemoval(m)\"\n [value]=\"m.role ?? ''\" (valueChange)=\"m.role = $event || null\" />\n </ng-template>\n\n <ng-template hmCell=\"joined_at\" let-m>\n @if (m.joined_at) {\n {{ formatDate(m.joined_at) }}\n @if (m.added_by_username) {\n <div class=\"gm-joined-by\">{{ fmt(text.addedBy, { user: m.added_by_username }) }}</div>\n }\n } @else {\n \u2014\n }\n </ng-template>\n </hm-data-grid>\n </section>\n\n <!-- Remo\u00E7\u00E3o: sempre ao lado; vazia, s\u00F3 a dica. A remo\u00E7\u00E3o s\u00F3 vale ao salvar. -->\n <aside class=\"gm-removal\" [attr.aria-label]=\"text.removal\">\n <div class=\"gm-removal__head\">\n <hm-icon shape=\"trash\" size=\"sm\" /> {{ text.removal }}\n @if (removalCount) { <hm-badge [value]=\"removalCount\" intent=\"danger\" /> }\n </div>\n @if (removalCount === 0) {\n <div class=\"gm-removal__empty\">\n <hm-icon shape=\"employee-group\" size=\"md\" />\n <p>{{ text.removalEmpty }}</p>\n </div>\n } @else {\n <ul class=\"gm-removal__list\">\n @for (m of checkedForRemoval; track m.user_id) {\n <li class=\"gm-removal__item\">\n <span class=\"gm-user\">\n <hm-account-avatar size=\"small\" [name]=\"m.name || m.username\" [imageUrl]=\"m.picture\" />\n <span class=\"gm-user__body\">\n <span class=\"gm-user__name\"><span>{{ m.name || m.username }}</span></span>\n <span class=\"gm-user__meta\">{{ metaOf(m) }}</span>\n </span>\n </span>\n <hm-icon-button emphasis=\"subtle\" icon=\"undo\" [label]=\"text.undoRemoval\" (clicked)=\"undoRemoval(m)\" />\n </li>\n }\n </ul>\n }\n </aside>\n </div>\n </div>\n\n <ng-container footer>\n <hm-button emphasis=\"medium\" (clicked)=\"close()\">{{ text.cancel }}</hm-button>\n <hm-button [loading]=\"saving\" [disabled]=\"saving\" (clicked)=\"save()\" icon=\"floppy\">{{ text.saveChanges }}</hm-button>\n </ng-container>\n</hm-modal>\n", styles: ["@charset \"UTF-8\";:host{display:contents}.gm{display:flex;flex-direction:column;gap:var(--hm-space-md);min-width:0}.gm-add{display:flex;flex-direction:column;gap:var(--hm-space-sm);padding:var(--hm-space-md);border:1px solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface-raised);min-width:0}.gm-add__row{display:flex;align-items:flex-end;gap:var(--hm-space-sm);flex-wrap:wrap;min-width:0}.gm-field{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0}.gm-field--grow{flex:1 1 18rem}.gm-field--role{flex:0 1 14rem}.gm-label{font-size:var(--hm-font-size-sm);font-weight:600}.gm-search{position:relative;display:flex;align-items:center;gap:var(--hm-space-xs);height:var(--hm-control-height);padding:0 var(--hm-space-sm);border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised);color:var(--hm-muted)}.gm-search:focus-within{outline:2px solid var(--hm-focus-color);outline-offset:1px}.gm-search input{flex:1;min-width:0;height:100%;border:0;outline:none;background:transparent;font:inherit;font-size:var(--hm-font-size-sm);color:var(--hm-text)}.gm-search input::placeholder{color:var(--hm-muted)}.gm-search__clear{display:inline-grid;place-items:center;flex:none;padding:0;border:0;background:none;color:inherit;cursor:pointer;line-height:0}.gm-search__results{position:absolute;inset-block-start:calc(100% + 4px);inset-inline:0;z-index:20;max-height:16rem;margin:0;padding:var(--hm-space-xs) 0;overflow-y:auto;list-style:none;background:var(--hm-surface-overlay);border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);box-shadow:var(--hm-shadow-overlay)}.gm-search__result{display:flex;align-items:center;gap:var(--hm-space-sm);width:100%;padding:var(--hm-space-xs) var(--hm-space-sm);border:0;background:none;color:var(--hm-text);font:inherit;text-align:start;cursor:pointer}.gm-search__result:hover,.gm-search__result:focus-visible{background:var(--hm-hover)}.gm-search__result:focus-visible{outline:2px solid var(--hm-focus-color);outline-offset:-2px}.gm-suggestions{display:flex;align-items:flex-start;gap:var(--hm-space-sm);min-width:0}.gm-suggestions__title{flex:none;display:inline-flex;align-items:center;min-height:2rem;font-size:var(--hm-font-size-xs);font-weight:600;color:var(--hm-muted)}.gm-suggestions__list{display:flex;flex-wrap:wrap;gap:var(--hm-space-xs);min-width:0;margin:0;padding:0;list-style:none}.gm-chip{display:inline-flex;align-items:center;gap:var(--hm-space-xs);max-width:16rem;min-height:2rem;padding:2px var(--hm-space-sm) 2px 2px;border:1px solid var(--hm-border);border-radius:var(--hm-radius-pill);background:var(--hm-surface-raised);color:var(--hm-text);font:inherit;font-size:var(--hm-font-size-sm);cursor:pointer}.gm-chip hm-account-avatar{--hm-avatar-size: 1.625rem;flex:none}.gm-chip:hover{background:var(--hm-hover);border-color:var(--hm-accent)}.gm-chip:focus-visible{outline:2px solid var(--hm-focus-color);outline-offset:1px}.gm-chip__name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.gm-chip__plus{flex:none;color:var(--hm-accent)}.gm-split{display:grid;grid-template-columns:minmax(0,1fr) minmax(15rem,18rem);gap:var(--hm-space-md);align-items:start;min-width:0}@media(max-width:960px){.gm-split{grid-template-columns:minmax(0,1fr)}}.gm-members{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-width:0}.gm-members__head{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-sm);flex-wrap:wrap}.gm-members__title{display:inline-flex;align-items:center;gap:var(--hm-space-xs);margin:0;font-size:var(--hm-font-size-md);font-weight:600}.gm-members__filter{flex:0 1 16rem;height:2rem}.gm-user{display:flex;align-items:center;gap:var(--hm-space-sm);min-width:0}.gm-user hm-account-avatar{flex:none}.gm-user__body{display:flex;flex-direction:column;min-width:0}.gm-user__name{display:flex;align-items:center;gap:var(--hm-space-xs);min-width:0;font-weight:600}.gm-user__name>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.gm-user__meta{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.gm-role{display:block;min-width:11rem}.gm-joined-by{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.gm-removal{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-width:0;padding:var(--hm-space-md);border:1px solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.gm-removal__head{display:flex;align-items:center;gap:var(--hm-space-xs);font-weight:600;color:var(--hm-danger)}.gm-removal__empty{display:flex;align-items:flex-start;gap:var(--hm-space-sm);padding:var(--hm-space-sm);border-radius:var(--hm-radius-control);background:var(--hm-surface-muted);color:var(--hm-muted);font-size:var(--hm-font-size-sm);line-height:1.45}.gm-removal__empty hm-icon{flex:none;margin-top:2px}.gm-removal__empty p{margin:0}.gm-removal__list{display:flex;flex-direction:column;gap:var(--hm-space-xs);margin:0;padding:0;list-style:none}.gm-removal__item{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-xs);min-width:0}.gm-removal__item .gm-user{flex:1}\n"] }]
584
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<hm-modal [open]=\"open\" (openChange)=\"onModalOpenChange($event)\" [heading]=\"fmt(text.membersHeading, { group: groupName })\" size=\"large\" (closed)=\"close()\">\n <div class=\"gm\">\n @if (saveError) {\n <hm-alert variant=\"error\">{{ saveError }}</hm-alert>\n }\n\n <!-- Adicionar: busca (candidatos da API a cada tecla) + fun\u00E7\u00E3o + Adicionar; sugest\u00F5es em chips. -->\n <section class=\"gm-add\" [attr.aria-label]=\"text.addUser\">\n <div class=\"gm-add__row\">\n <div class=\"gm-field gm-field--grow\">\n <label class=\"gm-label\" [attr.for]=\"searchId\">{{ text.addUser }}</label>\n <div class=\"gm-search\">\n <hm-icon shape=\"search\" size=\"sm\" />\n <input type=\"text\" [id]=\"searchId\" [placeholder]=\"text.userSearch\" autocomplete=\"off\"\n [ngModel]=\"addSearch\" (ngModelChange)=\"onAddSearchChange($event)\" />\n @if (addSearch) {\n <button type=\"button\" class=\"gm-search__clear\" [attr.aria-label]=\"clearLabel\" (click)=\"clearSelectedToAdd()\">\n <hm-icon shape=\"times\" size=\"sm\" />\n </button>\n }\n @if (addSearchResults.length) {\n <ul class=\"gm-search__results\">\n @for (u of addSearchResults; track u.id) {\n <li>\n <button type=\"button\" class=\"gm-search__result\" (click)=\"selectCandidate(u)\">\n <span class=\"gm-user\">\n <hm-account-avatar size=\"small\" [name]=\"u.name || u.username\" [imageUrl]=\"u.picture\" />\n <span class=\"gm-user__body\">\n <span class=\"gm-user__name\"><span>{{ u.name || u.username }}</span></span>\n <span class=\"gm-user__meta\">{{ metaOf(u) }}</span>\n </span>\n </span>\n </button>\n </li>\n }\n </ul>\n }\n </div>\n </div>\n\n <hm-combobox class=\"gm-field--role\" [label]=\"text.role\" [options]=\"roleCombo()\" [value]=\"selectedRole ?? ''\" (valueChange)=\"selectedRole = $event || null\" />\n\n <hm-button icon=\"plus\" [disabled]=\"!selectedToAdd\" (clicked)=\"addSelectedMember()\">{{ text.add }}</hm-button>\n </div>\n\n @if (suggestions.length) {\n <div class=\"gm-suggestions\">\n <span class=\"gm-suggestions__title\" [title]=\"text.suggestionsHint\">{{ text.suggestions }}</span>\n <ul class=\"gm-suggestions__list\" [attr.aria-label]=\"text.suggestionsHint\">\n @for (u of suggestions; track u.id) {\n <li>\n <button type=\"button\" class=\"gm-chip\" [title]=\"suggestionTitle(u)\"\n [attr.aria-label]=\"fmt(text.addSuggestion, { name: u.name || u.username })\" (click)=\"addSuggestion(u)\">\n <hm-account-avatar [name]=\"u.name || u.username\" [imageUrl]=\"u.picture\" />\n <span class=\"gm-chip__name\">{{ u.name || u.username }}</span>\n <hm-icon class=\"gm-chip__plus\" shape=\"plus\" size=\"xs\" />\n </button>\n </li>\n }\n </ul>\n </div>\n }\n </section>\n\n <div class=\"gm-split\">\n <section class=\"gm-members\" [attr.aria-labelledby]=\"titleId\">\n <div class=\"gm-members__head\">\n <h3 class=\"gm-members__title\" [id]=\"titleId\">{{ text.membersTitle }} <hm-badge [value]=\"memberCount()\" intent=\"primary\" [max]=\"9999\" /></h3>\n <div class=\"gm-search gm-members__filter\">\n <hm-icon shape=\"search\" size=\"sm\" />\n <input type=\"text\" [placeholder]=\"text.memberSearch\" [attr.aria-label]=\"text.memberSearch\" [ngModel]=\"memberSearch()\" (ngModelChange)=\"memberSearch.set($event)\" />\n </div>\n </div>\n\n @if (loader.error()) {\n <div class=\"gm-status\">\n <span class=\"gm-status__error\">{{ incompleteLabel() }}</span>\n <hm-button emphasis=\"medium\" size=\"small\" icon=\"refresh\" (clicked)=\"loader.retry()\">{{ text.retryLoad }}</hm-button>\n </div>\n } @else if (loader.loading() && loader.ready()) {\n <p class=\"gm-status gm-status--progress\" role=\"status\">\n {{ progressLabel() }}@if (memberSearch().trim()) { \u00B7 {{ text.membersPartialResults }} }\n </p>\n }\n\n <!-- Todos os membros (carregados aos poucos), os novos no topo; busca local, rolagem virtual, sem paginador. -->\n <hm-data-grid\n [columns]=\"columns()\"\n [rows]=\"filteredMembers()\"\n [rowId]=\"rowId\"\n [caption]=\"fmt(text.membersOf, { group: groupName })\"\n [loading]=\"!loader.ready()\"\n [filterable]=\"false\"\n [paginated]=\"false\"\n [pagination]=\"false\"\n [virtual]=\"true\"\n maxHeight=\"24rem\"\n [selectable]=\"true\"\n [selection]=\"checkedForRemovalIds\"\n (selectionChange)=\"onSelectionChange($event)\"\n [emptyMessage]=\"memberSearch().trim() ? text.noMembersMatch : text.noMembersShort\"\n >\n <ng-template hmCell=\"username\" let-m>\n <span class=\"gm-user\">\n <hm-account-avatar [name]=\"m.name || m.username\" [imageUrl]=\"m.picture\" />\n <span class=\"gm-user__body\">\n <span class=\"gm-user__name\">\n <span>{{ m.name || m.username }}</span>\n @if (m.new) { <hm-tag [name]=\"text.newMember\" color=\"info\" /> }\n </span>\n <span class=\"gm-user__meta\">{{ metaOf(m) }}</span>\n </span>\n </span>\n </ng-template>\n\n <ng-template hmCell=\"role\" let-m>\n <hm-combobox class=\"gm-role\" [label]=\"text.colRole\" labelHidden [options]=\"roleCombo()\" [disabled]=\"isMarkedForRemoval(m)\"\n [value]=\"m.role ?? ''\" (valueChange)=\"setRole(m, $event)\" />\n </ng-template>\n\n <ng-template hmCell=\"joined_at\" let-m>\n @if (m.joined_at) {\n {{ formatDate(m.joined_at) }}\n @if (m.added_by_username) {\n <div class=\"gm-joined-by\">{{ fmt(text.addedBy, { user: m.added_by_username }) }}</div>\n }\n } @else {\n \u2014\n }\n </ng-template>\n </hm-data-grid>\n </section>\n\n <!-- Remo\u00E7\u00E3o: sempre ao lado; vazia, s\u00F3 a dica. A remo\u00E7\u00E3o s\u00F3 vale ao salvar. -->\n <aside class=\"gm-removal\" [attr.aria-label]=\"text.removal\">\n <div class=\"gm-removal__head\">\n <hm-icon shape=\"trash\" size=\"sm\" /> {{ text.removal }}\n @if (removalCount) { <hm-badge [value]=\"removalCount\" intent=\"danger\" /> }\n </div>\n @if (removalCount === 0) {\n <div class=\"gm-removal__empty\">\n <hm-icon shape=\"employee-group\" size=\"md\" />\n <p>{{ text.removalEmpty }}</p>\n </div>\n } @else {\n <ul class=\"gm-removal__list\">\n @for (m of checkedForRemoval; track m.user_id) {\n <li class=\"gm-removal__item\">\n <span class=\"gm-user\">\n <hm-account-avatar size=\"small\" [name]=\"m.name || m.username\" [imageUrl]=\"m.picture\" />\n <span class=\"gm-user__body\">\n <span class=\"gm-user__name\"><span>{{ m.name || m.username }}</span></span>\n <span class=\"gm-user__meta\">{{ metaOf(m) }}</span>\n </span>\n </span>\n <hm-icon-button emphasis=\"subtle\" icon=\"undo\" [label]=\"text.undoRemoval\" (clicked)=\"undoRemoval(m)\" />\n </li>\n }\n </ul>\n }\n </aside>\n </div>\n </div>\n\n <ng-container footer>\n <hm-button emphasis=\"medium\" (clicked)=\"close()\">{{ text.cancel }}</hm-button>\n <hm-button [loading]=\"saving\" [disabled]=\"saving\" (clicked)=\"save()\" icon=\"floppy\">{{ text.saveChanges }}</hm-button>\n </ng-container>\n</hm-modal>\n", styles: ["@charset \"UTF-8\";:host{display:contents}.gm{display:flex;flex-direction:column;gap:var(--hm-space-md);min-width:0}.gm-add{display:flex;flex-direction:column;gap:var(--hm-space-sm);padding:var(--hm-space-md);border:1px solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface-raised);min-width:0}.gm-add__row{display:flex;align-items:flex-end;gap:var(--hm-space-sm);flex-wrap:wrap;min-width:0}.gm-field{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0}.gm-field--grow{flex:1 1 18rem}.gm-field--role{flex:0 1 14rem}.gm-label{font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.gm-search{position:relative;display:flex;align-items:center;gap:var(--hm-space-xs);height:var(--hm-control-height);padding:0 var(--hm-space-sm);border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface-raised);color:var(--hm-muted)}.gm-search:focus-within{outline:2px solid var(--hm-focus-color);outline-offset:1px}.gm-search input{flex:1;min-width:0;height:100%;border:0;outline:none;background:transparent;font:inherit;font-size:var(--hm-font-size-sm);color:var(--hm-text)}.gm-search input::placeholder{color:var(--hm-muted)}.gm-search__clear{display:inline-grid;place-items:center;flex:none;padding:0;border:0;background:none;color:inherit;cursor:pointer;line-height:0}.gm-search__results{position:absolute;inset-block-start:calc(100% + 4px);inset-inline:0;z-index:20;max-height:16rem;margin:0;padding:var(--hm-space-xs) 0;overflow-y:auto;list-style:none;background:var(--hm-surface-overlay);border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);box-shadow:var(--hm-shadow-overlay)}.gm-search__result{display:flex;align-items:center;gap:var(--hm-space-sm);width:100%;padding:var(--hm-space-xs) var(--hm-space-sm);border:0;background:none;color:var(--hm-text);font:inherit;font-size:var(--hm-font-size-sm);text-align:start;cursor:pointer}.gm-search__result:hover,.gm-search__result:focus-visible{background:var(--hm-hover)}.gm-search__result:focus-visible{outline:2px solid var(--hm-focus-color);outline-offset:-2px}.gm-suggestions{display:flex;align-items:flex-start;gap:var(--hm-space-sm);min-width:0}.gm-suggestions__title{flex:none;display:inline-flex;align-items:center;min-height:2rem;font-size:var(--hm-font-size-xs);font-weight:var(--hm-font-weight-semibold);color:var(--hm-muted)}.gm-suggestions__list{display:flex;flex-wrap:wrap;gap:var(--hm-space-xs);min-width:0;margin:0;padding:0;list-style:none}.gm-chip{display:inline-flex;align-items:center;gap:var(--hm-space-xs);max-width:16rem;min-height:2rem;padding:2px var(--hm-space-sm) 2px 2px;border:1px solid var(--hm-border);border-radius:var(--hm-radius-pill);background:var(--hm-surface-raised);color:var(--hm-text);font:inherit;font-size:var(--hm-font-size-sm);cursor:pointer}.gm-chip hm-account-avatar{--hm-avatar-size: 1.625rem;flex:none}.gm-chip:hover{background:var(--hm-hover);border-color:var(--hm-accent)}.gm-chip:focus-visible{outline:2px solid var(--hm-focus-color);outline-offset:1px}.gm-chip__name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.gm-chip__plus{flex:none;color:var(--hm-accent)}.gm-split{display:grid;grid-template-columns:minmax(0,1fr) minmax(15rem,18rem);gap:var(--hm-space-md);align-items:start;min-width:0}@media(max-width:960px){.gm-split{grid-template-columns:minmax(0,1fr)}}.gm-members{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-width:0}.gm-members__head{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-sm);flex-wrap:wrap}.gm-members__title{display:inline-flex;align-items:center;gap:var(--hm-space-xs);margin:0;font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold)}.gm-members__filter{flex:0 1 16rem;height:2rem}.gm-user{display:flex;align-items:center;gap:var(--hm-space-sm);min-width:0}.gm-user hm-account-avatar{flex:none}.gm-user__body{display:flex;flex-direction:column;min-width:0}.gm-user__name{display:flex;align-items:center;gap:var(--hm-space-xs);min-width:0;font-weight:var(--hm-font-weight-semibold)}.gm-user__name>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.gm-user__meta{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.gm-role{display:block;min-width:11rem}.gm-joined-by{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.gm-status{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs);margin:0;font-size:var(--hm-font-size-sm)}.gm-status--progress{color:var(--hm-muted)}.gm-status__error{color:var(--hm-danger)}.gm-removal{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-width:0;padding:var(--hm-space-md);border:1px solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.gm-removal__head{display:flex;align-items:center;gap:var(--hm-space-xs);font-weight:var(--hm-font-weight-semibold);color:var(--hm-danger)}.gm-removal__empty{display:flex;align-items:flex-start;gap:var(--hm-space-sm);padding:var(--hm-space-sm);border-radius:var(--hm-radius-control);background:var(--hm-surface-muted);color:var(--hm-muted);font-size:var(--hm-font-size-sm);line-height:1.45}.gm-removal__empty hm-icon{flex:none;margin-top:2px}.gm-removal__empty p{margin:0}.gm-removal__list{display:flex;flex-direction:column;gap:var(--hm-space-xs);margin:0;padding:0;list-style:none}.gm-removal__item{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-xs);min-width:0}.gm-removal__item .gm-user{flex:1}\n"] }]
419
585
  }], ctorParameters: () => [{ type: AutoServiceService }], propDecorators: { clientId: [{
420
586
  type: Input
421
587
  }], groupId: [{
@@ -632,4 +798,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
632
798
  }] });
633
799
 
634
800
  export { AutoServiceGroupsComponent, NO_LEVEL };
635
- //# sourceMappingURL=trismegisto-spine-auto-service-auto-service-groups.component-BYjIQ9ID.mjs.map
801
+ //# sourceMappingURL=trismegisto-spine-auto-service-auto-service-groups.component-DE2QIGRH.mjs.map