Skip to content

Commit

Permalink
Merge pull request #10456 from DestinyItemManager/search-type
Browse files Browse the repository at this point in the history
Allow saving loadout search history
  • Loading branch information
bhollis authored May 25, 2024
2 parents 828cf06 + f75d241 commit 53353dc
Show file tree
Hide file tree
Showing 18 changed files with 156 additions and 70 deletions.
4 changes: 3 additions & 1 deletion config/i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -1099,7 +1099,9 @@
"UsageCount": "# Used",
"Query": "Search",
"Link": "View and edit search history",
"Title": "Search History"
"Title": "Search History",
"Item": "Item Searches",
"Loadout": "Loadout Searches"
},
"Settings": {
"AutoLockTagged": "Sync item lock state with tag",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@
},
"dependencies": {
"@babel/runtime": "^7.24.5",
"@destinyitemmanager/dim-api-types": "^1.30.0",
"@destinyitemmanager/dim-api-types": "^1.31.0",
"@fortawesome/fontawesome-free": "^5.15.4",
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/react-fontawesome": "^0.2.1",
Expand Down
8 changes: 4 additions & 4 deletions pnpm-lock.yaml

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

12 changes: 10 additions & 2 deletions src/app/dim-api/basic-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
GlobalSettings,
ProfileResponse,
ProfileUpdateResult,
SearchType,
} from '@destinyitemmanager/dim-api-types';
import { DestinyAccount } from 'app/accounts/destiny-account';
import { createAction } from 'typesafe-actions';
Expand Down Expand Up @@ -40,16 +41,23 @@ export const trackTriumph = createAction('dim-api/TRACK_TRIUMPH')<{
}>();

/** Record that a search was used */
export const searchUsed = createAction('dim-api/SEARCH_USED')<string>();
export const searchUsed = createAction('dim-api/SEARCH_USED')<{
query: string;
type: SearchType;
}>();

/** Save or un-save a search */
export const saveSearch = createAction('dim-api/SAVE_SEARCH')<{
query: string;
saved: boolean;
type: SearchType;
}>();

/** Delete a saved search */
export const searchDeleted = createAction('dim-api/DELETE_SEARCH')<string>();
export const searchDeleted = createAction('dim-api/DELETE_SEARCH')<{
query: string;
type: SearchType;
}>();

/**
* This signals that we are about to flush the update queue.
Expand Down
25 changes: 17 additions & 8 deletions src/app/dim-api/reducer.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SearchType } from '@destinyitemmanager/dim-api-types';
import { DestinyAccount } from 'app/accounts/destiny-account';
import { setItemHashTag, setItemTag } from 'app/inventory/actions';
import { setSettingAction } from 'app/settings/actions';
Expand Down Expand Up @@ -643,13 +644,13 @@ describe('saveSearch', () => {
};
const updatedState = dimApi(
state,
saveSearch({ query: '(is:masterwork) (is:weapon)', saved: true }),
saveSearch({ query: '(is:masterwork) (is:weapon)', saved: true, type: SearchType.Item }),
currentAccount,
);

expect(updatedState.searches).toMatchObject({
[1]: [],
[2]: [{ query: 'is:masterwork is:weapon', saved: true }],
[2]: [{ query: 'is:masterwork is:weapon', saved: true, type: SearchType.Item }],
});
});

Expand All @@ -659,19 +660,19 @@ describe('saveSearch', () => {
};
let updatedState = dimApi(
state,
saveSearch({ query: '(is:masterwork) (is:weapon)', saved: true }),
saveSearch({ query: '(is:masterwork) (is:weapon)', saved: true, type: SearchType.Item }),
currentAccount,
);

updatedState = dimApi(
updatedState,
saveSearch({ query: '(is:masterwork) (is:weapon)', saved: false }),
saveSearch({ query: '(is:masterwork) (is:weapon)', saved: false, type: SearchType.Item }),
currentAccount,
);

expect(updatedState.searches).toMatchObject({
[1]: [],
[2]: [{ query: 'is:masterwork is:weapon', saved: false }],
[2]: [{ query: 'is:masterwork is:weapon', saved: false, type: SearchType.Item }],
});
});

Expand All @@ -681,7 +682,7 @@ describe('saveSearch', () => {
};
const updatedState = dimApi(
state,
saveSearch({ query: 'deepsight:incomplete', saved: true }),
saveSearch({ query: 'deepsight:incomplete', saved: true, type: SearchType.Item }),
currentAccount,
);
expect(updatedState.searches).toMatchObject({
Expand All @@ -695,12 +696,20 @@ describe('saveSearch', () => {
...initialState,
searches: {
[1]: [],
[2]: [{ usageCount: 1, lastUsage: 919191, saved: true, query: 'deepsight:incomplete' }],
[2]: [
{
usageCount: 1,
lastUsage: 919191,
saved: true,
query: 'deepsight:incomplete',
type: SearchType.Item,
},
],
},
};
const updatedState = dimApi(
state,
saveSearch({ query: 'deepsight:incomplete', saved: false }),
saveSearch({ query: 'deepsight:incomplete', saved: false, type: SearchType.Item }),
currentAccount,
);

Expand Down
43 changes: 34 additions & 9 deletions src/app/dim-api/reducer.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import {
CustomStatWeights,
defaultGlobalSettings,
DestinyVersion,
GlobalSettings,
ItemAnnotation,
ItemHashTag,
Loadout,
ProfileUpdateResult,
Search,
SearchType,
TagValue,
defaultGlobalSettings,
} from '@destinyitemmanager/dim-api-types';
import { DestinyAccount } from 'app/accounts/destiny-account';
import { t } from 'app/i18next-t';
Expand All @@ -31,7 +32,7 @@ import * as inventoryActions from '../inventory/actions';
import * as loadoutActions from '../loadout-drawer/actions';
import { Loadout as DimLoadout } from '../loadout-drawer/loadout-types';
import * as settingsActions from '../settings/actions';
import { initialSettingsState, Settings } from '../settings/initial-settings';
import { Settings, initialSettingsState } from '../settings/initial-settings';
import { DeleteLoadoutUpdateWithRollback, ProfileUpdateWithRollback } from './api-types';
import * as actions from './basic-actions';
import { makeProfileKey, makeProfileKeyFromAccount } from './selectors';
Expand Down Expand Up @@ -384,17 +385,23 @@ export const dimApi = (

case getType(actions.searchUsed):
return produce(state, (draft) => {
searchUsed(draft, account!, action.payload);
searchUsed(draft, account!, action.payload.query, action.payload.type);
});

case getType(actions.saveSearch):
return produce(state, (draft) => {
saveSearch(account!, draft, action.payload.query, action.payload.saved);
saveSearch(
account!,
draft,
action.payload.query,
action.payload.saved,
action.payload.type,
);
});

case getType(actions.searchDeleted):
return produce(state, (draft) => {
deleteSearch(draft, account!.destinyVersion, action.payload);
deleteSearch(draft, account!.destinyVersion, action.payload.query, action.payload.type);
});

// *** Triumphs ***
Expand Down Expand Up @@ -1147,7 +1154,12 @@ function trackTriumph(
draft.updateQueue.push(updateAction);
}

function searchUsed(draft: Draft<DimApiState>, account: DestinyAccount, query: string) {
function searchUsed(
draft: Draft<DimApiState>,
account: DestinyAccount,
query: string,
type: SearchType,
) {
const destinyVersion = account.destinyVersion;
// Note: memoized
const filtersMap = buildItemFiltersMap(destinyVersion);
Expand All @@ -1166,6 +1178,7 @@ function searchUsed(draft: Draft<DimApiState>, account: DestinyAccount, query: s
action: 'search',
payload: {
query,
type,
},
destinyVersion,
};
Expand All @@ -1182,9 +1195,11 @@ function searchUsed(draft: Draft<DimApiState>, account: DestinyAccount, query: s
usageCount: 1,
saved: false,
lastUsage: Date.now(),
type,
});
}

// TODO: maybe this should be max per type?
if (searches.length > MAX_SEARCH_HISTORY) {
const sortedSearches = searches.toSorted(recentSearchComparator);

Expand All @@ -1195,7 +1210,7 @@ function searchUsed(draft: Draft<DimApiState>, account: DestinyAccount, query: s
const lastSearch = sortedSearches.pop()!;
// Never try to delete the built-in searches or saved searches
if (!lastSearch.saved && lastSearch.usageCount > 0) {
deleteSearch(draft, destinyVersion, lastSearch.query);
deleteSearch(draft, destinyVersion, lastSearch.query, lastSearch.type);
}
}
}
Expand All @@ -1208,6 +1223,7 @@ function saveSearch(
draft: Draft<DimApiState>,
query: string,
saved: boolean,
type: SearchType,
) {
const destinyVersion = account.destinyVersion;
// Note: memoized
Expand All @@ -1228,6 +1244,7 @@ function saveSearch(
payload: {
query,
saved,
type,
},
destinyVersion,
};
Expand All @@ -1245,11 +1262,13 @@ function saveSearch(
usageCount: 1,
saved: true,
lastUsage: Date.now(),
type,
});
draft.updateQueue.push({
action: 'search',
payload: {
query,
type,
},
destinyVersion,
});
Expand All @@ -1258,11 +1277,17 @@ function saveSearch(
draft.updateQueue.push(updateAction);
}

function deleteSearch(draft: Draft<DimApiState>, destinyVersion: DestinyVersion, query: string) {
function deleteSearch(
draft: Draft<DimApiState>,
destinyVersion: DestinyVersion,
query: string,
type: SearchType,
) {
const updateAction: ProfileUpdateWithRollback = {
action: 'delete_search',
payload: {
query,
type,
},
destinyVersion,
};
Expand Down Expand Up @@ -1293,7 +1318,7 @@ function cleanupInvalidSearches(draft: Draft<DimApiState>, account: DestinyAccou
customStats: draft.settings.customStats ?? [],
} as FilterContext);
if (!saveInHistory) {
deleteSearch(draft, account.destinyVersion, search.query);
deleteSearch(draft, account.destinyVersion, search.query, search.type);
}
}
}
Expand Down
20 changes: 14 additions & 6 deletions src/app/dim-api/selectors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { DestinyVersion, defaultLoadoutParameters } from '@destinyitemmanager/dim-api-types';
import {
DestinyVersion,
SearchType,
defaultLoadoutParameters,
} from '@destinyitemmanager/dim-api-types';
import { DestinyAccount } from 'app/accounts/destiny-account';
import { currentAccountSelector, destinyVersionSelector } from 'app/accounts/selectors';
import { Settings } from 'app/settings/initial-settings';
Expand Down Expand Up @@ -57,13 +61,17 @@ export const currentProfileSelector = createSelector(
currentAccount ? profiles[makeProfileKeyFromAccount(currentAccount)] : undefined,
);

const recentSearchesSelectorCached = createSelector(
(state: RootState) => state.dimApi.searches[destinyVersionSelector(state)],
(_state: RootState, searchType: SearchType) => searchType,
(searches, searchType) => searches.filter((s) => s.type === searchType),
);

/**
* Returns all recent/saved searches.
*
* TODO: Sort/trim this list
* Returns all recent/saved searches of the given type.
*/
export const recentSearchesSelector = (state: RootState) =>
state.dimApi.searches[destinyVersionSelector(state)];
export const recentSearchesSelector = (searchType: SearchType) => (state: RootState) =>
recentSearchesSelectorCached(state, searchType);

export const trackedTriumphsSelector = createSelector(
currentProfileSelector,
Expand Down
5 changes: 3 additions & 2 deletions src/app/item-actions/ItemActionsDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SearchType } from '@destinyitemmanager/dim-api-types';
import { destinyVersionSelector } from 'app/accounts/selectors';
import { compareFilteredItems } from 'app/compare/actions';
import { saveSearch } from 'app/dim-api/basic-actions';
Expand Down Expand Up @@ -102,15 +103,15 @@ export default memo(function ItemActionsDropdown({
bulkItemTags.push({ type: 'clear', label: t('Tags.ClearTag'), icon: clearIcon });

// Is the current search saved?
const recentSearches = useSelector(recentSearchesSelector);
const recentSearches = useSelector(recentSearchesSelector(SearchType.Item));
const validateQuery = useSelector(validateQuerySelector);
const { valid, saveable } = validateQuery(searchQuery);
const canonical = searchQuery ? canonicalizeQuery(parseQuery(searchQuery)) : '';
const saved = canonical ? recentSearches.find((s) => s.query === canonical)?.saved : false;

const toggleSaved = () => {
// TODO: keep track of the last search, if you search for something more narrow immediately after then replace?
dispatch(saveSearch({ query: searchQuery, saved: !saved }));
dispatch(saveSearch({ query: searchQuery, saved: !saved, type: SearchType.Item }));
};

const location = useLocation();
Expand Down
3 changes: 2 additions & 1 deletion src/app/loadout/loadout-menu/LoadoutPopup.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SearchType } from '@destinyitemmanager/dim-api-types';
import { languageSelector, settingSelector } from 'app/dim-api/selectors';
import { AlertIcon } from 'app/dim-ui/AlertIcon';
import ClassIcon from 'app/dim-ui/ClassIcon';
Expand Down Expand Up @@ -166,7 +167,7 @@ export default function LoadoutPopup({
className={styles.filterInput}
placeholder={t('Header.FilterHelpLoadouts')}
onQueryChanged={setLoadoutQuery}
loadouts
searchType={SearchType.Loadout}
instant
/>
</div>
Expand Down
Loading

0 comments on commit 53353dc

Please sign in to comment.