@things-factory/meta-ui 5.0.0-zeta.1

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 (45) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/LICENSE.md +21 -0
  3. package/client/actions/main.js +1 -0
  4. package/client/bootstrap.js +8 -0
  5. package/client/index.js +23 -0
  6. package/client/mixin/handler-basic-button-mixin.js +202 -0
  7. package/client/mixin/handler-common-button-mixin.js +90 -0
  8. package/client/mixin/handler-custom-button-mixin.js +127 -0
  9. package/client/mixin/handler-graphql-mixin.js +297 -0
  10. package/client/mixin/handler-grist-button-mixin.js +119 -0
  11. package/client/mixin/meta-set-mixin.js +69 -0
  12. package/client/mixin/meta-util-mixin.js +577 -0
  13. package/client/mixin/render-basic-form-mixin.js +264 -0
  14. package/client/mixin/render-basic-grist-mixin.js +85 -0
  15. package/client/mixin/render-button-mixin.js +163 -0
  16. package/client/mixin/render-grist-mixin.js +478 -0
  17. package/client/mixin/render-search-mixin.js +37 -0
  18. package/client/pages/basic-form-element.js +9 -0
  19. package/client/pages/basic-grist-element.js +11 -0
  20. package/client/pages/basic-grist-page.js +12 -0
  21. package/client/pages/main.js +27 -0
  22. package/client/reducers/main.js +17 -0
  23. package/client/route.js +7 -0
  24. package/dist-server/controllers/index.js +1 -0
  25. package/dist-server/controllers/index.js.map +1 -0
  26. package/dist-server/index.js +20 -0
  27. package/dist-server/index.js.map +1 -0
  28. package/dist-server/middlewares/index.js +8 -0
  29. package/dist-server/middlewares/index.js.map +1 -0
  30. package/dist-server/migrations/index.js +12 -0
  31. package/dist-server/migrations/index.js.map +1 -0
  32. package/dist-server/routes.js +25 -0
  33. package/dist-server/routes.js.map +1 -0
  34. package/package.json +30 -0
  35. package/server/controllers/index.ts +0 -0
  36. package/server/index.ts +4 -0
  37. package/server/middlewares/index.ts +3 -0
  38. package/server/migrations/index.ts +9 -0
  39. package/server/routes.ts +28 -0
  40. package/things-factory.config.js +13 -0
  41. package/translations/en.json +1 -0
  42. package/translations/ko.json +1 -0
  43. package/translations/ms.json +1 -0
  44. package/translations/zh.json +1 -0
  45. package/tsconfig.json +9 -0
@@ -0,0 +1,577 @@
1
+ import { i18next } from '@operato/i18n'
2
+ import { navigate, CustomAlert } from '@things-factory/shell'
3
+ import { getCodeByName } from '@things-factory/code-base'
4
+ import { openPopup } from '@things-factory/layout-base'
5
+ import isEqual from 'lodash-es/isEqual'
6
+
7
+
8
+ /**
9
+ * 유틸리티
10
+ ******************************************************
11
+ * @param {Object} baseElement
12
+ * @returns
13
+ */
14
+ export const MetaUtilMixin = (baseElement) => class extends baseElement {
15
+
16
+ static get properties() {
17
+ return {
18
+ /* 메뉴 메타 정보 */
19
+ menuInfo: Object, // 메뉴 기본 정보
20
+ gridConfig: Object, // 그리드 구성 정보
21
+ gridColumnConfig: Array, // 그리드 컬럼 정보
22
+ formColumnConfig: Array, // form 컬럼 정보
23
+ buttonConfig: Array, // 버튼 구성 정보
24
+ searchConfig: Array, // 서치폼 구성 정보
25
+ gqlInfo: String, // GraphQL 정보
26
+ gqlFetchField: String, // GraphQL 조회 필드
27
+ searchFormElement: String, // 화면에서 사용 되는 서치폼 (grist, filter, not)
28
+
29
+ /* 화면 구성 */
30
+ gristConfigSet: Object, // 메타정보에서 변환된 그리드 구성 정보
31
+ formConfigSet: Array, // 메타정보에서 변환된 폼 구성 정보
32
+ use_button_export: Boolean, // 내보내기 버튼 사용 여부
33
+ use_button_import: Boolean, // 가져오기 버튼 사용 여부
34
+ use_button_add: Boolean, // 추가 버튼 사용 여부
35
+ grid_mobile_mode: String, // 모바일 기기에서 그리드 뷰
36
+ grid_desk_mode: String, // 데스크탑에서 그리드 뷰
37
+ grid_view_options: Array, // 그리드 뷰 옵션
38
+ grid_mode: String, // 현재 그리드 뷰 모드
39
+ infinity_page: true, // 페이지 사용시 false
40
+
41
+ /* 속성 ? */
42
+ is_detail: { // 페이지내 컴포넌트 여부
43
+ type: Boolean,
44
+ converter(value) {
45
+ let retVal = Boolean(value);
46
+ return retVal;
47
+ }
48
+ },
49
+ is_popup: { // 팝업 여부
50
+ type: Boolean,
51
+ converter(value) {
52
+ let retVal = Boolean(value);
53
+ return retVal;
54
+ }
55
+ },
56
+
57
+ parent_id: String, // 상세 폼뷰 상위 데이터 ID
58
+
59
+ }
60
+ }
61
+
62
+ /**
63
+ * 페이지내 컴포넌트 이거나 팝업으로 오픈된 경우
64
+ */
65
+ get isElement() {
66
+ return (this.isDetail === true || this.isPopup === true);
67
+ }
68
+
69
+ /**
70
+ * isDetail 여부 리턴 (페이지내 포함된 하나의 컴포넌트)
71
+ */
72
+ get isDetail() {
73
+ return this.isEmpty(this.is_detail) ? false : this.is_detail;
74
+ }
75
+
76
+ /**
77
+ * Popup 오픈 여부 리턴
78
+ */
79
+ get isPopup() {
80
+ return this.isEmpty(this.is_popup) ? false : this.is_popup;
81
+ }
82
+
83
+ /**
84
+ * 페이지 여부 리턴
85
+ */
86
+ get isPage() {
87
+ return (this.isDetail === false && this.isPopup === false);
88
+ }
89
+
90
+
91
+ /**
92
+ * Document 내의 attribute 변화를 감지 및 로깅 (개발용)
93
+ **************************************
94
+ * @param {String} name
95
+ * @param {*} oldVal
96
+ * @param {*} newVal
97
+ */
98
+ attributeChangedCallback(name, oldVal, newVal) {
99
+ console.log(this.tagName, 'attribute change: ', name, oldVal, newVal);
100
+ super.attributeChangedCallback(name, oldVal, newVal);
101
+
102
+ if (name === 'context-path' && this.active === true && this.isNotEmpty(this.contextPath)) {
103
+ // 페이지 링크 URL 을 서치폼에 셋팅한다.
104
+ const urlParams = new URLSearchParams(window.location.search);
105
+
106
+ if (this.isNotEmpty(urlParams)) {
107
+ urlParams.forEach((value, key) => {
108
+ this.setSearchFormEditorValue(key, value);
109
+ })
110
+ this.grist.fetch();
111
+ }
112
+ }
113
+ }
114
+
115
+
116
+ /**
117
+ * 서치폼 ( grist or filter or undefined) 을 찾아 에디터에 값을 셋팅
118
+ *****************************************************
119
+ * @param {String} name
120
+ * @param {Object} value
121
+ */
122
+ setSearchFormEditorValue(name, value) {
123
+ let filterForm = this.filterForm;
124
+
125
+ if (this.isNotEmpty(filterForm)) {
126
+ let filterEditor = filterForm.renderRoot?.querySelector(`[name='${name}']`);
127
+
128
+ if (this.isNotEmpty(filterEditor)) {
129
+ filterEditor.value = value;
130
+ }
131
+ }
132
+ }
133
+
134
+ /**
135
+ * 메시지 코드 변환
136
+ * @param {String} code
137
+ * @returns
138
+ */
139
+ convertMsgCode(code) {
140
+ return i18next.t(code);
141
+ }
142
+
143
+
144
+ /**
145
+ * logic 에 포함된 element 정보를 이용해 팝업을 연다
146
+ *************************************************
147
+ * @description
148
+ * @param {String} title 팝업 이름
149
+ * @param {Object} logic { "module": '', "location": '', "tagname": '', size : '', popup_field: '', parent_field: ''}
150
+ * @param {Object} paramData
151
+ */
152
+ async openDynamicPopup(title, logic, paramData) {
153
+
154
+ // import
155
+ await this.dynamicImport(logic.module, logic.location);
156
+
157
+ let parentValue = undefined;
158
+
159
+ if (this.isNotEmpty(logic.parent_field)) {
160
+ parentValue = paramData[logic.parent_field];
161
+ }
162
+
163
+ // element 생성
164
+ let htmlText = `<${logic.tagname} route_name='${logic.menu}' parent_id='${parentValue}' is_popup=true></${logic.tagname}>`;
165
+ let htmlElemnts = this.htmlToElement(htmlText);
166
+
167
+ // 파라미터 설정
168
+ if (this.isNotEmpty(logic.popup_field)) {
169
+ htmlElemnts[logic.popup_field] = paramData;
170
+ }
171
+
172
+ // 팝업 오픈
173
+ let popup = openPopup(htmlElemnts, {
174
+ backdrop: true,
175
+ size: logic.size,
176
+ title: title
177
+ })
178
+ popup.onclosed = async (e) => {
179
+ // 팝업이 닫히면 조회
180
+ await this.fetch();
181
+ }
182
+ }
183
+
184
+
185
+
186
+
187
+ /**
188
+ * js 다이다믹 임포트
189
+ * module 이 미리 지정 되어 있어야 한다.
190
+ **************************************
191
+ * @param {String} module
192
+ * @param {String} url
193
+ */
194
+ async dynamicImport(module, url) {
195
+ // 페이지 import 요청 이벤트 전파
196
+ document.dispatchEvent(new CustomEvent('dynamic-page-import-request', {
197
+ detail: {
198
+ module: module,
199
+ url: url
200
+ }
201
+ }))
202
+ }
203
+
204
+ /**
205
+ * 페이지를 이동 한다 .
206
+ **************************************
207
+ * @param {String} url
208
+ * @param {Object} params undefined or Object
209
+ */
210
+ pageNavite(url, params) {
211
+ if (params) {
212
+ let paramStr = '';
213
+
214
+ Object.keys(params).forEach(x => {
215
+ paramStr += `${x}=${params[x]}&`
216
+ })
217
+
218
+ navigate(`${url}?${paramStr}`)
219
+ } else {
220
+ navigate(url);
221
+ }
222
+ }
223
+
224
+ /**
225
+ * 공통 코드를 조회해 서치폼, 그리드에서 표현 가능한 데이터로 변환 한다.
226
+ ************************************************************
227
+ * @param {String} name
228
+ * @returns
229
+ */
230
+ async getCommonCodesByName(name) {
231
+ let codes = await getCodeByName(name);
232
+ let options = [
233
+ { value: '', display: '' }
234
+ ]
235
+
236
+ codes.forEach(x => {
237
+ options.push({
238
+ value: x.name,
239
+ display: x.description
240
+ })
241
+ })
242
+
243
+ return options;
244
+ }
245
+
246
+
247
+ /**
248
+ * 시나리오를 호출해 코드 정보를 가져온다.
249
+ ****************************************
250
+ * @description 시나리오에서는 name, description 필드를 리턴해 줘야 한다.
251
+ * @param {String} name
252
+ * @param {Array} args
253
+ */
254
+ async getCodeByScenario(name, args) {
255
+ let codes = await this.gqlCallScenario(name, args);
256
+ let options = [
257
+ { value: '', display: '' }
258
+ ]
259
+
260
+ codes.forEach(x => {
261
+ options.push({
262
+ value: x.name,
263
+ display: x.description
264
+ })
265
+ })
266
+
267
+ return options;
268
+ }
269
+
270
+ /**
271
+ * dataObject 에서 key 값을 찾아 isTransMsg 옵션을 적용해 retObjct 에 반영 한다. *
272
+ * *********************************************************************
273
+ * @param {Object} retObj
274
+ * @param {Object} dataObj
275
+ * @param {String} key
276
+ * @param {Boolean} isTransMsg
277
+ * @returns
278
+ */
279
+ setParams(retObj, dataObj, key, isTransMsg) {
280
+ let value = this.getParams(dataObj, key);
281
+
282
+ if (this.isNotEmpty(value)) {
283
+ retObj[key] = isTransMsg === true ? this.convertMsgCode(value) : value;
284
+ }
285
+ return retObj;
286
+ }
287
+
288
+ /**
289
+ * data 에서 key(Array) 를 찾아 리턴
290
+ ************************************
291
+ * @param {Object} data
292
+ * @param {...String} keys
293
+ * @returns
294
+ */
295
+ getParams(data, ...keys) {
296
+ // keys 파라미터가 없으면 return
297
+ if (arguments.length <= 1) {
298
+ return undefined;
299
+ }
300
+
301
+ let key = keys[0];
302
+ // data 에 key 가 없으면 return
303
+ if (this.isEmpty(data[key])) {
304
+ return undefined;
305
+ }
306
+
307
+ let paramData = data[key];
308
+
309
+ if (keys.length > 1) {
310
+ // 현재 이후에 키가 더 있으면 재귀 호출
311
+ return this.getParams(paramData, ...keys.slice(1));
312
+ } else {
313
+ return paramData;
314
+ }
315
+ }
316
+
317
+ /**
318
+ * 버튼관 연결될 핸들러를 리턴 한다.
319
+ ***********************
320
+ * @description 없으면 message 처리 .
321
+ * @param {String} type
322
+ * @param {String} action
323
+ * @param {Object} logic
324
+ */
325
+ getButtonActionHandler(type, action, logic) {
326
+ if (type == 'basic') {
327
+ if (this[action]) {
328
+ return this[action].bind(this);
329
+ }
330
+ } else {
331
+ return () => this.customButtonHandler(logic);
332
+ }
333
+
334
+ // TODO 메시지 코드 처리
335
+ return () => {
336
+ this.showCustomAlert('label.info', '버튼과 연결된 함수가 없습니다.')
337
+ }
338
+ }
339
+
340
+
341
+ /**
342
+ * 토스트 메시지
343
+ * @param {String} textCode
344
+ */
345
+ showToast(textCode) {
346
+ let message = this.convertMsgCode(textCode);
347
+ document.dispatchEvent(new CustomEvent('notify', { detail: { message } }))
348
+ }
349
+
350
+
351
+ /**
352
+ * alert 박스 보기
353
+ **************************************
354
+ * @param {String} titleCode
355
+ * @param {String} textCode
356
+ * @param {String} type
357
+ * @param {String} confirmButtonCode
358
+ * @param {String} cancelButtonCode
359
+ * @returns {Object}
360
+ */
361
+ async showCustomAlert(titleCode, textCode, type, confirmButtonCode, cancelButtonCode) {
362
+
363
+ // alert 정보 생성
364
+ let alertObj = {
365
+ title: this.convertMsgCode(titleCode),
366
+ text: this.convertMsgCode(textCode)
367
+ };
368
+
369
+ if (this.isNotEmpty(type)) {
370
+ alertObj['type'] = type;
371
+ }
372
+
373
+ if (this.isNotEmpty(confirmButtonCode)) {
374
+ alertObj['confirmButton'] = { text: this.convertMsgCode(confirmButtonCode) };
375
+ }
376
+
377
+ if (this.isNotEmpty(cancelButtonCode)) {
378
+ alertObj['cancelButton'] = { text: this.convertMsgCode(cancelButtonCode) };
379
+ }
380
+
381
+ return await CustomAlert(alertObj);
382
+ }
383
+
384
+ /**
385
+ * object, string, number, array 빈 값 여부 검사
386
+ **************************************
387
+ * @param {Object} param
388
+ * @returns {Boolean}
389
+ */
390
+ isEmpty(param) {
391
+ if (param === undefined) {
392
+ return true;
393
+ } else if (param === null) {
394
+ return true;
395
+ } else if (typeof param === 'boolean') {
396
+ return false;
397
+ } else if (typeof param === 'string' || typeof param === 'number') {
398
+ if (param == '') return true;
399
+ } else if (Array.isArray(param)) {
400
+ if (param.length == 0) return true;
401
+ } else if (typeof param === 'object') {
402
+ if (Object.keys(param).length == 0) return true;
403
+ }
404
+ return false;
405
+ }
406
+
407
+ /**
408
+ * object, string, number, array 빈 값 isNot 여부 검사
409
+ **************************************
410
+ * @param {Object} param
411
+ * @returns {Boolean}
412
+ */
413
+ isNotEmpty(param) {
414
+ if (param === undefined) {
415
+ return false;
416
+ } else if (param === null) {
417
+ return false;
418
+ } else if (typeof param === 'boolean') {
419
+ return true;
420
+ } else if (typeof param === 'string' || typeof param === 'number') {
421
+ if (param != '') return true;
422
+ } else if (Array.isArray(param)) {
423
+ if (param.length > 0) return true;
424
+ } else if (typeof param === 'object') {
425
+ if (Object.keys(param).length > 0) return true;
426
+ }
427
+
428
+ return false;
429
+ }
430
+
431
+
432
+ /**
433
+ * HTML 문자열을 elements 로 반환
434
+ **************************************
435
+ * @param {String} htmlString
436
+ * @returns {HTMLElement}
437
+ */
438
+ htmlToElement(htmlString) {
439
+ var template = document.createElement('template');
440
+ template.innerHTML = htmlString;
441
+ var elements = template.content.childNodes;
442
+ var element = elements[0];
443
+
444
+ template.content.removeChild(element);
445
+
446
+ return element;
447
+ }
448
+
449
+ /**
450
+ * 두 객체가 동일한지 비교
451
+ * @param {*} data1
452
+ * @param {*} data2
453
+ * @returns
454
+ */
455
+ isEquals(data1, data2) {
456
+ return isEqual(data1, data2);
457
+ }
458
+
459
+
460
+ /**
461
+ * config, data 의 key 값을 비교 해 결과를 리턴한다.
462
+ * 값에 * 는 체크 하지 않는다.
463
+ ****************************************
464
+ * @param {Object} config
465
+ * @param {Object} data
466
+ * @param {String Array} keys
467
+ * @returns
468
+ */
469
+ compareObjectValues(config, data, keys) {
470
+ let isEquals = true;
471
+
472
+ keys.forEach(key => {
473
+ let compareValue = config[key];
474
+
475
+ if (compareValue === '*') {
476
+ return;
477
+ }
478
+
479
+ let recordValue = data[key] || '';
480
+
481
+ if (this.isEmpty(compareValue)) {
482
+ if (this.isNotEmpty(recordValue)) {
483
+ isEquals = false;
484
+ }
485
+ } else {
486
+ if (compareValue != recordValue) {
487
+ isEquals = false;
488
+ }
489
+ }
490
+ })
491
+
492
+ return isEquals;
493
+ }
494
+
495
+
496
+ /**
497
+ * 메뉴 메타 정보에서 페이지 Context 정보 추출
498
+ ***************************
499
+ * @description 메뉴 타이틀, 도움말, import, export, 각종 버튼들 포함
500
+ * @returns {Object}
501
+ */
502
+ getContextObject() {
503
+
504
+ // 리턴 오브젝트
505
+ let retContext = { actions: [] };
506
+ // 기본 타이틀 및 도움말 설정
507
+ retContext = this.setParams(retContext, this.menuInfo, 'title', true);
508
+ retContext = this.setParams(retContext, this.menuInfo, 'help');
509
+
510
+ // 버튼 정보 추출
511
+ let buttons = this.getContextButtons();
512
+
513
+ // 버튼 context 오브젝트에 포함
514
+ retContext.actions.push(...buttons);
515
+
516
+ // 내보내기 버튼
517
+ if (this.use_button_export) {
518
+ retContext.exportable = {
519
+ name: retContext.title,
520
+ data: this._exportableData.bind(this)
521
+ }
522
+ }
523
+
524
+ // 임포트 버튼
525
+ if (this.use_button_import) {
526
+ retContext.importable = {
527
+ handler: this._importableData.bind(this)
528
+ }
529
+ }
530
+
531
+ return retContext;
532
+ }
533
+
534
+
535
+
536
+ /**
537
+ * 화면에 구성된 콤포넌트에 따라 조회를 호출 한다.
538
+ */
539
+ async fetch() {
540
+ if (this.isNotEmpty(this.grist)) {
541
+ this.grist.fetch();
542
+ } else if (this.isNotEmpty(this.filterForm)) {
543
+ await this.fetchHandler();
544
+ } else {
545
+ await this.fetchHandler();
546
+ }
547
+ }
548
+
549
+
550
+ /**********************************
551
+ * LifeCycle
552
+ ***********************************/
553
+
554
+ async connectedCallback() {
555
+ if (super.connectedCallback) {
556
+ await super.connectedCallback();
557
+ }
558
+ }
559
+
560
+ async firstUpdated() {
561
+ if (super.firstUpdated) {
562
+ await super.firstUpdated();
563
+ }
564
+ }
565
+
566
+ async pageInitialized() {
567
+ if (super.pageInitialized) {
568
+ await super.pageInitialized();
569
+ }
570
+ }
571
+
572
+ async disconnectedCallback() {
573
+ if (super.disconnectedCallback) {
574
+ await super.disconnectedCallback();
575
+ }
576
+ }
577
+ }