@sinequa/atomic-angular 1.19.3 → 1.20.0
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.
- package/features/user-profile/i18n/de.json +5 -0
- package/features/user-profile/i18n/en.json +5 -0
- package/features/user-profile/i18n/fr.json +5 -0
- package/fesm2022/sinequa-atomic-angular.mjs +381 -70
- package/fesm2022/sinequa-atomic-angular.mjs.map +1 -1
- package/index.d.ts +337 -227
- package/package.json +2 -2
|
@@ -2,12 +2,12 @@ import * as i0 from '@angular/core';
|
|
|
2
2
|
import { Injectable, inject, HostBinding, Component, Pipe, InjectionToken, computed, ChangeDetectorRef, DestroyRef, LOCALE_ID, Inject, Optional, input, output, signal, effect, assertInInjectionContext, runInInjectionContext, EnvironmentInjector, Injector, EventEmitter, Directive, viewChild, ElementRef, afterNextRender, untracked, linkedSignal, model, TemplateRef, HostListener, Renderer2, contentChildren, contentChild, booleanAttribute, resource, ChangeDetectionStrategy, ViewContainerRef, numberAttribute, viewChildren, afterRenderEffect, afterEveryRender } from '@angular/core';
|
|
3
3
|
import { BehaviorSubject, Subscription, Subject, of, catchError, map, tap, EMPTY, throwError, from, filter as filter$1, shareReplay, switchMap, firstValueFrom, fromEvent, debounceTime } from 'rxjs';
|
|
4
4
|
import { TranslocoService, TranslocoPipe, provideTranslocoScope } from '@jsverse/transloco';
|
|
5
|
-
import { DropdownComponent, DropdownContentComponent, InputComponent, ButtonComponent, cn, FaIconComponent, EllipsisIcon, ChevronRightIcon, MenuComponent, MenuContentComponent, MenuItemComponent, LinkComponent, CardComponent, CardHeaderComponent, CardContentComponent, BadgeComponent, DialogComponent, DialogHeaderComponent, DialogTitleComponent, DialogContentComponent, DialogFooterComponent, ListItemComponent, SquareCheckIcon, SquareMinusIcon, SquareIcon, SwitchComponent, SelectOptionDirective, DialogService, XMarkIcon, InboxIcon, SparklesIcon, FileOutputIcon, TabsComponent, TabsListComponent, TabComponent, SidebarMenuComponent, SidebarMenuItemComponent, SidebarMenuButtonComponent, TooltipDirective, FrownIcon, ChevronLeftIcon, ChevronsLeftIcon, ChevronsRightIcon, ArrowUpAzIcon, ArrowDownZaIcon, ArrowUpRightFromSquareIcon, ToggleRightIcon, ToggleLeftIcon, Separator, SheetCloseDirective, ArrowLeftIcon, SheetService, DateRangePickerDirective, DatepickerDirective, FilterIcon, FilterXIcon, IconButtonComponent, HighlighterIcon, TagsIcon, SpinnerIcon, MagnifyingGlassIcon, LoadingCircleIcon, CircleCheckIcon, PopoverComponent, BellIcon, TrashIcon, BarsIcon, CardFooterComponent, InputGroupInput, InputGroupComponent, InputGroupAddonComponent, EyeSlashIcon, EyeIcon, BookmarkIcon, PopoverContentComponent, UserIcon, FolderIcon, DialogDescriptionComponent, VerticalDividerComponent, CommentIcon, ThumbsUpIcon, ThumbsDownIcon, DropdownDirective, SearchIcon, TriangleAlertIcon, BreakpointObserverService, ListFilterIcon, SheetComponent, SheetHeaderComponent, SheetTitleComponent, TrashCanIcon, CircleXIcon, InfoCircleIcon, HorizontalDividerComponent, HistoryIcon, StarIcon, FlagEnglishIcon, FlagFrenchIcon, FlagGermanIcon, EditIcon, UndoIcon, SaveIcon, AvatarComponent, AvatarFallbackComponent, AvatarImageComponent } from '@sinequa/ui';
|
|
5
|
+
import { DropdownComponent, DropdownContentComponent, InputComponent, ButtonComponent, cn, FaIconComponent, EllipsisIcon, ChevronRightIcon, MenuComponent, MenuContentComponent, MenuItemComponent, LinkComponent, CardComponent, CardHeaderComponent, CardContentComponent, BadgeComponent, DialogComponent, DialogHeaderComponent, DialogTitleComponent, DialogContentComponent, DialogFooterComponent, ListItemComponent, SquareCheckIcon, SquareMinusIcon, SquareIcon, SwitchComponent, SelectOptionDirective, DialogService, XMarkIcon, InboxIcon, SparklesIcon, FileOutputIcon, TabsComponent, TabsListComponent, TabComponent, SidebarMenuComponent, SidebarMenuItemComponent, SidebarMenuButtonComponent, TooltipDirective, FrownIcon, ChevronLeftIcon, ChevronsLeftIcon, ChevronsRightIcon, ArrowUpAzIcon, ArrowDownZaIcon, ArrowUpRightFromSquareIcon, ToggleRightIcon, ToggleLeftIcon, Separator, SheetCloseDirective, ArrowLeftIcon, SheetService, DateRangePickerDirective, DatepickerDirective, FilterIcon, FilterXIcon, IconButtonComponent, HighlighterIcon, TagsIcon, SpinnerIcon, MagnifyingGlassIcon, LoadingCircleIcon, CircleCheckIcon, PopoverComponent, BellIcon, TrashIcon, BarsIcon, CardFooterComponent, InputGroupInput, InputGroupComponent, InputGroupAddonComponent, EyeSlashIcon, EyeIcon, BookmarkIcon, PopoverContentComponent, UserIcon, FolderIcon, DialogDescriptionComponent, VerticalDividerComponent, CommentIcon, ThumbsUpIcon, ThumbsDownIcon, DropdownDirective, SearchIcon, TriangleAlertIcon, BreakpointObserverService, ListFilterIcon, SheetComponent, SheetHeaderComponent, SheetTitleComponent, TrashCanIcon, CircleXIcon, InfoCircleIcon, HorizontalDividerComponent, HistoryIcon, StarIcon, FlagEnglishIcon, FlagFrenchIcon, FlagGermanIcon, EditIcon, UndoIcon, SaveIcon, AvatarComponent, AvatarFallbackComponent, GlobeIcon, UserSecretIcon, AvatarImageComponent } from '@sinequa/ui';
|
|
6
6
|
import highlightWords from 'highlight-words';
|
|
7
7
|
import { ActivatedRoute, Router, NavigationEnd, RouterLink, RouterLinkActive, RouterModule } from '@angular/router';
|
|
8
8
|
import { withDevtools } from '@angular-architects/ngrx-toolkit';
|
|
9
9
|
import { signalStore, signalStoreFeature, withState, withMethods, patchState, getState, withComputed } from '@ngrx/signals';
|
|
10
|
-
import { EngineType, fetchApp, extraColumns, sysLang, fetchPrincipal, globalConfig, filter, getQueryParamsFromUrl, isRedirectPending, info, clearSessionTokens, login, error, setGlobalConfig, initializeAppConfig, notify, addConcepts, queryParamsFromUrl, patchUserSettings, deleteUserSettings, fetchUserSettings, warn, buildPathsAndLevels, escapeExpr, fetchQuery, flattenLegacyFilterValues, isAuthenticated, isExpired, debug, AuditEventType, fetchSuggest, Audit, isObject, getMetadata, bisect, isNotInputEvent, fetchSponsoredLinks, translateAggregationToDateOptions, aggItemRegex, parseValueAndOperatorFromItem, fetchSuggestField, fetchSimilarDocuments, logout, fetchChangePassword, fetchSendPasswordResetEmail, expiresSoon, suggestionsToTreeAggregationNodes, labels, fetchLabels, guid, getRelativeDate, createUserProfile, deleteUserProfileProperty, patchUserProfile, isJsonable, addAuditAdditionalInfo, getToken, setToken, createHeaders } from '@sinequa/atomic';
|
|
10
|
+
import { EngineType, fetchApp, extraColumns, sysLang, fetchPrincipal, globalConfig, filter, getQueryParamsFromUrl, isRedirectPending, info, clearSessionTokens, login, error, setGlobalConfig, initializeAppConfig, notify, addConcepts, queryParamsFromUrl, patchUserSettings, deleteUserSettings, fetchUserSettings, warn, buildPathsAndLevels, escapeExpr, fetchQuery, flattenLegacyFilterValues, isAuthenticated, isExpired, debug, AuditEventType, fetchSuggest, Audit, isObject, getMetadata, bisect, isNotInputEvent, fetchSponsoredLinks, translateAggregationToDateOptions, aggItemRegex, parseValueAndOperatorFromItem, fetchSuggestField, fetchSimilarDocuments, logout, fetchChangePassword, fetchSendPasswordResetEmail, expiresSoon, suggestionsToTreeAggregationNodes, labels, fetchLabels, guid, getRelativeDate, createUserProfile, deleteUserProfilePrivateCustomData, deleteUserProfileProperty, patchUserProfilePrivateCustomData, patchUserProfile, isJsonable, addAuditAdditionalInfo, getToken, setToken, createHeaders } from '@sinequa/atomic';
|
|
11
11
|
import { takeUntilDestroyed, toObservable, toSignal } from '@angular/core/rxjs-interop';
|
|
12
12
|
import { DatePipe, DATE_PIPE_DEFAULT_TIMEZONE, DATE_PIPE_DEFAULT_OPTIONS, Location, NgTemplateOutlet, NgStyle, NgClass, NgComponentOutlet } from '@angular/common';
|
|
13
13
|
import { HttpClient, HttpParams, httpResource, HttpResponse, HttpContextToken, HttpHeaders } from '@angular/common/http';
|
|
@@ -3262,7 +3262,12 @@ class QueryService {
|
|
|
3262
3262
|
};
|
|
3263
3263
|
return r;
|
|
3264
3264
|
}), tap(response => {
|
|
3265
|
-
|
|
3265
|
+
// An "open" query (expanding a tree aggregation node) only fetches the node's
|
|
3266
|
+
// children: its response is not the page's search result (no tabs, no records...).
|
|
3267
|
+
// Publishing it would wipe the state derived from `result` (e.g. the tab counts).
|
|
3268
|
+
if (query.action !== "open") {
|
|
3269
|
+
this.result.set(response);
|
|
3270
|
+
}
|
|
3266
3271
|
info("queryService.getResults success - data: ", response);
|
|
3267
3272
|
}));
|
|
3268
3273
|
}
|
|
@@ -6114,25 +6119,41 @@ class MissingTermsComponent {
|
|
|
6114
6119
|
missingTerms = computed(() => {
|
|
6115
6120
|
const query = this.queryParamsStore.getQuery();
|
|
6116
6121
|
return this.article()
|
|
6117
|
-
.termspresence?.filter(tp => tp.presence ===
|
|
6122
|
+
.termspresence?.filter(tp => tp.presence === "missing")
|
|
6118
6123
|
.map(tp => {
|
|
6119
|
-
const
|
|
6120
|
-
|
|
6121
|
-
const escapedTerm = cleanTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
6122
|
-
const text = (query.text || '').replace(new RegExp(`\\b${escapedTerm}\\b`, 'gi'), '').replace(/\s+/g, ' ').trim();
|
|
6123
|
-
return {
|
|
6124
|
-
value: cleanTerm,
|
|
6125
|
-
queryParams: { q: `${text} +[${cleanTerm}]` }
|
|
6126
|
-
};
|
|
6124
|
+
const { value, q } = mustIncludeQuery(query.text || "", tp.term);
|
|
6125
|
+
return { value, queryParams: { q } };
|
|
6127
6126
|
});
|
|
6128
6127
|
}, ...(ngDevMode ? [{ debugName: "missingTerms" }] : []));
|
|
6129
6128
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: MissingTermsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6130
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.29", type: MissingTermsComponent, isStandalone: true, selector: "missing-terms, MissingTerms, missingterms", inputs: { article: { classPropertyName: "article", publicName: "article", isSignal: true, isRequired: true, transformFunction: null } }, providers: [provideTranslocoScope(
|
|
6129
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.29", type: MissingTermsComponent, isStandalone: true, selector: "missing-terms, MissingTerms, missingterms", inputs: { article: { classPropertyName: "article", publicName: "article", isSignal: true, isRequired: true, transformFunction: null } }, providers: [provideTranslocoScope("article")], ngImport: i0, template: "@if ((missingTerms() || []).length > 0) {\n <span class=\"terms-label\">{{ 'article.missingTerms' | transloco }}</span>\n <span class=\"terms\">\n @for (term of missingTerms(); track $index) {\n <span class=\"term ms-1\">{{ term.value }}</span>\n }\n </span>\n <span class=\"mx-1\">|</span>\n <span class=\"terms-label ms-1\">{{ 'article.mustInclude' | transloco }}</span>\n <span class=\"terms\">\n @for (term of missingTerms(); track $index) {\n <a role=\"button\" class=\"ms-1\" routerLink=\"search\" [queryParams]=\"term.queryParams\" (click)=\"$event.stopPropagation()\">{{ term.value }}</a>\n }\n </span>\n}\n", styles: [".term{text-decoration-line:line-through}\n"], dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
|
|
6131
6130
|
}
|
|
6132
6131
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: MissingTermsComponent, decorators: [{
|
|
6133
6132
|
type: Component,
|
|
6134
|
-
args: [{ selector:
|
|
6133
|
+
args: [{ selector: "missing-terms, MissingTerms, missingterms", standalone: true, imports: [TranslocoPipe, RouterLink], providers: [provideTranslocoScope("article")], template: "@if ((missingTerms() || []).length > 0) {\n <span class=\"terms-label\">{{ 'article.missingTerms' | transloco }}</span>\n <span class=\"terms\">\n @for (term of missingTerms(); track $index) {\n <span class=\"term ms-1\">{{ term.value }}</span>\n }\n </span>\n <span class=\"mx-1\">|</span>\n <span class=\"terms-label ms-1\">{{ 'article.mustInclude' | transloco }}</span>\n <span class=\"terms\">\n @for (term of missingTerms(); track $index) {\n <a role=\"button\" class=\"ms-1\" routerLink=\"search\" [queryParams]=\"term.queryParams\" (click)=\"$event.stopPropagation()\">{{ term.value }}</a>\n }\n </span>\n}\n", styles: [".term{text-decoration-line:line-through}\n"] }]
|
|
6135
6134
|
}], propDecorators: { article: [{ type: i0.Input, args: [{ isSignal: true, alias: "article", required: true }] }] } });
|
|
6135
|
+
/**
|
|
6136
|
+
* The backend returns each term with the syntax it has in the query: `[exit code]`, `"exit code"`, `(exit code)` or `/exit code/`.
|
|
6137
|
+
* Removes that syntax (and any +/- operator) to get the bare value, which is what goes inside `+[...]`.
|
|
6138
|
+
*/
|
|
6139
|
+
const DELIMITED_TERM = /^[+-]?(?:\[(.+)\]|"(.+)"|\((.+)\)|\/(.+)\/)$/;
|
|
6140
|
+
/**
|
|
6141
|
+
* Builds the query text which makes `term` mandatory: the term is removed from the current text, then added as a `+[term]` concept.
|
|
6142
|
+
*/
|
|
6143
|
+
function mustIncludeQuery(queryText, term) {
|
|
6144
|
+
const delimited = term.match(DELIMITED_TERM);
|
|
6145
|
+
const value = delimited ? (delimited[1] ?? delimited[2] ?? delimited[3] ?? delimited[4]) : term;
|
|
6146
|
+
// Escape special regex characters to avoid regex injection (e.g. brackets becoming character classes)
|
|
6147
|
+
const escapedTerm = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6148
|
+
// 1. The term is a whole pattern of the query (e.g. "exit code" or +[exit code]): remove it with its delimiters.
|
|
6149
|
+
// A word boundary cannot be used here, as the delimiters are not word characters.
|
|
6150
|
+
const wholePattern = new RegExp(`(^|\\s)[+-]?(?:\\[${escapedTerm}\\]|"${escapedTerm}"|\\(${escapedTerm}\\)|\\/${escapedTerm}\\/)(?=\\s|$)`, "gi");
|
|
6151
|
+
const withoutPattern = queryText.replace(wholePattern, "$1");
|
|
6152
|
+
// 2. Otherwise, the term is made of plain words of the query: remove them.
|
|
6153
|
+
const remaining = withoutPattern !== queryText ? withoutPattern : queryText.replace(new RegExp(`\\b${escapedTerm}\\b`, "gi"), "");
|
|
6154
|
+
const text = remaining.replace(/\s+/g, " ").trim();
|
|
6155
|
+
return { value, q: `${text} +[${value}]` };
|
|
6156
|
+
}
|
|
6136
6157
|
|
|
6137
6158
|
class CollectionsDialog {
|
|
6138
6159
|
closed = output();
|
|
@@ -18255,17 +18276,71 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
|
|
|
18255
18276
|
|
|
18256
18277
|
/** Whether a value is an object (an array excluded): a structure, which this form cannot edit. */
|
|
18257
18278
|
const isPlainObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
18279
|
+
/** Segments of a `/`-separated property path. */
|
|
18280
|
+
const toPath = (key) => key.split("/");
|
|
18281
|
+
/** The value stored at a `/`-separated path of `source`, or `undefined` when a node on the way is missing or not an object. */
|
|
18282
|
+
function getAtPath(source, key) {
|
|
18283
|
+
let node = source;
|
|
18284
|
+
for (const segment of toPath(key)) {
|
|
18285
|
+
if (!isPlainObject(node)) {
|
|
18286
|
+
return undefined;
|
|
18287
|
+
}
|
|
18288
|
+
node = node[segment];
|
|
18289
|
+
}
|
|
18290
|
+
return node;
|
|
18291
|
+
}
|
|
18292
|
+
/**
|
|
18293
|
+
* A copy of `source` holding `value` at a `/`-separated path (`undefined` removes it), siblings untouched.
|
|
18294
|
+
* The nodes on the way are created when missing, or replaced when they are not objects.
|
|
18295
|
+
*/
|
|
18296
|
+
function setAtPath(source, key, value) {
|
|
18297
|
+
const [head, ...rest] = toPath(key);
|
|
18298
|
+
const copy = { ...(source ?? {}) };
|
|
18299
|
+
if (rest.length) {
|
|
18300
|
+
const child = copy[head];
|
|
18301
|
+
copy[head] = setAtPath(isPlainObject(child) ? child : undefined, rest.join("/"), value);
|
|
18302
|
+
}
|
|
18303
|
+
else {
|
|
18304
|
+
copy[head] = value;
|
|
18305
|
+
}
|
|
18306
|
+
return copy;
|
|
18307
|
+
}
|
|
18308
|
+
/**
|
|
18309
|
+
* Where to write `value` for the property at `key` so that the server accepts it.
|
|
18310
|
+
*
|
|
18311
|
+
* The server does not create the nodes on the way: a PATCH on `obj/deep/leaf` answers 404 while `obj` or
|
|
18312
|
+
* `obj/deep` does not exist in the profile. The write is therefore moved up to the first missing node (an
|
|
18313
|
+
* absent one, or one that is not an object), and carries the rest of the path as nested objects: with an empty
|
|
18314
|
+
* `source`, `obj/deep/leaf = "x"` is written as `obj = { deep: { leaf: "x" } }`. When every node on the way
|
|
18315
|
+
* exists, the target is `key` itself.
|
|
18316
|
+
*/
|
|
18317
|
+
function writeTarget(source, key, value) {
|
|
18318
|
+
const path = toPath(key);
|
|
18319
|
+
let node = source;
|
|
18320
|
+
for (let depth = 0; depth < path.length - 1; depth++) {
|
|
18321
|
+
const child = isPlainObject(node) ? node[path[depth]] : undefined;
|
|
18322
|
+
if (!isPlainObject(child)) {
|
|
18323
|
+
const nested = path.slice(depth + 1).reduceRight((inner, segment) => ({ [segment]: inner }), value);
|
|
18324
|
+
return { key: path.slice(0, depth + 1).join("/"), value: nested };
|
|
18325
|
+
}
|
|
18326
|
+
node = child;
|
|
18327
|
+
}
|
|
18328
|
+
return { key, value };
|
|
18329
|
+
}
|
|
18258
18330
|
/**
|
|
18259
|
-
* Normalizes `features.userProfile.customData`, as written in
|
|
18331
|
+
* Normalizes `features.userProfile.customData` / `features.userProfile.privateCustomData`, as written in
|
|
18332
|
+
* the custom JSON, into a flat list of `/`-separated paths.
|
|
18260
18333
|
*
|
|
18261
|
-
*
|
|
18262
|
-
* - `"nickname"` — a property without default;
|
|
18334
|
+
* Entry shapes accepted, which can be mixed:
|
|
18335
|
+
* - `"nickname"` — a property without default; `"agent/favorites"` is a nested one;
|
|
18263
18336
|
* - `{ "nickname": "none" }` — a property and its default value. An object with several keys declares
|
|
18264
|
-
* one property per key
|
|
18337
|
+
* one property per key;
|
|
18338
|
+
* - `{ "obj": { "property1": "val2" } }` — a hierarchy: the property `obj/property1`, default `val2`, at
|
|
18339
|
+
* any depth.
|
|
18265
18340
|
*
|
|
18266
18341
|
* The JSON is read at run time and nothing validates it, so an entry that is neither a string nor a
|
|
18267
|
-
* plain object is dropped, with a warning, rather than thrown on. When
|
|
18268
|
-
* first declaration wins.
|
|
18342
|
+
* plain object, or a path with an empty segment, is dropped, with a warning, rather than thrown on. When
|
|
18343
|
+
* a path is declared twice, the first declaration wins.
|
|
18269
18344
|
*/
|
|
18270
18345
|
function normalizeCustomData(entries) {
|
|
18271
18346
|
const properties = new Map();
|
|
@@ -18273,14 +18348,26 @@ function normalizeCustomData(entries) {
|
|
|
18273
18348
|
if (!key || properties.has(key)) {
|
|
18274
18349
|
return;
|
|
18275
18350
|
}
|
|
18351
|
+
if (toPath(key).some(segment => !segment)) {
|
|
18352
|
+
console.warn("[user-profile] customData: path ignored, it has an empty segment.", key);
|
|
18353
|
+
return;
|
|
18354
|
+
}
|
|
18276
18355
|
properties.set(key, value === undefined || value === null ? { key } : { key, defaultValue: value });
|
|
18277
18356
|
};
|
|
18357
|
+
const addTree = (prefix, value) => {
|
|
18358
|
+
if (isPlainObject(value) && Object.keys(value).length) {
|
|
18359
|
+
Object.entries(value).forEach(([key, child]) => addTree(prefix ? `${prefix}/${key}` : key, child));
|
|
18360
|
+
}
|
|
18361
|
+
else {
|
|
18362
|
+
add(prefix, isPlainObject(value) ? undefined : value);
|
|
18363
|
+
}
|
|
18364
|
+
};
|
|
18278
18365
|
for (const entry of Array.isArray(entries) ? entries : []) {
|
|
18279
18366
|
if (typeof entry === "string") {
|
|
18280
18367
|
add(entry);
|
|
18281
18368
|
}
|
|
18282
18369
|
else if (isPlainObject(entry)) {
|
|
18283
|
-
|
|
18370
|
+
addTree("", entry);
|
|
18284
18371
|
}
|
|
18285
18372
|
else {
|
|
18286
18373
|
console.warn("[user-profile] customData: entry ignored, expected a string or an object.", entry);
|
|
@@ -18289,6 +18376,101 @@ function normalizeCustomData(entries) {
|
|
|
18289
18376
|
return [...properties.values()];
|
|
18290
18377
|
}
|
|
18291
18378
|
|
|
18379
|
+
/** Whether a value is a structure (an array or an object) rather than a scalar. */
|
|
18380
|
+
const isStructured = (value) => Array.isArray(value) || isPlainObject(value);
|
|
18381
|
+
/**
|
|
18382
|
+
* Read-only view of any JSON value, to show a profile property whose content is not known in advance.
|
|
18383
|
+
*
|
|
18384
|
+
* - a scalar is shown as text;
|
|
18385
|
+
* - an array of scalars is a row of chips;
|
|
18386
|
+
* - any other array, and an object, is a nested list: each item (or key) is shown with this same
|
|
18387
|
+
* component, so values, objects and arrays can be mixed at any depth.
|
|
18388
|
+
*
|
|
18389
|
+
* It renders nothing interactive, so the profile form keeps the edition to itself.
|
|
18390
|
+
*/
|
|
18391
|
+
class JsonValueView {
|
|
18392
|
+
value = input(...(ngDevMode ? [undefined, { debugName: "value" }] : []));
|
|
18393
|
+
isArray = computed(() => Array.isArray(this.value()), ...(ngDevMode ? [{ debugName: "isArray" }] : []));
|
|
18394
|
+
/** The items of a structure (a label only for an object key), or `undefined` for a scalar. */
|
|
18395
|
+
entries = computed(() => {
|
|
18396
|
+
const value = this.value();
|
|
18397
|
+
if (Array.isArray(value)) {
|
|
18398
|
+
return value.map(item => ({ label: undefined, value: item }));
|
|
18399
|
+
}
|
|
18400
|
+
if (isPlainObject(value)) {
|
|
18401
|
+
return Object.entries(value).map(([label, item]) => ({ label: label, value: item }));
|
|
18402
|
+
}
|
|
18403
|
+
return undefined;
|
|
18404
|
+
}, ...(ngDevMode ? [{ debugName: "entries" }] : []));
|
|
18405
|
+
scalarArray = computed(() => this.isArray() && (this.entries() ?? []).length > 0 && (this.entries() ?? []).every(item => !isStructured(item.value)), ...(ngDevMode ? [{ debugName: "scalarArray" }] : []));
|
|
18406
|
+
text(value) {
|
|
18407
|
+
return value === null || value === undefined ? "∅" : String(value);
|
|
18408
|
+
}
|
|
18409
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: JsonValueView, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
18410
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.29", type: JsonValueView, isStandalone: true, selector: "json-value-view", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "block min-w-0" }, ngImport: i0, template: `
|
|
18411
|
+
@if (entries(); as list) {
|
|
18412
|
+
@if (scalarArray()) {
|
|
18413
|
+
<ul class="flex flex-wrap gap-1">
|
|
18414
|
+
@for (item of list; track $index) {
|
|
18415
|
+
<li class="bg-muted rounded-full px-2 py-0.5 text-sm break-words">{{ text(item.value) }}</li>
|
|
18416
|
+
}
|
|
18417
|
+
</ul>
|
|
18418
|
+
} @else if (list.length) {
|
|
18419
|
+
<ul class="border-foreground/10 flex flex-col gap-1 border-l-2 pl-2">
|
|
18420
|
+
@for (item of list; track $index) {
|
|
18421
|
+
<li class="flex min-w-0 flex-col">
|
|
18422
|
+
@if (item.label !== undefined) {
|
|
18423
|
+
<span class="text-muted-foreground text-xs">{{ item.label }}</span>
|
|
18424
|
+
}
|
|
18425
|
+
<json-value-view [value]="item.value" />
|
|
18426
|
+
</li>
|
|
18427
|
+
}
|
|
18428
|
+
</ul>
|
|
18429
|
+
} @else {
|
|
18430
|
+
<span class="text-muted-foreground">{{ isArray() ? "[ ]" : "{ }" }}</span>
|
|
18431
|
+
}
|
|
18432
|
+
} @else {
|
|
18433
|
+
<span class="break-words whitespace-pre-line">{{ text(value()) }}</span>
|
|
18434
|
+
}
|
|
18435
|
+
`, isInline: true, dependencies: [{ kind: "component", type: JsonValueView, selector: "json-value-view", inputs: ["value"] }] });
|
|
18436
|
+
}
|
|
18437
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: JsonValueView, decorators: [{
|
|
18438
|
+
type: Component,
|
|
18439
|
+
args: [{
|
|
18440
|
+
selector: "json-value-view",
|
|
18441
|
+
standalone: true,
|
|
18442
|
+
host: { class: "block min-w-0" },
|
|
18443
|
+
template: `
|
|
18444
|
+
@if (entries(); as list) {
|
|
18445
|
+
@if (scalarArray()) {
|
|
18446
|
+
<ul class="flex flex-wrap gap-1">
|
|
18447
|
+
@for (item of list; track $index) {
|
|
18448
|
+
<li class="bg-muted rounded-full px-2 py-0.5 text-sm break-words">{{ text(item.value) }}</li>
|
|
18449
|
+
}
|
|
18450
|
+
</ul>
|
|
18451
|
+
} @else if (list.length) {
|
|
18452
|
+
<ul class="border-foreground/10 flex flex-col gap-1 border-l-2 pl-2">
|
|
18453
|
+
@for (item of list; track $index) {
|
|
18454
|
+
<li class="flex min-w-0 flex-col">
|
|
18455
|
+
@if (item.label !== undefined) {
|
|
18456
|
+
<span class="text-muted-foreground text-xs">{{ item.label }}</span>
|
|
18457
|
+
}
|
|
18458
|
+
<json-value-view [value]="item.value" />
|
|
18459
|
+
</li>
|
|
18460
|
+
}
|
|
18461
|
+
</ul>
|
|
18462
|
+
} @else {
|
|
18463
|
+
<span class="text-muted-foreground">{{ isArray() ? "[ ]" : "{ }" }}</span>
|
|
18464
|
+
}
|
|
18465
|
+
} @else {
|
|
18466
|
+
<span class="break-words whitespace-pre-line">{{ text(value()) }}</span>
|
|
18467
|
+
}
|
|
18468
|
+
`
|
|
18469
|
+
}]
|
|
18470
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }] } });
|
|
18471
|
+
|
|
18472
|
+
/** Marks a JSON editor content that does not parse. */
|
|
18473
|
+
const INVALID = Symbol("invalid-json");
|
|
18292
18474
|
const SUPPORTED_LANGUAGES = ["en", "fr", "de"];
|
|
18293
18475
|
class UserProfileFormComponent {
|
|
18294
18476
|
principalStore = inject(PrincipalStore);
|
|
@@ -18320,16 +18502,36 @@ class UserProfileFormComponent {
|
|
|
18320
18502
|
];
|
|
18321
18503
|
changingPassword = signal(false, ...(ngDevMode ? [{ debugName: "changingPassword" }] : []));
|
|
18322
18504
|
customDataConfig = computed(() => normalizeCustomData(this.appStore.general()?.features?.userProfile?.customData), ...(ngDevMode ? [{ debugName: "customDataConfig" }] : []));
|
|
18505
|
+
privateCustomDataConfig = computed(() => normalizeCustomData(this.appStore.general()?.features?.userProfile?.privateCustomData), ...(ngDevMode ? [{ debugName: "privateCustomDataConfig" }] : []));
|
|
18323
18506
|
/**
|
|
18324
|
-
* The
|
|
18325
|
-
*
|
|
18507
|
+
* The custom properties of the form. A property may hold any JSON value, a structure included: it is
|
|
18508
|
+
* shown structured and edited as JSON (see {@link editingJson}).
|
|
18326
18509
|
*/
|
|
18327
|
-
customData = computed(() =>
|
|
18328
|
-
|
|
18329
|
-
|
|
18330
|
-
|
|
18331
|
-
|
|
18332
|
-
|
|
18510
|
+
customData = computed(() => this.customDataConfig()
|
|
18511
|
+
.map(({ key }) => key)
|
|
18512
|
+
.filter(key => !this.keys().some(k => k.toLowerCase() === key.toLowerCase())), ...(ngDevMode ? [{ debugName: "customData" }] : []));
|
|
18513
|
+
/** The private properties of the form; they only ever go through the private endpoints. */
|
|
18514
|
+
privateCustomData = computed(() => this.privateCustomDataConfig().map(({ key }) => key), ...(ngDevMode ? [{ debugName: "privateCustomData" }] : []));
|
|
18515
|
+
/** Whether the editor holds JSON (the property is an array or an object) rather than a single text. */
|
|
18516
|
+
editingJson = signal(false, ...(ngDevMode ? [{ debugName: "editingJson" }] : []));
|
|
18517
|
+
/** Whether the JSON being edited does not parse: the save is then not offered. */
|
|
18518
|
+
invalidJson = computed(() => {
|
|
18519
|
+
if (!this.editingJson())
|
|
18520
|
+
return false;
|
|
18521
|
+
try {
|
|
18522
|
+
JSON.parse(this.value() ?? "");
|
|
18523
|
+
return false;
|
|
18524
|
+
}
|
|
18525
|
+
catch {
|
|
18526
|
+
return true;
|
|
18527
|
+
}
|
|
18528
|
+
}, ...(ngDevMode ? [{ debugName: "invalidJson" }] : []));
|
|
18529
|
+
/** Every field of the form, in display order. A key declared both public and private gives two fields. */
|
|
18530
|
+
fields = computed(() => [
|
|
18531
|
+
...this.formKeys().map((key) => ({ category: "data", key })),
|
|
18532
|
+
...this.customData().map((key) => ({ category: "customData", key })),
|
|
18533
|
+
...this.privateCustomData().map((key) => ({ category: "privateCustomData", key }))
|
|
18534
|
+
], ...(ngDevMode ? [{ debugName: "fields" }] : []));
|
|
18333
18535
|
allowChangePassword = computed(() => {
|
|
18334
18536
|
const { useCredentials } = globalConfig;
|
|
18335
18537
|
const { allowChangePassword = false } = this.appStore.general()?.features || {};
|
|
@@ -18379,9 +18581,10 @@ class UserProfileFormComponent {
|
|
|
18379
18581
|
* @param propertyName property name
|
|
18380
18582
|
*/
|
|
18381
18583
|
onEdit(category, propertyName) {
|
|
18382
|
-
const
|
|
18584
|
+
const raw = this.getRawValue(category, propertyName);
|
|
18383
18585
|
this.feedback.set(undefined);
|
|
18384
|
-
this.
|
|
18586
|
+
this.editingJson.set(isStructured(raw));
|
|
18587
|
+
this.value.set(isStructured(raw) ? JSON.stringify(raw, null, 2) : raw === undefined || raw === null ? "" : String(raw).trim());
|
|
18385
18588
|
this.propertyToEdit.set(`${category}.${propertyName}`);
|
|
18386
18589
|
}
|
|
18387
18590
|
/** Name of the property as the user sees it: the translated label for `data`, the raw key otherwise. */
|
|
@@ -18406,8 +18609,21 @@ class UserProfileFormComponent {
|
|
|
18406
18609
|
/** Leaves the editor without saving. */
|
|
18407
18610
|
onCancelEdit() {
|
|
18408
18611
|
this.feedback.set(undefined);
|
|
18612
|
+
this.editingJson.set(false);
|
|
18409
18613
|
this.propertyToEdit.set(undefined);
|
|
18410
18614
|
}
|
|
18615
|
+
/**
|
|
18616
|
+
* Whether the profile holds a value for a property. Only then is there something to delete: a property
|
|
18617
|
+
* that shows its configured default, or that is simply not set, is absent from the profile and the
|
|
18618
|
+
* server answers 404 to a delete on it (ES-34462).
|
|
18619
|
+
*
|
|
18620
|
+
* @param category data or customData
|
|
18621
|
+
* @param propertyName property name
|
|
18622
|
+
*/
|
|
18623
|
+
hasStoredValue(category, propertyName) {
|
|
18624
|
+
const stored = this.storedValue(category, propertyName);
|
|
18625
|
+
return stored !== undefined && stored !== null;
|
|
18626
|
+
}
|
|
18411
18627
|
/**
|
|
18412
18628
|
* On data property deletion, for now we make the property an empty string
|
|
18413
18629
|
*
|
|
@@ -18417,16 +18633,22 @@ class UserProfileFormComponent {
|
|
|
18417
18633
|
async onDeleteData(category, propertyName) {
|
|
18418
18634
|
if (this.pending())
|
|
18419
18635
|
return;
|
|
18636
|
+
// Nothing stored, nothing to delete: not a failure, and no request to send.
|
|
18637
|
+
if (!this.hasStoredValue(category, propertyName)) {
|
|
18638
|
+
info("User profile property not stored, nothing to delete", { category, propertyName });
|
|
18639
|
+
return;
|
|
18640
|
+
}
|
|
18420
18641
|
const key = `${category}.${propertyName}`;
|
|
18421
18642
|
this.feedback.set(undefined);
|
|
18422
18643
|
this.pending.set(key);
|
|
18423
18644
|
try {
|
|
18424
|
-
|
|
18645
|
+
// A private property is only ever deleted through the private endpoint, never the public one.
|
|
18646
|
+
const response = category === "privateCustomData"
|
|
18647
|
+
? await deleteUserProfilePrivateCustomData(propertyName)
|
|
18648
|
+
: await deleteUserProfileProperty(this.userProfile(), category, propertyName);
|
|
18425
18649
|
if (response) {
|
|
18426
18650
|
info("Deleted user profile property", response);
|
|
18427
|
-
|
|
18428
|
-
userProfile[category][propertyName] = undefined;
|
|
18429
|
-
this.userProfileResource.set(userProfile);
|
|
18651
|
+
this.forgetProperty(category, propertyName);
|
|
18430
18652
|
this.feedback.set({ key, type: "success", message: this.message("deleted", category, propertyName) });
|
|
18431
18653
|
}
|
|
18432
18654
|
else {
|
|
@@ -18434,13 +18656,34 @@ class UserProfileFormComponent {
|
|
|
18434
18656
|
this.feedback.set({ key, type: "error", message: this.message("deleteFailed", category, propertyName) });
|
|
18435
18657
|
}
|
|
18436
18658
|
}
|
|
18437
|
-
catch {
|
|
18438
|
-
|
|
18659
|
+
catch (err) {
|
|
18660
|
+
if (err?.status === 404) {
|
|
18661
|
+
// The property is not in the profile (anymore), e.g. removed from another tab: the state asked for is
|
|
18662
|
+
// already reached, so it is not an error to show.
|
|
18663
|
+
info("User profile property already absent", { category, propertyName });
|
|
18664
|
+
this.forgetProperty(category, propertyName);
|
|
18665
|
+
this.feedback.set({ key, type: "success", message: this.message("deleted", category, propertyName) });
|
|
18666
|
+
}
|
|
18667
|
+
else {
|
|
18668
|
+
this.feedback.set({ key, type: "error", message: this.message("deleteFailed", category, propertyName) });
|
|
18669
|
+
}
|
|
18439
18670
|
}
|
|
18440
18671
|
finally {
|
|
18441
18672
|
this.pending.set(undefined);
|
|
18442
18673
|
}
|
|
18443
18674
|
}
|
|
18675
|
+
/** Takes a deleted property out of the stored profile; a custom property with a default shows that default again. */
|
|
18676
|
+
forgetProperty(category, propertyName) {
|
|
18677
|
+
const userProfile = JSON.parse(JSON.stringify(this.userProfile()));
|
|
18678
|
+
if (category === "data") {
|
|
18679
|
+
if (userProfile.data)
|
|
18680
|
+
userProfile.data[propertyName] = undefined;
|
|
18681
|
+
}
|
|
18682
|
+
else if (userProfile[category]) {
|
|
18683
|
+
userProfile[category] = setAtPath(userProfile[category], propertyName, undefined);
|
|
18684
|
+
}
|
|
18685
|
+
this.userProfileResource.set(userProfile);
|
|
18686
|
+
}
|
|
18444
18687
|
/**
|
|
18445
18688
|
* Saving a data property
|
|
18446
18689
|
*
|
|
@@ -18452,24 +18695,30 @@ class UserProfileFormComponent {
|
|
|
18452
18695
|
return;
|
|
18453
18696
|
const key = `${category}.${propertyName}`;
|
|
18454
18697
|
this.feedback.set(undefined);
|
|
18698
|
+
const written = this.valueToWrite(category, propertyName);
|
|
18699
|
+
if (written === INVALID) {
|
|
18700
|
+
this.feedback.set({ key, type: "error", message: this.transloco.translate("userProfile.invalidJson") });
|
|
18701
|
+
return;
|
|
18702
|
+
}
|
|
18455
18703
|
this.pending.set(key);
|
|
18456
18704
|
// cloning the user profile to avoid changing the signal value before confirming the update
|
|
18457
18705
|
const userProfile = JSON.parse(JSON.stringify(this.userProfile()));
|
|
18458
|
-
if (
|
|
18459
|
-
userProfile
|
|
18460
|
-
|
|
18706
|
+
if (category === "data") {
|
|
18707
|
+
userProfile.data = { ...(userProfile.data ?? {}), [propertyName]: written };
|
|
18708
|
+
}
|
|
18709
|
+
else {
|
|
18710
|
+
userProfile[category] = setAtPath(userProfile[category], propertyName, written);
|
|
18711
|
+
}
|
|
18712
|
+
// The server does not create the nodes on the way (404): write at the first missing one, with the rest nested.
|
|
18713
|
+
const target = category === "data" ? { key: propertyName, value: written } : writeTarget(this.userProfile()?.[category], propertyName, written);
|
|
18461
18714
|
try {
|
|
18462
|
-
|
|
18463
|
-
|
|
18464
|
-
|
|
18465
|
-
|
|
18466
|
-
|
|
18467
|
-
...response,
|
|
18468
|
-
data: { ...(newUserProfile?.data || {}), ...(response.data || {}) },
|
|
18469
|
-
customData: { ...(newUserProfile?.customData || {}), ...(response.customData || {}) }
|
|
18470
|
-
});
|
|
18471
|
-
}
|
|
18715
|
+
// A private property is only ever written through the private endpoint, never the public one.
|
|
18716
|
+
const response = category === "privateCustomData"
|
|
18717
|
+
? await patchUserProfilePrivateCustomData(target.key, target.value)
|
|
18718
|
+
: await patchUserProfile(userProfile, category, target.key);
|
|
18719
|
+
this.foldSavedValue(category, target.key, response, target.value);
|
|
18472
18720
|
this.value.set(undefined);
|
|
18721
|
+
this.editingJson.set(false);
|
|
18473
18722
|
this.propertyToEdit.set(undefined);
|
|
18474
18723
|
this.feedback.set({ key, type: "success", message: this.message("saved", category, propertyName) });
|
|
18475
18724
|
}
|
|
@@ -18481,20 +18730,86 @@ class UserProfileFormComponent {
|
|
|
18481
18730
|
}
|
|
18482
18731
|
}
|
|
18483
18732
|
/**
|
|
18484
|
-
*
|
|
18733
|
+
* Folds the answer of a PATCH back into the stored profile, so the form shows what was just saved.
|
|
18734
|
+
*
|
|
18735
|
+
* The answer depends on the server: before ES-34373 it is the whole updated profile (`{ data, customData, … }`),
|
|
18736
|
+
* after it is the written value alone (`"fav-1AAA"`). Reading the latter as a profile used to leave the stored
|
|
18737
|
+
* profile unchanged, and the form kept showing the old value (or the default) until the page was reloaded.
|
|
18738
|
+
*/
|
|
18739
|
+
foldSavedValue(category, propertyName, response, written) {
|
|
18740
|
+
const current = this.userProfile();
|
|
18741
|
+
if (category !== "privateCustomData" && !isStructured(written) && isPlainObject(response) && ("data" in response || "customData" in response)) {
|
|
18742
|
+
const profile = response;
|
|
18743
|
+
this.userProfileResource.set({
|
|
18744
|
+
...current,
|
|
18745
|
+
...profile,
|
|
18746
|
+
data: { ...(current?.data || {}), ...(profile.data || {}) },
|
|
18747
|
+
customData: { ...(current?.customData || {}), ...(profile.customData || {}) }
|
|
18748
|
+
});
|
|
18749
|
+
return;
|
|
18750
|
+
}
|
|
18751
|
+
// The value alone: what the server read back, or what was sent when it answers with nothing usable.
|
|
18752
|
+
const value = response === undefined || response === null ? written : response;
|
|
18753
|
+
this.userProfileResource.set({
|
|
18754
|
+
...current,
|
|
18755
|
+
[category]: category === "data" ? { ...(current?.data || {}), [propertyName]: value } : setAtPath(current?.[category], propertyName, value)
|
|
18756
|
+
});
|
|
18757
|
+
}
|
|
18758
|
+
/** The value the profile holds for a property, whichever category it lives in; `propertyName` is a path for custom data. */
|
|
18759
|
+
storedValue(category, propertyName) {
|
|
18760
|
+
const source = this.userProfile()?.[category];
|
|
18761
|
+
return category === "data" ? source?.[propertyName] : getAtPath(source, propertyName);
|
|
18762
|
+
}
|
|
18763
|
+
/**
|
|
18764
|
+
* The value of a property as the profile holds it, of any JSON type; while it holds none, the default the
|
|
18765
|
+
* administrator configured for a custom property.
|
|
18766
|
+
*/
|
|
18767
|
+
getRawValue(category, key) {
|
|
18768
|
+
const value = this.storedValue(category, key);
|
|
18769
|
+
if (value === undefined && category !== "data") {
|
|
18770
|
+
const config = category === "customData" ? this.customDataConfig() : this.privateCustomDataConfig();
|
|
18771
|
+
return config.find(property => property.key === key)?.defaultValue;
|
|
18772
|
+
}
|
|
18773
|
+
return value;
|
|
18774
|
+
}
|
|
18775
|
+
/** Whether the value of a property is a structure (an array or an object), shown with {@link JsonValueView}. */
|
|
18776
|
+
isStructuredValue(category, key) {
|
|
18777
|
+
return isStructured(this.getRawValue(category, key));
|
|
18778
|
+
}
|
|
18779
|
+
/**
|
|
18780
|
+
* Get the value of a data key, when it is a scalar
|
|
18485
18781
|
*
|
|
18486
18782
|
* @param category data or customData
|
|
18487
18783
|
* @param key property key
|
|
18488
|
-
* @returns the value
|
|
18784
|
+
* @returns the value, or `undefined` when there is none or when it is a structure
|
|
18489
18785
|
*/
|
|
18490
18786
|
getDataValue(category, key) {
|
|
18491
|
-
const
|
|
18492
|
-
|
|
18493
|
-
|
|
18494
|
-
|
|
18495
|
-
|
|
18787
|
+
const value = this.getRawValue(category, key);
|
|
18788
|
+
return value === undefined || isStructured(value) ? undefined : value;
|
|
18789
|
+
}
|
|
18790
|
+
/**
|
|
18791
|
+
* What the editor holds, as the value to send: the parsed JSON for a structure (or `INVALID`), the text
|
|
18792
|
+
* otherwise. A property stored as a number or a boolean stays one when the text still reads as such, so that
|
|
18793
|
+
* saving does not silently turn it into a string.
|
|
18794
|
+
*/
|
|
18795
|
+
valueToWrite(category, propertyName) {
|
|
18796
|
+
if (this.editingJson()) {
|
|
18797
|
+
try {
|
|
18798
|
+
return JSON.parse(this.value() ?? "");
|
|
18799
|
+
}
|
|
18800
|
+
catch {
|
|
18801
|
+
return INVALID;
|
|
18802
|
+
}
|
|
18496
18803
|
}
|
|
18497
|
-
|
|
18804
|
+
const text = this.value()?.trim() || "";
|
|
18805
|
+
if (category === "data")
|
|
18806
|
+
return text;
|
|
18807
|
+
const stored = this.storedValue(category, propertyName);
|
|
18808
|
+
if (typeof stored === "number" && text !== "" && Number.isFinite(Number(text)))
|
|
18809
|
+
return Number(text);
|
|
18810
|
+
if (typeof stored === "boolean" && (text === "true" || text === "false"))
|
|
18811
|
+
return text === "true";
|
|
18812
|
+
return text;
|
|
18498
18813
|
}
|
|
18499
18814
|
/**
|
|
18500
18815
|
* TODO: how do we save the picture?
|
|
@@ -18526,7 +18841,7 @@ class UserProfileFormComponent {
|
|
|
18526
18841
|
}
|
|
18527
18842
|
}
|
|
18528
18843
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: UserProfileFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
18529
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.29", type: UserProfileFormComponent, isStandalone: true, selector: "user-profile-form, UserProfileForm, userprofileform", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, currentLanguage: { classPropertyName: "currentLanguage", publicName: "currentLanguage", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", currentLanguage: "currentLanguageChange" }, host: { classAttribute: "flex flex-col h-full overflow-hidden" }, providers: [provideTranslocoScope("user-profile", "login")], viewQueries: [{ propertyName: "createInputElement", first: true, predicate: ["avatarInput"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (userProfileResource.hasValue()) {\n @let profile = userProfile();\n <div class=\"flex min-h-0 flex-1 flex-col gap-2 p-4\">\n <div class=\"mb-4 flex\">\n @if (allowProfilePhoto()) {\n <!-- AVATAR -->\n <Avatar\n class=\"size-14 cursor-pointer\"\n [class.ring-2]=\"!!fieldState('data.profilePhoto')\"\n [class.ring-destructive]=\"fieldState('data.profilePhoto') === 'error'\"\n [class.ring-green-600]=\"fieldState('data.profilePhoto') === 'success'\"\n (click)=\"avatarInput.click()\">\n @if (profile?.data?.profilePhoto) {\n <AvatarImage\n [src]=\"profile?.data?.profilePhoto!\"\n width=\"44\"\n height=\"44\"\n alt=\"avatar\" />\n }\n <AvatarFallback class=\"text-lg\">\n @if (initials()) {\n <span>{{ initials() }}</span>\n } @else {\n <UserIcon class=\"size-7\" />\n }\n </AvatarFallback>\n </Avatar>\n @if (profile?.data?.profilePhoto) {\n <button\n variant=\"ghost\"\n [iconOnly]=\"true\"\n class=\"text-destructive cursor-pointer\"\n title=\"{{ 'userProfile.deleteProperty' | transloco }}\"\n [attr.aria-label]=\"'userProfile.deleteProperty' | transloco\"\n [disabled]=\"!!pending()\"\n (click)=\"onDeleteData('data', 'profilePhoto')\">\n <TrashIcon />\n </button>\n }\n }\n <div class=\"grow\"></div>\n <div class=\"flex flex-col gap-4\">\n <!-- LANGUAGE -->\n <select\n class=\"hover:outline-primary focus:outline-primary border-foreground/10 bg-background hover:bg-muted focus:bg-muted h-8 rounded-md border px-2 hover:outline focus:outline\"\n [(ngModel)]=\"currentLanguage\"\n (change)=\"changeLanguage()\">\n @for (lang of AllLanguages; track lang.code) {\n <option [value]=\"lang.code\">\n {{ lang.label }}\n </option>\n }\n </select>\n <!-- CHANGE PASSWORD -->\n @if (allowChangePassword()) {\n <button\n [disabled]=\"changingPassword()\"\n (click)=\"changingPassword.set(true)\">\n {{ \"login.changePassword\" | transloco }}\n </button>\n }\n </div>\n </div>\n <!-- always rendered, so the room is reserved and the layout doesn't shift when a message appears -->\n <p\n class=\"min-h-5 text-sm\"\n role=\"status\"\n [attr.aria-live]=\"feedback()?.type === 'error' ? 'assertive' : 'polite'\"\n [class.text-destructive]=\"feedback()?.type === 'error'\"\n [class.text-green-600]=\"feedback()?.type === 'success'\">\n {{ feedback()?.message }}\n </p>\n @if (changingPassword()) {\n <ChangePassword\n [username]=\"username()\"\n [redirectAfterSuccess]=\"false\"\n [redirectAfterCancel]=\"false\"\n (cancel)=\"changingPassword.set(false)\"\n (success)=\"changingPassword.set(false)\" />\n } @else {\n <!-- -m-1 p-1: room for the outline of a field, which the scroll container would otherwise clip -->\n <div class=\"-m-1 min-h-0 flex-1 overflow-auto p-1\">\n <div>\n @for (key of formKeys(); track key) {\n <div>\n <p class=\"px-2\">{{ `userProfile.data.${key}` | transloco }}</p>\n <div\n class=\"group flex flex-row rounded-md px-2 py-0.5\"\n [class.ring-1]=\"!!fieldState('data.' + key)\"\n [class.ring-destructive]=\"fieldState('data.' + key) === 'error'\"\n [class.ring-green-600]=\"\n fieldState('data.' + key) === 'success'\n \">\n <div\n class=\"min-w-0 grow break-words whitespace-pre-line\"\n [class.text-muted-foreground]=\"!getDataValue('data', key)\">\n @if (propertyToEdit() === \"data.\" + key) {\n <textarea\n class=\"hover:outline-primary focus:outline-primary border-foreground/20 hover:bg-muted focus:bg-muted mt-2 w-full rounded-md border px-2 pt-1 hover:outline focus:outline\"\n id=\"user-profile-{{ key }}\"\n [(ngModel)]=\"value\"></textarea>\n } @else {\n {{\n getDataValue(\"data\", key) ||\n (\"userProfile.notDefined\" | transloco)\n }}\n }\n </div>\n @if (propertyToEdit() === \"data.\" + key) {\n <button\n variant=\"ghost\"\n [iconOnly]=\"true\"\n class=\"visible mx-2 cursor-pointer sm:invisible sm:group-hover:visible\"\n title=\"{{ 'userProfile.editProperty' | transloco }}\"\n [attr.aria-label]=\"'userProfile.editProperty' | transloco\"\n [disabled]=\"!!pending()\"\n (click)=\"onSaveData('data', key)\">\n <save-icon class=\"ml-2\" />\n </button>\n <button\n variant=\"ghost\"\n [iconOnly]=\"true\"\n class=\"visible cursor-pointer sm:invisible sm:group-hover:visible\"\n title=\"{{ 'userProfile.editProperty' | transloco }}\"\n [attr.aria-label]=\"'userProfile.editProperty' | transloco\"\n (click)=\"onCancelEdit()\">\n <UndoIcon />\n </button>\n } @else {\n <button\n variant=\"ghost\"\n [iconOnly]=\"true\"\n class=\"visible mx-2 cursor-pointer sm:invisible sm:group-hover:visible\"\n title=\"{{ 'userProfile.editProperty' | transloco }}\"\n [attr.aria-label]=\"'userProfile.editProperty' | transloco\"\n (click)=\"onEdit('data', key)\">\n <EditIcon />\n </button>\n <button\n variant=\"ghost\"\n [iconOnly]=\"true\"\n class=\"text-destructive visible cursor-pointer sm:invisible sm:group-hover:visible\"\n title=\"{{ 'userProfile.deleteProperty' | transloco }}\"\n [attr.aria-label]=\"'userProfile.deleteProperty' | transloco\"\n [disabled]=\"!!pending()\"\n (click)=\"onDeleteData('data', key)\">\n <TrashIcon />\n </button>\n }\n </div>\n </div>\n }\n @for (key of customData(); track key) {\n <div>\n <p class=\"px-2\">{{ key }}</p>\n <div\n class=\"group flex flex-row rounded-md px-2 py-0.5\"\n [class.ring-1]=\"!!fieldState('customData.' + key)\"\n [class.ring-destructive]=\"\n fieldState('customData.' + key) === 'error'\n \"\n [class.ring-green-600]=\"\n fieldState('customData.' + key) === 'success'\n \">\n <div\n class=\"min-w-0 grow break-words whitespace-pre-line\"\n [class.text-muted-foreground]=\"\n !getDataValue('customData', key)\n \">\n @if (propertyToEdit() === \"customData.\" + key) {\n <textarea\n class=\"hover:outline-primary focus:outline-primary border-foreground/20 hover:bg-muted focus:bg-muted mt-2 w-full rounded-md border px-2 pt-1 hover:outline focus:outline\"\n id=\"user-profile-{{ key }}\"\n [(ngModel)]=\"value\"></textarea>\n } @else {\n {{\n getDataValue(\"customData\", key) ||\n (\"userProfile.notDefined\" | transloco)\n }}\n }\n </div>\n @if (propertyToEdit() === \"customData.\" + key) {\n <button\n variant=\"ghost\"\n [iconOnly]=\"true\"\n class=\"visible mx-2 cursor-pointer sm:invisible sm:group-hover:visible\"\n title=\"{{ 'userProfile.editProperty' | transloco }}\"\n [attr.aria-label]=\"'userProfile.editProperty' | transloco\"\n [disabled]=\"!!pending()\"\n (click)=\"onSaveData('customData', key)\">\n <save-icon class=\"ml-2\" />\n </button>\n <button\n variant=\"ghost\"\n [iconOnly]=\"true\"\n class=\"visible cursor-pointer sm:invisible sm:group-hover:visible\"\n title=\"{{ 'userProfile.editProperty' | transloco }}\"\n [attr.aria-label]=\"'userProfile.editProperty' | transloco\"\n (click)=\"onCancelEdit()\">\n <UndoIcon />\n </button>\n } @else {\n <button\n variant=\"ghost\"\n [iconOnly]=\"true\"\n class=\"visible mx-2 cursor-pointer sm:invisible sm:group-hover:visible\"\n title=\"{{ 'userProfile.editProperty' | transloco }}\"\n [attr.aria-label]=\"'userProfile.editProperty' | transloco\"\n (click)=\"onEdit('customData', key)\">\n <EditIcon />\n </button>\n <button\n variant=\"ghost\"\n [iconOnly]=\"true\"\n class=\"text-destructive visible cursor-pointer sm:invisible sm:group-hover:visible\"\n title=\"{{ 'userProfile.deleteProperty' | transloco }}\"\n [attr.aria-label]=\"'userProfile.deleteProperty' | transloco\"\n [disabled]=\"!!pending()\"\n (click)=\"onDeleteData('customData', key)\">\n <TrashIcon />\n </button>\n }\n </div>\n </div>\n }\n </div>\n </div>\n }\n </div>\n} @else if (userProfileResource.isLoading()) {\n <span>Loading...</span>\n} @else if (userProfileResource.error()) {\n <p>Please contact an administrator to create your user profile.</p>\n}\n\n<!-- AVATAR UPLOAD -->\n<input\n #avatarInput\n class=\"hidden\"\n type=\"file\"\n accept=\"image/*\"\n (change)=\"uploadAvatar($event)\" />\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.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: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: RouterModule }, { kind: "component", type: TrashIcon, selector: "trash-icon, TrashIcon", inputs: ["class"] }, { kind: "component", type: EditIcon, selector: "edit-icon, EditIcon, editicon", inputs: ["class"] }, { kind: "component", type: UndoIcon, selector: "UndoIcon, undoicon, undo-icon", inputs: ["class"] }, { kind: "component", type: SaveIcon, selector: "save-icon, SaveIcon", inputs: ["class"] }, { kind: "component", type: AvatarComponent, selector: "avatar, Avatar", inputs: ["class", "size"] }, { kind: "component", type: AvatarFallbackComponent, selector: "avatar-fallback, avatarfallback, AvatarFallback", inputs: ["class", "size"] }, { kind: "component", type: UserIcon, selector: "user-icon, UserIcon", inputs: ["class"] }, { kind: "component", type: AvatarImageComponent, selector: "avatar-image, AvatarImage, avatarimage", inputs: ["class", "src", "alt", "width", "height", "referrerPolicy", "crossOrigin"] }, { kind: "directive", type: ButtonComponent, selector: "button", inputs: ["class", "variant", "decoration", "scheme", "iconOnly", "size", "solid"] }, { kind: "component", type: ChangePasswordComponent, selector: "change-password, ChangePassword, changepassword", inputs: ["username", "alert", "redirectAfterSuccess", "redirectAfterCancel", "currentPassword", "newPassword", "confirmPassword"], outputs: ["success", "cancel", "currentPasswordChange", "newPasswordChange", "confirmPasswordChange"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
|
|
18844
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.29", type: UserProfileFormComponent, isStandalone: true, selector: "user-profile-form, UserProfileForm, userprofileform", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, currentLanguage: { classPropertyName: "currentLanguage", publicName: "currentLanguage", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", currentLanguage: "currentLanguageChange" }, host: { classAttribute: "flex flex-col h-full overflow-hidden" }, providers: [provideTranslocoScope("user-profile", "login")], viewQueries: [{ propertyName: "createInputElement", first: true, predicate: ["avatarInput"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (userProfileResource.hasValue()) {\n @let profile = userProfile();\n <div class=\"flex min-h-0 flex-1 flex-col gap-2 p-4\">\n <div class=\"mb-4 flex\">\n @if (allowProfilePhoto()) {\n <!-- AVATAR -->\n <Avatar\n class=\"size-14 cursor-pointer\"\n [class.ring-2]=\"!!fieldState('data.profilePhoto')\"\n [class.ring-destructive]=\"fieldState('data.profilePhoto') === 'error'\"\n [class.ring-green-600]=\"fieldState('data.profilePhoto') === 'success'\"\n (click)=\"avatarInput.click()\">\n @if (profile?.data?.profilePhoto) {\n <AvatarImage\n [src]=\"profile?.data?.profilePhoto!\"\n width=\"44\"\n height=\"44\"\n alt=\"avatar\" />\n }\n <AvatarFallback class=\"text-lg\">\n @if (initials()) {\n <span>{{ initials() }}</span>\n } @else {\n <UserIcon class=\"size-7\" />\n }\n </AvatarFallback>\n </Avatar>\n @if (profile?.data?.profilePhoto) {\n <button\n variant=\"ghost\"\n [iconOnly]=\"true\"\n class=\"text-destructive cursor-pointer\"\n title=\"{{ 'userProfile.deleteProperty' | transloco }}\"\n [attr.aria-label]=\"'userProfile.deleteProperty' | transloco\"\n [disabled]=\"!!pending()\"\n (click)=\"onDeleteData('data', 'profilePhoto')\">\n <TrashIcon />\n </button>\n }\n }\n <div class=\"grow\"></div>\n <div class=\"flex flex-col gap-4\">\n <!-- LANGUAGE -->\n <select\n class=\"hover:outline-primary focus:outline-primary border-foreground/10 bg-background hover:bg-muted focus:bg-muted h-8 rounded-md border px-2 hover:outline focus:outline\"\n [(ngModel)]=\"currentLanguage\"\n (change)=\"changeLanguage()\">\n @for (lang of AllLanguages; track lang.code) {\n <option [value]=\"lang.code\">\n {{ lang.label }}\n </option>\n }\n </select>\n <!-- CHANGE PASSWORD -->\n @if (allowChangePassword()) {\n <button\n [disabled]=\"changingPassword()\"\n (click)=\"changingPassword.set(true)\">\n {{ \"login.changePassword\" | transloco }}\n </button>\n }\n </div>\n </div>\n <!-- always rendered, so the room is reserved and the layout doesn't shift when a message appears -->\n <p\n class=\"min-h-5 text-sm\"\n role=\"status\"\n [attr.aria-live]=\"feedback()?.type === 'error' ? 'assertive' : 'polite'\"\n [class.text-destructive]=\"feedback()?.type === 'error'\"\n [class.text-green-600]=\"feedback()?.type === 'success'\">\n {{ feedback()?.message }}\n </p>\n @if (changingPassword()) {\n <ChangePassword\n [username]=\"username()\"\n [redirectAfterSuccess]=\"false\"\n [redirectAfterCancel]=\"false\"\n (cancel)=\"changingPassword.set(false)\"\n (success)=\"changingPassword.set(false)\" />\n } @else {\n <!-- -m-1 p-1: room for the outline of a field, which the scroll container would otherwise clip -->\n <div class=\"-m-1 min-h-0 flex-1 overflow-auto p-1\">\n <!-- two columns from the sm breakpoint; a structured value or the JSON editor takes the full width -->\n <div\n class=\"grid grid-cols-1 items-start gap-x-6 gap-y-2 sm:grid-cols-2\">\n @for (field of fields(); track field.category + \".\" + field.key) {\n @let id = field.category + \".\" + field.key;\n @let isPrivate = field.category === \"privateCustomData\";\n <div\n [class]=\"\n isStructuredValue(field.category, field.key) ||\n (propertyToEdit() === id && editingJson())\n ? 'min-w-0 sm:col-span-2'\n : 'min-w-0'\n \">\n <p class=\"flex items-center gap-2 px-2\">\n <span>\n @if (field.category === \"data\") {\n {{ `userProfile.data.${field.key}` | transloco }}\n } @else {\n {{ field.key.split(\"/\").join(\" \u203A \") }}\n }\n </span>\n <!-- PRIVATE / PUBLIC: icon AND text, so the information is not carried by the icon or a color alone -->\n <span\n class=\"text-muted-foreground inline-flex items-center gap-1 text-xs\"\n [attr.data-visibility]=\"isPrivate ? 'private' : 'public'\"\n [title]=\"\n (isPrivate\n ? 'userProfile.privateHint'\n : 'userProfile.publicHint'\n ) | transloco\n \">\n @if (isPrivate) {\n <user-secret-icon class=\"size-3.5 shrink-0\" />\n } @else {\n <globe-icon class=\"size-3.5 shrink-0\" />\n }\n <span>{{\n (isPrivate ? \"userProfile.private\" : \"userProfile.public\")\n | transloco\n }}</span>\n <span class=\"sr-only\">\n {{\n (isPrivate\n ? \"userProfile.privateHint\"\n : \"userProfile.publicHint\"\n ) | transloco\n }}\n </span>\n </span>\n </p>\n <div\n class=\"group flex flex-row rounded-md px-2 py-0.5\"\n [class.ring-1]=\"!!fieldState(id)\"\n [class.ring-destructive]=\"fieldState(id) === 'error'\"\n [class.ring-green-600]=\"fieldState(id) === 'success'\">\n <div\n class=\"min-w-0 grow break-words whitespace-pre-line\"\n [class.text-muted-foreground]=\"\n getRawValue(field.category, field.key) == null\n \">\n @if (propertyToEdit() === id) {\n @if (editingJson()) {\n <!-- an array or an object is edited as JSON, on several lines -->\n <textarea\n class=\"hover:outline-primary focus:outline-primary border-foreground/20 hover:bg-muted focus:bg-muted mt-2 w-full rounded-md border px-2 pt-1 font-mono text-sm hover:outline focus:outline\"\n rows=\"8\"\n [attr.aria-invalid]=\"invalidJson() || null\"\n [attr.aria-describedby]=\"\n invalidJson() ? 'user-profile-json-error' : null\n \"\n id=\"user-profile-{{ field.category }}-{{ field.key }}\"\n [(ngModel)]=\"value\"></textarea>\n } @else {\n <input\n type=\"text\"\n class=\"hover:outline-primary focus:outline-primary border-foreground/20 hover:bg-muted focus:bg-muted mt-2 h-8 w-full rounded-md border px-2 hover:outline focus:outline\"\n id=\"user-profile-{{ field.category }}-{{ field.key }}\"\n [(ngModel)]=\"value\" />\n }\n @if (invalidJson()) {\n <p\n id=\"user-profile-json-error\"\n class=\"text-destructive text-sm\">\n {{ \"userProfile.invalidJson\" | transloco }}\n </p>\n }\n } @else if (isStructuredValue(field.category, field.key)) {\n <json-value-view\n [value]=\"getRawValue(field.category, field.key)\" />\n } @else {\n {{\n getDataValue(field.category, field.key) ??\n (\"userProfile.notDefined\" | transloco)\n }}\n }\n </div>\n @if (propertyToEdit() === id) {\n <button\n variant=\"ghost\"\n [iconOnly]=\"true\"\n class=\"visible mx-2 cursor-pointer sm:invisible sm:group-hover:visible\"\n title=\"{{ 'userProfile.editProperty' | transloco }}\"\n [attr.aria-label]=\"'userProfile.editProperty' | transloco\"\n [disabled]=\"!!pending() || invalidJson()\"\n (click)=\"onSaveData(field.category, field.key)\">\n <save-icon class=\"ml-2\" />\n </button>\n <button\n variant=\"ghost\"\n [iconOnly]=\"true\"\n class=\"visible cursor-pointer sm:invisible sm:group-hover:visible\"\n title=\"{{ 'userProfile.editProperty' | transloco }}\"\n [attr.aria-label]=\"'userProfile.editProperty' | transloco\"\n (click)=\"onCancelEdit()\">\n <UndoIcon />\n </button>\n } @else {\n <button\n variant=\"ghost\"\n [iconOnly]=\"true\"\n class=\"visible mx-2 cursor-pointer sm:invisible sm:group-hover:visible\"\n title=\"{{ 'userProfile.editProperty' | transloco }}\"\n [attr.aria-label]=\"'userProfile.editProperty' | transloco\"\n (click)=\"onEdit(field.category, field.key)\">\n <EditIcon />\n </button>\n <!-- nothing stored, nothing to delete: the button is not offered rather than shown disabled -->\n @if (hasStoredValue(field.category, field.key)) {\n <button\n variant=\"ghost\"\n [iconOnly]=\"true\"\n class=\"text-destructive visible cursor-pointer sm:invisible sm:group-hover:visible\"\n title=\"{{ 'userProfile.deleteProperty' | transloco }}\"\n [attr.aria-label]=\"\n 'userProfile.deleteProperty' | transloco\n \"\n [disabled]=\"!!pending()\"\n (click)=\"onDeleteData(field.category, field.key)\">\n <TrashIcon />\n </button>\n }\n }\n </div>\n </div>\n }\n </div>\n </div>\n }\n </div>\n} @else if (userProfileResource.isLoading()) {\n <span>Loading...</span>\n} @else if (userProfileResource.error()) {\n <p>Please contact an administrator to create your user profile.</p>\n}\n\n<!-- AVATAR UPLOAD -->\n<input\n #avatarInput\n class=\"hidden\"\n type=\"file\"\n accept=\"image/*\"\n (change)=\"uploadAvatar($event)\" />\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.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: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: RouterModule }, { kind: "component", type: TrashIcon, selector: "trash-icon, TrashIcon", inputs: ["class"] }, { kind: "component", type: EditIcon, selector: "edit-icon, EditIcon, editicon", inputs: ["class"] }, { kind: "component", type: UndoIcon, selector: "UndoIcon, undoicon, undo-icon", inputs: ["class"] }, { kind: "component", type: SaveIcon, selector: "save-icon, SaveIcon", inputs: ["class"] }, { kind: "component", type: AvatarComponent, selector: "avatar, Avatar", inputs: ["class", "size"] }, { kind: "component", type: AvatarFallbackComponent, selector: "avatar-fallback, avatarfallback, AvatarFallback", inputs: ["class", "size"] }, { kind: "component", type: UserIcon, selector: "user-icon, UserIcon", inputs: ["class"] }, { kind: "component", type: GlobeIcon, selector: "globe-icon, GlobeIcon, globeicon", inputs: ["class"] }, { kind: "component", type: UserSecretIcon, selector: "user-secret-icon, UserSecretIcon", inputs: ["class"] }, { kind: "component", type: JsonValueView, selector: "json-value-view", inputs: ["value"] }, { kind: "component", type: AvatarImageComponent, selector: "avatar-image, AvatarImage, avatarimage", inputs: ["class", "src", "alt", "width", "height", "referrerPolicy", "crossOrigin"] }, { kind: "directive", type: ButtonComponent, selector: "button", inputs: ["class", "variant", "decoration", "scheme", "iconOnly", "size", "solid"] }, { kind: "component", type: ChangePasswordComponent, selector: "change-password, ChangePassword, changepassword", inputs: ["username", "alert", "redirectAfterSuccess", "redirectAfterCancel", "currentPassword", "newPassword", "confirmPassword"], outputs: ["success", "cancel", "currentPasswordChange", "newPasswordChange", "confirmPasswordChange"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
|
|
18530
18845
|
}
|
|
18531
18846
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: UserProfileFormComponent, decorators: [{
|
|
18532
18847
|
type: Component,
|
|
@@ -18541,10 +18856,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
|
|
|
18541
18856
|
AvatarComponent,
|
|
18542
18857
|
AvatarFallbackComponent,
|
|
18543
18858
|
UserIcon,
|
|
18859
|
+
GlobeIcon,
|
|
18860
|
+
UserSecretIcon,
|
|
18861
|
+
JsonValueView,
|
|
18544
18862
|
AvatarImageComponent,
|
|
18545
18863
|
ButtonComponent,
|
|
18546
18864
|
ChangePasswordComponent
|
|
18547
|
-
], providers: [provideTranslocoScope("user-profile", "login")], template: "@if (userProfileResource.hasValue()) {\n @let profile = userProfile();\n <div class=\"flex min-h-0 flex-1 flex-col gap-2 p-4\">\n <div class=\"mb-4 flex\">\n @if (allowProfilePhoto()) {\n <!-- AVATAR -->\n <Avatar\n class=\"size-14 cursor-pointer\"\n [class.ring-2]=\"!!fieldState('data.profilePhoto')\"\n [class.ring-destructive]=\"fieldState('data.profilePhoto') === 'error'\"\n [class.ring-green-600]=\"fieldState('data.profilePhoto') === 'success'\"\n (click)=\"avatarInput.click()\">\n @if (profile?.data?.profilePhoto) {\n <AvatarImage\n [src]=\"profile?.data?.profilePhoto!\"\n width=\"44\"\n height=\"44\"\n alt=\"avatar\" />\n }\n <AvatarFallback class=\"text-lg\">\n @if (initials()) {\n <span>{{ initials() }}</span>\n } @else {\n <UserIcon class=\"size-7\" />\n }\n </AvatarFallback>\n </Avatar>\n @if (profile?.data?.profilePhoto) {\n <button\n variant=\"ghost\"\n [iconOnly]=\"true\"\n class=\"text-destructive cursor-pointer\"\n title=\"{{ 'userProfile.deleteProperty' | transloco }}\"\n [attr.aria-label]=\"'userProfile.deleteProperty' | transloco\"\n [disabled]=\"!!pending()\"\n (click)=\"onDeleteData('data', 'profilePhoto')\">\n <TrashIcon />\n </button>\n }\n }\n <div class=\"grow\"></div>\n <div class=\"flex flex-col gap-4\">\n <!-- LANGUAGE -->\n <select\n class=\"hover:outline-primary focus:outline-primary border-foreground/10 bg-background hover:bg-muted focus:bg-muted h-8 rounded-md border px-2 hover:outline focus:outline\"\n [(ngModel)]=\"currentLanguage\"\n (change)=\"changeLanguage()\">\n @for (lang of AllLanguages; track lang.code) {\n <option [value]=\"lang.code\">\n {{ lang.label }}\n </option>\n }\n </select>\n <!-- CHANGE PASSWORD -->\n @if (allowChangePassword()) {\n <button\n [disabled]=\"changingPassword()\"\n (click)=\"changingPassword.set(true)\">\n {{ \"login.changePassword\" | transloco }}\n </button>\n }\n </div>\n </div>\n <!-- always rendered, so the room is reserved and the layout doesn't shift when a message appears -->\n <p\n class=\"min-h-5 text-sm\"\n role=\"status\"\n [attr.aria-live]=\"feedback()?.type === 'error' ? 'assertive' : 'polite'\"\n [class.text-destructive]=\"feedback()?.type === 'error'\"\n [class.text-green-600]=\"feedback()?.type === 'success'\">\n {{ feedback()?.message }}\n </p>\n @if (changingPassword()) {\n <ChangePassword\n [username]=\"username()\"\n [redirectAfterSuccess]=\"false\"\n [redirectAfterCancel]=\"false\"\n (cancel)=\"changingPassword.set(false)\"\n (success)=\"changingPassword.set(false)\" />\n } @else {\n <!-- -m-1 p-1: room for the outline of a field, which the scroll container would otherwise clip -->\n <div class=\"-m-1 min-h-0 flex-1 overflow-auto p-1\">\n <div>\n @for (key of formKeys(); track key) {\n <div>\n <p class=\"px-2\">{{ `userProfile.data.${key}` | transloco }}</p>\n <div\n class=\"group flex flex-row rounded-md px-2 py-0.5\"\n [class.ring-1]=\"!!fieldState('data.' + key)\"\n [class.ring-destructive]=\"fieldState('data.' + key) === 'error'\"\n [class.ring-green-600]=\"\n fieldState('data.' + key) === 'success'\n \">\n <div\n class=\"min-w-0 grow break-words whitespace-pre-line\"\n [class.text-muted-foreground]=\"!getDataValue('data', key)\">\n @if (propertyToEdit() === \"data.\" + key) {\n <textarea\n class=\"hover:outline-primary focus:outline-primary border-foreground/20 hover:bg-muted focus:bg-muted mt-2 w-full rounded-md border px-2 pt-1 hover:outline focus:outline\"\n id=\"user-profile-{{ key }}\"\n [(ngModel)]=\"value\"></textarea>\n } @else {\n {{\n getDataValue(\"data\", key) ||\n (\"userProfile.notDefined\" | transloco)\n }}\n }\n </div>\n @if (propertyToEdit() === \"data.\" + key) {\n <button\n variant=\"ghost\"\n [iconOnly]=\"true\"\n class=\"visible mx-2 cursor-pointer sm:invisible sm:group-hover:visible\"\n title=\"{{ 'userProfile.editProperty' | transloco }}\"\n [attr.aria-label]=\"'userProfile.editProperty' | transloco\"\n [disabled]=\"!!pending()\"\n (click)=\"onSaveData('data', key)\">\n <save-icon class=\"ml-2\" />\n </button>\n <button\n variant=\"ghost\"\n [iconOnly]=\"true\"\n class=\"visible cursor-pointer sm:invisible sm:group-hover:visible\"\n title=\"{{ 'userProfile.editProperty' | transloco }}\"\n [attr.aria-label]=\"'userProfile.editProperty' | transloco\"\n (click)=\"onCancelEdit()\">\n <UndoIcon />\n </button>\n } @else {\n <button\n variant=\"ghost\"\n [iconOnly]=\"true\"\n class=\"visible mx-2 cursor-pointer sm:invisible sm:group-hover:visible\"\n title=\"{{ 'userProfile.editProperty' | transloco }}\"\n [attr.aria-label]=\"'userProfile.editProperty' | transloco\"\n (click)=\"onEdit('data', key)\">\n <EditIcon />\n </button>\n <button\n variant=\"ghost\"\n [iconOnly]=\"true\"\n class=\"text-destructive visible cursor-pointer sm:invisible sm:group-hover:visible\"\n title=\"{{ 'userProfile.deleteProperty' | transloco }}\"\n [attr.aria-label]=\"'userProfile.deleteProperty' | transloco\"\n [disabled]=\"!!pending()\"\n (click)=\"onDeleteData('data', key)\">\n <TrashIcon />\n </button>\n }\n </div>\n </div>\n }\n @for (key of customData(); track key) {\n <div>\n <p class=\"px-2\">{{ key }}</p>\n <div\n class=\"group flex flex-row rounded-md px-2 py-0.5\"\n [class.ring-1]=\"!!fieldState('customData.' + key)\"\n [class.ring-destructive]=\"\n fieldState('customData.' + key) === 'error'\n \"\n [class.ring-green-600]=\"\n fieldState('customData.' + key) === 'success'\n \">\n <div\n class=\"min-w-0 grow break-words whitespace-pre-line\"\n [class.text-muted-foreground]=\"\n !getDataValue('customData', key)\n \">\n @if (propertyToEdit() === \"customData.\" + key) {\n <textarea\n class=\"hover:outline-primary focus:outline-primary border-foreground/20 hover:bg-muted focus:bg-muted mt-2 w-full rounded-md border px-2 pt-1 hover:outline focus:outline\"\n id=\"user-profile-{{ key }}\"\n [(ngModel)]=\"value\"></textarea>\n } @else {\n {{\n getDataValue(\"customData\", key) ||\n (\"userProfile.notDefined\" | transloco)\n }}\n }\n </div>\n @if (propertyToEdit() === \"customData.\" + key) {\n <button\n variant=\"ghost\"\n [iconOnly]=\"true\"\n class=\"visible mx-2 cursor-pointer sm:invisible sm:group-hover:visible\"\n title=\"{{ 'userProfile.editProperty' | transloco }}\"\n [attr.aria-label]=\"'userProfile.editProperty' | transloco\"\n [disabled]=\"!!pending()\"\n (click)=\"onSaveData('customData', key)\">\n <save-icon class=\"ml-2\" />\n </button>\n <button\n variant=\"ghost\"\n [iconOnly]=\"true\"\n class=\"visible cursor-pointer sm:invisible sm:group-hover:visible\"\n title=\"{{ 'userProfile.editProperty' | transloco }}\"\n [attr.aria-label]=\"'userProfile.editProperty' | transloco\"\n (click)=\"onCancelEdit()\">\n <UndoIcon />\n </button>\n } @else {\n <button\n variant=\"ghost\"\n [iconOnly]=\"true\"\n class=\"visible mx-2 cursor-pointer sm:invisible sm:group-hover:visible\"\n title=\"{{ 'userProfile.editProperty' | transloco }}\"\n [attr.aria-label]=\"'userProfile.editProperty' | transloco\"\n (click)=\"onEdit('customData', key)\">\n <EditIcon />\n </button>\n <button\n variant=\"ghost\"\n [iconOnly]=\"true\"\n class=\"text-destructive visible cursor-pointer sm:invisible sm:group-hover:visible\"\n title=\"{{ 'userProfile.deleteProperty' | transloco }}\"\n [attr.aria-label]=\"'userProfile.deleteProperty' | transloco\"\n [disabled]=\"!!pending()\"\n (click)=\"onDeleteData('customData', key)\">\n <TrashIcon />\n </button>\n }\n </div>\n </div>\n }\n </div>\n </div>\n }\n </div>\n} @else if (userProfileResource.isLoading()) {\n <span>Loading...</span>\n} @else if (userProfileResource.error()) {\n <p>Please contact an administrator to create your user profile.</p>\n}\n\n<!-- AVATAR UPLOAD -->\n<input\n #avatarInput\n class=\"hidden\"\n type=\"file\"\n accept=\"image/*\"\n (change)=\"uploadAvatar($event)\" />\n" }]
|
|
18865
|
+
], providers: [provideTranslocoScope("user-profile", "login")], template: "@if (userProfileResource.hasValue()) {\n @let profile = userProfile();\n <div class=\"flex min-h-0 flex-1 flex-col gap-2 p-4\">\n <div class=\"mb-4 flex\">\n @if (allowProfilePhoto()) {\n <!-- AVATAR -->\n <Avatar\n class=\"size-14 cursor-pointer\"\n [class.ring-2]=\"!!fieldState('data.profilePhoto')\"\n [class.ring-destructive]=\"fieldState('data.profilePhoto') === 'error'\"\n [class.ring-green-600]=\"fieldState('data.profilePhoto') === 'success'\"\n (click)=\"avatarInput.click()\">\n @if (profile?.data?.profilePhoto) {\n <AvatarImage\n [src]=\"profile?.data?.profilePhoto!\"\n width=\"44\"\n height=\"44\"\n alt=\"avatar\" />\n }\n <AvatarFallback class=\"text-lg\">\n @if (initials()) {\n <span>{{ initials() }}</span>\n } @else {\n <UserIcon class=\"size-7\" />\n }\n </AvatarFallback>\n </Avatar>\n @if (profile?.data?.profilePhoto) {\n <button\n variant=\"ghost\"\n [iconOnly]=\"true\"\n class=\"text-destructive cursor-pointer\"\n title=\"{{ 'userProfile.deleteProperty' | transloco }}\"\n [attr.aria-label]=\"'userProfile.deleteProperty' | transloco\"\n [disabled]=\"!!pending()\"\n (click)=\"onDeleteData('data', 'profilePhoto')\">\n <TrashIcon />\n </button>\n }\n }\n <div class=\"grow\"></div>\n <div class=\"flex flex-col gap-4\">\n <!-- LANGUAGE -->\n <select\n class=\"hover:outline-primary focus:outline-primary border-foreground/10 bg-background hover:bg-muted focus:bg-muted h-8 rounded-md border px-2 hover:outline focus:outline\"\n [(ngModel)]=\"currentLanguage\"\n (change)=\"changeLanguage()\">\n @for (lang of AllLanguages; track lang.code) {\n <option [value]=\"lang.code\">\n {{ lang.label }}\n </option>\n }\n </select>\n <!-- CHANGE PASSWORD -->\n @if (allowChangePassword()) {\n <button\n [disabled]=\"changingPassword()\"\n (click)=\"changingPassword.set(true)\">\n {{ \"login.changePassword\" | transloco }}\n </button>\n }\n </div>\n </div>\n <!-- always rendered, so the room is reserved and the layout doesn't shift when a message appears -->\n <p\n class=\"min-h-5 text-sm\"\n role=\"status\"\n [attr.aria-live]=\"feedback()?.type === 'error' ? 'assertive' : 'polite'\"\n [class.text-destructive]=\"feedback()?.type === 'error'\"\n [class.text-green-600]=\"feedback()?.type === 'success'\">\n {{ feedback()?.message }}\n </p>\n @if (changingPassword()) {\n <ChangePassword\n [username]=\"username()\"\n [redirectAfterSuccess]=\"false\"\n [redirectAfterCancel]=\"false\"\n (cancel)=\"changingPassword.set(false)\"\n (success)=\"changingPassword.set(false)\" />\n } @else {\n <!-- -m-1 p-1: room for the outline of a field, which the scroll container would otherwise clip -->\n <div class=\"-m-1 min-h-0 flex-1 overflow-auto p-1\">\n <!-- two columns from the sm breakpoint; a structured value or the JSON editor takes the full width -->\n <div\n class=\"grid grid-cols-1 items-start gap-x-6 gap-y-2 sm:grid-cols-2\">\n @for (field of fields(); track field.category + \".\" + field.key) {\n @let id = field.category + \".\" + field.key;\n @let isPrivate = field.category === \"privateCustomData\";\n <div\n [class]=\"\n isStructuredValue(field.category, field.key) ||\n (propertyToEdit() === id && editingJson())\n ? 'min-w-0 sm:col-span-2'\n : 'min-w-0'\n \">\n <p class=\"flex items-center gap-2 px-2\">\n <span>\n @if (field.category === \"data\") {\n {{ `userProfile.data.${field.key}` | transloco }}\n } @else {\n {{ field.key.split(\"/\").join(\" \u203A \") }}\n }\n </span>\n <!-- PRIVATE / PUBLIC: icon AND text, so the information is not carried by the icon or a color alone -->\n <span\n class=\"text-muted-foreground inline-flex items-center gap-1 text-xs\"\n [attr.data-visibility]=\"isPrivate ? 'private' : 'public'\"\n [title]=\"\n (isPrivate\n ? 'userProfile.privateHint'\n : 'userProfile.publicHint'\n ) | transloco\n \">\n @if (isPrivate) {\n <user-secret-icon class=\"size-3.5 shrink-0\" />\n } @else {\n <globe-icon class=\"size-3.5 shrink-0\" />\n }\n <span>{{\n (isPrivate ? \"userProfile.private\" : \"userProfile.public\")\n | transloco\n }}</span>\n <span class=\"sr-only\">\n {{\n (isPrivate\n ? \"userProfile.privateHint\"\n : \"userProfile.publicHint\"\n ) | transloco\n }}\n </span>\n </span>\n </p>\n <div\n class=\"group flex flex-row rounded-md px-2 py-0.5\"\n [class.ring-1]=\"!!fieldState(id)\"\n [class.ring-destructive]=\"fieldState(id) === 'error'\"\n [class.ring-green-600]=\"fieldState(id) === 'success'\">\n <div\n class=\"min-w-0 grow break-words whitespace-pre-line\"\n [class.text-muted-foreground]=\"\n getRawValue(field.category, field.key) == null\n \">\n @if (propertyToEdit() === id) {\n @if (editingJson()) {\n <!-- an array or an object is edited as JSON, on several lines -->\n <textarea\n class=\"hover:outline-primary focus:outline-primary border-foreground/20 hover:bg-muted focus:bg-muted mt-2 w-full rounded-md border px-2 pt-1 font-mono text-sm hover:outline focus:outline\"\n rows=\"8\"\n [attr.aria-invalid]=\"invalidJson() || null\"\n [attr.aria-describedby]=\"\n invalidJson() ? 'user-profile-json-error' : null\n \"\n id=\"user-profile-{{ field.category }}-{{ field.key }}\"\n [(ngModel)]=\"value\"></textarea>\n } @else {\n <input\n type=\"text\"\n class=\"hover:outline-primary focus:outline-primary border-foreground/20 hover:bg-muted focus:bg-muted mt-2 h-8 w-full rounded-md border px-2 hover:outline focus:outline\"\n id=\"user-profile-{{ field.category }}-{{ field.key }}\"\n [(ngModel)]=\"value\" />\n }\n @if (invalidJson()) {\n <p\n id=\"user-profile-json-error\"\n class=\"text-destructive text-sm\">\n {{ \"userProfile.invalidJson\" | transloco }}\n </p>\n }\n } @else if (isStructuredValue(field.category, field.key)) {\n <json-value-view\n [value]=\"getRawValue(field.category, field.key)\" />\n } @else {\n {{\n getDataValue(field.category, field.key) ??\n (\"userProfile.notDefined\" | transloco)\n }}\n }\n </div>\n @if (propertyToEdit() === id) {\n <button\n variant=\"ghost\"\n [iconOnly]=\"true\"\n class=\"visible mx-2 cursor-pointer sm:invisible sm:group-hover:visible\"\n title=\"{{ 'userProfile.editProperty' | transloco }}\"\n [attr.aria-label]=\"'userProfile.editProperty' | transloco\"\n [disabled]=\"!!pending() || invalidJson()\"\n (click)=\"onSaveData(field.category, field.key)\">\n <save-icon class=\"ml-2\" />\n </button>\n <button\n variant=\"ghost\"\n [iconOnly]=\"true\"\n class=\"visible cursor-pointer sm:invisible sm:group-hover:visible\"\n title=\"{{ 'userProfile.editProperty' | transloco }}\"\n [attr.aria-label]=\"'userProfile.editProperty' | transloco\"\n (click)=\"onCancelEdit()\">\n <UndoIcon />\n </button>\n } @else {\n <button\n variant=\"ghost\"\n [iconOnly]=\"true\"\n class=\"visible mx-2 cursor-pointer sm:invisible sm:group-hover:visible\"\n title=\"{{ 'userProfile.editProperty' | transloco }}\"\n [attr.aria-label]=\"'userProfile.editProperty' | transloco\"\n (click)=\"onEdit(field.category, field.key)\">\n <EditIcon />\n </button>\n <!-- nothing stored, nothing to delete: the button is not offered rather than shown disabled -->\n @if (hasStoredValue(field.category, field.key)) {\n <button\n variant=\"ghost\"\n [iconOnly]=\"true\"\n class=\"text-destructive visible cursor-pointer sm:invisible sm:group-hover:visible\"\n title=\"{{ 'userProfile.deleteProperty' | transloco }}\"\n [attr.aria-label]=\"\n 'userProfile.deleteProperty' | transloco\n \"\n [disabled]=\"!!pending()\"\n (click)=\"onDeleteData(field.category, field.key)\">\n <TrashIcon />\n </button>\n }\n }\n </div>\n </div>\n }\n </div>\n </div>\n }\n </div>\n} @else if (userProfileResource.isLoading()) {\n <span>Loading...</span>\n} @else if (userProfileResource.error()) {\n <p>Please contact an administrator to create your user profile.</p>\n}\n\n<!-- AVATAR UPLOAD -->\n<input\n #avatarInput\n class=\"hidden\"\n type=\"file\"\n accept=\"image/*\"\n (change)=\"uploadAvatar($event)\" />\n" }]
|
|
18548
18866
|
}], ctorParameters: () => [], propDecorators: { createInputElement: [{ type: i0.ViewChild, args: ["avatarInput", { isSignal: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], currentLanguage: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentLanguage", required: false }] }, { type: i0.Output, args: ["currentLanguageChange"] }] } });
|
|
18549
18867
|
|
|
18550
18868
|
class UserProfileDialog {
|
|
@@ -18557,7 +18875,7 @@ class UserProfileDialog {
|
|
|
18557
18875
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: UserProfileDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
18558
18876
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.29", type: UserProfileDialog, isStandalone: true, selector: "user-profile-dialog, userprofiledialog, UserProfileDialog", providers: [provideTranslocoScope("user-profile")], viewQueries: [{ propertyName: "dialog", first: true, predicate: DialogComponent, descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
18559
18877
|
<div dialog #dialog="dialog">
|
|
18560
|
-
<DialogContent class="flex flex-col max-h-11/12 overflow-hidden">
|
|
18878
|
+
<DialogContent class="flex flex-col max-h-11/12 overflow-hidden sm:max-w-3xl">
|
|
18561
18879
|
<DialogHeader>
|
|
18562
18880
|
<DialogTitle>{{ 'userProfile.title' | transloco }}</DialogTitle>
|
|
18563
18881
|
</DialogHeader>
|
|
@@ -18574,18 +18892,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImpo
|
|
|
18574
18892
|
args: [{
|
|
18575
18893
|
selector: "user-profile-dialog, userprofiledialog, UserProfileDialog",
|
|
18576
18894
|
standalone: true,
|
|
18577
|
-
imports: [
|
|
18578
|
-
DialogComponent,
|
|
18579
|
-
DialogHeaderComponent,
|
|
18580
|
-
DialogContentComponent,
|
|
18581
|
-
UserProfileFormComponent,
|
|
18582
|
-
DialogTitleComponent,
|
|
18583
|
-
TranslocoPipe
|
|
18584
|
-
],
|
|
18895
|
+
imports: [DialogComponent, DialogHeaderComponent, DialogContentComponent, UserProfileFormComponent, DialogTitleComponent, TranslocoPipe],
|
|
18585
18896
|
providers: [provideTranslocoScope("user-profile")],
|
|
18586
18897
|
template: `
|
|
18587
18898
|
<div dialog #dialog="dialog">
|
|
18588
|
-
<DialogContent class="flex flex-col max-h-11/12 overflow-hidden">
|
|
18899
|
+
<DialogContent class="flex flex-col max-h-11/12 overflow-hidden sm:max-w-3xl">
|
|
18589
18900
|
<DialogHeader>
|
|
18590
18901
|
<DialogTitle>{{ 'userProfile.title' | transloco }}</DialogTitle>
|
|
18591
18902
|
</DialogHeader>
|