@sd-angular/core 19.0.45 → 19.0.47
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/README.md +3 -1
- package/assets/scss/core/form.scss +33 -4
- package/assets/scss/themes/_material-tokens.scss +10 -25
- package/components/upload-file/src/upload-file.component.d.ts +28 -2
- package/fesm2022/sd-angular-core-components-upload-file.mjs +142 -56
- package/fesm2022/sd-angular-core-components-upload-file.mjs.map +1 -1
- package/fesm2022/sd-angular-core-forms-select.mjs +15 -16
- package/fesm2022/sd-angular-core-forms-select.mjs.map +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -120,7 +120,9 @@ export const appConfig: ApplicationConfig = {
|
|
|
120
120
|
| `--sd-view-label-color` | `var(--sd-black400)` | Label chỉ-đọc (`sd-view`, control ở `viewed`, `sd-section-item`), class `.sd-view-label` |
|
|
121
121
|
| `--sd-view-value-color` | `var(--sd-black500)` | Giá trị chỉ-đọc, class `.sd-view-value` |
|
|
122
122
|
|
|
123
|
-
`theme()` còn ánh xạ token Material của form field, option và datepicker sang palette này (`assets/scss/themes/_material-tokens.scss`):
|
|
123
|
+
`theme()` còn ánh xạ token Material của form field, option và datepicker sang palette này (`assets/scss/themes/_material-tokens.scss`): label/caret/mũi tên select khi focus và option/ngày đang chọn dùng `--sd-primary`. Đổi `primary` trong `theme()` là các trạng thái này đổi theo.
|
|
124
|
+
|
|
125
|
+
Viền (outline) của form field luôn là `--sd-black200` ở mọi trạng thái — nghỉ, hover, focus (chỉ dày hơn) và lỗi (`assets/scss/core/form.scss`, `!important`). Field lỗi báo bằng label, message và dấu `*` màu đỏ; riêng field `hideInlineError` bị lỗi (không có message) có viền `--sd-error`.
|
|
124
126
|
|
|
125
127
|
### Custom Theme / Tuỳ chỉnh theme
|
|
126
128
|
|
|
@@ -11,8 +11,22 @@
|
|
|
11
11
|
color: map.get(color.$color_map, 'error');
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
//
|
|
15
|
-
//
|
|
14
|
+
// Xử lý màu border nhạt bớt
|
|
15
|
+
// why: viền xám ở MỌI trạng thái (nghỉ, hover, focus, lỗi) — focus chỉ dày hơn, lỗi báo bằng label và
|
|
16
|
+
// message đỏ. 19.0.45 từng đổi viền focus sang primary và viền lỗi sang đỏ; 19.0.46 trả lại giao diện
|
|
17
|
+
// cũ vì các app đang dùng không được báo trước thay đổi này.
|
|
18
|
+
.mat-mdc-notch-piece {
|
|
19
|
+
&.mdc-notched-outline__leading {
|
|
20
|
+
border-color: map.get(color.$color_map, 'black200') !important;
|
|
21
|
+
}
|
|
22
|
+
&.mdc-notched-outline__notch {
|
|
23
|
+
border-top-color: map.get(color.$color_map, 'black200') !important;
|
|
24
|
+
border-bottom-color: map.get(color.$color_map, 'black200') !important;
|
|
25
|
+
}
|
|
26
|
+
&.mdc-notched-outline__trailing {
|
|
27
|
+
border-color: map.get(color.$color_map, 'black200') !important;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
16
30
|
|
|
17
31
|
// Xử lý icon
|
|
18
32
|
.sd-suffix-icon {
|
|
@@ -113,12 +127,27 @@
|
|
|
113
127
|
}
|
|
114
128
|
}
|
|
115
129
|
|
|
116
|
-
// Bỏ padding nếu hideInlineError
|
|
117
|
-
// như mọi field khác.
|
|
130
|
+
// Bỏ padding nếu hideInlineError
|
|
118
131
|
&.hide-inline-error {
|
|
119
132
|
.mat-mdc-form-field-subscript-wrapper {
|
|
120
133
|
display: none;
|
|
121
134
|
}
|
|
135
|
+
|
|
136
|
+
// why: không có message lỗi bên dưới nên field lỗi tô viền đỏ — ngoại lệ duy nhất của viền xám ở trên.
|
|
137
|
+
&.mat-form-field-invalid {
|
|
138
|
+
.mat-mdc-notch-piece {
|
|
139
|
+
&.mdc-notched-outline__leading {
|
|
140
|
+
border-color: map.get(color.$color_map, 'error') !important;
|
|
141
|
+
}
|
|
142
|
+
&.mdc-notched-outline__notch {
|
|
143
|
+
border-top-color: map.get(color.$color_map, 'error') !important;
|
|
144
|
+
border-bottom-color: map.get(color.$color_map, 'error') !important;
|
|
145
|
+
}
|
|
146
|
+
&.mdc-notched-outline__trailing {
|
|
147
|
+
border-color: map.get(color.$color_map, 'error') !important;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
122
151
|
}
|
|
123
152
|
|
|
124
153
|
// Size normal`
|
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
// Angular Material component tokens mapped onto the Core palette.
|
|
2
2
|
//
|
|
3
|
-
// why:
|
|
4
|
-
//
|
|
5
|
-
// a grey outline under a red label. The colours now go through Material's own tokens, declared by
|
|
6
|
-
// theme() so every theme scope resolves them against its palette and a parent of a field can still
|
|
7
|
-
// override a token for its subtree.
|
|
3
|
+
// why: theme() declares these, so every theme scope resolves them against its palette and a parent of a
|
|
4
|
+
// field can still override a token for its subtree.
|
|
8
5
|
//
|
|
9
|
-
// -
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
6
|
+
// - Outline: not here. `core/form.scss` forces `--sd-black200` with `!important` on the outline pieces in
|
|
7
|
+
// every state (red only for an invalid `hideInlineError` field). 19.0.45 moved the outline onto the
|
|
8
|
+
// focus/error tokens (primary while focused, red when invalid); 19.0.46 restored the grey outline.
|
|
9
|
+
// - Focus: --sd-primary for the floating label, caret, select arrow, datepicker toggle and the filled
|
|
10
|
+
// active indicator.
|
|
11
|
+
// - Error: Material's own error colours (the Material 2 warn palette of `material-theme.scss`): red label,
|
|
12
|
+
// caret and select arrow; the filled active indicator turns --sd-error.
|
|
14
13
|
// - Options, checkmarks and datepicker: selected state in --sd-primary.
|
|
15
14
|
//
|
|
16
15
|
// Token names are the Material 19 ones (`--mdc-*-text-field-*`, `--mat-{full,minimal}-pseudo-checkbox-*`).
|
|
17
|
-
// The label references fall back to the Material 2 theme values of `material-theme.scss`.
|
|
18
16
|
|
|
19
17
|
$-text-field-prefixes: (
|
|
20
18
|
outlined: '--mdc-outlined-text-field-',
|
|
@@ -24,20 +22,8 @@ $-text-field-prefixes: (
|
|
|
24
22
|
@mixin emit() {
|
|
25
23
|
@each $appearance, $prefix in $-text-field-prefixes {
|
|
26
24
|
#{$prefix}caret-color: var(--sd-primary);
|
|
27
|
-
#{$prefix}error-caret-color: var(--sd-primary);
|
|
28
25
|
#{$prefix}focus-label-text-color: var(--sd-primary);
|
|
29
|
-
|
|
30
|
-
#{$prefix}error-label-text-color: var(#{$prefix}label-text-color, rgba(0, 0, 0, 0.6));
|
|
31
|
-
#{$prefix}error-hover-label-text-color: var(#{$prefix}hover-label-text-color, rgba(0, 0, 0, 0.87));
|
|
32
|
-
@if $appearance == outlined {
|
|
33
|
-
#{$prefix}outline-color: var(--sd-black200);
|
|
34
|
-
#{$prefix}hover-outline-color: var(--sd-black200);
|
|
35
|
-
#{$prefix}disabled-outline-color: var(--sd-black200);
|
|
36
|
-
#{$prefix}focus-outline-color: var(--sd-primary);
|
|
37
|
-
#{$prefix}error-outline-color: var(--sd-error);
|
|
38
|
-
#{$prefix}error-hover-outline-color: var(--sd-error);
|
|
39
|
-
#{$prefix}error-focus-outline-color: var(--sd-error);
|
|
40
|
-
} @else {
|
|
26
|
+
@if $appearance == filled {
|
|
41
27
|
#{$prefix}focus-active-indicator-color: var(--sd-primary);
|
|
42
28
|
#{$prefix}error-active-indicator-color: var(--sd-error);
|
|
43
29
|
#{$prefix}error-hover-active-indicator-color: var(--sd-error);
|
|
@@ -46,7 +32,6 @@ $-text-field-prefixes: (
|
|
|
46
32
|
}
|
|
47
33
|
--mat-form-field-focus-select-arrow-color: var(--sd-primary);
|
|
48
34
|
--mat-select-focused-arrow-color: var(--sd-primary);
|
|
49
|
-
--mat-select-invalid-arrow-color: var(--mat-select-enabled-arrow-color, rgba(0, 0, 0, 0.54));
|
|
50
35
|
--mat-datepicker-toggle-active-state-icon-color: var(--sd-primary);
|
|
51
36
|
|
|
52
37
|
--mat-option-selected-state-label-text-color: var(--sd-primary);
|
|
@@ -66,8 +66,34 @@ export declare class SdUploadFile<TArgs = any> {
|
|
|
66
66
|
*/
|
|
67
67
|
getFiles: () => Promise<File[]>;
|
|
68
68
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
69
|
+
* Upload the local files referenced by the model, then replace their temporary
|
|
70
|
+
* keys with server IDs. Call and await this method before submitting the model.
|
|
71
|
+
*
|
|
72
|
+
* Files are read from the model and local snapshots/cache, so pending or failed metadata
|
|
73
|
+
* requests do not omit files. Each distinct local key is uploaded once per
|
|
74
|
+
* batch; duplicate occurrences retain their positions and share the returned ID.
|
|
75
|
+
* Server IDs and URLs are preserved even when they have no preview metadata.
|
|
76
|
+
*
|
|
77
|
+
* Concurrent calls share the active batch. Changes made while it is pending
|
|
78
|
+
* are preserved: removed keys stay removed, reordering is retained, and newly
|
|
79
|
+
* added local keys remain pending for a subsequent call. After destruction, no
|
|
80
|
+
* new upload starts and an in-flight upload cannot update the model.
|
|
81
|
+
*
|
|
82
|
+
* @returns A promise resolving to void after the batch finishes. The preview
|
|
83
|
+
* metadata refresh may still be pending. With no local files this is a no-op.
|
|
84
|
+
* A missing upload handler displays the configuration error and resolves
|
|
85
|
+
* without changing the model or cache.
|
|
86
|
+
* @throws The upload handler's error, without changing the model or cache.
|
|
87
|
+
* @throws Error if the handler does not return exactly one non-empty string ID
|
|
88
|
+
* per distinct local key; the model and cache are retained in that case.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* import { SdUploadFile } from '@sd-angular/core/components/upload-file';
|
|
92
|
+
*
|
|
93
|
+
* async function submit(control: SdUploadFile) {
|
|
94
|
+
* await control.upload();
|
|
95
|
+
* return control.model(); // Server IDs/URLs for the uploaded batch.
|
|
96
|
+
* }
|
|
71
97
|
*/
|
|
72
98
|
upload: () => Promise<void>;
|
|
73
99
|
/**
|
|
@@ -199,6 +199,9 @@ class SdUploadFile {
|
|
|
199
199
|
#canvas2 = `C${Utilities.generateUuid()}`;
|
|
200
200
|
#name = Utilities.generateUuid();
|
|
201
201
|
#formGroup;
|
|
202
|
+
#uploadPromise;
|
|
203
|
+
#destroyed = false;
|
|
204
|
+
#localFiles = new Map();
|
|
202
205
|
formControl = new SdFormControl();
|
|
203
206
|
// why: component là OnPush nhưng template đọc formControl.touched/errors như thuộc tính
|
|
204
207
|
// THƯỜNG (không reactive). Khi cha submit (markAllAsTouched) hoặc validator gắn lỗi, không
|
|
@@ -324,19 +327,34 @@ class SdUploadFile {
|
|
|
324
327
|
}
|
|
325
328
|
});
|
|
326
329
|
// Reactive sync for model changes
|
|
327
|
-
effect(
|
|
328
|
-
const
|
|
330
|
+
effect(onCleanup => {
|
|
331
|
+
const modelValue = this.model();
|
|
332
|
+
const values = modelValue || [];
|
|
333
|
+
let active = true;
|
|
334
|
+
let cancelReaders;
|
|
335
|
+
// why: model có thể đổi trước lần effect kế tiếp; cleanup thôi chưa chặn được
|
|
336
|
+
// Promise cũ resolve trong khoảng đó. Destroy cũng phải chặn mọi emit/ghi state.
|
|
337
|
+
const isCurrent = () => active && !this.#destroyed && this.model() === modelValue;
|
|
338
|
+
onCleanup(() => {
|
|
339
|
+
active = false;
|
|
340
|
+
cancelReaders?.();
|
|
341
|
+
});
|
|
329
342
|
this.formControl.setValue(values);
|
|
330
343
|
if (Array.isArray(values)) {
|
|
331
344
|
this.#details(values).then(previewFiles => {
|
|
345
|
+
if (!isCurrent())
|
|
346
|
+
return;
|
|
332
347
|
this.previewFiles.set(previewFiles);
|
|
333
348
|
this.filesChanged.emit(this.previewFiles()
|
|
334
349
|
.map(e => e.file || e.idOrKey || e.src)
|
|
335
350
|
.filter(val => !!val));
|
|
336
|
-
|
|
351
|
+
// why: consumer của filesChanged có thể đổi model hoặc destroy đồng bộ.
|
|
352
|
+
if (isCurrent())
|
|
353
|
+
cancelReaders = this.#preview(isCurrent);
|
|
337
354
|
});
|
|
338
355
|
}
|
|
339
356
|
else {
|
|
357
|
+
this.#localFiles.clear();
|
|
340
358
|
this.previewFiles.set([]);
|
|
341
359
|
}
|
|
342
360
|
});
|
|
@@ -364,6 +382,8 @@ class SdUploadFile {
|
|
|
364
382
|
});
|
|
365
383
|
// Cleanup on destroy
|
|
366
384
|
this.#destroyRef.onDestroy(() => {
|
|
385
|
+
this.#destroyed = true;
|
|
386
|
+
this.#localFiles.clear();
|
|
367
387
|
this.#revokePreviewBlobURLs();
|
|
368
388
|
this.#formGroup?.removeControl(this.#name);
|
|
369
389
|
});
|
|
@@ -495,17 +515,28 @@ class SdUploadFile {
|
|
|
495
515
|
}
|
|
496
516
|
return null;
|
|
497
517
|
};
|
|
498
|
-
#preview = () => {
|
|
518
|
+
#preview = (isCurrent) => {
|
|
519
|
+
const readers = [];
|
|
499
520
|
for (const previewFile of this.previewFiles().filter(e => e.file)) {
|
|
500
521
|
const reader = new FileReader();
|
|
501
522
|
reader.onload = evt => {
|
|
523
|
+
if (!isCurrent())
|
|
524
|
+
return;
|
|
502
525
|
previewFile.previewSrc = evt.target.result;
|
|
503
526
|
previewFile.isPreviewImage = previewFile.file?.type.split('/')[0] === 'image' && previewFile.file?.type !== 'image/tiff';
|
|
504
527
|
// trigger signal update
|
|
505
528
|
this.previewFiles.set([...this.previewFiles()]);
|
|
506
529
|
};
|
|
530
|
+
readers.push(reader);
|
|
507
531
|
reader.readAsDataURL(previewFile.file);
|
|
508
532
|
}
|
|
533
|
+
return () => {
|
|
534
|
+
for (const reader of readers) {
|
|
535
|
+
reader.onload = null;
|
|
536
|
+
if (reader.readyState === FileReader.LOADING)
|
|
537
|
+
reader.abort();
|
|
538
|
+
}
|
|
539
|
+
};
|
|
509
540
|
};
|
|
510
541
|
// ─── Public Event Handlers ────────────────────────────────────────────
|
|
511
542
|
onUpload = () => {
|
|
@@ -535,6 +566,7 @@ class SdUploadFile {
|
|
|
535
566
|
};
|
|
536
567
|
#uploadFile = async (originFiles) => {
|
|
537
568
|
const currentModel = [...(this.model() || [])];
|
|
569
|
+
const pickedFiles = new Map();
|
|
538
570
|
if (originFiles.length + currentModel.length > this.max()) {
|
|
539
571
|
this.#notifyService.warning(this.#i18n.t('core.component.upload-file.too-many-files'));
|
|
540
572
|
return;
|
|
@@ -551,9 +583,15 @@ class SdUploadFile {
|
|
|
551
583
|
}
|
|
552
584
|
const hashedKey = this.#uploadFileService.add(file);
|
|
553
585
|
if (hashedKey) {
|
|
586
|
+
pickedFiles.set(hashedKey, file);
|
|
554
587
|
currentModel.push(hashedKey);
|
|
555
588
|
}
|
|
556
589
|
}
|
|
590
|
+
// why: giữ batch riêng qua từng await validate/resize; cache chung và effect của
|
|
591
|
+
// model cũ có thể mất hash trước khi picker hoàn tất. Chỉ nhận ownership khi commit.
|
|
592
|
+
for (const [key, file] of pickedFiles)
|
|
593
|
+
this.#localFiles.set(key, file);
|
|
594
|
+
this.#snapshotFiles(currentModel);
|
|
557
595
|
this.model.set(currentModel);
|
|
558
596
|
this.#touch();
|
|
559
597
|
};
|
|
@@ -685,50 +723,81 @@ class SdUploadFile {
|
|
|
685
723
|
return items.map(e => e?.file).filter(file => !!file);
|
|
686
724
|
};
|
|
687
725
|
/**
|
|
688
|
-
*
|
|
689
|
-
*
|
|
726
|
+
* Upload the local files referenced by the model, then replace their temporary
|
|
727
|
+
* keys with server IDs. Call and await this method before submitting the model.
|
|
728
|
+
*
|
|
729
|
+
* Files are read from the model and local snapshots/cache, so pending or failed metadata
|
|
730
|
+
* requests do not omit files. Each distinct local key is uploaded once per
|
|
731
|
+
* batch; duplicate occurrences retain their positions and share the returned ID.
|
|
732
|
+
* Server IDs and URLs are preserved even when they have no preview metadata.
|
|
733
|
+
*
|
|
734
|
+
* Concurrent calls share the active batch. Changes made while it is pending
|
|
735
|
+
* are preserved: removed keys stay removed, reordering is retained, and newly
|
|
736
|
+
* added local keys remain pending for a subsequent call. After destruction, no
|
|
737
|
+
* new upload starts and an in-flight upload cannot update the model.
|
|
738
|
+
*
|
|
739
|
+
* @returns A promise resolving to void after the batch finishes. The preview
|
|
740
|
+
* metadata refresh may still be pending. With no local files this is a no-op.
|
|
741
|
+
* A missing upload handler displays the configuration error and resolves
|
|
742
|
+
* without changing the model or cache.
|
|
743
|
+
* @throws The upload handler's error, without changing the model or cache.
|
|
744
|
+
* @throws Error if the handler does not return exactly one non-empty string ID
|
|
745
|
+
* per distinct local key; the model and cache are retained in that case.
|
|
746
|
+
*
|
|
747
|
+
* @example
|
|
748
|
+
* import { SdUploadFile } from '@sd-angular/core/components/upload-file';
|
|
749
|
+
*
|
|
750
|
+
* async function submit(control: SdUploadFile) {
|
|
751
|
+
* await control.upload();
|
|
752
|
+
* return control.model(); // Server IDs/URLs for the uploaded batch.
|
|
753
|
+
* }
|
|
690
754
|
*/
|
|
691
|
-
upload =
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
755
|
+
upload = () => {
|
|
756
|
+
if (this.#destroyed)
|
|
757
|
+
return Promise.resolve();
|
|
758
|
+
if (!this.#uploadPromise) {
|
|
759
|
+
// why: hai lần submit đồng thời phải dùng chung batch, tránh upload trùng File.
|
|
760
|
+
this.#uploadPromise = this.#uploadPendingFiles().finally(() => {
|
|
761
|
+
this.#uploadPromise = undefined;
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
return this.#uploadPromise;
|
|
765
|
+
};
|
|
766
|
+
#uploadPendingFiles = async () => {
|
|
767
|
+
const pending = [
|
|
768
|
+
...new Map(this.#snapshotFiles(this.model())
|
|
769
|
+
.filter(entry => entry.file)
|
|
770
|
+
.map(entry => [entry.key, entry])).values(),
|
|
771
|
+
];
|
|
772
|
+
if (!pending.length)
|
|
773
|
+
return;
|
|
774
|
+
const uploadHandler = this.#getUploadHandler();
|
|
775
|
+
if (!uploadHandler) {
|
|
776
|
+
const key = this.key();
|
|
777
|
+
this.#notifyService.error(key === undefined
|
|
778
|
+
? this.#i18n.t('core.component.upload-file.no-configuration')
|
|
779
|
+
: this.#i18n.t('core.component.upload-file.no-configuration-key', { key }));
|
|
695
780
|
return;
|
|
696
781
|
}
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
if (uploadHandler) {
|
|
703
|
-
idOrKeys = await uploadHandler(uploadedFiles, this.args()).then(results => {
|
|
704
|
-
for (const uploadedFile of uploadedFiles) {
|
|
705
|
-
this.#uploadFileService.remove(uploadedFile);
|
|
706
|
-
}
|
|
707
|
-
return results;
|
|
708
|
-
});
|
|
709
|
-
}
|
|
710
|
-
else {
|
|
711
|
-
const key = this.key();
|
|
712
|
-
this.#notifyService.error(key === undefined
|
|
713
|
-
? this.#i18n.t('core.component.upload-file.no-configuration')
|
|
714
|
-
: this.#i18n.t('core.component.upload-file.no-configuration-key', { key }));
|
|
715
|
-
}
|
|
782
|
+
const idOrKeys = await uploadHandler(pending.map(entry => entry.file), this.args());
|
|
783
|
+
if (!Array.isArray(idOrKeys) ||
|
|
784
|
+
idOrKeys.length !== pending.length ||
|
|
785
|
+
pending.some((_, index) => typeof idOrKeys[index] !== 'string' || !idOrKeys[index])) {
|
|
786
|
+
throw new Error('[sd-upload-file] Upload must return one non-empty string ID per file.');
|
|
716
787
|
}
|
|
717
|
-
|
|
718
|
-
for (const
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
}
|
|
788
|
+
const replacements = new Map(pending.map((entry, index) => [entry.key, idOrKeys[index]]));
|
|
789
|
+
for (const entry of pending) {
|
|
790
|
+
this.#uploadFileService.remove(entry.key.toString());
|
|
791
|
+
this.#localFiles.delete(entry.key.toString());
|
|
792
|
+
}
|
|
793
|
+
if (this.#destroyed)
|
|
794
|
+
return;
|
|
795
|
+
// why: thay đúng hash trên model MỚI NHẤT, không phục hồi file đã xoá hoặc
|
|
796
|
+
// ghi đè các thao tác thêm/sắp xếp trong lúc upload đang chờ.
|
|
797
|
+
const values = this.model();
|
|
798
|
+
if (Array.isArray(values)) {
|
|
799
|
+
this.model.set(values.map(key => replacements.get(key) ?? key));
|
|
730
800
|
}
|
|
731
|
-
this.model.set(results);
|
|
732
801
|
};
|
|
733
802
|
/**
|
|
734
803
|
* Mở popup preview ảnh.
|
|
@@ -771,20 +840,23 @@ class SdUploadFile {
|
|
|
771
840
|
* cần render ra ảnh/tệp tương ứng.
|
|
772
841
|
*/
|
|
773
842
|
#details = async (keys) => {
|
|
774
|
-
|
|
843
|
+
const entries = this.#snapshotFiles(keys);
|
|
844
|
+
if (!entries.length) {
|
|
775
845
|
return [];
|
|
776
846
|
}
|
|
777
|
-
const idOrKeys =
|
|
847
|
+
const idOrKeys = entries.filter(entry => !this.#isCdn(entry.key) && !entry.file).map(entry => entry.key);
|
|
778
848
|
let detailsList = [];
|
|
779
849
|
const detailsHandler = this.#getDetailsHandler();
|
|
780
850
|
if (idOrKeys.length && detailsHandler) {
|
|
781
|
-
|
|
851
|
+
try {
|
|
852
|
+
detailsList = await detailsHandler(idOrKeys, this.args());
|
|
853
|
+
}
|
|
854
|
+
catch (error) {
|
|
782
855
|
console.error(error);
|
|
783
|
-
|
|
784
|
-
});
|
|
856
|
+
}
|
|
785
857
|
}
|
|
786
858
|
const results = [];
|
|
787
|
-
for (const key
|
|
859
|
+
for (const { key, file } of entries) {
|
|
788
860
|
if (this.#isCdn(key)) {
|
|
789
861
|
const cdn = key?.toString();
|
|
790
862
|
const extension = this.#getExtension(cdn);
|
|
@@ -799,9 +871,7 @@ class SdUploadFile {
|
|
|
799
871
|
extension,
|
|
800
872
|
});
|
|
801
873
|
}
|
|
802
|
-
else if (
|
|
803
|
-
const hashedKey = key?.toString();
|
|
804
|
-
const file = this.#uploadFileService.get(hashedKey);
|
|
874
|
+
else if (file) {
|
|
805
875
|
const lastDot = file.name.lastIndexOf('.');
|
|
806
876
|
const extension = file.name.substring(lastDot + 1);
|
|
807
877
|
const isPreviewImage = (file.type.split('/')[0] === 'image' && file.type !== 'image/tiff') || IsImage(extension);
|
|
@@ -842,10 +912,26 @@ class SdUploadFile {
|
|
|
842
912
|
return false;
|
|
843
913
|
return idOrKey.startsWith('http');
|
|
844
914
|
};
|
|
845
|
-
#
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
915
|
+
#snapshotFiles = (keys) => {
|
|
916
|
+
const values = Array.isArray(keys) ? keys : [];
|
|
917
|
+
const activeKeys = new Set(values);
|
|
918
|
+
for (const key of this.#localFiles.keys()) {
|
|
919
|
+
if (!activeKeys.has(key))
|
|
920
|
+
this.#localFiles.delete(key);
|
|
921
|
+
}
|
|
922
|
+
// why: cache có thể bị upload xoá trong lúc chờ details; giữ File theo snapshot
|
|
923
|
+
// của từng ô và request, kể cả khi ô khác upload cùng hash rồi xoá cache chung.
|
|
924
|
+
return values
|
|
925
|
+
.filter(key => !!key)
|
|
926
|
+
.map(key => {
|
|
927
|
+
let file = null;
|
|
928
|
+
if (typeof key === 'string' && !this.#isCdn(key)) {
|
|
929
|
+
file = this.#localFiles.get(key) ?? this.#uploadFileService.get(key);
|
|
930
|
+
if (file)
|
|
931
|
+
this.#localFiles.set(key, file);
|
|
932
|
+
}
|
|
933
|
+
return { key, file };
|
|
934
|
+
});
|
|
849
935
|
};
|
|
850
936
|
#getExtension = (cdn) => {
|
|
851
937
|
if (!cdn)
|