@simplysm/angular 14.0.86 → 14.0.88
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/dist/controls/pagination/sd-pagination.d.ts.map +1 -1
- package/dist/controls/pagination/sd-pagination.js +2 -0
- package/dist/controls/select/sd-select-item.js +1 -1
- package/dist/controls/select/sd-select.js +1 -1
- package/dist/core/app-structure/sd-app-structure.provider.d.ts +1 -0
- package/dist/core/app-structure/sd-app-structure.provider.d.ts.map +1 -1
- package/dist/core/app-structure/sd-app-structure.provider.js +3 -0
- package/dist/core/app-structure/sd-app-structure.utils.d.ts +2 -0
- package/dist/core/app-structure/sd-app-structure.utils.d.ts.map +1 -1
- package/dist/core/app-structure/sd-app-structure.utils.js +9 -1
- package/dist/core/busy/sd-busy-container.d.ts +1 -1
- package/dist/core/busy/sd-busy-container.d.ts.map +1 -1
- package/dist/core/busy/sd-busy-container.js +11 -11
- package/dist/core/busy/sd-busy.provider.d.ts.map +1 -1
- package/dist/core/busy/sd-busy.provider.js +3 -0
- package/dist/core/config/sd-system-config.provider.d.ts +4 -4
- package/dist/core/config/sd-system-config.provider.d.ts.map +1 -1
- package/dist/core/config/sd-system-config.provider.js +3 -0
- package/dist/core/error-handler/sd-global-error-handler.plugin.d.ts +1 -1
- package/dist/core/error-handler/sd-global-error-handler.plugin.d.ts.map +1 -1
- package/dist/core/file-dialog/sd-file-dialog.provider.d.ts.map +1 -1
- package/dist/core/file-dialog/sd-file-dialog.provider.js +4 -0
- package/dist/core/modal/sd-activated-modal.provider.d.ts +2 -1
- package/dist/core/modal/sd-activated-modal.provider.d.ts.map +1 -1
- package/dist/core/modal/sd-modal.d.ts.map +1 -1
- package/dist/core/modal/sd-modal.js +3 -3
- package/dist/core/modal/sd-modal.provider.d.ts.map +1 -1
- package/dist/core/modal/sd-modal.provider.js +8 -0
- package/dist/core/modal/sd-prompt-modal.d.ts +2 -2
- package/dist/core/modal/sd-prompt-modal.d.ts.map +1 -1
- package/dist/core/modal/sd-prompt-modal.js +32 -37
- package/dist/core/print/sd-print.provider.d.ts.map +1 -1
- package/dist/core/print/sd-print.provider.js +19 -6
- package/dist/core/provideSdAngular.d.ts.map +1 -1
- package/dist/core/provideSdAngular.js +4 -1
- package/dist/core/routing/injectViewTitleSignal.d.ts.map +1 -1
- package/dist/core/routing/injectViewTitleSignal.js +1 -6
- package/dist/core/shared-data/sd-shared-data.provider.d.ts +2 -1
- package/dist/core/shared-data/sd-shared-data.provider.d.ts.map +1 -1
- package/dist/core/shared-data/sd-shared-data.provider.js +1 -1
- package/dist/core/toast/sd-toast.provider.d.ts +1 -0
- package/dist/core/toast/sd-toast.provider.d.ts.map +1 -1
- package/dist/core/toast/sd-toast.provider.js +18 -4
- package/dist/data/kanban/sd-kanban-lane.d.ts.map +1 -1
- package/dist/data/shared-data/sd-shared-data-select-button.d.ts +1 -2
- package/dist/data/shared-data/sd-shared-data-select-button.d.ts.map +1 -1
- package/dist/data/shared-data/sd-shared-data-select-button.js +18 -21
- package/dist/data/shared-data/sd-shared-data-select.d.ts +3 -2
- package/dist/data/shared-data/sd-shared-data-select.d.ts.map +1 -1
- package/dist/data/shared-data/sd-shared-data-select.js +10 -10
- package/dist/data/sheet/sd-sheet-config.modal.d.ts +3 -3
- package/dist/data/sheet/sd-sheet-config.modal.d.ts.map +1 -1
- package/dist/data/sheet/sd-sheet-config.modal.js +37 -34
- package/dist/data/sheet/sd-sheet.d.ts.map +1 -1
- package/dist/data/sheet/sd-sheet.js +5 -1
- package/dist/data/sheet/useSheetCellStyling.d.ts.map +1 -1
- package/dist/data/sheet/useSheetCellStyling.js +4 -22
- package/dist/data/state-preset/sd-state-preset.d.ts +10 -10
- package/dist/data/state-preset/sd-state-preset.d.ts.map +1 -1
- package/dist/data/state-preset/sd-state-preset.js +4 -4
- package/dist/features/address/sd-address-search.modal.js +1 -1
- package/dist/features/editor/sd-tiptap-editor.d.ts.map +1 -1
- package/dist/features/editor/sd-tiptap-editor.js +3 -2
- package/dist/features/theme/sd-theme-selector.d.ts +6 -8
- package/dist/features/theme/sd-theme-selector.d.ts.map +1 -1
- package/dist/features/theme/sd-theme-selector.js +54 -51
- package/dist/features/visual/sd-barcode.d.ts.map +1 -1
- package/dist/features/visual/sd-barcode.js +5 -10
- package/dist/layout/sidebar/sd-sidebar-user.d.ts.map +1 -1
- package/dist/layout/sidebar/sd-sidebar-user.js +26 -36
- package/package.json +5 -5
- package/src/controls/button/sd-modal-select-button.ts +1 -1
- package/src/controls/form/sd-form.ts +1 -1
- package/src/controls/input/sd-textfield-type-handlers.ts +2 -2
- package/src/controls/pagination/sd-pagination.ts +1 -0
- package/src/controls/select/sd-select-item.ts +1 -1
- package/src/controls/select/sd-select.ts +1 -1
- package/src/core/app-structure/sd-app-structure.provider.ts +4 -0
- package/src/core/app-structure/sd-app-structure.utils.ts +14 -2
- package/src/core/busy/sd-busy-container.ts +6 -13
- package/src/core/busy/sd-busy.provider.ts +4 -0
- package/src/core/config/injectSdSystemConfigResource.ts +1 -1
- package/src/core/config/sd-system-config.provider.ts +5 -3
- package/src/core/error-handler/sd-global-error-handler.plugin.ts +1 -1
- package/src/core/file-dialog/sd-file-dialog.provider.ts +5 -1
- package/src/core/modal/sd-activated-modal.provider.ts +2 -1
- package/src/core/modal/sd-modal.provider.ts +8 -0
- package/src/core/modal/sd-modal.ts +6 -3
- package/src/core/modal/sd-prompt-modal.ts +13 -23
- package/src/core/print/sd-print.provider.ts +21 -6
- package/src/core/provideSdAngular.ts +5 -1
- package/src/core/routing/injectViewTitleSignal.ts +1 -5
- package/src/core/shared-data/sd-shared-data.provider.ts +2 -2
- package/src/core/toast/sd-toast.provider.ts +35 -12
- package/src/data/kanban/sd-kanban-lane.ts +2 -2
- package/src/data/shared-data/sd-shared-data-select-button.ts +15 -20
- package/src/data/shared-data/sd-shared-data-select-list.ts +4 -4
- package/src/data/shared-data/sd-shared-data-select.ts +10 -12
- package/src/data/sheet/sd-sheet-column.ts +1 -1
- package/src/data/sheet/sd-sheet-config.modal.ts +39 -35
- package/src/data/sheet/sd-sheet.ts +6 -1
- package/src/data/sheet/useSheetCellStyling.ts +3 -20
- package/src/data/state-preset/sd-state-preset.ts +11 -11
- package/src/features/address/sd-address-search.modal.ts +1 -1
- package/src/features/editor/sd-tiptap-editor.ts +3 -2
- package/src/features/theme/sd-theme-selector.ts +77 -75
- package/src/features/visual/sd-barcode.ts +5 -9
- package/src/features/visual/sd-calendar.ts +1 -1
- package/src/layout/sidebar/sd-sidebar-user.ts +3 -5
|
@@ -9,10 +9,22 @@ import type { SdMenu, SdFlatMenu, SdPermission } from "./sd-app-structure.types"
|
|
|
9
9
|
export abstract class SdAppStructureUtils {
|
|
10
10
|
//---------- Info
|
|
11
11
|
|
|
12
|
-
static getTitleByFullCode<TModule>(items: AppStructureItem<TModule>[], fullCode: string) {
|
|
12
|
+
static getTitleByFullCode<TModule>(items: AppStructureItem<TModule>[], fullCode: string): string {
|
|
13
|
+
const title = this.findTitleByFullCode(items, fullCode);
|
|
14
|
+
if (title == null) {
|
|
15
|
+
throw new Error(`Item not found for fullCode: ${fullCode}`);
|
|
16
|
+
}
|
|
17
|
+
return title;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 항목을 못 찾으면 throw 대신 undefined 반환 (결측 보존). */
|
|
21
|
+
static findTitleByFullCode<TModule>(
|
|
22
|
+
items: AppStructureItem<TModule>[],
|
|
23
|
+
fullCode: string,
|
|
24
|
+
): string | undefined {
|
|
13
25
|
const itemChain = this.getItemChainByFullCode(items, fullCode);
|
|
14
26
|
if (itemChain.length === 0) {
|
|
15
|
-
|
|
27
|
+
return undefined;
|
|
16
28
|
}
|
|
17
29
|
const parent = itemChain
|
|
18
30
|
.slice(0, -1)
|
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
ChangeDetectionStrategy,
|
|
4
4
|
Component,
|
|
5
5
|
computed,
|
|
6
|
-
ElementRef,
|
|
7
6
|
inject,
|
|
8
7
|
input,
|
|
9
8
|
ViewEncapsulation,
|
|
@@ -18,6 +17,7 @@ import { SdBusyProvider, type SdBusyType } from "./sd-busy.provider";
|
|
|
18
17
|
host: {
|
|
19
18
|
"[attr.data-sd-busy]": "busy() || undefined",
|
|
20
19
|
"[attr.data-sd-type]": "currType()",
|
|
20
|
+
"(keydown.capture)": "onKeydownCapture($event)",
|
|
21
21
|
},
|
|
22
22
|
template: `
|
|
23
23
|
<div class="_screen">
|
|
@@ -339,17 +339,10 @@ export class SdBusyContainer {
|
|
|
339
339
|
|
|
340
340
|
currType = computed(() => this.type() ?? this._sdBusy.type());
|
|
341
341
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if (this.busy()) {
|
|
348
|
-
event.preventDefault();
|
|
349
|
-
event.stopPropagation();
|
|
350
|
-
}
|
|
351
|
-
},
|
|
352
|
-
true,
|
|
353
|
-
);
|
|
342
|
+
onKeydownCapture(event: Event): void {
|
|
343
|
+
if (this.busy()) {
|
|
344
|
+
event.preventDefault();
|
|
345
|
+
event.stopPropagation();
|
|
346
|
+
}
|
|
354
347
|
}
|
|
355
348
|
}
|
|
@@ -44,6 +44,10 @@ export class SdBusyProvider {
|
|
|
44
44
|
constructor() {
|
|
45
45
|
effect(() => {
|
|
46
46
|
const busy = this.globalBusyCount() > 0;
|
|
47
|
+
|
|
48
|
+
// busy 가 처음 true 가 되기 전엔 컨테이너를 생성하지 않음 (lazy)
|
|
49
|
+
if (!busy && this._containerRef == null) return;
|
|
50
|
+
|
|
47
51
|
const ref = this.containerRef;
|
|
48
52
|
const el = ref.location.nativeElement as HTMLElement;
|
|
49
53
|
|
|
@@ -20,7 +20,7 @@ export function injectSdSystemConfigResource<T>(options: { key: Signal<string |
|
|
|
20
20
|
const key = options.key();
|
|
21
21
|
if (key == null) return;
|
|
22
22
|
queueMicrotask(() => {
|
|
23
|
-
sdSystemConfig.setAsync(`${elTag}.${key}`, value
|
|
23
|
+
sdSystemConfig.setAsync(`${elTag}.${key}`, value).catch((err) => {
|
|
24
24
|
errorHandler.handleError(err);
|
|
25
25
|
});
|
|
26
26
|
});
|
|
@@ -6,13 +6,15 @@ export class SdSystemConfigProvider<T> {
|
|
|
6
6
|
private readonly _sdLocalStorage = inject<SdLocalStorageProvider<T>>(SdLocalStorageProvider);
|
|
7
7
|
|
|
8
8
|
fn?: {
|
|
9
|
-
set<K extends keyof T & string>(key: K, data: T[K]): Promise<void> | void;
|
|
10
|
-
get(key: keyof T & string): PromiseLike<
|
|
9
|
+
set<K extends keyof T & string>(key: K, data: T[K] | undefined): Promise<void> | void;
|
|
10
|
+
get(key: keyof T & string): PromiseLike<unknown>;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
async setAsync<K extends keyof T & string>(key: K, data: T[K]) {
|
|
13
|
+
async setAsync<K extends keyof T & string>(key: K, data: T[K] | undefined) {
|
|
14
14
|
if (this.fn) {
|
|
15
15
|
await this.fn.set(key, data);
|
|
16
|
+
} else if (data == null) {
|
|
17
|
+
this._sdLocalStorage.remove(key);
|
|
16
18
|
} else {
|
|
17
19
|
this._sdLocalStorage.set(key, data);
|
|
18
20
|
}
|
|
@@ -17,7 +17,7 @@ export class SdGlobalErrorHandlerPlugin implements ErrorHandler {
|
|
|
17
17
|
private readonly _sdSystemLog = inject(SdSystemLogProvider);
|
|
18
18
|
private _hasDisplayedError = false;
|
|
19
19
|
|
|
20
|
-
handleError(event:
|
|
20
|
+
handleError(event: unknown) {
|
|
21
21
|
try {
|
|
22
22
|
if (event instanceof PromiseRejectionEvent) {
|
|
23
23
|
const reason = event.reason;
|
|
@@ -23,7 +23,11 @@ export class SdFileDialogProvider {
|
|
|
23
23
|
}
|
|
24
24
|
inputEl.onchange = (event: Event): void => {
|
|
25
25
|
cleanup();
|
|
26
|
-
const files = (event.target as HTMLInputElement).files
|
|
26
|
+
const files = (event.target as HTMLInputElement).files;
|
|
27
|
+
if (files == null) {
|
|
28
|
+
resolve(undefined);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
27
31
|
resolve(multiple ? Array.from(files) : files[0]);
|
|
28
32
|
};
|
|
29
33
|
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { Injectable, signal } from "@angular/core";
|
|
2
2
|
import type { SdModalContentDef } from "./sd-modal.provider";
|
|
3
|
+
import type { SdModal } from "./sd-modal";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* 모달 내부에서 inject하여 사용하는 프로바이더
|
|
6
7
|
*/
|
|
7
8
|
@Injectable()
|
|
8
9
|
export class SdActivatedModalProvider<T extends SdModalContentDef<any> = SdModalContentDef<any>> {
|
|
9
|
-
modalComponent = signal<
|
|
10
|
+
modalComponent = signal<SdModal | undefined>(undefined);
|
|
10
11
|
contentComponent = signal<T | undefined>(undefined);
|
|
11
12
|
canDeactivateFn: () => boolean = () => true;
|
|
12
13
|
}
|
|
@@ -195,7 +195,12 @@ export class SdModalProvider {
|
|
|
195
195
|
|
|
196
196
|
const modalEl = modalRef.location.nativeElement as HTMLElement;
|
|
197
197
|
|
|
198
|
+
let destroyed = false;
|
|
198
199
|
const doDestroy = () => {
|
|
200
|
+
// transitionend 와 setTimeout 폴백 중 먼저 도달한 쪽에서 1회만 실행
|
|
201
|
+
if (destroyed) return;
|
|
202
|
+
destroyed = true;
|
|
203
|
+
|
|
199
204
|
// DOM에서 제거
|
|
200
205
|
if (modalEl.parentNode != null) {
|
|
201
206
|
modalEl.parentNode.removeChild(modalEl);
|
|
@@ -221,7 +226,10 @@ export class SdModalProvider {
|
|
|
221
226
|
// transition duration 확인 후 대기 또는 즉시 destroy
|
|
222
227
|
const duration = parseFloat(getComputedStyle(modalEl).transitionDuration || "0");
|
|
223
228
|
if (duration > 0) {
|
|
229
|
+
// transitionend 가 정상 경로. 단 백그라운드 탭·transition 인터럽트 시 미발화하므로
|
|
230
|
+
// duration(초→ms) + 여유 100ms 후 발화하는 setTimeout 폴백을 함께 걸어 destroy 누락을 방지.
|
|
224
231
|
modalEl.addEventListener("transitionend", doDestroy, { once: true });
|
|
232
|
+
setTimeout(doDestroy, duration * 1000 + 100);
|
|
225
233
|
} else {
|
|
226
234
|
doDestroy();
|
|
227
235
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
afterNextRender,
|
|
2
3
|
ChangeDetectionStrategy,
|
|
3
4
|
Component,
|
|
4
5
|
effect,
|
|
@@ -323,7 +324,9 @@ import "@simplysm/core-browser";
|
|
|
323
324
|
export class SdModal {
|
|
324
325
|
private readonly _elRef = inject(ElementRef<HTMLElement>);
|
|
325
326
|
private readonly _sdActivatedModal = inject(SdActivatedModalProvider, { optional: true });
|
|
326
|
-
private readonly _sdSystemConfig = inject
|
|
327
|
+
private readonly _sdSystemConfig = inject<
|
|
328
|
+
SdSystemConfigProvider<Record<string, Record<string, string>>>
|
|
329
|
+
>(SdSystemConfigProvider, { optional: true });
|
|
327
330
|
private readonly _errorHandler = inject(ErrorHandler);
|
|
328
331
|
private readonly _focusTrap = injectFocusTrap();
|
|
329
332
|
|
|
@@ -359,7 +362,7 @@ export class SdModal {
|
|
|
359
362
|
|
|
360
363
|
constructor() {
|
|
361
364
|
// data-sd-init: 첫 렌더 후 설정하여 CSS transition 트리거 허용
|
|
362
|
-
|
|
365
|
+
afterNextRender(() => {
|
|
363
366
|
this._elRef.nativeElement.setAttribute("data-sd-init", "");
|
|
364
367
|
});
|
|
365
368
|
|
|
@@ -508,7 +511,7 @@ export class SdModal {
|
|
|
508
511
|
if (dialogEl.style.left !== "") config["left"] = dialogEl.style.left;
|
|
509
512
|
if (dialogEl.style.top !== "") config["top"] = dialogEl.style.top;
|
|
510
513
|
|
|
511
|
-
await this._sdSystemConfig.setAsync(`sd-modal.${k}
|
|
514
|
+
await this._sdSystemConfig.setAsync(`sd-modal.${k}`, config);
|
|
512
515
|
}
|
|
513
516
|
|
|
514
517
|
private async _restoreConfig(k: string): Promise<void> {
|
|
@@ -9,35 +9,33 @@ import {
|
|
|
9
9
|
import type { SdModalContentDef } from "./sd-modal.provider";
|
|
10
10
|
import { SdButton } from "../../controls/button/sd-button";
|
|
11
11
|
import { SdTextfield } from "../../controls/input/sd-textfield";
|
|
12
|
+
import { SdForm } from "../../controls/form/sd-form";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* 범용 프롬프트 모달.
|
|
15
16
|
* SdModalContentDef<string> 구현. message를 표시하고, 텍스트 입력 후 확인/취소.
|
|
16
17
|
* 확인 시 입력값 emit, 취소 시 undefined emit.
|
|
18
|
+
* 입력값은 필수(required)이며, 빈 값으로 확인 시 sd-form 네이티브 검증으로 차단된다.
|
|
17
19
|
*/
|
|
18
20
|
@Component({
|
|
19
21
|
selector: "sd-prompt-modal",
|
|
20
22
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
21
23
|
encapsulation: ViewEncapsulation.None,
|
|
22
24
|
standalone: true,
|
|
23
|
-
imports: [SdButton, SdTextfield],
|
|
25
|
+
imports: [SdForm, SdButton, SdTextfield],
|
|
24
26
|
host: {
|
|
25
27
|
style: "display: block",
|
|
26
28
|
class: "p-default",
|
|
27
29
|
},
|
|
28
30
|
template: `
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
[type]="'text'"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
<div class="flex-row main-align-end gap-sm">
|
|
38
|
-
<sd-button [theme]="'primary'" (click)="onConfirm()">확인</sd-button>
|
|
39
|
-
<sd-button (click)="onCancel()">취소</sd-button>
|
|
40
|
-
</div>
|
|
31
|
+
<sd-form (formSubmit)="onConfirm()">
|
|
32
|
+
<p class="mb-default" [innerHTML]="message()"></p>
|
|
33
|
+
<sd-textfield [type]="'text'" [(value)]="_inputValue" [required]="true" class="mb-default" />
|
|
34
|
+
<div class="flex-row main-align-end gap-sm">
|
|
35
|
+
<sd-button [type]="'submit'" [theme]="'primary'">확인</sd-button>
|
|
36
|
+
<sd-button (click)="onCancel()">취소</sd-button>
|
|
37
|
+
</div>
|
|
38
|
+
</sd-form>
|
|
41
39
|
`,
|
|
42
40
|
})
|
|
43
41
|
export class SdPromptModal implements SdModalContentDef<string> {
|
|
@@ -46,18 +44,10 @@ export class SdPromptModal implements SdModalContentDef<string> {
|
|
|
46
44
|
|
|
47
45
|
message = input.required<string>();
|
|
48
46
|
|
|
49
|
-
_inputValue = signal(
|
|
50
|
-
|
|
51
|
-
onInputChange(event: Event): void {
|
|
52
|
-
const inputEl = event.target as HTMLInputElement;
|
|
53
|
-
this._inputValue.set(inputEl.value);
|
|
54
|
-
}
|
|
47
|
+
_inputValue = signal<string | undefined>(undefined);
|
|
55
48
|
|
|
56
49
|
onConfirm(): void {
|
|
57
|
-
|
|
58
|
-
if (val !== "") {
|
|
59
|
-
this.close.emit(val);
|
|
60
|
-
}
|
|
50
|
+
this.close.emit(this._inputValue());
|
|
61
51
|
}
|
|
62
52
|
|
|
63
53
|
onCancel(): void {
|
|
@@ -9,10 +9,12 @@ import {
|
|
|
9
9
|
} from "@angular/core";
|
|
10
10
|
import type { DirectiveInputSignals, WithOptional } from "../directive-input-signals";
|
|
11
11
|
import { SdBusyProvider } from "../busy/sd-busy.provider";
|
|
12
|
-
import { wait } from "@simplysm/core-common";
|
|
12
|
+
import { createLogger, wait } from "@simplysm/core-common";
|
|
13
13
|
import { jsPDF } from "jspdf";
|
|
14
14
|
import * as htmlToImage from "html-to-image";
|
|
15
15
|
|
|
16
|
+
const logger = createLogger("angular:print");
|
|
17
|
+
|
|
16
18
|
export interface SdPrint {
|
|
17
19
|
initialized: Signal<boolean>;
|
|
18
20
|
readonly _optionalPrintInputs?: string;
|
|
@@ -175,6 +177,7 @@ export class SdPrintProvider {
|
|
|
175
177
|
|
|
176
178
|
private async _waitForAllImagesLoadedAsync(container: HTMLElement): Promise<void> {
|
|
177
179
|
const imgs = Array.from(container.querySelectorAll("img"));
|
|
180
|
+
const failedSrcs: string[] = [];
|
|
178
181
|
|
|
179
182
|
await Promise.all(
|
|
180
183
|
imgs.map((img) => {
|
|
@@ -182,16 +185,28 @@ export class SdPrintProvider {
|
|
|
182
185
|
if (img.complete && img.naturalWidth !== 0) {
|
|
183
186
|
resolve();
|
|
184
187
|
} else {
|
|
185
|
-
const
|
|
186
|
-
img.removeEventListener("load",
|
|
187
|
-
img.removeEventListener("error",
|
|
188
|
+
const cleanup = () => {
|
|
189
|
+
img.removeEventListener("load", onLoad);
|
|
190
|
+
img.removeEventListener("error", onError);
|
|
191
|
+
};
|
|
192
|
+
const onLoad = () => {
|
|
193
|
+
cleanup();
|
|
194
|
+
resolve();
|
|
195
|
+
};
|
|
196
|
+
const onError = () => {
|
|
197
|
+
failedSrcs.push(img.src);
|
|
198
|
+
cleanup();
|
|
188
199
|
resolve();
|
|
189
200
|
};
|
|
190
|
-
img.addEventListener("load",
|
|
191
|
-
img.addEventListener("error",
|
|
201
|
+
img.addEventListener("load", onLoad);
|
|
202
|
+
img.addEventListener("error", onError);
|
|
192
203
|
}
|
|
193
204
|
});
|
|
194
205
|
}),
|
|
195
206
|
);
|
|
207
|
+
|
|
208
|
+
if (failedSrcs.length > 0) {
|
|
209
|
+
logger.error(`인쇄/PDF: 이미지 ${failedSrcs.length}건 로드 실패`, failedSrcs);
|
|
210
|
+
}
|
|
196
211
|
}
|
|
197
212
|
}
|
|
@@ -28,6 +28,9 @@ import { SdThemeProvider } from "../features/theme/sd-theme-provider";
|
|
|
28
28
|
import { SdLocalStorageProvider } from "./config/sd-local-storage.provider";
|
|
29
29
|
import { SdGlobalErrorHandlerPlugin } from "./error-handler/sd-global-error-handler.plugin";
|
|
30
30
|
import { SdOptionEventPlugin } from "./events/sd-option-event.plugin";
|
|
31
|
+
import { createLogger } from "@simplysm/core-common";
|
|
32
|
+
|
|
33
|
+
const logger = createLogger("angular:sw-update");
|
|
31
34
|
|
|
32
35
|
export function provideSdAngular(opt: { clientName: string }): EnvironmentProviders {
|
|
33
36
|
return makeEnvironmentProviders([
|
|
@@ -131,8 +134,9 @@ export function provideSdAngular(opt: { clientName: string }): EnvironmentProvid
|
|
|
131
134
|
}
|
|
132
135
|
}
|
|
133
136
|
failCount = 0;
|
|
134
|
-
} catch {
|
|
137
|
+
} catch (err) {
|
|
135
138
|
failCount++;
|
|
139
|
+
logger.error(err);
|
|
136
140
|
} finally {
|
|
137
141
|
const interval = Math.min(
|
|
138
142
|
BASE_INTERVAL * Math.pow(2, Math.max(0, failCount - 1)),
|
|
@@ -16,10 +16,6 @@ export function injectViewTitleSignal(): Signal<string> {
|
|
|
16
16
|
return sdActivatedModal.modalComponent()?.title() ?? "";
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
return sdAppStructure.getTitleByFullCode(currPageCode?.() ?? fullPageCode());
|
|
21
|
-
} catch {
|
|
22
|
-
return "";
|
|
23
|
-
}
|
|
19
|
+
return sdAppStructure.findTitleByFullCode(currPageCode?.() ?? fullPageCode()) ?? "";
|
|
24
20
|
});
|
|
25
21
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { computed, ErrorHandler, inject, Injectable, signal, type Signal, type WritableSignal } from "@angular/core";
|
|
2
2
|
import { defineEvent } from "@simplysm/service-common";
|
|
3
|
-
import { obj, wait as waitUtil } from "@simplysm/core-common";
|
|
3
|
+
import { DateOnly, DateTime, obj, Time, wait as waitUtil } from "@simplysm/core-common";
|
|
4
4
|
import { SdServiceClientFactoryProvider } from "../service-client/sd-service-client-factory.provider";
|
|
5
5
|
import "@simplysm/core-common";
|
|
6
6
|
|
|
@@ -15,7 +15,7 @@ export interface SharedDataInfo<T extends SharedDataBase<string | number>> {
|
|
|
15
15
|
serviceKey: string;
|
|
16
16
|
getter: (changeKeys?: (string | number)[]) => Promise<T[]>;
|
|
17
17
|
filter?: unknown;
|
|
18
|
-
orderBy?: (item: T) =>
|
|
18
|
+
orderBy?: (item: T) => string | number | DateOnly | DateTime | Time | undefined;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export interface SharedDataHandle<T extends SharedDataBase<string | number>> {
|
|
@@ -45,6 +45,7 @@ export class SdToastProvider {
|
|
|
45
45
|
private _containerRef: ComponentRef<SdToastContainer> | undefined;
|
|
46
46
|
private readonly _toastRefs: ComponentRef<SdToast>[] = [];
|
|
47
47
|
private readonly _contentRefs = new Map<ComponentRef<SdToast>, ComponentRef<any>>();
|
|
48
|
+
private readonly _autoDismissCleanups = new Map<ComponentRef<SdToast>, () => void>();
|
|
48
49
|
|
|
49
50
|
constructor() {
|
|
50
51
|
effect(() => {
|
|
@@ -234,19 +235,30 @@ export class SdToastProvider {
|
|
|
234
235
|
onDismiss?: () => void,
|
|
235
236
|
): void {
|
|
236
237
|
const el = toastRef.location.nativeElement as HTMLElement;
|
|
238
|
+
const ac = new AbortController();
|
|
239
|
+
let timerId: ReturnType<typeof setTimeout> | undefined;
|
|
237
240
|
let isHovering = false;
|
|
238
241
|
let dismissPending = false;
|
|
239
242
|
let dismissed = false;
|
|
240
243
|
|
|
244
|
+
// 타이머·리스너 정리. 외부 파괴 경로(_dismissToast/_destroyToast)에서도 호출되어
|
|
245
|
+
// 죽은 toastRef 에 대해 타이머가 깨어나는 race 와 리스너 누수를 막는다.
|
|
246
|
+
const cleanup = () => {
|
|
247
|
+
dismissed = true;
|
|
248
|
+
if (timerId != null) clearTimeout(timerId);
|
|
249
|
+
ac.abort();
|
|
250
|
+
this._autoDismissCleanups.delete(toastRef);
|
|
251
|
+
};
|
|
252
|
+
this._autoDismissCleanups.set(toastRef, cleanup);
|
|
253
|
+
|
|
241
254
|
const dismiss = () => {
|
|
242
255
|
if (dismissed) return;
|
|
243
|
-
dismissed = true;
|
|
244
256
|
onDismiss?.();
|
|
245
257
|
this._dismissToast(toastRef);
|
|
246
258
|
};
|
|
247
259
|
|
|
248
260
|
const dismissAfterDelay = (ms: number) => {
|
|
249
|
-
setTimeout(() => {
|
|
261
|
+
timerId = setTimeout(() => {
|
|
250
262
|
if (dismissed) return;
|
|
251
263
|
if (isHovering) {
|
|
252
264
|
dismissPending = true;
|
|
@@ -256,21 +268,30 @@ export class SdToastProvider {
|
|
|
256
268
|
}, ms);
|
|
257
269
|
};
|
|
258
270
|
|
|
259
|
-
el.addEventListener(
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
271
|
+
el.addEventListener(
|
|
272
|
+
"mouseenter",
|
|
273
|
+
() => {
|
|
274
|
+
isHovering = true;
|
|
275
|
+
},
|
|
276
|
+
{ signal: ac.signal },
|
|
277
|
+
);
|
|
278
|
+
el.addEventListener(
|
|
279
|
+
"mouseleave",
|
|
280
|
+
() => {
|
|
281
|
+
isHovering = false;
|
|
282
|
+
if (dismissPending) {
|
|
283
|
+
dismissPending = false;
|
|
284
|
+
dismissAfterDelay(1000);
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
{ signal: ac.signal },
|
|
288
|
+
);
|
|
269
289
|
|
|
270
290
|
dismissAfterDelay(delayMs);
|
|
271
291
|
}
|
|
272
292
|
|
|
273
293
|
private _dismissToast(toastRef: ComponentRef<SdToast>): void {
|
|
294
|
+
this._autoDismissCleanups.get(toastRef)?.();
|
|
274
295
|
toastRef.instance.open.set(false);
|
|
275
296
|
|
|
276
297
|
const el = toastRef.location.nativeElement as HTMLElement;
|
|
@@ -292,6 +313,8 @@ export class SdToastProvider {
|
|
|
292
313
|
}
|
|
293
314
|
|
|
294
315
|
private _destroyToast(toastRef: ComponentRef<SdToast>): void {
|
|
316
|
+
this._autoDismissCleanups.get(toastRef)?.();
|
|
317
|
+
|
|
295
318
|
const idx = this._toastRefs.indexOf(toastRef);
|
|
296
319
|
if (idx === -1) return; // 이미 파괴됨
|
|
297
320
|
|
|
@@ -133,8 +133,8 @@ export class SdKanbanLane<L, T> implements SdKanbanDropTarget<L, T> {
|
|
|
133
133
|
|
|
134
134
|
kanbanControls = contentChildren<SdKanban<L, T>>(SdKanban, { descendants: true });
|
|
135
135
|
|
|
136
|
-
toolTplRef = contentChild<
|
|
137
|
-
titleTplRef = contentChild<
|
|
136
|
+
toolTplRef = contentChild<unknown, TemplateRef<void>>("toolTpl", { read: TemplateRef });
|
|
137
|
+
titleTplRef = contentChild<unknown, TemplateRef<void>>("titleTpl", { read: TemplateRef });
|
|
138
138
|
|
|
139
139
|
isAllSelected = computed(() => {
|
|
140
140
|
const selectableControls = this.kanbanControls().filter((ctrl) => ctrl.selectable());
|
|
@@ -3,11 +3,10 @@ import {
|
|
|
3
3
|
booleanAttribute,
|
|
4
4
|
ChangeDetectionStrategy,
|
|
5
5
|
Component,
|
|
6
|
+
computed,
|
|
6
7
|
contentChild,
|
|
7
|
-
effect,
|
|
8
8
|
input,
|
|
9
9
|
model,
|
|
10
|
-
signal,
|
|
11
10
|
TemplateRef,
|
|
12
11
|
ViewEncapsulation,
|
|
13
12
|
} from "@angular/core";
|
|
@@ -70,27 +69,23 @@ export class SdSharedDataSelectButton<
|
|
|
70
69
|
inset = input(false, { transform: booleanAttribute });
|
|
71
70
|
size = input<"sm" | "lg">();
|
|
72
71
|
|
|
73
|
-
itemTplRef = contentChild.required<
|
|
72
|
+
itemTplRef = contentChild.required<SdItemOfTemplate<TItem>, TemplateRef<SdItemOfTemplateContext<TItem>>>(
|
|
74
73
|
SdItemOfTemplate,
|
|
75
74
|
{ read: TemplateRef },
|
|
76
75
|
);
|
|
77
76
|
|
|
78
|
-
|
|
77
|
+
_selectedItems = computed<TItem[]>(() => {
|
|
78
|
+
const v = this.value();
|
|
79
|
+
const items = this.items();
|
|
80
|
+
const mode = this.selectMode();
|
|
79
81
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
} else if (mode === "single" && !Array.isArray(v) && v != null) {
|
|
90
|
-
this._selectedItems.set(items.filter((it) => it.__valueKey === v));
|
|
91
|
-
} else {
|
|
92
|
-
this._selectedItems.set([]);
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
}
|
|
82
|
+
if (mode === "multi" && Array.isArray(v) && v.filterExists().length > 0) {
|
|
83
|
+
const keys = v.filterExists() as (string | number)[];
|
|
84
|
+
return items.filter((it) => keys.includes(it.__valueKey));
|
|
85
|
+
} else if (mode === "single" && !Array.isArray(v) && v != null) {
|
|
86
|
+
return items.filter((it) => it.__valueKey === v);
|
|
87
|
+
} else {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
});
|
|
96
91
|
}
|
|
@@ -146,17 +146,17 @@ export class SdSharedDataSelectList<
|
|
|
146
146
|
|
|
147
147
|
header = input<string>();
|
|
148
148
|
|
|
149
|
-
headerTplRef = contentChild<
|
|
149
|
+
headerTplRef = contentChild<unknown, TemplateRef<void>>("headerTpl", {
|
|
150
150
|
read: TemplateRef,
|
|
151
151
|
});
|
|
152
|
-
filterTplRef = contentChild<
|
|
152
|
+
filterTplRef = contentChild<unknown, TemplateRef<void>>("filterTpl", {
|
|
153
153
|
read: TemplateRef,
|
|
154
154
|
});
|
|
155
|
-
itemTplRef = contentChild<
|
|
155
|
+
itemTplRef = contentChild<SdItemOfTemplate<TItem>, TemplateRef<SdItemOfTemplateContext<TItem>>>(
|
|
156
156
|
SdItemOfTemplate,
|
|
157
157
|
{ read: TemplateRef },
|
|
158
158
|
);
|
|
159
|
-
undefinedTplRef = contentChild<
|
|
159
|
+
undefinedTplRef = contentChild<unknown, TemplateRef<void>>("undefinedTpl", {
|
|
160
160
|
read: TemplateRef,
|
|
161
161
|
});
|
|
162
162
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NgTemplateOutlet } from "@angular/common";
|
|
2
|
+
import type { DateOnly, DateTime, Time } from "@simplysm/core-common";
|
|
2
3
|
import {
|
|
3
4
|
booleanAttribute,
|
|
4
5
|
ChangeDetectionStrategy,
|
|
@@ -170,13 +171,13 @@ export class SdSharedDataSelect<
|
|
|
170
171
|
getSearchTextFn = input<(item: TItem, index: number) => string>(
|
|
171
172
|
(item) => item.__searchText,
|
|
172
173
|
);
|
|
173
|
-
|
|
174
|
+
displayOrderByFn = input<(item: TItem) => string | number | DateOnly | DateTime | Time | undefined>();
|
|
174
175
|
|
|
175
|
-
itemTplRef = contentChild<
|
|
176
|
+
itemTplRef = contentChild<SdItemOfTemplate<TItem>, TemplateRef<SdItemOfTemplateContext<TItem>>>(
|
|
176
177
|
SdItemOfTemplate,
|
|
177
178
|
{ read: TemplateRef },
|
|
178
179
|
);
|
|
179
|
-
undefinedTplRef = contentChild<
|
|
180
|
+
undefinedTplRef = contentChild<unknown, TemplateRef<void>>("undefinedTpl", {
|
|
180
181
|
read: TemplateRef,
|
|
181
182
|
});
|
|
182
183
|
|
|
@@ -219,9 +220,9 @@ export class SdSharedDataSelect<
|
|
|
219
220
|
return true;
|
|
220
221
|
});
|
|
221
222
|
|
|
222
|
-
const
|
|
223
|
-
if (
|
|
224
|
-
result = result.orderBy(
|
|
223
|
+
const orderBy = this.displayOrderByFn();
|
|
224
|
+
if (orderBy != null) {
|
|
225
|
+
result = result.orderBy(orderBy);
|
|
225
226
|
}
|
|
226
227
|
|
|
227
228
|
return result;
|
|
@@ -316,15 +317,12 @@ export class SdSharedDataSelect<
|
|
|
316
317
|
const parentMap = this.itemByParentKeyMap();
|
|
317
318
|
if (parentMap == null) return undefined;
|
|
318
319
|
|
|
319
|
-
const
|
|
320
|
-
if (
|
|
320
|
+
const orderBy = this.displayOrderByFn();
|
|
321
|
+
if (orderBy == null) return parentMap;
|
|
321
322
|
|
|
322
323
|
const sorted = new Map<TItem["__valueKey"] | undefined, TItem[]>();
|
|
323
324
|
for (const [key, children] of parentMap) {
|
|
324
|
-
sorted.set(
|
|
325
|
-
key,
|
|
326
|
-
children.orderBy((item) => (item as any)[orderProp]),
|
|
327
|
-
);
|
|
325
|
+
sorted.set(key, children.orderBy(orderBy));
|
|
328
326
|
}
|
|
329
327
|
return sorted;
|
|
330
328
|
});
|
|
@@ -32,7 +32,7 @@ export class SdSheetColumn<T = unknown> {
|
|
|
32
32
|
disableResizing = input(false, { transform: booleanAttribute });
|
|
33
33
|
ordering = input(0);
|
|
34
34
|
|
|
35
|
-
cellTplRef = contentChild.required<
|
|
35
|
+
cellTplRef = contentChild.required<SdSheetColumnCellTemplate<T>, TemplateRef<SdSheetCellContext<T>>>(
|
|
36
36
|
SdSheetColumnCellTemplate,
|
|
37
37
|
{ read: TemplateRef },
|
|
38
38
|
);
|