share common code: misc
Massimo Melina committed
Mar 6, 2022 at 19:56 UTC
bc59d22a1e61ad0dc7399db0190d904497eb24f5
11 files changed
+182
-174
.gitignore
+1
@@ -6,3 +6,4 @@ counters.yaml
6
*.log
7
config.yaml
8
accounts.yaml
9
+shared/lib
admin/src/misc.ts
+2
-100
@@ -1,101 +1,26 @@
1
// This file is part of HFS - Copyright 2020-2021, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
-import { createElement as h, Fragment, ReactElement,
4
- ReactNode, useCallback, useEffect, useRef, useState } from 'react'
3
+import { createElement as h } from 'react'
4
import { Box, CircularProgress, IconButton, Link, Tooltip } from '@mui/material'
5
import { Link as RouterLink } from 'react-router-dom'
6
import { SxProps } from '@mui/system'
7
import { SvgIconComponent } from '@mui/icons-material'
9
-
10
-export type Dict<T = any> = Record<string, T>
11
-export type Falsy = false | null | undefined | '' | 0
8
+export * from 'shared'
9
10
export function spinner() {
11
return h(CircularProgress)
12
}
13
17
-export function getCookie(name: string) {
18
- const pre = name + '='
19
- let decodedCookie = decodeURIComponent(document.cookie)
20
- let ca = decodedCookie.split(';')
21
- for (let c of ca) {
22
- c = c.trim()
23
- if (c.startsWith(pre))
24
- return c.substring(pre.length, c.length)
25
- }
26
- return ''
27
-}
28
-
29
-export function formatBytes(n: number, post: string = 'B', k=1024) {
30
- if (isNaN(Number(n)) || n < 0)
31
- return ''
32
- let x = ['', 'K', 'M', 'G', 'T']
33
- let prevMul = 1
34
- let mul = k
35
- let i = 0
36
- while (i < x.length && n > mul) {
37
- prevMul = mul
38
- mul *= k
39
- ++i
40
- }
41
- n /= prevMul
42
- return round(n, 1) + ' ' + (x[i]||'') + post
43
-} // formatBytes
44
-
45
-export function round(v: number, decimals: number = 0) {
46
- decimals = Math.pow(10, decimals)
47
- return Math.round(v * decimals) / decimals
48
-} // round
49
-
50
-export function objSameKeys<T,R>(src: Record<string,T>, newValue:(value:T,key:string)=>R) {
51
- return Object.fromEntries(Object.entries(src).map(([k,v]) => [k, newValue(v,k)]))
52
-}
53
-
54
-export function enforceFinal(sub:string, s:string) {
55
- return !s || s.endsWith(sub) ? s : s+sub
56
-}
57
-
14
export function isWindowsDrive(s?: string) {
15
return s && /^[a-zA-Z]:$/.test(s)
16
}
17
62
-export function useIsMounted() {
63
- const mountRef = useRef(false)
64
- useEffect(() => {
65
- mountRef.current = true
66
- return () => {
67
- mountRef.current = false
68
- }
69
- }, [])
70
- return useCallback(()=> mountRef.current, [mountRef])
71
-}
72
-
73
-export function useStateMounted<T>(init: T) {
74
- const isMounted = useIsMounted()
75
- const [v, set] = useState(init)
76
- const setIfMounted = useCallback((newValue:T | ((previous:T)=>T)) => {
77
- if (isMounted())
78
- set(newValue)
79
- }, [isMounted, set])
80
- return [v, setIfMounted, isMounted] as [T, typeof setIfMounted, typeof isMounted]
81
-}
82
-
18
export function isEqualLax(a: any,b: any): boolean {
19
return a == b //eslint-disable-line
20
|| (a && b && typeof a === 'object' && typeof b === 'object'
21
&& Object.entries(a).every(([k,v]) => isEqualLax(v, b[k])) )
22
}
23
89
-type Truthy<T> = T extends false | '' | 0 | null | undefined ? never : T
90
-
91
-export function truthy<T>(value: T): value is Truthy<T> {
92
- return Boolean(value)
93
-}
94
-
95
-export function onlyTruthy<T>(arr: T[]) {
96
- return arr.filter(truthy)
97
-}
98
-
24
export function IconBtn({ title, icon, ...rest }: { title?: string, icon: SvgIconComponent, [rest:string]:any }) {
25
const ret = h(IconButton, { ...rest }, h(icon))
26
return title ? h(Tooltip, { title, children: ret }) : ret
@@ -105,29 +30,6 @@ export function iconTooltip(icon: SvgIconComponent, tooltip: string, sx?: SxProp
30
return h(Tooltip, { title: tooltip, children: h(icon, { sx }) })
31
}
32
108
-export function prefix(pre:string, v:string|number, post:string='') {
109
- return v ? pre+v+post : ''
110
-}
111
-
112
-export function reactFilter(elements: any[]) {
113
- return elements.filter(x=> x===0 || x && (!Array.isArray(x) || x.length))
114
-}
115
-
116
-export function reactJoin(joiner: string | ReactElement, elements: Parameters<typeof reactFilter>[0]) {
117
- const ret = []
118
- for (const x of reactFilter(elements))
119
- ret.push(x, joiner)
120
- ret.splice(-1,1)
121
- return dontBotherWithKeys(ret)
122
-}
123
-
124
-export function dontBotherWithKeys(elements: ReactNode[]): (ReactNode|string)[] {
125
- return elements.map((e,i)=>
126
- !e || typeof e === 'string' ? e
127
- : Array.isArray(e) ? dontBotherWithKeys(e)
128
- : h(Fragment, { key:i, children:e }) )
129
-}
130
-
33
export function InLink(props:any) {
34
return h(Link, { component: RouterLink, ...props })
35
}
frontend/src/misc.ts
+3
-67
@@ -1,13 +1,11 @@
1
// This file is part of HFS - Copyright 2020-2021, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
-import { createElement as h, useCallback, useState } from 'react'
3
+import { createElement as h } from 'react'
4
import { Spinner } from './components'
5
import { newDialog } from './dialog'
6
import { Icon } from './icons'
7
-
8
-export type Falsy = false | null | undefined | '' | 0
9
-
10
-export type Dict = Record<string, any>
7
+import { Dict } from 'shared'
8
+export * from 'shared'
9
10
export function hIcon(name: string, props?:any) {
11
return h(Icon, { name, ...props })
@@ -17,51 +15,6 @@ export function hError(err: Error | string | null) {
15
return err && h('div', { className:'error-msg' }, typeof err === 'string' ? err : err.message)
16
}
17
20
-export function formatBytes(n: number, post: string = 'B') {
21
- if (isNaN(Number(n)))
22
- return ''
23
- let x = ['', 'K', 'M', 'G', 'T']
24
- let prevMul = 1
25
- let mul = 1024
26
- let i = 0
27
- while (i < x.length && n > mul) {
28
- prevMul = mul
29
- mul *= 1024
30
- ++i
31
- }
32
- n /= prevMul
33
- return round(n, 1) + ' ' + (x[i]||'') + post
34
-} // formatBytes
35
-
36
-export function round(v: number, decimals: number = 0) {
37
- decimals = Math.pow(10, decimals)
38
- return Math.round(v * decimals) / decimals
39
-} // round
40
-
41
-export function prefix(pre:string, v:string|number|undefined|null|false, post:string='') {
42
- return v ? pre+v+post : ''
43
-}
44
-
45
-export function wait(ms: number) {
46
- return new Promise(res=> setTimeout(res,ms))
47
-}
48
-
49
-export function waitFor<T>(cb:()=>T, ms:number=200) : Promise<Exclude<T,Falsy>> {
50
- return new Promise(resolve=>{
51
- let h: NodeJS.Timeout
52
- if (go())
53
- h = setInterval(go, ms)
54
-
55
- function go() {
56
- const v = cb()
57
- if (!v) return true
58
- // @ts-ignore we know it's not falsy
59
- resolve(v)
60
- clearInterval(h)
61
- }
62
- })
63
-}
64
-
18
let isWorking = false // we want the 'working' thing to be singleton
19
export function working() {
20
if (isWorking)
@@ -79,23 +32,6 @@ export function working() {
32
})
33
}
34
82
-export function useForceUpdate(): [()=>void, number] {
83
- const [n, setN] = useState(0)
84
- return [ useCallback(()=> setN(n => n+1), [setN]), n ]
85
-}
86
-
87
-export function getCookie(name: string) {
88
- const pre = name + '='
89
- let decodedCookie = decodeURIComponent(document.cookie)
90
- let ca = decodedCookie.split(';')
91
- for (let c of ca) {
92
- c = c.trim()
93
- if (c.startsWith(pre))
94
- return c.substring(pre.length, c.length)
95
- }
96
- return ''
97
-}
98
-
35
export function hfsEvent(name: string, params?:Dict) {
36
const output: any[] = []
37
document.dispatchEvent(new CustomEvent('hfs.'+name, { detail:{ params, output } }))
package-lock.json
+26
-4
@@ -11,7 +11,8 @@
11
"workspaces": [
12
"server",
13
"admin",
14
- "frontend"
14
+ "frontend",
15
+ "shared"
16
],
17
"bin": {
18
"hfs": "dist/src/index.js"
@@ -17585,6 +17586,10 @@
17586
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
17587
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
17588
},
17589
+ "node_modules/shared": {
17590
+ "resolved": "shared",
17591
+ "link": true
17592
+ },
17593
"node_modules/shebang-command": {
17594
"version": "2.0.0",
17595
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -18883,7 +18888,6 @@
18888
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz",
18889
"integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==",
18890
"dev": true,
18886
- "peer": true,
18891
"bin": {
18892
"tsc": "bin/tsc",
18893
"tsserver": "bin/tsserver"
@@ -20372,6 +20376,15 @@
20376
"engines": {
20377
"node": ">=16.13.0"
20378
}
20379
+ },
20380
+ "shared": {
20381
+ "devDependencies": {
20382
+ "react": "*",
20383
+ "react-dom": "*",
20384
+ "react-scripts": "*",
20385
+ "sass": "*",
20386
+ "typescript": "*"
20387
+ }
20388
}
20389
},
20390
"dependencies": {
@@ -33405,6 +33418,16 @@
33418
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
33419
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
33420
},
33421
+ "shared": {
33422
+ "version": "file:shared",
33423
+ "requires": {
33424
+ "react": "*",
33425
+ "react-dom": "*",
33426
+ "react-scripts": "*",
33427
+ "sass": "*",
33428
+ "typescript": "*"
33429
+ }
33430
+ },
33431
"shebang-command": {
33432
"version": "2.0.0",
33433
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -34401,8 +34424,7 @@
34424
"version": "4.5.2",
34425
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz",
34426
"integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==",
34404
- "dev": true,
34405
- "peer": true
34427
+ "dev": true
34428
},
34429
"unbox-primitive": {
34430
"version": "1.0.1",
package.json
+2
-1
@@ -9,9 +9,10 @@
9
"homepage": "https://rejetto.com/hfs",
10
"license": "GPL-3.0",
11
"author": "Massimo Melina <a@rejetto.com>",
12
- "workspaces": [ "server", "admin", "frontend" ],
12
+ "workspaces": [ "server", "admin", "frontend", "shared" ],
13
"scripts": {
14
"watch-server": "nodemon --ignore server/tests/ --watch server/src -e ts,tsx --exec ts-node server/src/index.ts",
15
+ "watch-shared": "cd shared && tsc --watch",
16
"start-frontend": "cd frontend && npm run start",
17
"start-admin": "cd admin && npm run start",
18
"start": "node dist",
shared/package.json
new
+16
@@ -0,0 +1,16 @@
1
+{
2
+ "name": "shared",
3
+ "main": "./lib/index.js",
4
+ "types": "./lib/index.d.ts",
5
+ "scripts": {
6
+ "test": "test",
7
+ "build": "tsc && yarn sass src/:lib/"
8
+ },
9
+ "devDependencies": {
10
+ "react": "*",
11
+ "react-dom": "*",
12
+ "react-scripts": "*",
13
+ "typescript": "*",
14
+ "sass": "*"
15
+ }
16
+}
shared/src/index.ts
new
+59
@@ -0,0 +1,59 @@
1
+import _ from 'lodash'
2
+export * from './react'
3
+
4
+export type Dict<T=any> = Record<string, T>
5
+export type Falsy = false | null | undefined | '' | 0
6
+type Truthy<T> = T extends false | '' | 0 | null | undefined ? never : T
7
+
8
+export function formatBytes(n: number, post: string = 'B', k=1024) {
9
+ if (isNaN(Number(n)) || n < 0)
10
+ return ''
11
+ let x = ['', 'K', 'M', 'G', 'T']
12
+ let prevMul = 1
13
+ let mul = k
14
+ let i = 0
15
+ while (i < x.length && n > mul) {
16
+ prevMul = mul
17
+ mul *= k
18
+ ++i
19
+ }
20
+ n /= prevMul
21
+ return _.round(n, 1) + ' ' + (x[i]||'') + post
22
+} // formatBytes
23
+
24
+export function prefix(pre:string, v:string|number|undefined|null|false, post:string='') {
25
+ return v ? pre+v+post : ''
26
+}
27
+
28
+export function wait(ms: number) {
29
+ return new Promise(res=> setTimeout(res,ms))
30
+}
31
+
32
+export function getCookie(name: string) {
33
+ const pre = name + '='
34
+ let decodedCookie = decodeURIComponent(document.cookie)
35
+ let ca = decodedCookie.split(';')
36
+ for (let c of ca) {
37
+ c = c.trim()
38
+ if (c.startsWith(pre))
39
+ return c.substring(pre.length, c.length)
40
+ }
41
+ return ''
42
+}
43
+
44
+export function objSameKeys<T,R>(src: Record<string,T>, newValue:(value:T,key:string)=>R) {
45
+ return Object.fromEntries(Object.entries(src).map(([k,v]) => [k, newValue(v,k)]))
46
+}
47
+
48
+export function enforceFinal(sub:string, s:string) {
49
+ return !s || s.endsWith(sub) ? s : s+sub
50
+}
51
+
52
+export function truthy<T>(value: T): value is Truthy<T> {
53
+ return Boolean(value)
54
+}
55
+
56
+export function onlyTruthy<T>(arr: T[]) {
57
+ return arr.filter(truthy)
58
+}
59
+
shared/src/react.ts
new
+42
@@ -0,0 +1,42 @@
1
+import { createElement as h, Fragment, ReactElement, ReactNode, useCallback, useEffect, useRef, useState } from 'react'
2
+
3
+export function useIsMounted() {
4
+ const mountRef = useRef(false)
5
+ useEffect(() => {
6
+ mountRef.current = true
7
+ return () => {
8
+ mountRef.current = false
9
+ }
10
+ }, [])
11
+ return useCallback(()=> mountRef.current, [mountRef])
12
+}
13
+
14
+export function useStateMounted<T>(init: T) {
15
+ const isMounted = useIsMounted()
16
+ const [v, set] = useState(init)
17
+ const setIfMounted = useCallback((newValue:T | ((previous:T)=>T)) => {
18
+ if (isMounted())
19
+ set(newValue)
20
+ }, [isMounted, set])
21
+ return [v, setIfMounted, isMounted] as [T, typeof setIfMounted, typeof isMounted]
22
+}
23
+
24
+export function reactFilter(elements: any[]) {
25
+ return elements.filter(x=> x===0 || x && (!Array.isArray(x) || x.length))
26
+}
27
+
28
+export function reactJoin(joiner: string | ReactElement, elements: Parameters<typeof reactFilter>[0]) {
29
+ const ret = []
30
+ for (const x of reactFilter(elements))
31
+ ret.push(x, joiner)
32
+ ret.splice(-1,1)
33
+ return dontBotherWithKeys(ret)
34
+}
35
+
36
+export function dontBotherWithKeys(elements: ReactNode[]): (ReactNode|string)[] {
37
+ return elements.map((e,i)=>
38
+ !e || typeof e === 'string' ? e
39
+ : Array.isArray(e) ? dontBotherWithKeys(e)
40
+ : h(Fragment, { key:i, children:e }) )
41
+}
42
+
shared/tsconfig.json
new
+30
@@ -0,0 +1,30 @@
1
+{
2
+ "compilerOptions": {
3
+ "target": "es2017",
4
+ "lib": [
5
+ "dom",
6
+ "dom.iterable",
7
+ "esnext"
8
+ ],
9
+ "allowJs": true,
10
+ "skipLibCheck": true,
11
+ "allowSyntheticDefaultImports": true,
12
+ "strict": true,
13
+ "forceConsistentCasingInFileNames": true,
14
+ "module": "esnext",
15
+ "moduleResolution": "node",
16
+ "resolveJsonModule": true,
17
+ "isolatedModules": true,
18
+ "outDir": "lib",
19
+ "declaration": true
20
+ },
21
+ "exclude": [
22
+ "node_modules",
23
+ "lib-esm",
24
+ "lib",
25
+ "**/*.spec*"
26
+ ],
27
+ "include": [
28
+ "src"
29
+ ]
30
+}
todo.md
-1
@@ -5,7 +5,6 @@ aggiornare admin gui
5
counters: non contare richieste fallite
6
consider having mime as ext,ext instead of *.ext|*.ext
7
# To do
8
-- monorepo + share code between apps
8
- admin/accounts: show icon for accounts with (possibly inherited) admin access
9
- expose admin at same port of frontend
10
- admin: improve masks editor
tsconfig.json
+1
-1
@@ -1,5 +1,5 @@
1
{
2
- "exclude": ["frontend","admin","tests","dist"],
2
+ "exclude": ["frontend","admin","tests","dist","shared"],
3
"compilerOptions": {
4
/* Visit https://aka.ms/tsconfig.json to read more about this file */
5