Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend Undo/Redo functionality to LO #8819

Merged
merged 5 commits into from
Nov 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## Next

* Loadout Optimizer now has Undo/Redo buttons covering all configuration options.

## 7.42.3 <span class="changelog-date">(2022-11-10)</span>

* Telesto has been reprimanded.
Expand Down
11 changes: 11 additions & 0 deletions src/app/loadout-builder/LoadoutBuilder.m.scss
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,17 @@
}
}

.undoRedo {
display: flex;
flex-direction: row;
margin: 10px 0;

> * {
flex: 1;
}
@include horizontal-space-children(4px);
}

.speedReport {
font-size: 11px;
user-select: text;
Expand Down
1 change: 1 addition & 0 deletions src/app/loadout-builder/LoadoutBuilder.m.scss.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 21 additions & 1 deletion src/app/loadout-builder/LoadoutBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { showNotification } from 'app/notifications/notifications';
import { armorStats } from 'app/search/d2-known-values';
import { searchFilterSelector } from 'app/search/search-filter';
import { useSetSetting } from 'app/settings/hooks';
import { AppIcon, refreshIcon } from 'app/shell/icons';
import { AppIcon, redoIcon, refreshIcon, undoIcon } from 'app/shell/icons';
import { querySelector, useIsPhonePortrait } from 'app/shell/selectors';
import { RootState } from 'app/store/types';
import { compareBy } from 'app/utils/comparators';
Expand Down Expand Up @@ -164,6 +164,8 @@ export default memo(function LoadoutBuilder({
statFilters,
modPicker,
compareSet,
canRedo,
canUndo,
},
lbDispatch,
] = useLbState(stores, defs, preloadedLoadout, initialClassType, initialLoadoutParameters);
Expand Down Expand Up @@ -363,6 +365,24 @@ export default memo(function LoadoutBuilder({
</ol>
</div>
)}
<div className={styles.undoRedo}>
<button
className="dim-button"
onClick={() => lbDispatch({ type: 'undo' })}
type="button"
disabled={!canUndo}
>
<AppIcon icon={undoIcon} /> {t('Loadouts.Undo')}
</button>
<button
className="dim-button"
onClick={() => lbDispatch({ type: 'redo' })}
type="button"
disabled={!canRedo}
>
<AppIcon icon={redoIcon} /> {t('Loadouts.Redo')}
</button>
</div>
<TierSelect
stats={statFilters}
statRangesFiltered={result?.statRangesFiltered}
Expand Down
8 changes: 4 additions & 4 deletions src/app/loadout-builder/filter/LockArmorAndPerks.m.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@import '../../variables';

.itemGrid {
display: grid;
grid-template-columns: repeat(auto-fill, var(--item-size));
Expand All @@ -24,11 +26,9 @@
flex-direction: row;
> * {
flex: 1;
margin-right: 4px;
&:last-child {
margin-right: 0;
}
}

@include horizontal-space-children(4px);
}

// TODO: dedupe this
Expand Down
141 changes: 105 additions & 36 deletions src/app/loadout-builder/loadout-builder-reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ import { showNotification } from 'app/notifications/notifications';
import { armor2PlugCategoryHashesByName } from 'app/search/d2-known-values';
import { emptyObject } from 'app/utils/empty';
import { getDefaultAbilityChoiceHash, getSocketsByCategoryHashes } from 'app/utils/socket-utils';
import { useHistory } from 'app/utils/undo-redo-history';
import { DestinyClass } from 'bungie-api-ts/destiny2';
import { BucketHashes, SocketCategoryHashes } from 'data/d2/generated-enums';
import _ from 'lodash';
import { useReducer } from 'react';
import { useCallback, useMemo, useReducer } from 'react';
import { useSelector } from 'react-redux';
import { statFiltersFromLoadoutParamaters, statOrderFromLoadoutParameters } from './loadout-params';
import {
Expand All @@ -40,7 +41,15 @@ import {
StatFilters,
} from './types';

export interface LoadoutBuilderState {
interface LoadoutBuilderUI {
modPicker: {
open: boolean;
plugCategoryHashWhitelist?: number[];
};
compareSet?: ArmorSet;
}

interface LoadoutBuilderConfiguration {
loadoutParameters: LoadoutParameters;
// TODO: also fold statOrder, statFilters into loadoutParameters
statOrder: ArmorStatHashes[]; // stat hashes, including disabled stats
Expand All @@ -49,13 +58,10 @@ export interface LoadoutBuilderState {
excludedItems: ExcludedItems;
selectedStoreId?: string;
subclass?: ResolvedLoadoutItem;
modPicker: {
open: boolean;
plugCategoryHashWhitelist?: number[];
};
compareSet?: ArmorSet;
}

export type LoadoutBuilderState = LoadoutBuilderUI & LoadoutBuilderConfiguration;

export function warnMissingClass(classType: DestinyClass, defs: D2ManifestDefinitions) {
const missingClassName = Object.values(defs.Class).find((c) => c.classType === classType)!
.displayProperties.name;
Expand All @@ -67,7 +73,7 @@ export function warnMissingClass(classType: DestinyClass, defs: D2ManifestDefini
});
}

const lbStateInit = ({
const lbConfigInit = ({
stores,
defs,
preloadedLoadout,
Expand All @@ -83,7 +89,7 @@ const lbStateInit = ({
initialLoadoutParameters: LoadoutParameters | undefined;
savedLoadoutBuilderParameters: LoadoutParameters;
savedStatConstraintsPerClass: { [classType: number]: StatConstraint[] };
}): LoadoutBuilderState => {
}): LoadoutBuilderConfiguration => {
const pinnedItems: PinnedItems = {};

// Preloaded loadouts from the "Optimize Armor" button take priority
Expand Down Expand Up @@ -172,20 +178,17 @@ const lbStateInit = ({
statFilters,
subclass,
selectedStoreId,
modPicker: {
open: false,
},
};
};

export type LoadoutBuilderAction =
type LoadoutBuilderConfigAction =
| {
type: 'changeCharacter';
store: DimStore;
savedStatConstraintsByClass: { [classType: number]: StatConstraint[] };
}
| { type: 'statFiltersChanged'; statFilters: LoadoutBuilderState['statFilters'] }
| { type: 'sortOrderChanged'; sortOrder: LoadoutBuilderState['statOrder'] }
| { type: 'statFiltersChanged'; statFilters: LoadoutBuilderConfiguration['statFilters'] }
| { type: 'sortOrderChanged'; sortOrder: LoadoutBuilderConfiguration['statOrder'] }
| {
type: 'assumeArmorMasterworkChanged';
assumeArmorMasterwork: AssumeArmorMasterwork | undefined;
Expand All @@ -205,15 +208,45 @@ export type LoadoutBuilderAction =
| { type: 'updateSubclassSocketOverrides'; socketOverrides: { [socketIndex: number]: number } }
| { type: 'removeSingleSubclassSocketOverride'; plug: PluggableInventoryItemDefinition }
| { type: 'lockExotic'; lockedExoticHash: number }
| { type: 'removeLockedExotic' }
| { type: 'removeLockedExotic' };

type LoadoutBuilderUIAction =
| { type: 'openModPicker'; plugCategoryHashWhitelist?: number[] }
| { type: 'closeModPicker' }
| { type: 'openCompareDrawer'; set: ArmorSet }
| { type: 'closeCompareDrawer' };

export type LoadoutBuilderAction =
| LoadoutBuilderConfigAction
| LoadoutBuilderUIAction
| { type: 'undo' }
| { type: 'redo' };

function lbUIReducer(state: LoadoutBuilderUI, action: LoadoutBuilderUIAction) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neat, I like having these be split up.

A la Redux, if you have the reducers do default: return state, you can just call all of them for all actions and maybe simplify your combined reducer. But it's not that big a deal here.

switch (action.type) {
case 'openCompareDrawer':
return { ...state, compareSet: action.set };
case 'openModPicker':
return {
...state,
modPicker: {
open: true,
plugCategoryHashWhitelist: action.plugCategoryHashWhitelist,
},
};
case 'closeCompareDrawer':
return { ...state, compareSet: undefined };
case 'closeModPicker':
return { ...state, modPicker: { open: false } };
}
}

// TODO: Move more logic inside the reducer
function lbStateReducer(defs: D2ManifestDefinitions) {
return (state: LoadoutBuilderState, action: LoadoutBuilderAction): LoadoutBuilderState => {
function lbConfigReducer(defs: D2ManifestDefinitions) {
return (
state: LoadoutBuilderConfiguration,
action: LoadoutBuilderConfigAction
): LoadoutBuilderConfiguration => {
switch (action.type) {
case 'changeCharacter': {
let loadoutParameters = {
Expand Down Expand Up @@ -490,20 +523,6 @@ function lbStateReducer(defs: D2ManifestDefinitions) {
},
};
}
case 'openModPicker':
return {
...state,
modPicker: {
open: true,
plugCategoryHashWhitelist: action.plugCategoryHashWhitelist,
},
};
case 'closeModPicker':
return { ...state, modPicker: { open: false } };
case 'openCompareDrawer':
return { ...state, compareSet: action.set };
case 'closeCompareDrawer':
return { ...state, compareSet: undefined };
case 'autoStatModsChanged':
return {
...state,
Expand All @@ -525,17 +544,67 @@ export function useLbState(
) {
const savedLoadoutBuilderParameters = useSelector(savedLoadoutParametersSelector);
const savedStatConstraintsPerClass = useSelector(savedLoStatConstraintsByClassSelector);
return useReducer(
lbStateReducer(defs),
{

const {
state: lbConfState,
setState,
redo,
undo,
canRedo,
canUndo,
} = useHistory(
lbConfigInit({
stores,
defs,
preloadedLoadout,
initialClassType,
initialLoadoutParameters,
savedLoadoutBuilderParameters,
savedStatConstraintsPerClass,
})
);

const lbConfReducer = useMemo(() => lbConfigReducer(defs), [defs]);

const [lbUIState, lbUIDispatch] = useReducer(lbUIReducer, {
compareSet: undefined,
modPicker: { open: false },
});

const dispatch = useCallback(
(action: LoadoutBuilderAction) => {
switch (action.type) {
case 'undo':
undo();
lbUIDispatch({ type: 'closeCompareDrawer' });
lbUIDispatch({ type: 'closeModPicker' });
break;
case 'redo':
redo();
lbUIDispatch({ type: 'closeCompareDrawer' });
lbUIDispatch({ type: 'closeModPicker' });
break;
case 'openCompareDrawer':
case 'closeCompareDrawer':
case 'openModPicker':
case 'closeModPicker':
lbUIDispatch(action);
break;
default:
setState((oldState) => lbConfReducer(oldState, action));
break;
}
},
lbStateInit
[lbConfReducer, redo, setState, undo]
);

return [
{
...lbConfState,
...lbUIState,
canUndo,
canRedo,
},
dispatch,
] as const;
}
12 changes: 9 additions & 3 deletions src/app/loadout-drawer/LoadoutDrawer2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { addIcon, AppIcon } from 'app/shell/icons';
import { useThunkDispatch } from 'app/store/thunk-dispatch';
import { useEventBusListener } from 'app/utils/hooks';
import { itemCanBeInLoadout } from 'app/utils/item-utils';
import { useHistory } from 'app/utils/undo-redo-history';
import { DestinyClass } from 'bungie-api-ts/destiny2';
import { BucketHashes } from 'data/d2/generated-enums';
import produce from 'immer';
Expand All @@ -37,7 +38,6 @@ import {
setName,
setNotes,
} from './loadout-drawer-reducer';
import { useLoadoutEditHistory } from './loadout-edit-history';
import { addItem$ } from './loadout-events';
import { Loadout, ResolvedLoadoutItem } from './loadout-types';
import { createSubclassDefaultSocketOverrides, findSameLoadoutItemIndex } from './loadout-utils';
Expand Down Expand Up @@ -74,8 +74,14 @@ export default function LoadoutDrawer2({
const defs = useDefinitions()!;
const stores = useSelector(storesSelector);
const [showingItemPicker, setShowingItemPicker] = useState(false);
const { loadout, setLoadout, undo, redo, canUndo, canRedo } =
useLoadoutEditHistory(initialLoadout);
const {
state: loadout,
setState: setLoadout,
undo,
redo,
canUndo,
canRedo,
} = useHistory(initialLoadout);
const apiPermissionGranted = useSelector(apiPermissionGrantedSelector);

function withUpdater<T extends unknown[]>(fn: (...args: T) => LoadoutUpdateFunction) {
Expand Down
Loading