From 3010052110c022cf1677a21dc3f3898157268c4b Mon Sep 17 00:00:00 2001 From: NovaBot <154629622+NovaBot13@users.noreply.github.com> Date: Mon, 22 Apr 2024 21:32:37 -0400 Subject: [PATCH] [MIRROR] Cargo ui refactor -> TS (#2135) Cargo ui refactor -> TS Co-authored-by: Jeremiah <42397676+jlsnow301@users.noreply.github.com> --- tgui/packages/common/type-utils.ts | 41 +++ .../tgui/interfaces/Cargo/CargoButtons.tsx | 39 +++ .../tgui/interfaces/Cargo/CargoCart.tsx | 130 ++++++++++ .../tgui/interfaces/Cargo/CargoCatalog.tsx | 234 ++++++++++++++++++ .../tgui/interfaces/Cargo/CargoHelp.tsx | 80 ++++++ .../tgui/interfaces/Cargo/CargoRequests.tsx | 85 +++++++ .../tgui/interfaces/Cargo/CargoStatus.tsx | 75 ++++++ .../packages/tgui/interfaces/Cargo/helpers.ts | 35 +++ tgui/packages/tgui/interfaces/Cargo/index.tsx | 91 +++++++ tgui/packages/tgui/interfaces/Cargo/types.ts | 58 +++++ .../packages/tgui/interfaces/CargoExpress.tsx | 26 +- 11 files changed, 879 insertions(+), 15 deletions(-) create mode 100644 tgui/packages/common/type-utils.ts create mode 100644 tgui/packages/tgui/interfaces/Cargo/CargoButtons.tsx create mode 100644 tgui/packages/tgui/interfaces/Cargo/CargoCart.tsx create mode 100644 tgui/packages/tgui/interfaces/Cargo/CargoCatalog.tsx create mode 100644 tgui/packages/tgui/interfaces/Cargo/CargoHelp.tsx create mode 100644 tgui/packages/tgui/interfaces/Cargo/CargoRequests.tsx create mode 100644 tgui/packages/tgui/interfaces/Cargo/CargoStatus.tsx create mode 100644 tgui/packages/tgui/interfaces/Cargo/helpers.ts create mode 100644 tgui/packages/tgui/interfaces/Cargo/index.tsx create mode 100644 tgui/packages/tgui/interfaces/Cargo/types.ts diff --git a/tgui/packages/common/type-utils.ts b/tgui/packages/common/type-utils.ts new file mode 100644 index 00000000000..a73c0c1d595 --- /dev/null +++ b/tgui/packages/common/type-utils.ts @@ -0,0 +1,41 @@ +/** + * Helps visualize highly complex ui data on the fly. + * @example + * ```tsx + * const { data } = useBackend(); + * logger.log(getShallowTypes(data)); + * ``` + */ +export function getShallowTypes( + data: Record, +): Record { + const output = {}; + + for (const key in data) { + if (Array.isArray(data[key])) { + const arr: any[] = data[key]; + + // Return the first array item if it exists + if (data[key].length > 0) { + output[key] = arr[0]; + continue; + } + + output[key] = 'emptyarray'; + } else if (typeof data[key] === 'object' && data[key] !== null) { + // Please inspect it further and make a new type for it + output[key] = 'object (inspect) || Record'; + } else if (typeof data[key] === 'number') { + const num = Number(data[key]); + + // 0 and 1 could be booleans from byond + if (num === 1 || num === 0) { + output[key] = `${num}, BooleanLike?`; + continue; + } + output[key] = data[key]; + } + } + + return output; +} diff --git a/tgui/packages/tgui/interfaces/Cargo/CargoButtons.tsx b/tgui/packages/tgui/interfaces/Cargo/CargoButtons.tsx new file mode 100644 index 00000000000..82c0d51a1f4 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Cargo/CargoButtons.tsx @@ -0,0 +1,39 @@ +import { useBackend } from '../../backend'; +import { Box, Button } from '../../components'; +import { formatMoney } from '../../format'; +import { CargoData } from './types'; + +export function CargoCartButtons(props) { + const { act, data } = useBackend(); + const { cart = [], requestonly, can_send, can_approve_requests } = data; + + let total = 0; + let amount = 0; + for (let i = 0; i < cart.length; i++) { + amount += cart[i].amount; + total += cart[i].cost; + } + + const canClear = + !requestonly && !!can_send && !!can_approve_requests && cart.length > 0; + + return ( + <> + + {amount === 0 && 'Cart is empty'} + {amount === 1 && '1 item'} + {amount >= 2 && amount + ' items'}{' '} + {total > 0 && `(${formatMoney(total)} cr)`} + + + + + ); +} diff --git a/tgui/packages/tgui/interfaces/Cargo/CargoCart.tsx b/tgui/packages/tgui/interfaces/Cargo/CargoCart.tsx new file mode 100644 index 00000000000..473c33146d2 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Cargo/CargoCart.tsx @@ -0,0 +1,130 @@ +import { useBackend } from '../../backend'; +import { + Button, + Icon, + Input, + NoticeBox, + RestrictedInput, + Section, + Stack, + Table, +} from '../../components'; +import { formatMoney } from '../../format'; +import { CargoCartButtons } from './CargoButtons'; +import { CargoData } from './types'; + +export function CargoCart(props) { + const { act, data } = useBackend(); + const { requestonly, away, cart = [], docked, location } = data; + + const sendable = !away && !!docked; + + return ( + + +
}> + +
+
+ + {cart.length > 0 && !requestonly && ( +
+ + + {!sendable && } + + + + + +
+ )} +
+
+ ); +} + +function CheckoutItems(props) { + const { act, data } = useBackend(); + const { amount_by_name, can_send, cart = [], max_order } = data; + + if (cart.length === 0) { + return Nothing in cart; + } + + return ( + + + ID + Supply Type + Amount + + + Cost + + + + {cart.map((entry) => ( + + + #{entry.id} + + {entry.object} + + + {can_send && entry.can_be_cancelled ? ( + + act('modify', { + order_name: entry.object, + amount: value, + }) + } + /> + ) : ( + + )} + + {!!can_send && !!entry.can_be_cancelled && ( + <> +
+ ); +} diff --git a/tgui/packages/tgui/interfaces/Cargo/CargoCatalog.tsx b/tgui/packages/tgui/interfaces/Cargo/CargoCatalog.tsx new file mode 100644 index 00000000000..115708410a4 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Cargo/CargoCatalog.tsx @@ -0,0 +1,234 @@ +import { sortBy } from 'common/collections'; +import { useMemo } from 'react'; + +import { useBackend, useSharedState } from '../../backend'; +import { + Button, + Icon, + Input, + Section, + Stack, + Table, + Tabs, + Tooltip, +} from '../../components'; +import { formatMoney } from '../../format'; +import { CargoCartButtons } from './CargoButtons'; +import { searchForSupplies } from './helpers'; +import { CargoData, Supply, SupplyCategory } from './types'; + +export function CargoCatalog(props) { + const { express } = props; + const { act, data } = useBackend(); + const { self_paid } = data; + + const supplies = Object.values(data.supplies); + + const [activeSupplyName, setActiveSupplyName] = useSharedState( + 'supply', + supplies[0]?.name, + ); + + const [searchText, setSearchText] = useSharedState('search_text', ''); + + const packs = useMemo(() => { + let fetched: Supply[] | undefined; + + if (activeSupplyName === 'search_results') { + fetched = searchForSupplies(supplies, searchText); + } else { + fetched = supplies.find( + (supply) => supply.name === activeSupplyName, + )?.packs; + } + + if (!fetched) return []; + + fetched = sortBy(fetched, (pack: Supply) => pack.name); + + return fetched; + }, [activeSupplyName, supplies, searchText]); + + return ( +
+ + + + ) + } + > + + + + + + + + + +
+ ); +} + +type CatalogTabsProps = { + activeSupplyName: string; + categories: SupplyCategory[]; + searchText: string; + setActiveSupplyName: (name: string) => void; + setSearchText: (text: string) => void; +}; + +function CatalogTabs(props: CatalogTabsProps) { + const { + activeSupplyName, + categories, + searchText, + setActiveSupplyName, + setSearchText, + } = props; + + const sorted = sortBy(categories, (supply) => supply.name); + + return ( + + + + + + + + { + if (value === searchText) { + return; + } + + if (value.length) { + // Start showing results + setActiveSupplyName('search_results'); + } else if (activeSupplyName === 'search_results') { + // return to normal category + setActiveSupplyName(sorted[0]?.name); + } + setSearchText(value); + }} + /> + + + + + {sorted.map((supply) => ( + { + setActiveSupplyName(supply.name); + setSearchText(''); + }} + > +
+ {supply.name} + {supply.packs.length} +
+
+ ))} +
+ ); +} + +type CatalogListProps = { + packs: SupplyCategory['packs']; +}; + +function CatalogList(props: CatalogListProps) { + const { act, data } = useBackend(); + const { amount_by_name, max_order, self_paid, app_cost } = data; + const { packs = [] } = props; + + return ( +
+ + {packs.map((pack) => { + let color = ''; + const digits = Math.floor(Math.log10(pack.cost) + 1); + if (self_paid) { + color = 'caution'; + } else if (digits >= 5 && digits <= 6) { + color = 'yellow'; + } else if (digits > 6) { + color = 'bad'; + } + + return ( + + {pack.name} + + {!!pack.small_item && ( + + + + )} + + + {!!pack.access && ( + + + + )} + + + + + + ); + })} +
+
+ ); +} diff --git a/tgui/packages/tgui/interfaces/Cargo/CargoHelp.tsx b/tgui/packages/tgui/interfaces/Cargo/CargoHelp.tsx new file mode 100644 index 00000000000..841ded71fb7 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Cargo/CargoHelp.tsx @@ -0,0 +1,80 @@ +import { Box, NoticeBox, Section, Stack } from '../../components'; + +const ORDER_TEXT = `Each department on the station will order crates from their own personal + consoles. These orders are ENTIRELY FREE! They do not come out of + cargo's budget, and rather put the consoles on cooldown. So + here's where you come in: The ordered crates will show up on your + supply console, and you need to deliver the crates to the orderers. + You'll actually be paid the full value of the department crate on + delivery if the crate was not tampered with, making the system a good + source of income.`; + +const DISPOSAL_TEXT = `In addition to MULEs and hand-deliveries, you can also make use of the + disposals mailing system. Note that a break in the disposal piping could + cause your package to be lost (this hardly ever happens), so this is not + always the most secure ways to deliver something. You can wrap up a + piece of paper and mail it the same way if you (or someone at the desk) + wants to mail a letter.`; + +export function CargoHelp(props) { + return ( + + +
+
+ {ORDER_TEXT} +
+
+ Examine a department order crate to get specific details about where + the crate needs to go. +
+
+ + MULEbots are slow but loyal delivery bots that will get crates + delivered with minimal technician effort required. It is slow, + though, and can be tampered with while en route. + +
+ + Setting up a MULEbot is easy: + + 1. Drag the crate you want to deliver next to the MULEbot. +
+ 2. Drag the crate on top of MULEbot. It should load on. +
+ 3. Open your PDA. +
+ 4. Click Delivery Bot Control.
+ 5. Click Scan for Active Bots.
+ 6. Choose your MULE. +
+ 7. Click on Destination: (set).
+ 8. Choose a destination and click OK. +
+ 9. Click Proceed. +
+
+ {DISPOSAL_TEXT} +
+ + Using the Disposals Delivery System is even easier: + + 1. Wrap your item/crate in packaging paper. +
+ 2. Use the destinations tagger to choose where to send it. +
+ 3. Tag the package. +
+ 4. Stick it on the conveyor and let the system handle it. +
+
+
+
+ + + Pondering something not included here? When in doubt, ask the QM! + + +
+ ); +} diff --git a/tgui/packages/tgui/interfaces/Cargo/CargoRequests.tsx b/tgui/packages/tgui/interfaces/Cargo/CargoRequests.tsx new file mode 100644 index 00000000000..3a53efb45e9 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Cargo/CargoRequests.tsx @@ -0,0 +1,85 @@ +import { decodeHtmlEntities } from 'common/string'; + +import { useBackend } from '../../backend'; +import { Button, NoticeBox, Section, Table } from '../../components'; +import { TableCell, TableRow } from '../../components/Table'; +import { formatMoney } from '../../format'; +import { CargoData } from './types'; + +export function CargoRequests(props) { + const { act, data } = useBackend(); + const { requests = [], requestonly, can_send, can_approve_requests } = data; + + return ( +
act('denyall')} + > + Clear + + ) + } + > + {requests.length === 0 && No Requests} + {requests.length > 0 && ( + + + ID + Object + Orderer + Reason + Cost + {(!requestonly || !!can_send) && !!can_approve_requests && ( + Actions + )} + + + {requests.map((request) => ( + + #{request.id} + {request.object} + + {request.orderer} + + + {decodeHtmlEntities(request.reason)} + + + {formatMoney(request.cost)} cr + + {(!requestonly || !!can_send) && !!can_approve_requests && ( + +
+ )} +
+ ); +} diff --git a/tgui/packages/tgui/interfaces/Cargo/CargoStatus.tsx b/tgui/packages/tgui/interfaces/Cargo/CargoStatus.tsx new file mode 100644 index 00000000000..b230400e4e3 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Cargo/CargoStatus.tsx @@ -0,0 +1,75 @@ +import { useBackend } from '../../backend'; +import { + AnimatedNumber, + Box, + Button, + LabeledList, + Section, +} from '../../components'; +import { formatMoney } from '../../format'; +import { CargoData } from './types'; + +export function CargoStatus(props) { + const { act, data } = useBackend(); + const { + department, + grocery, + away, + docked, + loan, + loan_dispatched, + location, + message, + points, + requestonly, + can_send, + } = data; + + return ( +
+ formatMoney(value)} + /> + {' credits'} + + } + > + + + {!!docked && !requestonly && !!can_send ? ( + + ) : ( + String(location) + )} + + {message} + {!!loan && !requestonly && ( + + {!loan_dispatched ? ( + + ) : ( + Loaned to Centcom + )} + + )} + +
+ ); +} diff --git a/tgui/packages/tgui/interfaces/Cargo/helpers.ts b/tgui/packages/tgui/interfaces/Cargo/helpers.ts new file mode 100644 index 00000000000..e6b67f8ff6d --- /dev/null +++ b/tgui/packages/tgui/interfaces/Cargo/helpers.ts @@ -0,0 +1,35 @@ +import { filter } from 'common/collections'; +import { flow } from 'common/fp'; + +import { Supply, SupplyCategory } from './types'; + +/** + * Take entire supplies tree + * and return a flat supply pack list that matches search, + * sorted by name and only the first page. + * @param {Supply[]} supplies Supplies list, aka Object.values(data.supplies) + * @param {string} search The search term + * @returns {Supply[]} The flat list of supply packs. + */ +export function searchForSupplies( + supplies: SupplyCategory[], + search: string, +): Supply[] { + const lowerSearch = search.toLowerCase(); + + return flow([ + // Flat categories + (initialSupplies: SupplyCategory[]) => + initialSupplies.flatMap((category) => category.packs), + // Filter by name or desc + (flatMapped: Supply[]) => + filter( + flatMapped, + (pack: Supply) => + pack.name?.toLowerCase().includes(lowerSearch) || + pack.desc?.toLowerCase().includes(lowerSearch), + ), + // Just the first page + (filtered: Supply[]) => filtered.slice(0, 25), + ])(supplies); +} diff --git a/tgui/packages/tgui/interfaces/Cargo/index.tsx b/tgui/packages/tgui/interfaces/Cargo/index.tsx new file mode 100644 index 00000000000..d39435f4cd9 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Cargo/index.tsx @@ -0,0 +1,91 @@ +import { useBackend, useSharedState } from '../../backend'; +import { Stack, Tabs } from '../../components'; +import { Window } from '../../layouts'; +import { CargoCart } from './CargoCart'; +import { CargoCatalog } from './CargoCatalog'; +import { CargoHelp } from './CargoHelp'; +import { CargoRequests } from './CargoRequests'; +import { CargoStatus } from './CargoStatus'; +import { CargoData } from './types'; + +enum TAB { + Catalog = 'catalog', + Requests = 'requests', + Cart = 'cart', + Help = 'help', +} + +export function Cargo(props) { + return ( + + + + + + ); +} + +export function CargoContent(props) { + const { data } = useBackend(); + + const { cart = [], requests = [], requestonly } = data; + + const [tab, setTab] = useSharedState('cargotab', TAB.Catalog); + + let amount = 0; + for (let i = 0; i < cart.length; i++) { + amount += cart[i].amount; + } + + return ( + + + + + + + setTab(TAB.Catalog)} + > + Catalog + + 0 && 'yellow'} + selected={tab === TAB.Requests} + onClick={() => setTab(TAB.Requests)} + > + Requests ({requests.length}) + + {!requestonly && ( + <> + 0 && 'yellow'} + selected={tab === TAB.Cart} + onClick={() => setTab(TAB.Cart)} + > + Checkout ({amount}) + + setTab(TAB.Help)} + > + Help + + + )} + + + + {tab === TAB.Catalog && } + {tab === TAB.Requests && } + {tab === TAB.Cart && } + {tab === TAB.Help && } + + + ); +} diff --git a/tgui/packages/tgui/interfaces/Cargo/types.ts b/tgui/packages/tgui/interfaces/Cargo/types.ts new file mode 100644 index 00000000000..5cb70838cbd --- /dev/null +++ b/tgui/packages/tgui/interfaces/Cargo/types.ts @@ -0,0 +1,58 @@ +import { BooleanLike } from 'common/react'; + +export type CargoData = { + amount_by_name: Record; + app_cost?: number; + away: BooleanLike; + can_approve_requests: BooleanLike; + can_send: BooleanLike; + cart: CartEntry[]; + department: string; + docked: BooleanLike; + grocery: number; + loan_dispatched: BooleanLike; + loan: BooleanLike; + location: string; + max_order: number; + message: string; + points: number; + requests: Request[]; + requestonly: BooleanLike; + self_paid: BooleanLike; + supplies: Record; +}; + +export type SupplyCategory = { + name: string; + packs: Supply[]; +}; + +export type Supply = { + access: BooleanLike; + cost: number; + desc: string; + goody: BooleanLike; + id: string; + name: string; + small_item: BooleanLike; +}; + +type CartEntry = { + amount: number; + can_be_cancelled: BooleanLike; + cost_type: string; + cost: number; + dep_order: BooleanLike; + id: string; + object: string; + orderer: string; + paid: BooleanLike; +}; + +type Request = { + cost: number; + id: string; + object: string; + orderer: string; + reason: string; +}; diff --git a/tgui/packages/tgui/interfaces/CargoExpress.tsx b/tgui/packages/tgui/interfaces/CargoExpress.tsx index 0983e685f7d..83947b5adc4 100644 --- a/tgui/packages/tgui/interfaces/CargoExpress.tsx +++ b/tgui/packages/tgui/interfaces/CargoExpress.tsx @@ -9,7 +9,7 @@ import { Section, } from '../components'; import { Window } from '../layouts'; -import { CargoCatalog } from './Cargo'; +import { CargoCatalog } from './Cargo/CargoCatalog'; import { InterfaceLockNoticeBox } from './common/InterfaceLockNoticeBox'; type Data = { @@ -24,7 +24,7 @@ type Data = { message: string; }; -export const CargoExpress = (props) => { +export function CargoExpress(props) { const { data } = useBackend(); const { locked } = data; @@ -36,9 +36,9 @@ export const CargoExpress = (props) => { ); -}; +} -const CargoExpressContent = (props) => { +function CargoExpressContent(props) { const { act, data } = useBackend(); const { hasBeacon, @@ -64,11 +64,9 @@ const CargoExpressContent = (props) => { > - - {message} @@ -88,4 +84,4 @@ const CargoExpressContent = (props) => { ); -}; +}