fix: admin/accounts: filling and emptying a text field left the "modified" visual clue on
Massimo Melina committed
Aug 17, 2024 at 12:32 UTC
c2a3332e20119a513c093bb8737389bb43bed347
7 files changed
+24
-17
admin/src/AccountForm.ts
+6
-2
@@ -6,7 +6,7 @@ import { Alert } from '@mui/material'
6
import { apiCall } from './api'
7
import { alertDialog, useDialogBarColors } from './dialog'
8
import { formatTimestamp, isEqualLax, prefix, useIsMobile, wantArray } from './misc'
9
-import { IconBtn, modifiedProps } from './mui'
9
+import { IconBtn, propsForModifiedValues } from './mui'
10
import { Account } from './AccountsPage'
11
import { createVerifierAndSalt, SRPParameters, SRPRoutines } from 'tssrp6a'
12
import { AutoDelete, Delete } from '@mui/icons-material'
@@ -88,7 +88,7 @@ export default function AccountForm({ account, done, groups, addToBar, reload }:
88
],
89
onError: alertDialog,
90
save: {
91
- ...modifiedProps( !isEqualLax(values, account)),
91
+ ...propsForModifiedValues(isModifiedConfig(values, account)),
92
async onClick() {
93
const { password='', password2, adminActualAccess, hasPassword, invalidated, ...withoutPassword } = values
94
if (add) {
@@ -116,6 +116,10 @@ export default function AccountForm({ account, done, groups, addToBar, reload }:
116
})
117
}
118
119
+export function isModifiedConfig(a: any, b: any) {
120
+ return !isEqualLax(a, b, (a,b) => !a && !b || undefined)
121
+}
122
+
123
// you can set password directly in add/set_account, but using this api instead will add extra security because it is not sent as clear-text, so it's especially good if you are not in localhost and not using https
124
export async function apiNewPassword(username: string, password: string) {
125
const srp6aNimbusRoutines = new SRPRoutines(new SRPParameters())
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, modifiedProps } from './mui'
5
+import { IconBtn, propsForModifiedValues } from './mui'
6
import { RestartAlt } from '@mui/icons-material'
7
import { Callback, onlyTruthy } from '../../src/cross'
8
@@ -35,7 +35,7 @@ export function ConfigForm<T=any>({ keys, form, saveOnChange, onSave, ...rest }:
35
},
36
save: saveOnChange ? false : {
37
onClick: save,
38
- ...modifiedProps(modified),
38
+ ...propsForModifiedValues(modified),
39
},
40
...formProps,
41
...rest,
admin/src/FileForm.ts
+4
-3
@@ -7,11 +7,12 @@ import { BoolField, DisplayField, Field, FieldProps, Form, MultiSelectField, Sel
7
} from '@hfs/mui-grid-form'
8
import { apiCall, UseApi } from './api'
9
import {
10
- basename, defaultPerms, formatBytes, formatTimestamp, isEqualLax, isWhoObject, newDialog, objSameKeys,
10
+ basename, defaultPerms, formatBytes, formatTimestamp, isWhoObject, newDialog, objSameKeys,
11
onlyTruthy, prefix, VfsPerms, wantArray, Who, WhoObject, matches, HTTP_MESSAGES, xlate, md, Callback,
12
useRequestRender, splitAt, IMAGE_FILEMASK
13
} from './misc'
14
-import { Btn, Flex, IconBtn, LinkBtn, modifiedProps, useBreakpoint, wikiLink } from './mui'
14
+import { isModifiedConfig } from './AccountForm'
15
+import { Btn, Flex, IconBtn, LinkBtn, propsForModifiedValues, useBreakpoint, wikiLink } from './mui'
16
import { reloadVfs, VfsNode } from './VfsPage'
17
import _ from 'lodash'
18
import FileField from './FileField'
@@ -134,7 +135,7 @@ export default function FileForm({ file, addToBar, statusApi, accounts, saved }:
135
],
136
onError: alertDialog,
137
save: {
137
- ...modifiedProps(!isEqualLax(values, rest)),
138
+ ...propsForModifiedValues(isModifiedConfig(values, rest)),
139
async onClick() {
140
const props = _.omit(values, ['ctime','mtime','size','id'])
141
;(props as any).masks ||= null // undefined cannot be serialized
admin/src/OptionsPage.ts
+2
-2
@@ -8,7 +8,7 @@ import { Link as RouterLink } from 'react-router-dom'
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, md, IMAGE_FILEMASK } from './misc'
11
-import { iconTooltip, InLink, LinkBtn, modifiedProps, wikiLink, useBreakpoint, NetmaskField, WildcardsSupported } from './mui'
11
+import { iconTooltip, InLink, LinkBtn, propsForModifiedValues, 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'
@@ -73,7 +73,7 @@ export default function OptionsPage() {
73
onError: alertDialog,
74
save: {
75
onClick: save,
76
- ...modifiedProps( Object.keys(changes).length>0),
76
+ ...propsForModifiedValues( Object.keys(changes).length>0),
77
},
78
barSx: { gap: 2 },
79
addToBar: [
admin/src/mui.ts
+3
-2
@@ -99,7 +99,8 @@ export function reloadBtn(onClick: any, props?: any) {
99
return h(IconBtn, { icon: Refresh, title: "Reload", onClick, ...props })
100
}
101
102
-export function modifiedProps(modified: boolean | undefined) {
102
+// modify look to convey that a form has been modified
103
+export function propsForModifiedValues(modified: boolean | undefined) {
104
return modified ? { sx: { outline: '2px solid' } } : undefined
105
}
106
@@ -145,7 +146,7 @@ export const Btn = forwardRef(({ icon, title, onClick, disabled, progress, link,
146
onClick = () => window.open(link)
147
const showLabel = useBreakpoint(labelFrom || 'xs')
148
const ref = useRefPass<HTMLButtonElement>(forwarded)
148
- const common = _.merge(modifiedProps(modified), {
149
+ const common = _.merge(propsForModifiedValues(modified), {
150
ref,
151
disabled,
152
'aria-hidden': disabled,
src/cross.ts
+6
-5
@@ -363,11 +363,12 @@ export function isTimestampString(v: unknown) {
363
return typeof v === 'string' && /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?Z*$/.test(v)
364
}
365
366
-export function isEqualLax(a: any,b: any): boolean {
367
- return a == b //eslint-disable-line
368
- || (a && b && typeof a === 'object' && typeof b === 'object'
369
- && Object.entries(a).every(([k, v]) => isEqualLax(v, b[k]))
370
- && Object.entries(b).every(([k, v]) => k in a || isEqualLax(v, a[k])) )
366
+export function isEqualLax(a: any,b: any, overrideRule?: (a: any, b: any) => boolean | undefined): boolean {
367
+ return overrideRule?.(a, b) ?? (
368
+ a == b || a && b && typeof a === 'object' && typeof b === 'object'
369
+ && Object.entries(a).every(([k, v]) => isEqualLax(v, b[k], overrideRule))
370
+ && Object.entries(b).every(([k, v]) => k in a /*already checked*/ || isEqualLax(v, a[k], overrideRule))
371
+ )
372
}
373
374
export function xlate(input: any, table: Record<string, any>) {
src/listen.ts
+1
-1
@@ -179,7 +179,7 @@ function renderHost(host: string) {
179
interface StartServer { port: number, host?:string }
180
export function startServer(srv: typeof httpSrv, { port, host }: StartServer) {
181
return new Promise<number>(async resolve => {
182
- if (!srv) return 0
182
+ if (!srv) return resolve(0)
183
try {
184
if (port === PORT_DISABLED || !host && !await testIpV4()) // !host means ipV4+6, and if v4 port alone is busy we won't be notified of the failure, so we'll first test it on its own
185
return resolve(0)