@simplysm/angular 14.0.53 → 14.0.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/controls/button/sd-modal-select-button.d.ts +9 -10
  2. package/dist/controls/button/sd-modal-select-button.d.ts.map +1 -1
  3. package/dist/controls/button/sd-modal-select-button.js +4 -7
  4. package/dist/core/select-modal-output-result.d.ts +2 -3
  5. package/dist/core/select-modal-output-result.d.ts.map +1 -1
  6. package/dist/core/selection/useSelectionManager.d.ts +11 -11
  7. package/dist/core/selection/useSelectionManager.d.ts.map +1 -1
  8. package/dist/core/selection/useSelectionManager.js +29 -28
  9. package/dist/data/crud/sd-base-container.d.ts +21 -0
  10. package/dist/data/crud/sd-base-container.d.ts.map +1 -0
  11. package/dist/data/crud/sd-base-container.js +200 -0
  12. package/dist/data/crud/sd-crud-detail.d.ts +22 -0
  13. package/dist/data/crud/sd-crud-detail.d.ts.map +1 -0
  14. package/dist/data/crud/sd-crud-detail.js +267 -0
  15. package/dist/data/crud/sd-crud-list.d.ts +54 -0
  16. package/dist/data/crud/sd-crud-list.d.ts.map +1 -0
  17. package/dist/data/crud/sd-crud-list.js +592 -0
  18. package/dist/data/shared-data/sd-shared-data-select-button.d.ts.map +1 -1
  19. package/dist/data/shared-data/sd-shared-data-select-button.js +4 -5
  20. package/dist/data/shared-data/sd-shared-data-select-list.js +2 -2
  21. package/dist/data/shared-data/sd-shared-data-select.js +3 -3
  22. package/dist/data/sheet/injectSheetSelectRowIndicator.d.ts +4 -3
  23. package/dist/data/sheet/injectSheetSelectRowIndicator.d.ts.map +1 -1
  24. package/dist/data/sheet/injectSheetSelectRowIndicator.js +12 -4
  25. package/dist/data/sheet/sd-sheet.d.ts +39 -39
  26. package/dist/data/sheet/sd-sheet.d.ts.map +1 -1
  27. package/dist/data/sheet/sd-sheet.js +96 -74
  28. package/dist/data/sheet/types.d.ts +1 -0
  29. package/dist/data/sheet/types.d.ts.map +1 -1
  30. package/dist/data/sheet/useSheetLayoutEngine.d.ts.map +1 -1
  31. package/dist/data/sheet/useSheetLayoutEngine.js +3 -1
  32. package/dist/index.d.ts +3 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +4 -0
  35. package/dist/styles.css +2 -2
  36. package/package.json +13 -13
  37. package/scss/commons/_variables.scss +2 -2
  38. package/src/controls/button/sd-modal-select-button.ts +9 -13
  39. package/src/core/select-modal-output-result.ts +2 -3
  40. package/src/core/selection/useSelectionManager.ts +47 -49
  41. package/src/data/crud/sd-base-container.ts +115 -0
  42. package/src/data/crud/sd-crud-detail.ts +122 -0
  43. package/src/data/crud/sd-crud-list.ts +316 -0
  44. package/src/data/shared-data/sd-shared-data-select-button.ts +0 -1
  45. package/src/data/shared-data/sd-shared-data-select-list.ts +2 -2
  46. package/src/data/shared-data/sd-shared-data-select.ts +3 -3
  47. package/src/data/sheet/injectSheetSelectRowIndicator.ts +17 -7
  48. package/src/data/sheet/sd-sheet.ts +59 -42
  49. package/src/data/sheet/types.ts +1 -0
  50. package/src/data/sheet/useSheetLayoutEngine.ts +3 -1
  51. package/src/index.ts +5 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplysm/angular",
3
- "version": "14.0.53",
3
+ "version": "14.0.55",
4
4
  "description": "심플리즘 패키지 - Angular",
5
5
  "author": "심플리즘",
6
6
  "license": "Apache-2.0",
@@ -24,12 +24,12 @@
24
24
  "./dist/**/*.css"
25
25
  ],
26
26
  "dependencies": {
27
- "@angular/animations": "^21.2.9",
28
- "@angular/common": "^21.2.9",
29
- "@angular/core": "^21.2.9",
30
- "@angular/platform-browser": "^21.2.9",
31
- "@angular/router": "^21.2.9",
32
- "@angular/service-worker": "^21.2.9",
27
+ "@angular/animations": "^21.2.10",
28
+ "@angular/common": "^21.2.10",
29
+ "@angular/core": "^21.2.10",
30
+ "@angular/platform-browser": "^21.2.10",
31
+ "@angular/router": "^21.2.10",
32
+ "@angular/service-worker": "^21.2.10",
33
33
  "@ng-icons/core": "^33.2.2",
34
34
  "@ng-icons/tabler-icons": "^33.2.2",
35
35
  "@tiptap/core": "^3.22.4",
@@ -50,14 +50,14 @@
50
50
  "jspdf": "^4.2.1",
51
51
  "rxjs": "^7.8.2",
52
52
  "tabbable": "^6.4.0",
53
- "@simplysm/core-browser": "14.0.53",
54
- "@simplysm/service-client": "14.0.53",
55
- "@simplysm/core-common": "14.0.53",
56
- "@simplysm/service-common": "14.0.53"
53
+ "@simplysm/core-browser": "14.0.55",
54
+ "@simplysm/core-common": "14.0.55",
55
+ "@simplysm/service-client": "14.0.55",
56
+ "@simplysm/service-common": "14.0.55"
57
57
  },
58
58
  "devDependencies": {
59
- "@angular/compiler": "^21.2.9",
60
- "@angular/platform-browser-dynamic": "^21.2.9"
59
+ "@angular/compiler": "^21.2.10",
60
+ "@angular/platform-browser-dynamic": "^21.2.10"
61
61
  },
62
62
  "scripts": {}
63
63
  }
@@ -139,10 +139,10 @@ $vars: (
139
139
  animation-duration: 0.2s,
140
140
  elevation-size: 0.0833rem,
141
141
  sidebar: (
142
- width: 15em,
142
+ width: 15rem,
143
143
  ),
144
144
  topbar: (
145
- height: 3em,
145
+ height: 3rem,
146
146
  ),
147
147
  sheet: (
148
148
  pv: var(--gap-xs),
@@ -25,19 +25,19 @@ import { tablerSearch, tablerEraser } from "@ng-icons/tabler-icons";
25
25
 
26
26
  /**
27
27
  * 모달 선택 컴포넌트가 구현해야 하는 인터페이스
28
- * SdModalContentDef을 확장하여 selectMode와 selectedItemKeys를 추가한다.
28
+ * SdModalContentDef을 확장하여 selectMode와 selectedKeys를 추가한다.
29
29
  */
30
- export interface SdSelectModal<T> extends SdModalContentDef<SelectModalOutputResult<T>> {
30
+ export interface SdSelectModal<TKey> extends SdModalContentDef<SelectModalOutputResult<TKey>> {
31
31
  selectMode: InputSignal<"single" | "multi" | undefined>;
32
- selectedItemKeys: InputSignal<any[]>;
32
+ selectedKeys: InputSignal<TKey[]>;
33
33
  }
34
34
 
35
35
  /**
36
- * 모달 선택 정보 (selectMode/selectedItemKeys를 제외한 inputs)
36
+ * 모달 선택 정보 (selectMode/selectedKeys를 제외한 inputs)
37
37
  */
38
38
  export type SdSelectModalInfo<T extends SdSelectModal<any>> = SdModalInfo<
39
39
  T,
40
- "selectMode" | "selectedItemKeys"
40
+ "selectMode" | "selectedKeys"
41
41
  >;
42
42
 
43
43
  @Component({
@@ -146,16 +146,14 @@ export type SdSelectModalInfo<T extends SdSelectModal<any>> = SdModalInfo<
146
146
  },
147
147
  })
148
148
  export class SdModalSelectButton<
149
- T extends object,
150
149
  K,
151
150
  M extends keyof SelectModeValue<K> = keyof SelectModeValue<K>,
152
151
  > {
153
152
  private readonly _sdModal = inject(SdModalProvider);
154
153
 
155
- modal = input.required<SdSelectModalInfo<SdSelectModal<T>>>();
154
+ modal = input.required<SdSelectModalInfo<SdSelectModal<K>>>();
156
155
 
157
156
  value = model<SelectModeValue<K>[M]>();
158
- selectedItems = model<T[]>([]);
159
157
 
160
158
  disabled = input(false, { transform: booleanAttribute });
161
159
  required = input(false, { transform: booleanAttribute });
@@ -187,7 +185,6 @@ export class SdModalSelectButton<
187
185
 
188
186
  onEraseClick(): void {
189
187
  this.value.set((this.selectMode() === "multi" ? [] : undefined) as any);
190
- this.selectedItems.set([]);
191
188
  }
192
189
 
193
190
  async onSearchClick(event: MouseEvent): Promise<void> {
@@ -199,7 +196,7 @@ export class SdModalSelectButton<
199
196
  ...modal,
200
197
  inputs: {
201
198
  selectMode: this.selectMode(),
202
- selectedItemKeys: (this.selectMode() === "multi"
199
+ selectedKeys: (this.selectMode() === "multi"
203
200
  ? ((this.value() as any[] | undefined) ?? [])
204
201
  : [this.value()]
205
202
  ).filterExists(),
@@ -209,9 +206,8 @@ export class SdModalSelectButton<
209
206
 
210
207
  if (result) {
211
208
  const newValue =
212
- this.selectMode() === "multi" ? result.selectedItemKeys : result.selectedItemKeys[0];
213
- this.value.set(newValue);
214
- this.selectedItems.set(result.selectedItems);
209
+ this.selectMode() === "multi" ? result.selectedKeys : result.selectedKeys[0];
210
+ this.value.set(newValue as SelectModeValue<K>[M]);
215
211
  }
216
212
  }
217
213
  }
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * 모달 선택 결과
3
3
  */
4
- export interface SelectModalOutputResult<T> {
5
- selectedItemKeys: any[];
6
- selectedItems: T[];
4
+ export interface SelectModalOutputResult<TKey = any> {
5
+ selectedKeys: TKey[];
7
6
  }
@@ -1,27 +1,33 @@
1
1
  import { computed, type Signal, type WritableSignal } from "@angular/core";
2
2
  import { obj } from "@simplysm/core-common";
3
3
 
4
- export function useSelectionManager<T>(options: {
5
- displayItems: Signal<T[]>;
6
- selectedItems: WritableSignal<T[]>;
4
+ export function useSelectionManager<TItem, TKey>(options: {
5
+ displayItems: Signal<TItem[]>;
6
+ selectedKeys: WritableSignal<NonNullable<TKey>[]>;
7
7
  selectMode: Signal<"single" | "multi" | undefined>;
8
- getItemSelectableFn: Signal<((item: T) => boolean | string) | undefined>;
9
- trackByFn: Signal<(item: T, index: number) => unknown>;
8
+ getItemSelectableFn: Signal<((item: TItem) => boolean | string) | undefined>;
9
+ trackByFn: Signal<(item: TItem, index: number) => TKey>;
10
10
  }): {
11
11
  hasSelectable: Signal<boolean>;
12
12
  isAllSelected: Signal<boolean>;
13
- getSelectable(item: T): true | string | undefined;
14
- getCanChangeFn(item: T): () => boolean;
15
- select(item: T): void;
16
- deselect(item: T): void;
17
- toggle(item: T): void;
13
+ getSelectable(item: TItem): true | string | undefined;
14
+ getCanChangeFn(item: TItem): () => boolean;
15
+ select(item: TItem): void;
16
+ deselect(item: TItem): void;
17
+ toggle(item: TItem): void;
18
18
  toggleAll(): void;
19
- isSelected(item: T): boolean;
19
+ isSelected(item: TItem): boolean;
20
20
  } {
21
21
  const selectableItems = computed(() => {
22
+ const trackFn = options.trackByFn();
23
+ const idxMap = new Map<TItem, number>();
24
+ options.displayItems().forEach((it, i) => idxMap.set(it, i));
22
25
  const fn = options.getItemSelectableFn();
23
- if (fn == null) return options.displayItems();
24
- return options.displayItems().filter((item) => fn(item) === true);
26
+ return options.displayItems().filter((item) => {
27
+ if (trackFn(item, idxMap.get(item) ?? 0) == null) return false;
28
+ if (fn == null) return true;
29
+ return fn(item) === true;
30
+ });
25
31
  });
26
32
 
27
33
  const hasSelectable = computed(() => {
@@ -29,22 +35,17 @@ export function useSelectionManager<T>(options: {
29
35
  });
30
36
 
31
37
  const displayItemIndexMap = computed(() => {
32
- const map = new Map<T, number>();
38
+ const map = new Map<TItem, number>();
33
39
  options.displayItems().forEach((it, i) => map.set(it, i));
34
40
  return map;
35
41
  });
36
42
 
37
- const selectedKeys = computed(() => {
38
- const fn = options.trackByFn();
39
- return options.selectedItems().map((it, i) => fn(it, i));
40
- });
41
-
42
43
  function isKeyEqual(a: unknown, b: unknown): boolean {
43
44
  if (a === b) return true;
44
45
  return obj.equal(a, b);
45
46
  }
46
47
 
47
- function keyOf(item: T): unknown {
48
+ function keyOf(item: TItem): TKey {
48
49
  const idx = displayItemIndexMap().get(item);
49
50
  return options.trackByFn()(item, idx ?? 0);
50
51
  }
@@ -52,7 +53,7 @@ export function useSelectionManager<T>(options: {
52
53
  const isAllSelected = computed(() => {
53
54
  const items = selectableItems();
54
55
  if (items.length === 0) return false;
55
- const keys = selectedKeys();
56
+ const keys = options.selectedKeys();
56
57
  const fn = options.trackByFn();
57
58
  return items.every((it, i) => {
58
59
  const key = fn(it, i);
@@ -60,9 +61,10 @@ export function useSelectionManager<T>(options: {
60
61
  });
61
62
  });
62
63
 
63
- function getSelectable(item: T): true | string | undefined {
64
+ function getSelectable(item: TItem): true | string | undefined {
64
65
  const mode = options.selectMode();
65
66
  if (mode == null) return undefined;
67
+ if (keyOf(item) == null) return undefined;
66
68
  const fn = options.getItemSelectableFn();
67
69
  if (fn == null) return true;
68
70
  const result = fn(item);
@@ -71,40 +73,40 @@ export function useSelectionManager<T>(options: {
71
73
  return result; // string reason
72
74
  }
73
75
 
74
- function getCanChangeFn(item: T): () => boolean {
76
+ function getCanChangeFn(item: TItem): () => boolean {
75
77
  return () => {
76
78
  const selectable = getSelectable(item);
77
79
  return selectable === true;
78
80
  };
79
81
  }
80
82
 
81
- function select(item: T): void {
83
+ function select(item: TItem): void {
82
84
  const mode = options.selectMode();
83
85
  if (mode == null) return;
84
86
  if (getSelectable(item) !== true) return;
85
87
 
88
+ const key = keyOf(item);
89
+ if (key == null) return;
90
+
86
91
  if (mode === "single") {
87
- options.selectedItems.set([item]);
92
+ options.selectedKeys.set([key]);
88
93
  return;
89
94
  }
90
95
 
91
- const key = keyOf(item);
92
- const fn = options.trackByFn();
93
- options.selectedItems.update((arr) => {
94
- if (arr.some((it, i) => isKeyEqual(fn(it, i), key))) return arr;
95
- return [...arr, item];
96
+ options.selectedKeys.update((arr) => {
97
+ if (arr.some((k) => isKeyEqual(k, key))) return arr;
98
+ return [...arr, key];
96
99
  });
97
100
  }
98
101
 
99
- function deselect(item: T): void {
102
+ function deselect(item: TItem): void {
100
103
  const key = keyOf(item);
101
- const fn = options.trackByFn();
102
- options.selectedItems.update((arr) =>
103
- arr.filter((it, i) => !isKeyEqual(fn(it, i), key)),
104
+ options.selectedKeys.update((arr) =>
105
+ arr.filter((k) => !isKeyEqual(k, key)),
104
106
  );
105
107
  }
106
108
 
107
- function toggle(item: T): void {
109
+ function toggle(item: TItem): void {
108
110
  if (isSelected(item)) {
109
111
  deselect(item);
110
112
  } else {
@@ -119,27 +121,23 @@ export function useSelectionManager<T>(options: {
119
121
 
120
122
  if (isAllSelected()) {
121
123
  const keysToRemove = selectable.map((it) => fn(it, idxMap.get(it) ?? 0));
122
- options.selectedItems.update((arr) =>
123
- arr.filter((it, i) => {
124
- const k = fn(it, i);
125
- return !keysToRemove.some((rk) => isKeyEqual(rk, k));
126
- }),
124
+ options.selectedKeys.update((arr) =>
125
+ arr.filter((k) => !keysToRemove.some((rk) => isKeyEqual(rk, k))),
127
126
  );
128
127
  } else {
129
- options.selectedItems.update((arr) => {
130
- const existingKeys = arr.map((it, i) => fn(it, i));
131
- const toAdd = selectable.filter((it) => {
132
- const k = fn(it, idxMap.get(it) ?? 0);
133
- return !existingKeys.some((ek) => isKeyEqual(ek, k));
134
- });
135
- return [...arr, ...toAdd];
128
+ options.selectedKeys.update((arr) => {
129
+ const newKeys = selectable
130
+ .map((it) => fn(it, idxMap.get(it) ?? 0))
131
+ .filter((k): k is NonNullable<TKey> => k != null)
132
+ .filter((k) => !arr.some((ek) => isKeyEqual(ek, k)));
133
+ return [...arr, ...newKeys];
136
134
  });
137
135
  }
138
136
  }
139
137
 
140
- function isSelected(item: T): boolean {
138
+ function isSelected(item: TItem): boolean {
141
139
  const key = keyOf(item);
142
- return selectedKeys().some((k) => isKeyEqual(k, key));
140
+ return options.selectedKeys().some((k) => isKeyEqual(k, key));
143
141
  }
144
142
 
145
143
  return {
@@ -0,0 +1,115 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ contentChild,
5
+ effect,
6
+ inject,
7
+ input,
8
+ model,
9
+ TemplateRef,
10
+ untracked,
11
+ ViewEncapsulation,
12
+ } from "@angular/core";
13
+ import { injectViewTitleSignal } from "../../core/routing/injectViewTitleSignal";
14
+ import { SdBusyContainer } from "../../core/busy/sd-busy-container";
15
+ import { SdSharedDataProvider } from "../../core/shared-data/sd-shared-data.provider";
16
+ import { SdToastProvider } from "../../core/toast/sd-toast.provider";
17
+ import { SdTopbar } from "../../layout/topbar/sd-topbar";
18
+ import { SdTopbarContainer } from "../../layout/topbar/sd-topbar-container";
19
+ import type { SdViewType } from "../../core/routing/injectViewTypeSignal";
20
+ import { NgIcon } from "@ng-icons/core";
21
+ import { tablerAlertTriangle } from "@ng-icons/tabler-icons";
22
+ import { NgTemplateOutlet } from "@angular/common";
23
+
24
+ @Component({
25
+ selector: "sd-base-container",
26
+ changeDetection: ChangeDetectionStrategy.OnPush,
27
+ encapsulation: ViewEncapsulation.None,
28
+ standalone: true,
29
+ imports: [SdBusyContainer, NgIcon, SdTopbarContainer, NgTemplateOutlet, SdTopbar],
30
+ template: `
31
+ <sd-busy-container [busy]="initialized() && busyCount() > 0">
32
+ @if (initialized()) {
33
+ @if (restricted()) {
34
+ <div class="fill tx-theme-gray-light p-xxl tx-center">
35
+ <br />
36
+ <ng-icon [svg]="tablerAlertTriangle" [size]="'5rem'" />
37
+ <br />
38
+ <br />
39
+ '{{ viewTitle() }}'에 대한 사용권한이 없습니다. 시스템 관리자에게 문의하세요.
40
+ </div>
41
+ } @else if (viewType() === "page") {
42
+ <sd-topbar-container>
43
+ <sd-topbar>
44
+ <h4>{{ viewTitle() }}</h4>
45
+
46
+ <ng-template [ngTemplateOutlet]="topbarTplRef()" />
47
+ </sd-topbar>
48
+
49
+ <ng-template [ngTemplateOutlet]="content" />
50
+ </sd-topbar-container>
51
+ } @else {
52
+ <ng-template [ngTemplateOutlet]="content" />
53
+ }
54
+ }
55
+ </sd-busy-container>
56
+
57
+ <ng-template #content>
58
+ <div class="flex-column fill">
59
+ @if (commandTplRef()) {
60
+ <div class="p-default flex-row gap-default bdb bdb-theme-gray-lightest">
61
+ <ng-template [ngTemplateOutlet]="commandTplRef()" />
62
+ </div>
63
+ }
64
+
65
+ <div class="flex-fill">
66
+ <ng-content />
67
+ </div>
68
+
69
+ @if (bottomCommandTplRef()) {
70
+ <div class="p-sm-default flex-row main-align-end gap-sm bdt bdt-theme-gray-lightest">
71
+ <ng-template [ngTemplateOutlet]="bottomCommandTplRef()" />
72
+ </div>
73
+ }
74
+ </div>
75
+ </ng-template>
76
+ `,
77
+ })
78
+ export class SdBaseContainer {
79
+ private readonly _sdSharedData = inject(SdSharedDataProvider);
80
+ private readonly _sdToast = inject(SdToastProvider);
81
+
82
+ viewTitle = injectViewTitleSignal();
83
+
84
+ ready = model(false);
85
+ initialized = input(false);
86
+ busyCount = model(0);
87
+
88
+ restricted = input(false);
89
+
90
+ viewType = input.required<SdViewType>();
91
+
92
+ topbarTplRef = contentChild<TemplateRef<void>>("topbarTpl");
93
+ commandTplRef = contentChild<TemplateRef<void>>("commandTpl");
94
+ bottomCommandTplRef = contentChild<TemplateRef<void>>("bottomCommandTpl");
95
+
96
+ constructor() {
97
+ effect(() => {
98
+ if (this.restricted()) {
99
+ this.ready.set(true);
100
+ return;
101
+ }
102
+
103
+ void untracked(async () => {
104
+ this.busyCount.update((v) => v + 1);
105
+ await this._sdToast.try(async () => {
106
+ await this._sdSharedData.wait();
107
+ });
108
+ this.busyCount.update((v) => v - 1);
109
+ this.ready.set(true);
110
+ });
111
+ });
112
+ }
113
+
114
+ protected readonly tablerAlertTriangle = tablerAlertTriangle;
115
+ }
@@ -0,0 +1,122 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ contentChild,
5
+ input,
6
+ model,
7
+ output,
8
+ type TemplateRef,
9
+ viewChild,
10
+ ViewEncapsulation,
11
+ } from "@angular/core";
12
+ import { SdButton } from "../../controls/button/sd-button";
13
+ import { SdCommandDirective } from "../../core/commands/sd-command";
14
+ import { SdForm } from "../../controls/form/sd-form";
15
+ import type { SdViewType } from "../../core/routing/injectViewTypeSignal";
16
+ import { SdBaseContainer } from "./sd-base-container";
17
+ import { NgIcon } from "@ng-icons/core";
18
+ import { NgTemplateOutlet } from "@angular/common";
19
+ import { tablerDeviceFloppy } from "@ng-icons/tabler-icons";
20
+
21
+ @Component({
22
+ selector: "sd-crud-detail",
23
+ changeDetection: ChangeDetectionStrategy.OnPush,
24
+ encapsulation: ViewEncapsulation.None,
25
+ standalone: true,
26
+ imports: [SdBaseContainer, SdButton, NgIcon, NgTemplateOutlet, SdForm],
27
+ hostDirectives: [{ directive: SdCommandDirective, outputs: ["sdSaveCommand"] }],
28
+ host: {
29
+ "(sdSaveCommand)": "onSaveButtonClick()",
30
+ },
31
+ template: `
32
+ <sd-base-container
33
+ [(ready)]="ready"
34
+ [initialized]="initialized()"
35
+ [(busyCount)]="busyCount"
36
+ [restricted]="restricted()"
37
+ [viewType]="viewType()"
38
+ >
39
+ <!-- TOP -->
40
+ @if (viewType() === "page" && (!readonly() || commandTplRef())) {
41
+ <ng-template #topbarTpl>
42
+ @if (!readonly()) {
43
+ <sd-button [theme]="'link-primary'" (click)="formCtrl()?.requestSubmit()">
44
+ <ng-icon [svg]="tablerDeviceFloppy" />
45
+ 저장
46
+ <small>(CTRL+S)</small>
47
+ </sd-button>
48
+ }
49
+
50
+ <ng-template [ngTemplateOutlet]="commandTplRef()" />
51
+ </ng-template>
52
+ } @else if (viewType() === "control" && (!readonly() || commandTplRef())) {
53
+ <ng-template #commandTpl>
54
+ @if (!readonly()) {
55
+ <sd-button [theme]="'primary'" (click)="onSaveButtonClick()">
56
+ <ng-icon [svg]="tablerDeviceFloppy" />
57
+ 저장
58
+ <small>(CTRL+S)</small>
59
+ </sd-button>
60
+ }
61
+
62
+ <ng-template [ngTemplateOutlet]="commandTplRef()" />
63
+ </ng-template>
64
+ } @else if (commandTplRef()) {
65
+ <ng-template #commandTpl>
66
+ <ng-template [ngTemplateOutlet]="commandTplRef()" />
67
+ </ng-template>
68
+ }
69
+
70
+ <!-- BOTTOM -->
71
+ @if (viewType() === "modal" || bottomCommandTplRef()) {
72
+ <ng-template #bottomCommandTpl>
73
+ @if (bottomCommandTplRef()) {
74
+ <div class="flex-fill flex-row main-align-start gap-sm">
75
+ <ng-template [ngTemplateOutlet]="bottomCommandTplRef()" />
76
+ </div>
77
+ }
78
+
79
+ <sd-button [size]="'sm'" [theme]="'primary'" (click)="onSaveButtonClick()">
80
+ 확인
81
+ </sd-button>
82
+ </ng-template>
83
+ }
84
+
85
+ <!-- CONTENT -->
86
+ <ng-template #contentTpl>
87
+ <ng-content />
88
+ </ng-template>
89
+
90
+ @if (readonly()) {
91
+ <div class="fill">
92
+ <ng-template [ngTemplateOutlet]="contentTpl" />
93
+ </div>
94
+ } @else {
95
+ <sd-form #formCtrl (formSubmit)="submit.emit()" class="block fill">
96
+ <ng-template [ngTemplateOutlet]="contentTpl" />
97
+ </sd-form>
98
+ }
99
+ </sd-base-container>
100
+ `,
101
+ })
102
+ export class SdCrudDetail {
103
+ ready = model(false);
104
+ initialized = input(false);
105
+ busyCount = model(0);
106
+ restricted = input(false);
107
+ readonly = input(false);
108
+ viewType = input.required<SdViewType>();
109
+
110
+ formCtrl = viewChild<SdForm>("formCtrl");
111
+
112
+ submit = output();
113
+
114
+ commandTplRef = contentChild<TemplateRef<void>>("commandTpl");
115
+ bottomCommandTplRef = contentChild<TemplateRef<void>>("bottomCommandTpl");
116
+
117
+ onSaveButtonClick() {
118
+ this.formCtrl()?.requestSubmit();
119
+ }
120
+
121
+ protected readonly tablerDeviceFloppy = tablerDeviceFloppy;
122
+ }