mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-06-13 09:34:47 -07:00
upgrade packages + improve typing
This commit is contained in:
parent
fd55f4fb7f
commit
19f5eefdd2
138 changed files with 4504 additions and 11015 deletions
|
|
@ -1,48 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
"root": true,
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"parserOptions": {"ecmaVersion": 2020, "sourceType": "module", "ecmaFeatures": {"jsx": true}},
|
|
||||||
"plugins": [
|
|
||||||
"@typescript-eslint"
|
|
||||||
],
|
|
||||||
"ignorePatterns": ["node_modules/*", "build/*", "public/pb/*"],
|
|
||||||
"env": {
|
|
||||||
"jest": true
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"array-bracket-spacing": ["error", "never"],
|
|
||||||
"arrow-spacing": ["error", {"before": true, "after": true}],
|
|
||||||
"block-spacing": ["error", "always"],
|
|
||||||
"brace-style": ["error", "1tbs", {"allowSingleLine": false}],
|
|
||||||
"comma-spacing": ["error", {"before": false, "after": true}],
|
|
||||||
"comma-style": ["error", "last"],
|
|
||||||
"computed-property-spacing": ["error", "never"],
|
|
||||||
"curly": ["error", "all"],
|
|
||||||
"dot-location": ["error", "property"],
|
|
||||||
"eol-last": ["error"],
|
|
||||||
"func-names": ["warn"],
|
|
||||||
"indent": ["error", 2, {"SwitchCase": 1}],
|
|
||||||
"key-spacing": ["error", {"beforeColon": false, "afterColon": true}],
|
|
||||||
"keyword-spacing": ["error"],
|
|
||||||
"linebreak-style": ["error", (process.platform === "win32" ? "windows" : "unix")],
|
|
||||||
"max-len": ["error", {"code": 140}],
|
|
||||||
"no-eq-null": ["off"],
|
|
||||||
"no-func-assign": ["error"],
|
|
||||||
"no-inline-comments": ["error"],
|
|
||||||
"no-mixed-spaces-and-tabs": ["error"],
|
|
||||||
"no-multi-spaces": ["error"],
|
|
||||||
"no-spaced-func": ["error"],
|
|
||||||
"no-trailing-spaces": ["error"],
|
|
||||||
"no-var": ["error"],
|
|
||||||
"object-curly-spacing": ["error", "always"],
|
|
||||||
"one-var": ["error", "never"],
|
|
||||||
"one-var-declaration-per-line": ["error"],
|
|
||||||
"quotes": ["error", "single"],
|
|
||||||
"semi-spacing": ["error", {"before": false, "after": true}],
|
|
||||||
"space-before-blocks": ["error"],
|
|
||||||
"space-before-function-paren": ["error", {"asyncArrow": "always", "anonymous": "never", "named": "never"}],
|
|
||||||
"space-in-parens": ["error", "never"],
|
|
||||||
"space-infix-ops": ["error"],
|
|
||||||
"space-unary-ops": ["error", {"words": true, "nonwords": false}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
78
webclient/eslint.config.mjs
Normal file
78
webclient/eslint.config.mjs
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
import js from '@eslint/js';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
import globals from 'globals';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
// Global ignores
|
||||||
|
{ ignores: ['node_modules/**', 'build/**', 'public/pb/**', 'src/generated/**'] },
|
||||||
|
|
||||||
|
// Base JS recommended
|
||||||
|
js.configs.recommended,
|
||||||
|
|
||||||
|
// TypeScript recommended (sets up parser + plugin)
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
|
||||||
|
// Project-specific config
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
sourceType: 'module',
|
||||||
|
parserOptions: {
|
||||||
|
ecmaFeatures: { jsx: true },
|
||||||
|
},
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.es2020,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// TypeScript overrides
|
||||||
|
'@typescript-eslint/no-unused-vars': ['warn', { varsIgnorePattern: '^_', argsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' }],
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-require-imports': 'off',
|
||||||
|
'@typescript-eslint/no-empty-object-type': 'off',
|
||||||
|
'@typescript-eslint/no-unsafe-function-type': 'off',
|
||||||
|
|
||||||
|
// Disable new rules not in original config
|
||||||
|
'prefer-const': 'off',
|
||||||
|
'no-extra-boolean-cast': 'off',
|
||||||
|
'no-case-declarations': 'off',
|
||||||
|
'preserve-caught-error': 'off',
|
||||||
|
|
||||||
|
// Spacing / formatting
|
||||||
|
'array-bracket-spacing': ['error', 'never'],
|
||||||
|
'arrow-spacing': ['error', { before: true, after: true }],
|
||||||
|
'block-spacing': ['error', 'always'],
|
||||||
|
'brace-style': ['error', '1tbs', { allowSingleLine: false }],
|
||||||
|
'comma-spacing': ['error', { before: false, after: true }],
|
||||||
|
'comma-style': ['error', 'last'],
|
||||||
|
'computed-property-spacing': ['error', 'never'],
|
||||||
|
'curly': ['error', 'all'],
|
||||||
|
'dot-location': ['error', 'property'],
|
||||||
|
'eol-last': ['error'],
|
||||||
|
'func-names': ['warn'],
|
||||||
|
'indent': ['error', 2, { SwitchCase: 1 }],
|
||||||
|
'key-spacing': ['error', { beforeColon: false, afterColon: true }],
|
||||||
|
'keyword-spacing': ['error'],
|
||||||
|
'linebreak-style': ['error', process.platform === 'win32' ? 'windows' : 'unix'],
|
||||||
|
'max-len': ['error', { code: 140 }],
|
||||||
|
'no-eq-null': ['off'],
|
||||||
|
'no-func-assign': ['error'],
|
||||||
|
'no-inline-comments': ['error'],
|
||||||
|
'no-mixed-spaces-and-tabs': ['error'],
|
||||||
|
'no-multi-spaces': ['error'],
|
||||||
|
'no-trailing-spaces': ['error'],
|
||||||
|
'no-var': ['error'],
|
||||||
|
'object-curly-spacing': ['error', 'always'],
|
||||||
|
'one-var': ['error', 'never'],
|
||||||
|
'one-var-declaration-per-line': ['error'],
|
||||||
|
'quotes': ['error', 'single'],
|
||||||
|
'semi-spacing': ['error', { before: false, after: true }],
|
||||||
|
'space-before-blocks': ['error'],
|
||||||
|
'space-before-function-paren': ['error', { asyncArrow: 'always', anonymous: 'never', named: 'never' }],
|
||||||
|
'space-in-parens': ['error', 'never'],
|
||||||
|
'space-infix-ops': ['error'],
|
||||||
|
'space-unary-ops': ['error', { words: true, nonwords: false }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
11370
webclient/package-lock.json
generated
11370
webclient/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -3,17 +3,17 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prebuild": "node prebuild.js",
|
"prebuild": "npm run proto:generate && node prebuild.js",
|
||||||
"prestart": "node prebuild.js",
|
"prestart": "npm run proto:generate && node prebuild.js",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"lint": "eslint src/**/*.{ts,tsx}",
|
"lint": "eslint src/",
|
||||||
"lint:fix": "eslint src/**/*.{ts,tsx} --fix",
|
"lint:fix": "eslint src/ --fix",
|
||||||
"golden": "npm run lint && npm run test",
|
"golden": "npm run lint && npm run test",
|
||||||
"prepare": "cd .. && husky install",
|
"prepare": "cd .. && husky",
|
||||||
"translate": "node prebuild.js -i18nOnly",
|
"translate": "node prebuild.js -i18nOnly",
|
||||||
"proto:generate": "npx buf generate"
|
"proto:generate": "npx buf generate"
|
||||||
},
|
},
|
||||||
|
|
@ -21,63 +21,61 @@
|
||||||
"@bufbuild/protobuf": "^2.11.0",
|
"@bufbuild/protobuf": "^2.11.0",
|
||||||
"@emotion/react": "^11.8.2",
|
"@emotion/react": "^11.8.2",
|
||||||
"@emotion/styled": "^11.8.1",
|
"@emotion/styled": "^11.8.1",
|
||||||
"@mui/icons-material": "^5.5.1",
|
"@mui/icons-material": "^7.3.10",
|
||||||
"@mui/material": "^5.5.1",
|
"@mui/material": "^7.3.10",
|
||||||
|
"@reduxjs/toolkit": "^2.11.2",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dexie": "^3.2.2",
|
"dexie": "^4.4.2",
|
||||||
"dompurify": "^3.3.3",
|
"dompurify": "^3.3.3",
|
||||||
"final-form": "^4.20.6",
|
"final-form": "^5.0.0",
|
||||||
"final-form-set-field-touched": "^1.0.1",
|
"final-form-set-field-touched": "^1.0.1",
|
||||||
"i18next": "^22.0.4",
|
"i18next": "^26.0.4",
|
||||||
"i18next-browser-languagedetector": "^7.0.0",
|
"i18next-browser-languagedetector": "^8.2.1",
|
||||||
"i18next-icu": "^2.0.3",
|
"i18next-icu": "^2.0.3",
|
||||||
"intl-messageformat": "^10.2.1",
|
"intl-messageformat": "^11.2.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-final-form": "^6.5.8",
|
"react-final-form": "^7.0.0",
|
||||||
"react-final-form-listeners": "^1.0.3",
|
"react-final-form-listeners": "^3.0.0",
|
||||||
"react-i18next": "^12.0.0",
|
"react-i18next": "^17.0.2",
|
||||||
"react-redux": "^8.0.4",
|
"react-redux": "^9.2.0",
|
||||||
"react-router-dom": "^6.2.2",
|
"react-router-dom": "^7.14.1",
|
||||||
"react-virtualized-auto-sizer": "^1.0.6",
|
"react-virtualized-auto-sizer": "^2.0.3",
|
||||||
"react-window": "^1.8.6",
|
"react-window": "^2.2.7",
|
||||||
"redux": "^4.1.2",
|
|
||||||
"redux-form": "^8.3.8",
|
|
||||||
"redux-thunk": "^2.4.1",
|
|
||||||
"rxjs": "^7.5.4"
|
"rxjs": "^7.5.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@bufbuild/buf": "^1.67.0",
|
"@bufbuild/buf": "^1.67.0",
|
||||||
"@bufbuild/protoc-gen-es": "^2.11.0",
|
"@bufbuild/protoc-gen-es": "^2.11.0",
|
||||||
|
"@eslint/js": "^10.0.1",
|
||||||
"@mui/types": "^7.1.3",
|
"@mui/types": "^7.1.3",
|
||||||
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@testing-library/jest-dom": "^6.4.0",
|
"@testing-library/jest-dom": "^6.4.0",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^16.3.2",
|
||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
"@types/jquery": "^3.5.14",
|
|
||||||
"@types/lodash": "^4.14.179",
|
"@types/lodash": "^4.14.179",
|
||||||
"@types/node": "18.11.7",
|
"@types/node": "^22.19.17",
|
||||||
"@types/prop-types": "^15.7.4",
|
"@types/prop-types": "^15.7.4",
|
||||||
"@types/react": "18.0.24",
|
"@types/react": "18.0.24",
|
||||||
"@types/react-dom": "18.0.8",
|
"@types/react-dom": "18.0.8",
|
||||||
"@types/react-redux": "^7.1.23",
|
|
||||||
"@types/react-router-dom": "^5.3.3",
|
|
||||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||||
"@types/react-window": "^1.8.5",
|
"@types/react-window": "^1.8.5",
|
||||||
"@types/redux-form": "^8.3.3",
|
"@typescript-eslint/eslint-plugin": "^8.58.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.14.0",
|
"@typescript-eslint/parser": "^8.58.2",
|
||||||
"@typescript-eslint/parser": "^5.14.0",
|
"@vitejs/plugin-react": "^5.2.0",
|
||||||
"@vitejs/plugin-react": "^4.2.0",
|
"@vitest/coverage-v8": "^4.1.4",
|
||||||
"@vitest/coverage-v8": "^1.3.0",
|
"eslint": "^10.2.0",
|
||||||
"eslint": "^8.0.0",
|
"fs-extra": "^11.3.4",
|
||||||
"fs-extra": "^10.0.1",
|
"globals": "^17.5.0",
|
||||||
"husky": "^8.0.1",
|
"husky": "^9.1.7",
|
||||||
"jsdom": "^24.0.0",
|
"jsdom": "^29.0.2",
|
||||||
"typescript": "^4.6.2",
|
"typescript": "~5.8",
|
||||||
"vite": "^5.1.0",
|
"typescript-eslint": "^8.58.2",
|
||||||
"vite-tsconfig-paths": "^4.3.1",
|
"vite": "^6.4.2",
|
||||||
"vitest": "^1.3.0"
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
|
"vitest": "^4.1.4"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,16 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { Navigate } from 'react-router-dom';
|
import { Navigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { ServerSelectors } from 'store';
|
import { ServerSelectors } from 'store';
|
||||||
import { RouteEnum } from 'types';
|
import { RouteEnum } from 'types';
|
||||||
|
import { useAppSelector } from 'store/store';
|
||||||
import { AuthenticationService } from 'api';
|
import { AuthenticationService } from 'api';
|
||||||
|
|
||||||
const AuthGuard = ({ state }: AuthGuardProps) => {
|
const AuthGuard = () => {
|
||||||
|
const state = useAppSelector(s => ServerSelectors.getState(s));
|
||||||
return !AuthenticationService.isConnected(state)
|
return !AuthenticationService.isConnected(state)
|
||||||
? <Navigate to={RouteEnum.LOGIN} />
|
? <Navigate to={RouteEnum.LOGIN} />
|
||||||
: <div></div>;
|
: <div></div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface AuthGuardProps {
|
export default AuthGuard;
|
||||||
state: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
state: ServerSelectors.getState(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(AuthGuard);
|
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,16 @@
|
||||||
import React, { Component } from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { Navigate } from 'react-router-dom';
|
import { Navigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { ServerSelectors } from 'store';
|
import { ServerSelectors } from 'store';
|
||||||
import { User } from 'types';
|
|
||||||
|
|
||||||
import { AuthenticationService } from 'api';
|
import { AuthenticationService } from 'api';
|
||||||
import { RouteEnum } from 'types';
|
import { RouteEnum } from 'types';
|
||||||
|
import { useAppSelector } from 'store/store';
|
||||||
|
|
||||||
class ModGuard extends Component<ModGuardProps> {
|
const ModGuard = () => {
|
||||||
render() {
|
const user = useAppSelector(state => ServerSelectors.getUser(state));
|
||||||
return !AuthenticationService.isModerator(this.props.user)
|
return !AuthenticationService.isModerator(user)
|
||||||
? <Navigate to={RouteEnum.SERVER} />
|
? <Navigate to={RouteEnum.SERVER} />
|
||||||
: '';
|
: <></>;
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ModGuardProps {
|
export default ModGuard;
|
||||||
user: User;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
user: ServerSelectors.getUser(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(ModGuard);
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { InputField } from 'components';
|
||||||
|
|
||||||
import './InputAction.css';
|
import './InputAction.css';
|
||||||
|
|
||||||
const InputAction = ({ action, label, name, validate, disabled }) => (
|
const InputAction = ({ action, label, name, validate = () => false, disabled = false }) => (
|
||||||
<div className="input-action">
|
<div className="input-action">
|
||||||
<div className="input-action__item">
|
<div className="input-action__item">
|
||||||
<Field label={label} name={name} component={InputField} validate={validate} />
|
<Field label={label} name={name} component={InputField} validate={validate} />
|
||||||
|
|
@ -19,9 +19,4 @@ const InputAction = ({ action, label, name, validate, disabled }) => (
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
InputAction.defaultProps = {
|
|
||||||
disabled: false,
|
|
||||||
validate: () => false,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default InputAction;
|
export default InputAction;
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ const KnownHosts = (props) => {
|
||||||
}, [loadKnownHosts]);
|
}, [loadKnownHosts]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { hosts, selectedHost } = hostsState;
|
const { selectedHost } = hostsState;
|
||||||
|
|
||||||
if (selectedHost?.id) {
|
if (selectedHost?.id) {
|
||||||
updateLastSelectedHost(selectedHost.id).then(() => {
|
updateLastSelectedHost(selectedHost.id).then(() => {
|
||||||
|
|
@ -255,7 +255,7 @@ const KnownHosts = (props) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ host.editable && (
|
{ host.editable && (
|
||||||
<IconButton className='KnownHosts-item__edit' size='small' color='primary' onClick={(e) => {
|
<IconButton className='KnownHosts-item__edit' size='small' color='primary' onClick={() => {
|
||||||
openEditKnownHostDialog(hostsState.hosts[index]);
|
openEditKnownHostDialog(hostsState.hosts[index]);
|
||||||
}}>
|
}}>
|
||||||
<EditRoundedIcon fontSize='small' />
|
<EditRoundedIcon fontSize='small' />
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
// eslint-disable-next-line
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Select, MenuItem } from '@mui/material';
|
import { Select, MenuItem } from '@mui/material';
|
||||||
import FormControl from '@mui/material/FormControl';
|
import FormControl from '@mui/material/FormControl';
|
||||||
import InputLabel from '@mui/material/InputLabel';
|
|
||||||
|
|
||||||
import { Images } from 'images/Images';
|
import { Images } from 'images/Images';
|
||||||
import { Language, LanguageCountry, LanguageNative } from 'types';
|
import { Language, LanguageCountry, LanguageNative } from 'types';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// eslint-disable-next-line
|
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import Popover from '@mui/material/Popover';
|
import Popover from '@mui/material/Popover';
|
||||||
|
|
@ -17,7 +17,7 @@ const classes = {
|
||||||
popoverContent: `${PREFIX}-popoverContent`
|
popoverContent: `${PREFIX}-popoverContent`
|
||||||
};
|
};
|
||||||
|
|
||||||
const Root = styled('span')(({ theme }) => ({
|
const Root = styled('span')(() => ({
|
||||||
[`& .${classes.popover}`]: {
|
[`& .${classes.popover}`]: {
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import {
|
||||||
import CardCallout from './CardCallout';
|
import CardCallout from './CardCallout';
|
||||||
import './Message.css';
|
import './Message.css';
|
||||||
|
|
||||||
const Message = ({ message: { message, messageType, timeOf, timeReceived } }) => (
|
const Message = ({ message: { message } }) => (
|
||||||
<div className='message'>
|
<div className='message'>
|
||||||
<div className='message__detail'>
|
<div className='message__detail'>
|
||||||
<ParsedMessage message={message} />
|
<ParsedMessage message={message} />
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
import { Component, CElement } from 'react';
|
import { Component, CElement } from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import Grid from '@mui/material/Grid';
|
import Grid from '@mui/material/Grid';
|
||||||
import Hidden from '@mui/material/Hidden';
|
|
||||||
|
|
||||||
import './ThreePaneLayout.css';
|
import './ThreePaneLayout.css';
|
||||||
|
|
||||||
|
|
@ -12,25 +10,23 @@ class ThreePaneLayout extends Component<ThreePaneLayoutProps> {
|
||||||
return (
|
return (
|
||||||
<div className="three-pane-layout">
|
<div className="three-pane-layout">
|
||||||
<Grid container rowSpacing={0} columnSpacing={2} className="grid">
|
<Grid container rowSpacing={0} columnSpacing={2} className="grid">
|
||||||
<Grid item xs={12} md={9} lg={10} className="grid-main">
|
<Grid size={{ xs: 12, md: 9, lg: 10 }} className="grid-main">
|
||||||
<Grid item className={
|
<Grid className={
|
||||||
'grid-main__top'
|
'grid-main__top'
|
||||||
+ (this.props.fixedHeight ? ' fixedHeight' : '')
|
+ (this.props.fixedHeight ? ' fixedHeight' : '')
|
||||||
}>
|
}>
|
||||||
{this.props.top}
|
{this.props.top}
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item className={
|
<Grid className={
|
||||||
'grid-main__bottom'
|
'grid-main__bottom'
|
||||||
+ (this.props.fixedHeight ? ' fixedHeight' : '')
|
+ (this.props.fixedHeight ? ' fixedHeight' : '')
|
||||||
}>
|
}>
|
||||||
{this.props.bottom}
|
{this.props.bottom}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Hidden mdDown>
|
<Grid size={{ md: 3, lg: 2 }} sx={{ display: { xs: 'none', md: 'block' } }} className="grid-side">
|
||||||
<Grid item md={3} lg={2} className="grid-side">
|
{this.props.side}
|
||||||
{this.props.side}
|
</Grid>
|
||||||
</Grid>
|
|
||||||
</Hidden>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -44,6 +40,4 @@ interface ThreePaneLayoutProps {
|
||||||
fixedHeight?: boolean,
|
fixedHeight?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = state => ({});
|
export default ThreePaneLayout;
|
||||||
|
|
||||||
export default connect(mapStateToProps)(ThreePaneLayout);
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
|
|
||||||
import Alert, { AlertProps } from '@mui/material/Alert';
|
import Alert from '@mui/material/Alert';
|
||||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||||
import Slide, { SlideProps } from '@mui/material/Slide';
|
import Slide from '@mui/material/Slide';
|
||||||
import Snackbar from '@mui/material/Snackbar';
|
import Snackbar from '@mui/material/Snackbar';
|
||||||
|
|
||||||
const iconMapping = {
|
const iconMapping = {
|
||||||
|
|
@ -11,7 +11,7 @@ const iconMapping = {
|
||||||
}
|
}
|
||||||
|
|
||||||
function Toast(props) {
|
function Toast(props) {
|
||||||
const { open, onClose, severity, autoHideDuration, children } = props
|
const { open, onClose, severity = 'success', autoHideDuration = 10000, children } = props
|
||||||
|
|
||||||
const rootElemRef = React.useRef(document.createElement('div'));
|
const rootElemRef = React.useRef(document.createElement('div'));
|
||||||
|
|
||||||
|
|
@ -37,9 +37,9 @@ function Toast(props) {
|
||||||
TransitionComponent={TransitionLeft}
|
TransitionComponent={TransitionLeft}
|
||||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||||
>
|
>
|
||||||
<Alert onClose={handleClose} severity={severity} iconMapping={iconMapping}>
|
<Alert onClose={handleClose} severity={severity} iconMapping={iconMapping}
|
||||||
{children}
|
slotProps={{ message: { children } }}
|
||||||
</Alert>
|
/>
|
||||||
</Snackbar>
|
</Snackbar>
|
||||||
)
|
)
|
||||||
if (!rootElemRef.current) {
|
if (!rootElemRef.current) {
|
||||||
|
|
@ -52,12 +52,6 @@ function Toast(props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast.defaultProps = {
|
|
||||||
severity: 'success',
|
|
||||||
// 10s wait before automatically dismissing the Toast.
|
|
||||||
autoHideDuration: 10000,
|
|
||||||
}
|
|
||||||
|
|
||||||
function TransitionLeft(props) {
|
function TransitionLeft(props) {
|
||||||
return <Slide {...props} direction="left" />;
|
return <Slide {...props} direction="left" />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { createContext, FC, PropsWithChildren, ReactChild, ReactNode, useContext, useEffect, useReducer, ContextType, Context } from 'react'
|
import { createContext, FC, PropsWithChildren, ReactChild, ReactNode, useContext, useEffect, useReducer, Context } from 'react'
|
||||||
|
|
||||||
import { ACTIONS, initialState, reducer } from './reducer';
|
import { ACTIONS, initialState, reducer } from './reducer';
|
||||||
import Toast from './Toast'
|
import Toast from './Toast'
|
||||||
|
|
@ -18,10 +18,10 @@ interface ToastState {
|
||||||
|
|
||||||
const ToastContext: Context<any> = createContext<ToastState>({
|
const ToastContext: Context<any> = createContext<ToastState>({
|
||||||
toasts: new Map<string, ToastEntry>(),
|
toasts: new Map<string, ToastEntry>(),
|
||||||
addToast: (key, children) => {},
|
addToast: (_key, _children) => {},
|
||||||
openToast: (key) => {},
|
openToast: (_key) => {},
|
||||||
closeToast: (key) => {},
|
closeToast: (_key) => {},
|
||||||
removeToast: (key) => {},
|
removeToast: (_key) => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ToastProvider: FC<PropsWithChildren> = (props) => {
|
export const ToastProvider: FC<PropsWithChildren> = (props) => {
|
||||||
|
|
@ -56,7 +56,7 @@ export interface ToastHookOptions {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useToast<ToastHookOptions>({ key, children }) {
|
export function useToast({ key, children }) {
|
||||||
const { addToast, openToast, closeToast, removeToast } = useContext(ToastContext)
|
const { addToast, openToast, closeToast, removeToast } = useContext(ToastContext)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
// eslint-disable-next-line
|
|
||||||
import React, { Component } from "react";
|
import React, { useState } from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { NavLink, generatePath } from 'react-router-dom';
|
import { NavLink, generatePath } from 'react-router-dom';
|
||||||
|
|
||||||
import Menu from '@mui/material/Menu';
|
import Menu from '@mui/material/Menu';
|
||||||
|
|
@ -10,141 +9,85 @@ import { Images } from 'images/Images';
|
||||||
import { SessionService } from 'api';
|
import { SessionService } from 'api';
|
||||||
import { ServerSelectors } from 'store';
|
import { ServerSelectors } from 'store';
|
||||||
import { RouteEnum, User } from 'types';
|
import { RouteEnum, User } from 'types';
|
||||||
|
import { useAppSelector } from 'store/store';
|
||||||
|
|
||||||
import './UserDisplay.css';
|
import './UserDisplay.css';
|
||||||
|
|
||||||
|
|
||||||
class UserDisplay extends Component<UserDisplayProps, UserDisplayState> {
|
const UserDisplay = ({ user }: UserDisplayProps) => {
|
||||||
constructor(props) {
|
const buddyList = useAppSelector(state => ServerSelectors.getBuddyList(state));
|
||||||
super(props);
|
const ignoreList = useAppSelector(state => ServerSelectors.getIgnoreList(state));
|
||||||
|
const [position, setPosition] = useState<{ x: number; y: number } | null>(null);
|
||||||
|
|
||||||
this.handleClick = this.handleClick.bind(this);
|
const { name, country } = user;
|
||||||
this.handleClose = this.handleClose.bind(this);
|
|
||||||
this.navigateToUserProfile = this.navigateToUserProfile.bind(this);
|
|
||||||
this.addToBuddyList = this.addToBuddyList.bind(this);
|
|
||||||
this.removeFromBuddyList = this.removeFromBuddyList.bind(this);
|
|
||||||
this.addToIgnoreList = this.addToIgnoreList.bind(this);
|
|
||||||
this.removeFromIgnoreList = this.removeFromIgnoreList.bind(this);
|
|
||||||
|
|
||||||
this.isABuddy = this.isABuddy.bind(this);
|
const handleClick = (event) => {
|
||||||
this.isIgnored = this.isIgnored.bind(this);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
position: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
handleClick(event) {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
setPosition({ x: event.clientX + 2, y: event.clientY + 4 });
|
||||||
|
};
|
||||||
|
|
||||||
this.setState({
|
const handleClose = () => setPosition(null);
|
||||||
position: {
|
|
||||||
x: event.clientX + 2,
|
|
||||||
y: event.clientY + 4,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleClose() {
|
const isABuddy = buddyList.filter(u => u.name === user.name).length;
|
||||||
this.setState({
|
const isIgnored = ignoreList.filter(u => u.name === user.name).length;
|
||||||
position: null
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
navigateToUserProfile() {
|
const onAddBuddy = () => {
|
||||||
this.handleClose();
|
SessionService.addToBuddyList(user.name);
|
||||||
}
|
handleClose();
|
||||||
|
};
|
||||||
|
const onRemoveBuddy = () => {
|
||||||
|
SessionService.removeFromBuddyList(user.name);
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
const onAddIgnore = () => {
|
||||||
|
SessionService.addToIgnoreList(user.name);
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
const onRemoveIgnore = () => {
|
||||||
|
SessionService.removeFromIgnoreList(user.name);
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
addToBuddyList() {
|
return (
|
||||||
SessionService.addToBuddyList(this.props.user.name);
|
<div className="user-display">
|
||||||
this.handleClose();
|
<NavLink to={generatePath(RouteEnum.PLAYER, { name })} className="plain-link">
|
||||||
}
|
<div className="user-display__details" onContextMenu={handleClick}>
|
||||||
|
<img className="user-display__country" src={Images.Countries[country]} alt={country}></img>
|
||||||
removeFromBuddyList() {
|
<div className="user-display__name single-line-ellipsis">{name}</div>
|
||||||
SessionService.removeFromBuddyList(this.props.user.name);
|
|
||||||
this.handleClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
addToIgnoreList() {
|
|
||||||
SessionService.addToIgnoreList(this.props.user.name);
|
|
||||||
this.handleClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
removeFromIgnoreList() {
|
|
||||||
SessionService.removeFromIgnoreList(this.props.user.name);
|
|
||||||
this.handleClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
isABuddy() {
|
|
||||||
return this.props.buddyList.filter(user => user.name === this.props.user.name).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
isIgnored() {
|
|
||||||
return this.props.ignoreList.filter(user => user.name === this.props.user.name).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { user } = this.props;
|
|
||||||
const { position } = this.state;
|
|
||||||
const { name, country } = user;
|
|
||||||
|
|
||||||
const isABuddy = this.isABuddy();
|
|
||||||
const isIgnored = this.isIgnored();
|
|
||||||
|
|
||||||
// console.log('user', name, !!isABuddy, !!isIgnored);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="user-display">
|
|
||||||
<NavLink to={generatePath(RouteEnum.PLAYER, { name })} className="plain-link">
|
|
||||||
<div className="user-display__details" onContextMenu={this.handleClick}>
|
|
||||||
<img className="user-display__country" src={Images.Countries[country]} alt={country}></img>
|
|
||||||
<div className="user-display__name single-line-ellipsis">{name}</div>
|
|
||||||
</div>
|
|
||||||
</NavLink>
|
|
||||||
<div className="user-display__menu">
|
|
||||||
<Menu
|
|
||||||
open={Boolean(position)}
|
|
||||||
onClose={this.handleClose}
|
|
||||||
anchorReference='anchorPosition'
|
|
||||||
anchorPosition={
|
|
||||||
position !== null
|
|
||||||
? { top: position.y, left: position.x }
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<NavLink to={generatePath(RouteEnum.PLAYER, { name })} className="user-display__link plain-link">
|
|
||||||
<MenuItem dense>Chat</MenuItem>
|
|
||||||
</NavLink>
|
|
||||||
{
|
|
||||||
!isABuddy
|
|
||||||
? (<MenuItem dense onClick={this.addToBuddyList}>Add to Buddy List</MenuItem>)
|
|
||||||
: (<MenuItem dense onClick={this.removeFromBuddyList}>Remove From Buddy List</MenuItem>)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
!isIgnored
|
|
||||||
? (<MenuItem dense onClick={this.addToIgnoreList}>Add to Ignore List</MenuItem>)
|
|
||||||
: (<MenuItem dense onClick={this.removeFromIgnoreList}>Remove From Ignore List</MenuItem>)
|
|
||||||
}
|
|
||||||
</Menu>
|
|
||||||
</div>
|
</div>
|
||||||
|
</NavLink>
|
||||||
|
<div className="user-display__menu">
|
||||||
|
<Menu
|
||||||
|
open={Boolean(position)}
|
||||||
|
onClose={handleClose}
|
||||||
|
anchorReference='anchorPosition'
|
||||||
|
anchorPosition={
|
||||||
|
position !== null
|
||||||
|
? { top: position.y, left: position.x }
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<NavLink to={generatePath(RouteEnum.PLAYER, { name })} className="user-display__link plain-link">
|
||||||
|
<MenuItem dense>Chat</MenuItem>
|
||||||
|
</NavLink>
|
||||||
|
{
|
||||||
|
!isABuddy
|
||||||
|
? (<MenuItem dense onClick={onAddBuddy}>Add to Buddy List</MenuItem>)
|
||||||
|
: (<MenuItem dense onClick={onRemoveBuddy}>Remove From Buddy List</MenuItem>)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
!isIgnored
|
||||||
|
? (<MenuItem dense onClick={onAddIgnore}>Add to Ignore List</MenuItem>)
|
||||||
|
: (<MenuItem dense onClick={onRemoveIgnore}>Remove From Ignore List</MenuItem>)
|
||||||
|
}
|
||||||
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
}
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
interface UserDisplayProps {
|
interface UserDisplayProps {
|
||||||
user: User;
|
user: User;
|
||||||
buddyList: User[];
|
|
||||||
ignoreList: User[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserDisplayState {
|
export default UserDisplay;
|
||||||
position: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = (state) => ({
|
|
||||||
buddyList: ServerSelectors.getBuddyList(state),
|
|
||||||
ignoreList: ServerSelectors.getIgnoreList(state)
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(UserDisplay);
|
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,29 @@
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { FixedSizeList as List } from 'react-window';
|
import { List, RowComponentProps } from 'react-window';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
|
||||||
|
|
||||||
import './VirtualList.css';
|
import './VirtualList.css';
|
||||||
|
|
||||||
const VirtualList = ({ items, itemKey, className = {}, size = 30 }) => (
|
interface RowData {
|
||||||
<div className="virtual-list">
|
items: any[];
|
||||||
<AutoSizer>
|
}
|
||||||
{({ height, width }) => (
|
|
||||||
<List
|
const Row = ({ index, style, items }: RowComponentProps<RowData>) => (
|
||||||
className={`virtual-list__list ${className}`}
|
<div style={style}>
|
||||||
height={height}
|
{items[index]}
|
||||||
width={width}
|
|
||||||
itemData={items}
|
|
||||||
itemCount={items.length}
|
|
||||||
itemSize={size}
|
|
||||||
itemKey={itemKey}
|
|
||||||
>
|
|
||||||
{Row}
|
|
||||||
</List>
|
|
||||||
)}
|
|
||||||
</AutoSizer>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const Row = ({ data, index, style }) => (
|
const VirtualList = ({ items, className = {}, size = 30 }) => (
|
||||||
<div style={style}>
|
<div className="virtual-list">
|
||||||
{data[index]}
|
<List<RowData>
|
||||||
|
className={`virtual-list__list ${className}`}
|
||||||
|
rowCount={items.length}
|
||||||
|
rowHeight={size}
|
||||||
|
rowComponent={Row}
|
||||||
|
rowProps={{ items }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,30 @@
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import ListItem from '@mui/material/ListItem';
|
import ListItemButton from '@mui/material/ListItemButton';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
|
|
||||||
import { UserDisplay, VirtualList, AuthGuard, LanguageDropdown } from 'components';
|
import { UserDisplay, VirtualList, AuthGuard, LanguageDropdown } from 'components';
|
||||||
import { AuthenticationService, SessionService } from 'api';
|
import { AuthenticationService, SessionService } from 'api';
|
||||||
import { ServerSelectors } from 'store';
|
import { ServerSelectors } from 'store';
|
||||||
import { User } from 'types';
|
|
||||||
import Layout from 'containers/Layout/Layout';
|
import Layout from 'containers/Layout/Layout';
|
||||||
|
import { useAppSelector } from 'store/store';
|
||||||
|
|
||||||
import AddToBuddies from './AddToBuddies';
|
import AddToBuddies from './AddToBuddies';
|
||||||
import AddToIgnore from './AddToIgnore';
|
import AddToIgnore from './AddToIgnore';
|
||||||
|
|
||||||
import './Account.css';
|
import './Account.css';
|
||||||
|
|
||||||
const Account = (props: AccountProps) => {
|
const Account = () => {
|
||||||
const { buddyList, ignoreList, serverName, serverVersion, user } = props;
|
const buddyList = useAppSelector(state => ServerSelectors.getBuddyList(state));
|
||||||
|
const ignoreList = useAppSelector(state => ServerSelectors.getIgnoreList(state));
|
||||||
|
const serverName = useAppSelector(state => ServerSelectors.getName(state));
|
||||||
|
const serverVersion = useAppSelector(state => ServerSelectors.getVersion(state));
|
||||||
|
const user = useAppSelector(state => ServerSelectors.getUser(state));
|
||||||
const { country, realName, name, userLevel, accountageSecs, avatarBmp } = user || {};
|
const { country, realName, name, userLevel, accountageSecs, avatarBmp } = user || {};
|
||||||
let url = URL.createObjectURL(new Blob([avatarBmp], { 'type': 'image/png' }));
|
let url = URL.createObjectURL(new Blob([avatarBmp as BlobPart], { 'type': 'image/png' }));
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
|
@ -42,11 +45,10 @@ const Account = (props: AccountProps) => {
|
||||||
Buddies Online: ?/{buddyList.length}
|
Buddies Online: ?/{buddyList.length}
|
||||||
</div>
|
</div>
|
||||||
<VirtualList
|
<VirtualList
|
||||||
itemKey={(index, data) => buddyList[index].name }
|
|
||||||
items={ buddyList.map(user => (
|
items={ buddyList.map(user => (
|
||||||
<ListItem button dense>
|
<ListItemButton dense>
|
||||||
<UserDisplay user={user} />
|
<UserDisplay user={user} />
|
||||||
</ListItem>
|
</ListItemButton>
|
||||||
)) }
|
)) }
|
||||||
/>
|
/>
|
||||||
<div className="" style={{ borderTop: '1px solid' }}>
|
<div className="" style={{ borderTop: '1px solid' }}>
|
||||||
|
|
@ -60,11 +62,10 @@ const Account = (props: AccountProps) => {
|
||||||
Ignored Users Online: ?/{ignoreList.length}
|
Ignored Users Online: ?/{ignoreList.length}
|
||||||
</div>
|
</div>
|
||||||
<VirtualList
|
<VirtualList
|
||||||
itemKey={(index, data) => ignoreList[index].name }
|
|
||||||
items={ ignoreList.map(user => (
|
items={ ignoreList.map(user => (
|
||||||
<ListItem button dense>
|
<ListItemButton dense>
|
||||||
<UserDisplay user={user} />
|
<UserDisplay user={user} />
|
||||||
</ListItem>
|
</ListItemButton>
|
||||||
)) }
|
)) }
|
||||||
/>
|
/>
|
||||||
<div className="" style={{ borderTop: '1px solid' }}>
|
<div className="" style={{ borderTop: '1px solid' }}>
|
||||||
|
|
@ -78,7 +79,7 @@ const Account = (props: AccountProps) => {
|
||||||
<p><strong>{name}</strong></p>
|
<p><strong>{name}</strong></p>
|
||||||
<p>Location: ({country?.toUpperCase()})</p>
|
<p>Location: ({country?.toUpperCase()})</p>
|
||||||
<p>User Level: {userLevel}</p>
|
<p>User Level: {userLevel}</p>
|
||||||
<p>Account Age: {accountageSecs}</p>
|
<p>Account Age: {String(accountageSecs)}</p>
|
||||||
<p>Real Name: {realName}</p>
|
<p>Real Name: {realName}</p>
|
||||||
<div className="account-details__actions">
|
<div className="account-details__actions">
|
||||||
<Button size="small" color="primary" variant="contained">Edit</Button>
|
<Button size="small" color="primary" variant="contained">Edit</Button>
|
||||||
|
|
@ -101,20 +102,4 @@ const Account = (props: AccountProps) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AccountProps {
|
export default Account;
|
||||||
buddyList: User[];
|
|
||||||
ignoreList: User[];
|
|
||||||
serverName: string;
|
|
||||||
serverVersion: string;
|
|
||||||
user: User;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
buddyList: ServerSelectors.getBuddyList(state),
|
|
||||||
ignoreList: ServerSelectors.getIgnoreList(state),
|
|
||||||
serverName: ServerSelectors.getName(state),
|
|
||||||
serverVersion: ServerSelectors.getVersion(state),
|
|
||||||
user: ServerSelectors.getUser(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(Account);
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
import { Route, Routes } from 'react-router-dom';
|
||||||
|
|
||||||
import { RouteEnum } from 'types';
|
import { RouteEnum } from 'types';
|
||||||
import {
|
import {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ const FeatureDetection = () => {
|
||||||
detectIndexedDB(),
|
detectIndexedDB(),
|
||||||
];
|
];
|
||||||
|
|
||||||
Promise.all(features).catch((e) => setUnsupported(true));
|
Promise.all(features).catch(() => setUnsupported(true));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return unsupported
|
return unsupported
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
import { useState } from 'react';
|
|
||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import { useTranslation, Trans } from 'react-i18next';
|
import { useTranslation, Trans } from 'react-i18next';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { Navigate } from 'react-router-dom';
|
import { Navigate } from 'react-router-dom';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
|
|
||||||
|
|
@ -9,6 +7,7 @@ import { Images } from 'images';
|
||||||
import { ServerSelectors } from 'store';
|
import { ServerSelectors } from 'store';
|
||||||
import { RouteEnum } from 'types';
|
import { RouteEnum } from 'types';
|
||||||
import Layout from 'containers/Layout/Layout';
|
import Layout from 'containers/Layout/Layout';
|
||||||
|
import { useAppSelector } from 'store/store';
|
||||||
|
|
||||||
import './Initialize.css';
|
import './Initialize.css';
|
||||||
|
|
||||||
|
|
@ -30,7 +29,8 @@ const Root = styled('div')(({ theme }) => ({
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const Initialize = ({ initialized }: InitializeProps) => {
|
const Initialize = () => {
|
||||||
|
const initialized = useAppSelector(state => ServerSelectors.getInitialized(state));
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return initialized
|
return initialized
|
||||||
|
|
@ -60,12 +60,4 @@ const Initialize = ({ initialized }: InitializeProps) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InitializeProps {
|
export default Initialize;
|
||||||
initialized: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
initialized: ServerSelectors.getInitialized(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(Initialize);
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ function Layout(props:LayoutProps) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function BottomBar(props) {
|
function BottomBar() {
|
||||||
return (
|
return (
|
||||||
<div className="bottom-bar__container">
|
<div className="bottom-bar__container">
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { NavLink, useNavigate, generatePath } from 'react-router-dom';
|
import { NavLink, useNavigate, generatePath } from 'react-router-dom';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import Menu from '@mui/material/Menu';
|
import Menu from '@mui/material/Menu';
|
||||||
|
|
@ -8,17 +7,26 @@ import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
import MailOutlineRoundedIcon from '@mui/icons-material/MailOutline';
|
import MailOutlineRoundedIcon from '@mui/icons-material/MailOutline';
|
||||||
import MenuRoundedIcon from '@mui/icons-material/MenuRounded';
|
import MenuRoundedIcon from '@mui/icons-material/MenuRounded';
|
||||||
import * as _ from 'lodash';
|
|
||||||
|
|
||||||
import { AuthenticationService, RoomsService } from 'api';
|
import { AuthenticationService, RoomsService } from 'api';
|
||||||
import { CardImportDialog } from 'dialogs';
|
import { CardImportDialog } from 'dialogs';
|
||||||
import { Images } from 'images';
|
import { Images } from 'images';
|
||||||
import { RoomsSelectors, ServerSelectors } from 'store';
|
import { RoomsSelectors, ServerSelectors } from 'store';
|
||||||
import { Room, RouteEnum, User } from 'types';
|
import { RouteEnum } from 'types';
|
||||||
|
import { useAppSelector } from 'store/store';
|
||||||
|
|
||||||
import './LeftNav.css';
|
import './LeftNav.css';
|
||||||
|
|
||||||
const LeftNav = ({ joinedRooms, serverState, user }: LeftNavProps) => {
|
interface LeftNavState {
|
||||||
|
anchorEl: Element;
|
||||||
|
showCardImportDialog: boolean;
|
||||||
|
options: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const LeftNav = () => {
|
||||||
|
const joinedRooms = useAppSelector(state => RoomsSelectors.getJoinedRooms(state));
|
||||||
|
const serverState = useAppSelector(state => ServerSelectors.getState(state));
|
||||||
|
const user = useAppSelector(state => ServerSelectors.getUser(state));
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [state, setState] = useState<LeftNavState>({
|
const [state, setState] = useState<LeftNavState>({
|
||||||
anchorEl: null,
|
anchorEl: null,
|
||||||
|
|
@ -147,12 +155,12 @@ const LeftNav = ({ joinedRooms, serverState, user }: LeftNavProps) => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{state.options.map((option) => (
|
{state.options.map((option) => (
|
||||||
<MenuItem key={option} onClick={(event) => handleMenuItemClick(option)}>
|
<MenuItem key={option} onClick={() => handleMenuItemClick(option)}>
|
||||||
{option}
|
{option}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<MenuItem key='Import Cards' onClick={(event) => openImportCardWizard()}>
|
<MenuItem key='Import Cards' onClick={() => openImportCardWizard()}>
|
||||||
Import Cards
|
Import Cards
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
@ -171,25 +179,4 @@ const LeftNav = ({ joinedRooms, serverState, user }: LeftNavProps) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LeftNavProps {
|
export default LeftNav;
|
||||||
serverState: number;
|
|
||||||
server: string;
|
|
||||||
user: User;
|
|
||||||
joinedRooms: Room[];
|
|
||||||
showNav?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LeftNavState {
|
|
||||||
anchorEl: Element;
|
|
||||||
showCardImportDialog: boolean;
|
|
||||||
options: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
serverState: ServerSelectors.getState(state),
|
|
||||||
server: ServerSelectors.getName(state),
|
|
||||||
user: ServerSelectors.getUser(state),
|
|
||||||
joinedRooms: RoomsSelectors.getJoinedRooms(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(LeftNav);
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { Navigate } from 'react-router-dom';
|
import { Navigate } from 'react-router-dom';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
|
|
@ -17,6 +16,7 @@ import { HostDTO, serverProps } from 'services';
|
||||||
import { RouteEnum, WebSocketConnectOptions, getHostPort } from 'types';
|
import { RouteEnum, WebSocketConnectOptions, getHostPort } from 'types';
|
||||||
import { ServerSelectors, ServerTypes } from 'store';
|
import { ServerSelectors, ServerTypes } from 'store';
|
||||||
import Layout from 'containers/Layout/Layout';
|
import Layout from 'containers/Layout/Layout';
|
||||||
|
import { useAppSelector } from 'store/store';
|
||||||
|
|
||||||
import './Login.css';
|
import './Login.css';
|
||||||
import { useToast } from 'components/Toast';
|
import { useToast } from 'components/Toast';
|
||||||
|
|
@ -64,7 +64,9 @@ const Root = styled('div')(({ theme }) => ({
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const Login = ({ state, description }: LoginProps) => {
|
const Login = () => {
|
||||||
|
const state = useAppSelector(s => ServerSelectors.getState(s));
|
||||||
|
const description = useAppSelector(s => ServerSelectors.getDescription(s));
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const isConnected = AuthenticationService.isConnected(state);
|
const isConnected = AuthenticationService.isConnected(state);
|
||||||
|
|
@ -349,14 +351,4 @@ const Login = ({ state, description }: LoginProps) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LoginProps {
|
export default Login;
|
||||||
state: number;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
state: ServerSelectors.getState(state),
|
|
||||||
description: ServerSelectors.getDescription(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(Login);
|
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,50 @@
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
import React, { Component } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
import { ModeratorService } from 'api';
|
import { ModeratorService } from 'api';
|
||||||
import { AuthGuard, ModGuard } from 'components';
|
import { AuthGuard, ModGuard } from 'components';
|
||||||
import { SearchForm } from 'forms';
|
import { SearchForm } from 'forms';
|
||||||
import { ServerDispatch, ServerSelectors, ServerStateLogs } from 'store';
|
import { ServerDispatch, ServerSelectors } from 'store';
|
||||||
import { LogFilters } from 'types';
|
import { LogFilters } from 'types';
|
||||||
|
import { useAppSelector } from 'store/store';
|
||||||
|
|
||||||
import LogResults from './LogResults';
|
import LogResults from './LogResults';
|
||||||
import './Logs.css';
|
import './Logs.css';
|
||||||
|
|
||||||
class Logs extends Component<LogsTypes> {
|
const Logs = () => {
|
||||||
MAXIMUM_RESULTS = 1000;
|
const logs = useAppSelector(state => ServerSelectors.getLogs(state));
|
||||||
|
const MAXIMUM_RESULTS = 1000;
|
||||||
|
|
||||||
constructor(props) {
|
useEffect(() => {
|
||||||
super(props);
|
return () => {
|
||||||
|
ServerDispatch.clearLogs();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
this.onSubmit = this.onSubmit.bind(this);
|
const trimFields = (fields) => {
|
||||||
}
|
return _.reduce(fields, (obj: any, field, key) => {
|
||||||
|
if (typeof field === 'string') {
|
||||||
|
const trimmed = _.trim(field);
|
||||||
|
if (!!trimmed) {
|
||||||
|
obj[key] = trimmed;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
obj[key] = field;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
||||||
componentWillUnmount() {
|
const flattenLogLocations = (logLocations) => {
|
||||||
ServerDispatch.clearLogs();
|
return _.reduce(logLocations, (arr: any[], loc, key) => {
|
||||||
}
|
arr.push(key);
|
||||||
|
return arr;
|
||||||
onSubmit(fields: LogFilters) {
|
}, []);
|
||||||
const trimmedFields: any = this.trimFields(fields);
|
};
|
||||||
|
|
||||||
|
const onSubmit = (fields: LogFilters) => {
|
||||||
|
const trimmedFields: any = trimFields(fields);
|
||||||
const { userName, ipAddress, gameName, gameId, message, logLocation } = trimmedFields;
|
const { userName, ipAddress, gameName, gameId, message, logLocation } = trimmedFields;
|
||||||
|
|
||||||
const required = _.filter({
|
const required = _.filter({
|
||||||
|
|
@ -35,68 +52,35 @@ class Logs extends Component<LogsTypes> {
|
||||||
}, field => field);
|
}, field => field);
|
||||||
|
|
||||||
if (logLocation) {
|
if (logLocation) {
|
||||||
trimmedFields.logLocation = this.flattenLogLocations(logLocation);
|
trimmedFields.logLocation = flattenLogLocations(logLocation);
|
||||||
}
|
}
|
||||||
|
|
||||||
trimmedFields.maximumResults = this.MAXIMUM_RESULTS;
|
trimmedFields.maximumResults = MAXIMUM_RESULTS;
|
||||||
|
|
||||||
if (_.size(required)) {
|
if (_.size(required)) {
|
||||||
ModeratorService.viewLogHistory(trimmedFields);
|
ModeratorService.viewLogHistory(trimmedFields);
|
||||||
} else {
|
} else {
|
||||||
// @TODO use yet-to-be-implemented banner/alert
|
// @TODO use yet-to-be-implemented banner/alert
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
private trimFields(fields) {
|
return (
|
||||||
return _.reduce(fields, (obj, field, key) => {
|
<div className="moderator-logs overflow-scroll">
|
||||||
if (typeof field === 'string') {
|
<AuthGuard />
|
||||||
const trimmed = _.trim(field);
|
<ModGuard />
|
||||||
|
|
||||||
if (!!trimmed) {
|
<div className="moderator-logs__form">
|
||||||
obj[key] = trimmed;
|
<SearchForm onSubmit={onSubmit} />
|
||||||
}
|
|
||||||
} else {
|
|
||||||
obj[key] = field;
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
private flattenLogLocations(logLocations) {
|
|
||||||
return _.reduce(logLocations, (arr, loc, key) => {
|
|
||||||
arr.push(key);
|
|
||||||
return arr;
|
|
||||||
}, [])
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className="moderator-logs overflow-scroll">
|
|
||||||
<AuthGuard />
|
|
||||||
<ModGuard />
|
|
||||||
|
|
||||||
<div className="moderator-logs__form">
|
|
||||||
<SearchForm onSubmit={this.onSubmit} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="moderator-logs__results">
|
|
||||||
<LogResults logs={this.props.logs} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LogsTypes {
|
<div className="moderator-logs__results">
|
||||||
logs: ServerStateLogs
|
<LogResults logs={logs} />
|
||||||
}
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
export default Logs;
|
||||||
logs: ServerSelectors.getLogs(state)
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(Logs);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
import React, { Component } from "react";
|
import React from "react";
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
import Table from '@mui/material/Table';
|
import Table from '@mui/material/Table';
|
||||||
|
|
@ -15,129 +14,97 @@ import Tooltip from '@mui/material/Tooltip';
|
||||||
|
|
||||||
import { SortUtil, RoomsDispatch, RoomsSelectors } from 'store';
|
import { SortUtil, RoomsDispatch, RoomsSelectors } from 'store';
|
||||||
import { UserDisplay } from 'components';
|
import { UserDisplay } from 'components';
|
||||||
|
import { useAppSelector } from 'store/store';
|
||||||
|
|
||||||
import './Games.css';
|
import './Games.css';
|
||||||
|
|
||||||
// @TODO run interval to update timeSinceCreated
|
// @TODO run interval to update timeSinceCreated
|
||||||
class Games extends Component<GamesProps> {
|
|
||||||
private headerCells = [
|
|
||||||
{
|
|
||||||
label: 'Age',
|
|
||||||
field: 'startTime'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Description',
|
|
||||||
field: 'description'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Creator',
|
|
||||||
field: 'creatorInfo.name'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Type',
|
|
||||||
field: 'gameType'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Restrictions',
|
|
||||||
// field: "?"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Players',
|
|
||||||
// field: ["maxPlayers", "playerCount"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Spectators',
|
|
||||||
field: 'spectatorsCount'
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
handleSort(sortByField) {
|
|
||||||
const { room: { roomId }, sortBy } = this.props;
|
|
||||||
const { field, order } = SortUtil.toggleSortBy(sortByField, sortBy);
|
|
||||||
RoomsDispatch.sortGames(roomId, field, order);
|
|
||||||
}
|
|
||||||
|
|
||||||
private isUnavailableGame({ started, maxPlayers, playerCount }) {
|
|
||||||
return !started && playerCount < maxPlayers;
|
|
||||||
}
|
|
||||||
|
|
||||||
private isPasswordProtectedGame({ withPassword }) {
|
|
||||||
return !withPassword;
|
|
||||||
}
|
|
||||||
|
|
||||||
private isBuddiesOnlyGame({ onlyBuddies }) {
|
|
||||||
return !onlyBuddies;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { room, sortBy } = this.props;
|
|
||||||
|
|
||||||
const games = room.gameList.filter(game => (
|
|
||||||
this.isUnavailableGame(game) &&
|
|
||||||
this.isPasswordProtectedGame(game) &&
|
|
||||||
this.isBuddiesOnlyGame(game)
|
|
||||||
));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="games">
|
|
||||||
<Table size="small">
|
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
|
||||||
{ _.map(this.headerCells, ({ label, field }) => {
|
|
||||||
const active = field === sortBy.field;
|
|
||||||
const order = sortBy.order.toLowerCase();
|
|
||||||
const sortDirection = active ? order : false;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableCell sortDirection={sortDirection} key={label}>
|
|
||||||
{!field ? label : (
|
|
||||||
<TableSortLabel
|
|
||||||
active={active}
|
|
||||||
direction={order}
|
|
||||||
onClick={() => this.handleSort(field)}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</TableSortLabel>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{ _.map(games, ({ description, gameId, gameType, creatorInfo, maxPlayers, playerCount, spectatorsCount, startTime }) => (
|
|
||||||
<TableRow key={gameId}>
|
|
||||||
<TableCell className="games-header__cell single-line-ellipsis">{startTime}</TableCell>
|
|
||||||
<TableCell className="games-header__cell">
|
|
||||||
<Tooltip title={description} placement="bottom-start" enterDelay={500}>
|
|
||||||
<div className="single-line-ellipsis">
|
|
||||||
{description}
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="games-header__cell">
|
|
||||||
<UserDisplay user={ creatorInfo } />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="games-header__cell single-line-ellipsis">{gameType}</TableCell>
|
|
||||||
<TableCell className="games-header__cell single-line-ellipsis">?</TableCell>
|
|
||||||
<TableCell className="games-header__cell single-line-ellipsis">{`${playerCount}/${maxPlayers}`}</TableCell>
|
|
||||||
<TableCell className="games-header__cell single-line-ellipsis">{spectatorsCount}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GamesProps {
|
interface GamesProps {
|
||||||
room: any;
|
room: any;
|
||||||
sortBy: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const Games = ({ room }: GamesProps) => {
|
||||||
sortBy: RoomsSelectors.getSortGamesBy(state)
|
const sortBy = useAppSelector(state => RoomsSelectors.getSortGamesBy(state));
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(Games);
|
const headerCells = [
|
||||||
|
{ label: 'Age', field: 'startTime' },
|
||||||
|
{ label: 'Description', field: 'description' },
|
||||||
|
{ label: 'Creator', field: 'creatorInfo.name' },
|
||||||
|
{ label: 'Type', field: 'gameType' },
|
||||||
|
{ label: 'Restrictions' },
|
||||||
|
{ label: 'Players' },
|
||||||
|
{ label: 'Spectators', field: 'spectatorsCount' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleSort = (sortByField) => {
|
||||||
|
const { roomId } = room;
|
||||||
|
const { field, order } = SortUtil.toggleSortBy(sortByField, sortBy);
|
||||||
|
RoomsDispatch.sortGames(roomId, field, order);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isUnavailableGame = ({ started, maxPlayers, playerCount }) =>
|
||||||
|
!started && playerCount < maxPlayers;
|
||||||
|
|
||||||
|
const isPasswordProtectedGame = ({ withPassword }) => !withPassword;
|
||||||
|
|
||||||
|
const isBuddiesOnlyGame = ({ onlyBuddies }) => !onlyBuddies;
|
||||||
|
|
||||||
|
const games = room.gameList.filter(game => (
|
||||||
|
isUnavailableGame(game) &&
|
||||||
|
isPasswordProtectedGame(game) &&
|
||||||
|
isBuddiesOnlyGame(game)
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="games">
|
||||||
|
<Table size="small">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
{ _.map(headerCells, ({ label, field }) => {
|
||||||
|
const active = field === sortBy.field;
|
||||||
|
const order = sortBy.order.toLowerCase();
|
||||||
|
const sortDirection = active ? (order === 'asc' ? 'asc' : 'desc') : false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableCell sortDirection={sortDirection} key={label}>
|
||||||
|
{!field ? label : (
|
||||||
|
<TableSortLabel
|
||||||
|
active={active}
|
||||||
|
direction={order}
|
||||||
|
onClick={() => handleSort(field)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</TableSortLabel>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{ _.map(games, ({ description, gameId, gameType, creatorInfo, maxPlayers, playerCount, spectatorsCount, startTime }) => (
|
||||||
|
<TableRow key={gameId}>
|
||||||
|
<TableCell className="games-header__cell single-line-ellipsis">{startTime}</TableCell>
|
||||||
|
<TableCell className="games-header__cell">
|
||||||
|
<Tooltip title={description} placement="bottom-start" enterDelay={500}>
|
||||||
|
<div className="single-line-ellipsis">
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="games-header__cell">
|
||||||
|
<UserDisplay user={ creatorInfo } />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="games-header__cell single-line-ellipsis">{gameType}</TableCell>
|
||||||
|
<TableCell className="games-header__cell single-line-ellipsis">?</TableCell>
|
||||||
|
<TableCell className="games-header__cell single-line-ellipsis">{`${playerCount}/${maxPlayers}`}</TableCell>
|
||||||
|
<TableCell className="games-header__cell single-line-ellipsis">{spectatorsCount}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Games;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import './Messages.css';
|
||||||
const Messages = ({ messages }) => (
|
const Messages = ({ messages }) => (
|
||||||
<div className="messages">
|
<div className="messages">
|
||||||
{
|
{
|
||||||
messages && messages.map((message, index) => (
|
messages && messages.map((message) => (
|
||||||
<div className="message-wrapper" key={message.timeReceived}>
|
<div className="message-wrapper" key={message.timeReceived}>
|
||||||
<Message message={message} />
|
<Message message={message} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
import React, { Component } from "react";
|
import React from "react";
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
import Table from '@mui/material/Table';
|
import Table from '@mui/material/Table';
|
||||||
|
|
@ -15,129 +14,97 @@ import Tooltip from '@mui/material/Tooltip';
|
||||||
|
|
||||||
import { SortUtil, RoomsDispatch, RoomsSelectors } from 'store';
|
import { SortUtil, RoomsDispatch, RoomsSelectors } from 'store';
|
||||||
import { UserDisplay } from 'components';
|
import { UserDisplay } from 'components';
|
||||||
|
import { useAppSelector } from 'store/store';
|
||||||
|
|
||||||
import './OpenGames.css';
|
import './OpenGames.css';
|
||||||
|
|
||||||
// @TODO run interval to update timeSinceCreated
|
// @TODO run interval to update timeSinceCreated
|
||||||
class OpenGames extends Component<OpenGamesProps> {
|
|
||||||
private headerCells = [
|
|
||||||
{
|
|
||||||
label: 'Age',
|
|
||||||
field: 'startTime'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Description',
|
|
||||||
field: 'description'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Creator',
|
|
||||||
field: 'creatorInfo.name'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Type',
|
|
||||||
field: 'gameType'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Restrictions',
|
|
||||||
// field: "?"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Players',
|
|
||||||
// field: ["maxPlayers", "playerCount"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Spectators',
|
|
||||||
field: 'spectatorsCount'
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
handleSort(sortByField) {
|
|
||||||
const { room: { roomId }, sortBy } = this.props;
|
|
||||||
const { field, order } = SortUtil.toggleSortBy(sortByField, sortBy);
|
|
||||||
RoomsDispatch.sortGames(roomId, field, order);
|
|
||||||
}
|
|
||||||
|
|
||||||
private isUnavailableGame({ started, maxPlayers, playerCount }) {
|
|
||||||
return !started && playerCount < maxPlayers;
|
|
||||||
}
|
|
||||||
|
|
||||||
private isPasswordProtectedGame({ withPassword }) {
|
|
||||||
return !withPassword;
|
|
||||||
}
|
|
||||||
|
|
||||||
private isBuddiesOnlyGame({ onlyBuddies }) {
|
|
||||||
return !onlyBuddies;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { room, sortBy } = this.props;
|
|
||||||
|
|
||||||
const games = room.gameList.filter(game => (
|
|
||||||
this.isUnavailableGame(game) &&
|
|
||||||
this.isPasswordProtectedGame(game) &&
|
|
||||||
this.isBuddiesOnlyGame(game)
|
|
||||||
));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="games">
|
|
||||||
<Table size="small">
|
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
|
||||||
{ _.map(this.headerCells, ({ label, field }) => {
|
|
||||||
const active = field === sortBy.field;
|
|
||||||
const order = sortBy.order.toLowerCase();
|
|
||||||
const sortDirection = active ? order : false;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableCell sortDirection={sortDirection} key={label}>
|
|
||||||
{!field ? label : (
|
|
||||||
<TableSortLabel
|
|
||||||
active={active}
|
|
||||||
direction={order}
|
|
||||||
onClick={() => this.handleSort(field)}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</TableSortLabel>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{ _.map(games, ({ description, gameId, gameType, creatorInfo, maxPlayers, playerCount, spectatorsCount, startTime }) => (
|
|
||||||
<TableRow key={gameId}>
|
|
||||||
<TableCell className="games-header__cell single-line-ellipsis">{startTime}</TableCell>
|
|
||||||
<TableCell className="games-header__cell">
|
|
||||||
<Tooltip title={description} placement="bottom-start" enterDelay={500}>
|
|
||||||
<div className="single-line-ellipsis">
|
|
||||||
{description}
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="games-header__cell">
|
|
||||||
<UserDisplay user={ creatorInfo } />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="games-header__cell single-line-ellipsis">{gameType}</TableCell>
|
|
||||||
<TableCell className="games-header__cell single-line-ellipsis">?</TableCell>
|
|
||||||
<TableCell className="games-header__cell single-line-ellipsis">{`${playerCount}/${maxPlayers}`}</TableCell>
|
|
||||||
<TableCell className="games-header__cell single-line-ellipsis">{spectatorsCount}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OpenGamesProps {
|
interface OpenGamesProps {
|
||||||
room: any;
|
room: any;
|
||||||
sortBy: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const OpenGames = ({ room }: OpenGamesProps) => {
|
||||||
sortBy: RoomsSelectors.getSortGamesBy(state)
|
const sortBy = useAppSelector(state => RoomsSelectors.getSortGamesBy(state));
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(OpenGames);
|
const headerCells = [
|
||||||
|
{ label: 'Age', field: 'startTime' },
|
||||||
|
{ label: 'Description', field: 'description' },
|
||||||
|
{ label: 'Creator', field: 'creatorInfo.name' },
|
||||||
|
{ label: 'Type', field: 'gameType' },
|
||||||
|
{ label: 'Restrictions' },
|
||||||
|
{ label: 'Players' },
|
||||||
|
{ label: 'Spectators', field: 'spectatorsCount' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleSort = (sortByField) => {
|
||||||
|
const { roomId } = room;
|
||||||
|
const { field, order } = SortUtil.toggleSortBy(sortByField, sortBy);
|
||||||
|
RoomsDispatch.sortGames(roomId, field, order);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isUnavailableGame = ({ started, maxPlayers, playerCount }) =>
|
||||||
|
!started && playerCount < maxPlayers;
|
||||||
|
|
||||||
|
const isPasswordProtectedGame = ({ withPassword }) => !withPassword;
|
||||||
|
|
||||||
|
const isBuddiesOnlyGame = ({ onlyBuddies }) => !onlyBuddies;
|
||||||
|
|
||||||
|
const games = room.gameList.filter(game => (
|
||||||
|
isUnavailableGame(game) &&
|
||||||
|
isPasswordProtectedGame(game) &&
|
||||||
|
isBuddiesOnlyGame(game)
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="games">
|
||||||
|
<Table size="small">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
{ _.map(headerCells, ({ label, field }) => {
|
||||||
|
const active = field === sortBy.field;
|
||||||
|
const order = sortBy.order.toLowerCase();
|
||||||
|
const sortDirection = active ? (order === 'asc' ? 'asc' : 'desc') : false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableCell sortDirection={sortDirection} key={label}>
|
||||||
|
{!field ? label : (
|
||||||
|
<TableSortLabel
|
||||||
|
active={active}
|
||||||
|
direction={order}
|
||||||
|
onClick={() => handleSort(field)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</TableSortLabel>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{ _.map(games, ({ description, gameId, gameType, creatorInfo, maxPlayers, playerCount, spectatorsCount, startTime }) => (
|
||||||
|
<TableRow key={gameId}>
|
||||||
|
<TableCell className="games-header__cell single-line-ellipsis">{startTime}</TableCell>
|
||||||
|
<TableCell className="games-header__cell">
|
||||||
|
<Tooltip title={description} placement="bottom-start" enterDelay={500}>
|
||||||
|
<div className="single-line-ellipsis">
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="games-header__cell">
|
||||||
|
<UserDisplay user={ creatorInfo } />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="games-header__cell single-line-ellipsis">{gameType}</TableCell>
|
||||||
|
<TableCell className="games-header__cell single-line-ellipsis">?</TableCell>
|
||||||
|
<TableCell className="games-header__cell single-line-ellipsis">{`${playerCount}/${maxPlayers}`}</TableCell>
|
||||||
|
<TableCell className="games-header__cell single-line-ellipsis">{spectatorsCount}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OpenGames;
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { useNavigate, useParams, generatePath } from 'react-router-dom';
|
import { useNavigate, useParams, generatePath } from 'react-router-dom';
|
||||||
|
|
||||||
import ListItem from '@mui/material/ListItem';
|
import ListItemButton from '@mui/material/ListItemButton';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
|
|
||||||
import { RoomsService } from 'api';
|
import { RoomsService } from 'api';
|
||||||
import { ScrollToBottomOnChanges, ThreePaneLayout, UserDisplay, VirtualList, AuthGuard } from 'components';
|
import { ScrollToBottomOnChanges, ThreePaneLayout, UserDisplay, VirtualList, AuthGuard } from 'components';
|
||||||
import { RoomsStateMessages, RoomsStateRooms, JoinedRooms, RoomsSelectors, RoomsTypes } from 'store';
|
import { RoomsSelectors } from 'store';
|
||||||
|
import { useAppSelector } from 'store/store';
|
||||||
import { RouteEnum } from 'types';
|
import { RouteEnum } from 'types';
|
||||||
import Layout from 'containers/Layout/Layout';
|
import Layout from 'containers/Layout/Layout';
|
||||||
|
|
||||||
|
|
@ -18,8 +18,10 @@ import SayMessage from './SayMessage';
|
||||||
import './Room.css';
|
import './Room.css';
|
||||||
|
|
||||||
// @TODO (3)
|
// @TODO (3)
|
||||||
const Room = (props) => {
|
const Room = () => {
|
||||||
const { joined, rooms, messages } = props;
|
const joined = useAppSelector(state => RoomsSelectors.getJoinedRooms(state));
|
||||||
|
const rooms = useAppSelector(state => RoomsSelectors.getRooms(state));
|
||||||
|
const messages = useAppSelector(state => RoomsSelectors.getMessages(state));
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
|
|
@ -74,11 +76,10 @@ const Room = (props) => {
|
||||||
</div>
|
</div>
|
||||||
<VirtualList
|
<VirtualList
|
||||||
className="room-view__side-list"
|
className="room-view__side-list"
|
||||||
itemKey={(index, data) => users[index].name }
|
|
||||||
items={ users.map(user => (
|
items={ users.map(user => (
|
||||||
<ListItem button className="room-view__side-list__item">
|
<ListItemButton className="room-view__side-list__item">
|
||||||
<UserDisplay user={user} />
|
<UserDisplay user={user} />
|
||||||
</ListItem>
|
</ListItemButton>
|
||||||
)) }
|
)) }
|
||||||
/>
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
@ -89,16 +90,4 @@ const Room = (props) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RoomProps {
|
export default Room;
|
||||||
messages: RoomsStateMessages;
|
|
||||||
rooms: RoomsStateRooms;
|
|
||||||
joined: JoinedRooms;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
messages: RoomsSelectors.getMessages(state),
|
|
||||||
rooms: RoomsSelectors.getRooms(state),
|
|
||||||
joined: RoomsSelectors.getJoinedRooms(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(Room);
|
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,25 @@
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
import React, { Component } from "react";
|
import React from "react";
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { generatePath, useNavigate } from 'react-router-dom';
|
import { generatePath, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import ListItem from '@mui/material/ListItem';
|
import ListItemButton from '@mui/material/ListItemButton';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
|
|
||||||
import { AuthGuard, ThreePaneLayout, UserDisplay, VirtualList } from 'components';
|
import { AuthGuard, ThreePaneLayout, UserDisplay, VirtualList } from 'components';
|
||||||
import { useReduxEffect } from 'hooks';
|
import { useReduxEffect } from 'hooks';
|
||||||
import { RoomsSelectors, RoomsTypes, ServerSelectors } from 'store';
|
import { RoomsSelectors, RoomsTypes, ServerSelectors } from 'store';
|
||||||
import { Room, RouteEnum, User } from 'types';
|
import { RouteEnum } from 'types';
|
||||||
|
import { useAppSelector } from 'store/store';
|
||||||
import Rooms from './Rooms';
|
import Rooms from './Rooms';
|
||||||
import Layout from 'containers/Layout/Layout';
|
import Layout from 'containers/Layout/Layout';
|
||||||
|
|
||||||
import './Server.css';
|
import './Server.css';
|
||||||
|
|
||||||
const Server = ({ message, rooms, joinedRooms, users }: ServerProps) => {
|
const Server = () => {
|
||||||
|
const message = useAppSelector(state => ServerSelectors.getMessage(state));
|
||||||
|
const rooms = useAppSelector(state => RoomsSelectors.getRooms(state));
|
||||||
|
const joinedRooms = useAppSelector(state => RoomsSelectors.getJoinedRooms(state));
|
||||||
|
const users = useAppSelector(state => ServerSelectors.getUsers(state));
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useReduxEffect((action: any) => {
|
useReduxEffect((action: any) => {
|
||||||
|
|
@ -46,11 +50,10 @@ const Server = ({ message, rooms, joinedRooms, users }: ServerProps) => {
|
||||||
Users connected to server: {users.length}
|
Users connected to server: {users.length}
|
||||||
</div>
|
</div>
|
||||||
<VirtualList
|
<VirtualList
|
||||||
itemKey={(index) => users[index].name }
|
|
||||||
items={ users.map(user => (
|
items={ users.map(user => (
|
||||||
<ListItem button dense>
|
<ListItemButton dense>
|
||||||
<UserDisplay user={user} />
|
<UserDisplay user={user} />
|
||||||
</ListItem>
|
</ListItemButton>
|
||||||
)) }
|
)) }
|
||||||
/>
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
@ -60,18 +63,4 @@ const Server = ({ message, rooms, joinedRooms, users }: ServerProps) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ServerProps {
|
export default Server;
|
||||||
message: string;
|
|
||||||
rooms: Room[];
|
|
||||||
joinedRooms: Room[];
|
|
||||||
users: User[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
message: ServerSelectors.getMessage(state),
|
|
||||||
rooms: RoomsSelectors.getRooms(state),
|
|
||||||
joinedRooms: RoomsSelectors.getJoinedRooms(state),
|
|
||||||
users: ServerSelectors.getUsers(state)
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(Server);
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
|
|
@ -23,8 +22,4 @@ const Unsupported = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
export default Unsupported;
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(Unsupported);
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { AccountActivationForm } from 'forms';
|
||||||
|
|
||||||
import './AccountActivationDialog.css';
|
import './AccountActivationDialog.css';
|
||||||
|
|
||||||
const AccountActivationDialog = ({ classes, handleClose, isOpen, onSubmit }: any) => {
|
const AccountActivationDialog = ({ handleClose, isOpen, onSubmit }: any) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleOnClose = () => {
|
const handleOnClose = () => {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { CardImportForm } from 'forms';
|
||||||
|
|
||||||
import './CardImportDialog.css';
|
import './CardImportDialog.css';
|
||||||
|
|
||||||
const CardImportDialog = ({ classes, handleClose, isOpen }: any) => {
|
const CardImportDialog = ({ handleClose, isOpen }: any) => {
|
||||||
const handleOnClose = () => {
|
const handleOnClose = () => {
|
||||||
handleClose();
|
handleClose();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import Dialog from '@mui/material/Dialog';
|
||||||
import DialogContent from '@mui/material/DialogContent';
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
import DialogTitle from '@mui/material/DialogTitle';
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { RegisterForm } from 'forms';
|
||||||
|
|
||||||
import './RegistrationDialog.css';
|
import './RegistrationDialog.css';
|
||||||
|
|
||||||
const RegistrationDialog = ({ classes, handleClose, isOpen, onSubmit }: any) => {
|
const RegistrationDialog = ({ handleClose, isOpen, onSubmit }: any) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleOnClose = () => {
|
const handleOnClose = () => {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { RequestPasswordResetForm } from 'forms';
|
||||||
|
|
||||||
import './RequestPasswordResetDialog.css';
|
import './RequestPasswordResetDialog.css';
|
||||||
|
|
||||||
const RequestPasswordResetDialog = ({ classes, handleClose, isOpen, onSubmit, skipTokenRequest }: any) => {
|
const RequestPasswordResetDialog = ({ handleClose, isOpen, onSubmit, skipTokenRequest }: any) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleOnClose = () => {
|
const handleOnClose = () => {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { ResetPasswordForm } from 'forms';
|
||||||
|
|
||||||
import './ResetPasswordDialog.css';
|
import './ResetPasswordDialog.css';
|
||||||
|
|
||||||
const ResetPasswordDialog = ({ classes, handleClose, isOpen, onSubmit, userName }: any) => {
|
const ResetPasswordDialog = ({ handleClose, isOpen, onSubmit, userName }: any) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleOnClose = () => {
|
const handleOnClose = () => {
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,17 @@
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { Form, Field } from 'react-final-form';
|
import { Form, Field } from 'react-final-form';
|
||||||
import { OnChange } from 'react-final-form-listeners';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
|
|
||||||
import { InputField, KnownHosts } from 'components';
|
import { InputField } from 'components';
|
||||||
import { FormKey } from 'types';
|
|
||||||
|
|
||||||
import './AccountActivationForm.css';
|
|
||||||
import { useReduxEffect } from 'hooks';
|
import { useReduxEffect } from 'hooks';
|
||||||
import { ServerTypes } from 'store';
|
import { ServerTypes } from 'store';
|
||||||
|
|
||||||
|
import './AccountActivationForm.css';
|
||||||
|
|
||||||
const AccountActivationForm = ({ onSubmit }) => {
|
const AccountActivationForm = ({ onSubmit }) => {
|
||||||
const [errorMessage, setErrorMessage] = useState(false);
|
const [errorMessage, setErrorMessage] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
@ -43,7 +40,7 @@ const AccountActivationForm = ({ onSubmit }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={handleOnSubmit} validate={validate}>
|
<Form onSubmit={handleOnSubmit} validate={validate}>
|
||||||
{({ handleSubmit, form }) => {
|
{({ handleSubmit }) => {
|
||||||
return (
|
return (
|
||||||
<form className="AccountActivationForm" onSubmit={handleSubmit}>
|
<form className="AccountActivationForm" onSubmit={handleSubmit}>
|
||||||
<div className="AccountActivationForm-item">
|
<div className="AccountActivationForm-item">
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
// eslint-disable-next-line
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { Form, Field } from 'react-final-form';
|
||||||
import { Form, Field, reduxForm } from 'redux-form'
|
|
||||||
|
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import Stepper from '@mui/material/Stepper';
|
import Stepper from '@mui/material/Stepper';
|
||||||
|
|
@ -11,12 +10,10 @@ import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
|
||||||
import { InputField, VirtualList } from 'components';
|
import { InputField, VirtualList } from 'components';
|
||||||
import { cardImporterService, CardDTO, SetDTO, TokenDTO } from 'services';
|
import { cardImporterService, CardDTO, SetDTO, TokenDTO } from 'services';
|
||||||
import { FormKey } from 'types';
|
|
||||||
|
|
||||||
import './CardImportForm.css';
|
import './CardImportForm.css';
|
||||||
|
|
||||||
const CardImportForm = (props) => {
|
const CardImportForm = ({ onSubmit: onClose }) => {
|
||||||
const { handleSubmit, onSubmit: onClose } = props;
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [activeStep, setActiveStep] = useState(0);
|
const [activeStep, setActiveStep] = useState(0);
|
||||||
|
|
@ -85,20 +82,27 @@ const CardImportForm = (props) => {
|
||||||
const getStepContent = (stepIndex) => {
|
const getStepContent = (stepIndex) => {
|
||||||
switch (stepIndex) {
|
switch (stepIndex) {
|
||||||
case 0: return (
|
case 0: return (
|
||||||
<Form className='cardImportForm' onSubmit={handleSubmit(handleCardDownload)}>
|
<Form
|
||||||
<div className='cardImportForm-item'>
|
onSubmit={handleCardDownload}
|
||||||
<Field label='Download URL' name='cardDownloadUrl' component={InputField} />
|
initialValues={{ cardDownloadUrl: 'https://www.mtgjson.com/api/v5/AllPrintings.json' }}
|
||||||
</div>
|
>
|
||||||
|
{({ handleSubmit }) => (
|
||||||
|
<form className='cardImportForm' onSubmit={handleSubmit}>
|
||||||
|
<div className='cardImportForm-item'>
|
||||||
|
<Field label='Download URL' name='cardDownloadUrl' component={InputField} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='cardImportForm-actions'>
|
<div className='cardImportForm-actions'>
|
||||||
<Button color='primary' type='submit' disabled={loading}>
|
<Button color='primary' type='submit' disabled={loading}>
|
||||||
Import
|
Import
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='cardImportForm-error'>
|
<div className='cardImportForm-error'>
|
||||||
<ErrorMessage error={error} />
|
<ErrorMessage error={error} />
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -122,21 +126,28 @@ const CardImportForm = (props) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
case 2: return (
|
case 2: return (
|
||||||
<Form className='cardImportForm' onSubmit={handleSubmit(handleTokenDownload)}>
|
<Form
|
||||||
<div className='cardImportForm-content'>
|
onSubmit={handleTokenDownload}
|
||||||
<Field label='Download URL' name='tokenDownloadUrl' component={InputField} />
|
initialValues={{ tokenDownloadUrl: 'https://raw.githubusercontent.com/Cockatrice/Magic-Token/master/tokens.xml' }}
|
||||||
</div>
|
>
|
||||||
|
{({ handleSubmit }) => (
|
||||||
|
<form className='cardImportForm' onSubmit={handleSubmit}>
|
||||||
|
<div className='cardImportForm-content'>
|
||||||
|
<Field label='Download URL' name='tokenDownloadUrl' component={InputField} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='cardImportForm-actions'>
|
<div className='cardImportForm-actions'>
|
||||||
<BackButton click={handleBack} disabled={loading} />
|
<BackButton click={handleBack} disabled={loading} />
|
||||||
<Button color='primary' type='submit' disabled={loading}>
|
<Button color='primary' type='submit' disabled={loading}>
|
||||||
Import
|
Import
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='cardImportForm-error'>
|
<div className='cardImportForm-error'>
|
||||||
<ErrorMessage error={error} />
|
<ErrorMessage error={error} />
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -204,7 +215,6 @@ const CardsImported = ({ cards, sets }) => {
|
||||||
return (
|
return (
|
||||||
<div className='card-import-list'>
|
<div className='card-import-list'>
|
||||||
<VirtualList
|
<VirtualList
|
||||||
itemKey={(index) => index }
|
|
||||||
items={items}
|
items={items}
|
||||||
size={15}
|
size={15}
|
||||||
/>
|
/>
|
||||||
|
|
@ -212,16 +222,4 @@ const CardsImported = ({ cards, sets }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const propsMap = {
|
export default CardImportForm;
|
||||||
form: FormKey.CARD_IMPORT,
|
|
||||||
onClose: Function
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapStateToProps = () => ({
|
|
||||||
initialValues: {
|
|
||||||
cardDownloadUrl: 'https://www.mtgjson.com/api/v5/AllPrintings.json',
|
|
||||||
tokenDownloadUrl: 'https://raw.githubusercontent.com/Cockatrice/Magic-Token/master/tokens.xml'
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(reduxForm(propsMap)(CardImportForm));
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { Form, Field } from 'react-final-form'
|
import { Form, Field } from 'react-final-form'
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
|
@ -87,8 +86,4 @@ const KnownHostForm = ({ host, onRemove, onSubmit }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = () => ({
|
export default KnownHostForm;
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(KnownHostForm);
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState, useCallback } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Form, Field } from 'react-final-form';
|
import { Form, Field } from 'react-final-form';
|
||||||
import { OnChange } from 'react-final-form-listeners';
|
import { OnChange } from 'react-final-form-listeners';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
@ -87,7 +87,7 @@ const LoginForm = ({ onSubmit, disableSubmitButton, onResetPassword }: LoginForm
|
||||||
const onUserNameChange = (userName) => {
|
const onUserNameChange = (userName) => {
|
||||||
const fieldChanged = host?.userName?.toLowerCase() !== values.userName?.toLowerCase();
|
const fieldChanged = host?.userName?.toLowerCase() !== values.userName?.toLowerCase();
|
||||||
if (useStoredPassword(values.remember, values.password) && fieldChanged) {
|
if (useStoredPassword(values.remember, values.password) && fieldChanged) {
|
||||||
setHost(({ hashedPassword, ...s }) => ({ ...s, userName }));
|
setHost(({ hashedPassword: _hashedPassword, ...s }) => ({ ...s, userName }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
import { Form, Field } from 'react-final-form';
|
import { Form, Field } from 'react-final-form';
|
||||||
import { OnChange } from 'react-final-form-listeners';
|
import { OnChange } from 'react-final-form-listeners';
|
||||||
import setFieldTouched from 'final-form-set-field-touched';
|
import setFieldTouched from 'final-form-set-field-touched';
|
||||||
|
|
@ -9,7 +10,7 @@ import Typography from '@mui/material/Typography';
|
||||||
|
|
||||||
import { CountryDropdown, InputField, KnownHosts } from 'components';
|
import { CountryDropdown, InputField, KnownHosts } from 'components';
|
||||||
import { useReduxEffect } from 'hooks';
|
import { useReduxEffect } from 'hooks';
|
||||||
import { ServerTypes } from 'store';
|
import { ServerDispatch, ServerSelectors, ServerTypes } from 'store';
|
||||||
|
|
||||||
import './RegisterForm.css';
|
import './RegisterForm.css';
|
||||||
import { useToast } from 'components/Toast';
|
import { useToast } from 'components/Toast';
|
||||||
|
|
@ -17,13 +18,13 @@ import { useToast } from 'components/Toast';
|
||||||
const RegisterForm = ({ onSubmit }: RegisterFormProps) => {
|
const RegisterForm = ({ onSubmit }: RegisterFormProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [emailRequired, setEmailRequired] = useState(false);
|
const [emailRequired, setEmailRequired] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
|
||||||
const [emailError, setEmailError] = useState(null);
|
const [emailError, setEmailError] = useState(null);
|
||||||
const [passwordError, setPasswordError] = useState(null);
|
const [passwordError, setPasswordError] = useState(null);
|
||||||
const [userNameError, setUserNameError] = useState(null);
|
const [userNameError, setUserNameError] = useState(null);
|
||||||
|
const error = useSelector(ServerSelectors.getRegistrationError);
|
||||||
const { openToast } = useToast({ key: 'registration-success', children: t('RegisterForm.toast.registerSuccess') })
|
const { openToast } = useToast({ key: 'registration-success', children: t('RegisterForm.toast.registerSuccess') })
|
||||||
|
|
||||||
const onHostChange = (host) => setEmailRequired(false);
|
const onHostChange = () => setEmailRequired(false);
|
||||||
const onEmailChange = () => emailError && setEmailError(null);
|
const onEmailChange = () => emailError && setEmailError(null);
|
||||||
const onPasswordChange = () => passwordError && setPasswordError(null);
|
const onPasswordChange = () => passwordError && setPasswordError(null);
|
||||||
const onUserNameChange = () => userNameError && setUserNameError(null);
|
const onUserNameChange = () => userNameError && setUserNameError(null);
|
||||||
|
|
@ -32,10 +33,6 @@ const RegisterForm = ({ onSubmit }: RegisterFormProps) => {
|
||||||
setEmailRequired(true);
|
setEmailRequired(true);
|
||||||
}, ServerTypes.REGISTRATION_REQUIRES_EMAIL);
|
}, ServerTypes.REGISTRATION_REQUIRES_EMAIL);
|
||||||
|
|
||||||
useReduxEffect(({ error }) => {
|
|
||||||
setError(error);
|
|
||||||
}, ServerTypes.REGISTRATION_FAILED);
|
|
||||||
|
|
||||||
useReduxEffect(() => {
|
useReduxEffect(() => {
|
||||||
openToast()
|
openToast()
|
||||||
}, ServerTypes.REGISTRATION_SUCCESS);
|
}, ServerTypes.REGISTRATION_SUCCESS);
|
||||||
|
|
@ -53,7 +50,7 @@ const RegisterForm = ({ onSubmit }: RegisterFormProps) => {
|
||||||
}, ServerTypes.REGISTRATION_USERNAME_ERROR);
|
}, ServerTypes.REGISTRATION_USERNAME_ERROR);
|
||||||
|
|
||||||
const handleOnSubmit = ({ userName, email, realName, ...values }) => {
|
const handleOnSubmit = ({ userName, email, realName, ...values }) => {
|
||||||
setError(null);
|
ServerDispatch.clearRegistrationErrors();
|
||||||
|
|
||||||
userName = userName?.trim();
|
userName = userName?.trim();
|
||||||
email = email?.trim();
|
email = email?.trim();
|
||||||
|
|
@ -100,8 +97,7 @@ const RegisterForm = ({ onSubmit }: RegisterFormProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={handleOnSubmit} validate={validate} mutators={{ setFieldTouched }}>
|
<Form onSubmit={handleOnSubmit} validate={validate} mutators={{ setFieldTouched }}>
|
||||||
{({ handleSubmit, form, ...args }) => {
|
{({ handleSubmit, form }) => {
|
||||||
const { values } = form.getState();
|
|
||||||
|
|
||||||
if (emailRequired) {
|
if (emailRequired) {
|
||||||
// Allow form render to complete
|
// Allow form render to complete
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { Form, Field } from 'react-final-form';
|
import { Form, Field } from 'react-final-form';
|
||||||
import { OnChange } from 'react-final-form-listeners';
|
import { OnChange } from 'react-final-form-listeners';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
@ -9,12 +8,11 @@ import Button from '@mui/material/Button';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
|
|
||||||
import { InputField, KnownHosts } from 'components';
|
import { InputField, KnownHosts } from 'components';
|
||||||
import { FormKey } from 'types';
|
|
||||||
|
|
||||||
import './RequestPasswordResetForm.css';
|
|
||||||
import { useReduxEffect } from 'hooks';
|
import { useReduxEffect } from 'hooks';
|
||||||
import { ServerTypes } from 'store';
|
import { ServerTypes } from 'store';
|
||||||
|
|
||||||
|
import './RequestPasswordResetForm.css';
|
||||||
|
|
||||||
const RequestPasswordResetForm = ({ onSubmit, skipTokenRequest }) => {
|
const RequestPasswordResetForm = ({ onSubmit, skipTokenRequest }) => {
|
||||||
const [errorMessage, setErrorMessage] = useState(false);
|
const [errorMessage, setErrorMessage] = useState(false);
|
||||||
const [isMFA, setIsMFA] = useState(false);
|
const [isMFA, setIsMFA] = useState(false);
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,17 @@
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { Form, Field } from 'react-final-form'
|
import { Form, Field } from 'react-final-form'
|
||||||
import { OnChange } from 'react-final-form-listeners';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
|
|
||||||
import { InputField, KnownHosts } from 'components';
|
import { InputField, KnownHosts } from 'components';
|
||||||
import { FormKey } from 'types';
|
|
||||||
|
|
||||||
import './ResetPasswordForm.css';
|
|
||||||
import { useReduxEffect } from '../../hooks';
|
import { useReduxEffect } from '../../hooks';
|
||||||
import { ServerTypes } from '../../store';
|
import { ServerTypes } from '../../store';
|
||||||
|
|
||||||
|
import './ResetPasswordForm.css';
|
||||||
|
|
||||||
const ResetPasswordForm = ({ onSubmit, userName }) => {
|
const ResetPasswordForm = ({ onSubmit, userName }) => {
|
||||||
const [errorMessage, setErrorMessage] = useState(false);
|
const [errorMessage, setErrorMessage] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
@ -60,7 +57,7 @@ const ResetPasswordForm = ({ onSubmit, userName }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={handleOnSubmit} validate={validate} initialValues={{ userName }}>
|
<Form onSubmit={handleOnSubmit} validate={validate} initialValues={{ userName }}>
|
||||||
{({ handleSubmit, form }) => (
|
{({ handleSubmit }) => (
|
||||||
<form className='ResetPasswordForm' onSubmit={handleSubmit}>
|
<form className='ResetPasswordForm' onSubmit={handleSubmit}>
|
||||||
<div className='ResetPasswordForm-items'>
|
<div className='ResetPasswordForm-items'>
|
||||||
<div className='ResetPasswordForm-item'>
|
<div className='ResetPasswordForm-item'>
|
||||||
|
|
|
||||||
|
|
@ -1,63 +1,57 @@
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
import React, { Component } from "react";
|
import React from "react";
|
||||||
import { connect } from 'react-redux';
|
import { Form, Field } from 'react-final-form';
|
||||||
import { Form, Field, reduxForm } from 'redux-form'
|
|
||||||
|
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import Divider from '@mui/material/Divider';
|
import Divider from '@mui/material/Divider';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
|
|
||||||
import { InputField, CheckboxField } from 'components';
|
import { InputField, CheckboxField } from 'components';
|
||||||
import { FormKey } from 'types';
|
|
||||||
|
|
||||||
import './SearchForm.css';
|
import './SearchForm.css';
|
||||||
|
|
||||||
const SearchForm = ({ handleSubmit }) => (
|
const SearchForm = ({ onSubmit }) => (
|
||||||
<Paper className="log-search">
|
<Form onSubmit={onSubmit}>
|
||||||
<Form className="log-search__form" onSubmit={handleSubmit}>
|
{({ handleSubmit }) => (
|
||||||
<div className="log-search__form-item">
|
<Paper className="log-search">
|
||||||
<Field label="Username" name="userName" component={InputField} />
|
<form className="log-search__form" onSubmit={handleSubmit}>
|
||||||
</div>
|
<div className="log-search__form-item">
|
||||||
<div className="log-search__form-item">
|
<Field label="Username" name="userName" component={InputField} />
|
||||||
<Field label="IP Address" name="ipAddress" component={InputField} />
|
</div>
|
||||||
</div>
|
<div className="log-search__form-item">
|
||||||
<div className="log-search__form-item">
|
<Field label="IP Address" name="ipAddress" component={InputField} />
|
||||||
<Field label="Game Name" name="gameName" component={InputField} />
|
</div>
|
||||||
</div>
|
<div className="log-search__form-item">
|
||||||
<div className="log-search__form-item">
|
<Field label="Game Name" name="gameName" component={InputField} />
|
||||||
<Field label="GameID" name="gameId" component={InputField} />
|
</div>
|
||||||
</div>
|
<div className="log-search__form-item">
|
||||||
<div className="log-search__form-item">
|
<Field label="GameID" name="gameId" component={InputField} />
|
||||||
<Field label="Message" name="message" component={InputField} />
|
</div>
|
||||||
</div>
|
<div className="log-search__form-item">
|
||||||
<Divider />
|
<Field label="Message" name="message" component={InputField} />
|
||||||
<div className="log-search__form-item log-location">
|
</div>
|
||||||
<Field label="Rooms" name="logLocation.room" component={CheckboxField} />
|
<Divider />
|
||||||
<Field label="Games" name="logLocation.game" component={CheckboxField} />
|
<div className="log-search__form-item log-location">
|
||||||
<Field label="Chats" name="logLocation.chat" component={CheckboxField} />
|
<Field label="Rooms" name="logLocation.room" component={CheckboxField} />
|
||||||
</div>
|
<Field label="Games" name="logLocation.game" component={CheckboxField} />
|
||||||
<Divider />
|
<Field label="Chats" name="logLocation.chat" component={CheckboxField} />
|
||||||
<div className="log-search__form-item">
|
</div>
|
||||||
<span>Date Range: Coming Soon</span>
|
<Divider />
|
||||||
</div>
|
<div className="log-search__form-item">
|
||||||
<Divider />
|
<span>Date Range: Coming Soon</span>
|
||||||
<div className="log-search__form-item">
|
</div>
|
||||||
<span>Maximum Results: 1000</span>
|
<Divider />
|
||||||
</div>
|
<div className="log-search__form-item">
|
||||||
<Divider />
|
<span>Maximum Results: 1000</span>
|
||||||
<Button className="log-search__form-submit" color="primary" variant="contained" type="submit">
|
</div>
|
||||||
Search Logs
|
<Divider />
|
||||||
</Button>
|
<Button className="log-search__form-submit" color="primary" variant="contained" type="submit">
|
||||||
</Form>
|
Search Logs
|
||||||
</Paper>
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
);
|
);
|
||||||
|
|
||||||
const propsMap = {
|
export default SearchForm;
|
||||||
form: FormKey.SEARCH_LOGS,
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapStateToProps = () => ({
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(reduxForm(propsMap)(SearchForm));
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { debounce, DebouncedFunc } from 'lodash';
|
|
||||||
|
|
||||||
import { SettingDTO } from 'services';
|
import { SettingDTO } from 'services';
|
||||||
import { APP_USER } from 'types';
|
import { APP_USER } from 'types';
|
||||||
|
|
||||||
type OnChange = () => void;
|
|
||||||
|
|
||||||
export function useAutoConnect() {
|
export function useAutoConnect() {
|
||||||
const [setting, setSetting] = useState(undefined);
|
const [setting, setSetting] = useState(undefined);
|
||||||
const [autoConnect, setAutoConnect] = useState(undefined);
|
const [autoConnect, setAutoConnect] = useState(undefined);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
import {
|
import {
|
||||||
render,
|
render,
|
||||||
fireEvent,
|
fireEvent,
|
||||||
getByRole,
|
|
||||||
waitFor,
|
waitFor,
|
||||||
act
|
|
||||||
} from '@testing-library/react';
|
} from '@testing-library/react';
|
||||||
import { useFireOnce } from './useFireOnce';
|
import { useFireOnce } from './useFireOnce';
|
||||||
|
|
||||||
|
|
@ -21,7 +19,7 @@ describe('useFireOnce hook', () => {
|
||||||
|
|
||||||
function Button(props) {
|
function Button(props) {
|
||||||
const { children, onClick } = props
|
const { children, onClick } = props
|
||||||
const [buttonIsDisabled, setButtonIsDisabled, handleClickOnce] = useFireOnce(onClick)
|
const [buttonIsDisabled, _setButtonIsDisabled, handleClickOnce] = useFireOnce(onClick)
|
||||||
return <button onClick={handleClickOnce} disabled={buttonIsDisabled}>{children}</button>
|
return <button onClick={handleClickOnce} disabled={buttonIsDisabled}>{children}</button>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,7 +63,7 @@ describe('useFireOnce hook', () => {
|
||||||
|
|
||||||
function Form(props) {
|
function Form(props) {
|
||||||
const { onSubmit } = props
|
const { onSubmit } = props
|
||||||
const [buttonIsDisabled, setButtonIsDisabled, handleSubmitOnce] = useFireOnce(onSubmit)
|
const [buttonIsDisabled, _setButtonIsDisabled, handleSubmitOnce] = useFireOnce(onSubmit)
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmitOnce}>
|
<form onSubmit={handleSubmitOnce}>
|
||||||
<input type="text" defaultValue="Hell World" name="thing-to-say" />
|
<input type="text" defaultValue="Hell World" name="thing-to-say" />
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { useReduxEffect } from 'hooks';
|
|
||||||
import { ServerTypes } from 'store';
|
|
||||||
|
|
||||||
type UseFireOnceType = (...args: any) => any;
|
type UseFireOnceType = (...args: any) => any;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,12 @@ File is adapted from https://github.com/Qeepsake/use-redux-effect under MIT Lice
|
||||||
|
|
||||||
import { useRef, useEffect, DependencyList } from 'react'
|
import { useRef, useEffect, DependencyList } from 'react'
|
||||||
import { useStore } from 'react-redux'
|
import { useStore } from 'react-redux'
|
||||||
import { AnyAction } from 'redux'
|
|
||||||
import { castArray } from 'lodash'
|
import { castArray } from 'lodash'
|
||||||
|
|
||||||
export type ReduxEffect = (action: AnyAction) => void
|
// Actions are identified by string `type` at runtime, so the callback
|
||||||
|
// receives an untyped action object to allow free property access.
|
||||||
|
|
||||||
|
export type ReduxEffect = (action: any) => void
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribes to redux store events
|
* Subscribes to redux store events
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { StrictMode } from 'react';
|
import { StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { Theme, StyledEngineProvider } from '@mui/material';
|
import { StyledEngineProvider } from '@mui/material';
|
||||||
import { ThemeProvider } from '@mui/material/styles';
|
import { ThemeProvider } from '@mui/material/styles';
|
||||||
|
|
||||||
import { AppShell } from './containers';
|
import { AppShell } from './containers';
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ class CardImporterService {
|
||||||
.map(key => unsortedCards[key]);
|
.map(key => unsortedCards[key]);
|
||||||
|
|
||||||
return { cards, sets };
|
return { cards, sets };
|
||||||
} catch (e) {
|
} catch {
|
||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -61,7 +61,7 @@ class CardImporterService {
|
||||||
);
|
);
|
||||||
|
|
||||||
return tokens;
|
return tokens;
|
||||||
} catch (e) {
|
} catch {
|
||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,9 @@
|
||||||
// ensure jest-dom is always available during testing to cut down on boilerplate
|
// ensure jest-dom is always available during testing to cut down on boilerplate
|
||||||
import '@testing-library/jest-dom/vitest';
|
import '@testing-library/jest-dom/vitest';
|
||||||
|
|
||||||
|
// With isolate: false, all test files share the same module context.
|
||||||
|
// Restore all mocks/spies after each test to prevent leakage between tests.
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,12 @@
|
||||||
* @description Application reducer.
|
* @description Application reducer.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AnyAction } from 'redux'
|
import { UnknownAction } from '@reduxjs/toolkit'
|
||||||
|
|
||||||
interface InitialState {
|
interface InitialState {
|
||||||
type: string | null
|
type: string | null
|
||||||
payload: any
|
payload: unknown
|
||||||
meta: any
|
meta: unknown
|
||||||
error: boolean
|
error: boolean
|
||||||
count: number
|
count: number
|
||||||
}
|
}
|
||||||
|
|
@ -33,7 +33,7 @@ const initialState: InitialState = {
|
||||||
*/
|
*/
|
||||||
export const actionReducer = (
|
export const actionReducer = (
|
||||||
state = initialState,
|
state = initialState,
|
||||||
action: AnyAction,
|
action: UnknownAction,
|
||||||
): InitialState => {
|
): InitialState => {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
|
import { create } from '@bufbuild/protobuf';
|
||||||
import { SortDirection } from 'types';
|
import { SortDirection } from 'types';
|
||||||
|
import { ServerInfo_UserSchema } from 'generated/proto/serverinfo_user_pb';
|
||||||
import SortUtil from './SortUtil';
|
import SortUtil from './SortUtil';
|
||||||
|
|
||||||
// ── sortByField ───────────────────────────────────────────────────────────────
|
// ── sortByField ───────────────────────────────────────────────────────────────
|
||||||
|
|
@ -118,11 +120,11 @@ describe('sortByFields', () => {
|
||||||
describe('sortUsersByField', () => {
|
describe('sortUsersByField', () => {
|
||||||
it('sorts by userLevel DESC first, then name ASC', () => {
|
it('sorts by userLevel DESC first, then name ASC', () => {
|
||||||
const users = [
|
const users = [
|
||||||
{ name: 'Alice', userLevel: 1, accountageSecs: 0n, privlevel: '' },
|
create(ServerInfo_UserSchema, { name: 'Alice', userLevel: 1, accountageSecs: 0n, privlevel: '' }),
|
||||||
{ name: 'Bob', userLevel: 8, accountageSecs: 0n, privlevel: '' },
|
create(ServerInfo_UserSchema, { name: 'Bob', userLevel: 8, accountageSecs: 0n, privlevel: '' }),
|
||||||
{ name: 'Carol', userLevel: 1, accountageSecs: 0n, privlevel: '' },
|
create(ServerInfo_UserSchema, { name: 'Carol', userLevel: 1, accountageSecs: 0n, privlevel: '' }),
|
||||||
];
|
];
|
||||||
SortUtil.sortUsersByField(users as any, { field: 'name', order: SortDirection.ASC });
|
SortUtil.sortUsersByField(users, { field: 'name', order: SortDirection.ASC });
|
||||||
expect(users[0].name).toBe('Bob');
|
expect(users[0].name).toBe('Bob');
|
||||||
expect(users[1].name).toBe('Alice');
|
expect(users[1].name).toBe('Alice');
|
||||||
expect(users[2].name).toBe('Carol');
|
expect(users[2].name).toBe('Carol');
|
||||||
|
|
@ -136,11 +138,11 @@ describe('sortUsersByField', () => {
|
||||||
|
|
||||||
it('returns 0 (stable) when two users tie on both userLevel and name', () => {
|
it('returns 0 (stable) when two users tie on both userLevel and name', () => {
|
||||||
const users = [
|
const users = [
|
||||||
{ name: 'Alice', userLevel: 1, accountageSecs: 0n, privlevel: '' },
|
create(ServerInfo_UserSchema, { name: 'Alice', userLevel: 1, accountageSecs: 0n, privlevel: '' }),
|
||||||
{ name: 'Alice', userLevel: 1, accountageSecs: 0n, privlevel: '' },
|
create(ServerInfo_UserSchema, { name: 'Alice', userLevel: 1, accountageSecs: 0n, privlevel: '' }),
|
||||||
];
|
];
|
||||||
expect(() =>
|
expect(() =>
|
||||||
SortUtil.sortUsersByField(users as any, { field: 'name', order: SortDirection.ASC })
|
SortUtil.sortUsersByField(users, { field: 'name', order: SortDirection.ASC })
|
||||||
).not.toThrow();
|
).not.toThrow();
|
||||||
expect(users).toHaveLength(2);
|
expect(users).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { SortBy, SortDirection, User } from 'types';
|
import { SortBy, SortDirection, User } from 'types';
|
||||||
|
|
||||||
export default class SortUtil {
|
export default class SortUtil {
|
||||||
static sortByField(arr: any[], sortBy: SortBy): void {
|
static sortByField<T extends object>(arr: T[], sortBy: SortBy): void {
|
||||||
if (arr.length) {
|
if (arr.length) {
|
||||||
const field = SortUtil.resolveFieldChain(arr[0], sortBy.field);
|
const field = SortUtil.resolveFieldChain(arr[0], sortBy.field);
|
||||||
const fieldType = typeof field;
|
const fieldType = typeof field;
|
||||||
|
|
@ -20,7 +20,7 @@ export default class SortUtil {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static sortByFields(arr: any[], sorts: SortBy[]) {
|
static sortByFields<T extends object>(arr: T[], sorts: SortBy[]) {
|
||||||
if (arr.length) {
|
if (arr.length) {
|
||||||
arr.sort((a, b) => {
|
arr.sort((a, b) => {
|
||||||
for (let i = 0; i < sorts.length; i++) {
|
for (let i = 0; i < sorts.length; i++) {
|
||||||
|
|
@ -57,7 +57,7 @@ export default class SortUtil {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static toggleSortBy(field: string, sortBy: SortBy) {
|
static toggleSortBy<F extends string>(field: F, sortBy: SortBy): { field: F; order: SortDirection } {
|
||||||
const sameField = field === sortBy.field;
|
const sameField = field === sortBy.field;
|
||||||
const isASC = sortBy.order === SortDirection.ASC;
|
const isASC = sortBy.order === SortDirection.ASC;
|
||||||
|
|
||||||
|
|
@ -67,15 +67,15 @@ export default class SortUtil {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static sortByNumber(arr: any[], sortBy: SortBy): void {
|
private static sortByNumber<T extends object>(arr: T[], sortBy: SortBy): void {
|
||||||
arr.sort((a, b) => SortUtil.numberComparator(a, b, sortBy));
|
arr.sort((a, b) => SortUtil.numberComparator(a, b, sortBy));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static sortByString(arr: any[], sortBy: SortBy): void {
|
private static sortByString<T extends object>(arr: T[], sortBy: SortBy): void {
|
||||||
arr.sort((a, b) => SortUtil.stringComparator(a, b, sortBy));
|
arr.sort((a, b) => SortUtil.stringComparator(a, b, sortBy));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static userComparator(a, b, sortBy, sortByUserLevel = true) {
|
private static userComparator(a: User, b: User, sortBy: SortBy, sortByUserLevel = true) {
|
||||||
if (sortByUserLevel) {
|
if (sortByUserLevel) {
|
||||||
const adminSortBy = {
|
const adminSortBy = {
|
||||||
field: 'userLevel',
|
field: 'userLevel',
|
||||||
|
|
@ -98,7 +98,7 @@ export default class SortUtil {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static numberComparator(a, b, { field, order }: SortBy) {
|
private static numberComparator<T extends object>(a: T, b: T, { field, order }: SortBy) {
|
||||||
const aResolved = SortUtil.resolveFieldChain(a, field);
|
const aResolved = SortUtil.resolveFieldChain(a, field);
|
||||||
const bResolved = SortUtil.resolveFieldChain(b, field);
|
const bResolved = SortUtil.resolveFieldChain(b, field);
|
||||||
|
|
||||||
|
|
@ -109,7 +109,7 @@ export default class SortUtil {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static stringComparator(a, b, { field, order }: SortBy) {
|
private static stringComparator<T extends object>(a: T, b: T, { field, order }: SortBy) {
|
||||||
const aResolved = SortUtil.resolveFieldChain(a, field);
|
const aResolved = SortUtil.resolveFieldChain(a, field);
|
||||||
const bResolved = SortUtil.resolveFieldChain(b, field);
|
const bResolved = SortUtil.resolveFieldChain(b, field);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
export { default as SortUtil } from './SortUtil';
|
export { default as SortUtil } from './SortUtil';
|
||||||
|
export * from './normalizers';
|
||||||
|
|
|
||||||
121
webclient/src/store/common/normalizers.spec.ts
Normal file
121
webclient/src/store/common/normalizers.spec.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
import { normalizeRoomInfo, normalizeGameObject, normalizeLogs, normalizeBannedUserError, normalizeUserMessage } from './normalizers';
|
||||||
|
import { create } from '@bufbuild/protobuf';
|
||||||
|
import { ServerInfo_RoomSchema } from 'generated/proto/serverinfo_room_pb';
|
||||||
|
import { ServerInfo_GameSchema } from 'generated/proto/serverinfo_game_pb';
|
||||||
|
import { Event_RoomSaySchema } from 'generated/proto/event_room_say_pb';
|
||||||
|
import { Message } from 'types';
|
||||||
|
|
||||||
|
describe('normalizeRoomInfo', () => {
|
||||||
|
it('builds gametypeMap from gametypeList and normalises games', () => {
|
||||||
|
const room = create(ServerInfo_RoomSchema, {
|
||||||
|
roomId: 1,
|
||||||
|
name: 'Lobby',
|
||||||
|
gametypeList: [{ gameTypeId: 1, description: 'Standard' }],
|
||||||
|
gameList: [
|
||||||
|
create(ServerInfo_GameSchema, { gameId: 10, gameTypes: [1], description: 'My Game' }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = normalizeRoomInfo(room);
|
||||||
|
|
||||||
|
expect(result.gametypeMap).toEqual({ 1: 'Standard' });
|
||||||
|
expect(result.gameList).toHaveLength(1);
|
||||||
|
expect(result.gameList[0].gameType).toBe('Standard');
|
||||||
|
expect(result.order).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles room with empty gametypeList', () => {
|
||||||
|
const room = create(ServerInfo_RoomSchema, { roomId: 2, name: 'Empty' });
|
||||||
|
const result = normalizeRoomInfo(room);
|
||||||
|
expect(result.gametypeMap).toEqual({});
|
||||||
|
expect(result.gameList).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('normalizeGameObject', () => {
|
||||||
|
it('maps gameTypes[0] to gameType string via gametypeMap', () => {
|
||||||
|
const game = create(ServerInfo_GameSchema, { gameId: 1, gameTypes: [5] });
|
||||||
|
const result = normalizeGameObject(game, { 5: 'Legacy' });
|
||||||
|
expect(result.gameType).toBe('Legacy');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty string when no gameTypes', () => {
|
||||||
|
const game = create(ServerInfo_GameSchema, { gameId: 2 });
|
||||||
|
const result = normalizeGameObject(game, {});
|
||||||
|
expect(result.gameType).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fills empty description with empty string', () => {
|
||||||
|
const game = create(ServerInfo_GameSchema, { gameId: 3 });
|
||||||
|
const result = normalizeGameObject(game, {});
|
||||||
|
expect(result.description).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('normalizeLogs', () => {
|
||||||
|
it('groups logs by targetType', () => {
|
||||||
|
const logs = [
|
||||||
|
{ targetType: 'room' },
|
||||||
|
{ targetType: 'game' },
|
||||||
|
{ targetType: 'room' },
|
||||||
|
] as any[];
|
||||||
|
const result = normalizeLogs(logs);
|
||||||
|
expect(result.room).toHaveLength(2);
|
||||||
|
expect(result.game).toHaveLength(1);
|
||||||
|
expect(result.chat).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty object for empty logs', () => {
|
||||||
|
expect(normalizeLogs([])).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('normalizeBannedUserError', () => {
|
||||||
|
it('returns permanently banned message when endTime is 0', () => {
|
||||||
|
expect(normalizeBannedUserError('', 0)).toBe('You are permanently banned');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns banned until date when endTime is given', () => {
|
||||||
|
const endTime = new Date('2030-01-01').getTime();
|
||||||
|
const result = normalizeBannedUserError('', endTime);
|
||||||
|
expect(result).toContain('You are banned until');
|
||||||
|
expect(result).toContain(new Date(endTime).toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('appends reason when provided', () => {
|
||||||
|
expect(normalizeBannedUserError('bad behavior', 0)).toContain('\n\nbad behavior');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not append separator when reason is empty', () => {
|
||||||
|
expect(normalizeBannedUserError('', 0)).not.toContain('\n\n');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('normalizeUserMessage', () => {
|
||||||
|
const makeMsg = (fields: Partial<Message>): Message => ({
|
||||||
|
...create(Event_RoomSaySchema),
|
||||||
|
timeReceived: 0,
|
||||||
|
...fields,
|
||||||
|
} as Message);
|
||||||
|
|
||||||
|
it('prepends "name: " to message when name is present', () => {
|
||||||
|
const result = normalizeUserMessage(makeMsg({ name: 'Alice', message: 'hello' }));
|
||||||
|
expect(result.message).toBe('Alice: hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns message unchanged when name is empty', () => {
|
||||||
|
const result = normalizeUserMessage(makeMsg({ name: '', message: 'system msg' }));
|
||||||
|
expect(result.message).toBe('system msg');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not mutate the original message', () => {
|
||||||
|
const original = makeMsg({ name: 'Bob', message: 'hi' });
|
||||||
|
normalizeUserMessage(original);
|
||||||
|
expect(original.message).toBe('hi');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the original reference when no name (no allocation)', () => {
|
||||||
|
const original = makeMsg({ name: '', message: 'hi' });
|
||||||
|
expect(normalizeUserMessage(original)).toBe(original);
|
||||||
|
});
|
||||||
|
});
|
||||||
85
webclient/src/store/common/normalizers.ts
Normal file
85
webclient/src/store/common/normalizers.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
import type { ServerInfo_Room } from 'generated/proto/serverinfo_room_pb';
|
||||||
|
import type { ServerInfo_Game } from 'generated/proto/serverinfo_game_pb';
|
||||||
|
import type { ServerInfo_GameType } from 'generated/proto/serverinfo_gametype_pb';
|
||||||
|
import { Game, GametypeMap, LogItem, LogGroups, Message, Room } from 'types';
|
||||||
|
|
||||||
|
/** Flatten a gametype list into a lookup map of { gameTypeId → description }. */
|
||||||
|
export function normalizeGametypeMap(gametypeList: ServerInfo_GameType[]): GametypeMap {
|
||||||
|
return gametypeList.reduce<GametypeMap>((map, type) => {
|
||||||
|
map[type.gameTypeId] = type.description;
|
||||||
|
return map;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Flatten room gameTypes into a map object and normalize all games inside. */
|
||||||
|
export function normalizeRoomInfo(roomInfo: ServerInfo_Room): Room {
|
||||||
|
const gametypeMap = normalizeGametypeMap(roomInfo.gametypeList);
|
||||||
|
|
||||||
|
const gameList = roomInfo.gameList.map(
|
||||||
|
(game) => normalizeGameObject(game, gametypeMap),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...roomInfo,
|
||||||
|
gametypeMap,
|
||||||
|
gameList,
|
||||||
|
order: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Flatten gameTypes[] into a gameType string; fill in default sortable values. */
|
||||||
|
export function normalizeGameObject(game: ServerInfo_Game, gametypeMap: GametypeMap): Game {
|
||||||
|
const { gameTypes, description } = game;
|
||||||
|
const hasType = gameTypes && gameTypes.length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...game,
|
||||||
|
gameType: hasType ? gametypeMap[gameTypes[0]] : '',
|
||||||
|
description: description || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Group a flat LogItem[] into { room, game, chat } buckets for the server store. */
|
||||||
|
export function normalizeLogs(logs: LogItem[]): LogGroups {
|
||||||
|
return logs.reduce((obj, log) => {
|
||||||
|
const type = log.targetType as keyof LogGroups;
|
||||||
|
obj[type] = obj[type] || [];
|
||||||
|
obj[type]!.push(log);
|
||||||
|
return obj;
|
||||||
|
}, {} as LogGroups);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepend "name: " to the message text when a sender name is present.
|
||||||
|
* Messages from the current user are sent without a name by the server,
|
||||||
|
* so this is a no-op for those.
|
||||||
|
* Returns a new Message — does not mutate the original.
|
||||||
|
*/
|
||||||
|
export function normalizeUserMessage(message: Message): Message {
|
||||||
|
if (!message.name) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
return { ...message, message: `${message.name}: ${message.message}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the user-facing ban error string from raw server data.
|
||||||
|
* The server sends a reason string and an endTime epoch ms (0 = permanent).
|
||||||
|
* Messages from the current user do not carry the username — this quirk is
|
||||||
|
* handled at the dispatch layer so the redux store always stores a clean string.
|
||||||
|
*/
|
||||||
|
export function normalizeBannedUserError(reason: string, endTime: number): string {
|
||||||
|
let error: string;
|
||||||
|
|
||||||
|
if (endTime) {
|
||||||
|
error = 'You are banned until ' + new Date(endTime).toString();
|
||||||
|
} else {
|
||||||
|
error = 'You are permanently banned';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reason) {
|
||||||
|
error += '\n\n' + reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,14 @@
|
||||||
import { ArrowInfo, CardInfo, CounterInfo, PlayerProperties } from 'types';
|
import { ArrowInfo, CardInfo, CounterInfo, PlayerProperties, ProtoInit } from 'types';
|
||||||
|
import { create } from '@bufbuild/protobuf';
|
||||||
|
import { ServerInfo_CardSchema } from 'generated/proto/serverinfo_card_pb';
|
||||||
|
import { ServerInfo_CounterSchema } from 'generated/proto/serverinfo_counter_pb';
|
||||||
|
import { colorSchema } from 'generated/proto/color_pb';
|
||||||
|
import { ServerInfo_ArrowSchema } from 'generated/proto/serverinfo_arrow_pb';
|
||||||
|
import { ServerInfo_PlayerPropertiesSchema } from 'generated/proto/serverinfo_playerproperties_pb';
|
||||||
import { GameEntry, GamesState, PlayerEntry, ZoneEntry } from '../game.interfaces';
|
import { GameEntry, GamesState, PlayerEntry, ZoneEntry } from '../game.interfaces';
|
||||||
|
|
||||||
export function makeCard(overrides: Partial<CardInfo> = {}): CardInfo {
|
export function makeCard(overrides: ProtoInit<CardInfo> = {}): CardInfo {
|
||||||
return {
|
return create(ServerInfo_CardSchema, {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Test Card',
|
name: 'Test Card',
|
||||||
x: 0,
|
x: 0,
|
||||||
|
|
@ -21,22 +27,22 @@ export function makeCard(overrides: Partial<CardInfo> = {}): CardInfo {
|
||||||
attachCardId: -1,
|
attachCardId: -1,
|
||||||
providerId: '',
|
providerId: '',
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeCounter(overrides: Partial<CounterInfo> = {}): CounterInfo {
|
export function makeCounter(overrides: ProtoInit<CounterInfo> = {}): CounterInfo {
|
||||||
return {
|
return create(ServerInfo_CounterSchema, {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Life',
|
name: 'Life',
|
||||||
counterColor: { r: 0, g: 0, b: 0, a: 255 },
|
counterColor: create(colorSchema, { r: 0, g: 0, b: 0, a: 255 }),
|
||||||
radius: 1,
|
radius: 1,
|
||||||
count: 20,
|
count: 20,
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeArrow(overrides: Partial<ArrowInfo> = {}): ArrowInfo {
|
export function makeArrow(overrides: ProtoInit<ArrowInfo> = {}): ArrowInfo {
|
||||||
return {
|
return create(ServerInfo_ArrowSchema, {
|
||||||
id: 1,
|
id: 1,
|
||||||
startPlayerId: 1,
|
startPlayerId: 1,
|
||||||
startZone: 'table',
|
startZone: 'table',
|
||||||
|
|
@ -44,9 +50,9 @@ export function makeArrow(overrides: Partial<ArrowInfo> = {}): ArrowInfo {
|
||||||
targetPlayerId: 1,
|
targetPlayerId: 1,
|
||||||
targetZone: 'table',
|
targetZone: 'table',
|
||||||
targetCardId: 2,
|
targetCardId: 2,
|
||||||
arrowColor: { r: 255, g: 0, b: 0, a: 255 },
|
arrowColor: create(colorSchema, { r: 255, g: 0, b: 0, a: 255 }),
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeZoneEntry(overrides: Partial<ZoneEntry> = {}): ZoneEntry {
|
export function makeZoneEntry(overrides: Partial<ZoneEntry> = {}): ZoneEntry {
|
||||||
|
|
@ -62,10 +68,9 @@ export function makeZoneEntry(overrides: Partial<ZoneEntry> = {}): ZoneEntry {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makePlayerProperties(overrides: Partial<PlayerProperties> = {}): PlayerProperties {
|
export function makePlayerProperties(overrides: ProtoInit<PlayerProperties> = {}): PlayerProperties {
|
||||||
return {
|
return create(ServerInfo_PlayerPropertiesSchema, {
|
||||||
playerId: 1,
|
playerId: 1,
|
||||||
userInfo: null,
|
|
||||||
spectator: false,
|
spectator: false,
|
||||||
conceded: false,
|
conceded: false,
|
||||||
readyStart: false,
|
readyStart: false,
|
||||||
|
|
@ -74,7 +79,7 @@ export function makePlayerProperties(overrides: Partial<PlayerProperties> = {}):
|
||||||
sideboardLocked: false,
|
sideboardLocked: false,
|
||||||
judge: false,
|
judge: false,
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makePlayerEntry(overrides: Partial<PlayerEntry> = {}): PlayerEntry {
|
export function makePlayerEntry(overrides: Partial<PlayerEntry> = {}): PlayerEntry {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { create } from '@bufbuild/protobuf';
|
||||||
import { Actions } from './game.actions';
|
import { Actions } from './game.actions';
|
||||||
import { Types } from './game.types';
|
import { Types } from './game.types';
|
||||||
import {
|
import {
|
||||||
|
|
@ -6,8 +7,26 @@ import {
|
||||||
makeCounter,
|
makeCounter,
|
||||||
makeGameEntry,
|
makeGameEntry,
|
||||||
makePlayerProperties,
|
makePlayerProperties,
|
||||||
makeZoneEntry,
|
|
||||||
} from './__mocks__/fixtures';
|
} from './__mocks__/fixtures';
|
||||||
|
import { Event_GameStateChangedSchema } from 'generated/proto/event_game_state_changed_pb';
|
||||||
|
import { Event_MoveCardSchema } from 'generated/proto/event_move_card_pb';
|
||||||
|
import { Event_FlipCardSchema } from 'generated/proto/event_flip_card_pb';
|
||||||
|
import { Event_DestroyCardSchema } from 'generated/proto/event_destroy_card_pb';
|
||||||
|
import { Event_AttachCardSchema } from 'generated/proto/event_attach_card_pb';
|
||||||
|
import { Event_CreateTokenSchema } from 'generated/proto/event_create_token_pb';
|
||||||
|
import { Event_SetCardAttrSchema } from 'generated/proto/event_set_card_attr_pb';
|
||||||
|
import { Event_SetCardCounterSchema } from 'generated/proto/event_set_card_counter_pb';
|
||||||
|
import { Event_CreateArrowSchema } from 'generated/proto/event_create_arrow_pb';
|
||||||
|
import { Event_DeleteArrowSchema } from 'generated/proto/event_delete_arrow_pb';
|
||||||
|
import { Event_CreateCounterSchema } from 'generated/proto/event_create_counter_pb';
|
||||||
|
import { Event_SetCounterSchema } from 'generated/proto/event_set_counter_pb';
|
||||||
|
import { Event_DelCounterSchema } from 'generated/proto/event_del_counter_pb';
|
||||||
|
import { Event_DrawCardsSchema } from 'generated/proto/event_draw_cards_pb';
|
||||||
|
import { Event_RevealCardsSchema } from 'generated/proto/event_reveal_cards_pb';
|
||||||
|
import { Event_ShuffleSchema } from 'generated/proto/event_shuffle_pb';
|
||||||
|
import { Event_RollDieSchema } from 'generated/proto/event_roll_die_pb';
|
||||||
|
import { Event_DumpZoneSchema } from 'generated/proto/event_dump_zone_pb';
|
||||||
|
import { Event_ChangeZonePropertiesSchema } from 'generated/proto/event_change_zone_properties_pb';
|
||||||
|
|
||||||
describe('Actions', () => {
|
describe('Actions', () => {
|
||||||
it('clearStore', () => {
|
it('clearStore', () => {
|
||||||
|
|
@ -32,7 +51,9 @@ describe('Actions', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('gameStateChanged', () => {
|
it('gameStateChanged', () => {
|
||||||
const data = { playerList: [], gameStarted: true, activePlayerId: 1, activePhase: 0, secondsElapsed: 0 };
|
const data = create(Event_GameStateChangedSchema, {
|
||||||
|
playerList: [], gameStarted: true, activePlayerId: 1, activePhase: 0, secondsElapsed: 0
|
||||||
|
});
|
||||||
expect(Actions.gameStateChanged(1, data)).toEqual({ type: Types.GAME_STATE_CHANGED, gameId: 1, data });
|
expect(Actions.gameStateChanged(1, data)).toEqual({ type: Types.GAME_STATE_CHANGED, gameId: 1, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -60,85 +81,85 @@ describe('Actions', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cardMoved', () => {
|
it('cardMoved', () => {
|
||||||
const data = { cardId: 1 } as any;
|
const data = create(Event_MoveCardSchema, { cardId: 1 });
|
||||||
expect(Actions.cardMoved(1, 2, data)).toEqual({ type: Types.CARD_MOVED, gameId: 1, playerId: 2, data });
|
expect(Actions.cardMoved(1, 2, data)).toEqual({ type: Types.CARD_MOVED, gameId: 1, playerId: 2, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cardFlipped', () => {
|
it('cardFlipped', () => {
|
||||||
const data = { cardId: 1 } as any;
|
const data = create(Event_FlipCardSchema, { cardId: 1 });
|
||||||
expect(Actions.cardFlipped(1, 2, data)).toEqual({ type: Types.CARD_FLIPPED, gameId: 1, playerId: 2, data });
|
expect(Actions.cardFlipped(1, 2, data)).toEqual({ type: Types.CARD_FLIPPED, gameId: 1, playerId: 2, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cardDestroyed', () => {
|
it('cardDestroyed', () => {
|
||||||
const data = { cardId: 1 } as any;
|
const data = create(Event_DestroyCardSchema, { cardId: 1 });
|
||||||
expect(Actions.cardDestroyed(1, 2, data)).toEqual({ type: Types.CARD_DESTROYED, gameId: 1, playerId: 2, data });
|
expect(Actions.cardDestroyed(1, 2, data)).toEqual({ type: Types.CARD_DESTROYED, gameId: 1, playerId: 2, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cardAttached', () => {
|
it('cardAttached', () => {
|
||||||
const data = { cardId: 1 } as any;
|
const data = create(Event_AttachCardSchema, { cardId: 1 });
|
||||||
expect(Actions.cardAttached(1, 2, data)).toEqual({ type: Types.CARD_ATTACHED, gameId: 1, playerId: 2, data });
|
expect(Actions.cardAttached(1, 2, data)).toEqual({ type: Types.CARD_ATTACHED, gameId: 1, playerId: 2, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('tokenCreated', () => {
|
it('tokenCreated', () => {
|
||||||
const data = { cardId: 1 } as any;
|
const data = create(Event_CreateTokenSchema, { cardId: 1 });
|
||||||
expect(Actions.tokenCreated(1, 2, data)).toEqual({ type: Types.TOKEN_CREATED, gameId: 1, playerId: 2, data });
|
expect(Actions.tokenCreated(1, 2, data)).toEqual({ type: Types.TOKEN_CREATED, gameId: 1, playerId: 2, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cardAttrChanged', () => {
|
it('cardAttrChanged', () => {
|
||||||
const data = { cardId: 1 } as any;
|
const data = create(Event_SetCardAttrSchema, { cardId: 1 });
|
||||||
expect(Actions.cardAttrChanged(1, 2, data)).toEqual({ type: Types.CARD_ATTR_CHANGED, gameId: 1, playerId: 2, data });
|
expect(Actions.cardAttrChanged(1, 2, data)).toEqual({ type: Types.CARD_ATTR_CHANGED, gameId: 1, playerId: 2, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cardCounterChanged', () => {
|
it('cardCounterChanged', () => {
|
||||||
const data = { cardId: 1 } as any;
|
const data = create(Event_SetCardCounterSchema, { cardId: 1 });
|
||||||
expect(Actions.cardCounterChanged(1, 2, data)).toEqual({ type: Types.CARD_COUNTER_CHANGED, gameId: 1, playerId: 2, data });
|
expect(Actions.cardCounterChanged(1, 2, data)).toEqual({ type: Types.CARD_COUNTER_CHANGED, gameId: 1, playerId: 2, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('arrowCreated', () => {
|
it('arrowCreated', () => {
|
||||||
const arrow = makeArrow();
|
const arrow = makeArrow();
|
||||||
const data = { arrowInfo: arrow };
|
const data = create(Event_CreateArrowSchema, { arrowInfo: arrow });
|
||||||
expect(Actions.arrowCreated(1, 2, data)).toEqual({ type: Types.ARROW_CREATED, gameId: 1, playerId: 2, data });
|
expect(Actions.arrowCreated(1, 2, data)).toEqual({ type: Types.ARROW_CREATED, gameId: 1, playerId: 2, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('arrowDeleted', () => {
|
it('arrowDeleted', () => {
|
||||||
const data = { arrowId: 3 };
|
const data = create(Event_DeleteArrowSchema, { arrowId: 3 });
|
||||||
expect(Actions.arrowDeleted(1, 2, data)).toEqual({ type: Types.ARROW_DELETED, gameId: 1, playerId: 2, data });
|
expect(Actions.arrowDeleted(1, 2, data)).toEqual({ type: Types.ARROW_DELETED, gameId: 1, playerId: 2, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('counterCreated', () => {
|
it('counterCreated', () => {
|
||||||
const counter = makeCounter();
|
const counter = makeCounter();
|
||||||
const data = { counterInfo: counter };
|
const data = create(Event_CreateCounterSchema, { counterInfo: counter });
|
||||||
expect(Actions.counterCreated(1, 2, data)).toEqual({ type: Types.COUNTER_CREATED, gameId: 1, playerId: 2, data });
|
expect(Actions.counterCreated(1, 2, data)).toEqual({ type: Types.COUNTER_CREATED, gameId: 1, playerId: 2, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('counterSet', () => {
|
it('counterSet', () => {
|
||||||
const data = { counterId: 1, value: 10 };
|
const data = create(Event_SetCounterSchema, { counterId: 1, value: 10 });
|
||||||
expect(Actions.counterSet(1, 2, data)).toEqual({ type: Types.COUNTER_SET, gameId: 1, playerId: 2, data });
|
expect(Actions.counterSet(1, 2, data)).toEqual({ type: Types.COUNTER_SET, gameId: 1, playerId: 2, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('counterDeleted', () => {
|
it('counterDeleted', () => {
|
||||||
const data = { counterId: 1 };
|
const data = create(Event_DelCounterSchema, { counterId: 1 });
|
||||||
expect(Actions.counterDeleted(1, 2, data)).toEqual({ type: Types.COUNTER_DELETED, gameId: 1, playerId: 2, data });
|
expect(Actions.counterDeleted(1, 2, data)).toEqual({ type: Types.COUNTER_DELETED, gameId: 1, playerId: 2, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cardsDrawn', () => {
|
it('cardsDrawn', () => {
|
||||||
const card = makeCard();
|
const card = makeCard();
|
||||||
const data = { number: 2, cards: [card] };
|
const data = create(Event_DrawCardsSchema, { number: 2, cards: [card] });
|
||||||
expect(Actions.cardsDrawn(1, 2, data)).toEqual({ type: Types.CARDS_DRAWN, gameId: 1, playerId: 2, data });
|
expect(Actions.cardsDrawn(1, 2, data)).toEqual({ type: Types.CARDS_DRAWN, gameId: 1, playerId: 2, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cardsRevealed', () => {
|
it('cardsRevealed', () => {
|
||||||
const data = { zoneName: 'hand', cards: [] } as any;
|
const data = create(Event_RevealCardsSchema, { zoneName: 'hand', cards: [] });
|
||||||
expect(Actions.cardsRevealed(1, 2, data)).toEqual({ type: Types.CARDS_REVEALED, gameId: 1, playerId: 2, data });
|
expect(Actions.cardsRevealed(1, 2, data)).toEqual({ type: Types.CARDS_REVEALED, gameId: 1, playerId: 2, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('zoneShuffled', () => {
|
it('zoneShuffled', () => {
|
||||||
const data = { zoneName: 'deck', start: 0, end: 39 };
|
const data = create(Event_ShuffleSchema, { zoneName: 'deck', start: 0, end: 39 });
|
||||||
expect(Actions.zoneShuffled(1, 2, data)).toEqual({ type: Types.ZONE_SHUFFLED, gameId: 1, playerId: 2, data });
|
expect(Actions.zoneShuffled(1, 2, data)).toEqual({ type: Types.ZONE_SHUFFLED, gameId: 1, playerId: 2, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('dieRolled', () => {
|
it('dieRolled', () => {
|
||||||
const data = { sides: 6, value: 4, values: [4] };
|
const data = create(Event_RollDieSchema, { sides: 6, value: 4, values: [4] });
|
||||||
expect(Actions.dieRolled(1, 2, data)).toEqual({ type: Types.DIE_ROLLED, gameId: 1, playerId: 2, data });
|
expect(Actions.dieRolled(1, 2, data)).toEqual({ type: Types.DIE_ROLLED, gameId: 1, playerId: 2, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -155,12 +176,12 @@ describe('Actions', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('zoneDumped', () => {
|
it('zoneDumped', () => {
|
||||||
const data = { zoneOwnerId: 1, zoneName: 'hand', numberCards: 3, isReversed: false };
|
const data = create(Event_DumpZoneSchema, { zoneOwnerId: 1, zoneName: 'hand', numberCards: 3, isReversed: false });
|
||||||
expect(Actions.zoneDumped(1, 2, data)).toEqual({ type: Types.ZONE_DUMPED, gameId: 1, playerId: 2, data });
|
expect(Actions.zoneDumped(1, 2, data)).toEqual({ type: Types.ZONE_DUMPED, gameId: 1, playerId: 2, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('zonePropertiesChanged', () => {
|
it('zonePropertiesChanged', () => {
|
||||||
const data = { zoneName: 'deck', alwaysRevealTopCard: true, alwaysLookAtTopCard: false };
|
const data = create(Event_ChangeZonePropertiesSchema, { zoneName: 'deck', alwaysRevealTopCard: true, alwaysLookAtTopCard: false });
|
||||||
expect(Actions.zonePropertiesChanged(1, 2, data)).toEqual({
|
expect(Actions.zonePropertiesChanged(1, 2, data)).toEqual({
|
||||||
type: Types.ZONE_PROPERTIES_CHANGED,
|
type: Types.ZONE_PROPERTIES_CHANGED,
|
||||||
gameId: 1,
|
gameId: 1,
|
||||||
|
|
|
||||||
|
|
@ -232,3 +232,5 @@ export const Actions = {
|
||||||
message,
|
message,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GameAction = ReturnType<typeof Actions[keyof typeof Actions]>;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
vi.mock('store/store', () => ({ store: { dispatch: vi.fn() } }));
|
vi.mock('store/store', () => ({ store: { dispatch: vi.fn() } }));
|
||||||
|
|
||||||
|
import { create } from '@bufbuild/protobuf';
|
||||||
import { store } from 'store/store';
|
import { store } from 'store/store';
|
||||||
import { Actions } from './game.actions';
|
import { Actions } from './game.actions';
|
||||||
import { Dispatch } from './game.dispatch';
|
import { Dispatch } from './game.dispatch';
|
||||||
|
|
@ -10,6 +11,25 @@ import {
|
||||||
makeGameEntry,
|
makeGameEntry,
|
||||||
makePlayerProperties,
|
makePlayerProperties,
|
||||||
} from './__mocks__/fixtures';
|
} from './__mocks__/fixtures';
|
||||||
|
import { Event_GameStateChangedSchema } from 'generated/proto/event_game_state_changed_pb';
|
||||||
|
import { Event_MoveCardSchema } from 'generated/proto/event_move_card_pb';
|
||||||
|
import { Event_FlipCardSchema } from 'generated/proto/event_flip_card_pb';
|
||||||
|
import { Event_DestroyCardSchema } from 'generated/proto/event_destroy_card_pb';
|
||||||
|
import { Event_AttachCardSchema } from 'generated/proto/event_attach_card_pb';
|
||||||
|
import { Event_CreateTokenSchema } from 'generated/proto/event_create_token_pb';
|
||||||
|
import { Event_SetCardAttrSchema } from 'generated/proto/event_set_card_attr_pb';
|
||||||
|
import { Event_SetCardCounterSchema } from 'generated/proto/event_set_card_counter_pb';
|
||||||
|
import { Event_CreateArrowSchema } from 'generated/proto/event_create_arrow_pb';
|
||||||
|
import { Event_DeleteArrowSchema } from 'generated/proto/event_delete_arrow_pb';
|
||||||
|
import { Event_CreateCounterSchema } from 'generated/proto/event_create_counter_pb';
|
||||||
|
import { Event_SetCounterSchema } from 'generated/proto/event_set_counter_pb';
|
||||||
|
import { Event_DelCounterSchema } from 'generated/proto/event_del_counter_pb';
|
||||||
|
import { Event_DrawCardsSchema } from 'generated/proto/event_draw_cards_pb';
|
||||||
|
import { Event_RevealCardsSchema } from 'generated/proto/event_reveal_cards_pb';
|
||||||
|
import { Event_ShuffleSchema } from 'generated/proto/event_shuffle_pb';
|
||||||
|
import { Event_RollDieSchema } from 'generated/proto/event_roll_die_pb';
|
||||||
|
import { Event_DumpZoneSchema } from 'generated/proto/event_dump_zone_pb';
|
||||||
|
import { Event_ChangeZonePropertiesSchema } from 'generated/proto/event_change_zone_properties_pb';
|
||||||
|
|
||||||
beforeEach(() => vi.clearAllMocks());
|
beforeEach(() => vi.clearAllMocks());
|
||||||
|
|
||||||
|
|
@ -41,7 +61,9 @@ describe('Dispatch', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('gameStateChanged dispatches Actions.gameStateChanged()', () => {
|
it('gameStateChanged dispatches Actions.gameStateChanged()', () => {
|
||||||
const data = { playerList: [], gameStarted: false, activePlayerId: 0, activePhase: 0, secondsElapsed: 0 };
|
const data = create(Event_GameStateChangedSchema, {
|
||||||
|
playerList: [], gameStarted: false, activePlayerId: 0, activePhase: 0, secondsElapsed: 0
|
||||||
|
});
|
||||||
Dispatch.gameStateChanged(1, data);
|
Dispatch.gameStateChanged(1, data);
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.gameStateChanged(1, data));
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.gameStateChanged(1, data));
|
||||||
});
|
});
|
||||||
|
|
@ -69,97 +91,97 @@ describe('Dispatch', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cardMoved dispatches Actions.cardMoved()', () => {
|
it('cardMoved dispatches Actions.cardMoved()', () => {
|
||||||
const data = { cardId: 1 } as any;
|
const data = create(Event_MoveCardSchema, { cardId: 1 });
|
||||||
Dispatch.cardMoved(1, 2, data);
|
Dispatch.cardMoved(1, 2, data);
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardMoved(1, 2, data));
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardMoved(1, 2, data));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cardFlipped dispatches Actions.cardFlipped()', () => {
|
it('cardFlipped dispatches Actions.cardFlipped()', () => {
|
||||||
const data = { cardId: 1 } as any;
|
const data = create(Event_FlipCardSchema, { cardId: 1 });
|
||||||
Dispatch.cardFlipped(1, 2, data);
|
Dispatch.cardFlipped(1, 2, data);
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardFlipped(1, 2, data));
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardFlipped(1, 2, data));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cardDestroyed dispatches Actions.cardDestroyed()', () => {
|
it('cardDestroyed dispatches Actions.cardDestroyed()', () => {
|
||||||
const data = { cardId: 1 } as any;
|
const data = create(Event_DestroyCardSchema, { cardId: 1 });
|
||||||
Dispatch.cardDestroyed(1, 2, data);
|
Dispatch.cardDestroyed(1, 2, data);
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardDestroyed(1, 2, data));
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardDestroyed(1, 2, data));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cardAttached dispatches Actions.cardAttached()', () => {
|
it('cardAttached dispatches Actions.cardAttached()', () => {
|
||||||
const data = { cardId: 1 } as any;
|
const data = create(Event_AttachCardSchema, { cardId: 1 });
|
||||||
Dispatch.cardAttached(1, 2, data);
|
Dispatch.cardAttached(1, 2, data);
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardAttached(1, 2, data));
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardAttached(1, 2, data));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('tokenCreated dispatches Actions.tokenCreated()', () => {
|
it('tokenCreated dispatches Actions.tokenCreated()', () => {
|
||||||
const data = { cardId: 1 } as any;
|
const data = create(Event_CreateTokenSchema, { cardId: 1 });
|
||||||
Dispatch.tokenCreated(1, 2, data);
|
Dispatch.tokenCreated(1, 2, data);
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.tokenCreated(1, 2, data));
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.tokenCreated(1, 2, data));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cardAttrChanged dispatches Actions.cardAttrChanged()', () => {
|
it('cardAttrChanged dispatches Actions.cardAttrChanged()', () => {
|
||||||
const data = { cardId: 1 } as any;
|
const data = create(Event_SetCardAttrSchema, { cardId: 1 });
|
||||||
Dispatch.cardAttrChanged(1, 2, data);
|
Dispatch.cardAttrChanged(1, 2, data);
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardAttrChanged(1, 2, data));
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardAttrChanged(1, 2, data));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cardCounterChanged dispatches Actions.cardCounterChanged()', () => {
|
it('cardCounterChanged dispatches Actions.cardCounterChanged()', () => {
|
||||||
const data = { cardId: 1 } as any;
|
const data = create(Event_SetCardCounterSchema, { cardId: 1 });
|
||||||
Dispatch.cardCounterChanged(1, 2, data);
|
Dispatch.cardCounterChanged(1, 2, data);
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardCounterChanged(1, 2, data));
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardCounterChanged(1, 2, data));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('arrowCreated dispatches Actions.arrowCreated()', () => {
|
it('arrowCreated dispatches Actions.arrowCreated()', () => {
|
||||||
const data = { arrowInfo: makeArrow() };
|
const data = create(Event_CreateArrowSchema, { arrowInfo: makeArrow() });
|
||||||
Dispatch.arrowCreated(1, 2, data);
|
Dispatch.arrowCreated(1, 2, data);
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.arrowCreated(1, 2, data));
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.arrowCreated(1, 2, data));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('arrowDeleted dispatches Actions.arrowDeleted()', () => {
|
it('arrowDeleted dispatches Actions.arrowDeleted()', () => {
|
||||||
const data = { arrowId: 3 };
|
const data = create(Event_DeleteArrowSchema, { arrowId: 3 });
|
||||||
Dispatch.arrowDeleted(1, 2, data);
|
Dispatch.arrowDeleted(1, 2, data);
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.arrowDeleted(1, 2, data));
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.arrowDeleted(1, 2, data));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('counterCreated dispatches Actions.counterCreated()', () => {
|
it('counterCreated dispatches Actions.counterCreated()', () => {
|
||||||
const data = { counterInfo: makeCounter() };
|
const data = create(Event_CreateCounterSchema, { counterInfo: makeCounter() });
|
||||||
Dispatch.counterCreated(1, 2, data);
|
Dispatch.counterCreated(1, 2, data);
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.counterCreated(1, 2, data));
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.counterCreated(1, 2, data));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('counterSet dispatches Actions.counterSet()', () => {
|
it('counterSet dispatches Actions.counterSet()', () => {
|
||||||
const data = { counterId: 1, value: 10 };
|
const data = create(Event_SetCounterSchema, { counterId: 1, value: 10 });
|
||||||
Dispatch.counterSet(1, 2, data);
|
Dispatch.counterSet(1, 2, data);
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.counterSet(1, 2, data));
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.counterSet(1, 2, data));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('counterDeleted dispatches Actions.counterDeleted()', () => {
|
it('counterDeleted dispatches Actions.counterDeleted()', () => {
|
||||||
const data = { counterId: 1 };
|
const data = create(Event_DelCounterSchema, { counterId: 1 });
|
||||||
Dispatch.counterDeleted(1, 2, data);
|
Dispatch.counterDeleted(1, 2, data);
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.counterDeleted(1, 2, data));
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.counterDeleted(1, 2, data));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cardsDrawn dispatches Actions.cardsDrawn()', () => {
|
it('cardsDrawn dispatches Actions.cardsDrawn()', () => {
|
||||||
const data = { number: 2, cards: [makeCard()] };
|
const data = create(Event_DrawCardsSchema, { number: 2, cards: [makeCard()] });
|
||||||
Dispatch.cardsDrawn(1, 2, data);
|
Dispatch.cardsDrawn(1, 2, data);
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardsDrawn(1, 2, data));
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardsDrawn(1, 2, data));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cardsRevealed dispatches Actions.cardsRevealed()', () => {
|
it('cardsRevealed dispatches Actions.cardsRevealed()', () => {
|
||||||
const data = { zoneName: 'hand', cards: [] } as any;
|
const data = create(Event_RevealCardsSchema, { zoneName: 'hand', cards: [] });
|
||||||
Dispatch.cardsRevealed(1, 2, data);
|
Dispatch.cardsRevealed(1, 2, data);
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardsRevealed(1, 2, data));
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.cardsRevealed(1, 2, data));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('zoneShuffled dispatches Actions.zoneShuffled()', () => {
|
it('zoneShuffled dispatches Actions.zoneShuffled()', () => {
|
||||||
const data = { zoneName: 'deck', start: 0, end: 39 };
|
const data = create(Event_ShuffleSchema, { zoneName: 'deck', start: 0, end: 39 });
|
||||||
Dispatch.zoneShuffled(1, 2, data);
|
Dispatch.zoneShuffled(1, 2, data);
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.zoneShuffled(1, 2, data));
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.zoneShuffled(1, 2, data));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('dieRolled dispatches Actions.dieRolled()', () => {
|
it('dieRolled dispatches Actions.dieRolled()', () => {
|
||||||
const data = { sides: 6, value: 4, values: [4] };
|
const data = create(Event_RollDieSchema, { sides: 6, value: 4, values: [4] });
|
||||||
Dispatch.dieRolled(1, 2, data);
|
Dispatch.dieRolled(1, 2, data);
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.dieRolled(1, 2, data));
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.dieRolled(1, 2, data));
|
||||||
});
|
});
|
||||||
|
|
@ -180,13 +202,13 @@ describe('Dispatch', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('zoneDumped dispatches Actions.zoneDumped()', () => {
|
it('zoneDumped dispatches Actions.zoneDumped()', () => {
|
||||||
const data = { zoneOwnerId: 1, zoneName: 'hand', numberCards: 3, isReversed: false };
|
const data = create(Event_DumpZoneSchema, { zoneOwnerId: 1, zoneName: 'hand', numberCards: 3, isReversed: false });
|
||||||
Dispatch.zoneDumped(1, 2, data);
|
Dispatch.zoneDumped(1, 2, data);
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.zoneDumped(1, 2, data));
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.zoneDumped(1, 2, data));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('zonePropertiesChanged dispatches Actions.zonePropertiesChanged()', () => {
|
it('zonePropertiesChanged dispatches Actions.zonePropertiesChanged()', () => {
|
||||||
const data = { zoneName: 'deck', alwaysRevealTopCard: true, alwaysLookAtTopCard: false };
|
const data = create(Event_ChangeZonePropertiesSchema, { zoneName: 'deck', alwaysRevealTopCard: true, alwaysLookAtTopCard: false });
|
||||||
Dispatch.zonePropertiesChanged(1, 2, data);
|
Dispatch.zonePropertiesChanged(1, 2, data);
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.zonePropertiesChanged(1, 2, data));
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.zonePropertiesChanged(1, 2, data));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { create } from '@bufbuild/protobuf';
|
||||||
import { CardAttribute, PlayerInfo } from 'types';
|
import { CardAttribute, PlayerInfo } from 'types';
|
||||||
import { gamesReducer } from './game.reducer';
|
import { gamesReducer } from './game.reducer';
|
||||||
import { Types } from './game.types';
|
import { Types } from './game.types';
|
||||||
|
|
@ -11,6 +12,7 @@ import {
|
||||||
makeState,
|
makeState,
|
||||||
makeZoneEntry,
|
makeZoneEntry,
|
||||||
} from './__mocks__/fixtures';
|
} from './__mocks__/fixtures';
|
||||||
|
import { ServerInfo_PlayerSchema } from 'generated/proto/serverinfo_player_pb';
|
||||||
|
|
||||||
// ── 2A: Initialisation & lifecycle ───────────────────────────────────────────
|
// ── 2A: Initialisation & lifecycle ───────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -67,7 +69,7 @@ describe('2B: Game state & player management', () => {
|
||||||
const counter = makeCounter({ id: 2 });
|
const counter = makeCounter({ id: 2 });
|
||||||
const arrow = makeArrow({ id: 3 });
|
const arrow = makeArrow({ id: 3 });
|
||||||
const playerList: PlayerInfo[] = [
|
const playerList: PlayerInfo[] = [
|
||||||
{
|
create(ServerInfo_PlayerSchema, {
|
||||||
properties: makePlayerProperties({ playerId: 7 }),
|
properties: makePlayerProperties({ playerId: 7 }),
|
||||||
deckList: 'some deck',
|
deckList: 'some deck',
|
||||||
zoneList: [
|
zoneList: [
|
||||||
|
|
@ -83,7 +85,7 @@ describe('2B: Game state & player management', () => {
|
||||||
],
|
],
|
||||||
counterList: [counter],
|
counterList: [counter],
|
||||||
arrowList: [arrow],
|
arrowList: [arrow],
|
||||||
},
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = gamesReducer(state, {
|
const result = gamesReducer(state, {
|
||||||
|
|
@ -620,7 +622,7 @@ describe('2F: CARD_COUNTER_CHANGED', () => {
|
||||||
playerId: 1,
|
playerId: 1,
|
||||||
data: { zoneName: 'table', cardId: 4, counterId: 1, counterValue: 3 },
|
data: { zoneName: 'table', cardId: 4, counterId: 1, counterValue: 3 },
|
||||||
});
|
});
|
||||||
expect(result.games[1].players[1].zones['table'].cards[0].counterList).toEqual([{ id: 1, value: 3 }]);
|
expect(result.games[1].players[1].zones['table'].cards[0].counterList).toEqual([expect.objectContaining({ id: 1, value: 3 })]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates existing counter value when counterId matches', () => {
|
it('updates existing counter value when counterId matches', () => {
|
||||||
|
|
@ -631,7 +633,7 @@ describe('2F: CARD_COUNTER_CHANGED', () => {
|
||||||
playerId: 1,
|
playerId: 1,
|
||||||
data: { zoneName: 'table', cardId: 4, counterId: 1, counterValue: 7 },
|
data: { zoneName: 'table', cardId: 4, counterId: 1, counterValue: 7 },
|
||||||
});
|
});
|
||||||
expect(result.games[1].players[1].zones['table'].cards[0].counterList).toEqual([{ id: 1, value: 7 }]);
|
expect(result.games[1].players[1].zones['table'].cards[0].counterList).toEqual([expect.objectContaining({ id: 1, value: 7 })]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('removes counter from counterList when counterValue ≤ 0', () => {
|
it('removes counter from counterList when counterValue ≤ 0', () => {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,10 @@ import {
|
||||||
PlayerInfo,
|
PlayerInfo,
|
||||||
PlayerProperties,
|
PlayerProperties,
|
||||||
} from 'types';
|
} from 'types';
|
||||||
|
import { create } from '@bufbuild/protobuf';
|
||||||
|
import { ServerInfo_CardSchema } from 'generated/proto/serverinfo_card_pb';
|
||||||
|
import { ServerInfo_CardCounterSchema } from 'generated/proto/serverinfo_cardcounter_pb';
|
||||||
|
import { GameAction } from './game.actions';
|
||||||
import { GameEntry, GameMessage, GamesState, PlayerEntry, ZoneEntry } from './game.interfaces';
|
import { GameEntry, GameMessage, GamesState, PlayerEntry, ZoneEntry } from './game.interfaces';
|
||||||
import { Types } from './game.types';
|
import { Types } from './game.types';
|
||||||
|
|
||||||
|
|
@ -120,7 +124,7 @@ function buildEmptyCard(
|
||||||
faceDown: boolean,
|
faceDown: boolean,
|
||||||
providerId: string
|
providerId: string
|
||||||
): CardInfo {
|
): CardInfo {
|
||||||
return {
|
return create(ServerInfo_CardSchema, {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
x,
|
x,
|
||||||
|
|
@ -138,7 +142,7 @@ function buildEmptyCard(
|
||||||
attachZone: '',
|
attachZone: '',
|
||||||
attachCardId: -1,
|
attachCardId: -1,
|
||||||
providerId,
|
providerId,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Initial state ─────────────────────────────────────────────────────────────
|
// ── Initial state ─────────────────────────────────────────────────────────────
|
||||||
|
|
@ -149,7 +153,7 @@ const initialState: GamesState = {
|
||||||
|
|
||||||
// ── Reducer ───────────────────────────────────────────────────────────────────
|
// ── Reducer ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const gamesReducer = (state: GamesState = initialState, action: any): GamesState => {
|
export const gamesReducer = (state: GamesState = initialState, action: GameAction): GamesState => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case Types.CLEAR_STORE: {
|
case Types.CLEAR_STORE: {
|
||||||
return initialState;
|
return initialState;
|
||||||
|
|
@ -422,7 +426,7 @@ export const gamesReducer = (state: GamesState = initialState, action: any): Gam
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newCard: CardInfo = {
|
const newCard: CardInfo = create(ServerInfo_CardSchema, {
|
||||||
id: cardId,
|
id: cardId,
|
||||||
name: cardName,
|
name: cardName,
|
||||||
x,
|
x,
|
||||||
|
|
@ -440,7 +444,7 @@ export const gamesReducer = (state: GamesState = initialState, action: any): Gam
|
||||||
attachZone: '',
|
attachZone: '',
|
||||||
attachCardId: -1,
|
attachCardId: -1,
|
||||||
providerId: cardProviderId,
|
providerId: cardProviderId,
|
||||||
};
|
});
|
||||||
return updateZone(state, gameId, playerId, zoneName, {
|
return updateZone(state, gameId, playerId, zoneName, {
|
||||||
cards: [...zone.cards, newCard],
|
cards: [...zone.cards, newCard],
|
||||||
cardCount: zone.cardCount + 1,
|
cardCount: zone.cardCount + 1,
|
||||||
|
|
@ -514,7 +518,7 @@ export const gamesReducer = (state: GamesState = initialState, action: any): Gam
|
||||||
newCounterList =
|
newCounterList =
|
||||||
existing >= 0
|
existing >= 0
|
||||||
? card.counterList.map(c => (c.id === counterId ? { ...c, value: counterValue } : c))
|
? card.counterList.map(c => (c.id === counterId ? { ...c, value: counterValue } : c))
|
||||||
: [...card.counterList, { id: counterId, value: counterValue }];
|
: [...card.counterList, create(ServerInfo_CardCounterSchema, { id: counterId, value: counterValue })];
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedCards = [...zone.cards];
|
const updatedCards = [...zone.cards];
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { Selectors } from './game.selectors';
|
import { Selectors } from './game.selectors';
|
||||||
import {
|
import { makeGameEntry, makePlayerEntry, makeState,
|
||||||
makeGameEntry, makePlayerEntry, makePlayerProperties, makeState,
|
|
||||||
makeZoneEntry, makeCard, makeCounter, makeArrow,
|
makeZoneEntry, makeCard, makeCounter, makeArrow,
|
||||||
} from './__mocks__/fixtures';
|
} from './__mocks__/fixtures';
|
||||||
import { GamesState } from './game.interfaces';
|
import { GamesState } from './game.interfaces';
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { CardInfo } from 'types';
|
||||||
import { GamesState, GameEntry, PlayerEntry, ZoneEntry } from './game.interfaces';
|
import { GamesState, GameEntry, PlayerEntry, ZoneEntry } from './game.interfaces';
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
games: GamesState;
|
games: GamesState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const EMPTY_ARRAY: CardInfo[] = [];
|
||||||
|
const EMPTY_OBJECT = {} as Record<string, never>;
|
||||||
|
|
||||||
export const Selectors = {
|
export const Selectors = {
|
||||||
getGames: ({ games }: State): { [gameId: number]: GameEntry } => games.games,
|
getGames: ({ games }: State): { [gameId: number]: GameEntry } => games.games,
|
||||||
|
|
||||||
|
|
@ -41,13 +46,13 @@ export const Selectors = {
|
||||||
): ZoneEntry | undefined => games.games[gameId]?.players[playerId]?.zones[zoneName],
|
): ZoneEntry | undefined => games.games[gameId]?.players[playerId]?.zones[zoneName],
|
||||||
|
|
||||||
getCards: ({ games }: State, gameId: number, playerId: number, zoneName: string) =>
|
getCards: ({ games }: State, gameId: number, playerId: number, zoneName: string) =>
|
||||||
games.games[gameId]?.players[playerId]?.zones[zoneName]?.cards ?? [],
|
games.games[gameId]?.players[playerId]?.zones[zoneName]?.cards ?? EMPTY_ARRAY,
|
||||||
|
|
||||||
getCounters: ({ games }: State, gameId: number, playerId: number) =>
|
getCounters: ({ games }: State, gameId: number, playerId: number) =>
|
||||||
games.games[gameId]?.players[playerId]?.counters ?? {},
|
games.games[gameId]?.players[playerId]?.counters ?? EMPTY_OBJECT,
|
||||||
|
|
||||||
getArrows: ({ games }: State, gameId: number, playerId: number) =>
|
getArrows: ({ games }: State, gameId: number, playerId: number) =>
|
||||||
games.games[gameId]?.players[playerId]?.arrows ?? {},
|
games.games[gameId]?.players[playerId]?.arrows ?? EMPTY_OBJECT,
|
||||||
|
|
||||||
getActivePlayerId: ({ games }: State, gameId: number): number | undefined =>
|
getActivePlayerId: ({ games }: State, gameId: number): number | undefined =>
|
||||||
games.games[gameId]?.activePlayerId,
|
games.games[gameId]?.activePlayerId,
|
||||||
|
|
@ -65,8 +70,10 @@ export const Selectors = {
|
||||||
games.games[gameId]?.reversed ?? false,
|
games.games[gameId]?.reversed ?? false,
|
||||||
|
|
||||||
getMessages: ({ games }: State, gameId: number) =>
|
getMessages: ({ games }: State, gameId: number) =>
|
||||||
games.games[gameId]?.messages ?? [],
|
games.games[gameId]?.messages ?? EMPTY_ARRAY,
|
||||||
|
|
||||||
getActiveGameIds: ({ games }: State): number[] =>
|
getActiveGameIds: createSelector(
|
||||||
Object.keys(games.games).map(Number),
|
[({ games }: State) => games.games],
|
||||||
|
(games) => Object.keys(games).map(Number)
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -31,4 +31,4 @@ export const Types = {
|
||||||
ZONE_DUMPED: '[Games] Zone Dumped',
|
ZONE_DUMPED: '[Games] Zone Dumped',
|
||||||
ZONE_PROPERTIES_CHANGED: '[Games] Zone Properties Changed',
|
ZONE_PROPERTIES_CHANGED: '[Games] Zone Properties Changed',
|
||||||
GAME_SAY: '[Games] Game Say',
|
GAME_SAY: '[Games] Game Say',
|
||||||
};
|
} as const;
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,63 @@
|
||||||
import {
|
import {
|
||||||
Game,
|
Game,
|
||||||
GameSortField,
|
GameSortField,
|
||||||
|
Message,
|
||||||
|
ProtoInit,
|
||||||
Room,
|
Room,
|
||||||
SortDirection,
|
SortDirection,
|
||||||
User,
|
User,
|
||||||
UserSortField,
|
UserSortField,
|
||||||
} from 'types';
|
} from 'types';
|
||||||
import { Message, RoomsState } from '../rooms.interfaces';
|
import { create } from '@bufbuild/protobuf';
|
||||||
|
import { ServerInfo_UserSchema } from 'generated/proto/serverinfo_user_pb';
|
||||||
|
import { ServerInfo_GameSchema } from 'generated/proto/serverinfo_game_pb';
|
||||||
|
import { ServerInfo_RoomSchema } from 'generated/proto/serverinfo_room_pb';
|
||||||
|
import { RoomsState } from '../rooms.interfaces';
|
||||||
|
|
||||||
export function makeUser(overrides: Partial<User> = {}): User {
|
export function makeUser(overrides: ProtoInit<User> = {}): User {
|
||||||
return {
|
return create(ServerInfo_UserSchema, {
|
||||||
name: 'TestUser',
|
name: 'TestUser',
|
||||||
accountageSecs: 0n,
|
accountageSecs: 0n,
|
||||||
privlevel: '',
|
privlevel: '',
|
||||||
userLevel: 0,
|
userLevel: 0,
|
||||||
...overrides,
|
...overrides,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeRoom(overrides: ProtoInit<Room> = {}): Room {
|
||||||
|
const { gametypeMap = {}, order = 0, gameList = [], ...protoOverrides } = overrides;
|
||||||
|
return {
|
||||||
|
...create(ServerInfo_RoomSchema, {
|
||||||
|
roomId: 1,
|
||||||
|
name: 'Test Room',
|
||||||
|
description: '',
|
||||||
|
gameCount: 0,
|
||||||
|
gameList: [],
|
||||||
|
gametypeList: [],
|
||||||
|
autoJoin: false,
|
||||||
|
playerCount: 0,
|
||||||
|
userList: [],
|
||||||
|
...protoOverrides,
|
||||||
|
}),
|
||||||
|
gameList,
|
||||||
|
gametypeMap,
|
||||||
|
order,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeRoom(overrides: Partial<Room> = {}): Room {
|
export function makeGame(overrides: ProtoInit<Game & { startTime: number }> = {}): Game & { startTime: number } {
|
||||||
|
const { gameType = '', startTime = 0, ...protoOverrides } = overrides;
|
||||||
return {
|
return {
|
||||||
roomId: 1,
|
...create(ServerInfo_GameSchema, {
|
||||||
name: 'Test Room',
|
gameId: 1,
|
||||||
description: '',
|
roomId: 1,
|
||||||
gameCount: 0,
|
description: 'Test Game',
|
||||||
gameList: [],
|
gameTypes: [],
|
||||||
gametypeList: [],
|
started: false,
|
||||||
gametypeMap: {},
|
...protoOverrides,
|
||||||
autoJoin: false,
|
}),
|
||||||
permissionlevel: 0 as any,
|
gameType,
|
||||||
playerCount: 0,
|
startTime,
|
||||||
privilegelevel: 0 as any,
|
|
||||||
userList: [],
|
|
||||||
order: 0,
|
|
||||||
...overrides,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function makeGame(overrides: Partial<Game & { startTime: number }> = {}): Game & { startTime: number } {
|
|
||||||
return {
|
|
||||||
gameId: 1,
|
|
||||||
roomId: 1,
|
|
||||||
description: 'Test Game',
|
|
||||||
gameType: '',
|
|
||||||
gameTypes: [],
|
|
||||||
started: false,
|
|
||||||
startTime: 0,
|
|
||||||
...overrides,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,71 +1,77 @@
|
||||||
|
import { GameSortField, Message, SortDirection, User } from 'types';
|
||||||
|
import type { ServerInfo_Room } from 'generated/proto/serverinfo_room_pb';
|
||||||
|
import type { ServerInfo_Game } from 'generated/proto/serverinfo_game_pb';
|
||||||
|
|
||||||
import { Types } from './rooms.types';
|
import { Types } from './rooms.types';
|
||||||
|
|
||||||
export const Actions = {
|
export const Actions = {
|
||||||
clearStore: () => ({
|
clearStore: () => ({
|
||||||
type: Types.CLEAR_STORE
|
type: Types.CLEAR_STORE,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
updateRooms: rooms => ({
|
updateRooms: (rooms: ServerInfo_Room[]) => ({
|
||||||
type: Types.UPDATE_ROOMS,
|
type: Types.UPDATE_ROOMS,
|
||||||
rooms
|
rooms,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
joinRoom: roomInfo => ({
|
joinRoom: (roomInfo: ServerInfo_Room) => ({
|
||||||
type: Types.JOIN_ROOM,
|
type: Types.JOIN_ROOM,
|
||||||
roomInfo
|
roomInfo,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
leaveRoom: roomId => ({
|
leaveRoom: (roomId: number) => ({
|
||||||
type: Types.LEAVE_ROOM,
|
type: Types.LEAVE_ROOM,
|
||||||
roomId
|
roomId,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
addMessage: (roomId, message) => ({
|
addMessage: (roomId: number, message: Message) => ({
|
||||||
type: Types.ADD_MESSAGE,
|
type: Types.ADD_MESSAGE,
|
||||||
roomId,
|
roomId,
|
||||||
message
|
message,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
updateGames: (roomId, games) => ({
|
updateGames: (roomId: number, games: ServerInfo_Game[]) => ({
|
||||||
type: Types.UPDATE_GAMES,
|
type: Types.UPDATE_GAMES,
|
||||||
roomId,
|
roomId,
|
||||||
games
|
games,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
userJoined: (roomId, user) => ({
|
userJoined: (roomId: number, user: User) => ({
|
||||||
type: Types.USER_JOINED,
|
type: Types.USER_JOINED,
|
||||||
roomId,
|
roomId,
|
||||||
user
|
user,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
userLeft: (roomId, name) => ({
|
userLeft: (roomId: number, name: string) => ({
|
||||||
type: Types.USER_LEFT,
|
type: Types.USER_LEFT,
|
||||||
roomId,
|
roomId,
|
||||||
name
|
name,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
sortGames: (roomId, field, order) => ({
|
sortGames: (roomId: number, field: GameSortField, order: SortDirection) => ({
|
||||||
type: Types.SORT_GAMES,
|
type: Types.SORT_GAMES,
|
||||||
roomId,
|
roomId,
|
||||||
field,
|
field,
|
||||||
order
|
order,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
removeMessages: (roomId, name, amount) => ({
|
removeMessages: (roomId: number, name: string, amount: number) => ({
|
||||||
type: Types.REMOVE_MESSAGES,
|
type: Types.REMOVE_MESSAGES,
|
||||||
roomId,
|
roomId,
|
||||||
name,
|
name,
|
||||||
amount
|
amount,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
gameCreated: (roomId) => ({
|
gameCreated: (roomId: number) => ({
|
||||||
type: Types.GAME_CREATED,
|
type: Types.GAME_CREATED,
|
||||||
roomId
|
roomId,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
joinedGame: (roomId, gameId) => ({
|
joinedGame: (roomId: number, gameId: number) => ({
|
||||||
type: Types.JOINED_GAME,
|
type: Types.JOINED_GAME,
|
||||||
roomId,
|
roomId,
|
||||||
gameId
|
gameId,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RoomsAction = ReturnType<typeof Actions[keyof typeof Actions]>;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
vi.mock('store/store', () => ({ store: { dispatch: vi.fn() } }));
|
vi.mock('store', () => ({ store: { dispatch: vi.fn() } }));
|
||||||
vi.mock('redux-form', () => ({
|
|
||||||
reset: vi.fn((form) => ({ type: '@@redux-form/RESET', meta: { form } })),
|
|
||||||
}));
|
|
||||||
|
|
||||||
import { store } from 'store/store';
|
import { store } from 'store';
|
||||||
import { reset } from 'redux-form';
|
|
||||||
import { Actions } from './rooms.actions';
|
import { Actions } from './rooms.actions';
|
||||||
import { Dispatch } from './rooms.dispatch';
|
import { Dispatch } from './rooms.dispatch';
|
||||||
import { makeGame, makeMessage, makeRoom, makeUser } from './__mocks__/rooms-fixtures';
|
import { makeGame, makeMessage, makeRoom, makeUser } from './__mocks__/rooms-fixtures';
|
||||||
|
|
@ -42,11 +38,11 @@ describe('Dispatch', () => {
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.addMessage(1, message));
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.addMessage(1, message));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('addMessage with message.name truthy → dispatches reset("sayMessage") then Actions.addMessage()', () => {
|
it('addMessage with message.name truthy → dispatches Actions.addMessage()', () => {
|
||||||
const message = { ...makeMessage(), name: 'Alice' };
|
const message = { ...makeMessage(), name: 'Alice' };
|
||||||
Dispatch.addMessage(1, message);
|
Dispatch.addMessage(1, message);
|
||||||
expect(store.dispatch).toHaveBeenNthCalledWith(1, (reset as vi.Mock)('sayMessage'));
|
expect(store.dispatch).toHaveBeenCalledTimes(1);
|
||||||
expect(store.dispatch).toHaveBeenNthCalledWith(2, Actions.addMessage(1, message));
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.addMessage(1, message));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updateGames dispatches Actions.updateGames()', () => {
|
it('updateGames dispatches Actions.updateGames()', () => {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
import { reset } from 'redux-form';
|
import { GameSortField, Message, SortDirection, User } from 'types';
|
||||||
|
import type { ServerInfo_Room } from 'generated/proto/serverinfo_room_pb';
|
||||||
|
import type { ServerInfo_Game } from 'generated/proto/serverinfo_game_pb';
|
||||||
|
|
||||||
import { Actions } from './rooms.actions';
|
import { Actions } from './rooms.actions';
|
||||||
import { store } from 'store';
|
import { store } from 'store';
|
||||||
|
|
||||||
|
|
@ -7,52 +10,48 @@ export const Dispatch = {
|
||||||
store.dispatch(Actions.clearStore());
|
store.dispatch(Actions.clearStore());
|
||||||
},
|
},
|
||||||
|
|
||||||
updateRooms: rooms => {
|
updateRooms: (rooms: ServerInfo_Room[]) => {
|
||||||
store.dispatch(Actions.updateRooms(rooms));
|
store.dispatch(Actions.updateRooms(rooms));
|
||||||
},
|
},
|
||||||
|
|
||||||
joinRoom: roomInfo => {
|
joinRoom: (roomInfo: ServerInfo_Room) => {
|
||||||
store.dispatch(Actions.joinRoom(roomInfo));
|
store.dispatch(Actions.joinRoom(roomInfo));
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
leaveRoom: roomId => {
|
leaveRoom: (roomId: number) => {
|
||||||
store.dispatch(Actions.leaveRoom(roomId));
|
store.dispatch(Actions.leaveRoom(roomId));
|
||||||
},
|
},
|
||||||
|
|
||||||
addMessage: (roomId, message) => {
|
addMessage: (roomId: number, message: Message) => {
|
||||||
if (message.name) {
|
|
||||||
store.dispatch(reset('sayMessage'));
|
|
||||||
}
|
|
||||||
|
|
||||||
store.dispatch(Actions.addMessage(roomId, message));
|
store.dispatch(Actions.addMessage(roomId, message));
|
||||||
},
|
},
|
||||||
|
|
||||||
updateGames: (roomId, games) => {
|
updateGames: (roomId: number, games: ServerInfo_Game[]) => {
|
||||||
store.dispatch(Actions.updateGames(roomId, games));
|
store.dispatch(Actions.updateGames(roomId, games));
|
||||||
},
|
},
|
||||||
|
|
||||||
userJoined: (roomId, user) => {
|
userJoined: (roomId: number, user: User) => {
|
||||||
store.dispatch(Actions.userJoined(roomId, user));
|
store.dispatch(Actions.userJoined(roomId, user));
|
||||||
},
|
},
|
||||||
|
|
||||||
userLeft: (roomId, name) => {
|
userLeft: (roomId: number, name: string) => {
|
||||||
store.dispatch(Actions.userLeft(roomId, name));
|
store.dispatch(Actions.userLeft(roomId, name));
|
||||||
},
|
},
|
||||||
|
|
||||||
sortGames: (roomId, field, order) => {
|
sortGames: (roomId: number, field: GameSortField, order: SortDirection) => {
|
||||||
store.dispatch(Actions.sortGames(roomId, field, order));
|
store.dispatch(Actions.sortGames(roomId, field, order));
|
||||||
},
|
},
|
||||||
|
|
||||||
removeMessages: (roomId, name, amount) => {
|
removeMessages: (roomId: number, name: string, amount: number) => {
|
||||||
store.dispatch(Actions.removeMessages(roomId, name, amount));
|
store.dispatch(Actions.removeMessages(roomId, name, amount));
|
||||||
},
|
},
|
||||||
|
|
||||||
gameCreated: (roomId) => {
|
gameCreated: (roomId: number) => {
|
||||||
store.dispatch(Actions.gameCreated(roomId));
|
store.dispatch(Actions.gameCreated(roomId));
|
||||||
},
|
},
|
||||||
|
|
||||||
joinedGame: (roomId, gameId) => {
|
joinedGame: (roomId: number, gameId: number) => {
|
||||||
store.dispatch(Actions.joinedGame(roomId, gameId));
|
store.dispatch(Actions.joinedGame(roomId, gameId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { GameSortField, Room, Game, SortBy, UserSortField } from 'types';
|
import { GameSortField, Message, Room, Game, SortBy, UserSortField } from 'types';
|
||||||
|
|
||||||
export interface RoomsState {
|
export interface RoomsState {
|
||||||
rooms: RoomsStateRooms;
|
rooms: RoomsStateRooms;
|
||||||
|
|
@ -41,10 +41,3 @@ export interface RoomsStateSortGamesBy extends SortBy {
|
||||||
export interface RoomsStateSortUsersBy extends SortBy {
|
export interface RoomsStateSortUsersBy extends SortBy {
|
||||||
field: UserSortField
|
field: UserSortField
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Message {
|
|
||||||
message: string;
|
|
||||||
messageType: number;
|
|
||||||
timeReceived: number;
|
|
||||||
timeOf?: number;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,20 @@ describe('ADD_MESSAGE', () => {
|
||||||
expect(result.messages[1][0].message).not.toBe('first');
|
expect(result.messages[1][0].message).not.toBe('first');
|
||||||
expect(result.messages[1][MAX_ROOM_MESSAGES - 1].message).toBe('new');
|
expect(result.messages[1][MAX_ROOM_MESSAGES - 1].message).toBe('new');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('prepends "name: " to message when name is present', () => {
|
||||||
|
const state = makeRoomsState({ messages: { 1: [] } });
|
||||||
|
const message = makeMessage({ name: 'Alice', message: 'hello' });
|
||||||
|
const result = roomsReducer(state, { type: Types.ADD_MESSAGE, roomId: 1, message });
|
||||||
|
expect(result.messages[1][0].message).toBe('Alice: hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not prepend when name is empty', () => {
|
||||||
|
const state = makeRoomsState({ messages: { 1: [] } });
|
||||||
|
const message = makeMessage({ name: '', message: 'system msg' });
|
||||||
|
const result = roomsReducer(state, { type: Types.ADD_MESSAGE, roomId: 1, message });
|
||||||
|
expect(result.messages[1][0].message).toBe('system msg');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── UPDATE_GAMES ──────────────────────────────────────────────────────────────
|
// ── UPDATE_GAMES ──────────────────────────────────────────────────────────────
|
||||||
|
|
@ -267,6 +281,16 @@ describe('REMOVE_MESSAGES', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── GAME_CREATED ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('GAME_CREATED', () => {
|
||||||
|
it('returns state unchanged', () => {
|
||||||
|
const state = makeRoomsState();
|
||||||
|
const result = roomsReducer(state, { type: Types.GAME_CREATED, roomId: 1 });
|
||||||
|
expect(result).toBe(state);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ── JOINED_GAME ───────────────────────────────────────────────────────────────
|
// ── JOINED_GAME ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
describe('JOINED_GAME', () => {
|
describe('JOINED_GAME', () => {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
import { GameSortField, UserSortField, SortDirection } from 'types';
|
import { GameSortField, Room, UserSortField, SortDirection } from 'types';
|
||||||
|
|
||||||
import { SortUtil } from '../common';
|
import { normalizeGameObject, normalizeGametypeMap, normalizeRoomInfo, normalizeUserMessage, SortUtil } from '../common';
|
||||||
|
|
||||||
|
import { RoomsAction } from './rooms.actions';
|
||||||
import { RoomsState } from './rooms.interfaces'
|
import { RoomsState } from './rooms.interfaces'
|
||||||
import { MAX_ROOM_MESSAGES, Types } from './rooms.types';
|
import { MAX_ROOM_MESSAGES, Types } from './rooms.types';
|
||||||
|
|
||||||
|
|
@ -23,7 +24,7 @@ const initialState: RoomsState = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const roomsReducer = (state = initialState, action: any) => {
|
export const roomsReducer = (state = initialState, action: RoomsAction) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case Types.CLEAR_STORE: {
|
case Types.CLEAR_STORE: {
|
||||||
return {
|
return {
|
||||||
|
|
@ -36,20 +37,21 @@ export const roomsReducer = (state = initialState, action: any) => {
|
||||||
...state.rooms
|
...state.rooms
|
||||||
};
|
};
|
||||||
|
|
||||||
// Server does not send everything on updates
|
// Server does not send everything on updates — preserve existing gameList/userList
|
||||||
_.each(action.rooms, (room, order) => {
|
_.each(action.rooms, (rawRoom, order) => {
|
||||||
const { roomId } = room;
|
const { gameList: _g, gametypeList, userList: _u, ...roomMeta } = rawRoom;
|
||||||
|
const { roomId } = roomMeta;
|
||||||
const existing = rooms[roomId] || {};
|
const existing = rooms[roomId] || {};
|
||||||
|
|
||||||
const update = { ...room };
|
const gametypeMap = normalizeGametypeMap(gametypeList);
|
||||||
delete update.gameList;
|
|
||||||
delete update.gametypeList;
|
|
||||||
delete update.userList;
|
|
||||||
|
|
||||||
rooms[roomId] = {
|
rooms[roomId] = {
|
||||||
...existing,
|
...(existing as Room),
|
||||||
...update,
|
...roomMeta,
|
||||||
order
|
gametypeMap,
|
||||||
|
gameList: (existing as Room).gameList,
|
||||||
|
userList: (existing as Room).userList,
|
||||||
|
order,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -57,9 +59,10 @@ export const roomsReducer = (state = initialState, action: any) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
case Types.JOIN_ROOM: {
|
case Types.JOIN_ROOM: {
|
||||||
const { roomInfo } = action;
|
const { roomInfo: rawRoomInfo } = action;
|
||||||
const { joinedRoomIds, rooms, sortGamesBy, sortUsersBy } = state;
|
const { joinedRoomIds, rooms, sortGamesBy, sortUsersBy } = state;
|
||||||
|
|
||||||
|
const roomInfo = normalizeRoomInfo(rawRoomInfo);
|
||||||
const { roomId } = roomInfo;
|
const { roomId } = roomInfo;
|
||||||
|
|
||||||
const gameList = [
|
const gameList = [
|
||||||
|
|
@ -125,8 +128,8 @@ export const roomsReducer = (state = initialState, action: any) => {
|
||||||
roomMessages.shift();
|
roomMessages.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
message.timeReceived = new Date().getTime();
|
const normalized = normalizeUserMessage({ ...message, timeReceived: Date.now() });
|
||||||
roomMessages.push(message);
|
roomMessages.push(normalized);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|
@ -150,8 +153,12 @@ export const roomsReducer = (state = initialState, action: any) => {
|
||||||
return { ...state };
|
return { ...state };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normalize incoming raw proto games using the room's gametypeMap
|
||||||
|
const gametypeMap = room.gametypeMap ?? {};
|
||||||
|
const normalizedGames = games.map(g => normalizeGameObject(g, gametypeMap));
|
||||||
|
|
||||||
// Create map of games with update objects
|
// Create map of games with update objects
|
||||||
const toUpdate = games.reduce((map, game) => {
|
const toUpdate = normalizedGames.reduce((map, game) => {
|
||||||
map[game.gameId] = game;
|
map[game.gameId] = game;
|
||||||
return map;
|
return map;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
@ -320,6 +327,10 @@ export const roomsReducer = (state = initialState, action: any) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Signal-only — no state mutation needed; explicit for discriminated-union exhaustiveness
|
||||||
|
case Types.GAME_CREATED:
|
||||||
|
return state;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,16 +92,14 @@ describe('Selectors', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getRoomGames → returns gameList for roomId', () => {
|
it('getRoomGames → returns gameList for roomId', () => {
|
||||||
const games = [makeGame()];
|
const room = makeRoom({ roomId: 1, gameList: [makeGame()] });
|
||||||
const room = makeRoom({ roomId: 1, gameList: games });
|
|
||||||
const state = makeRoomsState({ rooms: { 1: room } });
|
const state = makeRoomsState({ rooms: { 1: room } });
|
||||||
expect(Selectors.getRoomGames(rootState(state), 1)).toBe(games);
|
expect(Selectors.getRoomGames(rootState(state), 1)).toBe(room.gameList);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getRoomUsers → returns userList for roomId', () => {
|
it('getRoomUsers → returns userList for roomId', () => {
|
||||||
const users = [makeUser()];
|
const room = makeRoom({ roomId: 1, userList: [makeUser()] });
|
||||||
const room = makeRoom({ roomId: 1, userList: users });
|
|
||||||
const state = makeRoomsState({ rooms: { 1: room } });
|
const state = makeRoomsState({ rooms: { 1: room } });
|
||||||
expect(Selectors.getRoomUsers(rootState(state), 1)).toBe(users);
|
expect(Selectors.getRoomUsers(rootState(state), 1)).toBe(room.userList);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { RoomsState } from './rooms.interfaces';
|
import { RoomsState } from './rooms.interfaces';
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
|
@ -16,15 +17,15 @@ export const Selectors = {
|
||||||
getSortGamesBy: ({ rooms: { sortGamesBy } }: State) => sortGamesBy,
|
getSortGamesBy: ({ rooms: { sortGamesBy } }: State) => sortGamesBy,
|
||||||
getSortUsersBy: ({ rooms: { sortUsersBy } }: State) => sortUsersBy,
|
getSortUsersBy: ({ rooms: { sortUsersBy } }: State) => sortUsersBy,
|
||||||
|
|
||||||
getJoinedRooms: (state: State) => {
|
getJoinedRooms: createSelector(
|
||||||
const joined = Selectors.getJoinedRoomIds(state);
|
[(state: State) => state.rooms.rooms, (state: State) => state.rooms.joinedRoomIds],
|
||||||
return _.filter(Selectors.getRooms(state), room => joined[room.roomId]);
|
(rooms, joined) => _.filter(rooms, room => joined[room.roomId])
|
||||||
},
|
),
|
||||||
|
|
||||||
getJoinedGames: (state: State, roomId: number) => {
|
getJoinedGames: createSelector(
|
||||||
const joined = Selectors.getJoinedGameIds(state)[roomId];
|
[(state: State, roomId: number) => state.rooms.games[roomId], (state: State, roomId: number) => state.rooms.joinedGameIds[roomId]],
|
||||||
return _.filter(Selectors.getGames(state)[roomId], game => joined[game.gameId]);
|
(games, joined) => _.filter(games, game => joined[game.gameId])
|
||||||
},
|
),
|
||||||
|
|
||||||
getRoomMessages: (state: State, roomId: number) => Selectors.getMessages(state)[roomId],
|
getRoomMessages: (state: State, roomId: number) => Selectors.getMessages(state)[roomId],
|
||||||
getRoomGames: (state: State, roomId: number) => Selectors.getRooms(state)[roomId].gameList,
|
getRoomGames: (state: State, roomId: number) => Selectors.getRooms(state)[roomId].gameList,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,6 @@ export const Types = {
|
||||||
REMOVE_MESSAGES: '[Rooms] Remove Messages',
|
REMOVE_MESSAGES: '[Rooms] Remove Messages',
|
||||||
GAME_CREATED: '[Rooms] Game Created',
|
GAME_CREATED: '[Rooms] Game Created',
|
||||||
JOINED_GAME: '[Rooms] Joined Game',
|
JOINED_GAME: '[Rooms] Joined Game',
|
||||||
};
|
} as const;
|
||||||
|
|
||||||
export const MAX_ROOM_MESSAGES = 1000;
|
export const MAX_ROOM_MESSAGES = 1000;
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,13 @@
|
||||||
import { combineReducers } from 'redux';
|
import { combineReducers } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
import { gamesReducer } from './game';
|
import { gamesReducer } from './game';
|
||||||
import { roomsReducer } from './rooms';
|
import { roomsReducer } from './rooms';
|
||||||
import { serverReducer } from './server';
|
import { serverReducer } from './server';
|
||||||
import { reducer as formReducer } from 'redux-form'
|
import { actionReducer } from './actions';
|
||||||
import { actionReducer } from './actions'
|
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
games: gamesReducer,
|
games: gamesReducer,
|
||||||
rooms: roomsReducer,
|
rooms: roomsReducer,
|
||||||
server: serverReducer,
|
server: serverReducer,
|
||||||
|
|
||||||
form: formReducer,
|
|
||||||
action: actionReducer
|
action: actionReducer
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@ import {
|
||||||
BanHistoryItem,
|
BanHistoryItem,
|
||||||
DeckList,
|
DeckList,
|
||||||
DeckStorageTreeItem,
|
DeckStorageTreeItem,
|
||||||
|
Game,
|
||||||
LogItem,
|
LogItem,
|
||||||
|
ProtoInit,
|
||||||
ReplayMatch,
|
ReplayMatch,
|
||||||
SortDirection,
|
SortDirection,
|
||||||
StatusEnum,
|
StatusEnum,
|
||||||
|
|
@ -12,20 +14,30 @@ import {
|
||||||
WarnHistoryItem,
|
WarnHistoryItem,
|
||||||
WarnListItem,
|
WarnListItem,
|
||||||
} from 'types';
|
} from 'types';
|
||||||
|
import { create } from '@bufbuild/protobuf';
|
||||||
|
import { ServerInfo_GameSchema } from 'generated/proto/serverinfo_game_pb';
|
||||||
|
import { ServerInfo_UserSchema } from 'generated/proto/serverinfo_user_pb';
|
||||||
|
import { ServerInfo_ReplayMatchSchema } from 'generated/proto/serverinfo_replay_match_pb';
|
||||||
|
import { ServerInfo_ChatMessageSchema } from 'generated/proto/serverinfo_chat_message_pb';
|
||||||
|
import { ServerInfo_BanSchema } from 'generated/proto/serverinfo_ban_pb';
|
||||||
|
import { ServerInfo_WarningSchema } from 'generated/proto/serverinfo_warning_pb';
|
||||||
|
import { Response_WarnListSchema } from 'generated/proto/response_warn_list_pb';
|
||||||
|
import { ServerInfo_DeckStorage_TreeItemSchema, ServerInfo_DeckStorage_FolderSchema } from 'generated/proto/serverinfo_deckstorage_pb';
|
||||||
|
import { Response_DeckListSchema } from 'generated/proto/response_deck_list_pb';
|
||||||
import { ServerState } from '../server.interfaces';
|
import { ServerState } from '../server.interfaces';
|
||||||
|
|
||||||
export function makeUser(overrides: Partial<User> = {}): User {
|
export function makeUser(overrides: ProtoInit<User> = {}): User {
|
||||||
return {
|
return create(ServerInfo_UserSchema, {
|
||||||
name: 'TestUser',
|
name: 'TestUser',
|
||||||
accountageSecs: 0n,
|
accountageSecs: 0n,
|
||||||
privlevel: '',
|
privlevel: '',
|
||||||
userLevel: 0,
|
userLevel: 0,
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeLogItem(overrides: Partial<LogItem> = {}): LogItem {
|
export function makeLogItem(overrides: ProtoInit<LogItem> = {}): LogItem {
|
||||||
return {
|
return create(ServerInfo_ChatMessageSchema, {
|
||||||
message: '',
|
message: '',
|
||||||
senderId: '',
|
senderId: '',
|
||||||
senderIp: '',
|
senderIp: '',
|
||||||
|
|
@ -35,11 +47,11 @@ export function makeLogItem(overrides: Partial<LogItem> = {}): LogItem {
|
||||||
targetType: '',
|
targetType: '',
|
||||||
time: '',
|
time: '',
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeBanHistoryItem(overrides: Partial<BanHistoryItem> = {}): BanHistoryItem {
|
export function makeBanHistoryItem(overrides: ProtoInit<BanHistoryItem> = {}): BanHistoryItem {
|
||||||
return {
|
return create(ServerInfo_BanSchema, {
|
||||||
adminId: '',
|
adminId: '',
|
||||||
adminName: '',
|
adminName: '',
|
||||||
banTime: '',
|
banTime: '',
|
||||||
|
|
@ -47,47 +59,45 @@ export function makeBanHistoryItem(overrides: Partial<BanHistoryItem> = {}): Ban
|
||||||
banReason: '',
|
banReason: '',
|
||||||
visibleReason: '',
|
visibleReason: '',
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeWarnHistoryItem(overrides: Partial<WarnHistoryItem> = {}): WarnHistoryItem {
|
export function makeWarnHistoryItem(overrides: ProtoInit<WarnHistoryItem> = {}): WarnHistoryItem {
|
||||||
return {
|
return create(ServerInfo_WarningSchema, {
|
||||||
userName: '',
|
userName: '',
|
||||||
adminName: '',
|
adminName: '',
|
||||||
reason: '',
|
reason: '',
|
||||||
timeOf: '',
|
timeOf: '',
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeWarnListItem(overrides: Partial<WarnListItem> = {}): WarnListItem {
|
export function makeWarnListItem(overrides: ProtoInit<WarnListItem> = {}): WarnListItem {
|
||||||
return {
|
return create(Response_WarnListSchema, {
|
||||||
warning: '',
|
warning: [],
|
||||||
userName: '',
|
userName: '',
|
||||||
userClientid: '',
|
userClientid: '',
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeDeckTreeItem(overrides: Partial<DeckStorageTreeItem> = {}): DeckStorageTreeItem {
|
export function makeDeckTreeItem(overrides: ProtoInit<DeckStorageTreeItem> = {}): DeckStorageTreeItem {
|
||||||
return {
|
return create(ServerInfo_DeckStorage_TreeItemSchema, {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'item',
|
name: 'item',
|
||||||
file: { creationTime: 0 },
|
|
||||||
folder: null,
|
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeDeckList(overrides: Partial<DeckList> = {}): DeckList {
|
export function makeDeckList(overrides: ProtoInit<DeckList> = {}): DeckList {
|
||||||
return {
|
return create(Response_DeckListSchema, {
|
||||||
root: { items: [] },
|
root: create(ServerInfo_DeckStorage_FolderSchema, { items: [] }),
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeReplayMatch(overrides: Partial<ReplayMatch> = {}): ReplayMatch {
|
export function makeReplayMatch(overrides: ProtoInit<ReplayMatch> = {}): ReplayMatch {
|
||||||
return {
|
return create(ServerInfo_ReplayMatchSchema, {
|
||||||
gameId: 1,
|
gameId: 1,
|
||||||
roomName: 'Test Room',
|
roomName: 'Test Room',
|
||||||
timeStarted: 0,
|
timeStarted: 0,
|
||||||
|
|
@ -97,7 +107,11 @@ export function makeReplayMatch(overrides: Partial<ReplayMatch> = {}): ReplayMat
|
||||||
doNotHide: false,
|
doNotHide: false,
|
||||||
replayList: [],
|
replayList: [],
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeGame(overrides: Partial<Game> = {}): Game {
|
||||||
|
return { ...create(ServerInfo_GameSchema, { description: '' }), gameType: '', ...overrides };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeConnectOptions(overrides: Partial<WebSocketConnectOptions> = {}): WebSocketConnectOptions {
|
export function makeConnectOptions(overrides: Partial<WebSocketConnectOptions> = {}): WebSocketConnectOptions {
|
||||||
|
|
@ -148,6 +162,7 @@ export function makeServerState(overrides: Partial<ServerState> = {}): ServerSta
|
||||||
replays: [],
|
replays: [],
|
||||||
backendDecks: null,
|
backendDecks: null,
|
||||||
gamesOfUser: {},
|
gamesOfUser: {},
|
||||||
|
registrationError: null,
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
import { Actions } from './server.actions';
|
import { Actions } from './server.actions';
|
||||||
import { Types } from './server.types';
|
import { Types } from './server.types';
|
||||||
|
import { create } from '@bufbuild/protobuf';
|
||||||
|
import { Event_NotifyUserSchema } from 'generated/proto/event_notify_user_pb';
|
||||||
|
import { Event_ServerShutdownSchema } from 'generated/proto/event_server_shutdown_pb';
|
||||||
|
import { Event_UserMessageSchema } from 'generated/proto/event_user_message_pb';
|
||||||
import {
|
import {
|
||||||
makeBanHistoryItem,
|
makeBanHistoryItem,
|
||||||
makeConnectOptions,
|
makeConnectOptions,
|
||||||
makeDeckList,
|
makeDeckList,
|
||||||
makeDeckTreeItem,
|
makeDeckTreeItem,
|
||||||
makeReplayMatch,
|
makeReplayMatch,
|
||||||
|
makeGame,
|
||||||
makeUser,
|
makeUser,
|
||||||
makeWarnHistoryItem,
|
makeWarnHistoryItem,
|
||||||
makeWarnListItem,
|
makeWarnListItem,
|
||||||
|
|
@ -107,7 +112,7 @@ describe('Actions', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('viewLogs', () => {
|
it('viewLogs', () => {
|
||||||
const logs = { room: [], game: [], chat: [] };
|
const logs = [{ targetType: 'room' }] as any[];
|
||||||
expect(Actions.viewLogs(logs)).toEqual({ type: Types.VIEW_LOGS, logs });
|
expect(Actions.viewLogs(logs)).toEqual({ type: Types.VIEW_LOGS, logs });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -124,7 +129,11 @@ describe('Actions', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('registrationFailed', () => {
|
it('registrationFailed', () => {
|
||||||
expect(Actions.registrationFailed('err')).toEqual({ type: Types.REGISTRATION_FAILED, error: 'err' });
|
expect(Actions.registrationFailed('err', 999)).toEqual({ type: Types.REGISTRATION_FAILED, reason: 'err', endTime: 999 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('registrationFailed without endTime', () => {
|
||||||
|
expect(Actions.registrationFailed('err')).toEqual({ type: Types.REGISTRATION_FAILED, reason: 'err', endTime: undefined });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('registrationEmailError', () => {
|
it('registrationEmailError', () => {
|
||||||
|
|
@ -209,17 +218,17 @@ describe('Actions', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('notifyUser', () => {
|
it('notifyUser', () => {
|
||||||
const notification = { type: 1, warningReason: '', customTitle: '', customContent: '' };
|
const notification = create(Event_NotifyUserSchema, { type: 1, warningReason: '', customTitle: '', customContent: '' });
|
||||||
expect(Actions.notifyUser(notification)).toEqual({ type: Types.NOTIFY_USER, notification });
|
expect(Actions.notifyUser(notification)).toEqual({ type: Types.NOTIFY_USER, notification });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('serverShutdown', () => {
|
it('serverShutdown', () => {
|
||||||
const data = { reason: 'maintenance', minutes: 5 };
|
const data = create(Event_ServerShutdownSchema, { reason: 'maintenance', minutes: 5 });
|
||||||
expect(Actions.serverShutdown(data)).toEqual({ type: Types.SERVER_SHUTDOWN, data });
|
expect(Actions.serverShutdown(data)).toEqual({ type: Types.SERVER_SHUTDOWN, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('userMessage', () => {
|
it('userMessage', () => {
|
||||||
const messageData = { senderName: 'Alice', receiverName: 'Bob', message: 'hey' };
|
const messageData = create(Event_UserMessageSchema, { senderName: 'Alice', receiverName: 'Bob', message: 'hey' });
|
||||||
expect(Actions.userMessage(messageData)).toEqual({ type: Types.USER_MESSAGE, messageData });
|
expect(Actions.userMessage(messageData)).toEqual({ type: Types.USER_MESSAGE, messageData });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -347,7 +356,8 @@ describe('Actions', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('gamesOfUser', () => {
|
it('gamesOfUser', () => {
|
||||||
const games = [{ gameId: 1 }] as any;
|
const games = [makeGame({ gameId: 1 })];
|
||||||
expect(Actions.gamesOfUser('alice', games)).toEqual({ type: Types.GAMES_OF_USER, userName: 'alice', games });
|
const gametypeMap = { 1: 'Standard' };
|
||||||
|
expect(Actions.gamesOfUser('alice', games, gametypeMap)).toEqual({ type: Types.GAMES_OF_USER, userName: 'alice', games, gametypeMap });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,10 @@
|
||||||
import { DeckList, DeckStorageTreeItem, Game, ReplayMatch, WebSocketConnectOptions } from 'types';
|
import {
|
||||||
|
BanHistoryItem, DeckList, DeckStorageTreeItem, GametypeMap, LogItem, ReplayMatch,
|
||||||
|
User, WebSocketConnectOptions, WarnHistoryItem, WarnListItem
|
||||||
|
} from 'types';
|
||||||
|
import type { ServerInfo_Game } from 'generated/proto/serverinfo_game_pb';
|
||||||
|
import { NotifyUserData, ServerShutdownData, UserMessageData } from 'websocket/events/session/interfaces';
|
||||||
|
import { ServerStateStatus } from './server.interfaces';
|
||||||
import { Types } from './server.types';
|
import { Types } from './server.types';
|
||||||
|
|
||||||
export const Actions = {
|
export const Actions = {
|
||||||
|
|
@ -15,7 +21,7 @@ export const Actions = {
|
||||||
loginFailed: () => ({
|
loginFailed: () => ({
|
||||||
type: Types.LOGIN_FAILED,
|
type: Types.LOGIN_FAILED,
|
||||||
}),
|
}),
|
||||||
connectionClosed: reason => ({
|
connectionClosed: (reason: number) => ({
|
||||||
type: Types.CONNECTION_CLOSED,
|
type: Types.CONNECTION_CLOSED,
|
||||||
reason
|
reason
|
||||||
}),
|
}),
|
||||||
|
|
@ -28,59 +34,59 @@ export const Actions = {
|
||||||
testConnectionFailed: () => ({
|
testConnectionFailed: () => ({
|
||||||
type: Types.TEST_CONNECTION_FAILED,
|
type: Types.TEST_CONNECTION_FAILED,
|
||||||
}),
|
}),
|
||||||
serverMessage: message => ({
|
serverMessage: (message: string) => ({
|
||||||
type: Types.SERVER_MESSAGE,
|
type: Types.SERVER_MESSAGE,
|
||||||
message
|
message
|
||||||
}),
|
}),
|
||||||
updateBuddyList: buddyList => ({
|
updateBuddyList: (buddyList: User[]) => ({
|
||||||
type: Types.UPDATE_BUDDY_LIST,
|
type: Types.UPDATE_BUDDY_LIST,
|
||||||
buddyList
|
buddyList
|
||||||
}),
|
}),
|
||||||
addToBuddyList: user => ({
|
addToBuddyList: (user: User) => ({
|
||||||
type: Types.ADD_TO_BUDDY_LIST,
|
type: Types.ADD_TO_BUDDY_LIST,
|
||||||
user
|
user
|
||||||
}),
|
}),
|
||||||
removeFromBuddyList: userName => ({
|
removeFromBuddyList: (userName: string) => ({
|
||||||
type: Types.REMOVE_FROM_BUDDY_LIST,
|
type: Types.REMOVE_FROM_BUDDY_LIST,
|
||||||
userName
|
userName
|
||||||
}),
|
}),
|
||||||
updateIgnoreList: ignoreList => ({
|
updateIgnoreList: (ignoreList: User[]) => ({
|
||||||
type: Types.UPDATE_IGNORE_LIST,
|
type: Types.UPDATE_IGNORE_LIST,
|
||||||
ignoreList
|
ignoreList
|
||||||
}),
|
}),
|
||||||
addToIgnoreList: user => ({
|
addToIgnoreList: (user: User) => ({
|
||||||
type: Types.ADD_TO_IGNORE_LIST,
|
type: Types.ADD_TO_IGNORE_LIST,
|
||||||
user
|
user
|
||||||
}),
|
}),
|
||||||
removeFromIgnoreList: userName => ({
|
removeFromIgnoreList: (userName: string) => ({
|
||||||
type: Types.REMOVE_FROM_IGNORE_LIST,
|
type: Types.REMOVE_FROM_IGNORE_LIST,
|
||||||
userName
|
userName
|
||||||
}),
|
}),
|
||||||
updateInfo: info => ({
|
updateInfo: (info: { name: string; version: string }) => ({
|
||||||
type: Types.UPDATE_INFO,
|
type: Types.UPDATE_INFO,
|
||||||
info
|
info
|
||||||
}),
|
}),
|
||||||
updateStatus: status => ({
|
updateStatus: (status: ServerStateStatus) => ({
|
||||||
type: Types.UPDATE_STATUS,
|
type: Types.UPDATE_STATUS,
|
||||||
status
|
status
|
||||||
}),
|
}),
|
||||||
updateUser: user => ({
|
updateUser: (user: User) => ({
|
||||||
type: Types.UPDATE_USER,
|
type: Types.UPDATE_USER,
|
||||||
user
|
user
|
||||||
}),
|
}),
|
||||||
updateUsers: users => ({
|
updateUsers: (users: User[]) => ({
|
||||||
type: Types.UPDATE_USERS,
|
type: Types.UPDATE_USERS,
|
||||||
users
|
users
|
||||||
}),
|
}),
|
||||||
userJoined: user => ({
|
userJoined: (user: User) => ({
|
||||||
type: Types.USER_JOINED,
|
type: Types.USER_JOINED,
|
||||||
user
|
user
|
||||||
}),
|
}),
|
||||||
userLeft: name => ({
|
userLeft: (name: string) => ({
|
||||||
type: Types.USER_LEFT,
|
type: Types.USER_LEFT,
|
||||||
name
|
name
|
||||||
}),
|
}),
|
||||||
viewLogs: logs => ({
|
viewLogs: (logs: LogItem[]) => ({
|
||||||
type: Types.VIEW_LOGS,
|
type: Types.VIEW_LOGS,
|
||||||
logs
|
logs
|
||||||
}),
|
}),
|
||||||
|
|
@ -93,22 +99,26 @@ export const Actions = {
|
||||||
registrationSuccess: () => ({
|
registrationSuccess: () => ({
|
||||||
type: Types.REGISTRATION_SUCCESS,
|
type: Types.REGISTRATION_SUCCESS,
|
||||||
}),
|
}),
|
||||||
registrationFailed: (error) => ({
|
registrationFailed: (reason: string, endTime?: number) => ({
|
||||||
type: Types.REGISTRATION_FAILED,
|
type: Types.REGISTRATION_FAILED,
|
||||||
error
|
reason,
|
||||||
|
endTime,
|
||||||
}),
|
}),
|
||||||
registrationEmailError: (error) => ({
|
registrationEmailError: (error: string) => ({
|
||||||
type: Types.REGISTRATION_EMAIL_ERROR,
|
type: Types.REGISTRATION_EMAIL_ERROR,
|
||||||
error
|
error
|
||||||
}),
|
}),
|
||||||
registrationPasswordError: (error) => ({
|
registrationPasswordError: (error: string) => ({
|
||||||
type: Types.REGISTRATION_PASSWORD_ERROR,
|
type: Types.REGISTRATION_PASSWORD_ERROR,
|
||||||
error
|
error
|
||||||
}),
|
}),
|
||||||
registrationUserNameError: (error) => ({
|
registrationUserNameError: (error: string) => ({
|
||||||
type: Types.REGISTRATION_USERNAME_ERROR,
|
type: Types.REGISTRATION_USERNAME_ERROR,
|
||||||
error
|
error
|
||||||
}),
|
}),
|
||||||
|
clearRegistrationErrors: () => ({
|
||||||
|
type: Types.CLEAR_REGISTRATION_ERRORS,
|
||||||
|
}),
|
||||||
accountAwaitingActivation: (options: WebSocketConnectOptions) => ({
|
accountAwaitingActivation: (options: WebSocketConnectOptions) => ({
|
||||||
type: Types.ACCOUNT_AWAITING_ACTIVATION,
|
type: Types.ACCOUNT_AWAITING_ACTIVATION,
|
||||||
options
|
options
|
||||||
|
|
@ -131,7 +141,7 @@ export const Actions = {
|
||||||
resetPasswordSuccess: () => ({
|
resetPasswordSuccess: () => ({
|
||||||
type: Types.RESET_PASSWORD_SUCCESS,
|
type: Types.RESET_PASSWORD_SUCCESS,
|
||||||
}),
|
}),
|
||||||
adjustMod: (userName, shouldBeMod, shouldBeJudge) => ({
|
adjustMod: (userName: string, shouldBeMod: boolean, shouldBeJudge: boolean) => ({
|
||||||
type: Types.ADJUST_MOD,
|
type: Types.ADJUST_MOD,
|
||||||
userName,
|
userName,
|
||||||
shouldBeMod,
|
shouldBeMod,
|
||||||
|
|
@ -149,59 +159,59 @@ export const Actions = {
|
||||||
accountPasswordChange: () => ({
|
accountPasswordChange: () => ({
|
||||||
type: Types.ACCOUNT_PASSWORD_CHANGE,
|
type: Types.ACCOUNT_PASSWORD_CHANGE,
|
||||||
}),
|
}),
|
||||||
accountEditChanged: (user) => ({
|
accountEditChanged: (user: Partial<User>) => ({
|
||||||
type: Types.ACCOUNT_EDIT_CHANGED,
|
type: Types.ACCOUNT_EDIT_CHANGED,
|
||||||
user,
|
user,
|
||||||
}),
|
}),
|
||||||
accountImageChanged: (user) => ({
|
accountImageChanged: (user: Partial<User>) => ({
|
||||||
type: Types.ACCOUNT_IMAGE_CHANGED,
|
type: Types.ACCOUNT_IMAGE_CHANGED,
|
||||||
user,
|
user,
|
||||||
}),
|
}),
|
||||||
getUserInfo: (userInfo) => ({
|
getUserInfo: (userInfo: User) => ({
|
||||||
type: Types.GET_USER_INFO,
|
type: Types.GET_USER_INFO,
|
||||||
userInfo,
|
userInfo,
|
||||||
}),
|
}),
|
||||||
notifyUser: (notification) => ({
|
notifyUser: (notification: NotifyUserData) => ({
|
||||||
type: Types.NOTIFY_USER,
|
type: Types.NOTIFY_USER,
|
||||||
notification,
|
notification,
|
||||||
}),
|
}),
|
||||||
serverShutdown: (data) => ({
|
serverShutdown: (data: ServerShutdownData) => ({
|
||||||
type: Types.SERVER_SHUTDOWN,
|
type: Types.SERVER_SHUTDOWN,
|
||||||
data,
|
data,
|
||||||
}),
|
}),
|
||||||
userMessage: (messageData) => ({
|
userMessage: (messageData: UserMessageData) => ({
|
||||||
type: Types.USER_MESSAGE,
|
type: Types.USER_MESSAGE,
|
||||||
messageData,
|
messageData,
|
||||||
}),
|
}),
|
||||||
addToList: (list, userName) => ({
|
addToList: (list: string, userName: string) => ({
|
||||||
type: Types.ADD_TO_LIST,
|
type: Types.ADD_TO_LIST,
|
||||||
list,
|
list,
|
||||||
userName,
|
userName,
|
||||||
}),
|
}),
|
||||||
removeFromList: (list, userName) => ({
|
removeFromList: (list: string, userName: string) => ({
|
||||||
type: Types.REMOVE_FROM_LIST,
|
type: Types.REMOVE_FROM_LIST,
|
||||||
list,
|
list,
|
||||||
userName,
|
userName,
|
||||||
}),
|
}),
|
||||||
banFromServer: (userName) => ({
|
banFromServer: (userName: string) => ({
|
||||||
type: Types.BAN_FROM_SERVER,
|
type: Types.BAN_FROM_SERVER,
|
||||||
userName,
|
userName,
|
||||||
}),
|
}),
|
||||||
banHistory: (userName, banHistory) => ({
|
banHistory: (userName: string, banHistory: BanHistoryItem[]) => ({
|
||||||
type: Types.BAN_HISTORY,
|
type: Types.BAN_HISTORY,
|
||||||
userName,
|
userName,
|
||||||
banHistory,
|
banHistory,
|
||||||
}),
|
}),
|
||||||
warnHistory: (userName, warnHistory) => ({
|
warnHistory: (userName: string, warnHistory: WarnHistoryItem[]) => ({
|
||||||
type: Types.WARN_HISTORY,
|
type: Types.WARN_HISTORY,
|
||||||
userName,
|
userName,
|
||||||
warnHistory,
|
warnHistory,
|
||||||
}),
|
}),
|
||||||
warnListOptions: (warnList) => ({
|
warnListOptions: (warnList: WarnListItem[]) => ({
|
||||||
type: Types.WARN_LIST_OPTIONS,
|
type: Types.WARN_LIST_OPTIONS,
|
||||||
warnList,
|
warnList,
|
||||||
}),
|
}),
|
||||||
warnUser: (userName) => ({
|
warnUser: (userName: string) => ({
|
||||||
type: Types.WARN_USER,
|
type: Types.WARN_USER,
|
||||||
userName,
|
userName,
|
||||||
}),
|
}),
|
||||||
|
|
@ -234,5 +244,8 @@ export const Actions = {
|
||||||
deckDelDir: (path: string) => ({ type: Types.DECK_DEL_DIR, path }),
|
deckDelDir: (path: string) => ({ type: Types.DECK_DEL_DIR, path }),
|
||||||
deckUpload: (path: string, treeItem: DeckStorageTreeItem) => ({ type: Types.DECK_UPLOAD, path, treeItem }),
|
deckUpload: (path: string, treeItem: DeckStorageTreeItem) => ({ type: Types.DECK_UPLOAD, path, treeItem }),
|
||||||
deckDelete: (deckId: number) => ({ type: Types.DECK_DELETE, deckId }),
|
deckDelete: (deckId: number) => ({ type: Types.DECK_DELETE, deckId }),
|
||||||
gamesOfUser: (userName: string, games: Game[]) => ({ type: Types.GAMES_OF_USER, userName, games }),
|
gamesOfUser: (userName: string, games: ServerInfo_Game[], gametypeMap: GametypeMap) =>
|
||||||
|
({ type: Types.GAMES_OF_USER, userName, games, gametypeMap }),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ServerAction = ReturnType<typeof Actions[keyof typeof Actions]>;
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
vi.mock('store/store', () => ({ store: { dispatch: vi.fn() } }));
|
vi.mock('store', () => ({ store: { dispatch: vi.fn() } }));
|
||||||
vi.mock('redux-form', () => ({
|
|
||||||
reset: vi.fn((form) => ({ type: '@@redux-form/RESET', meta: { form } })),
|
|
||||||
}));
|
|
||||||
|
|
||||||
import { store } from 'store/store';
|
import { store } from 'store';
|
||||||
import { reset } from 'redux-form';
|
|
||||||
import { Actions } from './server.actions';
|
import { Actions } from './server.actions';
|
||||||
import { Dispatch } from './server.dispatch';
|
import { Dispatch } from './server.dispatch';
|
||||||
|
import { create } from '@bufbuild/protobuf';
|
||||||
|
import { Event_NotifyUserSchema } from 'generated/proto/event_notify_user_pb';
|
||||||
|
import { Event_ServerShutdownSchema } from 'generated/proto/event_server_shutdown_pb';
|
||||||
|
import { Event_UserMessageSchema } from 'generated/proto/event_user_message_pb';
|
||||||
import {
|
import {
|
||||||
makeBanHistoryItem,
|
makeBanHistoryItem,
|
||||||
makeConnectOptions,
|
makeConnectOptions,
|
||||||
makeDeckList,
|
makeDeckList,
|
||||||
makeDeckTreeItem,
|
makeDeckTreeItem,
|
||||||
|
makeGame,
|
||||||
makeReplayMatch,
|
makeReplayMatch,
|
||||||
makeUser,
|
makeUser,
|
||||||
makeWarnHistoryItem,
|
makeWarnHistoryItem,
|
||||||
|
|
@ -68,11 +69,10 @@ describe('Dispatch', () => {
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.updateBuddyList(list));
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.updateBuddyList(list));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('addToBuddyList dispatches reset("addToBuddies") then Actions.addToBuddyList()', () => {
|
it('addToBuddyList dispatches Actions.addToBuddyList()', () => {
|
||||||
const user = makeUser();
|
const user = makeUser();
|
||||||
Dispatch.addToBuddyList(user);
|
Dispatch.addToBuddyList(user);
|
||||||
expect(store.dispatch).toHaveBeenNthCalledWith(1, (reset as vi.Mock)('addToBuddies'));
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.addToBuddyList(user));
|
||||||
expect(store.dispatch).toHaveBeenNthCalledWith(2, Actions.addToBuddyList(user));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('removeFromBuddyList dispatches Actions.removeFromBuddyList()', () => {
|
it('removeFromBuddyList dispatches Actions.removeFromBuddyList()', () => {
|
||||||
|
|
@ -86,11 +86,10 @@ describe('Dispatch', () => {
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.updateIgnoreList(list));
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.updateIgnoreList(list));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('addToIgnoreList dispatches reset("addToIgnore") then Actions.addToIgnoreList()', () => {
|
it('addToIgnoreList dispatches Actions.addToIgnoreList()', () => {
|
||||||
const user = makeUser();
|
const user = makeUser();
|
||||||
Dispatch.addToIgnoreList(user);
|
Dispatch.addToIgnoreList(user);
|
||||||
expect(store.dispatch).toHaveBeenNthCalledWith(1, (reset as vi.Mock)('addToIgnore'));
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.addToIgnoreList(user));
|
||||||
expect(store.dispatch).toHaveBeenNthCalledWith(2, Actions.addToIgnoreList(user));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('removeFromIgnoreList dispatches Actions.removeFromIgnoreList()', () => {
|
it('removeFromIgnoreList dispatches Actions.removeFromIgnoreList()', () => {
|
||||||
|
|
@ -132,7 +131,7 @@ describe('Dispatch', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('viewLogs dispatches Actions.viewLogs()', () => {
|
it('viewLogs dispatches Actions.viewLogs()', () => {
|
||||||
const logs = { room: [], game: [], chat: [] };
|
const logs = [{ targetType: 'room' }] as any[];
|
||||||
Dispatch.viewLogs(logs);
|
Dispatch.viewLogs(logs);
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.viewLogs(logs));
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.viewLogs(logs));
|
||||||
});
|
});
|
||||||
|
|
@ -157,9 +156,14 @@ describe('Dispatch', () => {
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.registrationSuccess());
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.registrationSuccess());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('registrationFailed dispatches correctly', () => {
|
it('registrationFailed passes reason and endTime to action', () => {
|
||||||
Dispatch.registrationFailed('err');
|
Dispatch.registrationFailed('reason', 999);
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.registrationFailed('err'));
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.registrationFailed('reason', 999));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('registrationFailed passes reason only when no endTime', () => {
|
||||||
|
Dispatch.registrationFailed('plain reason');
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.registrationFailed('plain reason', undefined));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('registrationEmailError dispatches correctly', () => {
|
it('registrationEmailError dispatches correctly', () => {
|
||||||
|
|
@ -257,19 +261,19 @@ describe('Dispatch', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('notifyUser dispatches correctly', () => {
|
it('notifyUser dispatches correctly', () => {
|
||||||
const notification = { type: 1, warningReason: '', customTitle: '', customContent: '' };
|
const notification = create(Event_NotifyUserSchema, { type: 1, warningReason: '', customTitle: '', customContent: '' });
|
||||||
Dispatch.notifyUser(notification);
|
Dispatch.notifyUser(notification);
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.notifyUser(notification));
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.notifyUser(notification));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('serverShutdown dispatches correctly', () => {
|
it('serverShutdown dispatches correctly', () => {
|
||||||
const data = { reason: 'maintenance', minutes: 5 };
|
const data = create(Event_ServerShutdownSchema, { reason: 'maintenance', minutes: 5 });
|
||||||
Dispatch.serverShutdown(data);
|
Dispatch.serverShutdown(data);
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.serverShutdown(data));
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.serverShutdown(data));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('userMessage dispatches correctly', () => {
|
it('userMessage dispatches correctly', () => {
|
||||||
const messageData = { senderName: 'Alice', receiverName: 'Bob', message: 'hey' };
|
const messageData = create(Event_UserMessageSchema, { senderName: 'Alice', receiverName: 'Bob', message: 'hey' });
|
||||||
Dispatch.userMessage(messageData);
|
Dispatch.userMessage(messageData);
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.userMessage(messageData));
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.userMessage(messageData));
|
||||||
});
|
});
|
||||||
|
|
@ -382,8 +386,9 @@ describe('Dispatch', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('gamesOfUser dispatches correctly', () => {
|
it('gamesOfUser dispatches correctly', () => {
|
||||||
const games = [{ gameId: 1 }] as any;
|
const games = [makeGame({ gameId: 1 })];
|
||||||
Dispatch.gamesOfUser('alice', games);
|
const gametypeMap = { 1: 'Standard' };
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.gamesOfUser('alice', games));
|
Dispatch.gamesOfUser('alice', games, gametypeMap);
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(Actions.gamesOfUser('alice', games, gametypeMap));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
import { reset } from 'redux-form';
|
|
||||||
import { Actions } from './server.actions';
|
import { Actions } from './server.actions';
|
||||||
import { store } from 'store';
|
import { store } from 'store';
|
||||||
import { DeckList, DeckStorageTreeItem, Game, ReplayMatch, WebSocketConnectOptions } from 'types';
|
import {
|
||||||
|
BanHistoryItem, DeckList, DeckStorageTreeItem, GametypeMap, LogItem, ReplayMatch,
|
||||||
|
User, WarnHistoryItem, WarnListItem, WebSocketConnectOptions
|
||||||
|
} from 'types';
|
||||||
|
import type { ServerInfo_Game } from 'generated/proto/serverinfo_game_pb';
|
||||||
|
import { NotifyUserData, ServerShutdownData, UserMessageData } from 'websocket/events/session/interfaces';
|
||||||
|
|
||||||
export const Dispatch = {
|
export const Dispatch = {
|
||||||
initialized: () => {
|
initialized: () => {
|
||||||
|
|
@ -10,13 +14,13 @@ export const Dispatch = {
|
||||||
clearStore: () => {
|
clearStore: () => {
|
||||||
store.dispatch(Actions.clearStore());
|
store.dispatch(Actions.clearStore());
|
||||||
},
|
},
|
||||||
loginSuccessful: options => {
|
loginSuccessful: (options: WebSocketConnectOptions) => {
|
||||||
store.dispatch(Actions.loginSuccessful(options));
|
store.dispatch(Actions.loginSuccessful(options));
|
||||||
},
|
},
|
||||||
loginFailed: () => {
|
loginFailed: () => {
|
||||||
store.dispatch(Actions.loginFailed());
|
store.dispatch(Actions.loginFailed());
|
||||||
},
|
},
|
||||||
connectionClosed: reason => {
|
connectionClosed: (reason: number) => {
|
||||||
store.dispatch(Actions.connectionClosed(reason));
|
store.dispatch(Actions.connectionClosed(reason));
|
||||||
},
|
},
|
||||||
connectionFailed: () => {
|
connectionFailed: () => {
|
||||||
|
|
@ -28,57 +32,55 @@ export const Dispatch = {
|
||||||
testConnectionFailed: () => {
|
testConnectionFailed: () => {
|
||||||
store.dispatch(Actions.testConnectionFailed());
|
store.dispatch(Actions.testConnectionFailed());
|
||||||
},
|
},
|
||||||
updateBuddyList: buddyList => {
|
updateBuddyList: (buddyList: User[]) => {
|
||||||
store.dispatch(Actions.updateBuddyList(buddyList));
|
store.dispatch(Actions.updateBuddyList(buddyList));
|
||||||
},
|
},
|
||||||
addToBuddyList: user => {
|
addToBuddyList: (user: User) => {
|
||||||
store.dispatch(reset('addToBuddies'));
|
|
||||||
store.dispatch(Actions.addToBuddyList(user));
|
store.dispatch(Actions.addToBuddyList(user));
|
||||||
},
|
},
|
||||||
removeFromBuddyList: userName => {
|
removeFromBuddyList: (userName: string) => {
|
||||||
store.dispatch(Actions.removeFromBuddyList(userName));
|
store.dispatch(Actions.removeFromBuddyList(userName));
|
||||||
},
|
},
|
||||||
updateIgnoreList: ignoreList => {
|
updateIgnoreList: (ignoreList: User[]) => {
|
||||||
store.dispatch(Actions.updateIgnoreList(ignoreList));
|
store.dispatch(Actions.updateIgnoreList(ignoreList));
|
||||||
},
|
},
|
||||||
addToIgnoreList: user => {
|
addToIgnoreList: (user: User) => {
|
||||||
store.dispatch(reset('addToIgnore'));
|
|
||||||
store.dispatch(Actions.addToIgnoreList(user));
|
store.dispatch(Actions.addToIgnoreList(user));
|
||||||
},
|
},
|
||||||
removeFromIgnoreList: userName => {
|
removeFromIgnoreList: (userName: string) => {
|
||||||
store.dispatch(Actions.removeFromIgnoreList(userName));
|
store.dispatch(Actions.removeFromIgnoreList(userName));
|
||||||
},
|
},
|
||||||
updateInfo: (name, version) => {
|
updateInfo: (name: string, version: string) => {
|
||||||
store.dispatch(Actions.updateInfo({
|
store.dispatch(Actions.updateInfo({
|
||||||
name,
|
name,
|
||||||
version
|
version
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
updateStatus: (state, description) => {
|
updateStatus: (state: number, description: string) => {
|
||||||
store.dispatch(Actions.updateStatus({
|
store.dispatch(Actions.updateStatus({
|
||||||
state,
|
state,
|
||||||
description
|
description
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
updateUser: user => {
|
updateUser: (user: User) => {
|
||||||
store.dispatch(Actions.updateUser(user));
|
store.dispatch(Actions.updateUser(user));
|
||||||
},
|
},
|
||||||
updateUsers: users => {
|
updateUsers: (users: User[]) => {
|
||||||
store.dispatch(Actions.updateUsers(users));
|
store.dispatch(Actions.updateUsers(users));
|
||||||
},
|
},
|
||||||
userJoined: user => {
|
userJoined: (user: User) => {
|
||||||
store.dispatch(Actions.userJoined(user));
|
store.dispatch(Actions.userJoined(user));
|
||||||
},
|
},
|
||||||
userLeft: name => {
|
userLeft: (name: string) => {
|
||||||
store.dispatch(Actions.userLeft(name));
|
store.dispatch(Actions.userLeft(name));
|
||||||
},
|
},
|
||||||
viewLogs: name => {
|
viewLogs: (logs: LogItem[]) => {
|
||||||
store.dispatch(Actions.viewLogs(name));
|
store.dispatch(Actions.viewLogs(logs));
|
||||||
},
|
},
|
||||||
clearLogs: () => {
|
clearLogs: () => {
|
||||||
store.dispatch(Actions.clearLogs());
|
store.dispatch(Actions.clearLogs());
|
||||||
},
|
},
|
||||||
serverMessage: message => {
|
serverMessage: (message: string) => {
|
||||||
store.dispatch(Actions.serverMessage(message));
|
store.dispatch(Actions.serverMessage(message));
|
||||||
},
|
},
|
||||||
registrationRequiresEmail: () => {
|
registrationRequiresEmail: () => {
|
||||||
|
|
@ -87,16 +89,19 @@ export const Dispatch = {
|
||||||
registrationSuccess: () => {
|
registrationSuccess: () => {
|
||||||
store.dispatch(Actions.registrationSuccess())
|
store.dispatch(Actions.registrationSuccess())
|
||||||
},
|
},
|
||||||
registrationFailed: (error) => {
|
registrationFailed: (reason: string, endTime?: number) => {
|
||||||
store.dispatch(Actions.registrationFailed(error));
|
store.dispatch(Actions.registrationFailed(reason, endTime));
|
||||||
},
|
},
|
||||||
registrationEmailError: (error) => {
|
clearRegistrationErrors: () => {
|
||||||
|
store.dispatch(Actions.clearRegistrationErrors());
|
||||||
|
},
|
||||||
|
registrationEmailError: (error: string) => {
|
||||||
store.dispatch(Actions.registrationEmailError(error));
|
store.dispatch(Actions.registrationEmailError(error));
|
||||||
},
|
},
|
||||||
registrationPasswordError: (error) => {
|
registrationPasswordError: (error: string) => {
|
||||||
store.dispatch(Actions.registrationPasswordError(error));
|
store.dispatch(Actions.registrationPasswordError(error));
|
||||||
},
|
},
|
||||||
registrationUserNameError: (error) => {
|
registrationUserNameError: (error: string) => {
|
||||||
store.dispatch(Actions.registrationUserNameError(error));
|
store.dispatch(Actions.registrationUserNameError(error));
|
||||||
},
|
},
|
||||||
accountAwaitingActivation: (options: WebSocketConnectOptions) => {
|
accountAwaitingActivation: (options: WebSocketConnectOptions) => {
|
||||||
|
|
@ -120,7 +125,7 @@ export const Dispatch = {
|
||||||
resetPasswordSuccess: () => {
|
resetPasswordSuccess: () => {
|
||||||
store.dispatch(Actions.resetPasswordSuccess());
|
store.dispatch(Actions.resetPasswordSuccess());
|
||||||
},
|
},
|
||||||
adjustMod: (userName, shouldBeMod, shouldBeJudge) => {
|
adjustMod: (userName: string, shouldBeMod: boolean, shouldBeJudge: boolean) => {
|
||||||
store.dispatch(Actions.adjustMod(userName, shouldBeMod, shouldBeJudge));
|
store.dispatch(Actions.adjustMod(userName, shouldBeMod, shouldBeJudge));
|
||||||
},
|
},
|
||||||
reloadConfig: () => {
|
reloadConfig: () => {
|
||||||
|
|
@ -135,43 +140,43 @@ export const Dispatch = {
|
||||||
accountPasswordChange: () => {
|
accountPasswordChange: () => {
|
||||||
store.dispatch(Actions.accountPasswordChange());
|
store.dispatch(Actions.accountPasswordChange());
|
||||||
},
|
},
|
||||||
accountEditChanged: (user) => {
|
accountEditChanged: (user: Partial<User>) => {
|
||||||
store.dispatch(Actions.accountEditChanged(user));
|
store.dispatch(Actions.accountEditChanged(user));
|
||||||
},
|
},
|
||||||
accountImageChanged: (user) => {
|
accountImageChanged: (user: Partial<User>) => {
|
||||||
store.dispatch(Actions.accountImageChanged(user));
|
store.dispatch(Actions.accountImageChanged(user));
|
||||||
},
|
},
|
||||||
getUserInfo: (userInfo) => {
|
getUserInfo: (userInfo: User) => {
|
||||||
store.dispatch(Actions.getUserInfo(userInfo));
|
store.dispatch(Actions.getUserInfo(userInfo));
|
||||||
},
|
},
|
||||||
notifyUser: (notification) => {
|
notifyUser: (notification: NotifyUserData) => {
|
||||||
store.dispatch(Actions.notifyUser(notification))
|
store.dispatch(Actions.notifyUser(notification))
|
||||||
},
|
},
|
||||||
serverShutdown: (data) => {
|
serverShutdown: (data: ServerShutdownData) => {
|
||||||
store.dispatch(Actions.serverShutdown(data))
|
store.dispatch(Actions.serverShutdown(data))
|
||||||
},
|
},
|
||||||
userMessage: (messageData) => {
|
userMessage: (messageData: UserMessageData) => {
|
||||||
store.dispatch(Actions.userMessage(messageData))
|
store.dispatch(Actions.userMessage(messageData))
|
||||||
},
|
},
|
||||||
addToList: (list, userName) => {
|
addToList: (list: string, userName: string) => {
|
||||||
store.dispatch(Actions.addToList(list, userName))
|
store.dispatch(Actions.addToList(list, userName))
|
||||||
},
|
},
|
||||||
removeFromList: (list, userName) => {
|
removeFromList: (list: string, userName: string) => {
|
||||||
store.dispatch(Actions.removeFromList(list, userName))
|
store.dispatch(Actions.removeFromList(list, userName))
|
||||||
},
|
},
|
||||||
banFromServer: (userName) => {
|
banFromServer: (userName: string) => {
|
||||||
store.dispatch(Actions.banFromServer(userName));
|
store.dispatch(Actions.banFromServer(userName));
|
||||||
},
|
},
|
||||||
banHistory: (userName, banHistory) => {
|
banHistory: (userName: string, banHistory: BanHistoryItem[]) => {
|
||||||
store.dispatch(Actions.banHistory(userName, banHistory))
|
store.dispatch(Actions.banHistory(userName, banHistory))
|
||||||
},
|
},
|
||||||
warnHistory: (userName, warnHistory) => {
|
warnHistory: (userName: string, warnHistory: WarnHistoryItem[]) => {
|
||||||
store.dispatch(Actions.warnHistory(userName, warnHistory))
|
store.dispatch(Actions.warnHistory(userName, warnHistory))
|
||||||
},
|
},
|
||||||
warnListOptions: (warnList) => {
|
warnListOptions: (warnList: WarnListItem[]) => {
|
||||||
store.dispatch(Actions.warnListOptions(warnList))
|
store.dispatch(Actions.warnListOptions(warnList))
|
||||||
},
|
},
|
||||||
warnUser: (userName) => {
|
warnUser: (userName: string) => {
|
||||||
store.dispatch(Actions.warnUser(userName))
|
store.dispatch(Actions.warnUser(userName))
|
||||||
},
|
},
|
||||||
grantReplayAccess: (replayId: number, moderatorName: string) => {
|
grantReplayAccess: (replayId: number, moderatorName: string) => {
|
||||||
|
|
@ -213,7 +218,7 @@ export const Dispatch = {
|
||||||
deckDelete: (deckId: number) => {
|
deckDelete: (deckId: number) => {
|
||||||
store.dispatch(Actions.deckDelete(deckId));
|
store.dispatch(Actions.deckDelete(deckId));
|
||||||
},
|
},
|
||||||
gamesOfUser: (userName: string, games: Game[]) => {
|
gamesOfUser: (userName: string, games: ServerInfo_Game[], gametypeMap: GametypeMap) => {
|
||||||
store.dispatch(Actions.gamesOfUser(userName, games));
|
store.dispatch(Actions.gamesOfUser(userName, games, gametypeMap));
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ export interface ServerState {
|
||||||
replays: ReplayMatch[];
|
replays: ReplayMatch[];
|
||||||
backendDecks: DeckList | null;
|
backendDecks: DeckList | null;
|
||||||
gamesOfUser: { [userName: string]: Game[] };
|
gamesOfUser: { [userName: string]: Game[] };
|
||||||
|
registrationError: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServerStateStatus {
|
export interface ServerStateStatus {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
import { StatusEnum, UserLevelFlag } from 'types';
|
import { StatusEnum, UserLevelFlag } from 'types';
|
||||||
|
import { create } from '@bufbuild/protobuf';
|
||||||
|
import { Event_UserMessageSchema } from 'generated/proto/event_user_message_pb';
|
||||||
|
import { ServerInfo_DeckStorage_FolderSchema, ServerInfo_DeckStorage_TreeItemSchema } from 'generated/proto/serverinfo_deckstorage_pb';
|
||||||
import { serverReducer } from './server.reducer';
|
import { serverReducer } from './server.reducer';
|
||||||
import { Types } from './server.types';
|
import { Types } from './server.types';
|
||||||
import {
|
import {
|
||||||
|
|
@ -6,6 +9,7 @@ import {
|
||||||
makeConnectOptions,
|
makeConnectOptions,
|
||||||
makeDeckList,
|
makeDeckList,
|
||||||
makeDeckTreeItem,
|
makeDeckTreeItem,
|
||||||
|
makeGame,
|
||||||
makeLogItem,
|
makeLogItem,
|
||||||
makeReplayMatch,
|
makeReplayMatch,
|
||||||
makeServerState,
|
makeServerState,
|
||||||
|
|
@ -71,6 +75,35 @@ describe('Account & Connection', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Registration ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('Registration', () => {
|
||||||
|
it('REGISTRATION_FAILED → stores normalized error (plain reason)', () => {
|
||||||
|
const state = makeServerState({ registrationError: null });
|
||||||
|
const result = serverReducer(state, { type: Types.REGISTRATION_FAILED, reason: 'Server is disabled', endTime: undefined });
|
||||||
|
expect(result.registrationError).toBe('Server is disabled');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('REGISTRATION_FAILED → normalizes banned error when endTime is given', () => {
|
||||||
|
const state = makeServerState({ registrationError: null });
|
||||||
|
const result = serverReducer(state, { type: Types.REGISTRATION_FAILED, reason: 'bad actor', endTime: Date.now() + 100_000 });
|
||||||
|
expect(result.registrationError).toContain('banned');
|
||||||
|
expect(result.registrationError).toContain('bad actor');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('CLEAR_REGISTRATION_ERRORS → sets registrationError to null', () => {
|
||||||
|
const state = makeServerState({ registrationError: 'some error' });
|
||||||
|
const result = serverReducer(state, { type: Types.CLEAR_REGISTRATION_ERRORS });
|
||||||
|
expect(result.registrationError).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('CLEAR_STORE → resets registrationError to null', () => {
|
||||||
|
const state = makeServerState({ registrationError: 'stale error' });
|
||||||
|
const result = serverReducer(state, { type: Types.CLEAR_STORE });
|
||||||
|
expect(result.registrationError).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ── Server Info & Status ──────────────────────────────────────────────────────
|
// ── Server Info & Status ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
describe('Server Info & Status', () => {
|
describe('Server Info & Status', () => {
|
||||||
|
|
@ -205,11 +238,11 @@ describe('Ignore List', () => {
|
||||||
// ── Logs ─────────────────────────────────────────────────────────────────────
|
// ── Logs ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
describe('Logs', () => {
|
describe('Logs', () => {
|
||||||
it('VIEW_LOGS → replaces logs entirely', () => {
|
it('VIEW_LOGS → groups LogItem[] into room/game/chat buckets', () => {
|
||||||
const logs = { room: [makeLogItem()], game: [], chat: [] };
|
const log = makeLogItem({ targetType: 'room' });
|
||||||
const state = makeServerState();
|
const state = makeServerState();
|
||||||
const result = serverReducer(state, { type: Types.VIEW_LOGS, logs });
|
const result = serverReducer(state, { type: Types.VIEW_LOGS, logs: [log] });
|
||||||
expect(result.logs).toEqual(logs);
|
expect(result.logs.room).toEqual([log]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('CLEAR_LOGS → resets logs to empty arrays', () => {
|
it('CLEAR_LOGS → resets logs to empty arrays', () => {
|
||||||
|
|
@ -241,12 +274,12 @@ describe('Messaging', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('USER_MESSAGE → appends to existing messages for that user', () => {
|
it('USER_MESSAGE → appends to existing messages for that user', () => {
|
||||||
const existingMsg = { senderName: 'Alice', receiverName: 'Bob', message: 'first' };
|
const existingMsg = create(Event_UserMessageSchema, { senderName: 'Alice', receiverName: 'Bob', message: 'first' });
|
||||||
const state = makeServerState({
|
const state = makeServerState({
|
||||||
user: makeUser({ name: 'Bob' }),
|
user: makeUser({ name: 'Bob' }),
|
||||||
messages: { Alice: [existingMsg] },
|
messages: { Alice: [existingMsg] },
|
||||||
});
|
});
|
||||||
const newMsg = { senderName: 'Alice', receiverName: 'Bob', message: 'second' };
|
const newMsg = create(Event_UserMessageSchema, { senderName: 'Alice', receiverName: 'Bob', message: 'second' });
|
||||||
const result = serverReducer(state, { type: Types.USER_MESSAGE, messageData: newMsg });
|
const result = serverReducer(state, { type: Types.USER_MESSAGE, messageData: newMsg });
|
||||||
expect(result.messages['Alice']).toHaveLength(2);
|
expect(result.messages['Alice']).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
@ -442,8 +475,12 @@ describe('Deck Storage', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('DECK_UPLOAD with nested path → inserts into matching subfolder', () => {
|
it('DECK_UPLOAD with nested path → inserts into matching subfolder', () => {
|
||||||
const subfolder = { id: 0, name: 'myDecks', file: null, folder: { items: [] } };
|
const subfolder = create(ServerInfo_DeckStorage_TreeItemSchema, {
|
||||||
const state = makeServerState({ backendDecks: { root: { items: [subfolder] } } });
|
id: 0, name: 'myDecks', folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [] })
|
||||||
|
});
|
||||||
|
const state = makeServerState({
|
||||||
|
backendDecks: makeDeckList({ root: create(ServerInfo_DeckStorage_FolderSchema, { items: [subfolder] }) })
|
||||||
|
});
|
||||||
const item = makeDeckTreeItem({ name: 'new.cod' });
|
const item = makeDeckTreeItem({ name: 'new.cod' });
|
||||||
const result = serverReducer(state, { type: Types.DECK_UPLOAD, path: 'myDecks', treeItem: item });
|
const result = serverReducer(state, { type: Types.DECK_UPLOAD, path: 'myDecks', treeItem: item });
|
||||||
const folder = result.backendDecks.root.items.find(i => i.name === 'myDecks');
|
const folder = result.backendDecks.root.items.find(i => i.name === 'myDecks');
|
||||||
|
|
@ -468,15 +505,19 @@ describe('Deck Storage', () => {
|
||||||
|
|
||||||
it('DECK_DELETE → removes item by id from tree', () => {
|
it('DECK_DELETE → removes item by id from tree', () => {
|
||||||
const item = makeDeckTreeItem({ id: 7 });
|
const item = makeDeckTreeItem({ id: 7 });
|
||||||
const state = makeServerState({ backendDecks: { root: { items: [item] } } });
|
const state = makeServerState({ backendDecks: makeDeckList({ root: create(ServerInfo_DeckStorage_FolderSchema, { items: [item] }) }) });
|
||||||
const result = serverReducer(state, { type: Types.DECK_DELETE, deckId: 7 });
|
const result = serverReducer(state, { type: Types.DECK_DELETE, deckId: 7 });
|
||||||
expect(result.backendDecks.root.items).toHaveLength(0);
|
expect(result.backendDecks.root.items).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('DECK_DELETE → recursively removes item nested inside a subfolder', () => {
|
it('DECK_DELETE → recursively removes item nested inside a subfolder', () => {
|
||||||
const nested = makeDeckTreeItem({ id: 9, name: 'nested.cod' });
|
const nested = makeDeckTreeItem({ id: 9, name: 'nested.cod' });
|
||||||
const subfolder = { id: 0, name: 'sub', file: null, folder: { items: [nested] } };
|
const subfolder = create(ServerInfo_DeckStorage_TreeItemSchema, {
|
||||||
const state = makeServerState({ backendDecks: { root: { items: [subfolder] } } });
|
id: 0, name: 'sub', folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [nested] })
|
||||||
|
});
|
||||||
|
const state = makeServerState({
|
||||||
|
backendDecks: makeDeckList({ root: create(ServerInfo_DeckStorage_FolderSchema, { items: [subfolder] }) })
|
||||||
|
});
|
||||||
const result = serverReducer(state, { type: Types.DECK_DELETE, deckId: 9 });
|
const result = serverReducer(state, { type: Types.DECK_DELETE, deckId: 9 });
|
||||||
expect(result.backendDecks.root.items[0].folder.items).toHaveLength(0);
|
expect(result.backendDecks.root.items[0].folder.items).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
@ -492,12 +533,16 @@ describe('Deck Storage', () => {
|
||||||
const result = serverReducer(state, { type: Types.DECK_NEW_DIR, path: '', dirName: 'myDir' });
|
const result = serverReducer(state, { type: Types.DECK_NEW_DIR, path: '', dirName: 'myDir' });
|
||||||
expect(result.backendDecks.root.items).toHaveLength(1);
|
expect(result.backendDecks.root.items).toHaveLength(1);
|
||||||
expect(result.backendDecks.root.items[0].name).toBe('myDir');
|
expect(result.backendDecks.root.items[0].name).toBe('myDir');
|
||||||
expect(result.backendDecks.root.items[0].folder).toEqual({ items: [] });
|
expect(result.backendDecks.root.items[0].folder.items).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('DECK_NEW_DIR nested → inserts folder inside matching subfolder', () => {
|
it('DECK_NEW_DIR nested → inserts folder inside matching subfolder', () => {
|
||||||
const subfolder = { id: 0, name: 'parent', file: null, folder: { items: [] } };
|
const subfolder = create(ServerInfo_DeckStorage_TreeItemSchema, {
|
||||||
const state = makeServerState({ backendDecks: { root: { items: [subfolder] } } });
|
id: 0, name: 'parent', folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [] })
|
||||||
|
});
|
||||||
|
const state = makeServerState({
|
||||||
|
backendDecks: makeDeckList({ root: create(ServerInfo_DeckStorage_FolderSchema, { items: [subfolder] }) })
|
||||||
|
});
|
||||||
const result = serverReducer(state, { type: Types.DECK_NEW_DIR, path: 'parent', dirName: 'child' });
|
const result = serverReducer(state, { type: Types.DECK_NEW_DIR, path: 'parent', dirName: 'child' });
|
||||||
const parent = result.backendDecks.root.items.find(i => i.name === 'parent');
|
const parent = result.backendDecks.root.items.find(i => i.name === 'parent');
|
||||||
expect(parent.folder.items).toHaveLength(1);
|
expect(parent.folder.items).toHaveLength(1);
|
||||||
|
|
@ -511,23 +556,37 @@ describe('Deck Storage', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('DECK_DEL_DIR → removes folder from root by name', () => {
|
it('DECK_DEL_DIR → removes folder from root by name', () => {
|
||||||
const subfolder = { id: 0, name: 'myDir', file: null, folder: { items: [] } };
|
const subfolder = create(ServerInfo_DeckStorage_TreeItemSchema, {
|
||||||
const state = makeServerState({ backendDecks: { root: { items: [subfolder] } } });
|
id: 0, name: 'myDir', folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [] })
|
||||||
|
});
|
||||||
|
const state = makeServerState({
|
||||||
|
backendDecks: makeDeckList({ root: create(ServerInfo_DeckStorage_FolderSchema, { items: [subfolder] }) })
|
||||||
|
});
|
||||||
const result = serverReducer(state, { type: Types.DECK_DEL_DIR, path: 'myDir' });
|
const result = serverReducer(state, { type: Types.DECK_DEL_DIR, path: 'myDir' });
|
||||||
expect(result.backendDecks.root.items).toHaveLength(0);
|
expect(result.backendDecks.root.items).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('DECK_DEL_DIR → returns deck tree unchanged when path is empty', () => {
|
it('DECK_DEL_DIR → returns deck tree unchanged when path is empty', () => {
|
||||||
const subfolder = { id: 0, name: 'keep', file: null, folder: { items: [] } };
|
const subfolder = create(ServerInfo_DeckStorage_TreeItemSchema, {
|
||||||
const state = makeServerState({ backendDecks: { root: { items: [subfolder] } } });
|
id: 0, name: 'keep', folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [] })
|
||||||
|
});
|
||||||
|
const state = makeServerState({
|
||||||
|
backendDecks: makeDeckList({ root: create(ServerInfo_DeckStorage_FolderSchema, { items: [subfolder] }) })
|
||||||
|
});
|
||||||
const result = serverReducer(state, { type: Types.DECK_DEL_DIR, path: '' });
|
const result = serverReducer(state, { type: Types.DECK_DEL_DIR, path: '' });
|
||||||
expect(result.backendDecks.root.items).toHaveLength(1);
|
expect(result.backendDecks.root.items).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('DECK_DEL_DIR → recursively removes nested subfolder via multi-segment path', () => {
|
it('DECK_DEL_DIR → recursively removes nested subfolder via multi-segment path', () => {
|
||||||
const child = { id: 0, name: 'child', file: null, folder: { items: [] } };
|
const child = create(ServerInfo_DeckStorage_TreeItemSchema, {
|
||||||
const parent = { id: 0, name: 'parent', file: null, folder: { items: [child] } };
|
id: 0, name: 'child', folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [] })
|
||||||
const state = makeServerState({ backendDecks: { root: { items: [parent] } } });
|
});
|
||||||
|
const parent = create(ServerInfo_DeckStorage_TreeItemSchema, {
|
||||||
|
id: 0, name: 'parent', folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [child] })
|
||||||
|
});
|
||||||
|
const state = makeServerState({
|
||||||
|
backendDecks: makeDeckList({ root: create(ServerInfo_DeckStorage_FolderSchema, { items: [parent] }) })
|
||||||
|
});
|
||||||
const result = serverReducer(state, { type: Types.DECK_DEL_DIR, path: 'parent/child' });
|
const result = serverReducer(state, { type: Types.DECK_DEL_DIR, path: 'parent/child' });
|
||||||
expect(result.backendDecks.root.items[0].folder.items).toHaveLength(0);
|
expect(result.backendDecks.root.items[0].folder.items).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
@ -536,25 +595,25 @@ describe('Deck Storage', () => {
|
||||||
// ── GAMES_OF_USER ─────────────────────────────────────────────────────────────
|
// ── GAMES_OF_USER ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
describe('GAMES_OF_USER', () => {
|
describe('GAMES_OF_USER', () => {
|
||||||
it('stores games keyed by userName', () => {
|
it('stores normalized games keyed by userName', () => {
|
||||||
const games = [{ gameId: 5, roomId: 1 }] as any;
|
const games = [makeGame({ gameId: 5 })];
|
||||||
const state = makeServerState();
|
const state = makeServerState();
|
||||||
const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', games });
|
const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', games, gametypeMap: {} });
|
||||||
expect(result.gamesOfUser['alice']).toBe(games);
|
expect(result.gamesOfUser['alice']).toEqual(games);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('overwrites previous games for same user', () => {
|
it('overwrites previous games for same user', () => {
|
||||||
const old = [{ gameId: 1 }] as any;
|
const old = [makeGame({ gameId: 1 })];
|
||||||
const fresh = [{ gameId: 2 }] as any;
|
const fresh = [makeGame({ gameId: 2 })];
|
||||||
const state = makeServerState({ gamesOfUser: { alice: old } });
|
const state = makeServerState({ gamesOfUser: { alice: old } });
|
||||||
const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', games: fresh });
|
const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', games: fresh, gametypeMap: {} });
|
||||||
expect(result.gamesOfUser['alice']).toBe(fresh);
|
expect(result.gamesOfUser['alice']).toEqual(fresh);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not affect other users\' entries', () => {
|
it('does not affect other users\' entries', () => {
|
||||||
const bobGames = [{ gameId: 3 }] as any;
|
const bobGames = [makeGame({ gameId: 3 })];
|
||||||
const state = makeServerState({ gamesOfUser: { bob: bobGames } });
|
const state = makeServerState({ gamesOfUser: { bob: bobGames } });
|
||||||
const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', games: [] });
|
const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', games: [], gametypeMap: {} });
|
||||||
expect(result.gamesOfUser['bob']).toBe(bobGames);
|
expect(result.gamesOfUser['bob']).toBe(bobGames);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
import { DeckStorageFolder, DeckStorageTreeItem, SortDirection, StatusEnum, UserLevelFlag, UserSortField } from 'types';
|
import { DeckStorageFolder, DeckStorageTreeItem, SortDirection, StatusEnum, UserLevelFlag, UserSortField } from 'types';
|
||||||
|
import { create } from '@bufbuild/protobuf';
|
||||||
|
import { Response_DeckListSchema } from 'generated/proto/response_deck_list_pb';
|
||||||
|
import { ServerInfo_DeckStorage_FolderSchema, ServerInfo_DeckStorage_TreeItemSchema } from 'generated/proto/serverinfo_deckstorage_pb';
|
||||||
|
|
||||||
import { SortUtil } from '../common';
|
import { normalizeBannedUserError, normalizeGameObject, normalizeLogs, SortUtil } from '../common';
|
||||||
|
|
||||||
|
import { ServerAction } from './server.actions';
|
||||||
import { ServerState } from './server.interfaces'
|
import { ServerState } from './server.interfaces'
|
||||||
import { Types } from './server.types';
|
import { Types } from './server.types';
|
||||||
|
|
||||||
|
|
@ -11,31 +15,33 @@ function splitPath(path: string): string[] {
|
||||||
|
|
||||||
function insertAtPath(folder: DeckStorageFolder, pathSegments: string[], item: DeckStorageTreeItem): DeckStorageFolder {
|
function insertAtPath(folder: DeckStorageFolder, pathSegments: string[], item: DeckStorageTreeItem): DeckStorageFolder {
|
||||||
if (pathSegments.length === 0 || (pathSegments.length === 1 && pathSegments[0] === '')) {
|
if (pathSegments.length === 0 || (pathSegments.length === 1 && pathSegments[0] === '')) {
|
||||||
return { items: [...folder.items, item] };
|
return create(ServerInfo_DeckStorage_FolderSchema, { items: [...folder.items, item] });
|
||||||
}
|
}
|
||||||
const [head, ...tail] = pathSegments;
|
const [head, ...tail] = pathSegments;
|
||||||
const match = folder.items.find(child => child.name === head && child.folder);
|
const match = folder.items.find(child => child.name === head && child.folder);
|
||||||
if (match) {
|
if (match) {
|
||||||
return {
|
return create(ServerInfo_DeckStorage_FolderSchema, {
|
||||||
items: folder.items.map(child =>
|
items: folder.items.map(child =>
|
||||||
child === match
|
child === match
|
||||||
? { ...child, folder: insertAtPath(child.folder!, tail, item) }
|
? { ...child, folder: insertAtPath(child.folder!, tail, item) }
|
||||||
: child
|
: child
|
||||||
),
|
),
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
const created: DeckStorageTreeItem = { id: 0, name: head, file: null, folder: insertAtPath({ items: [] }, tail, item) };
|
const created: DeckStorageTreeItem = create(ServerInfo_DeckStorage_TreeItemSchema, {
|
||||||
return { items: [...folder.items, created] };
|
id: 0, name: head, folder: insertAtPath(create(ServerInfo_DeckStorage_FolderSchema, { items: [] }), tail, item)
|
||||||
|
});
|
||||||
|
return create(ServerInfo_DeckStorage_FolderSchema, { items: [...folder.items, created] });
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeById(folder: DeckStorageFolder, id: number): DeckStorageFolder {
|
function removeById(folder: DeckStorageFolder, id: number): DeckStorageFolder {
|
||||||
return {
|
return create(ServerInfo_DeckStorage_FolderSchema, {
|
||||||
items: folder.items
|
items: folder.items
|
||||||
.filter(item => item.id !== id)
|
.filter(item => item.id !== id)
|
||||||
.map(item =>
|
.map(item =>
|
||||||
item.folder ? { ...item, folder: removeById(item.folder, id) } : item
|
item.folder ? { ...item, folder: removeById(item.folder, id) } : item
|
||||||
),
|
),
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeByPath(folder: DeckStorageFolder, pathSegments: string[]): DeckStorageFolder {
|
function removeByPath(folder: DeckStorageFolder, pathSegments: string[]): DeckStorageFolder {
|
||||||
|
|
@ -44,15 +50,17 @@ function removeByPath(folder: DeckStorageFolder, pathSegments: string[]): DeckSt
|
||||||
}
|
}
|
||||||
const [head, ...tail] = pathSegments;
|
const [head, ...tail] = pathSegments;
|
||||||
if (tail.length === 0) {
|
if (tail.length === 0) {
|
||||||
return { items: folder.items.filter(item => !(item.name === head && item.folder !== null)) };
|
return create(ServerInfo_DeckStorage_FolderSchema, {
|
||||||
|
items: folder.items.filter(item => !(item.name === head && item.folder != null))
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return {
|
return create(ServerInfo_DeckStorage_FolderSchema, {
|
||||||
items: folder.items.map(item =>
|
items: folder.items.map(item =>
|
||||||
item.name === head && item.folder
|
item.name === head && item.folder
|
||||||
? { ...item, folder: removeByPath(item.folder, tail) }
|
? { ...item, folder: removeByPath(item.folder, tail) }
|
||||||
: item
|
: item
|
||||||
),
|
),
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: ServerState = {
|
const initialState: ServerState = {
|
||||||
|
|
@ -93,9 +101,10 @@ const initialState: ServerState = {
|
||||||
replays: [],
|
replays: [],
|
||||||
backendDecks: null,
|
backendDecks: null,
|
||||||
gamesOfUser: {},
|
gamesOfUser: {},
|
||||||
|
registrationError: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const serverReducer = (state = initialState, action: any) => {
|
export const serverReducer = (state = initialState, action: ServerAction) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case Types.INITIALIZED: {
|
case Types.INITIALIZED: {
|
||||||
return {
|
return {
|
||||||
|
|
@ -271,7 +280,7 @@ export const serverReducer = (state = initialState, action: any) => {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
logs: {
|
logs: {
|
||||||
...logs
|
...normalizeLogs(logs)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -424,60 +433,96 @@ export const serverReducer = (state = initialState, action: any) => {
|
||||||
return { ...state, backendDecks: action.deckList };
|
return { ...state, backendDecks: action.deckList };
|
||||||
}
|
}
|
||||||
case Types.DECK_UPLOAD: {
|
case Types.DECK_UPLOAD: {
|
||||||
if (!state.backendDecks) {
|
if (!state.backendDecks?.root) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
backendDecks: {
|
backendDecks: create(Response_DeckListSchema, {
|
||||||
root: insertAtPath(state.backendDecks.root, splitPath(action.path), action.treeItem),
|
root: insertAtPath(state.backendDecks.root, splitPath(action.path), action.treeItem),
|
||||||
},
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case Types.DECK_DELETE: {
|
case Types.DECK_DELETE: {
|
||||||
if (!state.backendDecks) {
|
if (!state.backendDecks?.root) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
backendDecks: {
|
backendDecks: create(Response_DeckListSchema, {
|
||||||
root: removeById(state.backendDecks.root, action.deckId),
|
root: removeById(state.backendDecks.root, action.deckId),
|
||||||
},
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case Types.DECK_NEW_DIR: {
|
case Types.DECK_NEW_DIR: {
|
||||||
if (!state.backendDecks) {
|
if (!state.backendDecks?.root) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
const newFolder: DeckStorageTreeItem = { id: 0, name: action.dirName, file: null, folder: { items: [] } };
|
const newFolder: DeckStorageTreeItem = create(ServerInfo_DeckStorage_TreeItemSchema, {
|
||||||
|
id: 0, name: action.dirName, folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [] })
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
backendDecks: {
|
backendDecks: create(Response_DeckListSchema, {
|
||||||
root: insertAtPath(state.backendDecks.root, splitPath(action.path), newFolder),
|
root: insertAtPath(state.backendDecks.root, splitPath(action.path), newFolder),
|
||||||
},
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case Types.DECK_DEL_DIR: {
|
case Types.DECK_DEL_DIR: {
|
||||||
if (!state.backendDecks) {
|
if (!state.backendDecks?.root) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
backendDecks: {
|
backendDecks: create(Response_DeckListSchema, {
|
||||||
root: removeByPath(state.backendDecks.root, splitPath(action.path)),
|
root: removeByPath(state.backendDecks.root, splitPath(action.path)),
|
||||||
},
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case Types.GAMES_OF_USER: {
|
case Types.GAMES_OF_USER: {
|
||||||
const { userName, games } = action;
|
const { userName, games, gametypeMap } = action;
|
||||||
|
const normalizedGames = games.map(g => normalizeGameObject(g, gametypeMap));
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
gamesOfUser: {
|
gamesOfUser: {
|
||||||
...state.gamesOfUser,
|
...state.gamesOfUser,
|
||||||
[userName]: games,
|
[userName]: normalizedGames,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case Types.REGISTRATION_FAILED: {
|
||||||
|
const error = action.endTime
|
||||||
|
? normalizeBannedUserError(action.reason, action.endTime)
|
||||||
|
: action.reason;
|
||||||
|
return { ...state, registrationError: error };
|
||||||
|
}
|
||||||
|
case Types.CLEAR_REGISTRATION_ERRORS:
|
||||||
|
return { ...state, registrationError: null };
|
||||||
|
// Signal-only action types — no state mutation, explicit for discriminated-union exhaustiveness
|
||||||
|
case Types.LOGIN_SUCCESSFUL:
|
||||||
|
case Types.LOGIN_FAILED:
|
||||||
|
case Types.CONNECTION_CLOSED:
|
||||||
|
case Types.CONNECTION_FAILED:
|
||||||
|
case Types.TEST_CONNECTION_SUCCESSFUL:
|
||||||
|
case Types.TEST_CONNECTION_FAILED:
|
||||||
|
case Types.REGISTRATION_REQUIRES_EMAIL:
|
||||||
|
case Types.REGISTRATION_SUCCESS:
|
||||||
|
case Types.REGISTRATION_EMAIL_ERROR:
|
||||||
|
case Types.REGISTRATION_PASSWORD_ERROR:
|
||||||
|
case Types.REGISTRATION_USERNAME_ERROR:
|
||||||
|
case Types.RESET_PASSWORD_REQUESTED:
|
||||||
|
case Types.RESET_PASSWORD_FAILED:
|
||||||
|
case Types.RESET_PASSWORD_CHALLENGE:
|
||||||
|
case Types.RESET_PASSWORD_SUCCESS:
|
||||||
|
case Types.RELOAD_CONFIG:
|
||||||
|
case Types.SHUTDOWN_SERVER:
|
||||||
|
case Types.UPDATE_SERVER_MESSAGE:
|
||||||
|
case Types.ACCOUNT_PASSWORD_CHANGE:
|
||||||
|
case Types.ADD_TO_LIST:
|
||||||
|
case Types.REMOVE_FROM_LIST:
|
||||||
|
case Types.GRANT_REPLAY_ACCESS:
|
||||||
|
case Types.FORCE_ACTIVATE_USER:
|
||||||
|
return state;
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,4 +18,5 @@ export const Selectors = {
|
||||||
getIgnoreList: ({ server }: State) => server.ignoreList,
|
getIgnoreList: ({ server }: State) => server.ignoreList,
|
||||||
getReplays: ({ server }: State) => server.replays,
|
getReplays: ({ server }: State) => server.replays,
|
||||||
getBackendDecks: ({ server }: State) => server.backendDecks,
|
getBackendDecks: ({ server }: State) => server.backendDecks,
|
||||||
|
getRegistrationError: ({ server }: State) => server.registrationError,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ export const Types = {
|
||||||
REGISTRATION_EMAIL_ERROR: '[Server] Registration Email Error',
|
REGISTRATION_EMAIL_ERROR: '[Server] Registration Email Error',
|
||||||
REGISTRATION_PASSWORD_ERROR: '[Server] Registration Password Error',
|
REGISTRATION_PASSWORD_ERROR: '[Server] Registration Password Error',
|
||||||
REGISTRATION_USERNAME_ERROR: '[Server] Registration Username Error',
|
REGISTRATION_USERNAME_ERROR: '[Server] Registration Username Error',
|
||||||
|
CLEAR_REGISTRATION_ERRORS: '[Server] Clear Registration Errors',
|
||||||
ACCOUNT_AWAITING_ACTIVATION: '[Server] Account Awaiting Activation',
|
ACCOUNT_AWAITING_ACTIVATION: '[Server] Account Awaiting Activation',
|
||||||
ACCOUNT_ACTIVATION_SUCCESS: '[Server] Account Activation Success',
|
ACCOUNT_ACTIVATION_SUCCESS: '[Server] Account Activation Success',
|
||||||
ACCOUNT_ACTIVATION_FAILED: '[Server] Account Activation Failed',
|
ACCOUNT_ACTIVATION_FAILED: '[Server] Account Activation Failed',
|
||||||
|
|
@ -70,4 +71,4 @@ export const Types = {
|
||||||
DECK_DELETE: '[Server] Deck Delete',
|
DECK_DELETE: '[Server] Deck Delete',
|
||||||
// User games
|
// User games
|
||||||
GAMES_OF_USER: '[Server] Games Of User',
|
GAMES_OF_USER: '[Server] Games Of User',
|
||||||
};
|
} as const;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,27 @@
|
||||||
import { createStore, applyMiddleware } from 'redux';
|
import { configureStore, isPlain } from '@reduxjs/toolkit';
|
||||||
import thunk from 'redux-thunk';
|
import { isMessage } from '@bufbuild/protobuf';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import rootReducer from './rootReducer';
|
import rootReducer from './rootReducer';
|
||||||
|
|
||||||
const initialState = {};
|
// Protobuf-es v2 messages are already plain objects (no class prototype, unlike v1).
|
||||||
|
// They carry $typeName (string, identifies the message) and $unknown (binary unknown
|
||||||
|
// fields) — both are serializable and harmless in Redux state. No conversion needed.
|
||||||
|
// Fields may include Uint8Array (bytes) and BigInt (int64/uint64), which fail Redux
|
||||||
|
// Toolkit’s default serializable check, so we extend it to accept these types.
|
||||||
|
function isSerializable(value: unknown): boolean {
|
||||||
|
return isPlain(value) || isMessage(value) || value instanceof Uint8Array || typeof value === 'bigint';
|
||||||
|
}
|
||||||
|
|
||||||
const middleware: any = [thunk];
|
export const store = configureStore({
|
||||||
|
reducer: rootReducer,
|
||||||
|
middleware: (getDefaultMiddleware) =>
|
||||||
|
getDefaultMiddleware({
|
||||||
|
immutableCheck: { warnAfter: 128 },
|
||||||
|
serializableCheck: { isSerializable, warnAfter: 128 },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
export type RootState = ReturnType<typeof store.getState>;
|
||||||
|
export type AppDispatch = typeof store.dispatch;
|
||||||
|
|
||||||
export const store = createStore(rootReducer, initialState, applyMiddleware(...middleware));
|
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
|
||||||
|
export const useAppSelector = useSelector.withTypes<RootState>();
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@ import {
|
||||||
URL_REGEX,
|
URL_REGEX,
|
||||||
MESSAGE_SENDER_REGEX,
|
MESSAGE_SENDER_REGEX,
|
||||||
MENTION_REGEX,
|
MENTION_REGEX,
|
||||||
CARD_CALLOUT_REGEX,
|
|
||||||
CALLOUT_BOUNDARY_REGEX,
|
|
||||||
} from './constants';
|
} from './constants';
|
||||||
|
|
||||||
describe('RegEx', () => {
|
describe('RegEx', () => {
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,9 @@
|
||||||
export interface DeckList {
|
import type {
|
||||||
root: DeckStorageFolder;
|
ServerInfo_DeckStorage_File, ServerInfo_DeckStorage_Folder, ServerInfo_DeckStorage_TreeItem
|
||||||
}
|
} from 'generated/proto/serverinfo_deckstorage_pb';
|
||||||
|
import type { Response_DeckList } from 'generated/proto/response_deck_list_pb';
|
||||||
|
|
||||||
export interface DeckStorageFolder {
|
export type DeckList = Response_DeckList;
|
||||||
items: DeckStorageTreeItem[];
|
export type DeckStorageFolder = ServerInfo_DeckStorage_Folder;
|
||||||
}
|
export type DeckStorageFile = ServerInfo_DeckStorage_File;
|
||||||
|
export type DeckStorageTreeItem = ServerInfo_DeckStorage_TreeItem;
|
||||||
export interface DeckStorageFile {
|
|
||||||
creationTime: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DeckStorageTreeItem {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
file: DeckStorageFile | null;
|
|
||||||
folder: DeckStorageFolder | null;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
export enum FormKey {
|
|
||||||
ADD_TO_BUDDIES = 'ADD_TO_BUDDIES',
|
|
||||||
ADD_TO_IGNORE = 'ADD_TO_IGNORE',
|
|
||||||
CARD_IMPORT = 'CARD_IMPORT',
|
|
||||||
CONNECT = 'CONNECT',
|
|
||||||
LOGIN = 'LOGIN',
|
|
||||||
RESET_PASSWORD_REQUEST = 'RESET_PASSWORD_REQUEST',
|
|
||||||
RESET_PASSWORD = 'RESET_PASSWORD',
|
|
||||||
REGISTER = 'REGISTER',
|
|
||||||
SEARCH_LOGS = 'SEARCH_LOGS',
|
|
||||||
}
|
|
||||||
|
|
@ -67,6 +67,17 @@ import type { ServerInfo_Player } from 'generated/proto/serverinfo_player_pb';
|
||||||
export { CardAttribute } from 'generated/proto/card_attributes_pb';
|
export { CardAttribute } from 'generated/proto/card_attributes_pb';
|
||||||
export { ServerInfo_Zone_ZoneType as ZoneType } from 'generated/proto/serverinfo_zone_pb';
|
export { ServerInfo_Zone_ZoneType as ZoneType } from 'generated/proto/serverinfo_zone_pb';
|
||||||
|
|
||||||
|
// ── Proto utility types ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init shape for constructing protobuf messages via create().
|
||||||
|
* Strips $typeName and $unknown branding, making all fields optional.
|
||||||
|
* Use for function parameters that feed into create().
|
||||||
|
*/
|
||||||
|
export type ProtoInit<T> = {
|
||||||
|
[K in keyof T as K extends '$typeName' | '$unknown' ? never : K]?: T[K];
|
||||||
|
};
|
||||||
|
|
||||||
// ── UI types (not proto mirrors) ──────────────────────────────────────────────
|
// ── UI types (not proto mirrors) ──────────────────────────────────────────────
|
||||||
|
|
||||||
export type Game = ServerInfo_Game & {
|
export type Game = ServerInfo_Game & {
|
||||||
|
|
@ -165,34 +176,36 @@ export interface GameEventMeta {
|
||||||
forcedByJudge: number;
|
forcedByJudge: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Type aliases for generated command param types ────────────────────────────
|
// ── Type aliases for generated command param types (init shapes) ──────────────
|
||||||
|
// These use ProtoInit<> because callers construct plain objects;
|
||||||
|
// the command functions internally call create(Schema, params).
|
||||||
|
|
||||||
export type { CardToMove };
|
export type { CardToMove };
|
||||||
export type MoveCardParams = Command_MoveCard;
|
export type MoveCardParams = ProtoInit<Command_MoveCard>;
|
||||||
export type DrawCardsParams = Command_DrawCards;
|
export type DrawCardsParams = ProtoInit<Command_DrawCards>;
|
||||||
export type RollDieParams = Command_RollDie;
|
export type RollDieParams = ProtoInit<Command_RollDie>;
|
||||||
export type ShuffleParams = Command_Shuffle;
|
export type ShuffleParams = ProtoInit<Command_Shuffle>;
|
||||||
export type FlipCardParams = Command_FlipCard;
|
export type FlipCardParams = ProtoInit<Command_FlipCard>;
|
||||||
export type AttachCardParams = Command_AttachCard;
|
export type AttachCardParams = ProtoInit<Command_AttachCard>;
|
||||||
export type CreateTokenParams = Command_CreateToken;
|
export type CreateTokenParams = ProtoInit<Command_CreateToken>;
|
||||||
export type SetCardAttrParams = Command_SetCardAttr;
|
export type SetCardAttrParams = ProtoInit<Command_SetCardAttr>;
|
||||||
export type SetCardCounterParams = Command_SetCardCounter;
|
export type SetCardCounterParams = ProtoInit<Command_SetCardCounter>;
|
||||||
export type IncCardCounterParams = Command_IncCardCounter;
|
export type IncCardCounterParams = ProtoInit<Command_IncCardCounter>;
|
||||||
export type RevealCardsParams = Command_RevealCards;
|
export type RevealCardsParams = ProtoInit<Command_RevealCards>;
|
||||||
export type DumpZoneParams = Command_DumpZone;
|
export type DumpZoneParams = ProtoInit<Command_DumpZone>;
|
||||||
export type ChangeZonePropertiesParams = Command_ChangeZoneProperties;
|
export type ChangeZonePropertiesParams = ProtoInit<Command_ChangeZoneProperties>;
|
||||||
export type CreateArrowParams = Command_CreateArrow;
|
export type CreateArrowParams = ProtoInit<Command_CreateArrow>;
|
||||||
export type DeleteArrowParams = Command_DeleteArrow;
|
export type DeleteArrowParams = ProtoInit<Command_DeleteArrow>;
|
||||||
export type CreateCounterParams = Command_CreateCounter;
|
export type CreateCounterParams = ProtoInit<Command_CreateCounter>;
|
||||||
export type SetCounterParams = Command_SetCounter;
|
export type SetCounterParams = ProtoInit<Command_SetCounter>;
|
||||||
export type IncCounterParams = Command_IncCounter;
|
export type IncCounterParams = ProtoInit<Command_IncCounter>;
|
||||||
export type DelCounterParams = Command_DelCounter;
|
export type DelCounterParams = ProtoInit<Command_DelCounter>;
|
||||||
export type KickFromGameParams = Command_KickFromGame;
|
export type KickFromGameParams = ProtoInit<Command_KickFromGame>;
|
||||||
export type ReadyStartParams = Command_ReadyStart;
|
export type ReadyStartParams = ProtoInit<Command_ReadyStart>;
|
||||||
export type MulliganParams = Command_Mulligan;
|
export type MulliganParams = ProtoInit<Command_Mulligan>;
|
||||||
export type DeckSelectParams = Command_DeckSelect;
|
export type DeckSelectParams = ProtoInit<Command_DeckSelect>;
|
||||||
export type MoveCardToZone = MoveCard_ToZone;
|
export type MoveCardToZone = MoveCard_ToZone;
|
||||||
export type SetSideboardPlanParams = Command_SetSideboardPlan;
|
export type SetSideboardPlanParams = ProtoInit<Command_SetSideboardPlan>;
|
||||||
export type SetSideboardLockParams = Command_SetSideboardLock;
|
export type SetSideboardLockParams = ProtoInit<Command_SetSideboardLock>;
|
||||||
export type SetActivePhaseParams = Command_SetActivePhase;
|
export type SetActivePhaseParams = ProtoInit<Command_SetActivePhase>;
|
||||||
export type GameSayParams = Command_GameSay;
|
export type GameSayParams = ProtoInit<Command_GameSay>;
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,6 @@ export * from './server';
|
||||||
export * from './sort';
|
export * from './sort';
|
||||||
export * from './user';
|
export * from './user';
|
||||||
export * from './routes';
|
export * from './routes';
|
||||||
export * from './sort';
|
|
||||||
export * from './forms';
|
|
||||||
export * from './message';
|
export * from './message';
|
||||||
export * from './settings';
|
export * from './settings';
|
||||||
export * from './languages';
|
export * from './languages';
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,7 @@
|
||||||
export interface BanHistoryItem {
|
import type { ServerInfo_Ban } from 'generated/proto/serverinfo_ban_pb';
|
||||||
adminId: string;
|
import type { ServerInfo_Warning } from 'generated/proto/serverinfo_warning_pb';
|
||||||
adminName: string;
|
import type { Response_WarnList } from 'generated/proto/response_warn_list_pb';
|
||||||
banTime: string;
|
|
||||||
banLength: string;
|
|
||||||
banReason: string;
|
|
||||||
visibleReason: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WarnHistoryItem {
|
export type BanHistoryItem = ServerInfo_Ban;
|
||||||
userName: string;
|
export type WarnHistoryItem = ServerInfo_Warning;
|
||||||
adminName: string;
|
export type WarnListItem = Response_WarnList;
|
||||||
reason: string;
|
|
||||||
timeOf: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WarnListItem {
|
|
||||||
warning: string;
|
|
||||||
userName: string;
|
|
||||||
userClientid: string;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import type { ServerInfo_Room } from 'generated/proto/serverinfo_room_pb';
|
import type { ServerInfo_Room } from 'generated/proto/serverinfo_room_pb';
|
||||||
|
import type { Game } from './game';
|
||||||
|
|
||||||
export interface GametypeMap { [index: number]: string }
|
export interface GametypeMap { [index: number]: string }
|
||||||
|
|
||||||
export type Room = ServerInfo_Room & {
|
export type Room = ServerInfo_Room & {
|
||||||
gametypeMap: GametypeMap;
|
gametypeMap: GametypeMap;
|
||||||
|
gameList: Game[];
|
||||||
order: number;
|
order: number;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import type { ServerInfo_ChatMessage } from 'generated/proto/serverinfo_chat_message_pb';
|
||||||
|
|
||||||
export interface ServerStatus {
|
export interface ServerStatus {
|
||||||
status: StatusEnum;
|
status: StatusEnum;
|
||||||
description: string;
|
description: string;
|
||||||
|
|
@ -109,16 +111,7 @@ export const KnownHosts = {
|
||||||
[KnownHost.TETRARCH]: { port: 443, host: 'mtg.tetrarch.co/servatrice' },
|
[KnownHost.TETRARCH]: { port: 443, host: 'mtg.tetrarch.co/servatrice' },
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LogItem {
|
export type LogItem = ServerInfo_ChatMessage;
|
||||||
message: string;
|
|
||||||
senderId: string;
|
|
||||||
senderIp: string;
|
|
||||||
senderName: string;
|
|
||||||
targetId: string;
|
|
||||||
targetName: string;
|
|
||||||
targetType: string;
|
|
||||||
time: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LogGroups {
|
export interface LogGroups {
|
||||||
room: LogItem[];
|
room: LogItem[];
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,22 @@
|
||||||
vi.mock('./services/WebSocketService', () => ({
|
vi.mock('./services/WebSocketService', () => ({
|
||||||
WebSocketService: vi.fn().mockImplementation(() => ({
|
WebSocketService: vi.fn().mockImplementation(function WebSocketServiceImpl() {
|
||||||
message$: { subscribe: vi.fn() },
|
return {
|
||||||
connect: vi.fn(),
|
message$: { subscribe: vi.fn() },
|
||||||
testConnect: vi.fn(),
|
connect: vi.fn(),
|
||||||
disconnect: vi.fn(),
|
testConnect: vi.fn(),
|
||||||
})),
|
disconnect: vi.fn(),
|
||||||
|
};
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./services/ProtobufService', () => ({
|
vi.mock('./services/ProtobufService', () => ({
|
||||||
ProtobufService: vi.fn().mockImplementation(() => ({
|
ProtobufService: vi.fn().mockImplementation(function ProtobufServiceImpl() {
|
||||||
handleMessageEvent: vi.fn(),
|
return {
|
||||||
sendKeepAliveCommand: vi.fn(),
|
handleMessageEvent: vi.fn(),
|
||||||
resetCommands: vi.fn(),
|
sendKeepAliveCommand: vi.fn(),
|
||||||
})),
|
resetCommands: vi.fn(),
|
||||||
|
};
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./persistence', () => ({
|
vi.mock('./persistence', () => ({
|
||||||
|
|
@ -20,12 +24,17 @@ vi.mock('./persistence', () => ({
|
||||||
SessionPersistence: { clearStore: vi.fn(), initialized: vi.fn() },
|
SessionPersistence: { clearStore: vi.fn(), initialized: vi.fn() },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('store', () => ({
|
||||||
|
GameDispatch: { clearStore: vi.fn() },
|
||||||
|
}));
|
||||||
|
|
||||||
import { WebClient } from './WebClient';
|
import { WebClient } from './WebClient';
|
||||||
import { WebSocketService } from './services/WebSocketService';
|
import { WebSocketService } from './services/WebSocketService';
|
||||||
import { ProtobufService } from './services/ProtobufService';
|
import { ProtobufService } from './services/ProtobufService';
|
||||||
import { RoomPersistence, SessionPersistence } from './persistence';
|
import { RoomPersistence, SessionPersistence } from './persistence';
|
||||||
import { StatusEnum } from 'types';
|
import { StatusEnum } from 'types';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
|
import { Mock } from 'vitest';
|
||||||
|
|
||||||
describe('WebClient', () => {
|
describe('WebClient', () => {
|
||||||
let client: WebClient;
|
let client: WebClient;
|
||||||
|
|
@ -33,18 +42,22 @@ describe('WebClient', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
(ProtobufService as vi.Mock).mockImplementation(() => ({
|
(ProtobufService as Mock).mockImplementation(function ProtobufServiceImpl() {
|
||||||
handleMessageEvent: vi.fn(),
|
return {
|
||||||
sendKeepAliveCommand: vi.fn(),
|
handleMessageEvent: vi.fn(),
|
||||||
resetCommands: vi.fn(),
|
sendKeepAliveCommand: vi.fn(),
|
||||||
}));
|
resetCommands: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
messageSubject = new Subject<MessageEvent>();
|
messageSubject = new Subject<MessageEvent>();
|
||||||
(WebSocketService as vi.Mock).mockImplementation(() => ({
|
(WebSocketService as Mock).mockImplementation(function WebSocketServiceImpl() {
|
||||||
message$: messageSubject,
|
return {
|
||||||
connect: vi.fn(),
|
message$: messageSubject,
|
||||||
testConnect: vi.fn(),
|
connect: vi.fn(),
|
||||||
disconnect: vi.fn(),
|
testConnect: vi.fn(),
|
||||||
}));
|
disconnect: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
// suppress console.log from constructor in non-test-env check
|
// suppress console.log from constructor in non-test-env check
|
||||||
vi.spyOn(console, 'log').mockImplementation(() => {});
|
vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||||
client = new WebClient();
|
client = new WebClient();
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ export class WebClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public keepAlive(pingReceived: Function) {
|
public keepAlive(pingReceived: () => void) {
|
||||||
this.protobuf.sendKeepAliveCommand(pingReceived);
|
this.protobuf.sendKeepAliveCommand(pingReceived);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,9 @@
|
||||||
* Defaults to 2 (ext, value, options).
|
* Defaults to 2 (ext, value, options).
|
||||||
* Use 3 for sendRoomCommand (roomId, ext, value, options).
|
* Use 3 for sendRoomCommand (roomId, ext, value, options).
|
||||||
*/
|
*/
|
||||||
export function makeCallbackHelpers(mockFn: vi.Mock, optsArgIndex = 2) {
|
import { Mock } from 'vitest';
|
||||||
|
|
||||||
|
export function makeCallbackHelpers(mockFn: Mock, optsArgIndex = 2) {
|
||||||
function getLastSendOpts() {
|
function getLastSendOpts() {
|
||||||
const calls = mockFn.mock.calls;
|
const calls = mockFn.mock.calls;
|
||||||
return calls[calls.length - 1]?.[optsArgIndex];
|
return calls[calls.length - 1]?.[optsArgIndex];
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ export function makeMockWebSocketInstance() {
|
||||||
return {
|
return {
|
||||||
send: vi.fn(),
|
send: vi.fn(),
|
||||||
close: vi.fn(),
|
close: vi.fn(),
|
||||||
readyState: WebSocket.OPEN,
|
readyState: WebSocket.OPEN as number,
|
||||||
binaryType: '' as BinaryType,
|
binaryType: '' as BinaryType,
|
||||||
onopen: null as any,
|
onopen: null as any,
|
||||||
onclose: null as any,
|
onclose: null as any,
|
||||||
|
|
@ -18,12 +18,17 @@ export function makeMockWebSocketInstance() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Installs a mock WebSocket constructor on global. Returns the mock instance. */
|
/** Installs a mock WebSocket constructor on global. Returns the mock instance and a cleanup function. */
|
||||||
export function installMockWebSocket() {
|
export function installMockWebSocket() {
|
||||||
|
const originalWebSocket = (globalThis as any).WebSocket;
|
||||||
const mockInstance = makeMockWebSocketInstance();
|
const mockInstance = makeMockWebSocketInstance();
|
||||||
const MockWS = vi.fn(() => mockInstance) as any;
|
const MockWS = vi.fn(function MockWebSocket() {
|
||||||
|
return mockInstance;
|
||||||
|
}) as any;
|
||||||
MockWS.OPEN = 1;
|
MockWS.OPEN = 1;
|
||||||
MockWS.CLOSED = 3;
|
MockWS.CLOSED = 3;
|
||||||
(global as any).WebSocket = MockWS;
|
(globalThis as any).WebSocket = MockWS;
|
||||||
return { MockWS, mockInstance };
|
return { MockWS, mockInstance, restore: () => {
|
||||||
|
(globalThis as any).WebSocket = originalWebSocket;
|
||||||
|
} };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue