diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0eeb2362..df0d0530 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,11 +20,7 @@ jobs: - name: Install deps run: npm ci --prefer-offline - - run: npm test -- --silent --coverage - - - uses: codecov/codecov-action@v4 - with: - token: ${{ secrets.CODECOV_TOKEN }} + - run: npm test -- --silent e2e-tests: runs-on: ubuntu-latest diff --git a/package-lock.json b/package-lock.json index 72060f94..901c0295 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { "name": "frontend", - "version": "0.38.1", + "version": "0.39.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "version": "0.38.1", + "version": "0.39.0", "license": "MIT", "dependencies": { "@dinero.js/currencies": "^2.0.0-alpha.11", @@ -323,11 +323,6 @@ "ms": "^2.1.1" } }, - "node_modules/@cypress/xvfb/node_modules/ms": { - "version": "2.1.3", - "dev": true, - "license": "MIT" - }, "node_modules/@dinero.js/calculator-number": { "version": "2.0.0-alpha.14", "license": "MIT", @@ -905,11 +900,15 @@ }, "node_modules/@hapi/hoek": { "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@hapi/topo": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -1418,7 +1417,9 @@ } }, "node_modules/@sideway/address": { - "version": "4.1.4", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -1427,11 +1428,15 @@ }, "node_modules/@sideway/formula": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@sideway/pinpoint": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", "dev": true, "license": "BSD-3-Clause" }, @@ -3402,9 +3407,9 @@ } }, "node_modules/axios": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", - "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -4332,13 +4337,13 @@ "license": "MIT" }, "node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -6621,13 +6626,15 @@ } }, "node_modules/joi": { - "version": "17.11.0", + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "@hapi/hoek": "^9.0.0", - "@hapi/topo": "^5.0.0", - "@sideway/address": "^4.1.3", + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", "@sideway/formula": "^3.0.1", "@sideway/pinpoint": "^2.0.0" } @@ -7525,7 +7532,9 @@ } }, "node_modules/ms": { - "version": "2.1.2", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, @@ -7955,9 +7964,9 @@ "dev": true }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, @@ -8020,9 +8029,9 @@ } }, "node_modules/postcss": { - "version": "8.4.40", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz", - "integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "dev": true, "funding": [ { @@ -8041,8 +8050,8 @@ "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.1", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -8943,10 +8952,11 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -8993,20 +9003,20 @@ "license": "MIT" }, "node_modules/start-server-and-test": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-2.0.5.tgz", - "integrity": "sha512-2CV4pz69NJVJKQmJeSr+O+SPtOreu0yxvhPmSXclzmAKkPREuMabyMh+Txpzemjx0RDzXOcG2XkhiUuxjztSQw==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-2.0.8.tgz", + "integrity": "sha512-v2fV6NV2F7tL1ocwfI4Wpait+IKjRbT5l3ZZ+ZikXdMLmxYsS8ynGAsCQAUVXkVyGyS+UibsRnvgHkMvJIvCsw==", "dev": true, "license": "MIT", "dependencies": { "arg": "^5.0.2", "bluebird": "3.7.2", "check-more-types": "2.24.0", - "debug": "4.3.6", + "debug": "4.3.7", "execa": "5.1.1", "lazy-ass": "1.6.0", "ps-tree": "1.2.0", - "wait-on": "7.2.0" + "wait-on": "8.0.1" }, "bin": { "server-test": "src/bin/start.js", @@ -10219,12 +10229,14 @@ } }, "node_modules/wait-on": { - "version": "7.2.0", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.1.tgz", + "integrity": "sha512-1wWQOyR2LVVtaqrcIL2+OM+x7bkpmzVROa0Nf6FryXkS+er5Sa1kzFGjzZRqLnHa3n1rACFLeTwUqE1ETL9Mig==", "dev": true, "license": "MIT", "dependencies": { - "axios": "^1.6.1", - "joi": "^17.11.0", + "axios": "^1.7.7", + "joi": "^17.13.3", "lodash": "^4.17.21", "minimist": "^1.2.8", "rxjs": "^7.8.1" diff --git a/package.json b/package.json index c9db7005..b67e00a2 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "lint-staged": { "*.{ts,js,tsx,jsx}": "eslint --fix" }, - "version": "0.38.1", + "version": "0.39.0", "engines": { "node": "20.x" }, diff --git a/src/__mocks__/playerAliasMock.ts b/src/__mocks__/playerAliasMock.ts index 55df3134..c7b38a69 100644 --- a/src/__mocks__/playerAliasMock.ts +++ b/src/__mocks__/playerAliasMock.ts @@ -7,6 +7,7 @@ export default function playerAliasMock(extra: Partial = {}): Playe service: PlayerAliasService.STEAM, identifier: 'yxre12', player: playerMock(), + lastSeenAt: new Date().toISOString(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), ...extra diff --git a/src/api/updateGame.ts b/src/api/updateGame.ts index 4adfeedc..de5500be 100644 --- a/src/api/updateGame.ts +++ b/src/api/updateGame.ts @@ -5,7 +5,7 @@ import makeValidatedRequest from './makeValidatedRequest' import { Prop } from '../entities/prop' const updateGame = makeValidatedRequest( - (gameId: number, data: { props: Prop[] }) => api.patch(`/games/${gameId}`, data), + (gameId: number, data: { name?: string, props?: Prop[] }) => api.patch(`/games/${gameId}`, data), z.object({ game: gameSchema }) diff --git a/src/components/PlayerAliases.tsx b/src/components/PlayerAliases.tsx index 8753fd57..a3241028 100644 --- a/src/components/PlayerAliases.tsx +++ b/src/components/PlayerAliases.tsx @@ -13,7 +13,7 @@ type PlayerAliasesProps = { export default function PlayerAliases({ aliases }: PlayerAliasesProps) { - const sortedAliases = useSortedItems(aliases, 'createdAt') + const sortedAliases = useSortedItems(aliases, 'lastSeenAt') const getIcon = useCallback((alias: PlayerAlias) => { /* v8ignore next */ diff --git a/src/components/__tests__/PlayerAliases.test.tsx b/src/components/__tests__/PlayerAliases.test.tsx index 5fdfe655..104eb72c 100644 --- a/src/components/__tests__/PlayerAliases.test.tsx +++ b/src/components/__tests__/PlayerAliases.test.tsx @@ -16,9 +16,9 @@ describe('', () => { it('should render the latest alias and an indicator for how many more', () => { const aliases: PlayerAlias[] = [ - playerAliasMock({ service: PlayerAliasService.STEAM, identifier: 'yxre12', createdAt: '2024-10-28 10:00:00' }), - playerAliasMock({ service: PlayerAliasService.USERNAME, identifier: 'ryet12', createdAt: '2024-10-27 10:00:00' }), - playerAliasMock({ service: PlayerAliasService.EPIC, identifier: 'epic_23rd', createdAt: '2024-10-26 10:00:00' }) + playerAliasMock({ service: PlayerAliasService.STEAM, identifier: 'yxre12', lastSeenAt: '2024-10-28 10:00:00' }), + playerAliasMock({ service: PlayerAliasService.USERNAME, identifier: 'ryet12', lastSeenAt: '2024-10-27 10:00:00' }), + playerAliasMock({ service: PlayerAliasService.EPIC, identifier: 'epic_23rd', lastSeenAt: '2024-10-26 10:00:00' }) ] render() diff --git a/src/entities/playerAlias.ts b/src/entities/playerAlias.ts index e7795be7..d221d6e1 100644 --- a/src/entities/playerAlias.ts +++ b/src/entities/playerAlias.ts @@ -15,6 +15,7 @@ export const playerAliasSchema = z.object({ service: z.string(), identifier: z.string(), player: z.lazy(() => basePlayerSchema), + lastSeenAt: z.string().datetime(), createdAt: z.string().datetime(), updatedAt: z.string().datetime() }) diff --git a/src/pages/Organisation.tsx b/src/pages/Organisation.tsx index 37804dec..5fe46488 100644 --- a/src/pages/Organisation.tsx +++ b/src/pages/Organisation.tsx @@ -1,6 +1,6 @@ -import { useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import { format } from 'date-fns' -import { useRecoilValue } from 'recoil' +import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil' import Page from '../components/Page' import DateCell from '../components/tables/cells/DateCell' import TableBody from '../components/tables/TableBody' @@ -8,8 +8,8 @@ import TableCell from '../components/tables/TableCell' import organisationState from '../state/organisationState' import useOrganisation from '../api/useOrganisation' import Button from '../components/Button' -import { IconPlus } from '@tabler/icons-react' -import ErrorMessage from '../components/ErrorMessage' +import { IconCheck, IconPencil, IconPlus, IconX } from '@tabler/icons-react' +import ErrorMessage, { TaloError } from '../components/ErrorMessage' import NewInvite from '../modals/NewInvite' import SecondaryNav from '../components/SecondaryNav' import { secondaryNavRoutes } from './Dashboard' @@ -19,6 +19,10 @@ import userState, { AuthedUser } from '../state/userState' import SecondaryTitle from '../components/SecondaryTitle' import { UserType } from '../entities/user' import userTypeMap from '../constants/userTypeMap' +import TextInput from '../components/TextInput' +import updateGame from '../api/updateGame' +import buildError from '../utils/buildError' +import activeGameState, { SelectedActiveGameState } from '../state/activeGameState' function Organisation() { const organisation = useRecoilValue(organisationState) @@ -26,6 +30,62 @@ function Organisation() { const [showModal, setShowModal] = useState(false) const user = useRecoilValue(userState) as AuthedUser + const [editingGameId, setEditingGameId] = useState(null) + const [editingGameName, setEditingGameName] = useState('') + const [editingGameNameError, setEditingGameNameError] = useState(null) + const setUser = useSetRecoilState(userState) + const [activeGame, setActiveGame] = useRecoilState(activeGameState) as SelectedActiveGameState + + useEffect(() => { + if (editingGameId) { + setEditingGameName(games.find((game) => game.id === editingGameId)!.name) + } else { + setEditingGameName('') + } + + setEditingGameNameError(null) + }, [editingGameId, games]) + + const onUpdateGameName = useCallback(async () => { + try { + const { game } = await updateGame(editingGameId!, { name: editingGameName }) + const updatedOrg = await mutate((data) => { + return { + ...data!, + games: data!.games.map((existingGame) => { + if (existingGame.id === editingGameId) return { ...existingGame, name: game.name } + return existingGame + }) + } + }, false) + + // game switcher dropdown + setUser({ + ...user, + organisation: { + ...user.organisation, + games: updatedOrg!.games + } + }) + // game switcher active game + if (activeGame.id === editingGameId) { + setActiveGame({ ...activeGame, name: game.name }) + } + + setEditingGameId(null) + } catch (err) { + setEditingGameNameError(buildError(err)) + } + }, [activeGame, editingGameId, editingGameName, mutate, setActiveGame, setUser, user]) + + const onGameNameInputKeyDown = useCallback((e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + onUpdateGameName() + } else if (e.key === 'Escape') { + setEditingGameId(null) + } + }, [onUpdateGameName]) + return ( Games + {editingGameNameError && } + {(game) => ( <> - {game.name} + + {editingGameId === game.id && + <> + +
+
({ @@ -117,7 +117,7 @@ export default function PlayerProfile() { <> {format(new Date(alias.createdAt), 'dd MMM Y, HH:mm')} - {format(new Date(alias.updatedAt), 'dd MMM Y, HH:mm')} + {format(new Date(alias.lastSeenAt), 'dd MMM Y, HH:mm')} )}