@wipco/sdui 0.50.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DatePicker-BK_jsV7i.d.ts → DatePicker-C_TT_SOw.d.ts} +25 -0
- package/dist/{chunk-OEWTVLPZ.js → chunk-2HLXCKK7.js} +2 -2
- package/dist/{chunk-PWVHAUJC.js → chunk-2YP5N4VV.js} +6 -4
- package/dist/{chunk-QTHYLNJW.js → chunk-37SFFW37.js} +2 -13
- package/dist/{chunk-YQOMSWW5.js → chunk-5WW4MBRO.js} +1 -1
- package/dist/{chunk-LIOS3PF2.js → chunk-5XPZ3RSA.js} +3 -4
- package/dist/{chunk-ZD3VNVRF.js → chunk-7BGDSSAN.js} +5 -13
- package/dist/{chunk-Y2LIEWQX.js → chunk-7FZTUI6U.js} +6 -5
- package/dist/{chunk-HJ5VSXR4.js → chunk-7P7N43JV.js} +2 -2
- package/dist/{chunk-55VB6SDU.js → chunk-A5XQOEVH.js} +23 -20
- package/dist/{chunk-77LKJCMM.js → chunk-A6LAIGUS.js} +20 -108
- package/dist/{chunk-YXS5FXVS.js → chunk-AC5TI3BE.js} +7 -16
- package/dist/{chunk-AIOJYVJ2.js → chunk-AXGF4M57.js} +1 -1
- package/dist/{chunk-5MPK3NZK.js → chunk-B6WMC2AN.js} +3 -3
- package/dist/{chunk-C6ZV4VBN.js → chunk-C37U3GCC.js} +4 -4
- package/dist/{chunk-265YPW4W.js → chunk-DMLDKW7K.js} +1 -1
- package/dist/{chunk-FZ72IDVF.js → chunk-E6ISNQK3.js} +2 -2
- package/dist/{chunk-STX2AX6Y.js → chunk-FLAN7CV3.js} +7 -39
- package/dist/{chunk-IV5YTVHR.js → chunk-GE6BYNQJ.js} +18 -33
- package/dist/{chunk-ZX36LKJ7.js → chunk-GTGS2QRL.js} +7 -6
- package/dist/{chunk-NL3I6F2I.js → chunk-HBBTENSV.js} +3 -3
- package/dist/{chunk-RUU7WBGR.js → chunk-I45EUZC6.js} +19 -9
- package/dist/{chunk-JBKNIALK.js → chunk-I5SD7XRA.js} +9 -6
- package/dist/{chunk-X7GHY2RI.js → chunk-IFKYGJXP.js} +14 -42
- package/dist/{chunk-J3YMUR6R.js → chunk-IZ3JA2BR.js} +3 -3
- package/dist/{chunk-AUW5RP24.js → chunk-JIV4YHCZ.js} +7 -6
- package/dist/{chunk-M6KM3FJS.js → chunk-JYAR7D5U.js} +2 -2
- package/dist/{chunk-DKPNFEVU.js → chunk-KO6OEY3G.js} +19 -6
- package/dist/{chunk-JHHMFL5D.js → chunk-LFEJLOFN.js} +28 -16
- package/dist/chunk-MMIGOPO5.js +111 -0
- package/dist/{chunk-YSSNP6JM.js → chunk-N25YP7ID.js} +6 -21
- package/dist/{chunk-F2W2FWOT.js → chunk-N5RKM6IC.js} +7 -7
- package/dist/{chunk-C4HYWZML.js → chunk-O4IISNE7.js} +1 -1
- package/dist/chunk-Q4SWUINA.js +51 -0
- package/dist/{chunk-TUOG7EEI.js → chunk-RNOUXUBO.js} +418 -22
- package/dist/{chunk-4VZI3PLI.js → chunk-RYHEV5E3.js} +15 -39
- package/dist/{chunk-AVRNQOB5.js → chunk-SAPBDJJQ.js} +14 -13
- package/dist/{chunk-OPJ4DYNI.js → chunk-SJ3737RA.js} +156 -158
- package/dist/{chunk-UMANGYRD.js → chunk-TFGNKFL2.js} +1 -1
- package/dist/{chunk-KYGSMUUU.js → chunk-VY7MVBSE.js} +2 -2
- package/dist/{chunk-VDBUXYEB.js → chunk-WOYMEEPH.js} +8 -8
- package/dist/{chunk-I5Z2RVDF.js → chunk-WSLT32Q4.js} +278 -30
- package/dist/{chunk-NYUHSMUU.js → chunk-XGFDY43I.js} +2 -2
- package/dist/{chunk-ODKZF4IT.js → chunk-YOZN35HD.js} +17 -9
- package/dist/{chunk-EKP6Z3G6.js → chunk-YVTV4ZV6.js} +1 -1
- package/dist/{chunk-6BCWFPNJ.js → chunk-YWUDBLO6.js} +2 -2
- package/dist/chunk-Z4PYMTTS.js +41 -0
- package/dist/{chunk-WXIQLWIX.js → chunk-ZFNFPXPR.js} +2 -2
- package/dist/{chunk-7GOWOSX6.js → chunk-ZORQPB6B.js} +11 -5
- package/dist/index.d.ts +1118 -943
- package/dist/index.js +369 -610
- package/dist/meta.d.ts +13 -12
- package/dist/meta.js +84 -16
- package/dist/rich/ExcelUploadCard.d.ts +7 -0
- package/dist/rich/ExcelUploadCard.js +17 -13
- package/dist/rich.d.ts +1 -1
- package/dist/rich.js +25 -12
- package/dist/styles.css +10 -2
- package/dist/{types-D14eqLpf.d.ts → types-C7y9CpgF.d.ts} +7 -5
- package/dist/widgets/ApprovalLineEditor.js +7 -6
- package/dist/widgets/ApprovalOverridesEditor.d.ts +11 -1
- package/dist/widgets/ApprovalOverridesEditor.js +9 -8
- package/dist/widgets/ApprovalStepper.js +3 -3
- package/dist/widgets/AutocompleteField.d.ts +1 -0
- package/dist/widgets/AutocompleteField.js +7 -6
- package/dist/widgets/ColumnBoard.js +3 -3
- package/dist/widgets/DiffView.js +3 -3
- package/dist/widgets/Disclosure.js +3 -3
- package/dist/widgets/EventCalendar.d.ts +16 -1
- package/dist/widgets/EventCalendar.js +3 -3
- package/dist/widgets/FileDropzone.js +7 -6
- package/dist/widgets/ImageLightbox.js +5 -4
- package/dist/widgets/InlineEdit.js +3 -3
- package/dist/widgets/JsonField.js +3 -3
- package/dist/widgets/LiveClock.js +2 -2
- package/dist/widgets/OptionSchemaEditor.js +4 -4
- package/dist/widgets/OptionValueFields.d.ts +5 -0
- package/dist/widgets/OptionValueFields.js +4 -4
- package/dist/widgets/OrgChart.js +4 -3
- package/dist/widgets/OrgChartBoard.d.ts +93 -4
- package/dist/widgets/OrgChartBoard.js +9 -6
- package/dist/widgets/PeriodFilter.js +4 -4
- package/dist/widgets/PermissionMatrixEditor.d.ts +16 -0
- package/dist/widgets/PermissionMatrixEditor.js +4 -4
- package/dist/widgets/PositionSelect.d.ts +6 -1
- package/dist/widgets/PositionSelect.js +5 -4
- package/dist/widgets/PrintSheet.js +3 -3
- package/dist/widgets/ProfileEditDiffCard.d.ts +6 -0
- package/dist/widgets/ProfileEditDiffCard.js +4 -4
- package/dist/widgets/QRDisplay.js +4 -4
- package/dist/widgets/RankBoard.d.ts +6 -0
- package/dist/widgets/RankBoard.js +6 -5
- package/dist/widgets/RecordSheet.js +2 -2
- package/dist/widgets/RoleMemberPicker.d.ts +11 -5
- package/dist/widgets/RoleMemberPicker.js +5 -4
- package/dist/widgets/SearchableMultiPicker.js +5 -5
- package/dist/widgets/SecureScreen.d.ts +12 -0
- package/dist/widgets/SecureScreen.js +3 -3
- package/dist/widgets/StackedMeter.js +2 -2
- package/dist/widgets/StagePeriodEditor.js +4 -5
- package/dist/widgets/Strip.js +2 -2
- package/dist/widgets/SummaryTable.js +2 -2
- package/dist/widgets/TagMultiSelect.d.ts +0 -1
- package/dist/widgets/TagMultiSelect.js +4 -4
- package/dist/widgets/Timeline.js +4 -4
- package/dist/widgets/TreePicker.js +6 -6
- package/dist/widgets/TreeView.js +5 -5
- package/dist/widgets/index.d.ts +3 -3
- package/dist/widgets/index.js +81 -79
- package/package.json +1 -1
- package/dist/chunk-EKVA7T7P.js +0 -198
|
@@ -1,7 +1,405 @@
|
|
|
1
|
-
import { mergeStyle, containerLayoutStyle, joinClass } from './chunk-
|
|
1
|
+
import { mergeStyle, containerLayoutStyle, joinClass } from './chunk-5WW4MBRO.js';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
4
4
|
|
|
5
|
+
// src/warn-once.ts
|
|
6
|
+
var warned = /* @__PURE__ */ new Set();
|
|
7
|
+
function warnOnce(key, message, level = "error") {
|
|
8
|
+
if (warned.has(key)) return;
|
|
9
|
+
warned.add(key);
|
|
10
|
+
if (level === "warn") console.warn(message);
|
|
11
|
+
else console.error(message);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// src/plain-object.ts
|
|
15
|
+
function isPlainObject(v) {
|
|
16
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/calc.ts
|
|
20
|
+
function toNumber(v) {
|
|
21
|
+
if (typeof v === "number") return v;
|
|
22
|
+
if (typeof v === "string") {
|
|
23
|
+
const n = Number(v.replace(/,/g, ""));
|
|
24
|
+
return Number.isFinite(n) ? n : 0;
|
|
25
|
+
}
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
function isNumericText(v) {
|
|
29
|
+
if (typeof v === "number") return true;
|
|
30
|
+
if (typeof v !== "string" || v === "") return false;
|
|
31
|
+
return Number.isFinite(Number(v.replace(/,/g, "")));
|
|
32
|
+
}
|
|
33
|
+
function orderingCompare(a, b) {
|
|
34
|
+
if (typeof a === "string" && typeof b === "string" && !isNumericText(a) && !isNumericText(b)) {
|
|
35
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
36
|
+
}
|
|
37
|
+
const x = toNumber(a);
|
|
38
|
+
const y = toNumber(b);
|
|
39
|
+
return x < y ? -1 : x > y ? 1 : 0;
|
|
40
|
+
}
|
|
41
|
+
function looseEquals(a, b) {
|
|
42
|
+
return String(a ?? "") === String(b ?? "");
|
|
43
|
+
}
|
|
44
|
+
function isBlank(v) {
|
|
45
|
+
return v === null || v === void 0 || v === "";
|
|
46
|
+
}
|
|
47
|
+
function isEmptyValue(v) {
|
|
48
|
+
if (isBlank(v)) return true;
|
|
49
|
+
if (Array.isArray(v)) return v.length === 0;
|
|
50
|
+
if (isPlainObject(v)) return Object.keys(v).length === 0;
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
function fieldOf(row, field) {
|
|
54
|
+
if (!field) return row;
|
|
55
|
+
if (!isPlainObject(row)) return void 0;
|
|
56
|
+
if (Object.hasOwn(row, field)) return row[field];
|
|
57
|
+
if (!field.includes(".")) return void 0;
|
|
58
|
+
let current = row;
|
|
59
|
+
for (const segment of field.split(".")) {
|
|
60
|
+
if (!isPlainObject(current) || !Object.hasOwn(current, segment)) return void 0;
|
|
61
|
+
current = current[segment];
|
|
62
|
+
}
|
|
63
|
+
return current;
|
|
64
|
+
}
|
|
65
|
+
var FIELD_OPS = /* @__PURE__ */ new Set([
|
|
66
|
+
"eq",
|
|
67
|
+
"ne",
|
|
68
|
+
"gt",
|
|
69
|
+
"gte",
|
|
70
|
+
"lt",
|
|
71
|
+
"lte",
|
|
72
|
+
"in",
|
|
73
|
+
"notIn",
|
|
74
|
+
"contains",
|
|
75
|
+
"startsWith",
|
|
76
|
+
"endsWith",
|
|
77
|
+
"isNull",
|
|
78
|
+
"isEmpty",
|
|
79
|
+
"intersects"
|
|
80
|
+
]);
|
|
81
|
+
function isFieldOpObject(v) {
|
|
82
|
+
return isPlainObject(v) && Object.keys(v).some((key) => FIELD_OPS.has(key));
|
|
83
|
+
}
|
|
84
|
+
function matchesFieldOps(actual, ops, resolve) {
|
|
85
|
+
return Object.entries(ops).every(([op, rawExpected]) => {
|
|
86
|
+
if (!FIELD_OPS.has(op)) return true;
|
|
87
|
+
const expected = resolve(rawExpected);
|
|
88
|
+
switch (op) {
|
|
89
|
+
case "eq":
|
|
90
|
+
return looseEquals(actual, expected);
|
|
91
|
+
case "ne":
|
|
92
|
+
return !looseEquals(actual, expected);
|
|
93
|
+
case "gt":
|
|
94
|
+
return orderingCompare(actual, expected) > 0;
|
|
95
|
+
case "gte":
|
|
96
|
+
return orderingCompare(actual, expected) >= 0;
|
|
97
|
+
case "lt":
|
|
98
|
+
return orderingCompare(actual, expected) < 0;
|
|
99
|
+
case "lte":
|
|
100
|
+
return orderingCompare(actual, expected) <= 0;
|
|
101
|
+
case "in":
|
|
102
|
+
return Array.isArray(expected) && expected.some((item) => looseEquals(actual, item));
|
|
103
|
+
case "notIn":
|
|
104
|
+
return !Array.isArray(expected) || !expected.some((item) => looseEquals(actual, item));
|
|
105
|
+
case "contains":
|
|
106
|
+
return String(actual ?? "").includes(String(expected ?? ""));
|
|
107
|
+
case "startsWith":
|
|
108
|
+
return String(actual ?? "").startsWith(String(expected ?? ""));
|
|
109
|
+
case "endsWith":
|
|
110
|
+
return String(actual ?? "").endsWith(String(expected ?? ""));
|
|
111
|
+
case "isNull":
|
|
112
|
+
return isBlank(actual) === Boolean(expected);
|
|
113
|
+
case "intersects": {
|
|
114
|
+
const mine = Array.isArray(actual) ? actual : [];
|
|
115
|
+
const theirs = Array.isArray(expected) ? expected : [];
|
|
116
|
+
if (mine.length === 0 || theirs.length === 0) return false;
|
|
117
|
+
const set = new Set(theirs.map((item) => String(item ?? "")));
|
|
118
|
+
return mine.some((item) => set.has(String(item ?? "")));
|
|
119
|
+
}
|
|
120
|
+
case "isEmpty":
|
|
121
|
+
return isEmptyValue(actual) === Boolean(expected);
|
|
122
|
+
default:
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function matchesWhere(row, where, resolve) {
|
|
128
|
+
if (!isPlainObject(where)) return true;
|
|
129
|
+
return Object.entries(where).every(([key, condition]) => {
|
|
130
|
+
if (key === "AND") {
|
|
131
|
+
return Array.isArray(condition) && condition.every((sub) => matchesWhere(row, sub, resolve));
|
|
132
|
+
}
|
|
133
|
+
if (key === "OR") {
|
|
134
|
+
return Array.isArray(condition) && condition.some((sub) => matchesWhere(row, sub, resolve));
|
|
135
|
+
}
|
|
136
|
+
if (key === "NOT") {
|
|
137
|
+
return !matchesWhere(row, condition, resolve);
|
|
138
|
+
}
|
|
139
|
+
const actual = fieldOf(row, key);
|
|
140
|
+
if (isFieldOpObject(condition)) return matchesFieldOps(actual, condition, resolve);
|
|
141
|
+
return looseEquals(actual, resolve(condition));
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
var CALC_OPS = /* @__PURE__ */ new Set([
|
|
145
|
+
// 산술 (기존)
|
|
146
|
+
"sum",
|
|
147
|
+
"sub",
|
|
148
|
+
"mul",
|
|
149
|
+
"div",
|
|
150
|
+
"percent",
|
|
151
|
+
"count",
|
|
152
|
+
// 문자 (기존)
|
|
153
|
+
"concat",
|
|
154
|
+
"encode",
|
|
155
|
+
// 비교·논리 (기존)
|
|
156
|
+
"eq",
|
|
157
|
+
"gt",
|
|
158
|
+
"gte",
|
|
159
|
+
"lt",
|
|
160
|
+
"lte",
|
|
161
|
+
"not",
|
|
162
|
+
// 논리 (신규) — 예전에는 스펙이 eq(concat(eq(a),eq(b)),"falsefalse")로 우회했다
|
|
163
|
+
"and",
|
|
164
|
+
"or",
|
|
165
|
+
// 값 고르기 (신규)
|
|
166
|
+
"if",
|
|
167
|
+
"match",
|
|
168
|
+
"coalesce",
|
|
169
|
+
// 목록·객체 (신규)
|
|
170
|
+
"get",
|
|
171
|
+
"pluck",
|
|
172
|
+
"filter",
|
|
173
|
+
"some",
|
|
174
|
+
"every",
|
|
175
|
+
"includes",
|
|
176
|
+
"distinct",
|
|
177
|
+
"except",
|
|
178
|
+
"groupBy",
|
|
179
|
+
"groupCount",
|
|
180
|
+
"entries",
|
|
181
|
+
"fromEntries",
|
|
182
|
+
"length",
|
|
183
|
+
"join",
|
|
184
|
+
"split",
|
|
185
|
+
"sortBy",
|
|
186
|
+
"map"
|
|
187
|
+
]);
|
|
188
|
+
function isFieldRef(value) {
|
|
189
|
+
return isPlainObject(value) && "$field" in value;
|
|
190
|
+
}
|
|
191
|
+
function substituteFields(template, row) {
|
|
192
|
+
if (isFieldRef(template)) {
|
|
193
|
+
return template.$field === true ? row : fieldOf(row, String(template.$field)) ?? null;
|
|
194
|
+
}
|
|
195
|
+
if (Array.isArray(template)) return template.map((item) => substituteFields(item, row));
|
|
196
|
+
if (isPlainObject(template)) {
|
|
197
|
+
const nested = isPlainObject(template.$calc) ? template.$calc : null;
|
|
198
|
+
if (nested && nested.op === "map") {
|
|
199
|
+
const inner = {};
|
|
200
|
+
for (const [key, value] of Object.entries(nested)) {
|
|
201
|
+
inner[key] = key === "select" ? value : substituteFields(value, row);
|
|
202
|
+
}
|
|
203
|
+
return { ...template, $calc: inner };
|
|
204
|
+
}
|
|
205
|
+
const out = {};
|
|
206
|
+
for (const [key, value] of Object.entries(template)) out[key] = substituteFields(value, row);
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
return template;
|
|
210
|
+
}
|
|
211
|
+
function evaluateCalc(calc, resolved, resolve) {
|
|
212
|
+
const op = typeof calc.op === "string" ? calc.op : "";
|
|
213
|
+
const field = typeof calc.field === "string" ? calc.field : void 0;
|
|
214
|
+
const first = resolved[0];
|
|
215
|
+
const list = Array.isArray(first) ? first : void 0;
|
|
216
|
+
const listOrEmpty = () => {
|
|
217
|
+
if (list) return list;
|
|
218
|
+
if (first !== void 0 && first !== null) {
|
|
219
|
+
warnOnce(
|
|
220
|
+
`calc:${op}:list`,
|
|
221
|
+
`[sdui] $calc(${op}): \uCCAB \uC778\uC790\uAC00 \uBC30\uC5F4\uC774 \uC544\uB2D9\uB2C8\uB2E4 \u2014 \uBE48 \uBAA9\uB85D\uC73C\uB85C \uCC98\uB9AC\uD569\uB2C8\uB2E4`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
return [];
|
|
225
|
+
};
|
|
226
|
+
const nums = (v) => {
|
|
227
|
+
if (Array.isArray(v)) return v.map((row) => toNumber(fieldOf(row, field)));
|
|
228
|
+
return [toNumber(v)];
|
|
229
|
+
};
|
|
230
|
+
switch (op) {
|
|
231
|
+
/* ── 산술 ── */
|
|
232
|
+
case "sum":
|
|
233
|
+
return resolved.flatMap(nums).reduce((a, b) => a + b, 0);
|
|
234
|
+
case "count":
|
|
235
|
+
return resolved.reduce(
|
|
236
|
+
(a, v) => a + (Array.isArray(v) ? v.length : v == null ? 0 : 1),
|
|
237
|
+
0
|
|
238
|
+
);
|
|
239
|
+
case "sub": {
|
|
240
|
+
const items = resolved.map((v) => toNumber(v));
|
|
241
|
+
return items.length === 0 ? 0 : items.reduce((a, b) => a - b);
|
|
242
|
+
}
|
|
243
|
+
case "mul":
|
|
244
|
+
return resolved.map((v) => toNumber(v)).reduce((a, b) => a * b, 1);
|
|
245
|
+
case "div": {
|
|
246
|
+
const [a, b] = resolved.map((v) => toNumber(v));
|
|
247
|
+
return b === 0 ? 0 : a / b;
|
|
248
|
+
}
|
|
249
|
+
case "percent": {
|
|
250
|
+
const [a, b] = resolved.map((v) => toNumber(v));
|
|
251
|
+
return b === 0 ? 0 : Math.round(a / b * 1e3) / 10;
|
|
252
|
+
}
|
|
253
|
+
/* ── 문자 ── */
|
|
254
|
+
case "concat":
|
|
255
|
+
return resolved.map((v) => v == null ? "" : String(v)).join("");
|
|
256
|
+
case "encode":
|
|
257
|
+
return encodeURIComponent(resolved.map((v) => v == null ? "" : String(v)).join(""));
|
|
258
|
+
/* ── 비교·논리 ── */
|
|
259
|
+
case "eq":
|
|
260
|
+
return looseEquals(resolved[0], resolved[1]);
|
|
261
|
+
case "gt":
|
|
262
|
+
return orderingCompare(resolved[0], resolved[1]) > 0;
|
|
263
|
+
case "gte":
|
|
264
|
+
return orderingCompare(resolved[0], resolved[1]) >= 0;
|
|
265
|
+
case "lt":
|
|
266
|
+
return orderingCompare(resolved[0], resolved[1]) < 0;
|
|
267
|
+
case "lte":
|
|
268
|
+
return orderingCompare(resolved[0], resolved[1]) <= 0;
|
|
269
|
+
case "not":
|
|
270
|
+
return !resolved[0];
|
|
271
|
+
case "and":
|
|
272
|
+
return resolved.every(Boolean);
|
|
273
|
+
case "or":
|
|
274
|
+
return resolved.some(Boolean);
|
|
275
|
+
/* ── 값 고르기 ── */
|
|
276
|
+
case "if":
|
|
277
|
+
return resolved[0] ? resolve(calc.then ?? null) : resolve(calc.else ?? null);
|
|
278
|
+
case "match": {
|
|
279
|
+
const key = String(resolved[0] ?? "");
|
|
280
|
+
const cases = isPlainObject(calc.cases) ? calc.cases : {};
|
|
281
|
+
if (Object.hasOwn(cases, key)) return resolve(cases[key]);
|
|
282
|
+
return resolve(calc.default ?? null);
|
|
283
|
+
}
|
|
284
|
+
case "coalesce":
|
|
285
|
+
return resolved.find((v) => !isBlank(v)) ?? null;
|
|
286
|
+
/* ── 목록·객체 ── */
|
|
287
|
+
case "get": {
|
|
288
|
+
const [container, rawKey] = resolved;
|
|
289
|
+
if (Array.isArray(container)) {
|
|
290
|
+
if (typeof rawKey !== "number" && (typeof rawKey !== "string" || rawKey.trim() === "")) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
const index = Number(rawKey);
|
|
294
|
+
if (!Number.isInteger(index) || index < 0 || index >= container.length) return null;
|
|
295
|
+
return container[index] ?? null;
|
|
296
|
+
}
|
|
297
|
+
if (isPlainObject(container)) {
|
|
298
|
+
const key = rawKey == null ? "" : String(rawKey);
|
|
299
|
+
return Object.hasOwn(container, key) ? container[key] ?? null : null;
|
|
300
|
+
}
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
case "pluck":
|
|
304
|
+
return listOrEmpty().map((row) => fieldOf(row, field)).filter((v) => v !== null && v !== void 0);
|
|
305
|
+
case "filter":
|
|
306
|
+
return listOrEmpty().filter((row) => matchesWhere(row, calc.where, resolve));
|
|
307
|
+
case "some":
|
|
308
|
+
return listOrEmpty().some((row) => matchesWhere(row, calc.where, resolve));
|
|
309
|
+
case "every":
|
|
310
|
+
return listOrEmpty().every((row) => matchesWhere(row, calc.where, resolve));
|
|
311
|
+
case "includes": {
|
|
312
|
+
const needle = resolved[1];
|
|
313
|
+
if (typeof first === "string") return first.includes(String(needle ?? ""));
|
|
314
|
+
return listOrEmpty().some((row) => looseEquals(fieldOf(row, field), needle));
|
|
315
|
+
}
|
|
316
|
+
case "distinct": {
|
|
317
|
+
const seen = /* @__PURE__ */ new Set();
|
|
318
|
+
return listOrEmpty().filter((row) => {
|
|
319
|
+
const key = String(fieldOf(row, field) ?? "");
|
|
320
|
+
if (seen.has(key)) return false;
|
|
321
|
+
seen.add(key);
|
|
322
|
+
return true;
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
case "except": {
|
|
326
|
+
const remove = new Set(
|
|
327
|
+
(Array.isArray(resolved[1]) ? resolved[1] : []).map(
|
|
328
|
+
(row) => String(fieldOf(row, field) ?? "")
|
|
329
|
+
)
|
|
330
|
+
);
|
|
331
|
+
return listOrEmpty().filter(
|
|
332
|
+
(row) => !remove.has(String(fieldOf(row, field) ?? ""))
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
case "groupBy":
|
|
336
|
+
case "groupCount": {
|
|
337
|
+
const groups = /* @__PURE__ */ new Map();
|
|
338
|
+
for (const row of listOrEmpty()) {
|
|
339
|
+
const key = String(fieldOf(row, field) ?? "");
|
|
340
|
+
const bucket = groups.get(key);
|
|
341
|
+
if (bucket) bucket.push(row);
|
|
342
|
+
else groups.set(key, [row]);
|
|
343
|
+
}
|
|
344
|
+
const out = [...groups.entries()].map(
|
|
345
|
+
([key, items]) => op === "groupCount" ? { key, count: items.length } : { key, items }
|
|
346
|
+
);
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
349
|
+
case "entries": {
|
|
350
|
+
if (!isPlainObject(first)) return [];
|
|
351
|
+
return Object.entries(first).map(([key, value]) => ({
|
|
352
|
+
key,
|
|
353
|
+
value
|
|
354
|
+
}));
|
|
355
|
+
}
|
|
356
|
+
case "fromEntries": {
|
|
357
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
358
|
+
for (const row of listOrEmpty()) {
|
|
359
|
+
if (!isPlainObject(row)) continue;
|
|
360
|
+
const key = row.key;
|
|
361
|
+
if (key === null || key === void 0 || key === "") continue;
|
|
362
|
+
out[String(key)] = row.value ?? null;
|
|
363
|
+
}
|
|
364
|
+
return { ...out };
|
|
365
|
+
}
|
|
366
|
+
case "length":
|
|
367
|
+
if (Array.isArray(first)) return first.length;
|
|
368
|
+
if (typeof first === "string") return first.length;
|
|
369
|
+
return 0;
|
|
370
|
+
case "join": {
|
|
371
|
+
const sep = typeof calc.sep === "string" ? calc.sep : ",";
|
|
372
|
+
return listOrEmpty().map((row) => {
|
|
373
|
+
const value = fieldOf(row, field);
|
|
374
|
+
return value == null ? "" : String(value);
|
|
375
|
+
}).join(sep);
|
|
376
|
+
}
|
|
377
|
+
case "split": {
|
|
378
|
+
const sep = typeof calc.sep === "string" ? calc.sep : ",";
|
|
379
|
+
let parts = String(first ?? "").split(sep);
|
|
380
|
+
if (calc.trim === true) parts = parts.map((part) => part.trim());
|
|
381
|
+
if (calc.take === void 0) return parts;
|
|
382
|
+
const index = toNumber(calc.take);
|
|
383
|
+
const at = index < 0 ? parts.length + index : index;
|
|
384
|
+
return parts[at] ?? null;
|
|
385
|
+
}
|
|
386
|
+
case "map": {
|
|
387
|
+
const template = calc.select;
|
|
388
|
+
if (template === void 0) return listOrEmpty();
|
|
389
|
+
return listOrEmpty().map((row) => resolve(substituteFields(template, row)));
|
|
390
|
+
}
|
|
391
|
+
case "sortBy": {
|
|
392
|
+
const desc = Boolean(calc.desc);
|
|
393
|
+
return [...listOrEmpty()].sort((a, b) => {
|
|
394
|
+
const order = orderingCompare(fieldOf(a, field), fieldOf(b, field));
|
|
395
|
+
return desc ? -order : order;
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
default:
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
5
403
|
// src/icons.ts
|
|
6
404
|
var ICON_NODES = {
|
|
7
405
|
"activity": [
|
|
@@ -687,14 +1085,8 @@ var ICON_NODES = {
|
|
|
687
1085
|
{ t: "line", x1: "8", x2: "14", y1: "11", y2: "11" }
|
|
688
1086
|
]
|
|
689
1087
|
};
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
var warned = /* @__PURE__ */ new Set();
|
|
693
|
-
function warnOnce(key, message, level = "error") {
|
|
694
|
-
if (warned.has(key)) return;
|
|
695
|
-
warned.add(key);
|
|
696
|
-
if (level === "warn") console.warn(message);
|
|
697
|
-
else console.error(message);
|
|
1088
|
+
function asArray(value) {
|
|
1089
|
+
return Array.isArray(value) ? value : [];
|
|
698
1090
|
}
|
|
699
1091
|
function Icon({ name, size = 18, strokeWidth = 1.5, className = "", style, title, ...rest }) {
|
|
700
1092
|
const nodes = Object.hasOwn(ICON_NODES, name) ? ICON_NODES[name] : void 0;
|
|
@@ -1000,7 +1392,7 @@ function Tabs({ items = [], value, onChange, size = "md", onChrome = false, clas
|
|
|
1000
1392
|
className: joinClass("wp-tabs", size === "lg" && "wp-tabs--lg", onChrome && "wp-tabs--onchrome", className),
|
|
1001
1393
|
role: "tablist",
|
|
1002
1394
|
...rest,
|
|
1003
|
-
children: items.map((item) => /* @__PURE__ */ jsxs(
|
|
1395
|
+
children: asArray(items).map((item) => /* @__PURE__ */ jsxs(
|
|
1004
1396
|
"button",
|
|
1005
1397
|
{
|
|
1006
1398
|
type: "button",
|
|
@@ -1039,9 +1431,12 @@ function Table({
|
|
|
1039
1431
|
if (hover) classes.push("wp-table--hover");
|
|
1040
1432
|
if (sticky) classes.push("wp-table--sticky");
|
|
1041
1433
|
if (className) classes.push(className);
|
|
1042
|
-
|
|
1434
|
+
const columnList = asArray(columns);
|
|
1435
|
+
const rowList = asArray(rows);
|
|
1436
|
+
const selectedList = asArray(selectedIds);
|
|
1437
|
+
if (!rowList.length && empty) return /* @__PURE__ */ jsx(Fragment, { children: empty });
|
|
1043
1438
|
return /* @__PURE__ */ jsxs("table", { className: classes.join(" "), ...rest, children: [
|
|
1044
|
-
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children:
|
|
1439
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: columnList.map((column) => /* @__PURE__ */ jsx("th", { style: { width: column.width, textAlign: column.align === "right" ? "right" : void 0 }, children: column.sortable && onSort ? /* @__PURE__ */ jsxs("button", { type: "button", className: "wp-table__sort", onClick: () => onSort(column.key), children: [
|
|
1045
1440
|
column.header,
|
|
1046
1441
|
/* @__PURE__ */ jsx(
|
|
1047
1442
|
Icon,
|
|
@@ -1051,18 +1446,18 @@ function Table({
|
|
|
1051
1446
|
}
|
|
1052
1447
|
)
|
|
1053
1448
|
] }) : column.header }, column.key)) }) }),
|
|
1054
|
-
/* @__PURE__ */ jsx("tbody", { children:
|
|
1449
|
+
/* @__PURE__ */ jsx("tbody", { children: rowList.map((row, index) => /* @__PURE__ */ jsx(
|
|
1055
1450
|
"tr",
|
|
1056
1451
|
{
|
|
1057
|
-
"aria-selected":
|
|
1452
|
+
"aria-selected": selectedList.indexOf(row?.[rowKey]) > -1 || void 0,
|
|
1058
1453
|
onClick: onRowClick ? () => onRowClick(row) : void 0,
|
|
1059
1454
|
style: onRowClick ? { cursor: "pointer" } : void 0,
|
|
1060
|
-
children:
|
|
1455
|
+
children: columnList.map((column) => {
|
|
1061
1456
|
const cls = [];
|
|
1062
1457
|
if (column.align === "right") cls.push("wp-table__num");
|
|
1063
1458
|
if (column.mono) cls.push("wp-table__mono");
|
|
1064
1459
|
if (column.strong) cls.push("wp-table__strong");
|
|
1065
|
-
const raw = row
|
|
1460
|
+
const raw = column.key ? fieldOf(row, column.key) : void 0;
|
|
1066
1461
|
let content;
|
|
1067
1462
|
if (column.render) {
|
|
1068
1463
|
content = column.render(row, index);
|
|
@@ -1224,7 +1619,7 @@ function SegmentedControl({ items = [], value, onChange, size = "md", className
|
|
|
1224
1619
|
const classes = ["wp-seg"];
|
|
1225
1620
|
if (size === "lg") classes.push("wp-seg--lg");
|
|
1226
1621
|
if (className) classes.push(className);
|
|
1227
|
-
return /* @__PURE__ */ jsx("div", { className: classes.join(" "), role: "tablist", ...rest, children: items.map((it) => /* @__PURE__ */ jsxs(
|
|
1622
|
+
return /* @__PURE__ */ jsx("div", { className: classes.join(" "), role: "tablist", ...rest, children: asArray(items).map((it) => /* @__PURE__ */ jsxs(
|
|
1228
1623
|
"button",
|
|
1229
1624
|
{
|
|
1230
1625
|
type: "button",
|
|
@@ -1249,8 +1644,9 @@ function Avatar({ name = "", src, shape = "hex", size = "md", tone = "brand", cl
|
|
|
1249
1644
|
return /* @__PURE__ */ jsx("span", { className: classes.join(" "), title: name || void 0, ...rest, children: src ? /* @__PURE__ */ jsx("img", { src, alt: name }) : initials });
|
|
1250
1645
|
}
|
|
1251
1646
|
function Breadcrumb({ items = [], className = "", ...rest }) {
|
|
1252
|
-
|
|
1253
|
-
|
|
1647
|
+
const list = asArray(items);
|
|
1648
|
+
return /* @__PURE__ */ jsx("nav", { className: ("wp-crumbs " + className).trim(), "aria-label": "\uC704\uCE58", ...rest, children: list.map((it, i) => {
|
|
1649
|
+
const last = i === list.length - 1;
|
|
1254
1650
|
return /* @__PURE__ */ jsxs(React.Fragment, { children: [
|
|
1255
1651
|
last ? /* @__PURE__ */ jsx("span", { className: "wp-crumbs__current", "aria-current": "page", children: it.label }) : /* @__PURE__ */ jsxs("a", { className: "wp-crumbs__item", href: it.href || "#", onClick: (e) => {
|
|
1256
1652
|
if (!it.href) e.preventDefault();
|
|
@@ -1281,9 +1677,9 @@ function SidebarNav({ sections = [], value, onSelect, collapsed = false, light =
|
|
|
1281
1677
|
if (collapsed) classes.push("wp-nav--collapsed");
|
|
1282
1678
|
if (light) classes.push("wp-nav--light");
|
|
1283
1679
|
if (className) classes.push(className);
|
|
1284
|
-
return /* @__PURE__ */ jsx("nav", { className: classes.join(" "), ...rest, children: sections.map((s, si) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
|
|
1680
|
+
return /* @__PURE__ */ jsx("nav", { className: classes.join(" "), ...rest, children: asArray(sections).map((s, si) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
|
|
1285
1681
|
s.label ? /* @__PURE__ */ jsx("div", { className: "wp-nav__section", children: s.label }) : null,
|
|
1286
|
-
(s.items
|
|
1682
|
+
asArray(s.items).map((it) => /* @__PURE__ */ jsxs(
|
|
1287
1683
|
"a",
|
|
1288
1684
|
{
|
|
1289
1685
|
href: it.href || "#",
|
|
@@ -1334,4 +1730,4 @@ function Tooltip({ label, shortcut, placement = "top", wrap = false, children, c
|
|
|
1334
1730
|
);
|
|
1335
1731
|
}
|
|
1336
1732
|
|
|
1337
|
-
export { Alert, Avatar, Badge, Breadcrumb, Button, Card, Checkbox, Dialog, EmptyState, Field, Icon, IconButton, Input, Pagination, ProgressBar, Radio, SegmentedControl, Select, SidebarNav, Spinner, StatCard, Switch, Table, Tabs, Tag, Textarea, Toast, Tooltip, warnOnce };
|
|
1733
|
+
export { Alert, Avatar, Badge, Breadcrumb, Button, CALC_OPS, Card, Checkbox, Dialog, EmptyState, Field, ICON_NODES, Icon, IconButton, Input, Pagination, ProgressBar, Radio, SegmentedControl, Select, SidebarNav, Spinner, StatCard, Switch, Table, Tabs, Tag, Textarea, Toast, Tooltip, evaluateCalc, fieldOf, isPlainObject, toNumber, warnOnce };
|
|
@@ -1,41 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { fetchAllJobPositions } from './chunk-Z4PYMTTS.js';
|
|
2
|
+
import { useSduiDispatch, useSduiCan, SduiDialog } from './chunk-TFGNKFL2.js';
|
|
3
|
+
import { useSduiAdapter, hostFetchOf, defaultAdapter } from './chunk-KO6OEY3G.js';
|
|
4
|
+
import { Card, Input, Button, Icon, Badge, IconButton } from './chunk-RNOUXUBO.js';
|
|
5
|
+
import { boxed } from './chunk-5WW4MBRO.js';
|
|
5
6
|
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
|
6
7
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
7
8
|
|
|
8
|
-
var
|
|
9
|
-
var MAX_PAGES = 10;
|
|
10
|
-
function toJobPosition(row) {
|
|
11
|
-
if (typeof row !== "object" || row === null) return [];
|
|
12
|
-
const record = row;
|
|
13
|
-
const { id, name } = record;
|
|
14
|
-
if (typeof id !== "string" || typeof name !== "string") return [];
|
|
15
|
-
const num = (a, b) => typeof a === "number" ? a : typeof b === "number" ? b : 0;
|
|
16
|
-
const active = record.is_active ?? record.isActive;
|
|
17
|
-
return [
|
|
18
|
-
{
|
|
19
|
-
id,
|
|
20
|
-
name,
|
|
21
|
-
rankOrder: num(record.rank_order, record.rankOrder),
|
|
22
|
-
sortOrder: num(record.sort_order, record.sortOrder),
|
|
23
|
-
isActive: typeof active === "boolean" ? active : true
|
|
24
|
-
}
|
|
25
|
-
];
|
|
26
|
-
}
|
|
9
|
+
var DELETE_CONFIRM_DEFAULT = "'{name}' \uC9C1\uC704\uB97C \uC0AD\uC81C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?";
|
|
27
10
|
async function fetchAllPositions(api, endpoint) {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
const body = await api(`${endpoint}?limit=${PAGE_LIMIT}&offset=${page * PAGE_LIMIT}`, {
|
|
31
|
-
method: "GET"
|
|
32
|
-
});
|
|
33
|
-
const rows = Array.isArray(body?.data) ? body.data : [];
|
|
34
|
-
all.push(...rows.flatMap(toJobPosition));
|
|
35
|
-
const total = typeof body?.total === "number" ? body.total : null;
|
|
36
|
-
if (rows.length === 0 || total === null || all.length >= total) break;
|
|
37
|
-
}
|
|
38
|
-
return all;
|
|
11
|
+
const rows = await fetchAllJobPositions(api, endpoint);
|
|
12
|
+
return rows.flatMap((row) => row.id ? [{ ...row, id: row.id }] : []);
|
|
39
13
|
}
|
|
40
14
|
function groupByRank(positions) {
|
|
41
15
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -112,7 +86,8 @@ function RankBoardImpl({
|
|
|
112
86
|
entity = "job_position",
|
|
113
87
|
canCreate: canCreateProp,
|
|
114
88
|
canUpdate: canUpdateProp,
|
|
115
|
-
canDelete: canDeleteProp
|
|
89
|
+
canDelete: canDeleteProp,
|
|
90
|
+
deleteConfirmMessage
|
|
116
91
|
}) {
|
|
117
92
|
const dispatch = useSduiDispatch();
|
|
118
93
|
const can = useSduiCan();
|
|
@@ -377,10 +352,11 @@ function RankBoardImpl({
|
|
|
377
352
|
);
|
|
378
353
|
};
|
|
379
354
|
const handleDelete = (position) => {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
355
|
+
const message = (deleteConfirmMessage || DELETE_CONFIRM_DEFAULT).replaceAll(
|
|
356
|
+
"{name}",
|
|
357
|
+
position.name
|
|
358
|
+
);
|
|
359
|
+
if (!window.confirm(message)) return;
|
|
384
360
|
void runMutation(
|
|
385
361
|
() => api(`${endpoint}/${encodeURIComponent(position.id)}`, {
|
|
386
362
|
method: "DELETE"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { useOverlayLayer, OverlayContainerContext, getOverlayRoot } from './chunk-XQPTSEJC.js';
|
|
2
|
+
import { IconButton } from './chunk-RNOUXUBO.js';
|
|
3
|
+
import { useState, useRef, useCallback, useEffect, useContext } from 'react';
|
|
3
4
|
import { createPortal } from 'react-dom';
|
|
4
5
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
5
6
|
|
|
@@ -14,6 +15,7 @@ function clamp(value, min, max) {
|
|
|
14
15
|
function ImageViewer({ src, alt, open, onClose }) {
|
|
15
16
|
const [view, setView] = useState(INITIAL_VIEW);
|
|
16
17
|
const [dragging, setDragging] = useState(false);
|
|
18
|
+
const rootRef = useRef(null);
|
|
17
19
|
const stageRef = useRef(null);
|
|
18
20
|
const imgRef = useRef(null);
|
|
19
21
|
const viewRef = useRef(view);
|
|
@@ -69,22 +71,15 @@ function ImageViewer({ src, alt, open, onClose }) {
|
|
|
69
71
|
useEffect(() => {
|
|
70
72
|
if (open) setView(INITIAL_VIEW);
|
|
71
73
|
}, [open]);
|
|
74
|
+
useOverlayLayer(open, close, rootRef);
|
|
72
75
|
useEffect(() => {
|
|
73
76
|
if (!open) return;
|
|
74
|
-
const onKeyDown = (event) => {
|
|
75
|
-
if (event.key === "Escape") {
|
|
76
|
-
event.stopPropagation();
|
|
77
|
-
close();
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
document.addEventListener("keydown", onKeyDown);
|
|
81
77
|
const previousOverflow = document.body.style.overflow;
|
|
82
78
|
document.body.style.overflow = "hidden";
|
|
83
79
|
return () => {
|
|
84
|
-
document.removeEventListener("keydown", onKeyDown);
|
|
85
80
|
document.body.style.overflow = previousOverflow;
|
|
86
81
|
};
|
|
87
|
-
}, [open
|
|
82
|
+
}, [open]);
|
|
88
83
|
useEffect(() => {
|
|
89
84
|
if (!open) return;
|
|
90
85
|
const stage = stageRef.current;
|
|
@@ -140,13 +135,17 @@ function ImageViewer({ src, alt, open, onClose }) {
|
|
|
140
135
|
if (dragRef.current?.moved) return;
|
|
141
136
|
if (event.target === event.currentTarget) close();
|
|
142
137
|
};
|
|
138
|
+
const container = useContext(OverlayContainerContext);
|
|
143
139
|
const zoomPercent = Math.round(view.scale * 100);
|
|
144
140
|
const isInitial = view.scale === 1 && view.x === 0 && view.y === 0;
|
|
145
141
|
if (!open || typeof document === "undefined") return null;
|
|
142
|
+
const portalRoot = container ?? getOverlayRoot();
|
|
143
|
+
if (!portalRoot) return null;
|
|
146
144
|
return createPortal(
|
|
147
145
|
/* @__PURE__ */ jsxs(
|
|
148
146
|
"div",
|
|
149
147
|
{
|
|
148
|
+
ref: rootRef,
|
|
150
149
|
role: "dialog",
|
|
151
150
|
"aria-modal": "true",
|
|
152
151
|
"aria-label": alt ?? "\uC774\uBBF8\uC9C0 \uBCF4\uAE30",
|
|
@@ -154,7 +153,9 @@ function ImageViewer({ src, alt, open, onClose }) {
|
|
|
154
153
|
position: "fixed",
|
|
155
154
|
inset: 0,
|
|
156
155
|
zIndex: "var(--wp-z-dialog)",
|
|
157
|
-
background: "var(--wp-surface-scrim)"
|
|
156
|
+
background: "var(--wp-surface-scrim)",
|
|
157
|
+
// 오버레이 루트가 pointer-events:none이다(styles.css) — 겹이 스스로 살린다
|
|
158
|
+
pointerEvents: "auto"
|
|
158
159
|
},
|
|
159
160
|
onClick: (event) => event.stopPropagation(),
|
|
160
161
|
onPointerDown: (event) => event.stopPropagation(),
|
|
@@ -268,7 +269,7 @@ function ImageViewer({ src, alt, open, onClose }) {
|
|
|
268
269
|
]
|
|
269
270
|
}
|
|
270
271
|
),
|
|
271
|
-
|
|
272
|
+
portalRoot
|
|
272
273
|
);
|
|
273
274
|
}
|
|
274
275
|
|