mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-27 15:17:01 +01:00
Converts ForceEvent.js to typescript and overhauls the UI to be good (#71750)
## About The Pull Request  ## Why It's Good For The Game The old UI annoyed me enough to improve it ## Changelog 🆑 refactor: Refactored and overhauled the ForceEvent UI /🆑
This commit is contained in:
@@ -29,19 +29,46 @@
|
||||
return GLOB.fun_state
|
||||
|
||||
/datum/force_event/ui_static_data(mob/user)
|
||||
var/static/list/category_to_icons
|
||||
if(!category_to_icons)
|
||||
category_to_icons = list(
|
||||
EVENT_CATEGORY_AI = "robot",
|
||||
EVENT_CATEGORY_ANOMALIES = "cloud-bolt",
|
||||
EVENT_CATEGORY_BUREAUCRATIC = "print",
|
||||
EVENT_CATEGORY_ENGINEERING = "wrench",
|
||||
EVENT_CATEGORY_ENTITIES = "ghost",
|
||||
EVENT_CATEGORY_FRIENDLY = "face-smile",
|
||||
EVENT_CATEGORY_HEALTH = "brain",
|
||||
EVENT_CATEGORY_HOLIDAY = "calendar",
|
||||
EVENT_CATEGORY_INVASION = "user-group",
|
||||
EVENT_CATEGORY_JANITORIAL = "bath",
|
||||
EVENT_CATEGORY_SPACE = "meteor",
|
||||
EVENT_CATEGORY_WIZARD = "hat-wizard",
|
||||
)
|
||||
var/list/data = list()
|
||||
data["categories"] = list()
|
||||
for(var/datum/round_event_control/event_control in SSevents.control)
|
||||
if(!data["categories"][event_control.category])
|
||||
data["categories"][event_control.category] = list(
|
||||
|
||||
var/list/categories_seen = list()
|
||||
var/list/categories = list()
|
||||
|
||||
var/list/events = list()
|
||||
|
||||
for(var/datum/round_event_control/event_control as anything in SSevents.control)
|
||||
//add category
|
||||
if(!categories_seen[event_control.category])
|
||||
categories_seen[event_control.category] = TRUE
|
||||
UNTYPED_LIST_ADD(categories, list(
|
||||
"name" = event_control.category,
|
||||
"events" = list()
|
||||
)
|
||||
data["categories"][event_control.category]["events"] += list(list(
|
||||
"icon" = category_to_icons[event_control.category],
|
||||
))
|
||||
//add event, with one value matching up the category
|
||||
UNTYPED_LIST_ADD(events, list(
|
||||
"name" = event_control.name,
|
||||
"description" = event_control.description,
|
||||
"type" = event_control.type
|
||||
"type" = event_control.type,
|
||||
"category" = event_control.category,
|
||||
))
|
||||
data["categories"] = categories
|
||||
data["events"] = events
|
||||
return data
|
||||
|
||||
/datum/force_event/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
|
||||
|
||||
@@ -301,6 +301,27 @@ export const binaryInsertWith =
|
||||
return copy;
|
||||
};
|
||||
|
||||
/**
|
||||
* This method takes a collection of items and a number, returning a collection
|
||||
* of collections, where the maximum amount of items in each is that second arg
|
||||
*/
|
||||
export const paginate = <T>(collection: T[], maxPerPage: number): T[][] => {
|
||||
const pages: T[][] = [];
|
||||
let page: T[] = [];
|
||||
let itemsToAdd = maxPerPage;
|
||||
|
||||
for (const item of collection) {
|
||||
page.push(item);
|
||||
itemsToAdd--;
|
||||
if (!itemsToAdd) {
|
||||
itemsToAdd = maxPerPage;
|
||||
pages.push(page);
|
||||
page = [];
|
||||
}
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
const isObject = (obj: unknown) => typeof obj === 'object' && obj !== null;
|
||||
|
||||
// Does a deep merge of two objects. DO NOT FEED CIRCULAR OBJECTS!!
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
import { useBackend, useLocalState } from '../backend';
|
||||
import { Stack, Button, Icon, Input, Section, Table } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
import { flow } from 'common/fp';
|
||||
import { filter, sortBy } from 'common/collections';
|
||||
|
||||
export const ForceEvent = (props, context) => {
|
||||
return (
|
||||
<Window title="Force Event" width={450} height={450}>
|
||||
<Window.Content scrollable>
|
||||
<EventSearch />
|
||||
<EventOptionsPanel />
|
||||
<EventContent />
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
|
||||
export const EventSearch = (props, context) => {
|
||||
const [searchQuery, setSearchQuery] = useLocalState(
|
||||
context,
|
||||
'searchQuery',
|
||||
''
|
||||
);
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<Stack>
|
||||
<Stack.Item>
|
||||
<Icon name="search" />
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<Input
|
||||
autoFocus
|
||||
fluid
|
||||
onInput={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search..."
|
||||
value={searchQuery}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
export const EventOptionsPanel = (props, context) => {
|
||||
const { data } = useBackend(context);
|
||||
|
||||
const [announce, setAnnounce] = useLocalState(context, 'announce', true);
|
||||
|
||||
return (
|
||||
<Button.Checkbox
|
||||
fluid
|
||||
checked={announce}
|
||||
onClick={() => setAnnounce(!announce)}>
|
||||
Announce event to the crew
|
||||
</Button.Checkbox>
|
||||
);
|
||||
};
|
||||
|
||||
export const EventContent = (props, context) => {
|
||||
const { data } = useBackend(context);
|
||||
|
||||
const categories = Object.values(data.categories);
|
||||
const sortCategories = sortBy((category) => category.name);
|
||||
|
||||
return sortCategories(categories).map((category) => (
|
||||
<EventList category={category} key={category.name} />
|
||||
));
|
||||
};
|
||||
|
||||
export const EventList = (props, context) => {
|
||||
const { act } = useBackend(context);
|
||||
const { category } = props;
|
||||
const [searchQuery] = useLocalState(context, 'searchQuery', '');
|
||||
const [announce] = useLocalState(context, 'announce', true);
|
||||
|
||||
const filtered_events = flow([
|
||||
filter((event) =>
|
||||
event.name?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
),
|
||||
sortBy((event) => event.name),
|
||||
])(category.events || []);
|
||||
|
||||
if (!filtered_events.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Section title={category.name}>
|
||||
<Table>
|
||||
{filtered_events.map((event) => {
|
||||
return (
|
||||
<Table.Row key={event.name} className="candystripe">
|
||||
<Table.Cell>{event.name}</Table.Cell>
|
||||
<Table.Cell collapsing textAlign="right">
|
||||
<Button
|
||||
mt={0.2}
|
||||
content="Trigger"
|
||||
tooltip={event.description || ''}
|
||||
tooltipPosition="right"
|
||||
onClick={() =>
|
||||
act('forceevent', {
|
||||
type: event.type,
|
||||
announce: announce,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
);
|
||||
})}
|
||||
</Table>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,200 @@
|
||||
import { paginate } from 'common/collections';
|
||||
import { useBackend, useLocalState } from '../backend';
|
||||
import { Stack, Button, Icon, Input, Section, Tabs } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
|
||||
const CATEGORY_PAGE_ITEMS = 4;
|
||||
const EVENT_PAGE_ITEMS = 2;
|
||||
const EVENT_PAGE_MAXCHARS = 48;
|
||||
|
||||
/**
|
||||
* Same as paginate, but respecting event names with a character max length
|
||||
* that will also create a new page if created
|
||||
*/
|
||||
const paginateEvents = (events: Event[], maxPerPage: number): Event[][] => {
|
||||
const pages: Event[][] = [];
|
||||
let page: Event[] = [];
|
||||
// conditions that make a new page
|
||||
let itemsToAdd = maxPerPage;
|
||||
let maxChars = EVENT_PAGE_MAXCHARS;
|
||||
|
||||
for (const event of events) {
|
||||
maxChars -= event.name.length;
|
||||
if (maxChars <= 0) {
|
||||
// would overflow the next line over
|
||||
itemsToAdd = maxPerPage;
|
||||
maxChars = EVENT_PAGE_MAXCHARS - event.name.length;
|
||||
pages.push(page);
|
||||
page = [];
|
||||
}
|
||||
page.push(event);
|
||||
itemsToAdd--;
|
||||
if (!itemsToAdd) {
|
||||
// max amount of items we allow
|
||||
itemsToAdd = maxPerPage;
|
||||
maxChars = EVENT_PAGE_MAXCHARS;
|
||||
pages.push(page);
|
||||
page = [];
|
||||
}
|
||||
}
|
||||
if (page.length) {
|
||||
pages.push(page);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
type Event = {
|
||||
name: string;
|
||||
description: string;
|
||||
type: string;
|
||||
category: string;
|
||||
};
|
||||
|
||||
type Category = {
|
||||
name: string;
|
||||
icon: string;
|
||||
};
|
||||
|
||||
type ForceEventData = {
|
||||
categories: Category[];
|
||||
events: Event[];
|
||||
};
|
||||
|
||||
export const ForceEvent = (props, context) => {
|
||||
return (
|
||||
<Window theme="admin" title="Force Event" width={450} height={450}>
|
||||
<Window.Content>
|
||||
<Stack vertical fill>
|
||||
<Stack.Item>
|
||||
<EventTabs />
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<EventSection />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
|
||||
export const PanelOptions = (props, context) => {
|
||||
const [searchQuery, setSearchQuery] = useLocalState(
|
||||
context,
|
||||
'searchQuery',
|
||||
''
|
||||
);
|
||||
|
||||
const [announce, setAnnounce] = useLocalState(context, 'announce', true);
|
||||
|
||||
return (
|
||||
<Stack width="240px">
|
||||
<Stack.Item>
|
||||
<Icon name="search" />
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<Input
|
||||
autoFocus
|
||||
fluid
|
||||
onInput={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search..."
|
||||
value={searchQuery}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button.Checkbox
|
||||
fluid
|
||||
checked={announce}
|
||||
onClick={() => setAnnounce(!announce)}>
|
||||
Announce
|
||||
</Button.Checkbox>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export const EventSection = (props, context) => {
|
||||
const { data, act } = useBackend<ForceEventData>(context);
|
||||
const { categories, events } = data;
|
||||
|
||||
const [category] = useLocalState(context, 'category', categories[0]);
|
||||
const [searchQuery] = useLocalState(context, 'searchQuery', '');
|
||||
const [announce] = useLocalState(context, 'announce', true);
|
||||
|
||||
const preparedEvents = paginateEvents(
|
||||
events.filter((event) => {
|
||||
// remove events not in the category you're looking at
|
||||
if (event.category !== category.name) {
|
||||
return false;
|
||||
}
|
||||
// remove events not being searched for, if a search is active
|
||||
if (searchQuery && !event.name.toLowerCase().includes(searchQuery)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
EVENT_PAGE_ITEMS
|
||||
);
|
||||
|
||||
return (
|
||||
<Section
|
||||
scrollable
|
||||
fill
|
||||
title={category.name + ' Events'}
|
||||
buttons={<PanelOptions />}>
|
||||
<Stack vertical>
|
||||
{preparedEvents.map((eventPage, i) => (
|
||||
<Stack.Item key={i}>
|
||||
<Stack>
|
||||
{eventPage.map((event) => (
|
||||
<Stack.Item grow key={event.type}>
|
||||
<Button
|
||||
tooltip={event.description}
|
||||
fluid
|
||||
onClick={() =>
|
||||
act('forceevent', {
|
||||
type: event.type,
|
||||
announce: announce,
|
||||
})
|
||||
}>
|
||||
{event.name}
|
||||
</Button>
|
||||
</Stack.Item>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
))}
|
||||
</Stack>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
export const EventTabs = (props, context) => {
|
||||
const { data } = useBackend<ForceEventData>(context);
|
||||
const { categories } = data;
|
||||
|
||||
const [category, setCategory] = useLocalState(
|
||||
context,
|
||||
'category',
|
||||
categories[0]
|
||||
);
|
||||
|
||||
const layerCats = paginate(categories, CATEGORY_PAGE_ITEMS);
|
||||
|
||||
return (
|
||||
<Section mb="-6px">
|
||||
{layerCats.map((page, i) => (
|
||||
<Tabs mb="-3px" fluid key={i}>
|
||||
{page.map((cat) => (
|
||||
<Tabs.Tab
|
||||
selected={category === cat}
|
||||
icon={cat.icon}
|
||||
key={cat.icon}
|
||||
onClick={() => setCategory(cat)}>
|
||||
{cat.name}
|
||||
</Tabs.Tab>
|
||||
))}
|
||||
</Tabs>
|
||||
))}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user