@opsnow-mcp/opsnow-mcp-common-ui-server 1.0.9 → 1.0.10
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.
|
@@ -1978,7 +1978,11 @@ export const DataGridExamples = [
|
|
|
1978
1978
|
usedSearch={true}
|
|
1979
1979
|
pagination={true}
|
|
1980
1980
|
paginationPageSize={5}
|
|
1981
|
-
pageSizeList={[
|
|
1981
|
+
pageSizeList={[
|
|
1982
|
+
{ label: '5', value: 5 },
|
|
1983
|
+
{ label: '10', value: 10 },
|
|
1984
|
+
{ label: '20', value: 20 },
|
|
1985
|
+
]}
|
|
1982
1986
|
langCd={i18n.getLocale()}
|
|
1983
1987
|
/>`
|
|
1984
1988
|
},
|
|
@@ -2083,4 +2087,130 @@ export const DataGridExamples = [
|
|
|
2083
2087
|
langCd={i18n.getLocale()}
|
|
2084
2088
|
/>`
|
|
2085
2089
|
},
|
|
2090
|
+
{
|
|
2091
|
+
title: 'Error Status',
|
|
2092
|
+
description: 'Error Status 예제입니다.',
|
|
2093
|
+
code_props_usage: `
|
|
2094
|
+
import { useCommonComponents, useGlobalContext } from '@opsnow-common/opsnow-finops-common-ui-loader'
|
|
2095
|
+
import i18n from '@opsnow-common/opsnow-finops-common-i18n'
|
|
2096
|
+
|
|
2097
|
+
// 컬럼 정의
|
|
2098
|
+
const columnDefs = [
|
|
2099
|
+
{ headerName: 'Make', field: 'make' },
|
|
2100
|
+
{ headerName: 'Model', field: 'model' },
|
|
2101
|
+
{ headerName: 'Price', field: 'price' },
|
|
2102
|
+
]
|
|
2103
|
+
|
|
2104
|
+
const gridOptions = {
|
|
2105
|
+
animateRows: true,
|
|
2106
|
+
rowSelection: { mode: 'singleRow', checkboxes: false, enableClickSelection: true },
|
|
2107
|
+
tooltipShowDelay: 100,
|
|
2108
|
+
domLayout: 'autoHeight',
|
|
2109
|
+
suppressCellFocus: true,
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
const { OpsnowCommonDataGrid } = useCommonComponents();
|
|
2113
|
+
const { CommonConst } = useGlobalContext()
|
|
2114
|
+
// errorStatus 상태를 관리
|
|
2115
|
+
const [errorStatus, setErrorStatus] = useState(CommonConst.GRID_STATUS.LOADING)
|
|
2116
|
+
|
|
2117
|
+
// 에러 상태 시뮬레이션을 위한 useEffect
|
|
2118
|
+
useEffect(() => {
|
|
2119
|
+
// 1초 후에 에러 상태로 설정
|
|
2120
|
+
const timeoutId = setTimeout(() => {
|
|
2121
|
+
setErrorStatus(CommonConst.GRID_STATUS.ERROR)
|
|
2122
|
+
}, 1000)
|
|
2123
|
+
|
|
2124
|
+
// 컴포넌트 언마운트 시 타임아웃 클리어
|
|
2125
|
+
return () => {
|
|
2126
|
+
clearTimeout(timeoutId)
|
|
2127
|
+
}
|
|
2128
|
+
}, [])
|
|
2129
|
+
`,
|
|
2130
|
+
code: `<OpsnowCommonDataGrid
|
|
2131
|
+
columnDefs={columnDefs}
|
|
2132
|
+
rowData={[]}
|
|
2133
|
+
status={errorStatus}
|
|
2134
|
+
gridOptions={gridOptions}
|
|
2135
|
+
textErrorTitle="데이터 로드 실패"
|
|
2136
|
+
textErrorDescription="데이터를 불러오는 중 오류가 발생했습니다."
|
|
2137
|
+
langCd={i18n.getLocale()}
|
|
2138
|
+
/>`
|
|
2139
|
+
},
|
|
2140
|
+
{
|
|
2141
|
+
title: 'Long Text with autoHeight',
|
|
2142
|
+
description: 'Long Text with autoHeight (긴 텍스트 자동 높이 조절) 예제입니다.',
|
|
2143
|
+
code_props_usage: `
|
|
2144
|
+
import { useCommonComponents, useGlobalContext } from '@opsnow-common/opsnow-finops-common-ui-loader'
|
|
2145
|
+
import i18n from '@opsnow-common/opsnow-finops-common-i18n'
|
|
2146
|
+
|
|
2147
|
+
// Long Text용 컬럼 정의
|
|
2148
|
+
const columnDefs = [
|
|
2149
|
+
{
|
|
2150
|
+
headerName: 'ID',
|
|
2151
|
+
field: 'id',
|
|
2152
|
+
flex: 1,
|
|
2153
|
+
minWidth: 80
|
|
2154
|
+
},
|
|
2155
|
+
{
|
|
2156
|
+
headerName: 'Description',
|
|
2157
|
+
field: 'description',
|
|
2158
|
+
flex: 3, // 가장 넓은 비율
|
|
2159
|
+
minWidth: 300,
|
|
2160
|
+
suppressSizeToFit: true, // 자동 리사이즈 방지
|
|
2161
|
+
cellStyle: {
|
|
2162
|
+
'align-items': 'center' // autoHeight 작동을 위해 필수
|
|
2163
|
+
}
|
|
2164
|
+
},
|
|
2165
|
+
{
|
|
2166
|
+
headerName: 'Status',
|
|
2167
|
+
field: 'status',
|
|
2168
|
+
flex: 1,
|
|
2169
|
+
minWidth: 120
|
|
2170
|
+
},
|
|
2171
|
+
]
|
|
2172
|
+
|
|
2173
|
+
// Long Text용 행 데이터
|
|
2174
|
+
const rowData = [
|
|
2175
|
+
{
|
|
2176
|
+
id: 1,
|
|
2177
|
+
description: 'This is a very long description text that will wrap to multiple lines when the column width is not sufficient to display all content in a single line. This demonstrates the autoHeight feature.',
|
|
2178
|
+
status: 'Active'
|
|
2179
|
+
},
|
|
2180
|
+
{
|
|
2181
|
+
id: 2,
|
|
2182
|
+
description: 'Short text',
|
|
2183
|
+
status: 'Inactive'
|
|
2184
|
+
},
|
|
2185
|
+
{
|
|
2186
|
+
id: 3,
|
|
2187
|
+
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.',
|
|
2188
|
+
status: 'Pending'
|
|
2189
|
+
},
|
|
2190
|
+
{
|
|
2191
|
+
id: 4,
|
|
2192
|
+
description: 'Another example of long text content that needs multiple lines',
|
|
2193
|
+
status: 'Active'
|
|
2194
|
+
},
|
|
2195
|
+
]
|
|
2196
|
+
|
|
2197
|
+
const gridOptions = {
|
|
2198
|
+
animateRows: true,
|
|
2199
|
+
rowSelection: { mode: 'singleRow', checkboxes: false, enableClickSelection: true },
|
|
2200
|
+
tooltipShowDelay: 100,
|
|
2201
|
+
domLayout: 'autoHeight',
|
|
2202
|
+
suppressCellFocus: true,
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
const { OpsnowCommonDataGrid } = useCommonComponents();
|
|
2206
|
+
const { CommonConst } = useGlobalContext()
|
|
2207
|
+
`,
|
|
2208
|
+
code: `<OpsnowCommonDataGrid
|
|
2209
|
+
columnDefs={columnDefs}
|
|
2210
|
+
rowData={rowData}
|
|
2211
|
+
status={CommonConst.GRID_STATUS.DATA_EXISTS}
|
|
2212
|
+
gridOptions={gridOptions}
|
|
2213
|
+
langCd={i18n.getLocale()}
|
|
2214
|
+
/>`
|
|
2215
|
+
},
|
|
2086
2216
|
];
|
|
@@ -37,7 +37,7 @@ const columnDefsSchema = z.lazy(() => z.object({
|
|
|
37
37
|
colId: z.string().optional().describe('컬럼 고유 ID'),
|
|
38
38
|
groupId: z.string().optional().describe('컬럼 그룹 고유 ID'),
|
|
39
39
|
width: z.number().optional().describe('컬럼 너비'),
|
|
40
|
-
minWidth: z.number().
|
|
40
|
+
minWidth: z.number().describe('컬럼 최소 너비'),
|
|
41
41
|
maxWidth: z.number().optional().describe('컬럼 최대 너비'),
|
|
42
42
|
pinned: z.enum(['left', 'right']).optional().describe('컬럼 고정 위치'),
|
|
43
43
|
type: z.array(z.enum([
|
|
@@ -126,7 +126,27 @@ export const DataGridSchema = z.object({
|
|
|
126
126
|
columnDefs: z.array(columnDefsSchema).describe("ag-grid 컬럼 정의").min(1),
|
|
127
127
|
defaultColDef: defaultColDefSchema.optional().describe('ag-grid 기본 컬럼 옵션'),
|
|
128
128
|
rowData: z.array(z.record(z.any())).optional().describe('그리드에 표시할 데이터'),
|
|
129
|
-
status: z.number().describe(
|
|
129
|
+
status: z.number().describe(`그리드 상태값: GRID.STATUS.LOADING(0), GRID.STATUS.DATA_EXISTS(1), GRID.STATUS.ERROR(9) 중 하나를 사용해야 합니다.
|
|
130
|
+
|
|
131
|
+
**필수 구현 패턴:**
|
|
132
|
+
반드시 useState로 상태를 관리하고, useEffect 등을 사용하여 데이터 로딩 로직을 구현해야 합니다.
|
|
133
|
+
사용자는 이 상태 제어 코드 안에 실제 비즈니스 로직(API 호출, 데이터 처리 등)을 추가할 수 있습니다.
|
|
134
|
+
|
|
135
|
+
예제:
|
|
136
|
+
const [status, setStatus] = useState(CommonConst.GRID_STATUS.LOADING)
|
|
137
|
+
const [rowData, setRowData] = useState([])
|
|
138
|
+
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
// 여기에 실제 비즈니스 로직을 추가합니다 (예: API 호출)
|
|
141
|
+
fetchData()
|
|
142
|
+
.then(data => {
|
|
143
|
+
setRowData(data)
|
|
144
|
+
setStatus(CommonConst.GRID_STATUS.DATA_EXISTS)
|
|
145
|
+
})
|
|
146
|
+
.catch(error => {
|
|
147
|
+
setStatus(CommonConst.GRID_STATUS.ERROR)
|
|
148
|
+
})
|
|
149
|
+
}, [])`),
|
|
130
150
|
gridHeight: z.number().optional().describe('그리드 높이'),
|
|
131
151
|
setClass: z.string().optional().describe('그리드 클래스명'),
|
|
132
152
|
localRowClass: z.string().optional().describe('로컬 행 클래스명'),
|
|
@@ -141,7 +161,10 @@ export const DataGridSchema = z.object({
|
|
|
141
161
|
searchTextNoData: z.string().optional().describe('검색 결과 없음 텍스트'),
|
|
142
162
|
searchTextNoDataDescription: z.string().optional().describe('검색 결과 없음 설명'),
|
|
143
163
|
pagingFromServer: z.boolean().optional().describe('서버 페이징 사용 여부'),
|
|
144
|
-
pageSizeList: z.array(z.
|
|
164
|
+
pageSizeList: z.array(z.object({
|
|
165
|
+
label: z.string().describe('페이지 사이즈 라벨'),
|
|
166
|
+
value: z.number().describe('페이지 사이즈 값')
|
|
167
|
+
})).optional().describe('페이지 사이즈 리스트 (예: [{ label: "5", value: 5 }, { label: "10", value: 10 }])'),
|
|
145
168
|
placeholderText: z.string().optional().describe('플레이스홀더 텍스트'),
|
|
146
169
|
usedRowClass: z.boolean().optional().describe('행 클래스 사용 여부 (셀 클릭 이벤트 사용 시 필수: true)'),
|
|
147
170
|
usedSearch: z.boolean().optional().describe('검색창 사용 여부'),
|
|
@@ -182,20 +205,112 @@ export function createDataGridComponent() {
|
|
|
182
205
|
**이 데이터 그리드 컴포넌트는 ag-grid를 기반으로 구현되었습니다.**
|
|
183
206
|
**데이터 그리드 데이터, 컬럼 정의 등의 데이터가 제공되지 않을 경우 목업 데이터를 사용하세요.**
|
|
184
207
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
208
|
+
**중요: 긴 텍스트 컬럼 처리 (필수)**
|
|
209
|
+
컬럼 값이 길어서 줄바꿈이 필요한 경우, 반드시 다음을 따라야 합니다:
|
|
210
|
+
1. 모든 컬럼에 flex 속성 필수 (비율로 너비 분배, 예: flex: 1, flex: 3)
|
|
211
|
+
2. 긴 텍스트 컬럼에 suppressSizeToFit: true 필수
|
|
212
|
+
3. 긴 텍스트 컬럼에 cellStyle: { 'align-items': 'center' } 필수
|
|
213
|
+
4. gridOptions에 domLayout: 'autoHeight' 설정
|
|
214
|
+
|
|
215
|
+
예시:
|
|
216
|
+
const columnDefs = [
|
|
217
|
+
{ headerName: 'ID', field: 'id', flex: 1, minWidth: 80 },
|
|
218
|
+
{
|
|
219
|
+
headerName: 'Description',
|
|
220
|
+
field: 'description',
|
|
221
|
+
flex: 3,
|
|
222
|
+
minWidth: 300,
|
|
223
|
+
suppressSizeToFit: true,
|
|
224
|
+
cellStyle: { 'align-items': 'center' }
|
|
225
|
+
},
|
|
226
|
+
{ headerName: 'Status', field: 'status', flex: 1, minWidth: 120 }
|
|
227
|
+
];
|
|
193
228
|
|
|
194
229
|
**import:**
|
|
195
230
|
\`\`\`javascript
|
|
196
231
|
import { useCommonComponents, useGlobalContext } from '@opsnow-common/opsnow-finops-common-ui-loader';
|
|
197
232
|
const { OpsnowCommonDataGrid } = useCommonComponents();
|
|
198
|
-
const { CommonConst } = useGlobalContext()
|
|
233
|
+
const { CommonConst } = useGlobalContext();
|
|
234
|
+
\`\`\`
|
|
235
|
+
|
|
236
|
+
**중요: 상태 관리 패턴 (필수 구현)**
|
|
237
|
+
데이터 그리드를 구현할 때는 반드시 아래의 세 가지 상태(LOADING, DATA_EXISTS, ERROR)를 모두 처리하는 코드를 포함해야 합니다.
|
|
238
|
+
|
|
239
|
+
**절대 하지 말아야 할 것:**
|
|
240
|
+
- useState에 직접 데이터를 넣지 마세요: const [rowData] = useState([{...}]) ❌
|
|
241
|
+
- status를 바로 DATA_EXISTS로 설정하지 마세요: const [status] = useState(CommonConst.GRID_STATUS.DATA_EXISTS) ❌
|
|
242
|
+
- useEffect 없이 구현하지 마세요 ❌
|
|
243
|
+
|
|
244
|
+
**반드시 따라야 할 구현 패턴:**
|
|
245
|
+
1. status는 반드시 CommonConst.GRID_STATUS.LOADING으로 시작
|
|
246
|
+
2. rowData는 반드시 빈 배열 []로 시작
|
|
247
|
+
3. useEffect를 반드시 사용하여 데이터 로딩 시뮬레이션
|
|
248
|
+
4. .then()으로 성공 처리 → CommonConst.GRID_STATUS.DATA_EXISTS로 변경
|
|
249
|
+
5. .catch()로 에러 처리 → CommonConst.GRID_STATUS.ERROR로 변경
|
|
250
|
+
6. OpsnowCommonDataGrid에 textErrorTitle, textErrorDescription 속성 반드시 포함
|
|
251
|
+
|
|
252
|
+
사용자는 생성된 코드의 useEffect 내부에 실제 비즈니스 로직(API 호출, 데이터 변환 등)을 추가할 수 있습니다.
|
|
253
|
+
|
|
254
|
+
**기본 패턴 예제 (정확히 이 구조를 따라야 함):**
|
|
255
|
+
\`\`\`javascript
|
|
256
|
+
// ✅ 올바른 방법: LOADING으로 시작, 빈 배열로 시작
|
|
257
|
+
const [status, setStatus] = useState(CommonConst.GRID_STATUS.LOADING);
|
|
258
|
+
const [rowData, setRowData] = useState([]);
|
|
259
|
+
|
|
260
|
+
// ✅ 올바른 방법: useEffect에서 데이터 로딩
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
// 사용자가 여기에 실제 API 호출 등 비즈니스 로직을 추가합니다
|
|
263
|
+
const fetchData = async () => {
|
|
264
|
+
try {
|
|
265
|
+
// 실제로는 여기에 API 호출을 넣습니다
|
|
266
|
+
// 예: const response = await fetch('/api/data');
|
|
267
|
+
// const data = await response.json();
|
|
268
|
+
|
|
269
|
+
// 데모를 위한 시뮬레이션
|
|
270
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
271
|
+
|
|
272
|
+
const mockData = [
|
|
273
|
+
{ make: 'Toyota', model: 'Celica', price: 35000 },
|
|
274
|
+
{ make: 'Ford', model: 'Mondeo', price: 32000 }
|
|
275
|
+
];
|
|
276
|
+
|
|
277
|
+
setRowData(mockData);
|
|
278
|
+
setStatus(CommonConst.GRID_STATUS.DATA_EXISTS);
|
|
279
|
+
} catch (error) {
|
|
280
|
+
console.error('Error loading data:', error);
|
|
281
|
+
setStatus(CommonConst.GRID_STATUS.ERROR);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
fetchData();
|
|
286
|
+
}, []);
|
|
287
|
+
|
|
288
|
+
// gridOptions 정의 (권장)
|
|
289
|
+
const gridOptions = {
|
|
290
|
+
animateRows: true,
|
|
291
|
+
rowSelection: { mode: 'singleRow', checkboxes: false, enableClickSelection: true },
|
|
292
|
+
tooltipShowDelay: 100,
|
|
293
|
+
domLayout: 'autoHeight',
|
|
294
|
+
suppressCellFocus: true,
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// ✅ 올바른 방법: 에러 관련 속성 포함
|
|
298
|
+
<OpsnowCommonDataGrid
|
|
299
|
+
columnDefs={columnDefs}
|
|
300
|
+
rowData={rowData}
|
|
301
|
+
status={status}
|
|
302
|
+
gridOptions={gridOptions}
|
|
303
|
+
pagination={true}
|
|
304
|
+
paginationPageSize={5}
|
|
305
|
+
pageSizeList={[
|
|
306
|
+
{ label: '5', value: 5 },
|
|
307
|
+
{ label: '10', value: 10 },
|
|
308
|
+
{ label: '20', value: 20 }
|
|
309
|
+
]}
|
|
310
|
+
textErrorTitle="데이터 로드 실패"
|
|
311
|
+
textErrorDescription="데이터를 불러오는 중 오류가 발생했습니다."
|
|
312
|
+
langCd={i18n.getLocale()}
|
|
313
|
+
/>
|
|
199
314
|
\`\`\``,
|
|
200
315
|
parameters: DataGridSchema,
|
|
201
316
|
handler: async (args) => {
|
|
@@ -51,13 +51,6 @@ export function createPaginationComponent() {
|
|
|
51
51
|
name: "createPagination",
|
|
52
52
|
description: `페이지네이션 컴포넌트 - 페이지 이동 및 크기 조절
|
|
53
53
|
|
|
54
|
-
**중요: OpsnowCommonDataGrid(데이터 그리드)와 함께 사용하는 경우**
|
|
55
|
-
- OpsnowCommonDataGrid는 내장 페이지네이션 기능을 제공합니다.
|
|
56
|
-
- 데이터 그리드에서 페이지네이션을 사용하려면 이 툴을 사용하지 말고 createDataGrid 툴을 사용하세요.
|
|
57
|
-
|
|
58
|
-
**이 툴을 사용해야 하는 경우**
|
|
59
|
-
- 데이터 그리드가 아닌 일반 목록이나 카드 뷰에서 페이지네이션이 필요한 경우에만 사용하세요.
|
|
60
|
-
|
|
61
54
|
**import**:
|
|
62
55
|
\`\`\`jsx
|
|
63
56
|
import { useCommonComponents } from '@opsnow-common/opsnow-finops-common-ui-loader'
|
package/build/index.js
CHANGED
|
@@ -26,6 +26,56 @@ import { createCommonExamplesComponent } from "./components/opsnow-common-exampl
|
|
|
26
26
|
const server = new McpServer({
|
|
27
27
|
name: "opsnow-mcp-common-ui-server",
|
|
28
28
|
version: "1.0.0"
|
|
29
|
+
}, {
|
|
30
|
+
instructions: `OpsNow UI Components Usage Guidelines:
|
|
31
|
+
|
|
32
|
+
IMPORTANT: All components require React 18.x or higher environment.
|
|
33
|
+
Always include required import statements and i18n configuration when needed.
|
|
34
|
+
|
|
35
|
+
**CRITICAL: OpsnowCommonDataGrid Usage Rules**
|
|
36
|
+
|
|
37
|
+
Before implementing OpsnowCommonDataGrid, you MUST use getUIExamples tool with 'DataGrid' parameter first!
|
|
38
|
+
|
|
39
|
+
1. Grid Top Search/Button Area - You MUST use children or searchAddon slots:
|
|
40
|
+
|
|
41
|
+
[CORRECT] Using children slot:
|
|
42
|
+
<OpsnowCommonDataGrid usedSearch={true}>
|
|
43
|
+
<OpsnowCommonButton label="Add" iconName="Plus" />
|
|
44
|
+
</OpsnowCommonDataGrid>
|
|
45
|
+
|
|
46
|
+
[CORRECT] Using searchAddon slot:
|
|
47
|
+
<OpsnowCommonDataGrid
|
|
48
|
+
usedSearch={true}
|
|
49
|
+
searchAddon={<OpsnowCommonDatePicker />}
|
|
50
|
+
/>
|
|
51
|
+
|
|
52
|
+
[FORBIDDEN] DO NOT create separate Stack/Box outside the grid:
|
|
53
|
+
<Stack direction="row"> <- DO NOT DO THIS!
|
|
54
|
+
<OpsnowCommonButton label="Add" />
|
|
55
|
+
</Stack>
|
|
56
|
+
<OpsnowCommonDataGrid usedSearch={true} />
|
|
57
|
+
|
|
58
|
+
Reference example: "Insert Button Inside the Common Grid Search Section"
|
|
59
|
+
|
|
60
|
+
2. Grid Bottom Pagination - You MUST use built-in pagination prop:
|
|
61
|
+
|
|
62
|
+
[CORRECT] Using built-in pagination:
|
|
63
|
+
<OpsnowCommonDataGrid
|
|
64
|
+
pagination={true}
|
|
65
|
+
paginationPageSize={10}
|
|
66
|
+
pageSizeList={[
|
|
67
|
+
{ label: '10', value: 10 },
|
|
68
|
+
{ label: '20', value: 20 }
|
|
69
|
+
]}
|
|
70
|
+
/>
|
|
71
|
+
|
|
72
|
+
[FORBIDDEN] DO NOT use OpsnowCommonPagination component separately:
|
|
73
|
+
<OpsnowCommonDataGrid />
|
|
74
|
+
<OpsnowCommonPagination /> <- DO NOT DO THIS!
|
|
75
|
+
|
|
76
|
+
Reference example: "Search + Pagination"
|
|
77
|
+
|
|
78
|
+
WARNING: Violating these rules will cause the UI to malfunction.`
|
|
29
79
|
});
|
|
30
80
|
// 컴포넌트들 등록
|
|
31
81
|
const componentFactories = [
|