import React, { useEffect, useState } from "react"; import { Box, Label, Flex, Button, Text, Checkbox } from "theme-ui"; import SimpleBar from "simplebar-react"; import Modal from "../components/Modal"; import LoadingOverlay from "../components/LoadingOverlay"; import Divider from "../components/Divider"; import { getDatabase } from "../database"; import { Map } from "../types/Map"; import { Group, GroupContainer } from "../types/Group"; import { MapState } from "../types/MapState"; import { Token } from "../types/Token"; import { RequestCloseEventHandler } from "../types/Events"; export type SelectData = { name: string; id: string; type: "default" | "file"; checked: boolean; }; export type ConfirmDataEventHandler = ( checkedMaps: SelectData[], checkedTokens: SelectData[], checkedMapGroups: Group[], checkedTokenGroups: Group[] ) => void; type SelectDataProps = { isOpen: boolean; onRequestClose: RequestCloseEventHandler; onConfirm: ConfirmDataEventHandler; confirmText: string; label: string; databaseName: string; filter: (table: string, data: Map | MapState | Token, id: string) => boolean; }; type DataRecord = Record; function SelectDataModal({ isOpen, onRequestClose, onConfirm, confirmText, label, databaseName, filter, }: SelectDataProps) { const [maps, setMaps] = useState({}); const [mapGroups, setMapGroups] = useState([]); const [tokensByMap, setTokensByMap] = useState>>( {} ); const [tokens, setTokens] = useState({}); const [tokenGroups, setTokenGroups] = useState([]); const [isLoading, setIsLoading] = useState(false); const hasMaps = Object.values(maps).length > 0; const hasTokens = Object.values(tokens).length > 0; useEffect(() => { async function loadData() { if (isOpen && databaseName) { setIsLoading(true); const db = getDatabase({ addons: [] }, databaseName); let loadedMaps: DataRecord = {}; let loadedTokensByMap: Record> = {}; let loadedTokens: DataRecord = {}; await db .table("maps") .filter((map: Map) => filter("maps", map, map.id)) .each((map: Map) => { loadedMaps[map.id] = { name: map.name, id: map.id, type: map.type, checked: true, }; }); await db .table("states") .filter((state: MapState) => filter("states", state, state.mapId)) .each((state: MapState) => { loadedTokensByMap[state.mapId] = new Set( Object.values(state.tokens).map( (tokenState) => tokenState.tokenId ) ); }); await db .table("tokens") .filter((token: Token) => filter("tokens", token, token.id)) .each((token: Token) => { loadedTokens[token.id] = { name: token.name, id: token.id, type: token.type, checked: true, }; }); const mapGroup = await db.table("groups").get("maps"); const tokenGroup = await db.table("groups").get("tokens"); db.close(); setMaps(loadedMaps); setMapGroups(mapGroup.items); setTokensByMap(loadedTokensByMap); setTokenGroups(tokenGroup.items); setTokens(loadedTokens); setIsLoading(false); } else { setMaps({}); setTokens({}); setTokenGroups([]); setMapGroups([]); setTokensByMap({}); } } loadData(); }, [isOpen, databaseName, filter]); // An object mapping a tokenId to how many checked maps it is currently used in const [tokenUsedCount, setTokenUsedCount] = useState>( {} ); useEffect(() => { let tokensUsed: Record = {}; for (let mapId in maps) { if (maps[mapId].checked && mapId in tokensByMap) { for (let tokenId of tokensByMap[mapId]) { if (tokenId in tokensUsed) { tokensUsed[tokenId] += 1; } else { tokensUsed[tokenId] = 1; } } } } setTokenUsedCount(tokensUsed); // Update tokens to ensure used tokens are checked setTokens((prevTokens) => { let newTokens = { ...prevTokens }; for (let id in newTokens) { if (id in tokensUsed && newTokens[id].type !== "default") { newTokens[id].checked = true; } } return newTokens; }); }, [maps, tokensByMap]); function getCheckedGroups(groups: Group[], data: DataRecord) { let checkedGroups = []; for (let group of groups) { if (group.type === "item") { if (data[group.id] && data[group.id].checked) { checkedGroups.push(group); } } else { let items = []; for (let item of group.items) { if (data[item.id] && data[item.id].checked) { items.push(item); } } if (items.length > 0) { checkedGroups.push({ ...group, items }); } } } return checkedGroups; } function handleConfirm() { let checkedMaps = Object.values(maps).filter((map) => map.checked); let checkedTokens = Object.values(tokens).filter((token) => token.checked); let checkedMapGroups = getCheckedGroups(mapGroups, maps); let checkedTokenGroups = getCheckedGroups(tokenGroups, tokens); onConfirm(checkedMaps, checkedTokens, checkedMapGroups, checkedTokenGroups); } function handleMapsChanged( event: React.ChangeEvent, maps: SelectData[] ) { setMaps((prevMaps) => { let newMaps = { ...prevMaps }; for (let map of maps) { newMaps[map.id].checked = event.target.checked; } return newMaps; }); // If all token select is unchecked then ensure all tokens are unchecked if (!event.target.checked && !tokensSelectChecked) { setTokens((prevTokens) => { let newTokens = { ...prevTokens }; let tempUsedCount = { ...tokenUsedCount }; for (let id in newTokens) { for (let map of maps) { if (tokensByMap[map.id].has(id)) { if (tempUsedCount[id] > 1) { tempUsedCount[id] -= 1; } else if (tempUsedCount[id] === 1) { tempUsedCount[id] = 0; newTokens[id].checked = false; } } } } return newTokens; }); } } function handleTokensChanged( event: React.ChangeEvent, tokens: SelectData[] ) { setTokens((prevTokens) => { let newTokens = { ...prevTokens }; for (let token of tokens) { if (!(token.id in tokenUsedCount) || token.type === "default") { newTokens[token.id].checked = event.target.checked; } } return newTokens; }); } // Some tokens are checked not by maps or all tokens are checked by maps const tokensSelectChecked = Object.values(tokens).some( (token) => !(token.id in tokenUsedCount) && token.checked ) || Object.values(tokens).every((token) => token.id in tokenUsedCount); function renderGroupContainer( group: GroupContainer, checked: boolean, renderItem: (group: Group) => React.ReactNode, onGroupChange: ( event: React.ChangeEvent, group: GroupContainer ) => void ) { return ( {group.items.map(renderItem)} ); } function renderMapGroup(group: Group) { if (group.type === "item") { const map = maps[group.id]; if (map) { return ( ); } } else { if (group.items.some((item) => item.id in maps)) { return renderGroupContainer( group, group.items.some((item) => maps[item.id]?.checked), renderMapGroup, (e, group) => handleMapsChanged( e, group.items .filter((group) => group.id in maps) .map((group) => maps[group.id]) ) ); } } } function renderTokenGroup(group: Group) { if (group.type === "item") { const token = tokens[group.id]; if (token) { return ( {token.id in tokenUsedCount && token.type !== "default" && ( Token used in {tokenUsedCount[token.id]} selected map {tokenUsedCount[token.id] > 1 && "s"} )} ); } } else { if (group.items.some((item) => item.id in tokens)) { const checked = group.items.some( (item) => !(item.id in tokenUsedCount) && tokens[item.id]?.checked ) || group.items.every((item) => item.id in tokenUsedCount); return renderGroupContainer( group, checked, renderTokenGroup, (e, group) => handleTokensChanged( e, group.items .filter((group) => group.id in tokens) .map((group) => tokens[group.id]) ) ); } } } return ( {!hasMaps && !hasTokens && ( No custom maps or tokens found. )} {hasMaps && ( <> {mapGroups.map(renderMapGroup)} )} {hasMaps && hasTokens && } {hasTokens && ( <> {tokenGroups.map(renderTokenGroup)} )} {isLoading && } ); } SelectDataModal.defaultProps = { label: "Select data", confirmText: "Yes", filter: () => true, databaseName: "OwlbearRodeoDB", }; export default SelectDataModal;