better code: better abstraction for save-buttons

Massimo Melina committed Feb 21, 2024 at 10:15 UTC ffb602db32607192a8634e6fa7fe8ef5f77b2a00
7 files changed +19 -17
admin/src/AccountForm.ts
+2 -2
@@ -6,7 +6,7 @@ import { Alert } from '@mui/material'
6 import { apiCall } from './api'
7 import { alertDialog, toast, useDialogBarColors } from './dialog'
8 import { isEqualLax, wantArray } from './misc'
9 -import { IconBtn, modifiedSx } from './mui'
9 +import { IconBtn, modifiedProps } from './mui'
10 import { Account } from './AccountsPage'
11 import { createVerifierAndSalt, SRPParameters, SRPRoutines } from 'tssrp6a'
12 import { AutoDelete, Delete } from '@mui/icons-material'
@@ -89,7 +89,7 @@ export default function AccountForm({ account, done, groups, addToBar, reload }:
89 ],
90 onError: alertDialog,
91 save: {
92 - sx: modifiedSx( !isEqualLax(values, account)),
92 + ...modifiedProps( !isEqualLax(values, account)),
93 async onClick() {
94 const { password='', password2, adminActualAccess, hasPassword, invalidated, ...withoutPassword } = values
95 if (add) {
admin/src/ConfigFilePage.ts
+2 -2
@@ -4,7 +4,7 @@ import { createElement as h, Fragment, useEffect, useState } from 'react';
4 import { apiCall, useApiEx } from './api'
5 import { Alert, Box } from '@mui/material'
6 import { focusSelector, isCtrlKey, KeepInScreen } from './misc';
7 -import { Btn, Flex, IconBtn, modifiedSx, reloadBtn } from './mui';
7 +import { Btn, Flex, IconBtn, reloadBtn } from './mui';
8 import { Save, ContentCopy, EditNote } from '@mui/icons-material'
9 import { TextEditor } from './TextEditor';
10 import { state } from './state';
@@ -27,7 +27,7 @@ export default function ConfigFilePage() {
27 h(IconBtn, {
28 icon: Save,
29 title: "Save\n(ctrl+enter)",
30 - sx: modifiedSx(text !== saved),
30 + modified: text !== saved,
31 onClick: save,
32 }),
33 h(Alert, { severity: 'warning', sx: { flex: 1, minWidth: '10em' } }, "Be careful, you can easily break things here"),
admin/src/ConfigForm.ts
+2 -2
@@ -2,7 +2,7 @@ import { Form, FormProps } from '@hfs/mui-grid-form'
2 import { apiCall, useApiEx } from './api'
3 import { createElement as h, useEffect, useState, Dispatch } from 'react'
4 import _ from 'lodash'
5 -import { IconBtn, modifiedSx } from './mui'
5 +import { IconBtn, modifiedProps } from './mui'
6 import { RestartAlt } from '@mui/icons-material'
7 import { Callback } from '../../src/cross'
8
@@ -30,7 +30,7 @@ export function ConfigForm<T=any>({ keys, form, saveOnChange, onSave, ...rest }:
30 },
31 save: saveOnChange ? false : {
32 onClick: save,
33 - sx: modifiedSx(modified),
33 + ...modifiedProps(modified),
34 },
35 ...Array.isArray(formProps) ? { fields: formProps } : formProps,
36 ...rest,
admin/src/CustomHtmlPage.ts
+2 -2
@@ -5,7 +5,7 @@ import { Field, SelectField } from '@hfs/mui-grid-form'
5 import { apiCall, useApiEx } from './api'
6 import { Alert, Box } from '@mui/material'
7 import { Dict, HTTP_MESSAGES, isCtrlKey, prefix } from './misc';
8 -import { IconBtn, modifiedSx, reloadBtn, wikiLink } from './mui';
8 +import { IconBtn, reloadBtn, wikiLink } from './mui';
9 import { Save } from '@mui/icons-material'
10 import _ from 'lodash'
11 import { useDebounce } from 'usehooks-ts'
@@ -46,7 +46,7 @@ export default function CustomHtmlPage() {
46 h(IconBtn, {
47 icon: Save,
48 title: "Save\n(ctrl+enter)",
49 - sx: modifiedSx(anyChange),
49 + modified: anyChange,
50 onClick: save,
51 }),
52 ),
admin/src/FileForm.ts
+2 -2
@@ -8,7 +8,7 @@ import { BoolField, DisplayField, Field, FieldProps, Form, MultiSelectField, Sel
8 import { apiCall, UseApi, useApiEx } from './api'
9 import { basename, defaultPerms, formatBytes, formatTimestamp, isEqualLax, isWhoObject, newDialog, objSameKeys,
10 onlyTruthy, prefix, VfsPerms, wantArray, Who, WhoObject, matches, HTTP_MESSAGES, xlate } from './misc'
11 -import { Btn, IconBtn, LinkBtn, modifiedSx, useBreakpoint, wikiLink } from './mui'
11 +import { Btn, IconBtn, LinkBtn, modifiedProps, useBreakpoint, wikiLink } from './mui'
12 import { reloadVfs, VfsNode } from './VfsPage'
13 import md from './md'
14 import _ from 'lodash'
@@ -132,7 +132,7 @@ export default function FileForm({ file, addToBar, statusApi }: FileFormProps) {
132 ],
133 onError: alertDialog,
134 save: {
135 - sx: modifiedSx(!isEqualLax(values, rest)),
135 + ...modifiedProps(!isEqualLax(values, rest)),
136 async onClick() {
137 const props = _.omit(values, ['ctime','mtime','size','id'])
138 ;(props as any).masks ||= null // undefined cannot be serialized
admin/src/OptionsPage.ts
+3 -3
@@ -5,10 +5,10 @@ import { createElement as h, Fragment, useEffect, useRef } from 'react';
5 import { apiCall, useApiEx } from './api'
6 import { state, useSnapState } from './state'
7 import { Link as RouterLink } from 'react-router-dom'
8 -import { CardMembership, Check, EditNote, Refresh, Warning } from '@mui/icons-material'
8 +import { CardMembership, EditNote, Refresh, Warning } from '@mui/icons-material'
9 import { Dict, MAX_TILE_SIZE, REPO_URL, isIpLocalHost, wait, with_, try_, ipForUrl, SORT_BY_OPTIONS, THEME_OPTIONS,
10 CFG } from './misc'
11 -import { iconTooltip, InLink, LinkBtn, modifiedSx, wikiLink, useBreakpoint, NetmaskField, WildcardsSupported } from './mui'
11 +import { iconTooltip, InLink, LinkBtn, modifiedProps, wikiLink, useBreakpoint, NetmaskField, WildcardsSupported } from './mui'
12 import { Form, BoolField, NumberField, SelectField, FieldProps, Field, StringField } from '@hfs/mui-grid-form';
13 import { ArrayField } from './ArrayField'
14 import FileField from './FileField'
@@ -80,7 +80,7 @@ export default function OptionsPage() {
80 onError: alertDialog,
81 save: {
82 onClick: save,
83 - sx: modifiedSx( Object.keys(changes).length>0),
83 + ...modifiedProps( Object.keys(changes).length>0),
84 },
85 barSx: { gap: 2 },
86 addToBar: [
admin/src/mui.ts
+6 -4
@@ -104,9 +104,10 @@ export function reloadBtn(onClick: any, props?: any) {
104 return h(IconBtn, { icon: Refresh, title: "Reload", onClick, ...props })
105 }
106
107 -export function modifiedSx(is: boolean) {
108 - return is ? { outline: '2px solid' } : undefined
107 +export function modifiedProps(modified: boolean) {
108 + return modified ? { sx: { outline: '2px solid' } } : undefined
109 }
110 +
111 interface IconBtnProps extends Omit<IconButtonProps, 'disabled'|'title'|'onClick'> {
112 title?: ReactNode
113 icon: SvgIconComponent
@@ -116,10 +117,11 @@ interface IconBtnProps extends Omit<IconButtonProps, 'disabled'|'title'|'onClick
117 confirm?: string
118 doneMessage?: boolean | string // displayed only if the result of onClick !== false
119 tooltipProps?: Partial<TooltipProps>
120 + modified?: boolean
121 onClick?: (...args: Parameters<NonNullable<IconButtonProps['onClick']>>) => Promisable<any>
122 }
123
122 -export const IconBtn = forwardRef(({ title, icon, onClick, disabled, progress, link, tooltipProps, confirm, doneMessage, sx, ...rest }: IconBtnProps, ref: any) => {
124 +export const IconBtn = forwardRef(({ title, icon, onClick, disabled, progress, link, tooltipProps, confirm, doneMessage, sx, modified, ...rest }: IconBtnProps, ref: any) => {
125 const [loading, setLoading] = useStateMounted(false)
126 if (typeof disabled === 'string')
127 title = disabled
@@ -130,7 +132,7 @@ export const IconBtn = forwardRef(({ title, icon, onClick, disabled, progress, l
132 ref,
133 disabled,
134 ...rest,
133 - sx: { height: 'fit-content', ...sx },
135 + sx: { height: 'fit-content', ...modifiedProps(modified || false)?.sx, ...sx },
136 async onClick(...args) {
137 if (confirm && !await confirmDialog(confirm)) return
138 const ret = onClick?.apply(this,args)