Skip to content

Commit

Permalink
Implement get set reset pattern for useSetting and useWebViewState (#697
Browse files Browse the repository at this point in the history
)
  • Loading branch information
rolfheij-sil authored Jan 16, 2024
2 parents 4f18f59 + 3826664 commit ff2fcc9
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 55 deletions.
46 changes: 36 additions & 10 deletions lib/papi-dts/papi.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4166,17 +4166,33 @@ declare module 'extension-host/extension-types/extension-manifest.model' {
}
declare module 'shared/services/settings.service' {
import { Unsubscriber } from 'shared/utils/papi-util';
import { SettingTypes } from 'papi-shared-types';
import { SettingNames, SettingTypes } from 'papi-shared-types';
/** Event to set or update a setting */
export type UpdateSettingEvent<SettingName extends SettingNames> = {
type: 'update-setting';
setting: SettingTypes[SettingName];
};
/** Event to remove a setting */
export type ResetSettingEvent = {
type: 'reset-setting';
};
/** All supported setting events */
export type SettingEvent<SettingName extends SettingNames> =
| UpdateSettingEvent<SettingName>
| ResetSettingEvent;
/**
* Retrieves the value of the specified setting
*
* @param key The string id of the setting for which the value is being retrieved
* @param defaultSetting The default value used for the setting if no value is available for the key
* @returns The value of the specified setting, parsed to an object. Returns `undefined` if setting
* is not present or no value is available
* @throws When defaultSetting is required but not provided
*/
const getSetting: <SettingName extends keyof SettingTypes>(
key: SettingName,
) => SettingTypes[SettingName] | undefined;
defaultSetting: SettingTypes[SettingName],
) => SettingTypes[SettingName];
/**
* Sets the value of the specified setting
*
Expand All @@ -4186,8 +4202,14 @@ declare module 'shared/services/settings.service' {
*/
const setSetting: <SettingName extends keyof SettingTypes>(
key: SettingName,
newSetting: SettingTypes[SettingName] | undefined,
newSetting: SettingTypes[SettingName],
) => void;
/**
* Removes the setting from memory
*
* @param key The string id of the setting for which the value is being removed
*/
const resetSetting: <SettingName extends keyof SettingTypes>(key: SettingName) => void;
/**
* Subscribes to updates of the specified setting. Whenever the value of the setting changes, the
* callback function is executed.
Expand All @@ -4198,11 +4220,12 @@ declare module 'shared/services/settings.service' {
*/
const subscribeToSetting: <SettingName extends keyof SettingTypes>(
key: SettingName,
callback: (newSetting: SettingTypes[SettingName] | undefined) => void,
callback: (newSetting: SettingEvent<SettingName>) => void,
) => Unsubscriber;
export interface SettingsService {
get: typeof getSetting;
set: typeof setSetting;
reset: typeof resetSetting;
subscribe: typeof subscribeToSetting;
}
/**
Expand Down Expand Up @@ -4516,10 +4539,8 @@ declare module 'renderer/hooks/papi-hooks/use-data.hook' {
declare module 'renderer/hooks/papi-hooks/use-setting.hook' {
import { SettingTypes } from 'papi-shared-types';
/**
* Gets and sets a setting on the papi. Also notifies subscribers when the setting changes and gets
* updated when the setting is changed by others.
*
* Setting the value to `undefined` is the equivalent of deleting the setting.
* Gets, sets and resets a setting on the papi. Also notifies subscribers when the setting changes
* and gets updated when the setting is changed by others.
*
* @param key The string id that is used to store the setting in local storage
*
Expand All @@ -4530,16 +4551,20 @@ declare module 'renderer/hooks/papi-hooks/use-setting.hook' {
*
* WARNING: MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be
* updated every render
* @returns `[setting, setSetting]`
* @returns `[setting, setSetting, resetSetting]`
*
* - `setting`: The current state of the setting, either the defaultState or the stored state on the
* papi, if any
* - `setSetting`: Function that updates the setting to a new value
* - `resetSetting`: Function that removes the setting
*
* @throws When subscription callback function is called with an update that has an unexpected
* message type
*/
const useSetting: <SettingName extends keyof SettingTypes>(
key: SettingName,
defaultState: SettingTypes[SettingName],
) => [SettingTypes[SettingName], (newSetting: SettingTypes[SettingName]) => void];
) => [SettingTypes[SettingName], (newSetting: SettingTypes[SettingName]) => void, () => void];
export default useSetting;
}
declare module 'renderer/hooks/papi-hooks/use-project-data-provider.hook' {
Expand Down Expand Up @@ -5199,4 +5224,5 @@ declare module '@papi/core' {
} from 'shared/models/web-view.model';
export type { Unsubscriber, UnsubscriberAsync } from 'shared/utils/papi-util';
export type { IWebViewProvider } from 'shared/models/web-view-provider.model';
export type { SettingEvent } from 'shared/services/settings.service';
}
11 changes: 9 additions & 2 deletions src/renderer/components/platform-bible-toolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import useSetting from '@renderer/hooks/papi-hooks/use-setting.hook';
import { Toolbar, RefSelector, ScriptureReference } from 'papi-components';
import { Toolbar, RefSelector, ScriptureReference, Button } from 'papi-components';
import { handleMenuCommand } from './platform-bible-menu.commands';
import { handleMenuData } from './platform-bible-menu.data';

Expand All @@ -10,7 +10,7 @@ const defaultScrRef: ScriptureReference = {
};

export default function PlatformBibleToolbar() {
const [scrRef, setScrRef] = useSetting('platform.verseRef', defaultScrRef);
const [scrRef, setScrRef, resetScrRef] = useSetting('platform.verseRef', defaultScrRef);

const handleReferenceChanged = (newScrRef: ScriptureReference) => {
setScrRef(newScrRef);
Expand All @@ -19,6 +19,13 @@ export default function PlatformBibleToolbar() {
return (
<Toolbar className="toolbar" dataHandler={handleMenuData} commandHandler={handleMenuCommand}>
<RefSelector handleSubmit={handleReferenceChanged} scrRef={scrRef} />
<Button
onClick={() => {
resetScrRef();
}}
>
Reset
</Button>
</Toolbar>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,11 @@ export const loadRunBasicChecksTab = (savedTabInfo: SavedTabInfo): TabInfo => {
<RunBasicChecksTab
// #region Test a .NET data provider
currentProjectId="32664dc3288a28df2e2bb75ded887fc8f17a15fb"
currentScriptureReference={settingsService.get('platform.verseRef')}
currentScriptureReference={settingsService.get('platform.verseRef', {
bookNum: 1,
chapterNum: 1,
verseNum: 1,
})}
/>
),
};
Expand Down
52 changes: 29 additions & 23 deletions src/renderer/hooks/papi-hooks/use-setting.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ import settingsService from '@shared/services/settings.service';
import { SettingNames, SettingTypes } from 'papi-shared-types';

/**
* Gets and sets a setting on the papi. Also notifies subscribers when the setting changes and gets
* updated when the setting is changed by others.
*
* Setting the value to `undefined` is the equivalent of deleting the setting.
* Gets, sets and resets a setting on the papi. Also notifies subscribers when the setting changes
* and gets updated when the setting is changed by others.
*
* @param key The string id that is used to store the setting in local storage
*
Expand All @@ -17,50 +15,58 @@ import { SettingNames, SettingTypes } from 'papi-shared-types';
*
* WARNING: MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be
* updated every render
* @returns `[setting, setSetting]`
* @returns `[setting, setSetting, resetSetting]`
*
* - `setting`: The current state of the setting, either the defaultState or the stored state on the
* papi, if any
* - `setSetting`: Function that updates the setting to a new value
* - `resetSetting`: Function that removes the setting
*
* @throws When subscription callback function is called with an update that has an unexpected
* message type
*/
const useSetting = <SettingName extends SettingNames>(
key: SettingName,
defaultState: SettingTypes[SettingName],
): [SettingTypes[SettingName], (newSetting: SettingTypes[SettingName]) => void] => {
const [setting, setSettingInternal] = useState(() => {
const initialSetting = settingsService.get(key);
return initialSetting !== undefined ? initialSetting : defaultState;
});
): [SettingTypes[SettingName], (newSetting: SettingTypes[SettingName]) => void, () => void] => {
const [setting, setSettingInternal] = useState(settingsService.get(key, defaultState));

useEffect(() => {
const updateSettingFromService = (newSetting: SettingTypes[SettingName] | undefined) => {
if (newSetting !== undefined) {
setSettingInternal(newSetting);
} else {
setSettingInternal(defaultState);
}
const updateSettingFromService = (newSettingState: SettingTypes[SettingName]) => {
setSettingInternal(newSettingState);
};

const initialSetting = settingsService.get(key);
const initialSetting = settingsService.get(key, defaultState);
updateSettingFromService(initialSetting);

const unsubscriber = settingsService.subscribe(key, (newSetting) => {
updateSettingFromService(newSetting);
if (newSetting.type === 'update-setting') {
updateSettingFromService(newSetting.setting);
} else if (newSetting.type === 'reset-setting') {
setSettingInternal(defaultState);
} else {
throw new Error('Unexpected message type used for updating setting');
}
});

return () => {
unsubscriber();
};
}, [key, defaultState]);
}, [defaultState, key]);

const setSetting = useCallback(
(newSetting: SettingTypes[SettingName] | undefined) => {
(newSetting: SettingTypes[SettingName]) => {
settingsService.set(key, newSetting);
setSettingInternal(newSetting !== undefined ? newSetting : defaultState);
setSettingInternal(newSetting);
},
[key, defaultState],
[key],
);

return [setting, setSetting];
const resetSetting = useCallback(() => {
settingsService.reset(key);
setSettingInternal(defaultState);
}, [defaultState, key]);

return [setting, setSetting, resetSetting];
};
export default useSetting;
2 changes: 2 additions & 0 deletions src/shared/services/papi-core.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,5 @@ export type {
export type { Unsubscriber, UnsubscriberAsync } from '@shared/utils/papi-util';

export type { IWebViewProvider } from '@shared/models/web-view-provider.model';

export type { SettingEvent } from '@shared/services/settings.service';
77 changes: 58 additions & 19 deletions src/shared/services/settings.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,51 @@ import { Unsubscriber, deserialize, serialize } from '@shared/utils/papi-util';
import PapiEventEmitter from '@shared/models/papi-event-emitter.model';
import { SettingNames, SettingTypes } from 'papi-shared-types';

/** Event to set or update a setting */
export type UpdateSettingEvent<SettingName extends SettingNames> = {
type: 'update-setting';
setting: SettingTypes[SettingName];
};

/** Event to remove a setting */
export type ResetSettingEvent = {
type: 'reset-setting';
};

/** All supported setting events */
export type SettingEvent<SettingName extends SettingNames> =
| UpdateSettingEvent<SettingName>
| ResetSettingEvent;

/** All message subscriptions - emitters that emit an event each time a setting is updated */
const onDidUpdateSettingEmitters = new Map<
SettingNames,
PapiEventEmitter<SettingTypes[SettingNames] | undefined>
PapiEventEmitter<SettingEvent<SettingNames>>
>();

/**
* Retrieves the value of the specified setting
*
* @param key The string id of the setting for which the value is being retrieved
* @param defaultSetting The default value used for the setting if no value is available for the key
* @returns The value of the specified setting, parsed to an object. Returns `undefined` if setting
* is not present or no value is available
* @throws When defaultSetting is required but not provided
*/
const getSetting = <SettingName extends SettingNames>(
key: SettingName,
): SettingTypes[SettingName] | undefined => {
defaultSetting: SettingTypes[SettingName],
): SettingTypes[SettingName] => {
const settingString = localStorage.getItem(key);
// Null is used by the external API
// eslint-disable-next-line no-null/no-null
return settingString !== null ? deserialize(settingString) : undefined;
if (settingString !== null) {
return deserialize(settingString);
}
if (defaultSetting) {
return defaultSetting;
}
throw new Error(`No default value provided for setting '${key}'`);
};

/**
Expand All @@ -33,16 +58,31 @@ const getSetting = <SettingName extends SettingNames>(
*/
const setSetting = <SettingName extends SettingNames>(
key: SettingName,
newSetting: SettingTypes[SettingName] | undefined,
newSetting: SettingTypes[SettingName],
) => {
if (newSetting === undefined) localStorage.removeItem(key);
else localStorage.setItem(key, serialize(newSetting));
localStorage.setItem(key, serialize(newSetting));
// Assert type of the particular SettingName of the emitter.
// eslint-disable-next-line no-type-assertion/no-type-assertion
const emitter = onDidUpdateSettingEmitters.get(key) as
| PapiEventEmitter<SettingTypes[SettingName] | undefined>
| undefined;
emitter?.emit(newSetting);
const emitter = onDidUpdateSettingEmitters.get(key);
const setMessage: UpdateSettingEvent<SettingName> = {
setting: newSetting,
type: 'update-setting',
};
emitter?.emit(setMessage);
};

/**
* Removes the setting from memory
*
* @param key The string id of the setting for which the value is being removed
*/
const resetSetting = <SettingName extends SettingNames>(key: SettingName) => {
localStorage.removeItem(key);
// Assert type of the particular SettingName of the emitter.
// eslint-disable-next-line no-type-assertion/no-type-assertion
const emitter = onDidUpdateSettingEmitters.get(key);
const resetMessage: ResetSettingEvent = { type: 'reset-setting' };
emitter?.emit(resetMessage);
};

/**
Expand All @@ -55,21 +95,18 @@ const setSetting = <SettingName extends SettingNames>(
*/
const subscribeToSetting = <SettingName extends SettingNames>(
key: SettingName,
callback: (newSetting: SettingTypes[SettingName] | undefined) => void,
callback: (newSetting: SettingEvent<SettingName>) => void,
): Unsubscriber => {
// Assert type of the particular SettingName of the emitter.
// eslint-disable-next-line no-type-assertion/no-type-assertion
let emitter = onDidUpdateSettingEmitters.get(key) as
| PapiEventEmitter<SettingTypes[SettingName] | undefined>
| PapiEventEmitter<SettingEvent<SettingName>>
| undefined;
if (!emitter) {
emitter = new PapiEventEmitter<SettingTypes[SettingName] | undefined>();
onDidUpdateSettingEmitters.set(
key,
// Assert type of the general SettingTypes of the emitter.
// eslint-disable-next-line no-type-assertion/no-type-assertion
emitter as PapiEventEmitter<SettingTypes[SettingNames] | undefined>,
);
emitter = new PapiEventEmitter<SettingEvent<SettingName>>();
// Assert type of the general SettingNames of the emitter.
// eslint-disable-next-line no-type-assertion/no-type-assertion
onDidUpdateSettingEmitters.set(key, emitter as PapiEventEmitter<SettingEvent<SettingNames>>);
}
return emitter.subscribe(callback);
};
Expand All @@ -78,6 +115,7 @@ const subscribeToSetting = <SettingName extends SettingNames>(
export interface SettingsService {
get: typeof getSetting;
set: typeof setSetting;
reset: typeof resetSetting;
subscribe: typeof subscribeToSetting;
}

Expand All @@ -89,6 +127,7 @@ export interface SettingsService {
const settingsService: SettingsService = {
get: getSetting,
set: setSetting,
reset: resetSetting,
subscribe: subscribeToSetting,
};
export default settingsService;

0 comments on commit ff2fcc9

Please sign in to comment.