@api-client/ui 0.1.3 → 0.1.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.
- package/build/src/core/Model.d.ts +67 -0
- package/build/src/core/Model.d.ts.map +1 -1
- package/build/src/core/Model.js +89 -0
- package/build/src/core/Model.js.map +1 -1
- package/build/src/elements/user/internals/UserAvatar.d.ts +20 -7
- package/build/src/elements/user/internals/UserAvatar.d.ts.map +1 -1
- package/build/src/elements/user/internals/UserAvatar.js +66 -30
- package/build/src/elements/user/internals/UserAvatar.js.map +1 -1
- package/build/src/elements/user/internals/UserAvatar.styles.d.ts.map +1 -1
- package/build/src/elements/user/internals/UserAvatar.styles.js +17 -11
- package/build/src/elements/user/internals/UserAvatar.styles.js.map +1 -1
- package/demo/elements/index.html +2 -2
- package/demo/elements/user/user-avatar.html +17 -0
- package/demo/elements/user/user-avatar.ts +56 -0
- package/package.json +21 -2
- package/src/core/Model.ts +119 -0
- package/src/elements/user/internals/UserAvatar.styles.ts +17 -11
- package/src/elements/user/internals/UserAvatar.ts +58 -28
- package/tsconfig.browser.json +1 -0
|
@@ -1,3 +1,43 @@
|
|
|
1
|
+
import type { Exception } from '@api-client/core/exceptions/exception.js';
|
|
2
|
+
/**
|
|
3
|
+
* Defines the options for the retry mechanism.
|
|
4
|
+
*/
|
|
5
|
+
export interface RetryOptions {
|
|
6
|
+
/**
|
|
7
|
+
* The maximum number of retry attempts.
|
|
8
|
+
* Defaults to 3.
|
|
9
|
+
*/
|
|
10
|
+
retries?: number;
|
|
11
|
+
/**
|
|
12
|
+
* The delay in milliseconds before the next retry.
|
|
13
|
+
* Can be a fixed number or a function that returns a number (e.g., for exponential backoff).
|
|
14
|
+
* Defaults to 1000ms.
|
|
15
|
+
*/
|
|
16
|
+
delayMs?: number | ((attempt: number) => number);
|
|
17
|
+
/**
|
|
18
|
+
* An optional function that determines if a retry should be attempted based on the error.
|
|
19
|
+
* If not provided, all errors will trigger a retry up to the maximum number of retries.
|
|
20
|
+
* @param error The error that occurred.
|
|
21
|
+
* @returns True if a retry should be attempted, false otherwise.
|
|
22
|
+
*/
|
|
23
|
+
shouldRetry?: (error: Error) => boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Calculates an exponential backoff delay for retry attempts.
|
|
27
|
+
* The delay doubles with each attempt.
|
|
28
|
+
*
|
|
29
|
+
* @param attempt The current retry attempt number (should be 1-indexed).
|
|
30
|
+
* @param initialDelay The initial delay in milliseconds. Defaults to 1000ms.
|
|
31
|
+
* @returns The calculated delay in milliseconds.
|
|
32
|
+
*/
|
|
33
|
+
export declare function exponentialBackoffDelay(attempt: number, initialDelay?: number): number;
|
|
34
|
+
/**
|
|
35
|
+
* Checks if the API error should be retried based on its status code.
|
|
36
|
+
* If the status code is 404, it returns false (do not retry).
|
|
37
|
+
* For other status codes, it returns true (retry).
|
|
38
|
+
* @param error The error to check.
|
|
39
|
+
*/
|
|
40
|
+
export declare function shouldRetryApiError(error: Exception): boolean;
|
|
1
41
|
/**
|
|
2
42
|
* The base class for models.
|
|
3
43
|
*
|
|
@@ -36,6 +76,10 @@ export declare abstract class Model<T> extends EventTarget {
|
|
|
36
76
|
* @type A promise resolved when the debounced task finished.
|
|
37
77
|
*/
|
|
38
78
|
get taskComplete(): Promise<void> | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* A getter for the raw value.
|
|
81
|
+
*/
|
|
82
|
+
get value(): T | undefined;
|
|
39
83
|
/**
|
|
40
84
|
* @param obj The source object to use.
|
|
41
85
|
*/
|
|
@@ -53,5 +97,28 @@ export declare abstract class Model<T> extends EventTarget {
|
|
|
53
97
|
debounce(callback: (...args: unknown[]) => unknown): void;
|
|
54
98
|
notifyError(error: Error): void;
|
|
55
99
|
protected resolveTaskPromise(): void;
|
|
100
|
+
/**
|
|
101
|
+
* Executes an asynchronous operation with a retry mechanism.
|
|
102
|
+
* @param operation A function that returns a Promise to be executed.
|
|
103
|
+
* @param options Optional retry configurations.
|
|
104
|
+
* @returns A Promise that resolves with the result of the operation or rejects if all retries fail.
|
|
105
|
+
* @example
|
|
106
|
+
* ```typescript
|
|
107
|
+
* override async create(data: Partial<IUser>): Promise<IUser> {
|
|
108
|
+
* const operation = async (): Promise<UserData> => {
|
|
109
|
+
* // ... perform API call to create user
|
|
110
|
+
* }
|
|
111
|
+
* const retryOptions: RetryOptions = {
|
|
112
|
+
* retries: 5,
|
|
113
|
+
* delayMs: exponentialBackoffDelay,
|
|
114
|
+
* shouldRetry: shouldRetryApiError,
|
|
115
|
+
* }
|
|
116
|
+
* this.raw = await this.retry(operation)
|
|
117
|
+
* this.dispatchEvent(new CustomEvent('data-changed', { detail: this.raw }))
|
|
118
|
+
* return this.raw;
|
|
119
|
+
* }
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
protected retry<R>(operation: () => Promise<R>, options?: RetryOptions): Promise<R>;
|
|
56
123
|
}
|
|
57
124
|
//# sourceMappingURL=Model.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Model.d.ts","sourceRoot":"","sources":["../../../src/core/Model.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Model.d.ts","sourceRoot":"","sources":["../../../src/core/Model.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0CAA0C,CAAA;AAEzE;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC,CAAA;IAChD;;;;;OAKG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAA;CACxC;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,SAAO,GAAG,MAAM,CAIpF;AACD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAO7D;AACD;;;;;;;;;GASG;AACH,8BAAsB,KAAK,CAAC,CAAC,CAAE,SAAQ,WAAW;;IAChD;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,CAAC,CAAA;IAEP;;;;OAIG;IACH,eAAe,SAAM;IAErB;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,OAAO,CAAA;IAExC;;OAEG;IACH,KAAK,SAAK;IAiBV;;OAEG;IACH,IAAI,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,SAAS,CAE5C;IAED;;OAEG;IACH,IAAI,KAAK,IAAI,CAAC,GAAG,SAAS,CAEzB;IAED;;OAEG;gBACS,GAAG,CAAC,EAAE,CAAC;IAMnB;;;;;;;;OAQG;IACH,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAIpC,QAAQ,CAAC,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,GAAG,IAAI;IAqBzD,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAe/B,SAAS,CAAC,kBAAkB,IAAI,IAAI;IAWpC;;;;;;;;;;;;;;;;;;;;;OAqBG;cACa,KAAK,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,CAAC,CAAC;CAmC9F"}
|
package/build/src/core/Model.js
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
2
|
+
/**
|
|
3
|
+
* Calculates an exponential backoff delay for retry attempts.
|
|
4
|
+
* The delay doubles with each attempt.
|
|
5
|
+
*
|
|
6
|
+
* @param attempt The current retry attempt number (should be 1-indexed).
|
|
7
|
+
* @param initialDelay The initial delay in milliseconds. Defaults to 1000ms.
|
|
8
|
+
* @returns The calculated delay in milliseconds.
|
|
9
|
+
*/
|
|
10
|
+
export function exponentialBackoffDelay(attempt, initialDelay = 1000) {
|
|
11
|
+
if (attempt <= 0)
|
|
12
|
+
return initialDelay;
|
|
13
|
+
// For attempt 1, delay is initialDelay * 2^0 = initialDelay
|
|
14
|
+
return initialDelay * Math.pow(2, attempt - 1);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Checks if the API error should be retried based on its status code.
|
|
18
|
+
* If the status code is 404, it returns false (do not retry).
|
|
19
|
+
* For other status codes, it returns true (retry).
|
|
20
|
+
* @param error The error to check.
|
|
21
|
+
*/
|
|
22
|
+
export function shouldRetryApiError(error) {
|
|
23
|
+
const { status } = error;
|
|
24
|
+
if (status === 404) {
|
|
25
|
+
// Do not retry on 404 errors
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
2
30
|
/**
|
|
3
31
|
* The base class for models.
|
|
4
32
|
*
|
|
@@ -50,6 +78,12 @@ export class Model extends EventTarget {
|
|
|
50
78
|
get taskComplete() {
|
|
51
79
|
return this.#taskComplete;
|
|
52
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* A getter for the raw value.
|
|
83
|
+
*/
|
|
84
|
+
get value() {
|
|
85
|
+
return this.raw;
|
|
86
|
+
}
|
|
53
87
|
/**
|
|
54
88
|
* @param obj The source object to use.
|
|
55
89
|
*/
|
|
@@ -113,5 +147,60 @@ export class Model extends EventTarget {
|
|
|
113
147
|
resolver();
|
|
114
148
|
}
|
|
115
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Executes an asynchronous operation with a retry mechanism.
|
|
152
|
+
* @param operation A function that returns a Promise to be executed.
|
|
153
|
+
* @param options Optional retry configurations.
|
|
154
|
+
* @returns A Promise that resolves with the result of the operation or rejects if all retries fail.
|
|
155
|
+
* @example
|
|
156
|
+
* ```typescript
|
|
157
|
+
* override async create(data: Partial<IUser>): Promise<IUser> {
|
|
158
|
+
* const operation = async (): Promise<UserData> => {
|
|
159
|
+
* // ... perform API call to create user
|
|
160
|
+
* }
|
|
161
|
+
* const retryOptions: RetryOptions = {
|
|
162
|
+
* retries: 5,
|
|
163
|
+
* delayMs: exponentialBackoffDelay,
|
|
164
|
+
* shouldRetry: shouldRetryApiError,
|
|
165
|
+
* }
|
|
166
|
+
* this.raw = await this.retry(operation)
|
|
167
|
+
* this.dispatchEvent(new CustomEvent('data-changed', { detail: this.raw }))
|
|
168
|
+
* return this.raw;
|
|
169
|
+
* }
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
async retry(operation, options = {}) {
|
|
173
|
+
const { retries = 3, delayMs = 1000, shouldRetry = () => true } = options;
|
|
174
|
+
let lastError;
|
|
175
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
176
|
+
try {
|
|
177
|
+
return await operation();
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
lastError = e;
|
|
181
|
+
if (!shouldRetry(lastError) || attempt === retries - 1) {
|
|
182
|
+
// Do not retry if shouldRetry returns false or if it's the last attempt
|
|
183
|
+
this.notifyError(lastError);
|
|
184
|
+
throw lastError;
|
|
185
|
+
}
|
|
186
|
+
const delay = typeof delayMs === 'function' ? delayMs(attempt + 1) : delayMs;
|
|
187
|
+
if (delay > 0) {
|
|
188
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// This part should ideally not be reached if retries > 0,
|
|
193
|
+
// as the loop would throw on the last attempt.
|
|
194
|
+
// However, to satisfy TypeScript and guard against retries = 0:
|
|
195
|
+
if (lastError) {
|
|
196
|
+
this.notifyError(lastError);
|
|
197
|
+
throw lastError;
|
|
198
|
+
}
|
|
199
|
+
// Should not happen if operation is called at least once.
|
|
200
|
+
// Adding a generic error for completeness.
|
|
201
|
+
const err = new Error('Retry operation failed without a specific error.');
|
|
202
|
+
this.notifyError(err);
|
|
203
|
+
throw err;
|
|
204
|
+
}
|
|
116
205
|
}
|
|
117
206
|
//# sourceMappingURL=Model.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Model.js","sourceRoot":"","sources":["../../../src/core/Model.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD;;;;;;;;;GASG;AACH,MAAM,OAAgB,KAAS,SAAQ,WAAW;IAChD;;;;;;OAMG;IACH,GAAG,CAAI;IAEP;;;;OAIG;IACH,eAAe,GAAG,GAAG,CAAA;IAErB;;OAEG;IACH,cAAc,CAA0B;IAExC;;OAEG;IACH,KAAK,GAAG,EAAE,CAAA;IAEV;;OAEG;IACH,sBAAsB,GAAG,KAAK,CAAA;IAE9B;;OAEG;IACH,aAAa,CAAgB;IAE7B;;OAEG;IACH,aAAa,CAAa;IAE1B;;OAEG;IACH,IAAI,YAAY;QACd,OAAO,IAAI,CAAC,aAAa,CAAA;IAC3B,CAAC;IAED;;OAEG;IACH,YAAY,GAAO;QACjB,KAAK,EAAE,CAAA;QACP,IAAI,CAAC,GAAG,GAAG,GAAG,CAAA;QACd,IAAI,CAAC,iBAAiB,EAAE,CAAA;IAC1B,CAAC;IAED;;;;;;;;OAQG;IACH,MAAM,CAAC,IAAgB;QACrB,MAAM,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAA;IACrC,CAAC;IAED,QAAQ,CAAC,QAAyC;QAChD,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACxB,YAAY,CAAC,IAAI,CAAC,cAAc,CAAC,CAAA;QACnC,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,sBAAsB,EAAE,CAAC;YACjC,IAAI,CAAC,iBAAiB,EAAE,CAAA;QAC1B,CAAC;QACD,IAAI,CAAC,cAAc,GAAG,UAAU,CAAC,KAAK,IAAI,EAAE;YAC1C,IAAI,CAAC,cAAc,GAAG,SAAS,CAAA;YAC/B,IAAI,CAAC;gBACH,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YAC3B,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,MAAM,GAAG,GAAG,CAAU,CAAA;gBACtB,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAA;gBACrB,MAAM,GAAG,CAAA;YACX,CAAC;oBAAS,CAAC;gBACT,IAAI,CAAC,kBAAkB,EAAE,CAAA;YAC3B,CAAC;QACH,CAAC,EAAE,IAAI,CAAC,eAAe,CAAC,CAAA;IAC1B,CAAC;IAED,WAAW,CAAC,KAAY;QACtB,IAAI,CAAC,aAAa,CAChB,IAAI,WAAW,CAAQ,OAAO,EAAE;YAC9B,MAAM,EAAE,KAAK;SACd,CAAC,CACH,CAAA;IACH,CAAC;IAED,iBAAiB;QACf,IAAI,CAAC,aAAa,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;YACjD,IAAI,CAAC,aAAa,GAAG,OAAO,CAAA;YAC5B,IAAI,CAAC,sBAAsB,GAAG,IAAI,CAAA;QACpC,CAAC,CAAC,CAAA;IACJ,CAAC;IAES,kBAAkB;QAC1B,IAAI,CAAC,IAAI,CAAC,sBAAsB,EAAE,CAAC;YACjC,OAAM;QACR,CAAC;QACD,IAAI,CAAC,sBAAsB,GAAG,KAAK,CAAA;QACnC,MAAM,QAAQ,GAAG,IAAI,CAAC,aAAa,CAAA;QACnC,IAAI,QAAQ,EAAE,CAAC;YACb,QAAQ,EAAE,CAAA;QACZ,CAAC;IACH,CAAC;CACF","sourcesContent":["/* eslint-disable @typescript-eslint/no-unused-vars */\n/**\n * The base class for models.\n *\n * Models wrap a logic related to manipulating model data, querying for data from the API,\n * retry logic, cashing, refreshing of data, offline storage, pooling endpoints for new data, etc.\n *\n * The application should use these models in order to centralize object manipulation.\n *\n * @template T The underlying domain model.\n */\nexport abstract class Model<T> extends EventTarget {\n /**\n * The underlying raw object.\n * Do not modify properties of the object directly.\n * Use one of the methods provided by the model.\n * Direct changes will not be reflected in the UI\n * and may cause issues when processing the object.\n */\n raw?: T\n\n /**\n * The timeout for the query debouncer.\n * When any property change this is the time the element will wait\n * until the actual query is made.\n */\n debounceTimeout = 100\n\n /**\n * A reference to the current debouncer.\n */\n debouncerValue?: number | NodeJS.Timeout\n\n /**\n * Default limit for list queries.\n */\n limit = 30\n\n /**\n * A flag that helps to determine whether the `taskComplete` is setup.\n */\n #hasPendingTaskPromise = false\n\n /**\n * A hidden value for the `taskComplete` getter.\n */\n #taskComplete?: Promise<void>\n\n /**\n * The resolver to call when the debounced task completes.\n */\n #taskResolver?: () => void\n\n /**\n * @type A promise resolved when the debounced task finished.\n */\n get taskComplete(): Promise<void> | undefined {\n return this.#taskComplete\n }\n\n /**\n * @param obj The source object to use.\n */\n constructor(obj?: T) {\n super()\n this.raw = obj\n this.#setUpdatePromise()\n }\n\n /**\n * Creates a new instance of the data. It often invokes API call.\n * This is what happens after the user triggers \"create new\" flow.\n *\n * After this method is calls, the `raw` object is set.\n *\n * @param data The partial data to create.\n * @returns The created object.\n */\n create(data: Partial<T>): Promise<T> {\n throw new Error(`Not implemented.`)\n }\n\n debounce(callback: (...args: unknown[]) => unknown): void {\n if (this.debouncerValue) {\n clearTimeout(this.debouncerValue)\n }\n if (!this.#hasPendingTaskPromise) {\n this.#setUpdatePromise()\n }\n this.debouncerValue = setTimeout(async () => {\n this.debouncerValue = undefined\n try {\n await callback.call(this)\n } catch (e) {\n const err = e as Error\n this.notifyError(err)\n throw err\n } finally {\n this.resolveTaskPromise()\n }\n }, this.debounceTimeout)\n }\n\n notifyError(error: Error): void {\n this.dispatchEvent(\n new CustomEvent<Error>('error', {\n detail: error,\n })\n )\n }\n\n #setUpdatePromise(): void {\n this.#taskComplete = new Promise<void>((resolve) => {\n this.#taskResolver = resolve\n this.#hasPendingTaskPromise = true\n })\n }\n\n protected resolveTaskPromise(): void {\n if (!this.#hasPendingTaskPromise) {\n return\n }\n this.#hasPendingTaskPromise = false\n const resolver = this.#taskResolver\n if (resolver) {\n resolver()\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"Model.js","sourceRoot":"","sources":["../../../src/core/Model.ts"],"names":[],"mappings":"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","sourcesContent":["/* eslint-disable @typescript-eslint/no-unused-vars */\n\nimport type { Exception } from '@api-client/core/exceptions/exception.js'\n\n/**\n * Defines the options for the retry mechanism.\n */\nexport interface RetryOptions {\n /**\n * The maximum number of retry attempts.\n * Defaults to 3.\n */\n retries?: number\n /**\n * The delay in milliseconds before the next retry.\n * Can be a fixed number or a function that returns a number (e.g., for exponential backoff).\n * Defaults to 1000ms.\n */\n delayMs?: number | ((attempt: number) => number)\n /**\n * An optional function that determines if a retry should be attempted based on the error.\n * If not provided, all errors will trigger a retry up to the maximum number of retries.\n * @param error The error that occurred.\n * @returns True if a retry should be attempted, false otherwise.\n */\n shouldRetry?: (error: Error) => boolean\n}\n\n/**\n * Calculates an exponential backoff delay for retry attempts.\n * The delay doubles with each attempt.\n *\n * @param attempt The current retry attempt number (should be 1-indexed).\n * @param initialDelay The initial delay in milliseconds. Defaults to 1000ms.\n * @returns The calculated delay in milliseconds.\n */\nexport function exponentialBackoffDelay(attempt: number, initialDelay = 1000): number {\n if (attempt <= 0) return initialDelay\n // For attempt 1, delay is initialDelay * 2^0 = initialDelay\n return initialDelay * Math.pow(2, attempt - 1)\n}\n/**\n * Checks if the API error should be retried based on its status code.\n * If the status code is 404, it returns false (do not retry).\n * For other status codes, it returns true (retry).\n * @param error The error to check.\n */\nexport function shouldRetryApiError(error: Exception): boolean {\n const { status } = error\n if (status === 404) {\n // Do not retry on 404 errors\n return false\n }\n return true\n}\n/**\n * The base class for models.\n *\n * Models wrap a logic related to manipulating model data, querying for data from the API,\n * retry logic, cashing, refreshing of data, offline storage, pooling endpoints for new data, etc.\n *\n * The application should use these models in order to centralize object manipulation.\n *\n * @template T The underlying domain model.\n */\nexport abstract class Model<T> extends EventTarget {\n /**\n * The underlying raw object.\n * Do not modify properties of the object directly.\n * Use one of the methods provided by the model.\n * Direct changes will not be reflected in the UI\n * and may cause issues when processing the object.\n */\n raw?: T\n\n /**\n * The timeout for the query debouncer.\n * When any property change this is the time the element will wait\n * until the actual query is made.\n */\n debounceTimeout = 100\n\n /**\n * A reference to the current debouncer.\n */\n debouncerValue?: number | NodeJS.Timeout\n\n /**\n * Default limit for list queries.\n */\n limit = 30\n\n /**\n * A flag that helps to determine whether the `taskComplete` is setup.\n */\n #hasPendingTaskPromise = false\n\n /**\n * A hidden value for the `taskComplete` getter.\n */\n #taskComplete?: Promise<void>\n\n /**\n * The resolver to call when the debounced task completes.\n */\n #taskResolver?: () => void\n\n /**\n * @type A promise resolved when the debounced task finished.\n */\n get taskComplete(): Promise<void> | undefined {\n return this.#taskComplete\n }\n\n /**\n * A getter for the raw value.\n */\n get value(): T | undefined {\n return this.raw\n }\n\n /**\n * @param obj The source object to use.\n */\n constructor(obj?: T) {\n super()\n this.raw = obj\n this.#setUpdatePromise()\n }\n\n /**\n * Creates a new instance of the data. It often invokes API call.\n * This is what happens after the user triggers \"create new\" flow.\n *\n * After this method is calls, the `raw` object is set.\n *\n * @param data The partial data to create.\n * @returns The created object.\n */\n create(data: Partial<T>): Promise<T> {\n throw new Error(`Not implemented.`)\n }\n\n debounce(callback: (...args: unknown[]) => unknown): void {\n if (this.debouncerValue) {\n clearTimeout(this.debouncerValue)\n }\n if (!this.#hasPendingTaskPromise) {\n this.#setUpdatePromise()\n }\n this.debouncerValue = setTimeout(async () => {\n this.debouncerValue = undefined\n try {\n await callback.call(this)\n } catch (e) {\n const err = e as Error\n this.notifyError(err)\n throw err\n } finally {\n this.resolveTaskPromise()\n }\n }, this.debounceTimeout)\n }\n\n notifyError(error: Error): void {\n this.dispatchEvent(\n new CustomEvent<Error>('error', {\n detail: error,\n })\n )\n }\n\n #setUpdatePromise(): void {\n this.#taskComplete = new Promise<void>((resolve) => {\n this.#taskResolver = resolve\n this.#hasPendingTaskPromise = true\n })\n }\n\n protected resolveTaskPromise(): void {\n if (!this.#hasPendingTaskPromise) {\n return\n }\n this.#hasPendingTaskPromise = false\n const resolver = this.#taskResolver\n if (resolver) {\n resolver()\n }\n }\n\n /**\n * Executes an asynchronous operation with a retry mechanism.\n * @param operation A function that returns a Promise to be executed.\n * @param options Optional retry configurations.\n * @returns A Promise that resolves with the result of the operation or rejects if all retries fail.\n * @example\n * ```typescript\n * override async create(data: Partial<IUser>): Promise<IUser> {\n * const operation = async (): Promise<UserData> => {\n * // ... perform API call to create user\n * }\n * const retryOptions: RetryOptions = {\n * retries: 5,\n * delayMs: exponentialBackoffDelay,\n * shouldRetry: shouldRetryApiError,\n * }\n * this.raw = await this.retry(operation)\n * this.dispatchEvent(new CustomEvent('data-changed', { detail: this.raw }))\n * return this.raw;\n * }\n * ```\n */\n protected async retry<R>(operation: () => Promise<R>, options: RetryOptions = {}): Promise<R> {\n const { retries = 3, delayMs = 1000, shouldRetry = () => true } = options\n\n let lastError: Error | undefined\n\n for (let attempt = 0; attempt < retries; attempt++) {\n try {\n return await operation()\n } catch (e) {\n lastError = e as Error\n if (!shouldRetry(lastError) || attempt === retries - 1) {\n // Do not retry if shouldRetry returns false or if it's the last attempt\n this.notifyError(lastError)\n throw lastError\n }\n\n const delay = typeof delayMs === 'function' ? delayMs(attempt + 1) : delayMs\n if (delay > 0) {\n await new Promise((resolve) => setTimeout(resolve, delay))\n }\n }\n }\n // This part should ideally not be reached if retries > 0,\n // as the loop would throw on the last attempt.\n // However, to satisfy TypeScript and guard against retries = 0:\n if (lastError) {\n this.notifyError(lastError)\n throw lastError\n }\n // Should not happen if operation is called at least once.\n // Adding a generic error for completeness.\n const err = new Error('Retry operation failed without a specific error.')\n this.notifyError(err)\n throw err\n }\n}\n"]}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { TemplateResult, LitElement } from 'lit';
|
|
1
|
+
import { TemplateResult, LitElement, PropertyValues, SVGTemplateResult } from 'lit';
|
|
2
2
|
import type { IUser } from '@api-client/core/models/store/User.js';
|
|
3
3
|
import '@material/web/focus/md-focus-ring.js';
|
|
4
4
|
import '@material/web/ripple/ripple.js';
|
|
5
|
+
export type AvatarType = 'button' | 'icon';
|
|
5
6
|
export default class UserAvatar extends LitElement {
|
|
6
|
-
private _user?;
|
|
7
7
|
/**
|
|
8
8
|
* Set with the user. The computed user initials.
|
|
9
9
|
*/
|
|
@@ -12,23 +12,36 @@ export default class UserAvatar extends LitElement {
|
|
|
12
12
|
* The URL to the user picture.
|
|
13
13
|
*/
|
|
14
14
|
protected accessor userPicture: string | undefined;
|
|
15
|
-
get user(): IUser | undefined;
|
|
16
15
|
/**
|
|
16
|
+
* The user object to display the avatar for.
|
|
17
|
+
*/
|
|
18
|
+
accessor user: IUser | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* The type of avatar to render, either 'button' or 'icon'.
|
|
17
21
|
* @attribute
|
|
18
22
|
*/
|
|
19
|
-
|
|
20
|
-
protected
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
accessor type: AvatarType;
|
|
24
|
+
protected willUpdate(cp: PropertyValues<this>): void;
|
|
25
|
+
/**
|
|
26
|
+
* Handles changes to the user property.
|
|
27
|
+
* @param user The user object
|
|
28
|
+
*/
|
|
29
|
+
protected handleUserChange(user?: IUser): void;
|
|
30
|
+
protected readUserInitials(user: IUser): string | undefined;
|
|
31
|
+
protected handlePictureError(): void;
|
|
23
32
|
/**
|
|
24
33
|
* @return Template result for an icon
|
|
25
34
|
*/
|
|
26
35
|
render(): TemplateResult;
|
|
36
|
+
protected renderButton(content: TemplateResult): TemplateResult;
|
|
37
|
+
protected renderIcon(content: TemplateResult): TemplateResult;
|
|
27
38
|
protected pictureTemplate(url: string): TemplateResult;
|
|
28
39
|
/**
|
|
29
40
|
* Renders a bubble with user initials
|
|
30
41
|
* @param initials The user initials
|
|
31
42
|
*/
|
|
32
43
|
protected nameTemplate(initials: string): TemplateResult;
|
|
44
|
+
protected renderDefaultAvatar(): TemplateResult;
|
|
45
|
+
protected defaultIcon(): SVGTemplateResult;
|
|
33
46
|
}
|
|
34
47
|
//# sourceMappingURL=UserAvatar.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UserAvatar.d.ts","sourceRoot":"","sources":["../../../../../src/elements/user/internals/UserAvatar.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,cAAc,EAAE,UAAU,EAAE,MAAM,KAAK,CAAA;
|
|
1
|
+
{"version":3,"file":"UserAvatar.d.ts","sourceRoot":"","sources":["../../../../../src/elements/user/internals/UserAvatar.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,cAAc,EAAE,UAAU,EAAE,cAAc,EAAE,iBAAiB,EAAO,MAAM,KAAK,CAAA;AAE9F,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,uCAAuC,CAAA;AAClE,OAAO,sCAAsC,CAAA;AAC7C,OAAO,gCAAgC,CAAA;AAEvC,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,MAAM,CAAA;AAE1C,MAAM,CAAC,OAAO,OAAO,UAAW,SAAQ,UAAU;IAChD;;OAEG;IACM,SAAS,CAAC,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAA;IAE5D;;OAEG;IACM,SAAS,CAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAA;IAE3D;;OAEG;IACyB,QAAQ,CAAC,IAAI,EAAE,KAAK,GAAG,SAAS,CAAA;IAC5D;;;OAGG;IACyB,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAW;cAE7C,UAAU,CAAC,EAAE,EAAE,cAAc,CAAC,IAAI,CAAC,GAAG,IAAI;IAO7D;;;OAGG;IACH,SAAS,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,KAAK,GAAG,IAAI;IAU9C,SAAS,CAAC,gBAAgB,CAAC,IAAI,EAAE,KAAK,GAAG,MAAM,GAAG,SAAS;IAc3D,SAAS,CAAC,kBAAkB,IAAI,IAAI;IAIpC;;OAEG;IACM,MAAM,IAAI,cAAc;IAgBjC,SAAS,CAAC,YAAY,CAAC,OAAO,EAAE,cAAc,GAAG,cAAc;IAW/D,SAAS,CAAC,UAAU,CAAC,OAAO,EAAE,cAAc,GAAG,cAAc;IAI7D,SAAS,CAAC,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc;IAWtD;;;OAGG;IACH,SAAS,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc;IAIxD,SAAS,CAAC,mBAAmB,IAAI,cAAc;IAI/C,SAAS,CAAC,WAAW,IAAI,iBAAiB;CAiB3C"}
|
|
@@ -1,30 +1,35 @@
|
|
|
1
1
|
import { __esDecorate, __runInitializers } from "tslib";
|
|
2
|
-
import { html, LitElement } from 'lit';
|
|
2
|
+
import { html, LitElement, svg } from 'lit';
|
|
3
3
|
import { property, state } from 'lit/decorators.js';
|
|
4
4
|
import '@material/web/focus/md-focus-ring.js';
|
|
5
5
|
import '@material/web/ripple/ripple.js';
|
|
6
6
|
let UserAvatar = (() => {
|
|
7
7
|
let _classSuper = LitElement;
|
|
8
|
-
let _instanceExtraInitializers = [];
|
|
9
8
|
let _userInitials_decorators;
|
|
10
9
|
let _userInitials_initializers = [];
|
|
11
10
|
let _userInitials_extraInitializers = [];
|
|
12
11
|
let _userPicture_decorators;
|
|
13
12
|
let _userPicture_initializers = [];
|
|
14
13
|
let _userPicture_extraInitializers = [];
|
|
15
|
-
let
|
|
14
|
+
let _user_decorators;
|
|
15
|
+
let _user_initializers = [];
|
|
16
|
+
let _user_extraInitializers = [];
|
|
17
|
+
let _type_decorators;
|
|
18
|
+
let _type_initializers = [];
|
|
19
|
+
let _type_extraInitializers = [];
|
|
16
20
|
return class UserAvatar extends _classSuper {
|
|
17
21
|
static {
|
|
18
22
|
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
19
23
|
_userInitials_decorators = [state()];
|
|
20
24
|
_userPicture_decorators = [state()];
|
|
21
|
-
|
|
25
|
+
_user_decorators = [property({ type: Object })];
|
|
26
|
+
_type_decorators = [property({ type: String })];
|
|
22
27
|
__esDecorate(this, null, _userInitials_decorators, { kind: "accessor", name: "userInitials", static: false, private: false, access: { has: obj => "userInitials" in obj, get: obj => obj.userInitials, set: (obj, value) => { obj.userInitials = value; } }, metadata: _metadata }, _userInitials_initializers, _userInitials_extraInitializers);
|
|
23
28
|
__esDecorate(this, null, _userPicture_decorators, { kind: "accessor", name: "userPicture", static: false, private: false, access: { has: obj => "userPicture" in obj, get: obj => obj.userPicture, set: (obj, value) => { obj.userPicture = value; } }, metadata: _metadata }, _userPicture_initializers, _userPicture_extraInitializers);
|
|
24
|
-
__esDecorate(this, null,
|
|
29
|
+
__esDecorate(this, null, _user_decorators, { kind: "accessor", name: "user", static: false, private: false, access: { has: obj => "user" in obj, get: obj => obj.user, set: (obj, value) => { obj.user = value; } }, metadata: _metadata }, _user_initializers, _user_extraInitializers);
|
|
30
|
+
__esDecorate(this, null, _type_decorators, { kind: "accessor", name: "type", static: false, private: false, access: { has: obj => "type" in obj, get: obj => obj.type, set: (obj, value) => { obj.type = value; } }, metadata: _metadata }, _type_initializers, _type_extraInitializers);
|
|
25
31
|
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
26
32
|
}
|
|
27
|
-
_user = __runInitializers(this, _instanceExtraInitializers);
|
|
28
33
|
#userInitials_accessor_storage = __runInitializers(this, _userInitials_initializers, void 0);
|
|
29
34
|
/**
|
|
30
35
|
* Set with the user. The computed user initials.
|
|
@@ -37,27 +42,32 @@ let UserAvatar = (() => {
|
|
|
37
42
|
*/
|
|
38
43
|
get userPicture() { return this.#userPicture_accessor_storage; }
|
|
39
44
|
set userPicture(value) { this.#userPicture_accessor_storage = value; }
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
#user_accessor_storage = (__runInitializers(this, _userPicture_extraInitializers), __runInitializers(this, _user_initializers, void 0));
|
|
46
|
+
/**
|
|
47
|
+
* The user object to display the avatar for.
|
|
48
|
+
*/
|
|
49
|
+
get user() { return this.#user_accessor_storage; }
|
|
50
|
+
set user(value) { this.#user_accessor_storage = value; }
|
|
51
|
+
#type_accessor_storage = (__runInitializers(this, _user_extraInitializers), __runInitializers(this, _type_initializers, 'button'));
|
|
43
52
|
/**
|
|
53
|
+
* The type of avatar to render, either 'button' or 'icon'.
|
|
44
54
|
* @attribute
|
|
45
55
|
*/
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (old && value && old.key === value.key) {
|
|
52
|
-
return;
|
|
56
|
+
get type() { return this.#type_accessor_storage; }
|
|
57
|
+
set type(value) { this.#type_accessor_storage = value; }
|
|
58
|
+
willUpdate(cp) {
|
|
59
|
+
if (cp.has('user')) {
|
|
60
|
+
this.handleUserChange(this.user);
|
|
53
61
|
}
|
|
54
|
-
|
|
55
|
-
this.requestUpdate('user', old);
|
|
56
|
-
this._processUser(value);
|
|
62
|
+
super.willUpdate(cp);
|
|
57
63
|
}
|
|
58
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Handles changes to the user property.
|
|
66
|
+
* @param user The user object
|
|
67
|
+
*/
|
|
68
|
+
handleUserChange(user) {
|
|
59
69
|
if (user) {
|
|
60
|
-
this.userInitials = this.
|
|
70
|
+
this.userInitials = this.readUserInitials(user);
|
|
61
71
|
this.userPicture = user.picture && user.picture.url;
|
|
62
72
|
}
|
|
63
73
|
else {
|
|
@@ -65,7 +75,7 @@ let UserAvatar = (() => {
|
|
|
65
75
|
this.userPicture = undefined;
|
|
66
76
|
}
|
|
67
77
|
}
|
|
68
|
-
|
|
78
|
+
readUserInitials(user) {
|
|
69
79
|
const { name } = user;
|
|
70
80
|
if (!name) {
|
|
71
81
|
return undefined;
|
|
@@ -78,17 +88,14 @@ let UserAvatar = (() => {
|
|
|
78
88
|
.map((i) => i[0]);
|
|
79
89
|
return parts.join('');
|
|
80
90
|
}
|
|
81
|
-
|
|
91
|
+
handlePictureError() {
|
|
82
92
|
this.userPicture = undefined;
|
|
83
93
|
}
|
|
84
94
|
/**
|
|
85
95
|
* @return Template result for an icon
|
|
86
96
|
*/
|
|
87
97
|
render() {
|
|
88
|
-
const {
|
|
89
|
-
if (!user || user.key === 'default') {
|
|
90
|
-
return html ``;
|
|
91
|
-
}
|
|
98
|
+
const { userPicture } = this;
|
|
92
99
|
let content;
|
|
93
100
|
if (userPicture) {
|
|
94
101
|
content = this.pictureTemplate(userPicture);
|
|
@@ -97,8 +104,14 @@ let UserAvatar = (() => {
|
|
|
97
104
|
content = this.nameTemplate(this.userInitials);
|
|
98
105
|
}
|
|
99
106
|
else {
|
|
100
|
-
content = this.
|
|
107
|
+
content = this.renderDefaultAvatar();
|
|
108
|
+
}
|
|
109
|
+
if (this.type === 'icon') {
|
|
110
|
+
return this.renderIcon(content);
|
|
101
111
|
}
|
|
112
|
+
return this.renderButton(content);
|
|
113
|
+
}
|
|
114
|
+
renderButton(content) {
|
|
102
115
|
return html `
|
|
103
116
|
<button id="button" class="icon-button">
|
|
104
117
|
<md-focus-ring part="focus-ring" for="button"></md-focus-ring>
|
|
@@ -108,13 +121,16 @@ let UserAvatar = (() => {
|
|
|
108
121
|
</button>
|
|
109
122
|
`;
|
|
110
123
|
}
|
|
124
|
+
renderIcon(content) {
|
|
125
|
+
return html `<span role="presentation" class="icon">${content}</span>`;
|
|
126
|
+
}
|
|
111
127
|
pictureTemplate(url) {
|
|
112
128
|
return html `
|
|
113
129
|
<img
|
|
114
130
|
src="${url}"
|
|
115
131
|
alt="${this.userInitials || 'Thumb'}"
|
|
116
132
|
class="user-picture user-icon"
|
|
117
|
-
@error="${this.
|
|
133
|
+
@error="${this.handlePictureError}"
|
|
118
134
|
/>
|
|
119
135
|
`;
|
|
120
136
|
}
|
|
@@ -125,9 +141,29 @@ let UserAvatar = (() => {
|
|
|
125
141
|
nameTemplate(initials) {
|
|
126
142
|
return html ` <span class="avatar-initials user-icon">${initials}</span> `;
|
|
127
143
|
}
|
|
144
|
+
renderDefaultAvatar() {
|
|
145
|
+
return html `<span class="avatar-default icon"> ${this.defaultIcon()} </span>`;
|
|
146
|
+
}
|
|
147
|
+
defaultIcon() {
|
|
148
|
+
return svg `
|
|
149
|
+
<svg viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon">
|
|
150
|
+
<rect width="120" height="120" rx="60" class="avatar-background" />
|
|
151
|
+
<path
|
|
152
|
+
fill-rule="evenodd"
|
|
153
|
+
clip-rule="evenodd"
|
|
154
|
+
d="M78.0007 48C78.0007 57.9411 69.9419 66 60.0007 66C50.0596 66 42.0007 57.9411 42.0007 48C42.0007 38.0589 50.0596 30 60.0007 30C69.9419 30 78.0007 38.0589 78.0007 48ZM72.0007 48C72.0007 54.6274 66.6282 60 60.0007 60C53.3733 60 48.0007 54.6274 48.0007 48C48.0007 41.3726 53.3733 36 60.0007 36C66.6282 36 72.0007 41.3726 72.0007 48Z"
|
|
155
|
+
class="avatar-lines"
|
|
156
|
+
/>
|
|
157
|
+
<path
|
|
158
|
+
d="M60.0007 75C40.5776 75 24.0286 86.4852 17.7246 102.576C19.2603 104.101 20.878 105.543 22.5706 106.896C27.2648 92.1231 41.9909 81 60.0007 81C78.0106 81 92.7367 92.1231 97.4309 106.896C99.1235 105.544 100.741 104.101 102.277 102.576C95.973 86.4853 79.4239 75 60.0007 75Z"
|
|
159
|
+
class="avatar-lines"
|
|
160
|
+
/>
|
|
161
|
+
</svg>
|
|
162
|
+
`;
|
|
163
|
+
}
|
|
128
164
|
constructor() {
|
|
129
165
|
super(...arguments);
|
|
130
|
-
__runInitializers(this,
|
|
166
|
+
__runInitializers(this, _type_extraInitializers);
|
|
131
167
|
}
|
|
132
168
|
};
|
|
133
169
|
})();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UserAvatar.js","sourceRoot":"","sources":["../../../../../src/elements/user/internals/UserAvatar.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,IAAI,EAAkB,UAAU,EAAE,MAAM,KAAK,CAAA;
|
|
1
|
+
{"version":3,"file":"UserAvatar.js","sourceRoot":"","sources":["../../../../../src/elements/user/internals/UserAvatar.ts"],"names":[],"mappings":"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","sourcesContent":["import { html, TemplateResult, LitElement, PropertyValues, SVGTemplateResult, svg } from 'lit'\nimport { property, state } from 'lit/decorators.js'\nimport type { IUser } from '@api-client/core/models/store/User.js'\nimport '@material/web/focus/md-focus-ring.js'\nimport '@material/web/ripple/ripple.js'\n\nexport type AvatarType = 'button' | 'icon'\n\nexport default class UserAvatar extends LitElement {\n /**\n * Set with the user. The computed user initials.\n */\n @state() protected accessor userInitials: string | undefined\n\n /**\n * The URL to the user picture.\n */\n @state() protected accessor userPicture: string | undefined\n\n /**\n * The user object to display the avatar for.\n */\n @property({ type: Object }) accessor user: IUser | undefined\n /**\n * The type of avatar to render, either 'button' or 'icon'.\n * @attribute\n */\n @property({ type: String }) accessor type: AvatarType = 'button'\n\n protected override willUpdate(cp: PropertyValues<this>): void {\n if (cp.has('user')) {\n this.handleUserChange(this.user)\n }\n super.willUpdate(cp)\n }\n\n /**\n * Handles changes to the user property.\n * @param user The user object\n */\n protected handleUserChange(user?: IUser): void {\n if (user) {\n this.userInitials = this.readUserInitials(user)\n this.userPicture = user.picture && user.picture.url\n } else {\n this.userInitials = undefined\n this.userPicture = undefined\n }\n }\n\n protected readUserInitials(user: IUser): string | undefined {\n const { name } = user\n if (!name) {\n return undefined\n }\n const max = 2\n const parts = name\n .split(/[\\s-]/)\n .slice(0, max)\n .filter((i) => !!i)\n .map((i) => i[0])\n return parts.join('')\n }\n\n protected handlePictureError(): void {\n this.userPicture = undefined\n }\n\n /**\n * @return Template result for an icon\n */\n override render(): TemplateResult {\n const { userPicture } = this\n let content: TemplateResult\n if (userPicture) {\n content = this.pictureTemplate(userPicture)\n } else if (this.userInitials) {\n content = this.nameTemplate(this.userInitials)\n } else {\n content = this.renderDefaultAvatar()\n }\n if (this.type === 'icon') {\n return this.renderIcon(content)\n }\n return this.renderButton(content)\n }\n\n protected renderButton(content: TemplateResult): TemplateResult {\n return html`\n <button id=\"button\" class=\"icon-button\">\n <md-focus-ring part=\"focus-ring\" for=\"button\"></md-focus-ring>\n <md-ripple></md-ripple>\n <span role=\"presentation\" class=\"icon\">${content}</span>\n <span class=\"touch\"></span>\n </button>\n `\n }\n\n protected renderIcon(content: TemplateResult): TemplateResult {\n return html`<span role=\"presentation\" class=\"icon\">${content}</span>`\n }\n\n protected pictureTemplate(url: string): TemplateResult {\n return html`\n <img\n src=\"${url}\"\n alt=\"${this.userInitials || 'Thumb'}\"\n class=\"user-picture user-icon\"\n @error=\"${this.handlePictureError}\"\n />\n `\n }\n\n /**\n * Renders a bubble with user initials\n * @param initials The user initials\n */\n protected nameTemplate(initials: string): TemplateResult {\n return html` <span class=\"avatar-initials user-icon\">${initials}</span> `\n }\n\n protected renderDefaultAvatar(): TemplateResult {\n return html`<span class=\"avatar-default icon\"> ${this.defaultIcon()} </span>`\n }\n\n protected defaultIcon(): SVGTemplateResult {\n return svg`\n <svg viewBox=\"0 0 120 120\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\">\n <rect width=\"120\" height=\"120\" rx=\"60\" class=\"avatar-background\" />\n <path\n fill-rule=\"evenodd\"\n clip-rule=\"evenodd\"\n d=\"M78.0007 48C78.0007 57.9411 69.9419 66 60.0007 66C50.0596 66 42.0007 57.9411 42.0007 48C42.0007 38.0589 50.0596 30 60.0007 30C69.9419 30 78.0007 38.0589 78.0007 48ZM72.0007 48C72.0007 54.6274 66.6282 60 60.0007 60C53.3733 60 48.0007 54.6274 48.0007 48C48.0007 41.3726 53.3733 36 60.0007 36C66.6282 36 72.0007 41.3726 72.0007 48Z\"\n class=\"avatar-lines\"\n />\n <path\n d=\"M60.0007 75C40.5776 75 24.0286 86.4852 17.7246 102.576C19.2603 104.101 20.878 105.543 22.5706 106.896C27.2648 92.1231 41.9909 81 60.0007 81C78.0106 81 92.7367 92.1231 97.4309 106.896C99.1235 105.544 100.741 104.101 102.277 102.576C95.973 86.4853 79.4239 75 60.0007 75Z\"\n class=\"avatar-lines\"\n />\n </svg>\n `\n }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UserAvatar.styles.d.ts","sourceRoot":"","sources":["../../../../../src/elements/user/internals/UserAvatar.styles.ts"],"names":[],"mappings":";AAEA,
|
|
1
|
+
{"version":3,"file":"UserAvatar.styles.d.ts","sourceRoot":"","sources":["../../../../../src/elements/user/internals/UserAvatar.styles.ts"],"names":[],"mappings":";AAEA,wBAmHC"}
|
|
@@ -5,8 +5,8 @@ export default css `
|
|
|
5
5
|
outline: none;
|
|
6
6
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
|
7
7
|
justify-content: center;
|
|
8
|
-
height:
|
|
9
|
-
width:
|
|
8
|
+
height: 40px;
|
|
9
|
+
width: 40px;
|
|
10
10
|
|
|
11
11
|
--_focus-icon-color: var(--md-icon-button-focus-icon-color, var(--md-sys-color-on-surface-variant, #49454f));
|
|
12
12
|
--_hover-icon-color: var(--md-icon-button-hover-icon-color, var(--md-sys-color-on-surface-variant, #49454f));
|
|
@@ -23,8 +23,6 @@ export default css `
|
|
|
23
23
|
);
|
|
24
24
|
--_pressed-state-layer-opacity: var(--md-icon-button-pressed-state-layer-opacity, 0.12);
|
|
25
25
|
--_state-layer-shape: var(--md-icon-button-state-layer-shape, var(--md-sys-shape-corner-full, 9999px));
|
|
26
|
-
--_state-layer-height: 40px;
|
|
27
|
-
--_state-layer-width: 40px;
|
|
28
26
|
}
|
|
29
27
|
|
|
30
28
|
.icon-button {
|
|
@@ -88,15 +86,13 @@ export default css `
|
|
|
88
86
|
|
|
89
87
|
.user-icon,
|
|
90
88
|
.avatar-initials {
|
|
91
|
-
background-color: var(--
|
|
92
|
-
color: var(--
|
|
89
|
+
background-color: var(--md-sys-color-primary-container, #9eeffe);
|
|
90
|
+
color: var(--md-sys-color-on-primary-container, #001f24);
|
|
93
91
|
}
|
|
94
92
|
|
|
95
93
|
.avatar-initials {
|
|
96
|
-
/* border-radius: 50%; */
|
|
97
94
|
text-transform: uppercase;
|
|
98
95
|
font-size: large;
|
|
99
|
-
border: 1px #0d47a1 solid;
|
|
100
96
|
width: 100%;
|
|
101
97
|
height: 100%;
|
|
102
98
|
display: flex;
|
|
@@ -104,9 +100,19 @@ export default css `
|
|
|
104
100
|
justify-content: center;
|
|
105
101
|
}
|
|
106
102
|
|
|
107
|
-
.user-icon
|
|
108
|
-
|
|
109
|
-
|
|
103
|
+
.user-icon,
|
|
104
|
+
.icon-button,
|
|
105
|
+
.icon {
|
|
106
|
+
height: inherit;
|
|
107
|
+
width: inherit;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.avatar-background {
|
|
111
|
+
fill: var(--md-sys-color-primary-container, #9eeffe);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.avatar-lines {
|
|
115
|
+
fill: var(--md-sys-color-on-primary-container, #001f24);
|
|
110
116
|
}
|
|
111
117
|
`;
|
|
112
118
|
//# sourceMappingURL=UserAvatar.styles.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UserAvatar.styles.js","sourceRoot":"","sources":["../../../../../src/elements/user/internals/UserAvatar.styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAEzB,eAAe,GAAG,CAAA
|
|
1
|
+
{"version":3,"file":"UserAvatar.styles.js","sourceRoot":"","sources":["../../../../../src/elements/user/internals/UserAvatar.styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAEzB,eAAe,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmHjB,CAAA","sourcesContent":["import { css } from 'lit'\n\nexport default css`\n :host {\n display: inline-flex;\n outline: none;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n justify-content: center;\n height: 40px;\n width: 40px;\n\n --_focus-icon-color: var(--md-icon-button-focus-icon-color, var(--md-sys-color-on-surface-variant, #49454f));\n --_hover-icon-color: var(--md-icon-button-hover-icon-color, var(--md-sys-color-on-surface-variant, #49454f));\n --_hover-state-layer-color: var(\n --md-icon-button-hover-state-layer-color,\n var(--md-sys-color-on-surface-variant, #49454f)\n );\n --_hover-state-layer-opacity: var(--md-icon-button-hover-state-layer-opacity, 0.08);\n --_icon-color: var(--md-icon-button-icon-color, var(--md-sys-color-on-surface-variant, #49454f));\n --_pressed-icon-color: var(--md-icon-button-pressed-icon-color, var(--md-sys-color-on-surface-variant, #49454f));\n --_pressed-state-layer-color: var(\n --md-icon-button-pressed-state-layer-color,\n var(--md-sys-color-on-surface-variant, #49454f)\n );\n --_pressed-state-layer-opacity: var(--md-icon-button-pressed-state-layer-opacity, 0.12);\n --_state-layer-shape: var(--md-icon-button-state-layer-shape, var(--md-sys-shape-corner-full, 9999px));\n }\n\n .icon-button {\n background-color: rgba(0, 0, 0, 0);\n color: var(--_icon-color);\n place-items: center;\n background: none;\n border: none;\n box-sizing: border-box;\n cursor: pointer;\n display: flex;\n place-content: center;\n outline: none;\n padding: 0;\n position: relative;\n text-decoration: none;\n user-select: none;\n z-index: 0;\n flex: 1;\n border-radius: var(--_state-layer-shape);\n --md-ripple-hover-color: var(--_hover-state-layer-color);\n --md-ripple-hover-opacity: var(--_hover-state-layer-opacity);\n --md-ripple-pressed-color: var(--_pressed-state-layer-color);\n --md-ripple-pressed-opacity: var(--_pressed-state-layer-opacity);\n }\n\n .icon-button:hover {\n color: var(--_hover-icon-color);\n }\n\n .icon-button:focus {\n color: var(--_focus-icon-color);\n }\n\n .icon-button:active {\n color: var(--_pressed-icon-color);\n }\n\n md-focus-ring {\n --md-focus-ring-shape-start-start: var(--_state-layer-shape);\n --md-focus-ring-shape-start-end: var(--_state-layer-shape);\n --md-focus-ring-shape-end-end: var(--_state-layer-shape);\n --md-focus-ring-shape-end-start: var(--_state-layer-shape);\n }\n\n md-ripple {\n border-radius: var(--_state-layer-shape);\n }\n\n .icon {\n display: inline-flex;\n border-radius: var(--_state-layer-shape);\n overflow: hidden;\n }\n\n .touch {\n position: absolute;\n height: max(48px, 100%);\n width: max(48px, 100%);\n }\n\n .user-icon,\n .avatar-initials {\n background-color: var(--md-sys-color-primary-container, #9eeffe);\n color: var(--md-sys-color-on-primary-container, #001f24);\n }\n\n .avatar-initials {\n text-transform: uppercase;\n font-size: large;\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n .user-icon,\n .icon-button,\n .icon {\n height: inherit;\n width: inherit;\n }\n\n .avatar-background {\n fill: var(--md-sys-color-primary-container, #9eeffe);\n }\n\n .avatar-lines {\n fill: var(--md-sys-color-on-primary-container, #001f24);\n }\n`\n"]}
|
package/demo/elements/index.html
CHANGED
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
<dt><a href="authorization/index.html">Authorization element</a></dt>
|
|
49
49
|
<dd>Elements to define HTTP authorization.</dd>
|
|
50
50
|
|
|
51
|
-
<dt><a href="
|
|
52
|
-
<dd>
|
|
51
|
+
<dt><a href="user/user-avatar.html">User avatar</a></dt>
|
|
52
|
+
<dd>User avatar element.</dd>
|
|
53
53
|
</dl>
|
|
54
54
|
</nav>
|
|
55
55
|
</main>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes">
|
|
7
|
+
<title>User Avatar</title>
|
|
8
|
+
<link href="../../../src/styles/m3/tokens.css" rel="stylesheet" type="text/css" />
|
|
9
|
+
<link href="../../../src/styles/m3/theme.css" rel="stylesheet" type="text/css" />
|
|
10
|
+
</head>
|
|
11
|
+
|
|
12
|
+
<body class="demo">
|
|
13
|
+
<div id="app"></div>
|
|
14
|
+
<script type="module" src="../../../.tmp/demo/demo/elements/user/user-avatar.js"></script>
|
|
15
|
+
</body>
|
|
16
|
+
|
|
17
|
+
</html>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { IUser } from '@api-client/core/models/store/User.js'
|
|
2
|
+
import { DemoPage } from '../../../src/demo/DemoPage.js'
|
|
3
|
+
import { reactive } from '../../../src/decorators/index.js'
|
|
4
|
+
import { UserKind } from '@api-client/core/models/kinds.js'
|
|
5
|
+
import { html, TemplateResult } from 'lit'
|
|
6
|
+
import '../../../src/elements/user/ui-user-avatar.js'
|
|
7
|
+
|
|
8
|
+
class ComponentDemoPage extends DemoPage {
|
|
9
|
+
@reactive() accessor user1: IUser
|
|
10
|
+
@reactive() accessor user2: IUser
|
|
11
|
+
|
|
12
|
+
constructor() {
|
|
13
|
+
super()
|
|
14
|
+
this.componentName = 'User Avatar'
|
|
15
|
+
this.user1 = {
|
|
16
|
+
key: '1',
|
|
17
|
+
name: 'John Doe',
|
|
18
|
+
email: [{ email: 'john.doe @example.com', verified: true }],
|
|
19
|
+
picture: {
|
|
20
|
+
url: 'https://i.pravatar.cc/300',
|
|
21
|
+
},
|
|
22
|
+
kind: UserKind,
|
|
23
|
+
status: 'active',
|
|
24
|
+
grantType: 'editor',
|
|
25
|
+
}
|
|
26
|
+
this.user2 = {
|
|
27
|
+
key: '2',
|
|
28
|
+
name: 'John Doe',
|
|
29
|
+
email: [{ email: 'john.doe @example.com', verified: true }],
|
|
30
|
+
kind: UserKind,
|
|
31
|
+
status: 'active',
|
|
32
|
+
grantType: 'editor',
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
contentTemplate(): TemplateResult {
|
|
37
|
+
return html`
|
|
38
|
+
<a href="./">Back</a>
|
|
39
|
+
<section class="centered">
|
|
40
|
+
<h3>Full data</h3>
|
|
41
|
+
<ui-user-avatar .user="${this.user1}"></ui-user-avatar>
|
|
42
|
+
<h3>No image</h3>
|
|
43
|
+
<ui-user-avatar .user="${this.user2}"></ui-user-avatar>
|
|
44
|
+
<h3>No user</h3>
|
|
45
|
+
<ui-user-avatar></ui-user-avatar>
|
|
46
|
+
<h3>Custom size</h3>
|
|
47
|
+
<ui-user-avatar style="width: 128px; height: 128px" .user="${this.user1}"></ui-user-avatar>
|
|
48
|
+
<h3>Icon type</h3>
|
|
49
|
+
<ui-user-avatar .user="${this.user1}" type="icon"></ui-user-avatar>
|
|
50
|
+
</section>
|
|
51
|
+
`
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const instance = new ComponentDemoPage()
|
|
56
|
+
instance.render()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@api-client/ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Internal UI component library for the API Client ecosystem.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"main": "build/src/index.js",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"tsc:watch": "wireit",
|
|
68
68
|
"tsc": "wireit",
|
|
69
69
|
"gen:apis": "node demo/model.js",
|
|
70
|
-
"dev": "
|
|
70
|
+
"dev": "wireit",
|
|
71
71
|
"tsc:tests": "wireit",
|
|
72
72
|
"tsc:demo": "wireit"
|
|
73
73
|
},
|
|
@@ -153,6 +153,25 @@
|
|
|
153
153
|
".tmp/tests/**",
|
|
154
154
|
".tsbuildinfo"
|
|
155
155
|
]
|
|
156
|
+
},
|
|
157
|
+
"dev": {
|
|
158
|
+
"command": "wds --watch --config=wds-demo.config.js",
|
|
159
|
+
"files": [
|
|
160
|
+
"demo/**"
|
|
161
|
+
],
|
|
162
|
+
"service": true,
|
|
163
|
+
"dependencies": [
|
|
164
|
+
"build:ts:watch"
|
|
165
|
+
]
|
|
166
|
+
},
|
|
167
|
+
"build:ts:watch": {
|
|
168
|
+
"command": "tsc --watch --project tsconfig.browser.json",
|
|
169
|
+
"files": [
|
|
170
|
+
"src/**",
|
|
171
|
+
"demo/**",
|
|
172
|
+
"tsconfig.browser.json"
|
|
173
|
+
],
|
|
174
|
+
"service": true
|
|
156
175
|
}
|
|
157
176
|
},
|
|
158
177
|
"dependencies": {
|
package/src/core/Model.ts
CHANGED
|
@@ -1,4 +1,58 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
2
|
+
|
|
3
|
+
import type { Exception } from '@api-client/core/exceptions/exception.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Defines the options for the retry mechanism.
|
|
7
|
+
*/
|
|
8
|
+
export interface RetryOptions {
|
|
9
|
+
/**
|
|
10
|
+
* The maximum number of retry attempts.
|
|
11
|
+
* Defaults to 3.
|
|
12
|
+
*/
|
|
13
|
+
retries?: number
|
|
14
|
+
/**
|
|
15
|
+
* The delay in milliseconds before the next retry.
|
|
16
|
+
* Can be a fixed number or a function that returns a number (e.g., for exponential backoff).
|
|
17
|
+
* Defaults to 1000ms.
|
|
18
|
+
*/
|
|
19
|
+
delayMs?: number | ((attempt: number) => number)
|
|
20
|
+
/**
|
|
21
|
+
* An optional function that determines if a retry should be attempted based on the error.
|
|
22
|
+
* If not provided, all errors will trigger a retry up to the maximum number of retries.
|
|
23
|
+
* @param error The error that occurred.
|
|
24
|
+
* @returns True if a retry should be attempted, false otherwise.
|
|
25
|
+
*/
|
|
26
|
+
shouldRetry?: (error: Error) => boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Calculates an exponential backoff delay for retry attempts.
|
|
31
|
+
* The delay doubles with each attempt.
|
|
32
|
+
*
|
|
33
|
+
* @param attempt The current retry attempt number (should be 1-indexed).
|
|
34
|
+
* @param initialDelay The initial delay in milliseconds. Defaults to 1000ms.
|
|
35
|
+
* @returns The calculated delay in milliseconds.
|
|
36
|
+
*/
|
|
37
|
+
export function exponentialBackoffDelay(attempt: number, initialDelay = 1000): number {
|
|
38
|
+
if (attempt <= 0) return initialDelay
|
|
39
|
+
// For attempt 1, delay is initialDelay * 2^0 = initialDelay
|
|
40
|
+
return initialDelay * Math.pow(2, attempt - 1)
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Checks if the API error should be retried based on its status code.
|
|
44
|
+
* If the status code is 404, it returns false (do not retry).
|
|
45
|
+
* For other status codes, it returns true (retry).
|
|
46
|
+
* @param error The error to check.
|
|
47
|
+
*/
|
|
48
|
+
export function shouldRetryApiError(error: Exception): boolean {
|
|
49
|
+
const { status } = error
|
|
50
|
+
if (status === 404) {
|
|
51
|
+
// Do not retry on 404 errors
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
return true
|
|
55
|
+
}
|
|
2
56
|
/**
|
|
3
57
|
* The base class for models.
|
|
4
58
|
*
|
|
@@ -58,6 +112,13 @@ export abstract class Model<T> extends EventTarget {
|
|
|
58
112
|
return this.#taskComplete
|
|
59
113
|
}
|
|
60
114
|
|
|
115
|
+
/**
|
|
116
|
+
* A getter for the raw value.
|
|
117
|
+
*/
|
|
118
|
+
get value(): T | undefined {
|
|
119
|
+
return this.raw
|
|
120
|
+
}
|
|
121
|
+
|
|
61
122
|
/**
|
|
62
123
|
* @param obj The source object to use.
|
|
63
124
|
*/
|
|
@@ -126,4 +187,62 @@ export abstract class Model<T> extends EventTarget {
|
|
|
126
187
|
resolver()
|
|
127
188
|
}
|
|
128
189
|
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Executes an asynchronous operation with a retry mechanism.
|
|
193
|
+
* @param operation A function that returns a Promise to be executed.
|
|
194
|
+
* @param options Optional retry configurations.
|
|
195
|
+
* @returns A Promise that resolves with the result of the operation or rejects if all retries fail.
|
|
196
|
+
* @example
|
|
197
|
+
* ```typescript
|
|
198
|
+
* override async create(data: Partial<IUser>): Promise<IUser> {
|
|
199
|
+
* const operation = async (): Promise<UserData> => {
|
|
200
|
+
* // ... perform API call to create user
|
|
201
|
+
* }
|
|
202
|
+
* const retryOptions: RetryOptions = {
|
|
203
|
+
* retries: 5,
|
|
204
|
+
* delayMs: exponentialBackoffDelay,
|
|
205
|
+
* shouldRetry: shouldRetryApiError,
|
|
206
|
+
* }
|
|
207
|
+
* this.raw = await this.retry(operation)
|
|
208
|
+
* this.dispatchEvent(new CustomEvent('data-changed', { detail: this.raw }))
|
|
209
|
+
* return this.raw;
|
|
210
|
+
* }
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
protected async retry<R>(operation: () => Promise<R>, options: RetryOptions = {}): Promise<R> {
|
|
214
|
+
const { retries = 3, delayMs = 1000, shouldRetry = () => true } = options
|
|
215
|
+
|
|
216
|
+
let lastError: Error | undefined
|
|
217
|
+
|
|
218
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
219
|
+
try {
|
|
220
|
+
return await operation()
|
|
221
|
+
} catch (e) {
|
|
222
|
+
lastError = e as Error
|
|
223
|
+
if (!shouldRetry(lastError) || attempt === retries - 1) {
|
|
224
|
+
// Do not retry if shouldRetry returns false or if it's the last attempt
|
|
225
|
+
this.notifyError(lastError)
|
|
226
|
+
throw lastError
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const delay = typeof delayMs === 'function' ? delayMs(attempt + 1) : delayMs
|
|
230
|
+
if (delay > 0) {
|
|
231
|
+
await new Promise((resolve) => setTimeout(resolve, delay))
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// This part should ideally not be reached if retries > 0,
|
|
236
|
+
// as the loop would throw on the last attempt.
|
|
237
|
+
// However, to satisfy TypeScript and guard against retries = 0:
|
|
238
|
+
if (lastError) {
|
|
239
|
+
this.notifyError(lastError)
|
|
240
|
+
throw lastError
|
|
241
|
+
}
|
|
242
|
+
// Should not happen if operation is called at least once.
|
|
243
|
+
// Adding a generic error for completeness.
|
|
244
|
+
const err = new Error('Retry operation failed without a specific error.')
|
|
245
|
+
this.notifyError(err)
|
|
246
|
+
throw err
|
|
247
|
+
}
|
|
129
248
|
}
|
|
@@ -6,8 +6,8 @@ export default css`
|
|
|
6
6
|
outline: none;
|
|
7
7
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
|
8
8
|
justify-content: center;
|
|
9
|
-
height:
|
|
10
|
-
width:
|
|
9
|
+
height: 40px;
|
|
10
|
+
width: 40px;
|
|
11
11
|
|
|
12
12
|
--_focus-icon-color: var(--md-icon-button-focus-icon-color, var(--md-sys-color-on-surface-variant, #49454f));
|
|
13
13
|
--_hover-icon-color: var(--md-icon-button-hover-icon-color, var(--md-sys-color-on-surface-variant, #49454f));
|
|
@@ -24,8 +24,6 @@ export default css`
|
|
|
24
24
|
);
|
|
25
25
|
--_pressed-state-layer-opacity: var(--md-icon-button-pressed-state-layer-opacity, 0.12);
|
|
26
26
|
--_state-layer-shape: var(--md-icon-button-state-layer-shape, var(--md-sys-shape-corner-full, 9999px));
|
|
27
|
-
--_state-layer-height: 40px;
|
|
28
|
-
--_state-layer-width: 40px;
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
.icon-button {
|
|
@@ -89,15 +87,13 @@ export default css`
|
|
|
89
87
|
|
|
90
88
|
.user-icon,
|
|
91
89
|
.avatar-initials {
|
|
92
|
-
background-color: var(--
|
|
93
|
-
color: var(--
|
|
90
|
+
background-color: var(--md-sys-color-primary-container, #9eeffe);
|
|
91
|
+
color: var(--md-sys-color-on-primary-container, #001f24);
|
|
94
92
|
}
|
|
95
93
|
|
|
96
94
|
.avatar-initials {
|
|
97
|
-
/* border-radius: 50%; */
|
|
98
95
|
text-transform: uppercase;
|
|
99
96
|
font-size: large;
|
|
100
|
-
border: 1px #0d47a1 solid;
|
|
101
97
|
width: 100%;
|
|
102
98
|
height: 100%;
|
|
103
99
|
display: flex;
|
|
@@ -105,8 +101,18 @@ export default css`
|
|
|
105
101
|
justify-content: center;
|
|
106
102
|
}
|
|
107
103
|
|
|
108
|
-
.user-icon
|
|
109
|
-
|
|
110
|
-
|
|
104
|
+
.user-icon,
|
|
105
|
+
.icon-button,
|
|
106
|
+
.icon {
|
|
107
|
+
height: inherit;
|
|
108
|
+
width: inherit;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.avatar-background {
|
|
112
|
+
fill: var(--md-sys-color-primary-container, #9eeffe);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.avatar-lines {
|
|
116
|
+
fill: var(--md-sys-color-on-primary-container, #001f24);
|
|
111
117
|
}
|
|
112
118
|
`
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { html, TemplateResult, LitElement } from 'lit'
|
|
1
|
+
import { html, TemplateResult, LitElement, PropertyValues, SVGTemplateResult, svg } from 'lit'
|
|
2
2
|
import { property, state } from 'lit/decorators.js'
|
|
3
3
|
import type { IUser } from '@api-client/core/models/store/User.js'
|
|
4
4
|
import '@material/web/focus/md-focus-ring.js'
|
|
5
5
|
import '@material/web/ripple/ripple.js'
|
|
6
6
|
|
|
7
|
-
export
|
|
8
|
-
private _user?: IUser
|
|
7
|
+
export type AvatarType = 'button' | 'icon'
|
|
9
8
|
|
|
9
|
+
export default class UserAvatar extends LitElement {
|
|
10
10
|
/**
|
|
11
11
|
* Set with the user. The computed user initials.
|
|
12
12
|
*/
|
|
@@ -17,30 +17,30 @@ export default class UserAvatar extends LitElement {
|
|
|
17
17
|
*/
|
|
18
18
|
@state() protected accessor userPicture: string | undefined
|
|
19
19
|
|
|
20
|
-
get user(): IUser | undefined {
|
|
21
|
-
return this._user
|
|
22
|
-
}
|
|
23
|
-
|
|
24
20
|
/**
|
|
21
|
+
* The user object to display the avatar for.
|
|
22
|
+
*/
|
|
23
|
+
@property({ type: Object }) accessor user: IUser | undefined
|
|
24
|
+
/**
|
|
25
|
+
* The type of avatar to render, either 'button' or 'icon'.
|
|
25
26
|
* @attribute
|
|
26
27
|
*/
|
|
27
|
-
@property({ type:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
if (old && value && old.key === value.key) {
|
|
34
|
-
return
|
|
28
|
+
@property({ type: String }) accessor type: AvatarType = 'button'
|
|
29
|
+
|
|
30
|
+
protected override willUpdate(cp: PropertyValues<this>): void {
|
|
31
|
+
if (cp.has('user')) {
|
|
32
|
+
this.handleUserChange(this.user)
|
|
35
33
|
}
|
|
36
|
-
|
|
37
|
-
this.requestUpdate('user', old)
|
|
38
|
-
this._processUser(value)
|
|
34
|
+
super.willUpdate(cp)
|
|
39
35
|
}
|
|
40
36
|
|
|
41
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Handles changes to the user property.
|
|
39
|
+
* @param user The user object
|
|
40
|
+
*/
|
|
41
|
+
protected handleUserChange(user?: IUser): void {
|
|
42
42
|
if (user) {
|
|
43
|
-
this.userInitials = this.
|
|
43
|
+
this.userInitials = this.readUserInitials(user)
|
|
44
44
|
this.userPicture = user.picture && user.picture.url
|
|
45
45
|
} else {
|
|
46
46
|
this.userInitials = undefined
|
|
@@ -48,7 +48,7 @@ export default class UserAvatar extends LitElement {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
protected
|
|
51
|
+
protected readUserInitials(user: IUser): string | undefined {
|
|
52
52
|
const { name } = user
|
|
53
53
|
if (!name) {
|
|
54
54
|
return undefined
|
|
@@ -62,7 +62,7 @@ export default class UserAvatar extends LitElement {
|
|
|
62
62
|
return parts.join('')
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
protected
|
|
65
|
+
protected handlePictureError(): void {
|
|
66
66
|
this.userPicture = undefined
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -70,18 +70,22 @@ export default class UserAvatar extends LitElement {
|
|
|
70
70
|
* @return Template result for an icon
|
|
71
71
|
*/
|
|
72
72
|
override render(): TemplateResult {
|
|
73
|
-
const {
|
|
74
|
-
if (!user || user.key === 'default') {
|
|
75
|
-
return html``
|
|
76
|
-
}
|
|
73
|
+
const { userPicture } = this
|
|
77
74
|
let content: TemplateResult
|
|
78
75
|
if (userPicture) {
|
|
79
76
|
content = this.pictureTemplate(userPicture)
|
|
80
77
|
} else if (this.userInitials) {
|
|
81
78
|
content = this.nameTemplate(this.userInitials)
|
|
82
79
|
} else {
|
|
83
|
-
content = this.
|
|
80
|
+
content = this.renderDefaultAvatar()
|
|
81
|
+
}
|
|
82
|
+
if (this.type === 'icon') {
|
|
83
|
+
return this.renderIcon(content)
|
|
84
84
|
}
|
|
85
|
+
return this.renderButton(content)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
protected renderButton(content: TemplateResult): TemplateResult {
|
|
85
89
|
return html`
|
|
86
90
|
<button id="button" class="icon-button">
|
|
87
91
|
<md-focus-ring part="focus-ring" for="button"></md-focus-ring>
|
|
@@ -92,13 +96,17 @@ export default class UserAvatar extends LitElement {
|
|
|
92
96
|
`
|
|
93
97
|
}
|
|
94
98
|
|
|
99
|
+
protected renderIcon(content: TemplateResult): TemplateResult {
|
|
100
|
+
return html`<span role="presentation" class="icon">${content}</span>`
|
|
101
|
+
}
|
|
102
|
+
|
|
95
103
|
protected pictureTemplate(url: string): TemplateResult {
|
|
96
104
|
return html`
|
|
97
105
|
<img
|
|
98
106
|
src="${url}"
|
|
99
107
|
alt="${this.userInitials || 'Thumb'}"
|
|
100
108
|
class="user-picture user-icon"
|
|
101
|
-
@error="${this.
|
|
109
|
+
@error="${this.handlePictureError}"
|
|
102
110
|
/>
|
|
103
111
|
`
|
|
104
112
|
}
|
|
@@ -110,4 +118,26 @@ export default class UserAvatar extends LitElement {
|
|
|
110
118
|
protected nameTemplate(initials: string): TemplateResult {
|
|
111
119
|
return html` <span class="avatar-initials user-icon">${initials}</span> `
|
|
112
120
|
}
|
|
121
|
+
|
|
122
|
+
protected renderDefaultAvatar(): TemplateResult {
|
|
123
|
+
return html`<span class="avatar-default icon"> ${this.defaultIcon()} </span>`
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
protected defaultIcon(): SVGTemplateResult {
|
|
127
|
+
return svg`
|
|
128
|
+
<svg viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon">
|
|
129
|
+
<rect width="120" height="120" rx="60" class="avatar-background" />
|
|
130
|
+
<path
|
|
131
|
+
fill-rule="evenodd"
|
|
132
|
+
clip-rule="evenodd"
|
|
133
|
+
d="M78.0007 48C78.0007 57.9411 69.9419 66 60.0007 66C50.0596 66 42.0007 57.9411 42.0007 48C42.0007 38.0589 50.0596 30 60.0007 30C69.9419 30 78.0007 38.0589 78.0007 48ZM72.0007 48C72.0007 54.6274 66.6282 60 60.0007 60C53.3733 60 48.0007 54.6274 48.0007 48C48.0007 41.3726 53.3733 36 60.0007 36C66.6282 36 72.0007 41.3726 72.0007 48Z"
|
|
134
|
+
class="avatar-lines"
|
|
135
|
+
/>
|
|
136
|
+
<path
|
|
137
|
+
d="M60.0007 75C40.5776 75 24.0286 86.4852 17.7246 102.576C19.2603 104.101 20.878 105.543 22.5706 106.896C27.2648 92.1231 41.9909 81 60.0007 81C78.0106 81 92.7367 92.1231 97.4309 106.896C99.1235 105.544 100.741 104.101 102.277 102.576C95.973 86.4853 79.4239 75 60.0007 75Z"
|
|
138
|
+
class="avatar-lines"
|
|
139
|
+
/>
|
|
140
|
+
</svg>
|
|
141
|
+
`
|
|
142
|
+
}
|
|
113
143
|
}
|