better code: split files
Massimo Melina committed
Sep 16, 2023 at 15:54 UTC
72f624a5f92346fa7ff496e40ac299b1d99e5240
5 files changed
+217
-212
admin/src/misc.ts
+1
-205
@@ -1,142 +1,9 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
-import { createElement as h, FC, Fragment, ReactNode, KeyboardEvent, forwardRef } from 'react'
4
-import {
5
- Box, BoxProps,
6
- Breakpoint,
7
- ButtonProps,
8
- CircularProgress,
9
- IconButton,
10
- IconButtonProps,
11
- Link, LinkProps,
12
- Tooltip, TooltipProps,
13
- useMediaQuery
14
-} from '@mui/material'
15
-import { Link as RouterLink } from 'react-router-dom'
16
-import { SxProps } from '@mui/system'
17
-import { Refresh, SvgIconComponent } from '@mui/icons-material'
18
-import { alertDialog, confirmDialog } from './dialog'
3
import { apiCall } from './api'
20
-import { dontBotherWithKeys, formatPerc, useStateMounted, WIKI_URL } from '@hfs/shared'
21
-import { Promisable } from '@hfs/mui-grid-form'
22
-import { LoadingButton, LoadingButtonProps } from '@mui/lab'
4
+export * from './mui'
5
export * from '@hfs/shared'
6
25
-export function spinner() {
26
- return h(CircularProgress)
27
-}
28
-
29
-export function isWindowsDrive(s?: string) {
30
- return s && /^[a-zA-Z]:$/.test(s)
31
-}
32
-
33
-export function isEqualLax(a: any,b: any): boolean {
34
- return a == b //eslint-disable-line
35
- || (a && b && typeof a === 'object' && typeof b === 'object'
36
- && Object.entries(a).every(([k,v]) => isEqualLax(v, b[k])) )
37
-}
38
-
39
-export function modifiedSx(is: boolean) {
40
- return is ? { outline: '2px solid' } : undefined
41
-}
42
-interface IconBtnProps extends Omit<IconButtonProps, 'disabled'|'title'|'onClick'> {
43
- title?: ReactNode
44
- icon: SvgIconComponent
45
- disabled?: boolean | string
46
- progress?: boolean | number
47
- link?: string
48
- confirm?: string
49
- tooltipProps?: Partial<TooltipProps>
50
- onClick: (...args: Parameters<NonNullable<IconButtonProps['onClick']>>) => Promisable<any>
51
-}
52
-
53
-export const IconBtn = forwardRef(({ title, icon, onClick, disabled, progress, link, tooltipProps, confirm, ...rest }: IconBtnProps, ref: any) => {
54
- const [loading, setLoading] = useStateMounted(false)
55
- if (typeof disabled === 'string')
56
- title = disabled
57
- if (link)
58
- onClick = () => window.open(link)
59
- let ret: ReturnType<FC> = h(IconButton, {
60
- ref,
61
- disabled: Boolean(loading || progress || disabled),
62
- ...rest,
63
- async onClick(...args) {
64
- if (confirm && !await confirmDialog(confirm)) return
65
- const ret = onClick?.apply(this,args)
66
- if (ret && ret instanceof Promise) {
67
- setLoading(true)
68
- ret.catch(alertDialog).finally(()=> setLoading(false))
69
- }
70
- }
71
- },
72
- (progress || loading) && progress !== false // false is also useful to inhibit behavior with loading
73
- && h(CircularProgress, {
74
- ...(typeof progress === 'number' ? { value: progress*100, variant: 'determinate' } : null),
75
- style: { position:'absolute', top: '10%', left: '10%', width: '80%', height: '80%' }
76
- }),
77
- h(icon)
78
- )
79
- if (title)
80
- ret = h(Tooltip, { title, ...tooltipProps, children: h('span',{},ret) })
81
- return ret
82
-})
83
-
84
-interface BtnProps extends Omit<LoadingButtonProps,'disabled'|'title'|'onClick'> {
85
- icon?: SvgIconComponent
86
- title?: ReactNode
87
- disabled?: boolean | string
88
- progress?: boolean | number
89
- link?: string
90
- confirm?: string
91
- tooltipProps?: TooltipProps
92
- onClick: (...args: Parameters<NonNullable<ButtonProps['onClick']>>) => Promisable<any>
93
-}
94
-export function Btn({ icon, title, onClick, disabled, progress, link, tooltipProps, confirm, ...rest }: BtnProps) {
95
- const [loading, setLoading] = useStateMounted(false)
96
- if (typeof disabled === 'string') {
97
- title = disabled
98
- disabled = true
99
- }
100
- if (link)
101
- onClick = () => window.open(link)
102
- let ret: ReturnType<FC> = h(LoadingButton, {
103
- variant: 'contained',
104
- startIcon: icon && h(icon),
105
- loading: Boolean(loading || progress),
106
- loadingPosition: icon && 'start',
107
- loadingIndicator: typeof progress !== 'number' ? undefined
108
- : h(CircularProgress, { size: '1rem', value: progress*100, variant: 'determinate' }),
109
- disabled,
110
- ...rest,
111
- async onClick(...args) {
112
- if (confirm && !await confirmDialog(confirm)) return
113
- const ret = onClick?.apply(this,args)
114
- if (ret && ret instanceof Promise) {
115
- setLoading(true)
116
- ret.catch(alertDialog).finally(()=> setLoading(false))
117
- }
118
- }
119
- })
120
- if (title)
121
- ret = h(Tooltip, { title, ...tooltipProps, children: h('span',{},ret) })
122
- return ret
123
-}
124
-
125
-export function iconTooltip(icon: SvgIconComponent, tooltip: ReactNode, sx?: SxProps) {
126
- return h(Tooltip, { title: tooltip, children: h(icon, { sx: { verticalAlign: 'bottom', ...sx } }) })
127
-}
128
-
129
-export function InLink(props:any) {
130
- return h(Link, { component: RouterLink, ...props })
131
-}
132
-
133
-export const Center = forwardRef((props: BoxProps, ref) =>
134
- h(Box, { ref, display:'flex', height:'100%', width:'100%', justifyContent:'center', alignItems:'center', flexDirection: 'column', ...props }))
135
-
136
-export function LinkBtn({ ...rest }: LinkProps) {
137
- return h(Link, { ...rest, sx: { cursor: 'pointer', ...rest.sx } })
138
-}
139
-
7
export async function manipulateConfig(k: string, work:(data:any) => any) {
8
const cfg = await apiCall('get_config', { only: [k] })
9
const was = cfg[k]
@@ -145,80 +12,9 @@ export async function manipulateConfig(k: string, work:(data:any) => any) {
12
await apiCall('set_config', { values: { [k]: will } })
13
}
14
148
-export function typedKeys<T extends {}>(o: T) {
149
- return Object.keys(o) as (keyof T)[]
150
-}
151
-
152
-export function xlate(input: any, table: Record<string, any>) {
153
- return table[input] ?? input
154
-}
155
-
156
-// return true if same size or larger
157
-export function useBreakpoint(breakpoint: Breakpoint) {
158
- return useMediaQuery((theme: any) => theme.breakpoints.up(breakpoint), { noSsr:true }) // without noSsr, first execution always returns false
159
-}
160
-
15
export function err2msg(code: string) {
16
return {
17
ENOENT: "Not found",
18
ENOTDIR: "Not a folder",
19
}[code] || code
20
}
167
-
168
-export function reloadBtn(onClick: any, props?: any) {
169
- return h(IconBtn, { icon: Refresh, title: "Reload", onClick, ...props })
170
-}
171
-
172
-const isMac = navigator.platform.match('Mac')
173
-export function isCtrlKey(ev: KeyboardEvent) {
174
- return (ev.ctrlKey || isMac && ev.metaKey) && ev.key
175
-}
176
-
177
-interface IconProgressProps {
178
- icon: SvgIconComponent,
179
- progress: number,
180
- offset?: number,
181
- sx?: SxProps,
182
- addTitle?: ReactNode
183
-}
184
-export function IconProgress({ icon, progress, offset, addTitle, sx }: IconProgressProps) {
185
- return h(Fragment, {},
186
- h(icon, { sx: { position:'absolute', ml: '4px' } }),
187
- h(CircularProgress, {
188
- value: progress * 100,
189
- variant: 'determinate',
190
- size: 32,
191
- sx: { position: 'absolute' },
192
- }),
193
- h(Tooltip, {
194
- title: h(Fragment, {}, formatPerc(progress), addTitle),
195
- children: h(CircularProgress, {
196
- color: 'success',
197
- value: (offset || 1e-7) * 100,
198
- variant: 'determinate',
199
- size: 32,
200
- sx: { display: 'flex', ...sx }, // workaround: without this the element is has 0 width when the space is crammy (monitor/file)
201
- }),
202
- })
203
- )
204
-}
205
-
206
-export function Flex({ gap='.8em', vert=false, center=false, children=null, props={}, ...rest }) {
207
- return h(Box, {
208
- sx: {
209
- display: 'flex',
210
- gap,
211
- flexDirection: vert ? 'column' : undefined,
212
- ...center && { alignItems: 'center', justifyContent: 'center' },
213
- ...rest,
214
- },
215
- ...props
216
- }, children)
217
-}
218
-
219
-
220
-export function wikiLink(uri: string, content: ReactNode) {
221
- if (Array.isArray(content))
222
- content = dontBotherWithKeys(content)
223
- return h(Link, { href: WIKI_URL + uri, target: 'help' }, content)
224
-}
\ No newline at end of file
admin/src/mui.ts
new
+185
@@ -0,0 +1,185 @@
1
+// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
+// all content here is shared between client and server
3
+
4
+import { Refresh, SvgIconComponent } from '@mui/icons-material'
5
+import { SxProps } from '@mui/system'
6
+import { createElement as h, FC, forwardRef, Fragment, ReactNode } from 'react'
7
+import {
8
+ Box, BoxProps, Breakpoint,
9
+ ButtonProps,
10
+ CircularProgress,
11
+ IconButton,
12
+ IconButtonProps,
13
+ Link, LinkProps,
14
+ Tooltip,
15
+ TooltipProps, useMediaQuery
16
+} from '@mui/material'
17
+import { formatPerc, WIKI_URL } from '../../src/cross'
18
+import { dontBotherWithKeys, useStateMounted } from '@hfs/shared'
19
+import { Promisable } from '@hfs/mui-grid-form'
20
+import { alertDialog, confirmDialog } from './dialog'
21
+import { LoadingButton, LoadingButtonProps } from '@mui/lab'
22
+import { Link as RouterLink } from 'react-router-dom'
23
+
24
+export function spinner() {
25
+ return h(CircularProgress)
26
+}
27
+
28
+// return true if same size or larger
29
+export function useBreakpoint(breakpoint: Breakpoint) {
30
+ return useMediaQuery((theme: any) => theme.breakpoints.up(breakpoint), { noSsr:true }) // without noSsr, first execution always returns false
31
+}
32
+
33
+interface IconProgressProps {
34
+ icon: SvgIconComponent,
35
+ progress: number,
36
+ offset?: number,
37
+ sx?: SxProps,
38
+ addTitle?: ReactNode
39
+}
40
+export function IconProgress({ icon, progress, offset, addTitle, sx }: IconProgressProps) {
41
+ return h(Fragment, {},
42
+ h(icon, { sx: { position:'absolute', ml: '4px' } }),
43
+ h(CircularProgress, {
44
+ value: progress * 100,
45
+ variant: 'determinate',
46
+ size: 32,
47
+ sx: { position: 'absolute' },
48
+ }),
49
+ h(Tooltip, {
50
+ title: h(Fragment, {}, formatPerc(progress), addTitle),
51
+ children: h(CircularProgress, {
52
+ color: 'success',
53
+ value: (offset || 1e-7) * 100,
54
+ variant: 'determinate',
55
+ size: 32,
56
+ sx: { display: 'flex', ...sx }, // workaround: without this the element is has 0 width when the space is crammy (monitor/file)
57
+ }),
58
+ })
59
+ )
60
+}
61
+
62
+export function Flex({ gap='.8em', vert=false, center=false, children=null, props={}, ...rest }) {
63
+ return h(Box, {
64
+ sx: {
65
+ display: 'flex',
66
+ gap,
67
+ flexDirection: vert ? 'column' : undefined,
68
+ ...center && { alignItems: 'center', justifyContent: 'center' },
69
+ ...rest,
70
+ },
71
+ ...props
72
+ }, children)
73
+}
74
+
75
+
76
+export function wikiLink(uri: string, content: ReactNode) {
77
+ if (Array.isArray(content))
78
+ content = dontBotherWithKeys(content)
79
+ return h(Link, { href: WIKI_URL + uri, target: 'help' }, content)
80
+}
81
+
82
+export function reloadBtn(onClick: any, props?: any) {
83
+ return h(IconBtn, { icon: Refresh, title: "Reload", onClick, ...props })
84
+}
85
+
86
+export function modifiedSx(is: boolean) {
87
+ return is ? { outline: '2px solid' } : undefined
88
+}
89
+interface IconBtnProps extends Omit<IconButtonProps, 'disabled'|'title'|'onClick'> {
90
+ title?: ReactNode
91
+ icon: SvgIconComponent
92
+ disabled?: boolean | string
93
+ progress?: boolean | number
94
+ link?: string
95
+ confirm?: string
96
+ tooltipProps?: Partial<TooltipProps>
97
+ onClick: (...args: Parameters<NonNullable<IconButtonProps['onClick']>>) => Promisable<any>
98
+}
99
+
100
+export const IconBtn = forwardRef(({ title, icon, onClick, disabled, progress, link, tooltipProps, confirm, ...rest }: IconBtnProps, ref: any) => {
101
+ const [loading, setLoading] = useStateMounted(false)
102
+ if (typeof disabled === 'string')
103
+ title = disabled
104
+ if (link)
105
+ onClick = () => window.open(link)
106
+ let ret: ReturnType<FC> = h(IconButton, {
107
+ ref,
108
+ disabled: Boolean(loading || progress || disabled),
109
+ ...rest,
110
+ async onClick(...args) {
111
+ if (confirm && !await confirmDialog(confirm)) return
112
+ const ret = onClick?.apply(this,args)
113
+ if (ret && ret instanceof Promise) {
114
+ setLoading(true)
115
+ ret.catch(alertDialog).finally(()=> setLoading(false))
116
+ }
117
+ }
118
+ },
119
+ (progress || loading) && progress !== false // false is also useful to inhibit behavior with loading
120
+ && h(CircularProgress, {
121
+ ...(typeof progress === 'number' ? { value: progress*100, variant: 'determinate' } : null),
122
+ style: { position:'absolute', top: '10%', left: '10%', width: '80%', height: '80%' }
123
+ }),
124
+ h(icon)
125
+ )
126
+ if (title)
127
+ ret = h(Tooltip, { title, ...tooltipProps, children: h('span',{},ret) })
128
+ return ret
129
+})
130
+
131
+interface BtnProps extends Omit<LoadingButtonProps,'disabled'|'title'|'onClick'> {
132
+ icon?: SvgIconComponent
133
+ title?: ReactNode
134
+ disabled?: boolean | string
135
+ progress?: boolean | number
136
+ link?: string
137
+ confirm?: string
138
+ tooltipProps?: TooltipProps
139
+ onClick: (...args: Parameters<NonNullable<ButtonProps['onClick']>>) => Promisable<any>
140
+}
141
+export function Btn({ icon, title, onClick, disabled, progress, link, tooltipProps, confirm, ...rest }: BtnProps) {
142
+ const [loading, setLoading] = useStateMounted(false)
143
+ if (typeof disabled === 'string') {
144
+ title = disabled
145
+ disabled = true
146
+ }
147
+ if (link)
148
+ onClick = () => window.open(link)
149
+ let ret: ReturnType<FC> = h(LoadingButton, {
150
+ variant: 'contained',
151
+ startIcon: icon && h(icon),
152
+ loading: Boolean(loading || progress),
153
+ loadingPosition: icon && 'start',
154
+ loadingIndicator: typeof progress !== 'number' ? undefined
155
+ : h(CircularProgress, { size: '1rem', value: progress*100, variant: 'determinate' }),
156
+ disabled,
157
+ ...rest,
158
+ async onClick(...args) {
159
+ if (confirm && !await confirmDialog(confirm)) return
160
+ const ret = onClick?.apply(this,args)
161
+ if (ret && ret instanceof Promise) {
162
+ setLoading(true)
163
+ ret.catch(alertDialog).finally(()=> setLoading(false))
164
+ }
165
+ }
166
+ })
167
+ if (title)
168
+ ret = h(Tooltip, { title, ...tooltipProps, children: h('span',{},ret) })
169
+ return ret
170
+}
171
+
172
+export function iconTooltip(icon: SvgIconComponent, tooltip: ReactNode, sx?: SxProps) {
173
+ return h(Tooltip, { title: tooltip, children: h(icon, { sx: { verticalAlign: 'bottom', ...sx } }) })
174
+}
175
+
176
+export function InLink(props:any) {
177
+ return h(Link, { component: RouterLink, ...props })
178
+}
179
+
180
+export const Center = forwardRef((props: BoxProps, ref) =>
181
+ h(Box, { ref, display:'flex', height:'100%', width:'100%', justifyContent:'center', alignItems:'center', flexDirection: 'column', ...props }))
182
+
183
+export function LinkBtn({ ...rest }: LinkProps) {
184
+ return h(Link, { ...rest, sx: { cursor: 'pointer', ...rest.sx } })
185
+}
shared/react.ts
+15
-1
@@ -1,6 +1,15 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
-import { createElement as h, Fragment, ReactElement, ReactNode, useCallback, useEffect, useState } from 'react'
3
+import {
4
+ createElement as h,
5
+ Fragment,
6
+ KeyboardEvent,
7
+ ReactElement,
8
+ ReactNode,
9
+ useCallback,
10
+ useEffect,
11
+ useState
12
+} from 'react'
13
import { useIsMounted } from 'usehooks-ts'
14
15
export function useStateMounted<T>(init: T) {
@@ -71,4 +80,9 @@ export function useBatch<Job=unknown,Result=unknown>(
80
if (env && cached === undefined)
81
env.batch.add(job)
82
return { data: cached, ...env } as Env & { data: Result | undefined | null } // so you can cache.clear
83
+}
84
+
85
+const isMac = navigator.platform.match('Mac')
86
+export function isCtrlKey(ev: KeyboardEvent) {
87
+ return (ev.ctrlKey || isMac && ev.metaKey) && ev.key
88
}
\ No newline at end of file
src/cross.ts
+15
-1
@@ -234,4 +234,18 @@ export function isPrimitive(x: unknown): x is boolean | string | number | undefi
234
235
export function isIP(address: string) {
236
return /^([.:\da-f]+)$/i.test(address)
237
-}
\ No newline at end of file
237
+}
238
+
239
+export function isWindowsDrive(s?: string) {
240
+ return s && /^[a-zA-Z]:$/.test(s)
241
+}
242
+
243
+export function isEqualLax(a: any,b: any): boolean {
244
+ return a == b //eslint-disable-line
245
+ || (a && b && typeof a === 'object' && typeof b === 'object'
246
+ && Object.entries(a).every(([k,v]) => isEqualLax(v, b[k])) )
247
+}
248
+
249
+export function xlate(input: any, table: Record<string, any>) {
250
+ return table[input] ?? input
251
+}
src/util-files.ts
+1
-5
@@ -1,7 +1,7 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
import fs from 'fs/promises'
4
-import { Promisable, try_, tryJson, wait } from './misc'
4
+import { Promisable, try_, tryJson, wait, isWindowsDrive } from './misc'
5
import { promisify } from 'util'
6
import { createWriteStream, mkdirSync, watch } from 'fs'
7
import { basename, dirname } from 'path'
@@ -69,10 +69,6 @@ export function dirTraversal(s?: string) {
69
return s && /(^|[/\\])\.\.($|[/\\])/.test(s)
70
}
71
72
-export function isWindowsDrive(s?: string) {
73
- return s && /^[a-zA-Z]:$/.test(s)
74
-}
75
-
72
// apply this to paths that may contain \ as separator (not supported by fast-glob) and other special chars to be escaped (parenthesis)
73
export function adjustStaticPathForGlob(path: string) {
74
return glob.escapePath(path.replace(/\\/g, '/'))