Skip to content

Commit

Permalink
frontend: PluginSettings: Rework plugin settings local storage usage
Browse files Browse the repository at this point in the history
Signed-off-by: Vincent T <[email protected]>
  • Loading branch information
vyncent-t committed Dec 2, 2024
1 parent 82eb285 commit 5c99964
Show file tree
Hide file tree
Showing 9 changed files with 187 additions and 100 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Meta, StoryFn } from '@storybook/react';
import { PluginInfo } from '../../../plugin/pluginsSlice';
import { TestContext } from '../../../test';
import { PluginSettingsPure, PluginSettingsPureProps } from './PluginSettings';

Expand Down Expand Up @@ -40,6 +41,17 @@ function createDemoData(arrSize: number, useHomepage?: boolean) {
return pluginArr;
}

/**
* create demo data for pluginsEnabledList
*/
function createDemoEnabledList(arr: PluginInfo[]): Record<string, boolean> {
const enabledList = arr.reduce((acc, p) => {
acc[p.name] = !!p.isEnabled;
return acc;
}, {} as Record<string, boolean>);
return enabledList;
}

/**
* Creation of data arrays ranging from 0 to 50 to demo state of empty, few, many, and large numbers of data objects.
* NOTE: The numbers used are up to the users preference.
Expand All @@ -55,6 +67,7 @@ const demoEmpty = createDemoData(0);
export const FewItems = Template.bind({});
FewItems.args = {
plugins: demoFew,
pluginsEnabledList: createDemoEnabledList(demoFew),
onSave: plugins => {
console.log('demo few', plugins);
},
Expand All @@ -63,12 +76,14 @@ FewItems.args = {
export const Empty = Template.bind({});
Empty.args = {
plugins: demoEmpty,
pluginsEnabledList: createDemoEnabledList(demoEmpty),
};

/** NOTE: The save button will load by default on plugin page regardless of data */
export const DefaultSaveEnable = Template.bind({});
DefaultSaveEnable.args = {
plugins: demoFewSaveEnable,
pluginsEnabledList: createDemoEnabledList(demoFewSaveEnable),
onSave: plugins => {
console.log('demo few', plugins);
},
Expand All @@ -78,6 +93,7 @@ DefaultSaveEnable.args = {
export const ManyItems = Template.bind({});
ManyItems.args = {
plugins: demoMany,
pluginsEnabledList: createDemoEnabledList(demoMany),
onSave: plugins => {
console.log('demo many', plugins);
},
Expand All @@ -86,6 +102,7 @@ ManyItems.args = {
export const MoreItems = Template.bind({});
MoreItems.args = {
plugins: demoMore,
pluginsEnabledList: createDemoEnabledList(demoMore),
onSave: plugins => {
console.log('demo more', plugins);
},
Expand All @@ -94,6 +111,7 @@ MoreItems.args = {
export const EmptyHomepageItems = Template.bind({});
EmptyHomepageItems.args = {
plugins: demoHomepageEmpty,
pluginsEnabledList: createDemoEnabledList(demoHomepageEmpty),
onSave: (plugins: any) => {
console.log('Empty Homepage', plugins);
},
Expand Down
166 changes: 109 additions & 57 deletions frontend/src/components/App/PluginSettings/PluginSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Link from '@mui/material/Link';
import { MRT_Row } from 'material-react-table';
import { useEffect, useState } from 'react';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import helpers from '../../../helpers';
import { useFilterFunc } from '../../../lib/util';
import { PluginInfo, reloadPage, setPluginSettings } from '../../../plugin/pluginsSlice';
import { PluginInfo, reloadPage, setEnablePlugin } from '../../../plugin/pluginsSlice';
import { useTypedSelector } from '../../../redux/reducers/reducers';
import { Link as HeadlampLink, SectionBox, Table } from '../../common';
import SectionFilterHeader from '../../common/SectionFilterHeader';
Expand All @@ -24,6 +24,7 @@ import SectionFilterHeader from '../../common/SectionFilterHeader';
*/
export interface PluginSettingsPureProps {
plugins: PluginInfo[];
pluginsEnabledList: Record<string, boolean>;
onSave: (plugins: PluginInfo[]) => void;
saveAlwaysEnable?: boolean;
}
Expand Down Expand Up @@ -92,20 +93,13 @@ const EnableSwitch = (props: SwitchProps) => {

/** PluginSettingsPure is the main component to where we render the plugin data. */
export function PluginSettingsPure(props: PluginSettingsPureProps) {
const dispatch = useDispatch();

const { t } = useTranslation(['translation']);

/** Plugin arr to be rendered to the page from prop data */
const pluginArr: any = props.plugins ? props.plugins : [];

/** enableSave state enables the save button when changes are made to the plugin list */
const [enableSave, setEnableSave] = useState(false);

/**
* pluginChanges state is the array of plugin data and any current changes made by the user to a plugin's "Enable" field via toggler.
* The name and origin fields are split for consistency.
*/
const [pluginChanges, setPluginChanges] = useState(() =>
pluginArr.map((plugin: PluginInfo) => {
const [pluginArr, setPluginArr] = useState<PluginInfo[]>(
props.plugins.map((plugin: PluginInfo) => {
const [author, name] = plugin.name.includes('@')
? plugin.name.split(/\/(.+)/)
: [null, plugin.name];
Expand All @@ -119,60 +113,58 @@ export function PluginSettingsPure(props: PluginSettingsPureProps) {
);

/**
* useEffect to control the rendering of the save button.
* By default, the enableSave is set to false.
* If props.plugins matches pluginChanges enableSave is set to false, disabling the save button.
* pendingPluginsEnabled is either from the local storage or the prop data
*/
useEffect(() => {
/** This matcher function compares the fields of name and isEnabled of each object in props.plugins to each object in pluginChanges */
function matcher(objA: PluginInfo, objB: PluginInfo) {
return objA.name === objB.name && objA.isEnabled === objB.isEnabled;
}

/**
* arrayComp returns true if each object in both arrays are identical by name and isEnabled.
* If both arrays are identical in this scope, then no changes need to be saved.
* If they do not match, there are changes in the pluginChanges array that can be saved and thus enableSave should be enabled.
*/
const arrayComp = props.plugins.every((val, key) => matcher(val, pluginChanges[key]));
const [pendingPluginsEnabled, setPendingPluginsEnabled] = useState<Record<string, boolean>>(
props.pluginsEnabledList
);

/** For storybook usage, determines if the save button should be enabled by default */
if (props.saveAlwaysEnable) {
setEnableSave(true);
} else {
if (arrayComp) {
setEnableSave(false);
}
if (!arrayComp) {
setEnableSave(true);
}
}
}, [pluginChanges]);
/**
* useEffect / useMemo to update the pluginArr state with the prop data
*/
const enableSave = useMemo(() => {
return !pluginArr.every((plugin: { name: string }) => {
// if the user selected state is the same as saved
return (
Boolean(props.pluginsEnabledList[plugin.name]) ===
Boolean(pendingPluginsEnabled[plugin.name])
);
});
}, [pendingPluginsEnabled, pluginArr, props.pluginsEnabledList]);

/**
* onSaveButton function to be called once the user clicks the Save button.
* This function then takes the current state of the pluginChanges array and inputs it to the onSave prop function.
*/
function onSaveButtonHandler() {
props.onSave(pluginChanges);
dispatch(setEnablePlugin(pendingPluginsEnabled));
dispatch(reloadPage());
}

/**
* On change function handler to control the enableSave state and update the pluginChanges state.
* This function is called on every plugin toggle action and recreates the state for pluginChanges.
* On change function handler to control the enableSave state and update the pluginArr state.
* This function is called on every plugin toggle action and recreates the state for pluginArr.
* Once the user clicks a toggle, the Save button is also rendered via setEnableSave.
*/
function switchChangeHanlder(plug: { name: any }) {
function switchChangeHandler(plug: { name: any }) {
const plugName = plug.name;

setPluginChanges((currentInfo: any[]) =>
currentInfo.map((p: { name: any; isEnabled: any }) => {
setPluginArr([
...pluginArr.map(p => {
if (p.name === plugName) {
return { ...p, isEnabled: !p.isEnabled };
return {
...p,
isEnabled: !p.isEnabled,
};
}
return p;
})
);
}),
]);

setPendingPluginsEnabled({
...pendingPluginsEnabled,
[plugName]: !pendingPluginsEnabled[plugName],
});
}

return (
Expand Down Expand Up @@ -235,7 +227,9 @@ export function PluginSettingsPure(props: PluginSettingsPureProps) {
if (plugin.isCompatible === false) {
return t('translation|Incompatible');
}
return plugin.isEnabled ? t('translation|Enabled') : t('translation|Disabled');
return pendingPluginsEnabled[plugin.name]
? t('translation|Enabled')
: t('translation|Disabled');
},
},
{
Expand All @@ -247,8 +241,8 @@ export function PluginSettingsPure(props: PluginSettingsPureProps) {
return (
<EnableSwitch
aria-label={`Toggle ${plugin.name}`}
checked={plugin.isEnabled}
onChange={() => switchChangeHanlder(plugin)}
checked={pendingPluginsEnabled[plugin.name]}
onChange={() => switchChangeHandler(plugin)}
color="primary"
name={plugin.name}
/>
Expand All @@ -260,7 +254,7 @@ export function PluginSettingsPure(props: PluginSettingsPureProps) {
]
// remove the enable column if we're not in app mode
.filter(el => !(el.header === t('translation|Enable') && !helpers.isElectron()))}
data={pluginChanges}
data={pluginArr}
filterFunction={useFilterFunc<PluginInfo>(['.name'])}
/>
</SectionBox>
Expand All @@ -284,13 +278,71 @@ export function PluginSettingsPure(props: PluginSettingsPureProps) {
export default function PluginSettings() {
const dispatch = useDispatch();

const pluginSettings = useTypedSelector(state => state.plugins.pluginSettings);
const pluginData = useTypedSelector(state => state.plugins.pluginData);

/**
* We need to search for the local storage before using it in the slice later
*/
const oldLocalEnabledList = localStorage.getItem('headlampPluginSettings');
const localEnabledList = localStorage.getItem('enabledPluginsList');

let pluginsEnabledList;

/**
* WIP making the old settings compatible with the new settings
*/

if (oldLocalEnabledList) {
const oldSettings = JSON.parse(oldLocalEnabledList);

const newSettings = oldSettings.map((setting: { name: string; isEnabled: boolean }) => {
return {
name: setting.name,
isEnabled: setting.isEnabled,
};
});

pluginsEnabledList = newSettings.reduce(
(acc: Record<string, boolean>, p: { name: string; isEnabled: boolean }) => {
acc[p.name] = !!p.isEnabled;
return acc;
},
{} as Record<string, boolean>
);

localStorage.setItem('enabledPluginsList', JSON.stringify(pluginsEnabledList));
localStorage.removeItem('headlampPluginSettings');

dispatch(setEnablePlugin(pluginsEnabledList));

dispatch(reloadPage());
} else {
/**
* If `localEnabledList` exists, parse it and assign it to `pluginsEnabledList`.
* This indicates that previous plugin settings have been saved and can be used.
*
* If `localEnabledList` does not exist, it means the settings are not initialized
* and no previous plugin settings have been saved. In this case, default the plugins
* to being disabled to allow users to turn on their desired plugins.
*/
if (localEnabledList) {
pluginsEnabledList = JSON.parse(localEnabledList) as Record<string, boolean>;
} else {
pluginsEnabledList = pluginData.reduce((acc, p) => {
acc[p.name] = !!p.isEnabled;
return acc;
}, {} as Record<string, boolean>);

dispatch(setEnablePlugin(pluginsEnabledList));
dispatch(reloadPage());
}
}

return (
<PluginSettingsPure
plugins={pluginSettings}
onSave={plugins => {
dispatch(setPluginSettings(plugins));
plugins={pluginData}
pluginsEnabledList={pluginsEnabledList}
onSave={() => {
dispatch(reloadPage());
}}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const PluginSettingsDetailsInitializer = (props: { plugin: PluginInfo }) => {
};

export default function PluginSettingsDetails() {
const pluginSettings = useTypedSelector(state => state.plugins.pluginSettings);
const pluginSettings = useTypedSelector(state => state.plugins.pluginData);
const { name } = useParams<{ name: string }>();

const plugin = useMemo(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -813,19 +813,5 @@
</div>
</div>
</div>
<div
class="MuiBox-root css-ova7y8"
>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium MuiButton-colorPrimary MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium MuiButton-colorPrimary css-wlshgd-MuiButtonBase-root-MuiButton-root"
tabindex="0"
type="button"
>
Save & Apply
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/>
</button>
</div>
</div>
</body>
8 changes: 5 additions & 3 deletions frontend/src/plugin/Plugins.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import helpers from '../helpers';
import { UI_INITIALIZE_PLUGIN_VIEWS } from '../redux/actions/actions';
import { useTypedSelector } from '../redux/reducers/reducers';
import { fetchAndExecutePlugins } from './index';
import { pluginsLoaded, setPluginSettings } from './pluginsSlice';
import { pluginsLoaded, setPluginData } from './pluginsSlice';

/**
* For discovering and executing plugins.
Expand All @@ -26,16 +26,18 @@ export default function Plugins() {
const history = useHistory();
const { t } = useTranslation();

const settingsPlugins = useTypedSelector(state => state.plugins.pluginSettings);
const settingsPlugins = useTypedSelector(state => state.plugins.pluginData);
const enabledPlugins = useTypedSelector(state => state.plugins.enabledPlugins);

// only run on first load
useEffect(() => {
dispatch({ type: UI_INITIALIZE_PLUGIN_VIEWS });

fetchAndExecutePlugins(
settingsPlugins,
enabledPlugins,
updatedSettingsPackages => {
dispatch(setPluginSettings(updatedSettingsPackages));
dispatch(setPluginData(updatedSettingsPackages));
},
incompatiblePlugins => {
const pluginList = Object.values(incompatiblePlugins)
Expand Down
Loading

0 comments on commit 5c99964

Please sign in to comment.