[MIRROR] Improves stack recipe UI Search [MDB IGNORE] (#15367)

* Improves stack recipe UI Search (#68780)

* Improves stack recipe ui search

* Changes Stack interface to be called StackCrafting

* Improves stack recipe UI Search

Co-authored-by: Tastyfish <crazychris32@gmail.com>
This commit is contained in:
SkyratBot
2022-08-04 15:48:19 +01:00
committed by GitHub
co-authored by Tastyfish
parent 7a034ea8f6
commit 797efc3ebe
3 changed files with 250 additions and 182 deletions
+1 -1
View File
@@ -262,7 +262,7 @@
/obj/item/stack/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "Stack", name)
ui = new(user, src, "StackCrafting", name)
ui.open()
/obj/item/stack/ui_data(mob/user)
-181
View File
@@ -1,181 +0,0 @@
import { createSearch } from 'common/string';
import { sortBy } from 'common/collections';
import { useBackend, useLocalState } from '../backend';
import { Box, Button, Input, NoticeBox, Section, Collapsible, Table } from '../components';
import { Window } from '../layouts';
export const Stack = (props, context) => {
const { act, data } = useBackend(context);
const { amount, recipes = [] } = data;
const [searchText, setSearchText] = useLocalState(context, 'searchText', '');
const testSearch = createSearch(searchText, (item) => {
return item;
});
const items =
(searchText.length > 0 &&
Object.keys(recipes)
.filter(testSearch)
.reduce((obj, key) => {
obj[key] = recipes[key];
return obj;
}, {})) ||
recipes;
const height = Math.max(94 + Object.keys(recipes).length * 26, 250);
return (
<Window width={400} height={Math.min(height, 500)}>
<Window.Content scrollable>
<Section
title={'Amount: ' + amount}
buttons={
<>
Search
<Input
autoFocus
value={searchText}
onInput={(e, value) => setSearchText(value)}
mx={1}
/>
</>
}>
{(items.length === 0 && <NoticeBox>No recipes found.</NoticeBox>) || (
<RecipeList recipes={items} />
)}
</Section>
</Window.Content>
</Window>
);
};
const RecipeList = (props, context) => {
const { act, data } = useBackend(context);
const { recipes } = props;
const sortedKeys = sortBy((key) => key.toLowerCase())(Object.keys(recipes));
return sortedKeys.map((title) => {
const recipe = recipes[title];
if (recipe.ref === undefined) {
return (
<Collapsible ml={1} color="label" title={title}>
<Box ml={1}>
<RecipeList recipes={recipe} />
</Box>
</Collapsible>
);
} else {
return <Recipe title={title} recipe={recipe} />;
}
});
};
const buildMultiplier = (recipe, amount) => {
if (recipe.req_amount > amount) {
return 0;
}
return Math.floor(amount / recipe.req_amount);
};
const Multipliers = (props, context) => {
const { act, data } = useBackend(context);
const { recipe, maxMultiplier } = props;
const maxM = Math.min(
maxMultiplier,
Math.floor(recipe.max_res_amount / recipe.res_amount)
);
const multipliers = [5, 10, 25];
let finalResult = [];
for (const multiplier of multipliers) {
if (maxM >= multiplier) {
finalResult.push(
<Button
content={multiplier * recipe.res_amount + 'x'}
onClick={() =>
act('make', {
ref: recipe.ref,
multiplier: multiplier,
})
}
/>
);
}
}
if (multipliers.indexOf(maxM) === -1) {
finalResult.push(
<Button
content={maxM * recipe.res_amount + 'x'}
onClick={() =>
act('make', {
ref: recipe.ref,
multiplier: maxM,
})
}
/>
);
}
return finalResult;
};
const Recipe = (props, context) => {
const { act, data } = useBackend(context);
const { amount } = data;
const { recipe, title } = props;
const { res_amount, max_res_amount, req_amount, ref } = recipe;
let buttonName = title;
buttonName += ' (';
buttonName += req_amount + ' ';
buttonName += 'sheet' + (req_amount > 1 ? 's' : '');
buttonName += ')';
if (res_amount > 1) {
buttonName = res_amount + 'x ' + buttonName;
}
const maxMultiplier = buildMultiplier(recipe, amount);
return (
<Box mb={1}>
<Table>
<Table.Row>
<Table.Cell>
<Button
fluid
disabled={!maxMultiplier}
icon="wrench"
content={buttonName}
onClick={() =>
act('make', {
ref: recipe.ref,
multiplier: 1,
})
}
/>
</Table.Cell>
{max_res_amount > 1 && maxMultiplier > 1 && (
<Table.Cell collapsing>
<Multipliers recipe={recipe} maxMultiplier={maxMultiplier} />
</Table.Cell>
)}
</Table.Row>
</Table>
</Box>
);
};
@@ -0,0 +1,249 @@
import { createSearch } from 'common/string';
import { filter, map, reduce, sortBy } from 'common/collections';
import { useBackend, useLocalState } from '../backend';
import { Box, Button, Input, NoticeBox, Section, Collapsible, Table } from '../components';
import { Window } from '../layouts';
import { clamp } from 'common/math';
import { flow } from 'common/fp';
type Recipe = {
ref: unknown | null;
req_amount: number;
// for multiplier buttons
res_amount: number;
max_res_amount: number;
};
type RecipeList = {
[key: string]: Recipe | RecipeList;
};
type StackCraftingProps = {
amount: number;
recipes: RecipeList;
};
type RecipeListProps = {
recipes: RecipeList;
};
type MultiplierProps = {
recipe: Recipe;
maxMultiplier: number;
};
type RecipeBoxProps = {
title: string;
recipe: Recipe;
};
// RecipeList converted via Object.entries() for filterRecipeList
type RecipeListEntry = [string, RecipeList | Recipe];
type RecipeListFilterableEntry = [string, RecipeList | Recipe | undefined];
/**
* Type guard for recipe vs recipe list
* @param value the value to test
* @returns type guard boolean
*/
// eslint-disable-next-line func-style
function isRecipeList(value: Recipe | RecipeList): value is RecipeList {
return (value as Recipe).ref === undefined;
}
/**
* Filter recipe list by keys, resursing into subcategories.
* Returns the filtered list, or undefined, if there is no list left.
* @param list the recipe list to filter
* @param filter the filter function for recipes
*/
const filterRecipeList = (
list: RecipeList,
keyFilter: (key: string) => boolean
) => {
const filteredList: RecipeList = flow([
map((entry: RecipeListEntry): RecipeListFilterableEntry => {
const [key, recipe] = entry;
if (isRecipeList(recipe)) {
// If category name matches, return the whole thing.
if (keyFilter(key)) {
return entry;
}
// otherwise, filter sub-entries.
return [key, filterRecipeList(recipe, keyFilter)];
}
return keyFilter(key) ? entry : [key, undefined];
}),
filter((entry: RecipeListFilterableEntry) => entry[1] !== undefined),
sortBy((entry: RecipeListEntry) => entry[0].toLowerCase()),
reduce((obj: RecipeList, entry: RecipeListEntry) => {
obj[entry[0]] = entry[1];
return obj;
}, {}),
])(Object.entries(list));
return Object.keys(filteredList).length ? filteredList : undefined;
};
export const StackCrafting = (_props, context) => {
const { data } = useBackend<StackCraftingProps>(context);
const { amount, recipes = {} } = data;
const [searchText, setSearchText] = useLocalState(context, 'searchText', '');
const testSearch = createSearch(searchText, (item: string) => item);
const filteredRecipes = filterRecipeList(recipes, testSearch);
const height: number = clamp(94 + Object.keys(recipes).length * 26, 250, 500);
return (
<Window width={400} height={height}>
<Window.Content scrollable>
<Section
title={'Amount: ' + amount}
buttons={
<>
Search
<Input
autoFocus
value={searchText}
onInput={(e, value) => setSearchText(value)}
mx={1}
/>
</>
}>
{filteredRecipes ? (
<RecipeListBox recipes={filteredRecipes} />
) : (
<NoticeBox>No recipes found.</NoticeBox>
)}
</Section>
</Window.Content>
</Window>
);
};
const RecipeListBox = (props: RecipeListProps) => {
const { recipes } = props;
return (
<>
{Object.keys(recipes).map((title) => {
const recipe = recipes[title];
if (isRecipeList(recipe)) {
return (
<Collapsible ml={1} color="label" title={title}>
<Box ml={2}>
<RecipeListBox recipes={recipe} />
</Box>
</Collapsible>
);
} else {
return <RecipeBox title={title} recipe={recipe} />;
}
})}
</>
);
};
const buildMultiplier = (recipe: Recipe, amount: number) => {
if (recipe.req_amount > amount) {
return 0;
}
return Math.floor(amount / recipe.req_amount);
};
const Multipliers = (props: MultiplierProps, context) => {
const { act } = useBackend(context);
const { recipe, maxMultiplier } = props;
const maxM = Math.min(
maxMultiplier,
Math.floor(recipe.max_res_amount / recipe.res_amount)
);
const multipliers = [5, 10, 25];
const finalResult: JSX.Element[] = [];
for (const multiplier of multipliers) {
if (maxM >= multiplier) {
finalResult.push(
<Button
content={multiplier * recipe.res_amount + 'x'}
onClick={() =>
act('make', {
ref: recipe.ref,
multiplier: multiplier,
})
}
/>
);
}
}
if (multipliers.indexOf(maxM) === -1) {
finalResult.push(
<Button
content={maxM * recipe.res_amount + 'x'}
onClick={() =>
act('make', {
ref: recipe.ref,
multiplier: maxM,
})
}
/>
);
}
return <>{finalResult.map((x) => x)}</>;
};
const RecipeBox = (props: RecipeBoxProps, context) => {
const { act, data } = useBackend<StackCraftingProps>(context);
const { amount } = data;
const { recipe, title } = props;
const { res_amount, max_res_amount, req_amount, ref } = recipe;
const resAmountLabel = res_amount > 1 ? `${res_amount}x ` : '';
const sheetSuffix = req_amount > 1 ? 's' : '';
const buttonName = `${resAmountLabel}${title} (${req_amount} sheet${sheetSuffix})`;
const maxMultiplier = buildMultiplier(recipe, amount);
return (
<Box mb={1}>
<Table>
<Table.Row>
<Table.Cell>
<Button
fluid
disabled={!maxMultiplier}
icon="wrench"
content={buttonName}
onClick={() =>
act('make', {
ref: ref,
multiplier: 1,
})
}
/>
</Table.Cell>
{max_res_amount > 1 && maxMultiplier > 1 && (
<Table.Cell collapsing>
<Multipliers recipe={recipe} maxMultiplier={maxMultiplier} />
</Table.Cell>
)}
</Table.Row>
</Table>
</Box>
);
};