es-application-template 0.0.8 → 0.0.11

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/index.mjs CHANGED
@@ -4,7 +4,7 @@ import axios from 'axios';
4
4
  import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query';
5
5
  import { useNavigate } from 'react-router-dom';
6
6
  import { UncontrolledTooltip, Input, FormFeedback, FormText, Label, Button } from 'reactstrap';
7
- import { Trash2, X, Edit, Info, Copy, Trash, Settings, Mail, Plus, ChevronLeft, ChevronRight, RefreshCw, Save } from 'becoxy-icons';
7
+ import { Trash2, X, Edit, Info, Copy, Trash, Settings, Mail, Plus, ChevronLeft, ChevronRight, Download, RefreshCw, Save } from 'becoxy-icons';
8
8
  import toast from 'react-hot-toast';
9
9
  import classNames from 'classnames';
10
10
  import { Controller } from 'react-hook-form';
@@ -5392,19 +5392,26 @@ const Sidebar = (props) => {
5392
5392
  toggleSidebar();
5393
5393
  }, 400);
5394
5394
  };
5395
- const sidebar = (jsxs("div", { tabIndex: 0, className: classNames({ 'd-none': !open }), onKeyDown: (e) => {
5395
+ const sidebar = (jsxs("div", { tabIndex: 0, className: classNames('es-application-sidebar-root', {
5396
+ 'is-hidden': !open
5397
+ }), onKeyDown: (e) => {
5396
5398
  if ((!keyboard) && e.code === 'Escape' && flag) {
5397
5399
  handClose();
5398
5400
  e.stopPropagation();
5399
5401
  e.preventDefault();
5400
5402
  }
5401
- }, children: [jsx("div", { className: classNames('offcanvas-backdrop fade', backdropClassName, {
5402
- 'd-none': !flag || hiddenBackground,
5403
+ }, children: [jsx("div", { className: classNames('es-application-sidebar-backdrop', backdropClassName, {
5404
+ 'is-hidden': !flag || hiddenBackground,
5403
5405
  show: flag
5404
- }), style: { zIndex: zIndex - 1 }, onClick: handClose }), jsxs("div", { className: classNames('customizer d-none d-md-block ', `customizer-${width ?? 600}`, panelClassName, {
5406
+ }), style: { zIndex: zIndex - 1 }, onClick: handClose }), jsxs("div", { className: classNames('es-application-sidebar', panelClassName, {
5405
5407
  open: flag,
5406
5408
  fullscreen: isFullScreen
5407
- }), style: { zIndex }, children: [jsxs("div", { style: {
5409
+ }), style: {
5410
+ '--es-application-sidebar-width': typeof width === 'number'
5411
+ ? `${width}px`
5412
+ : width ?? '600px',
5413
+ zIndex
5414
+ }, children: [jsxs("div", { style: {
5408
5415
  position: 'fixed',
5409
5416
  top: '50%',
5410
5417
  transform: 'translate(-50%, -50%)',
@@ -5566,83 +5573,156 @@ const ConfigTreeJsonDrawer = ({ open, onClose, rows, mode = 'VIEW', onApply, fil
5566
5573
  return (jsx(Sidebar, { open: open, toggleSidebar: onClose, width: 760, keyboard: true, hiddenFullScreenButton: true, zIndex: 1061, children: jsxs("div", { className: 'h-100 bg-white d-flex flex-column', children: [jsx(ModalHeader, { typeModal: 'Detail', handleModal: onClose, title: mode === 'IMPORT' ? 'Nhập JSON thiết lập' : 'Xem / Xuất JSON thiết lập' }), mode === 'VIEW' ? (jsxs(Fragment, { children: [jsxs("div", { className: 'd-flex justify-content-end gap-50 border-bottom p-1', children: [jsx(Button, { type: 'button', color: 'outline-primary', className: 'be-button', disabled: currentPreview.errors.length > 0, onClick: () => { void handleCopy(); }, children: "Sao ch\u00E9p" }), jsx(Button, { type: 'button', color: 'primary', className: 'be-button', disabled: currentPreview.errors.length > 0, onClick: () => downloadJson(currentJson, fileName), children: "T\u1EA3i file JSON" })] }), jsx("div", { className: 'flex-grow-1 overflow-auto', style: { minHeight: 0 }, children: jsx("div", { className: 'p-1', children: currentPreview.errors.length > 0 ? (jsxs("div", { className: 'alert alert-danger py-50 px-1 mb-0', children: [jsx("div", { className: 'fw-bolder mb-25', children: "D\u1EEF li\u1EC7u nh\u00E1p ch\u01B0a h\u1EE3p l\u1EC7:" }), currentPreview.errors.slice(0, 5).map((error, index) => (jsxs("div", { children: ["\u2022 ", error] }, `${index}-${error}`)))] })) : (jsx("pre", { className: 'bg-light border rounded p-1 mb-0', children: currentJson })) }) })] })) : (jsxs(Fragment, { children: [jsxs("div", { className: 'border-bottom p-1', children: [jsxs("div", { className: 'd-flex align-items-end gap-1 flex-wrap', children: [jsxs("div", { style: { width: 190 }, children: [jsx(Label, { className: 'mb-25', children: "Ch\u1EBF \u0111\u1ED9 nh\u1EADp" }), jsxs(Input, { type: 'select', value: importMode, onChange: event => setImportMode(event.target.value), children: [jsx("option", { value: 'MERGE', children: "H\u1EE3p nh\u1EA5t (khuy\u1EBFn ngh\u1ECB)" }), jsx("option", { value: 'REPLACE', children: "Thay th\u1EBF to\u00E0n b\u1ED9" })] })] }), jsxs("div", { children: [jsx("input", { ref: fileInputRef, type: 'file', accept: '.json,application/json', hidden: true, onChange: event => { void handleFileChange(event); } }), jsx(Button, { type: 'button', color: 'outline-primary', className: 'be-button', onClick: () => fileInputRef.current?.click(), children: "Ch\u1ECDn file .json" })] })] }), jsx("div", { className: 'small text-muted mt-50', children: "H\u1EE3p nh\u1EA5t: key m\u1EDBi \u0111\u01B0\u1EE3c th\u00EAm, key tr\u00F9ng \u0111\u01B0\u1EE3c c\u1EADp nh\u1EADt, key kh\u00E1c gi\u1EEF nguy\u00EAn. Thay th\u1EBF: d\u00F9ng JSON import l\u00E0m to\u00E0n b\u1ED9 d\u1EEF li\u1EC7u nh\u00E1p hi\u1EC7n t\u1EA1i." })] }), jsxs("div", { className: 'flex-grow-1 min-h-0 d-flex flex-column p-1', children: [jsx(Label, { for: 'config-json-import', className: 'mb-50', children: "D\u00E1n JSON ho\u1EB7c ch\u1ECDn file" }), jsx(Input, { id: 'config-json-import', type: 'textarea', className: 'font-monospace flex-grow-1', style: { minHeight: 280, resize: 'none' }, value: importText, placeholder: 'D\u00E1n JSON thi\u1EBFt l\u1EADp t\u1EA1i \u0111\u00E2y...', onChange: event => setImportText(event.target.value) }), !importText.trim() ? (jsx("div", { className: 'alert alert-info py-50 px-1 mt-1 mb-0', children: "Ch\u01B0a c\u00F3 d\u1EEF li\u1EC7u import. C\u00F3 th\u1EC3 d\u00F9ng ChatGPT t\u1EA1o file JSON r\u1ED3i d\u00E1n ho\u1EB7c t\u1EA3i file l\u00EAn." })) : importPreview.errors.length > 0 ? (jsxs("div", { className: 'alert alert-danger py-50 px-1 mt-1 mb-0', children: [jsx("div", { className: 'fw-bolder mb-25', children: "JSON ch\u01B0a h\u1EE3p l\u1EC7:" }), importPreview.errors.slice(0, 5).map((error, index) => (jsxs("div", { children: ["\u2022 ", error] }, `${index}-${error}`))), importPreview.errors.length > 5 ? (jsxs("div", { children: ["\u2022 ... v\u00E0 ", importPreview.errors.length - 5, " l\u1ED7i kh\u00E1c"] })) : null] })) : (jsxs("div", { className: 'alert alert-success py-50 px-1 mt-1 mb-0', children: ["JSON h\u1EE3p l\u1EC7. Sau khi \u00E1p d\u1EE5ng, Grid s\u1EBD c\u00F3 ", countConfigNodes(importPreview.rows), " node. D\u1EEF li\u1EC7u ch\u1EC9 l\u00E0 nh\u00E1p cho \u0111\u1EBFn khi b\u1EA5m L\u01B0u \u1EDF m\u00E0n ch\u00EDnh."] }))] }), importMode === 'REPLACE' ? (jsx("div", { className: 'alert alert-warning rounded-0 border-start-0 border-end-0 mb-0 py-50 px-1', children: "Ch\u1EBF \u0111\u1ED9 Thay th\u1EBF s\u1EBD b\u1ECF to\u00E0n b\u1ED9 key hi\u1EC7n c\u00F3 kh\u1ECFi d\u1EEF li\u1EC7u nh\u00E1p tr\u01B0\u1EDBc khi \u00E1p d\u1EE5ng JSON import." })) : null, jsxs("div", { className: 'd-flex justify-content-end gap-50 border-top p-1', children: [jsx(Button, { type: 'button', color: 'secondary', outline: true, className: 'be-button', onClick: onClose, children: "H\u1EE7y" }), jsx(Button, { type: 'button', color: 'primary', className: 'be-button', disabled: !importPreview.ready || !onApply, onClick: handleApply, children: "\u00C1p d\u1EE5ng v\u00E0o Grid" })] })] }))] }) }));
5567
5574
  };
5568
5575
 
5569
- const SELECT_MENU_PORTAL_STYLES = {
5570
- menuPortal: (base) => ({
5571
- ...base,
5572
- zIndex: 1100
5573
- })
5574
- };
5575
- const UI_KEY_SAMPLES = [
5576
+ const UI_RULES = [
5577
+ {
5578
+ id: 'list-grid-visible',
5579
+ section: 'list-grid',
5580
+ category: 'ui-list',
5581
+ pattern: 'ui.list.grid.<field>.visible',
5582
+ type: 'boolean',
5583
+ defaultValue: true,
5584
+ examples: ['ui.list.grid.email.visible'],
5585
+ description: 'Ẩn hoặc hiện một cột trên Grid của danh sách.'
5586
+ },
5576
5587
  {
5588
+ id: 'list-toolbar-visible',
5589
+ section: 'list-grid',
5590
+ category: 'ui-list',
5577
5591
  pattern: 'ui.list.toolbar.<keyCode>.visible',
5578
5592
  type: 'boolean',
5579
- examples: ['ui.list.toolbar.status.visible']
5593
+ defaultValue: true,
5594
+ examples: ['ui.list.toolbar.status.visible'],
5595
+ description: 'Ẩn hoặc hiện một điều khiển trên toolbar của danh sách.'
5580
5596
  },
5581
5597
  {
5598
+ id: 'list-toolbar-disabled',
5599
+ section: 'list-grid',
5600
+ category: 'ui-list',
5582
5601
  pattern: 'ui.list.toolbar.<keyCode>.disabled',
5583
5602
  type: 'boolean',
5584
- examples: ['ui.list.toolbar.status.disabled']
5603
+ defaultValue: false,
5604
+ examples: ['ui.list.toolbar.status.disabled'],
5605
+ description: 'Bật hoặc khóa một điều khiển trên toolbar của danh sách.'
5585
5606
  },
5586
5607
  {
5608
+ id: 'list-action-visible',
5609
+ section: 'list-grid',
5610
+ category: 'ui-list',
5587
5611
  pattern: 'ui.list.action.<id>.visible',
5588
5612
  type: 'boolean',
5589
- examples: ['ui.list.action.DELETE.visible']
5613
+ defaultValue: true,
5614
+ examples: ['ui.list.action.DELETE.visible'],
5615
+ description: 'Ẩn hoặc hiện một action của dòng hay danh sách.'
5590
5616
  },
5591
5617
  {
5618
+ id: 'list-action-disabled',
5619
+ section: 'list-grid',
5620
+ category: 'ui-list',
5592
5621
  pattern: 'ui.list.action.<id>.disabled',
5593
5622
  type: 'boolean',
5594
- examples: ['ui.list.action.DELETE.disabled']
5623
+ defaultValue: false,
5624
+ examples: ['ui.list.action.DELETE.disabled'],
5625
+ description: 'Bật hoặc khóa một action của dòng hay danh sách.'
5595
5626
  },
5596
5627
  {
5628
+ id: 'form-field-visible',
5629
+ section: 'form-field',
5630
+ category: 'ui-form',
5597
5631
  pattern: 'ui.form.field.<name>.visible',
5598
5632
  type: 'boolean',
5599
- examples: ['ui.form.field.email.visible']
5633
+ defaultValue: true,
5634
+ examples: ['ui.form.field.email.visible'],
5635
+ description: 'Ẩn hoặc hiện field trên form.'
5600
5636
  },
5601
5637
  {
5638
+ id: 'form-field-required',
5639
+ section: 'form-field',
5640
+ category: 'ui-form',
5602
5641
  pattern: 'ui.form.field.<name>.required',
5603
5642
  type: 'boolean',
5604
- examples: ['ui.form.field.email.required']
5643
+ defaultValue: false,
5644
+ examples: ['ui.form.field.email.required'],
5645
+ description: 'Quy định field có bắt buộc nhập hay không.'
5605
5646
  },
5606
5647
  {
5648
+ id: 'form-field-disabled',
5649
+ section: 'form-field',
5650
+ category: 'ui-form',
5607
5651
  pattern: 'ui.form.field.<name>.disabled',
5608
5652
  type: 'boolean',
5609
- examples: ['ui.form.field.email.disabled']
5653
+ defaultValue: false,
5654
+ examples: ['ui.form.field.email.disabled'],
5655
+ description: 'Bật hoặc khóa tương tác với field.'
5610
5656
  },
5611
5657
  {
5658
+ id: 'form-field-readonly',
5659
+ section: 'form-field',
5660
+ category: 'ui-form',
5612
5661
  pattern: 'ui.form.field.<name>.readOnly',
5613
5662
  type: 'boolean',
5614
- examples: ['ui.form.field.email.readOnly']
5663
+ defaultValue: false,
5664
+ examples: ['ui.form.field.email.readOnly'],
5665
+ description: 'Quy định field chỉ đọc hay có thể chỉnh sửa.'
5615
5666
  },
5616
5667
  {
5668
+ id: 'form-action-visible',
5669
+ section: 'form-action',
5670
+ category: 'ui-form',
5617
5671
  pattern: 'ui.form.action.<id>.visible',
5618
5672
  type: 'boolean',
5619
- examples: ['ui.form.action.SAVE.visible']
5673
+ defaultValue: true,
5674
+ examples: ['ui.form.action.SAVE.visible'],
5675
+ description: 'Ẩn hoặc hiện một action trên form.'
5620
5676
  },
5621
5677
  {
5678
+ id: 'form-action-disabled',
5679
+ section: 'form-action',
5680
+ category: 'ui-form',
5622
5681
  pattern: 'ui.form.action.<id>.disabled',
5623
5682
  type: 'boolean',
5624
- examples: ['ui.form.action.SAVE.disabled']
5683
+ defaultValue: false,
5684
+ examples: ['ui.form.action.SAVE.disabled'],
5685
+ description: 'Bật hoặc khóa một action trên form.'
5625
5686
  },
5626
5687
  {
5688
+ id: 'form-tab-visible',
5689
+ section: 'form-structure',
5690
+ category: 'ui-form',
5627
5691
  pattern: 'ui.form.tab.<key>.visible',
5628
5692
  type: 'boolean',
5629
- examples: ['ui.form.tab.permission.visible']
5693
+ defaultValue: true,
5694
+ examples: ['ui.form.tab.permission.visible'],
5695
+ description: 'Ẩn hoặc hiện tab trên form.'
5630
5696
  },
5631
5697
  {
5698
+ id: 'form-step-visible',
5699
+ section: 'form-structure',
5700
+ category: 'ui-form',
5632
5701
  pattern: 'ui.form.step.<key>.visible',
5633
5702
  type: 'boolean',
5634
- examples: ['ui.form.step.confirm.visible']
5703
+ defaultValue: true,
5704
+ examples: ['ui.form.step.confirm.visible'],
5705
+ description: 'Ẩn hoặc hiện một bước trong quy trình form.'
5635
5706
  },
5636
5707
  {
5708
+ id: 'form-section-visible',
5709
+ section: 'form-structure',
5710
+ category: 'ui-form',
5637
5711
  pattern: 'ui.form.section.<key>.visible',
5638
5712
  type: 'boolean',
5639
- examples: ['ui.form.section.security.visible']
5713
+ defaultValue: true,
5714
+ examples: ['ui.form.section.security.visible'],
5715
+ description: 'Ẩn hoặc hiện một khu vực trên form.'
5640
5716
  }
5641
5717
  ];
5642
- const EXPLICIT_KEY_SAMPLES = [
5718
+ const NON_UI_RULES = [
5643
5719
  {
5720
+ id: 'behavior-boolean',
5721
+ section: 'other-settings',
5722
+ category: 'behavior',
5644
5723
  pattern: 'behavior.<meaning>',
5645
5724
  type: 'boolean',
5725
+ sampleValue: true,
5646
5726
  examples: [
5647
5727
  'behavior.allowDelete',
5648
5728
  'behavior.allowEdit',
@@ -5652,182 +5732,389 @@ const EXPLICIT_KEY_SAMPLES = [
5652
5732
  'behavior.allowChangeStatus',
5653
5733
  'behavior.allowSort',
5654
5734
  'behavior.autoSubmit'
5655
- ]
5735
+ ],
5736
+ description: 'Điều khiển một hành vi nghiệp vụ có ý nghĩa ổn định.'
5656
5737
  },
5657
5738
  {
5739
+ id: 'default-number',
5740
+ section: 'other-settings',
5741
+ category: 'default',
5658
5742
  pattern: 'default.<fieldOrMeaning>',
5659
5743
  type: 'number',
5660
- examples: [
5661
- 'default.status',
5662
- 'default.pageSize'
5663
- ]
5744
+ sampleValue: 1,
5745
+ examples: ['default.status', 'default.pageSize'],
5746
+ description: 'Giá trị số mặc định cho field hoặc hành vi.'
5664
5747
  },
5665
5748
  {
5749
+ id: 'default-string',
5750
+ section: 'other-settings',
5751
+ category: 'default',
5666
5752
  pattern: 'default.<fieldOrMeaning>',
5667
5753
  type: 'string',
5668
- examples: [
5669
- 'default.note',
5670
- 'default.currencyCode'
5671
- ]
5754
+ sampleValue: 'VND',
5755
+ examples: ['default.note', 'default.currencyCode'],
5756
+ description: 'Giá trị chuỗi mặc định cho field hoặc hành vi.'
5672
5757
  },
5673
5758
  {
5759
+ id: 'default-date',
5760
+ section: 'other-settings',
5761
+ category: 'default',
5674
5762
  pattern: 'default.<fieldOrMeaning>',
5675
5763
  type: 'date',
5676
- examples: ['default.startDate']
5764
+ sampleValue: '2026-01-01',
5765
+ examples: ['default.startDate'],
5766
+ description: 'Giá trị ngày mặc định.'
5677
5767
  },
5678
5768
  {
5769
+ id: 'default-datetime',
5770
+ section: 'other-settings',
5771
+ category: 'default',
5679
5772
  pattern: 'default.<fieldOrMeaning>',
5680
5773
  type: 'datetime',
5681
- examples: ['default.executeAt']
5774
+ sampleValue: '2026-01-01T08:00:00+07:00',
5775
+ examples: ['default.executeAt'],
5776
+ description: 'Giá trị ngày giờ mặc định.'
5682
5777
  },
5683
5778
  {
5779
+ id: 'default-guid',
5780
+ section: 'other-settings',
5781
+ category: 'default',
5684
5782
  pattern: 'default.<fieldOrMeaning>',
5685
5783
  type: 'guid',
5686
- examples: [
5687
- 'default.departmentId',
5688
- 'default.warehouseId'
5689
- ]
5784
+ sampleValue: '00000000-0000-0000-0000-000000000001',
5785
+ examples: ['default.departmentId', 'default.warehouseId'],
5786
+ description: 'Giá trị định danh mặc định.'
5690
5787
  },
5691
5788
  {
5789
+ id: 'option-json',
5790
+ section: 'other-settings',
5791
+ category: 'option',
5692
5792
  pattern: 'option.<meaning>',
5693
5793
  type: 'json',
5794
+ sampleValue: [0, 1],
5694
5795
  examples: [
5695
5796
  'option.allowedStatuses',
5696
5797
  'option.allowedTypes',
5697
5798
  'option.exportFormats'
5698
- ]
5799
+ ],
5800
+ description: 'Danh sách hoặc cấu trúc tùy chọn có schema rõ ràng.'
5699
5801
  },
5700
5802
  {
5803
+ id: 'custom-domain-boolean',
5804
+ section: 'other-settings',
5805
+ category: 'custom',
5701
5806
  pattern: '<domain>.<meaning>',
5702
5807
  type: 'boolean',
5808
+ sampleValue: true,
5703
5809
  examples: [
5704
5810
  'approval.autoSubmit',
5705
5811
  'approval.requireComment',
5706
5812
  'password.requireChangeOnFirstLogin',
5707
5813
  'confirmation.requireRedeployConfirmation'
5708
- ]
5814
+ ],
5815
+ description: 'Cờ tùy chỉnh cho một miền nghiệp vụ chưa thuộc pattern chung.'
5709
5816
  },
5710
5817
  {
5818
+ id: 'custom-domain-number',
5819
+ section: 'other-settings',
5820
+ category: 'custom',
5711
5821
  pattern: '<domain>.<meaning>',
5712
5822
  type: 'number',
5823
+ sampleValue: 30,
5713
5824
  examples: [
5714
5825
  'integration.timeoutSeconds',
5715
5826
  'integration.retryCount',
5716
5827
  'calculation.decimalPlaces'
5717
- ]
5828
+ ],
5829
+ description: 'Giá trị số tùy chỉnh cho một miền nghiệp vụ.'
5718
5830
  },
5719
5831
  {
5832
+ id: 'custom-domain-string',
5833
+ section: 'other-settings',
5834
+ category: 'custom',
5720
5835
  pattern: '<domain>.<meaning>',
5721
5836
  type: 'string',
5722
- examples: ['calculation.roundingMode']
5837
+ sampleValue: 'HALF_UP',
5838
+ examples: ['calculation.roundingMode'],
5839
+ description: 'Giá trị chuỗi tùy chỉnh cho một miền nghiệp vụ.'
5723
5840
  },
5724
5841
  {
5842
+ id: 'custom-root-number',
5843
+ section: 'other-settings',
5844
+ category: 'custom',
5725
5845
  pattern: '<key>',
5726
5846
  type: 'number',
5727
- examples: ['maxDetailRows']
5847
+ sampleValue: 100,
5848
+ examples: ['maxDetailRows'],
5849
+ description: 'Key gốc chỉ dùng khi không có namespace phù hợp và ý nghĩa vẫn rõ ràng.'
5728
5850
  }
5729
5851
  ];
5730
- const KEY_SAMPLES = [
5731
- ...UI_KEY_SAMPLES,
5732
- ...EXPLICIT_KEY_SAMPLES
5852
+ const SETTING_KEY_RULES = [
5853
+ ...UI_RULES,
5854
+ ...NON_UI_RULES
5733
5855
  ];
5734
- const CATEGORY_OPTIONS = [
5735
- {
5736
- value: 'ui-list',
5737
- label: 'UI danh sách'
5738
- },
5739
- {
5740
- value: 'ui-form',
5741
- label: 'UI biểu mẫu'
5742
- },
5743
- {
5744
- value: 'behavior',
5745
- label: 'Hành vi'
5746
- },
5747
- {
5748
- value: 'default',
5749
- label: 'Mặc định'
5750
- },
5751
- {
5752
- value: 'option',
5753
- label: 'Tùy chọn'
5856
+ const SETTING_KEY_CONVENTION = {
5857
+ id: 'becoxy-setting-key-convention',
5858
+ version: 1,
5859
+ title: 'BECoxy Setting Key Convention',
5860
+ objective: [
5861
+ 'Các thiết lập UI và behavior phải được đọc qua setting.get(key, fallback).',
5862
+ 'Không hard-code cấu hình nếu giá trị cần được quản trị từ Setting.'
5863
+ ],
5864
+ keyStructure: {
5865
+ pattern: '<Resource>.<setting-key>',
5866
+ explanation: [
5867
+ 'Resource là mã ổn định của feature hoặc tài nguyên đang được cấu hình.',
5868
+ 'Phần setting key phải tuân thủ các pattern bên dưới.',
5869
+ 'Không nhúng productCode, userId hoặc scope vào key.'
5870
+ ],
5871
+ examples: [
5872
+ 'RESOURCE_CODE.ui.list.toolbar.status.visible',
5873
+ 'RESOURCE_CODE.ui.form.field.email.required',
5874
+ 'RESOURCE_CODE.ui.form.action.SAVE.disabled'
5875
+ ]
5754
5876
  },
5755
- {
5756
- value: 'custom',
5757
- label: 'Tùy chỉnh'
5758
- }
5759
- ];
5760
- const TYPE_OPTIONS = Array.from(new Set(KEY_SAMPLES.map(row => row.type))).map(value => ({
5761
- value,
5762
- label: value
5763
- }));
5764
- const getSampleCategory = (pattern) => {
5765
- if (pattern.startsWith('ui.list.')) {
5766
- return 'ui-list';
5767
- }
5768
- if (pattern.startsWith('ui.form.')) {
5769
- return 'ui-form';
5770
- }
5771
- if (pattern.startsWith('behavior.')) {
5772
- return 'behavior';
5773
- }
5774
- if (pattern.startsWith('default.')) {
5775
- return 'default';
5877
+ categories: [
5878
+ { value: 'ui-list', label: 'UI danh sách' },
5879
+ { value: 'ui-form', label: 'UI biểu mẫu' },
5880
+ { value: 'behavior', label: 'Hành vi' },
5881
+ { value: 'default', label: 'Mặc định' },
5882
+ { value: 'option', label: 'Tùy chọn' },
5883
+ { value: 'custom', label: 'Tùy chỉnh' }
5884
+ ],
5885
+ ruleSections: [
5886
+ {
5887
+ id: 'list-grid',
5888
+ title: 'LIST / GRID',
5889
+ description: 'Cấu hình cột Grid, toolbar và action trên màn danh sách.'
5890
+ },
5891
+ {
5892
+ id: 'form-field',
5893
+ title: 'FORM FIELD',
5894
+ description: 'Cấu hình trạng thái hiển thị và tương tác của field.'
5895
+ },
5896
+ {
5897
+ id: 'form-action',
5898
+ title: 'FORM ACTION',
5899
+ description: 'Cấu hình action trên form.'
5900
+ },
5901
+ {
5902
+ id: 'form-structure',
5903
+ title: 'FORM STRUCTURE',
5904
+ description: 'Cấu hình tab, step và section trên form.'
5905
+ },
5906
+ {
5907
+ id: 'other-settings',
5908
+ title: 'BEHAVIOR / DEFAULT / OPTION / CUSTOM',
5909
+ description: 'Các pattern ngoài UI chỉ dùng khi đúng ý nghĩa nghiệp vụ.'
5910
+ }
5911
+ ],
5912
+ rules: SETTING_KEY_RULES,
5913
+ guidance: [
5914
+ {
5915
+ title: 'CÁCH ĐỌC SETTING',
5916
+ paragraphs: ['Chỉ dùng public API của package:'],
5917
+ examples: [
5918
+ 'setting.get<T>(key)',
5919
+ 'setting.get<T>(key, fallback)',
5920
+ "setting.get('RESOURCE_CODE.ui.form.field.Code.visible', true)",
5921
+ "setting.get('RESOURCE_CODE.ui.form.action.SAVE.disabled', false)"
5922
+ ],
5923
+ items: [
5924
+ 'Không tạo useSetting(resource).',
5925
+ 'Không tạo useFeatureSetting hoặc useFieldSetting.',
5926
+ 'Không tạo getBoolean, getNumber hoặc getJson.',
5927
+ 'Không tạo SettingResourceScope.'
5928
+ ]
5929
+ },
5930
+ {
5931
+ title: 'FALLBACK',
5932
+ paragraphs: ['Boolean UI setting phải có fallback rõ ràng khi phù hợp.'],
5933
+ items: [
5934
+ 'visible: fallback thường là true.',
5935
+ 'disabled: fallback thường là false.',
5936
+ 'required: fallback thường là false.',
5937
+ 'readOnly: fallback thường là false.'
5938
+ ]
5939
+ },
5940
+ {
5941
+ title: 'NAMING',
5942
+ items: [
5943
+ 'Không dùng label hiển thị hoặc nội dung đã dịch làm key.',
5944
+ 'ID, action và key phải ổn định khi thay đổi ngôn ngữ.',
5945
+ 'Không nhúng productCode, userId hoặc scope vào key.',
5946
+ 'Sai: ui.form.field.Tên người dùng.visible.',
5947
+ 'Đúng: ui.form.field.username.visible.',
5948
+ 'Sai: PRODUCT.RESOURCE_CODE.ui.form.field.Code.visible.',
5949
+ 'Đúng: RESOURCE_CODE.ui.form.field.Code.visible.'
5950
+ ]
5951
+ },
5952
+ {
5953
+ title: 'SCOPE / PRECEDENCE',
5954
+ paragraphs: ['Effective Setting được giải quyết theo thứ tự SYSTEM → PRODUCT → USER → setting.get(...).'],
5955
+ items: [
5956
+ 'Feature code không tự xử lý precedence.',
5957
+ 'Feature code không tự request riêng SYSTEM, PRODUCT và USER.',
5958
+ 'Scope, Product và User được runtime quản lý ngoài key.'
5959
+ ]
5960
+ },
5961
+ {
5962
+ title: 'KHI THÊM SETTING MỚI',
5963
+ items: [
5964
+ 'Kiểm tra pattern hiện có và ưu tiên reuse convention.',
5965
+ 'Chỉ tạo namespace mới khi behavior không thuộc pattern hiện có.',
5966
+ 'Xác định type, fallback và description rõ nghĩa.',
5967
+ 'Thêm key vào Setting catalog khi cần quản trị.',
5968
+ 'Không tạo API wrapper riêng chỉ cho một key.'
5969
+ ]
5970
+ },
5971
+ {
5972
+ title: 'VÍ DỤ ÁP DỤNG',
5973
+ examples: [
5974
+ "const canShowCode = setting.get('RESOURCE_CODE.ui.form.field.Code.visible', true)",
5975
+ "const codeRequired = setting.get('RESOURCE_CODE.ui.form.field.Code.required', false)",
5976
+ "const saveDisabled = setting.get('RESOURCE_CODE.ui.form.action.SAVE.disabled', false)"
5977
+ ]
5978
+ },
5979
+ {
5980
+ title: 'NGUYÊN TẮC CHO AI/CODEX',
5981
+ items: [
5982
+ 'Đọc source hiện tại trước khi thay đổi.',
5983
+ 'Xác định resource hiện tại.',
5984
+ 'Liệt kê component, action và field cần cấu hình.',
5985
+ 'Ánh xạ vào convention có sẵn; không tự phát minh key khi pattern đã bao phủ.',
5986
+ 'Dùng setting.get(..., fallback).',
5987
+ 'Không thêm hook hoặc public Setting API mới.',
5988
+ 'Không thay đổi Setting Runtime.',
5989
+ 'Không thêm selector Scope, Product hoặc User vào feature.',
5990
+ 'Chỉ sửa các file thực sự sử dụng setting.'
5991
+ ]
5992
+ }
5993
+ ]
5994
+ };
5995
+ const getRuleSampleValue = (rule) => {
5996
+ if (rule.sampleValue !== undefined) {
5997
+ return rule.sampleValue;
5776
5998
  }
5777
- if (pattern.startsWith('option.')) {
5778
- return 'option';
5999
+ if (rule.defaultValue !== undefined) {
6000
+ return rule.defaultValue;
5779
6001
  }
5780
- return 'custom';
6002
+ return null;
6003
+ };
6004
+ const setNestedLeaf = (target, path, leaf) => {
6005
+ const segments = path.split('.').filter(Boolean);
6006
+ let current = target;
6007
+ segments.forEach((segment, index) => {
6008
+ if (index === segments.length - 1) {
6009
+ current[segment] = leaf;
6010
+ return;
6011
+ }
6012
+ const next = current[segment];
6013
+ if (!next || typeof next !== 'object' || Array.isArray(next)) {
6014
+ current[segment] = {};
6015
+ }
6016
+ current = current[segment];
6017
+ });
6018
+ };
6019
+ const buildSettingImportExample = (resourceCode = 'RESOURCE_CODE', convention = SETTING_KEY_CONVENTION) => {
6020
+ const resource = resourceCode.trim() || 'RESOURCE_CODE';
6021
+ const settings = {};
6022
+ convention.rules.forEach(rule => {
6023
+ const example = rule.examples[0];
6024
+ if (!example) {
6025
+ return;
6026
+ }
6027
+ setNestedLeaf(settings, example, {
6028
+ type: rule.type,
6029
+ value: getRuleSampleValue(rule),
6030
+ description: rule.description
6031
+ });
6032
+ });
6033
+ return {
6034
+ [resource]: settings
6035
+ };
5781
6036
  };
5782
- const filterSamples = (rows, searchTerm, category, type) => {
6037
+ const serializeSettingImportExample = (resourceCode = 'RESOURCE_CODE', convention = SETTING_KEY_CONVENTION) => JSON.stringify(buildSettingImportExample(resourceCode, convention), null, 2);
6038
+ const filterSettingKeyRules = (rows, searchTerm, category, type) => {
5783
6039
  const keyword = searchTerm.trim().toLowerCase();
5784
6040
  return rows.filter(row => {
5785
6041
  const matchesKeyword = !keyword || [
5786
6042
  row.pattern,
5787
6043
  row.type,
6044
+ row.description,
5788
6045
  ...row.examples
5789
6046
  ].some(value => value.toLowerCase().includes(keyword));
5790
- const matchesCategory = !category || getSampleCategory(row.pattern) === category;
6047
+ const matchesCategory = !category || row.category === category;
5791
6048
  const matchesType = !type || row.type === type;
5792
- return matchesKeyword &&
5793
- matchesCategory &&
5794
- matchesType;
6049
+ return matchesKeyword && matchesCategory && matchesType;
5795
6050
  });
5796
6051
  };
6052
+
6053
+ const RESOURCE_CODE_PLACEHOLDER = 'RESOURCE_CODE';
6054
+ const SELECT_MENU_PORTAL_STYLES = {
6055
+ menuPortal: (base) => ({
6056
+ ...base,
6057
+ zIndex: 1100
6058
+ })
6059
+ };
6060
+ const TYPE_OPTIONS = Array.from(new Set(SETTING_KEY_CONVENTION.rules.map(row => row.type))).map(value => ({
6061
+ value,
6062
+ label: value
6063
+ }));
5797
6064
  const TechnicalText = ({ children }) => (jsx("span", { className: 'config-key-sample-text text-break', children: children }));
5798
6065
  const KeySampleTable = ({ rows, onCopy }) => (jsx("div", { className: 'table-responsive', children: jsxs("table", { className: 'table table-sm table-bordered align-middle mb-0', style: {
5799
6066
  tableLayout: 'fixed',
5800
6067
  minWidth: 930
5801
- }, children: [jsx("caption", { className: 'visually-hidden', children: "Danh s\u00E1ch m\u1EABu key c\u1EA5u h\u00ECnh" }), jsx("thead", { children: jsxs("tr", { children: [jsx("th", { scope: 'col', style: { width: 360 }, children: "M\u1EABu key \u0111\u1EA7y \u0111\u1EE7" }), jsx("th", { scope: 'col', style: { width: 110 }, children: "Type" }), jsx("th", { scope: 'col', children: "V\u00ED d\u1EE5" })] }) }), jsx("tbody", { children: rows.map(row => (jsxs("tr", { children: [jsx("td", { children: jsx(TechnicalText, { children: row.pattern }) }), jsx("td", { children: row.type }), jsx("td", { children: row.examples.map(example => (jsxs("div", { className: 'config-key-sample-copy-row', children: [jsx(TechnicalText, { children: example }), jsx("button", { type: 'button', className: 'config-key-sample-copy-button', onClick: () => onCopy(example), "aria-label": `Sao chép key ${example}`, "data-tooltip-id": 'tooltip', "data-tooltip-content": 'Sao ch\u00E9p key', children: jsx(Copy, { fontSize: 15 }) })] }, example))) })] }, `${row.pattern}-${row.type}`))) })] }) }));
6068
+ }, children: [jsx("caption", { className: 'visually-hidden', children: "Danh s\u00E1ch m\u1EABu key c\u1EA5u h\u00ECnh" }), jsx("thead", { children: jsxs("tr", { children: [jsx("th", { scope: 'col', style: { width: 360 }, children: "M\u1EABu key \u0111\u1EA7y \u0111\u1EE7" }), jsx("th", { scope: 'col', style: { width: 110 }, children: "Type" }), jsx("th", { scope: 'col', children: "V\u00ED d\u1EE5" })] }) }), jsx("tbody", { children: rows.map(row => (jsxs("tr", { children: [jsx("td", { children: jsx(TechnicalText, { children: `${RESOURCE_CODE_PLACEHOLDER}.${row.pattern}` }) }), jsx("td", { children: row.type }), jsx("td", { children: row.examples.map(example => (jsxs("div", { className: 'config-key-sample-copy-row', children: [jsx(TechnicalText, { children: example }), jsx("button", { type: 'button', className: 'config-key-sample-copy-button', onClick: () => onCopy(`${RESOURCE_CODE_PLACEHOLDER}.${example}`), "aria-label": `Sao chép key ${RESOURCE_CODE_PLACEHOLDER}.${example}`, "data-tooltip-id": 'tooltip', "data-tooltip-content": 'Sao ch\u00E9p key', children: jsx(Copy, { fontSize: 15 }) })] }, example))) })] }, row.id))) })] }) }));
5802
6069
  const ConfigTreeRulesDrawer = ({ open, onClose }) => {
6070
+ const [activeTab, setActiveTab] = useState('samples');
5803
6071
  const [searchTerm, setSearchTerm] = useState('');
5804
6072
  const [categoryFilter, setCategoryFilter] = useState(null);
5805
6073
  const [typeFilter, setTypeFilter] = useState(null);
5806
6074
  useEffect(() => {
5807
6075
  if (!open) {
6076
+ setActiveTab('samples');
5808
6077
  setSearchTerm('');
5809
6078
  setCategoryFilter(null);
5810
6079
  setTypeFilter(null);
5811
6080
  }
5812
6081
  }, [open]);
5813
- const filteredSamples = useMemo(() => filterSamples(KEY_SAMPLES, searchTerm, categoryFilter?.value, typeFilter?.value), [
5814
- categoryFilter,
5815
- searchTerm,
5816
- typeFilter
5817
- ]);
5818
- const copyKey = async (value) => {
6082
+ const filteredSamples = useMemo(() => filterSettingKeyRules(SETTING_KEY_CONVENTION.rules, searchTerm, categoryFilter?.value, typeFilter?.value), [categoryFilter, searchTerm, typeFilter]);
6083
+ const importExampleText = useMemo(() => serializeSettingImportExample(RESOURCE_CODE_PLACEHOLDER), []);
6084
+ const copyText = async (value, successMessage) => {
5819
6085
  try {
5820
6086
  await navigator.clipboard.writeText(value);
5821
- notificationSuccess('Đã sao chép key');
6087
+ notificationSuccess(successMessage);
6088
+ }
6089
+ catch {
6090
+ notificationError('Không thể sao chép nội dung');
6091
+ }
6092
+ };
6093
+ const downloadImportExample = () => {
6094
+ try {
6095
+ const blob = new Blob([importExampleText], { type: 'application/json;charset=utf-8' });
6096
+ const url = URL.createObjectURL(blob);
6097
+ const link = document.createElement('a');
6098
+ link.href = url;
6099
+ link.download = 'becoxy-setting-resource-example.json';
6100
+ document.body.appendChild(link);
6101
+ link.click();
6102
+ link.remove();
6103
+ URL.revokeObjectURL(url);
6104
+ notificationSuccess('Đã tải JSON mẫu để import');
5822
6105
  }
5823
6106
  catch {
5824
- notificationError('Không thể sao chép key');
6107
+ notificationError('Không thể tải JSON mẫu');
5825
6108
  }
5826
6109
  };
5827
- return (jsx(Sidebar, { open: open, toggleSidebar: onClose, width: 1000, keyboard: true, hiddenFullScreenButton: true, zIndex: 1061, panelClassName: 'config-tree-rules-drawer', children: jsxs("div", { className: 'config-key-sample-layout h-100 bg-white d-flex flex-column', children: [jsx(ModalHeader, { typeModal: 'Setting', handleModal: onClose, title: 'M\u1EABu key c\u1EA5u h\u00ECnh' }), jsxs("div", { className: 'config-key-sample-toolbar border-bottom p-1 flex-shrink-0 d-flex align-items-center justify-content-start flex-wrap gap-1', children: [jsx(Input, { type: 'search', bsSize: 'sm', value: searchTerm, onChange: event => setSearchTerm(event.target.value), placeholder: 'T\u00ECm m\u1EABu key...', "aria-label": 'T\u00ECm m\u1EABu key', style: {
6110
+ return (jsx(Sidebar, { open: open, toggleSidebar: onClose, width: 1000, keyboard: true, hiddenFullScreenButton: true, zIndex: 1061, panelClassName: 'config-tree-rules-drawer', children: jsxs("div", { className: 'config-key-sample-layout h-100 bg-white d-flex flex-column', children: [jsx(ModalHeader, { typeModal: 'Setting', handleModal: onClose, title: 'M\u1EABu key c\u1EA5u h\u00ECnh' }), activeTab === 'samples' ? (jsxs("div", { className: 'config-key-sample-toolbar border-bottom p-1 flex-shrink-0 d-flex align-items-center justify-content-start flex-wrap gap-1', children: [jsx(Input, { type: 'search', bsSize: 'sm', value: searchTerm, onChange: event => setSearchTerm(event.target.value), placeholder: 'T\u00ECm m\u1EABu key...', "aria-label": 'T\u00ECm m\u1EABu key', style: {
5828
6111
  width: 300,
5829
6112
  maxWidth: '100%'
5830
- } }), jsx("div", { style: { width: 200 }, children: jsx(Select, { options: CATEGORY_OPTIONS, value: categoryFilter, onChange: (option) => setCategoryFilter(option), isClearable: true, menuPortalTarget: document.body, menuPosition: 'fixed', styles: SELECT_MENU_PORTAL_STYLES, placeholder: 'T\u1EA5t c\u1EA3 nh\u00F3m m\u1EABu', className: 'react-select w-100', classNamePrefix: 'select' }) }), jsx("div", { style: { width: 180 }, children: jsx(Select, { options: TYPE_OPTIONS, value: typeFilter, onChange: (option) => setTypeFilter(option), isClearable: true, menuPortalTarget: document.body, menuPosition: 'fixed', styles: SELECT_MENU_PORTAL_STYLES, placeholder: 'T\u1EA5t c\u1EA3 Type', className: 'react-select w-100', classNamePrefix: 'select' }) })] }), jsxs("div", { className: 'config-key-sample-content p-1 overflow-auto flex-grow-1', children: [filteredSamples.length > 0 ? (jsx(KeySampleTable, { rows: filteredSamples, onCopy: copyKey })) : null, filteredSamples.length === 0 ? (jsx("div", { className: 'config-key-sample-empty text-center py-2', children: "Kh\u00F4ng t\u00ECm th\u1EA5y m\u1EABu key." })) : null] })] }) }));
6113
+ } }), jsx("div", { style: { width: 200 }, children: jsx(Select, { options: SETTING_KEY_CONVENTION.categories, value: categoryFilter, onChange: (option) => setCategoryFilter(option), isClearable: true, menuPortalTarget: document.body, menuPosition: 'fixed', styles: SELECT_MENU_PORTAL_STYLES, placeholder: 'T\u1EA5t c\u1EA3 nh\u00F3m m\u1EABu', className: 'react-select w-100', classNamePrefix: 'select' }) }), jsx("div", { style: { width: 180 }, children: jsx(Select, { options: TYPE_OPTIONS, value: typeFilter, onChange: (option) => setTypeFilter(option), isClearable: true, menuPortalTarget: document.body, menuPosition: 'fixed', styles: SELECT_MENU_PORTAL_STYLES, placeholder: 'T\u1EA5t c\u1EA3 Type', className: 'react-select w-100', classNamePrefix: 'select' }) })] })) : null, jsxs("div", { className: 'config-key-sample-tabs border-bottom p-1 flex-shrink-0 d-flex align-items-center justify-content-between flex-wrap gap-1', children: [jsxs("div", { className: 'd-flex align-items-center gap-50', role: 'tablist', "aria-label": 'N\u1ED9i dung quy t\u1EAFc Setting', children: [jsx(Button, { type: 'button', size: 'sm', color: 'primary', outline: activeTab !== 'samples', role: 'tab', "aria-selected": activeTab === 'samples', onClick: () => setActiveTab('samples'), children: "M\u1EABu d\u1EA1ng key" }), jsx(Button, { type: 'button', size: 'sm', color: 'primary', outline: activeTab !== 'import-example', role: 'tab', "aria-selected": activeTab === 'import-example', onClick: () => setActiveTab('import-example'), children: "JSON m\u1EABu ho\u00E0n ch\u1EC9nh" })] }), jsx("div", { className: 'd-flex align-items-center gap-50', children: activeTab === 'import-example' ? (jsxs(Fragment, { children: [jsxs(Button, { type: 'button', size: 'sm', color: 'secondary', outline: true, onClick: () => {
6114
+ void copyText(importExampleText, 'Đã sao chép JSON mẫu');
6115
+ }, children: [jsx(Copy, { fontSize: 15, className: 'me-50' }), "Copy JSON m\u1EABu"] }), jsxs(Button, { type: 'button', size: 'sm', color: 'secondary', outline: true, onClick: downloadImportExample, children: [jsx(Download, { fontSize: 15, className: 'me-50' }), "T\u1EA3i JSON m\u1EABu"] })] })) : null })] }), jsx("div", { className: 'config-key-sample-content p-1 overflow-auto flex-grow-1', role: 'tabpanel', children: activeTab === 'samples' ? (jsxs(Fragment, { children: [filteredSamples.length > 0 ? (jsx(KeySampleTable, { rows: filteredSamples, onCopy: value => {
6116
+ void copyText(value, 'Đã sao chép key');
6117
+ } })) : null, filteredSamples.length === 0 ? (jsx("div", { className: 'config-key-sample-empty text-center py-2', children: "Kh\u00F4ng t\u00ECm th\u1EA5y m\u1EABu key." })) : null] })) : (jsxs(Fragment, { children: [jsxs("div", { className: 'alert alert-warning py-50 px-1 mb-1', children: ["Thay ", jsx("strong", { children: "RESOURCE_CODE" }), " b\u1EB1ng m\u00E3 Resource th\u1EF1c t\u1EBF tr\u01B0\u1EDBc khi import."] }), jsx("pre", { className: 'config-key-convention-text mb-0', children: importExampleText })] })) })] }) }));
5831
6118
  };
5832
6119
 
5833
6120
  const ConfigTreeGridFooter = ({ rows }) => {