From 48b8932a1d40fa17fbbc6776503fc1df502c5cfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Sun, 2 Jul 2023 15:38:52 +0100 Subject: [PATCH] fix: settings dirty state tracking + toast on save/discard (#38) --- src/features/settings/Settings.tsx | 38 ++++++++++++++++++++--- src/features/settings/settingsSelector.ts | 2 ++ src/features/settings/settingsSlice.ts | 20 ++++++++++-- 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/src/features/settings/Settings.tsx b/src/features/settings/Settings.tsx index a59155b..6e3ab6a 100644 --- a/src/features/settings/Settings.tsx +++ b/src/features/settings/Settings.tsx @@ -1,4 +1,5 @@ import { + chakra, Box, Button, Center, @@ -20,13 +21,15 @@ import { Text, VStack, useBreakpointValue, + useToast, } from '@chakra-ui/react'; import { PanelQMCv2Key } from './panels/PanelQMCv2Key'; import { useState } from 'react'; import { MdExpandMore, MdMenu, MdOutlineSettingsBackupRestore } from 'react-icons/md'; -import { useAppDispatch } from '~/hooks'; +import { useAppDispatch, useAppSelector } from '~/hooks'; import { commitStagingChange, discardStagingChanges } from './settingsSlice'; import { PanelKWMv2Key } from './panels/PanelKWMv2Key'; +import { selectIsSettingsNotSaved } from './settingsSelector'; const TABS: { name: string; Tab: () => JSX.Element }[] = [ { name: 'QMCv2 密钥', Tab: PanelQMCv2Key }, @@ -38,6 +41,7 @@ const TABS: { name: string; Tab: () => JSX.Element }[] = [ ]; export function Settings() { + const toast = useToast(); const dispatch = useAppDispatch(); const isLargeWidthDevice = useBreakpointValue({ @@ -49,8 +53,25 @@ export function Settings() { const handleTabChange = (idx: number) => { setTabIndex(idx); }; - const handleResetSettings = () => dispatch(discardStagingChanges()); - const handleApplySettings = () => dispatch(commitStagingChange()); + const handleResetSettings = () => { + dispatch(discardStagingChanges()); + + toast({ + status: 'info', + title: '未储存的设定已舍弃', + description: '已还原到更改前的状态。', + isClosable: true, + }); + }; + const handleApplySettings = () => { + dispatch(commitStagingChange()); + toast({ + status: 'success', + title: '设定已应用', + isClosable: true, + }); + }; + const isSettingsNotSaved = useAppSelector(selectIsSettingsNotSaved); return ( @@ -104,7 +125,16 @@ export function Settings() {
- 设置会在保存后生效。 + {isSettingsNotSaved ? ( + + 有未储存的更改{' '} + + 设定将在保存后生效 + + + ) : ( + 设定将在保存后生效 + )}
diff --git a/src/features/settings/settingsSelector.ts b/src/features/settings/settingsSelector.ts index 7e2a038..6b695e5 100644 --- a/src/features/settings/settingsSelector.ts +++ b/src/features/settings/settingsSelector.ts @@ -4,6 +4,8 @@ import { closestByLevenshtein } from '~/util/levenshtein'; import { hasOwn } from '~/util/objects'; import { kwm2StagingToProductionKey } from './keyFormats'; +export const selectIsSettingsNotSaved = (state: RootState) => state.settings.dirty; + export const selectStagingQMCv2Settings = (state: RootState) => state.settings.staging.qmc2; export const selectFinalQMCv2Settings = (state: RootState) => state.settings.production.qmc2; diff --git a/src/features/settings/settingsSlice.ts b/src/features/settings/settingsSlice.ts index e5722e2..10bbaf1 100644 --- a/src/features/settings/settingsSlice.ts +++ b/src/features/settings/settingsSlice.ts @@ -37,16 +37,18 @@ export interface ProductionSettings { } export interface SettingsState { + dirty: boolean; staging: StagingSettings; production: ProductionSettings; } const initialState: SettingsState = { + dirty: false, staging: { - qmc2: { allowFuzzyNameSearch: false, keys: [] }, + qmc2: { allowFuzzyNameSearch: true, keys: [] }, kwm2: { keys: [] }, }, production: { - qmc2: { allowFuzzyNameSearch: false, keys: {} }, + qmc2: { allowFuzzyNameSearch: true, keys: {} }, kwm2: { keys: {} }, }, }; @@ -77,6 +79,7 @@ export const settingsSlice = createSlice({ reducers: { setProductionChanges: (_state, { payload }: PayloadAction) => { return { + dirty: false, production: payload, staging: productionToStaging(payload), }; @@ -84,14 +87,17 @@ export const settingsSlice = createSlice({ // qmc2AddKey(state) { state.staging.qmc2.keys.push({ id: nanoid(), name: '', ekey: '' }); + state.dirty = true; }, qmc2ImportKeys(state, { payload }: PayloadAction[]>) { const newItems = payload.map((item) => ({ id: nanoid(), ...item })); state.staging.qmc2.keys.push(...newItems); + state.dirty = true; }, qmc2DeleteKey(state, { payload: { id } }: PayloadAction<{ id: string }>) { const qmc2 = state.staging.qmc2; qmc2.keys = qmc2.keys.filter((item) => item.id !== id); + state.dirty = true; }, qmc2UpdateKey( state, @@ -100,25 +106,31 @@ export const settingsSlice = createSlice({ const keyItem = state.staging.qmc2.keys.find((item) => item.id === id); if (keyItem) { keyItem[field] = value; + state.dirty = true; } }, qmc2ClearKeys(state) { state.staging.qmc2.keys = []; + state.dirty = true; }, qmc2AllowFuzzyNameSearch(state, { payload: { enable } }: PayloadAction<{ enable: boolean }>) { state.staging.qmc2.allowFuzzyNameSearch = enable; + state.dirty = true; }, // TODO: reuse the logic somehow? kwm2AddKey(state) { state.staging.kwm2.keys.push({ id: nanoid(), ekey: '', quality: '', rid: '' }); + state.dirty = true; }, kwm2ImportKeys(state, { payload }: PayloadAction[]>) { const newItems = payload.map((item) => ({ id: nanoid(), ...item })); state.staging.kwm2.keys.push(...newItems); + state.dirty = true; }, kwm2DeleteKey(state, { payload: { id } }: PayloadAction<{ id: string }>) { const kwm2 = state.staging.kwm2; kwm2.keys = kwm2.keys.filter((item) => item.id !== id); + state.dirty = true; }, kwm2UpdateKey( state, @@ -127,18 +139,22 @@ export const settingsSlice = createSlice({ const keyItem = state.staging.kwm2.keys.find((item) => item.id === id); if (keyItem) { keyItem[field] = value; + state.dirty = true; } }, kwm2ClearKeys(state) { state.staging.kwm2.keys = []; + state.dirty = true; }, // discardStagingChanges: (state) => { + state.dirty = false; state.staging = productionToStaging(state.production); }, commitStagingChange: (state) => { const production = stagingToProduction(state.staging); return { + dirty: false, // Sync back to staging staging: productionToStaging(production), production,