dinocollab-core 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/README.md +54 -0
- package/dist/_virtual/_rollupPluginBabelHelpers.js +431 -0
- package/dist/_virtual/_rollupPluginBabelHelpers.js.map +1 -0
- package/dist/assets/vector-404265a04f4f9c8be1f.webp +0 -0
- package/dist/node_modules/.pnpm/@rollup_plugin-typescript@1_d0d2002d9033600b6738d939bd598bc6/node_modules/tslib/tslib.es6.js +46 -0
- package/dist/node_modules/.pnpm/@rollup_plugin-typescript@1_d0d2002d9033600b6738d939bd598bc6/node_modules/tslib/tslib.es6.js.map +1 -0
- package/dist/src/api-context/alert-global.js +151 -0
- package/dist/src/api-context/alert-global.js.map +1 -0
- package/dist/src/api-context/drawer-global.js +105 -0
- package/dist/src/api-context/drawer-global.js.map +1 -0
- package/dist/src/api-context/global-modal.js +87 -0
- package/dist/src/api-context/global-modal.js.map +1 -0
- package/dist/src/api-context/popover-global.js +102 -0
- package/dist/src/api-context/popover-global.js.map +1 -0
- package/dist/src/api-context/popover.js +86 -0
- package/dist/src/api-context/popover.js.map +1 -0
- package/dist/src/api-context/ui.units.js +21 -0
- package/dist/src/api-context/ui.units.js.map +1 -0
- package/dist/src/components/copy-to-clipboard.js +105 -0
- package/dist/src/components/copy-to-clipboard.js.map +1 -0
- package/dist/src/components/custom.breadcrumbs.js +61 -0
- package/dist/src/components/custom.breadcrumbs.js.map +1 -0
- package/dist/src/components/help-tooltip.js +91 -0
- package/dist/src/components/help-tooltip.js.map +1 -0
- package/dist/src/components/image-with-fallback.js +48 -0
- package/dist/src/components/image-with-fallback.js.map +1 -0
- package/dist/src/components/text-editor.js +117 -0
- package/dist/src/components/text-editor.js.map +1 -0
- package/dist/src/form/create.autocomplete.chips.js +218 -0
- package/dist/src/form/create.autocomplete.chips.js.map +1 -0
- package/dist/src/form/create.date-expired.js +201 -0
- package/dist/src/form/create.date-expired.js.map +1 -0
- package/dist/src/form/create.date-picker.js +125 -0
- package/dist/src/form/create.date-picker.js.map +1 -0
- package/dist/src/form/create.form-base.js +135 -0
- package/dist/src/form/create.form-base.js.map +1 -0
- package/dist/src/form/create.form-comfirm.js +119 -0
- package/dist/src/form/create.form-comfirm.js.map +1 -0
- package/dist/src/form/create.form-grid-layout.js +177 -0
- package/dist/src/form/create.form-grid-layout.js.map +1 -0
- package/dist/src/form/create.form-grid-layout.units.js +39 -0
- package/dist/src/form/create.form-grid-layout.units.js.map +1 -0
- package/dist/src/form/create.input-base.js +260 -0
- package/dist/src/form/create.input-base.js.map +1 -0
- package/dist/src/form/create.input.file.js +74 -0
- package/dist/src/form/create.input.file.js.map +1 -0
- package/dist/src/form/create.select-simple.js +104 -0
- package/dist/src/form/create.select-simple.js.map +1 -0
- package/dist/src/form/create.select-with-api.js +271 -0
- package/dist/src/form/create.select-with-api.js.map +1 -0
- package/dist/src/form/create.text-editor.js +156 -0
- package/dist/src/form/create.text-editor.js.map +1 -0
- package/dist/src/form/dino-form.js +42 -0
- package/dist/src/form/dino-form.js.map +1 -0
- package/dist/src/form/helper.js +157 -0
- package/dist/src/form/helper.js.map +1 -0
- package/dist/src/form/modal-wrapper.js +75 -0
- package/dist/src/form/modal-wrapper.js.map +1 -0
- package/dist/src/form/validator.js +186 -0
- package/dist/src/form/validator.js.map +1 -0
- package/dist/src/hooks/index.js +48 -0
- package/dist/src/hooks/index.js.map +1 -0
- package/dist/src/index.js +26 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/redux/create.hoc-lazy.js +67 -0
- package/dist/src/redux/create.hoc-lazy.js.map +1 -0
- package/dist/src/redux/dino.js +11 -0
- package/dist/src/redux/dino.js.map +1 -0
- package/dist/src/redux/types.js +9 -0
- package/dist/src/redux/types.js.map +1 -0
- package/dist/src/redux/ui.error-page.js +80 -0
- package/dist/src/redux/ui.error-page.js.map +1 -0
- package/dist/src/redux/vector-404.webp.js +4 -0
- package/dist/src/redux/vector-404.webp.js.map +1 -0
- package/dist/src/table/context.js +12 -0
- package/dist/src/table/context.js.map +1 -0
- package/dist/src/table/create.action-row.js +135 -0
- package/dist/src/table/create.action-row.js.map +1 -0
- package/dist/src/table/create.status-cell.js +49 -0
- package/dist/src/table/create.status-cell.js.map +1 -0
- package/dist/src/table/create.table.js +233 -0
- package/dist/src/table/create.table.js.map +1 -0
- package/dist/src/table/custom.filter-operators.js +89 -0
- package/dist/src/table/custom.filter-operators.js.map +1 -0
- package/dist/src/table/dino.js +129 -0
- package/dist/src/table/dino.js.map +1 -0
- package/dist/src/table/helpers.js +116 -0
- package/dist/src/table/helpers.js.map +1 -0
- package/dist/src/table/model-filter.js +23 -0
- package/dist/src/table/model-filter.js.map +1 -0
- package/dist/src/table/toolbar-pannel.js +134 -0
- package/dist/src/table/toolbar-pannel.js.map +1 -0
- package/dist/src/table/ui.buttons.js +60 -0
- package/dist/src/table/ui.buttons.js.map +1 -0
- package/dist/src/table/ui.units.js +201 -0
- package/dist/src/table/ui.units.js.map +1 -0
- package/dist/src/utils/dayjs-config.js +12 -0
- package/dist/src/utils/dayjs-config.js.map +1 -0
- package/dist/src/utils/helpers.js +197 -0
- package/dist/src/utils/helpers.js.map +1 -0
- package/dist/src/utils/json-object.js +38 -0
- package/dist/src/utils/json-object.js.map +1 -0
- package/dist/src/utils/query-param.js +172 -0
- package/dist/src/utils/query-param.js.map +1 -0
- package/package.json +52 -0
- package/rollup.config.js +39 -0
- package/src/@types/global.d.ts +5 -0
- package/src/api-context/alert-global.tsx +174 -0
- package/src/api-context/drawer-global.tsx +116 -0
- package/src/api-context/global-modal.tsx +109 -0
- package/src/api-context/index.ts +13 -0
- package/src/api-context/popover-global.tsx +107 -0
- package/src/api-context/popover.tsx +89 -0
- package/src/api-context/ui.units.tsx +10 -0
- package/src/components/copy-to-clipboard.tsx +86 -0
- package/src/components/custom.breadcrumbs.tsx +67 -0
- package/src/components/help-tooltip.tsx +75 -0
- package/src/components/image-with-fallback.tsx +51 -0
- package/src/components/index.tsx +1 -0
- package/src/components/input-debounce-timer.tsx +138 -0
- package/src/components/loading-buttons.tsx +35 -0
- package/src/components/text-editor.preview.tsx +30 -0
- package/src/components/text-editor.tsx +125 -0
- package/src/form/README.md +55 -0
- package/src/form/create.autocomplete.chips.tsx +199 -0
- package/src/form/create.date-expired.tsx +195 -0
- package/src/form/create.date-picker.tsx +122 -0
- package/src/form/create.form-base.tsx +102 -0
- package/src/form/create.form-comfirm.tsx +83 -0
- package/src/form/create.form-grid-layout.tsx +170 -0
- package/src/form/create.form-grid-layout.units.tsx +37 -0
- package/src/form/create.input-base.tsx +222 -0
- package/src/form/create.input.file.tsx +76 -0
- package/src/form/create.select-simple.tsx +101 -0
- package/src/form/create.select-with-api.tsx +213 -0
- package/src/form/create.text-editor.tsx +161 -0
- package/src/form/dino-form.tsx +40 -0
- package/src/form/helper.ts +132 -0
- package/src/form/index.ts +12 -0
- package/src/form/modal-wrapper.tsx +75 -0
- package/src/form/types.ts +16 -0
- package/src/form/validator.ts +202 -0
- package/src/hooks/index.ts +44 -0
- package/src/index.ts +7 -0
- package/src/lab/create.autocomplete.simple.tsx +57 -0
- package/src/lab/create.dino-store.ts +59 -0
- package/src/lab/create.multi-select-dropdown.tsx +189 -0
- package/src/lab/create.select-mul-with-api/index.tsx +271 -0
- package/src/lab/create.select-mul-with-api/table-custom.tsx +194 -0
- package/src/lab/create.select-mul-with-api/types.ts +26 -0
- package/src/lab/create.select-mul-with-api/ui.units.tsx +163 -0
- package/src/lab/filter-bar/base.tsx +162 -0
- package/src/lab/filter-bar/create.filter-bar.tsx +190 -0
- package/src/lab/filter-bar/create.filter-menu.tsx +156 -0
- package/src/lab/filter-bar/create.filter-panel.tsx +95 -0
- package/src/lab/filter-bar/create.filtered.tsx +41 -0
- package/src/lab/filter-bar/create.sort-menu.tsx +43 -0
- package/src/lab/filter-bar/demo.tsx +50 -0
- package/src/lab/filter-bar/index.ts +6 -0
- package/src/lab/filter-bar/types.ts +105 -0
- package/src/lab/filter-bar/ui.units.tsx +70 -0
- package/src/lab/grafana-dashboard/configs.ts +43 -0
- package/src/lab/grafana-dashboard/date-time-range/absolute-time-rage.tsx +137 -0
- package/src/lab/grafana-dashboard/date-time-range/helpers.ts +126 -0
- package/src/lab/grafana-dashboard/date-time-range/index.tsx +62 -0
- package/src/lab/grafana-dashboard/date-time-range/menu-wrap.tsx +101 -0
- package/src/lab/grafana-dashboard/date-time-range/quick-ranges.tsx +161 -0
- package/src/lab/grafana-dashboard/date-time-range/types.ts +9 -0
- package/src/lab/grafana-dashboard/date-time-range/units.tsx +18 -0
- package/src/lab/grafana-dashboard/helper.ts +25 -0
- package/src/lab/grafana-dashboard/hooks.tsx +79 -0
- package/src/lab/grafana-dashboard/icons.tsx +67 -0
- package/src/lab/grafana-dashboard/index.tsx +120 -0
- package/src/lab/grafana-dashboard/top-bar.tsx +62 -0
- package/src/lab/grafana-dashboard/top-bar.types.ts +5 -0
- package/src/lab/grafana-dashboard/types.ts +8 -0
- package/src/lab/media-player.core1.tsx +273 -0
- package/src/lab/media-player.muted.tsx +62 -0
- package/src/lab/media-player.units.ts +80 -0
- package/src/lab/table-grid/create.table-grid.tsx +183 -0
- package/src/lab/table-grid/demo.tsx +53 -0
- package/src/lab/table-grid/dino.tsx +8 -0
- package/src/lab/table-grid/helpers.tsx +11 -0
- package/src/lab/table-grid/index.ts +3 -0
- package/src/lab/table-grid/item-actions.tsx +138 -0
- package/src/lab/table-grid/toolbar-pannel.tsx +98 -0
- package/src/lab/table-grid/types.ts +68 -0
- package/src/redux/create.hoc-lazy.tsx +80 -0
- package/src/redux/dino.ts +9 -0
- package/src/redux/index.ts +6 -0
- package/src/redux/types.ts +27 -0
- package/src/redux/ui.error-page.tsx +62 -0
- package/src/redux/ui.units.tsx +41 -0
- package/src/redux/vector-404.webp +0 -0
- package/src/table/context.tsx +16 -0
- package/src/table/create.action-row.tsx +91 -0
- package/src/table/create.status-cell.tsx +51 -0
- package/src/table/create.table.tsx +239 -0
- package/src/table/custom.filter-operators.ts +94 -0
- package/src/table/dino.tsx +120 -0
- package/src/table/helpers.ts +94 -0
- package/src/table/index.ts +13 -0
- package/src/table/model-filter.ts +43 -0
- package/src/table/toolbar-pannel.tsx +106 -0
- package/src/table/types.ts +50 -0
- package/src/table/ui.buttons.tsx +54 -0
- package/src/table/ui.units.tsx +189 -0
- package/src/utils/dayjs-config.ts +13 -0
- package/src/utils/helpers.ts +171 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/json-object.ts +29 -0
- package/src/utils/mfe-events.tsx +34 -0
- package/src/utils/query-param.ts +129 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import React, { FC, useCallback, useEffect, useRef } from 'react'
|
|
2
|
+
import { TextField, InputAdornment, IconButton, Fade, Tooltip, styled, Divider, Box } from '@mui/material'
|
|
3
|
+
import SearchIcon from '@mui/icons-material/Search'
|
|
4
|
+
import CancelIcon from '@mui/icons-material/Cancel'
|
|
5
|
+
|
|
6
|
+
interface IProps {
|
|
7
|
+
value?: string
|
|
8
|
+
onChange?: (value: string) => void
|
|
9
|
+
placeholder?: string
|
|
10
|
+
disabledTimer?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const InputDebounceTimer: FC<IProps> = (props) => {
|
|
14
|
+
const refInput = useRef<HTMLInputElement>(null)
|
|
15
|
+
const handleClear = () => {
|
|
16
|
+
props.onChange && props.onChange('')
|
|
17
|
+
if (refInput.current) refInput.current.value = ''
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const timer = useRef({ timeout: 700, store: 0 })
|
|
21
|
+
const handleChangeCallback = useCallback(
|
|
22
|
+
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
23
|
+
const value = e.target.value
|
|
24
|
+
clearTimeout(timer.current.store)
|
|
25
|
+
if (props?.disabledTimer === true) return
|
|
26
|
+
timer.current.store = setTimeout(() => {
|
|
27
|
+
props.onChange && props.onChange(value)
|
|
28
|
+
}, timer.current.timeout) as any
|
|
29
|
+
},
|
|
30
|
+
[props]
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
const handleKeyDown = useCallback(
|
|
34
|
+
(event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
35
|
+
if (event.key === 'Enter') {
|
|
36
|
+
clearTimeout(timer.current.store)
|
|
37
|
+
const value = refInput.current?.value ?? ''
|
|
38
|
+
props.onChange && props.onChange(value)
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
[props]
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const handleClickSearch = useCallback(() => {
|
|
45
|
+
clearTimeout(timer.current.store)
|
|
46
|
+
const value = refInput.current?.value ?? ''
|
|
47
|
+
props.onChange && props.onChange(value)
|
|
48
|
+
}, [props])
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (refInput.current) refInput.current.value = props.value ?? ''
|
|
52
|
+
return () => {}
|
|
53
|
+
}, [props.value])
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<WrapTextField className='theme-white'>
|
|
57
|
+
<TextField
|
|
58
|
+
fullWidth
|
|
59
|
+
variant='outlined'
|
|
60
|
+
placeholder={props.placeholder}
|
|
61
|
+
size='small'
|
|
62
|
+
defaultValue={props.value}
|
|
63
|
+
onChange={handleChangeCallback}
|
|
64
|
+
inputRef={refInput}
|
|
65
|
+
onKeyDown={handleKeyDown}
|
|
66
|
+
InputProps={{
|
|
67
|
+
startAdornment: (
|
|
68
|
+
<InputAdornment position='start'>
|
|
69
|
+
<IconButton type='submit' onClick={handleClickSearch}>
|
|
70
|
+
<SearchIcon />
|
|
71
|
+
</IconButton>
|
|
72
|
+
<Divider flexItem orientation='vertical' sx={{ height: '24px', transform: 'translateY(-50%)', mx: '3px' }} />
|
|
73
|
+
</InputAdornment>
|
|
74
|
+
),
|
|
75
|
+
endAdornment: (
|
|
76
|
+
<InputAdornment position='end'>
|
|
77
|
+
<Fade in={!!props.value}>
|
|
78
|
+
<Tooltip title='Clear'>
|
|
79
|
+
<IconButton onClick={handleClear}>
|
|
80
|
+
<CancelIcon fontSize='small' />
|
|
81
|
+
</IconButton>
|
|
82
|
+
</Tooltip>
|
|
83
|
+
</Fade>
|
|
84
|
+
</InputAdornment>
|
|
85
|
+
)
|
|
86
|
+
}}
|
|
87
|
+
/>
|
|
88
|
+
</WrapTextField>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export default InputDebounceTimer
|
|
93
|
+
|
|
94
|
+
const WrapTextField = styled(Box)<{ className?: string }>(({ className }) => {
|
|
95
|
+
const isWhite = className?.includes('theme-white')
|
|
96
|
+
return {
|
|
97
|
+
flex: 1,
|
|
98
|
+
'--input-search-text-color': isWhite ? '#000' : '#ffffff',
|
|
99
|
+
'--input-search-label-color': isWhite ? '#404040' : '#b0b0b0',
|
|
100
|
+
'--input-search-bg-color': isWhite ? '#f6f6f6' : 'rgba(255, 255, 255, 0.1)',
|
|
101
|
+
'--color-primary': '#42a5f5',
|
|
102
|
+
|
|
103
|
+
'& .MuiOutlinedInput-root': {
|
|
104
|
+
color: 'var(--input-search-text-color)',
|
|
105
|
+
backgroundColor: 'var(--input-search-bg-color)',
|
|
106
|
+
borderRadius: 8,
|
|
107
|
+
'& fieldset': {
|
|
108
|
+
borderColor: 'transparent'
|
|
109
|
+
},
|
|
110
|
+
'&:hover fieldset': {
|
|
111
|
+
borderColor: 'rgba(66, 165, 245, 0.7)' // $color-primary 70%
|
|
112
|
+
},
|
|
113
|
+
'&.Mui-focused fieldset': {
|
|
114
|
+
borderColor: 'var(--color-primary)'
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
'& .MuiInputBase-input': {
|
|
119
|
+
color: 'var(--input-search-text-color)'
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
'& .MuiInputLabel-root': {
|
|
123
|
+
color: 'var(--input-search-label-color)'
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
'& .MuiInputLabel-root.Mui-focused': {
|
|
127
|
+
color: '#90caf9'
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
'& .MuiInputAdornment-root': {
|
|
131
|
+
color: 'var(--input-search-text-color)'
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
'& .MuiIconButton-root': {
|
|
135
|
+
color: 'var(--input-search-text-color)'
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React, { FC } from 'react'
|
|
2
|
+
import { Button, ButtonProps, CircularProgress, Fade, styled } from '@mui/material'
|
|
3
|
+
|
|
4
|
+
export interface LoadingButtonProps extends ButtonProps {
|
|
5
|
+
loading?: boolean
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const LoadingButton: FC<LoadingButtonProps> = ({ loading, children, ...props }) => {
|
|
9
|
+
return (
|
|
10
|
+
<ButtonCustom {...props} disabled={props.disabled || loading}>
|
|
11
|
+
<Fade in={!loading}>
|
|
12
|
+
<span>{children}</span>
|
|
13
|
+
</Fade>
|
|
14
|
+
<Fade in={loading}>
|
|
15
|
+
<div className='LoadingButton-progress'>
|
|
16
|
+
<CircularProgress size={24} />
|
|
17
|
+
</div>
|
|
18
|
+
</Fade>
|
|
19
|
+
</ButtonCustom>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const ButtonCustom = styled(Button)({
|
|
24
|
+
position: 'relative',
|
|
25
|
+
[`.LoadingButton-progress`]: {
|
|
26
|
+
position: 'absolute',
|
|
27
|
+
top: 0,
|
|
28
|
+
left: 0,
|
|
29
|
+
width: '100%',
|
|
30
|
+
height: '100%',
|
|
31
|
+
display: 'flex',
|
|
32
|
+
alignItems: 'center',
|
|
33
|
+
justifyContent: 'center'
|
|
34
|
+
}
|
|
35
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React, { FC } from 'react'
|
|
2
|
+
|
|
3
|
+
const TextEditorPreview: FC<{ children: string }> = (props) => {
|
|
4
|
+
const contentRef = React.useRef<HTMLDivElement | null>(null)
|
|
5
|
+
|
|
6
|
+
React.useEffect(() => {
|
|
7
|
+
// Lấy phần tử template
|
|
8
|
+
const template = document.createElement('template')
|
|
9
|
+
template.innerHTML = `
|
|
10
|
+
<link rel="stylesheet" href="css/quill.snow.css">
|
|
11
|
+
<div class='ql-editor'>${props.children}</div>
|
|
12
|
+
`
|
|
13
|
+
|
|
14
|
+
// Tạo shadow DOM và chèn nội dung từ template
|
|
15
|
+
if (contentRef.current) {
|
|
16
|
+
const shadowRoot = contentRef.current.shadowRoot || contentRef.current.attachShadow({ mode: 'open' })
|
|
17
|
+
|
|
18
|
+
shadowRoot.innerHTML = '' // Reset nội dung cũ
|
|
19
|
+
shadowRoot.appendChild(template.content.cloneNode(true))
|
|
20
|
+
}
|
|
21
|
+
}, [props.children])
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
<div ref={contentRef}></div>
|
|
26
|
+
</>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default TextEditorPreview
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// /* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import React, { Component } from 'react'
|
|
3
|
+
import { Box, styled, SxProps, Theme } from '@mui/material'
|
|
4
|
+
/**
|
|
5
|
+
* yarn add react-quill@2.0.0
|
|
6
|
+
*/
|
|
7
|
+
import ReactQuill from 'react-quill'
|
|
8
|
+
import 'react-quill/dist/quill.snow.css'
|
|
9
|
+
|
|
10
|
+
export const textEditorClasses = {
|
|
11
|
+
root: 'DinoTextEditor-root',
|
|
12
|
+
fullscreen: 'DinoTextEditor-fullscreen'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const EditorToolbar: any = {}
|
|
16
|
+
|
|
17
|
+
EditorToolbar.formats = [
|
|
18
|
+
'header',
|
|
19
|
+
'bold',
|
|
20
|
+
'italic',
|
|
21
|
+
'underline',
|
|
22
|
+
'strike',
|
|
23
|
+
'blockquote',
|
|
24
|
+
'list',
|
|
25
|
+
'bullet',
|
|
26
|
+
'indent',
|
|
27
|
+
'link',
|
|
28
|
+
'image',
|
|
29
|
+
'align',
|
|
30
|
+
'color',
|
|
31
|
+
'background'
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
EditorToolbar.modules = {
|
|
35
|
+
toolbar: [
|
|
36
|
+
[{ header: [1, 2, 3, 4, 5, 6, false] }],
|
|
37
|
+
['bold', 'italic', 'underline', 'strike', 'blockquote'],
|
|
38
|
+
[{ align: '' }, { align: 'center' }, { align: 'right' }, { align: 'justify' }],
|
|
39
|
+
[{ list: 'ordered' }, { list: 'bullet' }, { indent: '-1' }, { indent: '+1' }],
|
|
40
|
+
[{ color: [] }, { background: [] }],
|
|
41
|
+
['link', 'image'],
|
|
42
|
+
['clean']
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface IProps {
|
|
47
|
+
id?: string
|
|
48
|
+
name: string
|
|
49
|
+
sx?: SxProps<Theme>
|
|
50
|
+
readOnly?: boolean
|
|
51
|
+
defautValue?: string
|
|
52
|
+
error?: boolean
|
|
53
|
+
onBlur?: () => void
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface IState {
|
|
57
|
+
value: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
class TextEditor extends Component<IProps, IState> {
|
|
61
|
+
constructor(props: IProps) {
|
|
62
|
+
super(props)
|
|
63
|
+
this.state = { value: props.defautValue ?? '' }
|
|
64
|
+
}
|
|
65
|
+
handleChange = (value: string) => {
|
|
66
|
+
let str = value
|
|
67
|
+
if (value === '<p><br></p>') str = ''
|
|
68
|
+
this.setState({ value: str })
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
id = 0
|
|
72
|
+
render() {
|
|
73
|
+
const classes = [textEditorClasses.root]
|
|
74
|
+
if (this.props.error) classes.push('error')
|
|
75
|
+
return (
|
|
76
|
+
<Wrapper id={this.props.id} sx={this.props.sx} className={classes.join(' ')}>
|
|
77
|
+
{!this.props.readOnly && <input key={++this.id} name={this.props.name} defaultValue={this.state.value} hidden />}
|
|
78
|
+
<ReactQuill
|
|
79
|
+
readOnly={this.props.readOnly}
|
|
80
|
+
placeholder='Enter the message'
|
|
81
|
+
value={this.state.value}
|
|
82
|
+
modules={EditorToolbar.modules}
|
|
83
|
+
formats={EditorToolbar.formats}
|
|
84
|
+
onChange={this.handleChange}
|
|
85
|
+
onBlur={this.props.onBlur}
|
|
86
|
+
/>
|
|
87
|
+
</Wrapper>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export default TextEditor
|
|
93
|
+
|
|
94
|
+
export { default as TextEditorPreview } from './text-editor.preview'
|
|
95
|
+
|
|
96
|
+
const borderColor = '#d32f2f'
|
|
97
|
+
|
|
98
|
+
const Wrapper = styled(Box)({
|
|
99
|
+
margin: 0,
|
|
100
|
+
minHeight: '450px',
|
|
101
|
+
display: 'flex',
|
|
102
|
+
'& .quill': { flex: 1 },
|
|
103
|
+
'& .ql-container': {
|
|
104
|
+
height: 'calc(100% - 42px)',
|
|
105
|
+
borderBottomLeftRadius: '6px',
|
|
106
|
+
borderBottomRightRadius: '6px'
|
|
107
|
+
},
|
|
108
|
+
'& .ql-toolbar': {
|
|
109
|
+
borderTopLeftRadius: '6px',
|
|
110
|
+
borderTopRightRadius: '6px'
|
|
111
|
+
},
|
|
112
|
+
'&.error .ql-toolbar': {
|
|
113
|
+
borderTopColor: borderColor,
|
|
114
|
+
borderLeftColor: borderColor,
|
|
115
|
+
borderRightColor: borderColor
|
|
116
|
+
},
|
|
117
|
+
'&.error .ql-container': {
|
|
118
|
+
borderBottomColor: borderColor,
|
|
119
|
+
borderLeftColor: borderColor,
|
|
120
|
+
borderRightColor: borderColor
|
|
121
|
+
},
|
|
122
|
+
'&.error .ql-editor.ql-blank::before': {
|
|
123
|
+
color: borderColor
|
|
124
|
+
}
|
|
125
|
+
})
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
## 🧩 Cấu trúc tổng quát
|
|
2
|
+
|
|
3
|
+
`DinoForm` gồm hai phần chính: các thành phần nền tảng (Base) và các thành phần nhập liệu (Inputs).
|
|
4
|
+
|
|
5
|
+
### 1. Base (Nền tảng)
|
|
6
|
+
|
|
7
|
+
| Tên | Kiểu | Mô tả |
|
|
8
|
+
| ---------------------- | ------------------------- | --------------------------------------------------------------- |
|
|
9
|
+
| `ModalWrap` | `React Component` | Lớp bộc khi dùng chung với GlobalModal, Form từ Table. |
|
|
10
|
+
| `BottomBarWrap` | `React Component` | Component thay thế cho BottomBar của FormBase. |
|
|
11
|
+
| `FormValidator` | `class` | Lớp xác thực, cung cấp phương thức tạo schema kiểm tra dữ liệu. |
|
|
12
|
+
| `validator` | `object` | Tạo nhanh FormValidator |
|
|
13
|
+
| `createFormBase` | `React Component Creator` | Tạo form cơ bản |
|
|
14
|
+
| `createFormComfirm` | `React Component Creator` | Tạo hộp thoại xác nhận khi gửi hoặc huỷ biểu mẫu. |
|
|
15
|
+
| `createFormGridLayout` | `React Component Creator` | Tạo layout dạng lưới để tổ chức các input trong form. |
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
### 2. Inputs (Thành phần nhập liệu)
|
|
20
|
+
|
|
21
|
+
| Tên | Kiểu | Mô tả |
|
|
22
|
+
| ------------------------ | ------------------------- | ------------------------------------------------------------ |
|
|
23
|
+
| `createInput` | `React Component Creator` | Input text cơ bản, có thể dùng cho tên, tiêu đề, v.v. |
|
|
24
|
+
| `createInputFile` | `React Component Creator` | Input cho phép người dùng chọn và tải lên file. |
|
|
25
|
+
| `createDatePicker` | `React Component Creator` | Chọn ngày bằng lịch, tương thích với dayjs. |
|
|
26
|
+
| `createAutocompleteChip` | `React Component Creator` | Tạo autocomplete cho phép chọn nhiều giá trị dưới dạng chip. |
|
|
27
|
+
| `createSelectSimple` | `React Component Creator` | Select tĩnh với danh sách tùy chọn truyền trực tiếp. |
|
|
28
|
+
| `createSelectWithApi` | `React Component Creator` | Select động, lấy dữ liệu từ API. |
|
|
29
|
+
| `createTextEditor` | `React Component Creator` | Trình soạn thảo văn bản phong phú (rich text editor). |
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 🛠️ Cách sử dụng
|
|
34
|
+
|
|
35
|
+
### Tạo form cơ bản `createFormBase`
|
|
36
|
+
|
|
37
|
+
### Tạo form dạng lưới `createFormGridLayout`
|
|
38
|
+
|
|
39
|
+
Tạo layout dạng lưới để tổ chức các input trong form.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
export interface IDataModel {
|
|
43
|
+
Id: string
|
|
44
|
+
TextValue: string
|
|
45
|
+
DateTimeValue: string
|
|
46
|
+
FileValue: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const FormInstance = DinoForm.createFormGridLayout<IDataModel>({
|
|
50
|
+
configs: [{ key: 'Id', label: 'Id', placeholder: 'Id' }],
|
|
51
|
+
validate: DinoForm.validator({
|
|
52
|
+
Id: { Rules: [{ rule: SingleRuleValidate.Required }] }
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
```
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import React, { Component } from 'react'
|
|
2
|
+
import { Autocomplete, Chip, IconButton, styled, TextField, TextFieldProps, Tooltip, Typography } from '@mui/material'
|
|
3
|
+
import RemoveIcon from '@mui/icons-material/Remove'
|
|
4
|
+
import { IFormInputBase } from './types'
|
|
5
|
+
import { getErrorMessage } from './helper'
|
|
6
|
+
|
|
7
|
+
interface IOptions {
|
|
8
|
+
optionItems?: string[]
|
|
9
|
+
limitTags?: number
|
|
10
|
+
separator?: string
|
|
11
|
+
saveLocalStorageByKey?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface IProps<T> extends IFormInputBase<T, IOptions> {}
|
|
15
|
+
|
|
16
|
+
interface IState {
|
|
17
|
+
value: string[]
|
|
18
|
+
options: string[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default function CreateAutocompleteChip<T>(options?: IOptions) {
|
|
22
|
+
const separator = options?.separator || '|'
|
|
23
|
+
const limitTags = options?.limitTags || 2
|
|
24
|
+
const storeItems = options?.saveLocalStorageByKey ? AutocompleteChipStore.initial(options.saveLocalStorageByKey) : undefined
|
|
25
|
+
|
|
26
|
+
return class AutocompleteChip extends Component<IProps<T>, IState> {
|
|
27
|
+
constructor(props: IProps<T>) {
|
|
28
|
+
super(props)
|
|
29
|
+
this.state = {
|
|
30
|
+
value: this.getDefaultValue(),
|
|
31
|
+
options: this.getSelectOptions()
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
refInput: HTMLInputElement | null = null
|
|
36
|
+
render() {
|
|
37
|
+
const { name } = this.props
|
|
38
|
+
return (
|
|
39
|
+
<>
|
|
40
|
+
<Autocomplete
|
|
41
|
+
multiple
|
|
42
|
+
freeSolo
|
|
43
|
+
limitTags={limitTags}
|
|
44
|
+
value={this.state.value}
|
|
45
|
+
onChange={this.handleAddOption}
|
|
46
|
+
options={this.state.options}
|
|
47
|
+
renderOption={(props, option) => (
|
|
48
|
+
<ItemOption {...props} key={option}>
|
|
49
|
+
<Typography variant='subtitle1' sx={{ flex: 1 }}>
|
|
50
|
+
{option}
|
|
51
|
+
</Typography>
|
|
52
|
+
{this.isBtnRemoveOption(option) && (
|
|
53
|
+
<Tooltip title='Remove option' arrow placement='left'>
|
|
54
|
+
<IconButton size='small' onClick={(e) => this.handleRemoveItem(e, option)}>
|
|
55
|
+
<RemoveIcon fontSize='small' />
|
|
56
|
+
</IconButton>
|
|
57
|
+
</Tooltip>
|
|
58
|
+
)}
|
|
59
|
+
</ItemOption>
|
|
60
|
+
)}
|
|
61
|
+
renderTags={(tagValue, getTagProps) => {
|
|
62
|
+
return tagValue.map((option, index) => <Chip label={option} {...getTagProps({ index })} key={option.toString()} />)
|
|
63
|
+
}}
|
|
64
|
+
renderInput={(params) => <TextField {...params} variant='outlined' {...this.getTextFieldProps()} />}
|
|
65
|
+
/>
|
|
66
|
+
<input
|
|
67
|
+
ref={(ref) => {
|
|
68
|
+
this.refInput = ref
|
|
69
|
+
}}
|
|
70
|
+
hidden
|
|
71
|
+
type='text'
|
|
72
|
+
name={name?.toString()}
|
|
73
|
+
defaultValue={this.state.value.join(separator)}
|
|
74
|
+
/>
|
|
75
|
+
</>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
handleRemoveItem = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>, value: string) => {
|
|
80
|
+
e.preventDefault()
|
|
81
|
+
e.stopPropagation()
|
|
82
|
+
console.log(value)
|
|
83
|
+
const list = storeItems?.delete(value) ?? []
|
|
84
|
+
this.setState({ options: list })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
handleAddOption = (_: React.SyntheticEvent, newValue: string[]) => {
|
|
88
|
+
if (options?.saveLocalStorageByKey) {
|
|
89
|
+
const options = storeItems?.setItems(this.state.options, newValue) ?? this.state.options
|
|
90
|
+
this.setState({ value: newValue, options })
|
|
91
|
+
} else {
|
|
92
|
+
this.setState({ value: newValue })
|
|
93
|
+
}
|
|
94
|
+
if (this.refInput) {
|
|
95
|
+
this.refInput.value = newValue.join(separator)
|
|
96
|
+
}
|
|
97
|
+
this.props.name && this.props.onBlur && this.props.onBlur(this.props.name)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
isBtnRemoveOption = (value: string) => {
|
|
101
|
+
const isSelectedOption = this.state.value.findIndex((x) => x === value) < 0
|
|
102
|
+
const selectOptions = options?.optionItems ?? this.props.slots?.optionItems ?? []
|
|
103
|
+
const isDefault = selectOptions.findIndex((x) => x === value) < 0
|
|
104
|
+
return isDefault && isSelectedOption
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getSelectOptions = () => {
|
|
108
|
+
let items = options?.optionItems ?? []
|
|
109
|
+
if (options?.saveLocalStorageByKey) {
|
|
110
|
+
items = storeItems?.getItemsAndMerge(items) ?? []
|
|
111
|
+
}
|
|
112
|
+
return items
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
getTextFieldProps = (): TextFieldProps => {
|
|
116
|
+
const { name, label, placeholder, onBlur, messageErrors } = this.props
|
|
117
|
+
const eMessage = getErrorMessage(messageErrors, name)
|
|
118
|
+
return {
|
|
119
|
+
label,
|
|
120
|
+
placeholder: placeholder || 'Add new',
|
|
121
|
+
onBlur: () => {
|
|
122
|
+
if (!name) return
|
|
123
|
+
onBlur && onBlur(name)
|
|
124
|
+
},
|
|
125
|
+
error: eMessage.error,
|
|
126
|
+
helperText: eMessage.message
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
getDefaultValue = (): string[] => {
|
|
131
|
+
const { name, data } = this.props
|
|
132
|
+
const value = name ? data?.[name]?.toString() : ''
|
|
133
|
+
return value?.split(separator).filter((x) => !!x) ?? []
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const ItemOption = styled('li')({
|
|
139
|
+
display: 'flex',
|
|
140
|
+
alignItems: 'center'
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
type TChipStoreRoot = { [key: string]: string[] }
|
|
144
|
+
|
|
145
|
+
class AutocompleteChipStore {
|
|
146
|
+
private storeKeyRoot = 'autocomplete_chip_store'
|
|
147
|
+
private storeKey: string
|
|
148
|
+
constructor(key: string, defaultValue?: string[]) {
|
|
149
|
+
this.storeKey = key
|
|
150
|
+
if (defaultValue) {
|
|
151
|
+
this.setChild(defaultValue)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
static initial = (key: string, defaultValue?: string[]) => new AutocompleteChipStore(key, defaultValue)
|
|
156
|
+
|
|
157
|
+
private getRoot = (): TChipStoreRoot => {
|
|
158
|
+
try {
|
|
159
|
+
const res = window.localStorage.getItem(this.storeKeyRoot)
|
|
160
|
+
return JSON.parse(res ?? '{}')
|
|
161
|
+
} catch (error) {
|
|
162
|
+
return {}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private getChild = (): string[] => {
|
|
167
|
+
return this.getRoot()[this.storeKey]
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private setRoot = (value: TChipStoreRoot) => {
|
|
171
|
+
window.localStorage.setItem(this.storeKeyRoot, JSON.stringify(value))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private setChild = (value: string[]) => {
|
|
175
|
+
const obj = this.getRoot()
|
|
176
|
+
obj[this.storeKey] = value
|
|
177
|
+
this.setRoot(obj)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
getItemsAndMerge = (value: string[]): string[] => {
|
|
181
|
+
try {
|
|
182
|
+
return Array.from(new Set([...this.getChild(), ...value])).sort()
|
|
183
|
+
} catch (error) {
|
|
184
|
+
return value
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
setItems = (options: string[], value: string[]): string[] => {
|
|
189
|
+
const list = Array.from(new Set([...options, ...value])).sort()
|
|
190
|
+
this.setChild(list)
|
|
191
|
+
return list
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
delete = (value: string) => {
|
|
195
|
+
const list = this.getChild().filter((x) => x !== value)
|
|
196
|
+
this.setChild(list)
|
|
197
|
+
return list
|
|
198
|
+
}
|
|
199
|
+
}
|