From bcfa8f5f31c5cbdb2addd222700f923923feee66 Mon Sep 17 00:00:00 2001 From: Emiel Van Severen <35073890+emielvanseveren@users.noreply.github.com> Date: Thu, 13 Jun 2024 22:41:50 +0200 Subject: [PATCH] Feat: bunch of improvements (#1036) --- .../src/components/data/Chip/style.ts | 5 +- .../src/components/data/CopyId/index.tsx | 5 +- .../src/components/data/Table/index.tsx | 57 ++++++++-------- .../data/Table/subcomponents/Filter/field.tsx | 2 +- .../data/Table/subcomponents/Filter/index.tsx | 2 + .../Pagination/{index.tsx => PagePicker.tsx} | 10 +-- .../Pagination/PageSizeSelect.tsx | 37 ++++++++++ .../Table/subcomponents/Pagination/style.ts | 2 +- .../data/Table/subcomponents/index.ts | 4 -- .../inputs/Date/DatePicker/Generic.tsx | 4 +- .../src/hooks/useTableActions.ts | 2 +- packages/web-main/src/components/Boolean.tsx | 25 +++++++ .../components/cards/GameServerCard/index.tsx | 16 ++++- .../cards/ModuleDefinitionCard/index.tsx | 2 +- .../selects/GameServerSelect/index.tsx | 24 ++++++- .../src/components/selects/ModuleSelect.tsx | 4 ++ .../src/components/selects/PlayerSelect.tsx | 2 + .../src/components/selects/RoleSelect.tsx | 4 ++ packages/web-main/src/hooks/useAuth.tsx | 68 ++++++++++++------- packages/web-main/src/queries/module.tsx | 4 ++ packages/web-main/src/queries/role.tsx | 2 +- .../_global/-modules/ModuleImportForm.tsx | 51 ++++++++------ .../-variables/VariableCreateUpdateForm.tsx | 4 +- .../_global/gameservers.create.import.tsx | 4 +- ...s.import.tsx => modules.create.import.tsx} | 10 ++- ...es.create.tsx => modules.create.index.tsx} | 2 +- .../src/routes/_auth/_global/modules.tsx | 2 +- .../player.$playerId/-PlayerCurrency.tsx | 41 +++++++++-- .../_global/player.$playerId/economy.tsx | 57 ++++++++++------ .../_global/player.$playerId/role.assign.tsx | 1 + .../src/routes/_auth/_global/players.tsx | 19 +++--- .../settings/-discord/LoginDiscordCard.tsx | 5 +- .../routes/_auth/_global/settings/discord.tsx | 5 +- .../src/routes/_auth/_global/variables.tsx | 43 +++++++++--- 34 files changed, 368 insertions(+), 157 deletions(-) rename packages/lib-components/src/components/data/Table/subcomponents/Pagination/{index.tsx => PagePicker.tsx} (93%) create mode 100644 packages/lib-components/src/components/data/Table/subcomponents/Pagination/PageSizeSelect.tsx delete mode 100644 packages/lib-components/src/components/data/Table/subcomponents/index.ts create mode 100644 packages/web-main/src/components/Boolean.tsx rename packages/web-main/src/routes/_auth/_global/{modules.import.tsx => modules.create.import.tsx} (69%) rename packages/web-main/src/routes/_auth/_global/{modules.create.tsx => modules.create.index.tsx} (99%) diff --git a/packages/lib-components/src/components/data/Chip/style.ts b/packages/lib-components/src/components/data/Chip/style.ts index 8b674acc8b..2c571d2235 100644 --- a/packages/lib-components/src/components/data/Chip/style.ts +++ b/packages/lib-components/src/components/data/Chip/style.ts @@ -1,5 +1,6 @@ import { styled } from '../../../styled'; import { ChipColor, ShowIcon } from '.'; +import { shade } from 'polished'; export const Container = styled.div<{ disabled: boolean; @@ -35,7 +36,7 @@ export const Container = styled.div<{ if (!outline) { return 'border: 0.1rem solid transparent;'; } - return `border: 0.1rem solid ${theme.colors[color]};`; + return `border: 0.1rem solid ${shade(0.5, theme.colors[color])};`; }} &:hover { @@ -69,7 +70,7 @@ export const Container = styled.div<{ ${({ theme, color, outline }): string => { if (outline) { - return 'background-color: transparent;'; + return `background-color: ${shade('0.8', theme.colors[color])};`; } return `background-color: ${theme.colors[color]};`; }} diff --git a/packages/lib-components/src/components/data/CopyId/index.tsx b/packages/lib-components/src/components/data/CopyId/index.tsx index ee7b9a5777..cc71f6d193 100644 --- a/packages/lib-components/src/components/data/CopyId/index.tsx +++ b/packages/lib-components/src/components/data/CopyId/index.tsx @@ -5,9 +5,10 @@ import { AiFillCopy as CopyIcon, AiOutlineCheck as CheckmarkIcon } from 'react-i export interface CopyIdProps { id?: string; placeholder?: string; + copyText?: string; } -export const CopyId: FC = ({ id, placeholder }) => { +export const CopyId: FC = ({ id, placeholder, copyText = 'copied' }) => { const [copied, setCopied] = useState(false); function handleCopy(text: string) { @@ -31,7 +32,7 @@ export const CopyId: FC = ({ id, placeholder }) => { icon={copied ? : } onClick={() => handleCopy(id)} variant="outline" - label={copied ? 'copied' : placeholder ? placeholder : id} + label={copied ? copyText : placeholder ? placeholder : id} color="backgroundAccent" /> ); diff --git a/packages/lib-components/src/components/data/Table/index.tsx b/packages/lib-components/src/components/data/Table/index.tsx index d82baaf466..3515406035 100644 --- a/packages/lib-components/src/components/data/Table/index.tsx +++ b/packages/lib-components/src/components/data/Table/index.tsx @@ -18,19 +18,23 @@ import { ColumnPinningState, RowSelectionState, } from '@tanstack/react-table'; -import { Wrapper, StyledTable, Toolbar, PaginationContainer, Flex, TableWrapper } from './style'; +import { Wrapper, StyledTable, Toolbar, Flex, TableWrapper } from './style'; import { Empty, Spinner, ToggleButtonGroup } from '../../../components'; import { AiOutlinePicCenter as RelaxedDensityIcon, AiOutlinePicRight as TightDensityIcon } from 'react-icons/ai'; -import { ColumnHeader, ColumnVisibility, Filter, Pagination } from './subcomponents'; + +import { ColumnHeader } from './subcomponents/ColumnHeader'; +import { ColumnVisibility } from './subcomponents/ColumnVisibility'; +import { Filter } from './subcomponents/Filter'; +import { PagePicker } from './subcomponents/Pagination/PagePicker'; +import { PageSizeSelect } from './subcomponents/Pagination/PageSizeSelect'; + import { ColumnFilter, PageOptions } from '../../../hooks/useTableActions'; import { GenericCheckBox as CheckBox } from '../../inputs/CheckBox/Generic'; import { useLocalStorage } from '../../../hooks'; export interface TableProps { id: string; - data: DataType[]; - isLoading?: boolean; // currently not possible to type this properly: https://github.com/TanStack/table/issues/4241 @@ -108,15 +112,6 @@ export function Table({ const ROW_SELECTION_COL_SPAN = rowSelection ? 1 : 0; - // table size - useEffect(() => { - if (density === 'tight') { - table.setPageSize(19); - } else { - table.resetPageSize(true); - } - }, [density]); - // handles the column visibility tooltip (shows tooltip when the first column is hidden) useEffect(() => { if ( @@ -170,7 +165,8 @@ export function Table({ onRowSelectionChange: rowSelection ? rowSelection?.setRowSelectionState : undefined, initialState: { - columnVisibility: columnVisibility, + columnVisibility, + columnOrder, sorting: sorting.sortingState, columnFilters: columnFiltering.columnFiltersState, globalFilter: columnSearch.columnSearchState, @@ -181,9 +177,9 @@ export function Table({ state: { columnVisibility, columnOrder, - sorting: sorting?.sortingState, - columnFilters: columnFiltering?.columnFiltersState, - globalFilter: columnSearch?.columnSearchState, + sorting: sorting.sortingState, + columnFilters: columnFiltering.columnFiltersState, + globalFilter: columnSearch.columnSearchState, pagination: pagination?.paginationState, rowSelection: rowSelection ? rowSelection.rowSelectionState : undefined, columnPinning, @@ -326,23 +322,20 @@ export function Table({ {!isLoading && table.getRowModel().rows.length > 1 && ( - + {/* This is the row selection */} + {ROW_SELECTION_COL_SPAN ? : null} {pagination && ( - - + <> + showing {table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1}- {table.getState().pagination.pageIndex * table.getState().pagination.pageSize + table.getRowModel().rows.length}{' '} of {pagination.pageOptions.total} entries - + + ({ pageIndex={table.getState().pagination.pageIndex} setPageIndex={table.setPageIndex} /> - - + + + table.setPageSize(Number(pageSize))} + pageSize={table.getState().pagination.pageSize.toString()} + /> + + )} diff --git a/packages/lib-components/src/components/data/Table/subcomponents/Filter/field.tsx b/packages/lib-components/src/components/data/Table/subcomponents/Filter/field.tsx index 3c81706f55..84d4c78fe0 100644 --- a/packages/lib-components/src/components/data/Table/subcomponents/Filter/field.tsx +++ b/packages/lib-components/src/components/data/Table/subcomponents/Filter/field.tsx @@ -45,7 +45,7 @@ export function FilterRow({ } const meta = column?.columnDef?.meta as Record | undefined; - switch (meta?.type) { + switch (meta?.dataType) { case 'uuid': case 'datetime': return [Operators.is]; diff --git a/packages/lib-components/src/components/data/Table/subcomponents/Filter/index.tsx b/packages/lib-components/src/components/data/Table/subcomponents/Filter/index.tsx index 293d1eabcc..b2d4fa2e04 100644 --- a/packages/lib-components/src/components/data/Table/subcomponents/Filter/index.tsx +++ b/packages/lib-components/src/components/data/Table/subcomponents/Filter/index.tsx @@ -159,6 +159,8 @@ export function Filter({ table }: FilterProps table.setColumnFilters(columnFiltersArray); table.setGlobalFilter(globalFiltersArray); + table.resetPagination(); + setOpen(false); }; diff --git a/packages/lib-components/src/components/data/Table/subcomponents/Pagination/index.tsx b/packages/lib-components/src/components/data/Table/subcomponents/Pagination/PagePicker.tsx similarity index 93% rename from packages/lib-components/src/components/data/Table/subcomponents/Pagination/index.tsx rename to packages/lib-components/src/components/data/Table/subcomponents/Pagination/PagePicker.tsx index 5c762f1417..727a077cab 100644 --- a/packages/lib-components/src/components/data/Table/subcomponents/Pagination/index.tsx +++ b/packages/lib-components/src/components/data/Table/subcomponents/Pagination/PagePicker.tsx @@ -1,6 +1,6 @@ import { FC, useMemo } from 'react'; import { Button, IconButton } from '../../../../../components'; -import { PaginationContainer } from './style'; +import { PagePickerContainer } from './style'; import { FaAngleRight as NextIcon, FaAnglesRight as LastIcon, @@ -27,7 +27,7 @@ const getPageWindow = (pageCount: number, windowSize: number, currentPage: numbe } }; -export interface PaginationProps { +export interface PagePickerProps { setPageIndex: (index: number) => void; pageIndex: number; hasPrevious: boolean; @@ -37,7 +37,7 @@ export interface PaginationProps { pageCount: number; } -export const Pagination: FC = ({ +export const PagePicker: FC = ({ setPageIndex, hasPrevious, pageIndex, @@ -63,7 +63,7 @@ export const Pagination: FC = ({ }; return ( - + {showJumps && ( = ({ ariaLabel="Last page" /> )} - + ); }; diff --git a/packages/lib-components/src/components/data/Table/subcomponents/Pagination/PageSizeSelect.tsx b/packages/lib-components/src/components/data/Table/subcomponents/Pagination/PageSizeSelect.tsx new file mode 100644 index 0000000000..522f587d2a --- /dev/null +++ b/packages/lib-components/src/components/data/Table/subcomponents/Pagination/PageSizeSelect.tsx @@ -0,0 +1,37 @@ +import { FC } from 'react'; +import { UnControlledSelectField } from '../../../../../components'; + +interface PageSizeSelectProps { + onPageSizeChange: (pageSize: string) => void; + pageSize: string; +} + +export const PageSizeSelect: FC = ({ onPageSizeChange, pageSize }) => { + return ( + { + if (selectedItems.length === 0) { + return
Select...
; + } + return
{selectedItems[0].label} items
; + }} + > + + {[5, 10, 20, 30, 40, 50, 100].map((val: number) => ( + +
+ {val} items +
+
+ ))} +
+
+ ); +}; diff --git a/packages/lib-components/src/components/data/Table/subcomponents/Pagination/style.ts b/packages/lib-components/src/components/data/Table/subcomponents/Pagination/style.ts index a5504fc1ec..9e0b7f0e95 100644 --- a/packages/lib-components/src/components/data/Table/subcomponents/Pagination/style.ts +++ b/packages/lib-components/src/components/data/Table/subcomponents/Pagination/style.ts @@ -1,6 +1,6 @@ import { styled } from '../../../../../styled'; -export const PaginationContainer = styled.div<{ border?: boolean }>` +export const PagePickerContainer = styled.div<{ border?: boolean }>` display: flex; justify-content: flex-end; diff --git a/packages/lib-components/src/components/data/Table/subcomponents/index.ts b/packages/lib-components/src/components/data/Table/subcomponents/index.ts deleted file mode 100644 index 700b73d06f..0000000000 --- a/packages/lib-components/src/components/data/Table/subcomponents/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { Pagination } from './Pagination'; -export { Filter } from './Filter'; -export { ColumnHeader } from './ColumnHeader'; -export { ColumnVisibility } from './ColumnVisibility'; diff --git a/packages/lib-components/src/components/inputs/Date/DatePicker/Generic.tsx b/packages/lib-components/src/components/inputs/Date/DatePicker/Generic.tsx index e9471345bf..a43b36ec77 100644 --- a/packages/lib-components/src/components/inputs/Date/DatePicker/Generic.tsx +++ b/packages/lib-components/src/components/inputs/Date/DatePicker/Generic.tsx @@ -1,6 +1,6 @@ import { FC, useLayoutEffect, useMemo, useState } from 'react'; import { Button, Popover } from '../../../../components'; -import { DateTime, DateTimeFormatOptions } from 'luxon'; +import { DateTime, DateTimeFormatOptions, Settings } from 'luxon'; import { dateFormats, timeFormats } from './formats'; import { GenericInputProps } from '../../InputProps'; import { ResultContainer, ContentContainer, InnerContainer, ButtonContainer } from './style'; @@ -22,6 +22,8 @@ interface RelativePickerOptions { showFriendlyName?: boolean; } +Settings.throwOnInvalid = true; + export interface DatePickerProps { /// Determines if the date picker is in absolute or relative mode /// Absolute mode is a calendar and time picker diff --git a/packages/lib-components/src/hooks/useTableActions.ts b/packages/lib-components/src/hooks/useTableActions.ts index 72f51f71c7..d467777083 100644 --- a/packages/lib-components/src/hooks/useTableActions.ts +++ b/packages/lib-components/src/hooks/useTableActions.ts @@ -23,7 +23,7 @@ export interface ColumnFilter { value: string[]; } -export function useTableActions({ pageIndex, pageSize }: TableActionOptions = { pageIndex: 0, pageSize: 9 }) { +export function useTableActions({ pageIndex, pageSize }: TableActionOptions = { pageIndex: 0, pageSize: 10 }) { const [paginationState, setPaginationState] = useState({ pageIndex, pageSize, diff --git a/packages/web-main/src/components/Boolean.tsx b/packages/web-main/src/components/Boolean.tsx new file mode 100644 index 0000000000..46d2f61872 --- /dev/null +++ b/packages/web-main/src/components/Boolean.tsx @@ -0,0 +1,25 @@ +import { Chip, ChipProps } from '@takaro/lib-components'; +import { FC } from 'react'; + +type LikeBoolean = boolean | 'true' | 'false' | 'yes' | 'no'; + +interface BooleanProps { + value: LikeBoolean; + truePositive?: boolean; +} + +export const Boolean: FC = ({ value, truePositive = true }) => { + let color: ChipProps['color'] = 'backgroundAccent'; + + if (value === true || value === 'true' || value === 'yes') { + color = truePositive ? 'success' : 'error'; + } else if (value === false || value === 'false' || value === 'no') { + color = truePositive ? 'error' : 'success'; + } + + if (typeof value === 'boolean') { + value = value ? 'true' : 'false'; + } + + return ; +}; diff --git a/packages/web-main/src/components/cards/GameServerCard/index.tsx b/packages/web-main/src/components/cards/GameServerCard/index.tsx index 9d456c62ab..990a9b3d20 100644 --- a/packages/web-main/src/components/cards/GameServerCard/index.tsx +++ b/packages/web-main/src/components/cards/GameServerCard/index.tsx @@ -19,7 +19,11 @@ import { AiOutlineDelete as DeleteIcon, AiOutlineEdit as EditIcon, AiOutlineLineChart as DashboardIcon, + AiOutlineCopy as CopyIcon, + AiOutlineFunction as ModulesIcon, + AiOutlineSetting as SettingsIcon, } from 'react-icons/ai'; + import { Header, TitleContainer, DetailsContainer } from './style'; import { useGameServerRemove } from 'queries/gameserver'; import { PermissionsGuard } from 'components/PermissionsGuard'; @@ -73,6 +77,11 @@ export const GameServerCard: FC = ({ id, name, type, reacha mutate({ id }); }; + const handleOnCopyClick = (e: MouseEvent) => { + e.stopPropagation(); + navigator.clipboard.writeText(id); + }; + return ( <> = ({ id, name, type, reacha + } onClick={handleOnCopyClick} label="Copy ID" /> } onClick={handleOnEditClick} label="Edit gameserver" /> } @@ -102,7 +112,7 @@ export const GameServerCard: FC = ({ id, name, type, reacha label="Delete gameserver" /> - + } onClick={() => @@ -111,14 +121,14 @@ export const GameServerCard: FC = ({ id, name, type, reacha label="go to dashboard" /> } + icon={} onClick={() => navigate({ to: '/gameserver/$gameServerId/modules', params: { gameServerId: id } }) } label="go to modules" /> } + icon={} onClick={() => navigate({ to: '/gameserver/$gameServerId/settings', params: { gameServerId: id } }) } diff --git a/packages/web-main/src/components/cards/ModuleDefinitionCard/index.tsx b/packages/web-main/src/components/cards/ModuleDefinitionCard/index.tsx index 615d01dff1..40908afa34 100644 --- a/packages/web-main/src/components/cards/ModuleDefinitionCard/index.tsx +++ b/packages/web-main/src/components/cards/ModuleDefinitionCard/index.tsx @@ -60,7 +60,7 @@ export const ModuleDefinitionCard: FC = ({ mod }) => { useEffect(() => { if (!isExporting && exported) { - const blob = new Blob([JSON.stringify(exported)], { type: 'text/plain' }); + const blob = new Blob([JSON.stringify(exported)], { type: 'application/json' }); const url = URL.createObjectURL(blob); setDownloadLink(url); return () => URL.revokeObjectURL(url); diff --git a/packages/web-main/src/components/selects/GameServerSelect/index.tsx b/packages/web-main/src/components/selects/GameServerSelect/index.tsx index ed9643ca4c..90ab100a72 100644 --- a/packages/web-main/src/components/selects/GameServerSelect/index.tsx +++ b/packages/web-main/src/components/selects/GameServerSelect/index.tsx @@ -15,7 +15,12 @@ const gameTypeMap = { [GameServerOutputDTOTypeEnum.Sevendaystodie]: { icon: }, }; -export const GameServerSelect: FC = ({ +interface GameServerSelectProps { + data?: GameServerOutputDTO[]; + filter?: (server: GameServerOutputDTO) => boolean; +} + +export const GameServerSelect: FC = ({ readOnly = false, hint, name: selectName, @@ -27,9 +32,15 @@ export const GameServerSelect: FC = ({ inPortal, description, required, + filter, + data: providedData, + canClear, }) => { - const { data, isLoading: isLoadingData } = useQuery(gameServersQueryOptions({ sortBy: 'type' })); - const gameServers = data ?? []; + const { data: fetchedData, isLoading: isLoadingData } = useQuery({ + ...gameServersQueryOptions({ sortBy: 'type' }), + enabled: !providedData, + }); + let gameServers = providedData ?? fetchedData ?? []; if (isLoadingData) { return ; @@ -39,6 +50,10 @@ export const GameServerSelect: FC = ({ return
no game servers
; } + if (filter) { + gameServers = gameServers.filter(filter); + } + return ( = ({ required={required} loading={loading} label={label} + canClear={canClear} /> ); }; @@ -71,6 +87,7 @@ export const GameServerSelectView: FC = ({ required, loading, label, + canClear, }) => { const renderOptionGroup = (groupLabel: string, typeEnum: GameServerOutputDTOTypeEnum) => { return ( @@ -113,6 +130,7 @@ export const GameServerSelectView: FC = ({ required={required} loading={loading} hasMargin={false} + canClear={canClear} render={(selectedItems) => { if (selectedItems.length === 0) { return

Select...

; diff --git a/packages/web-main/src/components/selects/ModuleSelect.tsx b/packages/web-main/src/components/selects/ModuleSelect.tsx index 4321e9ce1b..818a9ec076 100644 --- a/packages/web-main/src/components/selects/ModuleSelect.tsx +++ b/packages/web-main/src/components/selects/ModuleSelect.tsx @@ -28,6 +28,7 @@ export const ModuleSelect: FC = ({ hint, required, label = 'Module', + canClear, }) => { const { data, isLoading: isLoadingData } = useQuery(modulesQueryOptions()); @@ -56,6 +57,7 @@ export const ModuleSelect: FC = ({ name={selectName} label={label} modules={modules} + canClear={canClear} /> ); }; @@ -74,6 +76,7 @@ export const ModuleSelectView: FC = ({ required, loading, label, + canClear, }) => { return ( = ({ inPortal={inPortal} required={required} loading={loading} + canClear={canClear} render={(selectedItems) => { if (selectedItems.length === 0) { return
Select...
; diff --git a/packages/web-main/src/components/selects/PlayerSelect.tsx b/packages/web-main/src/components/selects/PlayerSelect.tsx index 9b829765de..76661647c9 100644 --- a/packages/web-main/src/components/selects/PlayerSelect.tsx +++ b/packages/web-main/src/components/selects/PlayerSelect.tsx @@ -27,6 +27,7 @@ export const PlayerSelect: FC = ({ size, required, hint, + canClear, }) => { const { data, isLoading: isLoadingData } = useQuery(playersQueryOptions()); @@ -55,6 +56,7 @@ export const PlayerSelect: FC = ({ inPortal={inPortal} enableFilter loading={loading} + canClear={canClear} render={(selectedItems) => { if (selectedItems.length === 0) { return
Select...
; diff --git a/packages/web-main/src/components/selects/RoleSelect.tsx b/packages/web-main/src/components/selects/RoleSelect.tsx index 73418cb76b..eb7e5e1474 100644 --- a/packages/web-main/src/components/selects/RoleSelect.tsx +++ b/packages/web-main/src/components/selects/RoleSelect.tsx @@ -24,6 +24,7 @@ export const RoleSelect: FC = ({ inPortal, description, readOnly, + canClear, }) => { const { data: roles, isLoading: isLoadingData } = useQuery( rolesQueryOptions({ sortBy: 'system', sortDirection: 'asc' }) @@ -52,6 +53,7 @@ export const RoleSelect: FC = ({ required={required} loading={loading} label={label} + canClear={canClear} /> ); }; @@ -68,6 +70,7 @@ export const RoleSelectView: FC = ({ inPortal, hint, required, + canClear, loading, label, }) => { @@ -85,6 +88,7 @@ export const RoleSelectView: FC = ({ required={required} enableFilter={roles.length > 10} loading={loading} + canClear={canClear} render={(selectedItems) => { if (selectedItems.length === 0) { return
Select...
; diff --git a/packages/web-main/src/hooks/useAuth.tsx b/packages/web-main/src/hooks/useAuth.tsx index 5df9e54fb7..8f79ce99c5 100644 --- a/packages/web-main/src/hooks/useAuth.tsx +++ b/packages/web-main/src/hooks/useAuth.tsx @@ -7,7 +7,7 @@ import { DateTime } from 'luxon'; import { getApiClient } from 'util/getApiClient'; import { redirect } from '@tanstack/react-router'; -const SESSION_EXPIRES_AFTER_MINUTES = 5; +const SESSION_EXPIRES_AFTER_MINUTES = 20; interface ExpirableSession { session: UserOutputWithRolesDTO; @@ -20,9 +20,8 @@ export interface IAuthContext { session: UserOutputWithRolesDTO; login: (user: UserOutputWithRolesDTO) => void; } -export const AuthContext = createContext(null); -function getStoredSession(): UserOutputWithRolesDTO | null { +function getLocalSession() { const expirableSessionString = localStorage.getItem('session'); if (!expirableSessionString) { @@ -30,29 +29,18 @@ function getStoredSession(): UserOutputWithRolesDTO | null { } const expirableSession: ExpirableSession = JSON.parse(expirableSessionString); - if (DateTime.fromISO(expirableSession.expiresAt ?? DateTime.now()).diffNow().milliseconds < 0) { - localStorage.removeItem('session'); + if (DateTime.fromISO(expirableSession.expiresAt ?? DateTime.now().toISO()).diffNow().seconds < 0) { return null; } return expirableSession.session; } -function setStoredSession(session: UserOutputWithRolesDTO | null) { - if (session) { - const expirableSession: ExpirableSession = { - session, - expiresAt: DateTime.now().plus({ minutes: SESSION_EXPIRES_AFTER_MINUTES }).toISO(), - }; - localStorage.setItem('session', JSON.stringify(expirableSession)); - } else { - localStorage.removeItem('session'); - } -} +export const AuthContext = createContext(null); export function AuthProvider({ children }: { children: React.ReactNode }) { const queryClient = useQueryClient(); const { oryClient } = useOry(); - const [user, setUser] = useState(getStoredSession()); + const [user, setUser] = useState(getLocalSession()); const isAuthenticated = !!user; @@ -64,9 +52,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { window.location.href = logoutFlowRes.data.logout_url; // Extra clean up is done in /logout-return return Promise.resolve(); - }, []); + }, [oryClient, queryClient]); - // Set session in both state and localStorage const login = useCallback((session: UserOutputWithRolesDTO) => { setStoredSession(session); Sentry.setUser({ id: session.id, email: session.email, username: session.name }); @@ -90,20 +77,51 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { } }, [login, queryClient]); + const getSession = useCallback(async (): Promise => { + const session = getLocalSession(); + + if (session) { + return session; + } + + try { + await refreshSession(); + return JSON.parse(localStorage.getItem('session')!).session; + } catch (error) { + localStorage.removeItem('session'); + return null; + } + }, [refreshSession]); + useEffect(() => { - const storedSession = getStoredSession(); - if (!storedSession) { - refreshSession(); + async function fetchSession() { + const storedSession = await getSession(); + if (!storedSession) { + refreshSession(); + } else { + setUser(storedSession); + } + } + fetchSession(); + }, [getSession, refreshSession]); + + function setStoredSession(session: UserOutputWithRolesDTO | null) { + if (session) { + const expirableSession: ExpirableSession = { + session, + expiresAt: DateTime.now().plus({ minutes: SESSION_EXPIRES_AFTER_MINUTES }).toISO(), + }; + localStorage.setItem('session', JSON.stringify(expirableSession)); } else { - setUser(storedSession); + localStorage.removeItem('session'); } - }, [refreshSession]); + } return ( { return mutationWrapper( useMutation, AxiosError, BuiltinModule>({ mutationFn: async (mod) => await apiClient.module.moduleControllerImport(mod), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: moduleKeys.list() }); + }, }), {} ); diff --git a/packages/web-main/src/queries/role.tsx b/packages/web-main/src/queries/role.tsx index a1cb468c9c..d663ce10bb 100644 --- a/packages/web-main/src/queries/role.tsx +++ b/packages/web-main/src/queries/role.tsx @@ -44,7 +44,7 @@ export const rolesQueryOptions = (opts: RoleSearchInputDTO = {}) => { export const rolesInfiniteQueryOptions = (opts: RoleSearchInputDTO = {}) => { return infiniteQueryOptions>({ - queryKey: [...roleKeys.list(), ...queryParamsToArray(opts)], + queryKey: [...roleKeys.list(), 'infinite', ...queryParamsToArray(opts)], queryFn: async () => (await getApiClient().role.roleControllerSearch(opts)).data, initialPageParam: 0, getNextPageParam: (lastPage) => hasNextPage(lastPage.meta), diff --git a/packages/web-main/src/routes/_auth/_global/-modules/ModuleImportForm.tsx b/packages/web-main/src/routes/_auth/_global/-modules/ModuleImportForm.tsx index 13f4c9c6ca..083feb23d3 100644 --- a/packages/web-main/src/routes/_auth/_global/-modules/ModuleImportForm.tsx +++ b/packages/web-main/src/routes/_auth/_global/-modules/ModuleImportForm.tsx @@ -1,7 +1,7 @@ -import { styled } from '@takaro/lib-components'; +import { FileField, styled } from '@takaro/lib-components'; import { FC, useEffect, useState } from 'react'; import { useForm, SubmitHandler } from 'react-hook-form'; -import { Button, TextField, Drawer, CollapseList, TextAreaField, FormError } from '@takaro/lib-components'; +import { Button, TextField, Drawer, CollapseList, FormError } from '@takaro/lib-components'; import { zodResolver } from '@hookform/resolvers/zod'; import { useNavigate } from '@tanstack/react-router'; import { z } from 'zod'; @@ -14,20 +14,22 @@ export const ButtonContainer = styled.div` export interface IFormInputs { name: string; - data: string; + importData: FileList; } interface ModuleFormProps { isLoading?: boolean; isSuccess?: boolean; - onSubmit?: (data: IFormInputs) => void; + onSubmit: (data: IFormInputs) => void; error: string | string[] | null; } +const MAX_FILE_SIZE = 5_000_000; // 50MB +const ACCEPTED_FILE_TYPES = ['application/json']; + export const ModuleImportForm: FC = ({ isSuccess = false, onSubmit, isLoading = false, error }) => { const [open, setOpen] = useState(true); const navigate = useNavigate(); - const readOnly = !onSubmit; useEffect(() => { if (!open) { @@ -38,7 +40,16 @@ export const ModuleImportForm: FC = ({ isSuccess = false, onSub const { handleSubmit, control } = useForm({ mode: 'onChange', defaultValues: {}, - resolver: zodResolver(z.object({ data: z.string(), name: z.string() })), + resolver: zodResolver( + z.object({ + importData: z + .any() + .refine((files) => files?.length == 1, 'Import data is required') + .refine((files) => files?.[0]?.size <= MAX_FILE_SIZE, 'Max file size is 50MB.') + .refine((files) => ACCEPTED_FILE_TYPES.includes(files?.[0]?.type), 'Only .json files are accepted.'), + name: z.string(), + }) + ), }); useEffect(() => { @@ -47,8 +58,8 @@ export const ModuleImportForm: FC = ({ isSuccess = false, onSub } }, [isSuccess]); - const submitHandler: SubmitHandler = ({ data, name }) => { - onSubmit!({ data, name }); + const submitHandler: SubmitHandler = ({ importData, name }) => { + onSubmit({ importData: importData, name }); }; return ( @@ -66,15 +77,15 @@ export const ModuleImportForm: FC = ({ isSuccess = false, onSub name="name" placeholder="My cool module" required - readOnly={readOnly} /> - @@ -82,14 +93,10 @@ export const ModuleImportForm: FC = ({ isSuccess = false, onSub - {!readOnly ? ( - -