diff --git a/package-lock.json b/package-lock.json index d17b5642..d363af6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3137,9 +3137,10 @@ } }, "node_modules/@socket.io/component-emitter": { - "version": "3.1.0", - "dev": true, - "license": "MIT" + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "dev": true }, "node_modules/@tootallnate/once": { "version": "2.0.0", @@ -3214,13 +3215,15 @@ }, "node_modules/@types/cookie": { "version": "0.4.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "dev": true }, "node_modules/@types/cors": { - "version": "2.8.13", + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } @@ -4251,8 +4254,9 @@ }, "node_modules/base64id": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", "dev": true, - "license": "MIT", "engines": { "node": "^4.5.0 || >= 5.9" } @@ -4367,11 +4371,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, - "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -4861,8 +4866,9 @@ }, "node_modules/cookie": { "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -4938,8 +4944,9 @@ }, "node_modules/cors": { "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "dev": true, - "license": "MIT", "dependencies": { "object-assign": "^4", "vary": "^1" @@ -5902,9 +5909,10 @@ } }, "node_modules/engine.io": { - "version": "6.4.2", + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", + "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", "dev": true, - "license": "MIT", "dependencies": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", @@ -5914,31 +5922,33 @@ "cookie": "~0.4.1", "cors": "~2.8.5", "debug": "~4.3.1", - "engine.io-parser": "~5.0.3", - "ws": "~8.11.0" + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" }, "engines": { - "node": ">=10.0.0" + "node": ">=10.2.0" } }, "node_modules/engine.io-parser": { - "version": "5.0.6", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=10.0.0" } }, "node_modules/engine.io/node_modules/ws": { - "version": "8.11.0", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -7113,9 +7123,10 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, - "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -8289,8 +8300,9 @@ }, "node_modules/is-number": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -10320,8 +10332,9 @@ }, "node_modules/object-assign": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -12050,39 +12063,44 @@ } }, "node_modules/socket.io": { - "version": "4.6.1", + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", + "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", "dev": true, - "license": "MIT", "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", + "cors": "~2.8.5", "debug": "~4.3.2", - "engine.io": "~6.4.1", + "engine.io": "~6.5.2", "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.1" + "socket.io-parser": "~4.2.4" }, "engines": { - "node": ">=10.0.0" + "node": ">=10.2.0" } }, "node_modules/socket.io-adapter": { - "version": "2.5.2", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", "dev": true, - "license": "MIT", "dependencies": { - "ws": "~8.11.0" + "debug": "~4.3.4", + "ws": "~8.17.1" } }, "node_modules/socket.io-adapter/node_modules/ws": { - "version": "8.11.0", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -12094,9 +12112,10 @@ } }, "node_modules/socket.io-parser": { - "version": "4.2.3", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", "dev": true, - "license": "MIT", "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" @@ -12458,13 +12477,14 @@ } }, "node_modules/tar": { - "version": "6.1.13", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "dev": true, - "license": "ISC", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", - "minipass": "^4.0.0", + "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" @@ -12495,6 +12515,15 @@ "node": ">=8" } }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/tar/node_modules/mkdirp": { "version": "1.0.4", "dev": true, @@ -12727,8 +12756,9 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, - "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -13410,9 +13440,9 @@ } }, "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "dev": true, "engines": { "node": ">=10.0.0" @@ -13664,8 +13694,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "7.5.9", - "license": "MIT", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "engines": { "node": ">=8.3.0" }, @@ -15699,7 +15730,9 @@ "dev": true }, "@socket.io/component-emitter": { - "version": "3.1.0", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "dev": true }, "@tootallnate/once": { @@ -15762,10 +15795,14 @@ }, "@types/cookie": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", "dev": true }, "@types/cors": { - "version": "2.8.13", + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", "dev": true, "requires": { "@types/node": "*" @@ -16469,6 +16506,8 @@ }, "base64id": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", "dev": true }, "batch": { @@ -16551,10 +16590,12 @@ } }, "braces": { - "version": "3.0.2", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "browserslist": { @@ -16877,6 +16918,8 @@ }, "cookie": { "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", "dev": true }, "cookie-signature": { @@ -16928,6 +16971,8 @@ }, "cors": { "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "dev": true, "requires": { "object-assign": "^4", @@ -17558,7 +17603,9 @@ } }, "engine.io": { - "version": "6.4.2", + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", + "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", "dev": true, "requires": { "@types/cookie": "^0.4.1", @@ -17569,19 +17616,23 @@ "cookie": "~0.4.1", "cors": "~2.8.5", "debug": "~4.3.1", - "engine.io-parser": "~5.0.3", - "ws": "~8.11.0" + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" }, "dependencies": { "ws": { - "version": "8.11.0", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, "requires": {} } } }, "engine.io-parser": { - "version": "5.0.6", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", "dev": true }, "enhanced-resolve": { @@ -18359,7 +18410,9 @@ } }, "fill-range": { - "version": "7.0.1", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "requires": { "to-regex-range": "^5.0.1" @@ -19108,6 +19161,8 @@ }, "is-number": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, "is-number-object": { @@ -20437,6 +20492,8 @@ }, "object-assign": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true }, "object-inspect": { @@ -21539,33 +21596,43 @@ "dev": true }, "socket.io": { - "version": "4.6.1", + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", + "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", "dev": true, "requires": { "accepts": "~1.3.4", "base64id": "~2.0.0", + "cors": "~2.8.5", "debug": "~4.3.2", - "engine.io": "~6.4.1", + "engine.io": "~6.5.2", "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.1" + "socket.io-parser": "~4.2.4" } }, "socket.io-adapter": { - "version": "2.5.2", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", "dev": true, "requires": { - "ws": "~8.11.0" + "debug": "~4.3.4", + "ws": "~8.17.1" }, "dependencies": { "ws": { - "version": "8.11.0", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, "requires": {} } } }, "socket.io-parser": { - "version": "4.2.3", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", "dev": true, "requires": { "@socket.io/component-emitter": "~3.1.0", @@ -21815,12 +21882,14 @@ "dev": true }, "tar": { - "version": "6.1.13", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "dev": true, "requires": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", - "minipass": "^4.0.0", + "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" @@ -21842,6 +21911,12 @@ } } }, + "minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true + }, "mkdirp": { "version": "1.0.4", "dev": true @@ -21992,6 +22067,8 @@ }, "to-regex-range": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "requires": { "is-number": "^7.0.0" @@ -22441,9 +22518,9 @@ } }, "ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "dev": true, "requires": {} } @@ -22571,7 +22648,9 @@ "dev": true }, "ws": { - "version": "7.5.9", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "requires": {} }, "xss": { diff --git a/patch-it b/patch-it deleted file mode 100644 index b9a1827d..00000000 --- a/patch-it +++ /dev/null @@ -1,46 +0,0 @@ -diff --git a/projects/gameboard-ui/src/app/components/gameboard-signalr-hubs/gameboard-signalr-hubs.component.ts b/projects/gameboard-ui/src/app/components/gameboard-signalr-hubs/gameboard-signalr-hubs.component.ts -index 4a62c8e6..90239460 100644 ---- a/projects/gameboard-ui/src/app/components/gameboard-signalr-hubs/gameboard-signalr-hubs.component.ts -+++ b/projects/gameboard-ui/src/app/components/gameboard-signalr-hubs/gameboard-signalr-hubs.component.ts -@@ -73,6 +73,7 @@ export class GameboardSignalRHubsComponent implements OnDestroy { - - // connect to the hubs - await this.gameHub.connect(); -+ await this.supportHub.connect(); - await this.userHub.connect(); - - // listen for interesting events to log -@@ -93,15 +94,10 @@ export class GameboardSignalRHubsComponent implements OnDestroy { - this.log("[GB UserHub]: Hub state is", userHubState); - this.userHubStatusLightState = this.hubStateToStatusLightState(userHubState); - this.userHubTooltip = `UserHub: ${userHubState}`; -- }) -- ); -+ }), - -- // join the support hub (which everyone uses to get ticket updates) -- if (u.isAdmin || u.isSupport) { -- await this.supportHub.connect(); -- await this.supportHub.joinStaffGroup(); -- this.unsub.add(this.supportHub.hubState$.subscribe(supportHubState => this.supportHubStatusLightState = this.hubStateToStatusLightState(supportHubState))); -- } -+ this.supportHub.hubState$.subscribe(supportHubState => this.supportHubStatusLightState = this.hubStateToStatusLightState(supportHubState)) -+ ); - } - } - -diff --git a/projects/gameboard-ui/src/app/services/signalR/support-hub.service.ts b/projects/gameboard-ui/src/app/services/signalR/support-hub.service.ts -index 67ba3dcb..c05474ae 100644 ---- a/projects/gameboard-ui/src/app/services/signalR/support-hub.service.ts -+++ b/projects/gameboard-ui/src/app/services/signalR/support-hub.service.ts -@@ -41,10 +41,6 @@ export class SupportHubService { - ); - } - -- public async joinStaffGroup() { -- await this._signalRService.sendMessage("joinStaffGroup"); -- } -- - private handleTicketClosed(ev: SupportHubEvent) { - this.logService.logInfo("[GB Support Hub Staff Group]: Ticket Closed", ev); - diff --git a/projects/gameboard-ui/src/app/admin/admin-player-session/admin-player-session.component.html b/projects/gameboard-ui/src/app/admin/admin-player-session/admin-player-session.component.html index 7ce3ce98..33bf3bb3 100644 --- a/projects/gameboard-ui/src/app/admin/admin-player-session/admin-player-session.component.html +++ b/projects/gameboard-ui/src/app/admin/admin-player-session/admin-player-session.component.html @@ -77,18 +77,18 @@

Other tools

(click)="onManageManualBonusesRequest.emit(team.teamId)">Manage Challenge Bonuses Unenroll - + diff --git a/projects/gameboard-ui/src/app/admin/admin.module.ts b/projects/gameboard-ui/src/app/admin/admin.module.ts index 697937ee..3b9e2f88 100644 --- a/projects/gameboard-ui/src/app/admin/admin.module.ts +++ b/projects/gameboard-ui/src/app/admin/admin.module.ts @@ -15,6 +15,7 @@ import { UtilityModule } from '../utility/utility.module'; import { ActiveChallengesModalComponent } from './components/active-challenges-modal/active-challenges-modal.component'; import { AdminOverviewComponent } from './components/admin-overview/admin-overview.component'; import { AdminPageComponent } from './admin-page/admin-page.component'; +import { AdminSystemNotificationsComponent } from '@/system-notifications/components/admin-system-notifications/admin-system-notifications.component'; import { AnnounceComponent } from './announce/announce.component'; import { ChallengeBrowserComponent } from './challenge-browser/challenge-browser.component'; import { ChallengeObserverComponent } from './challenge-observer/challenge-observer.component'; @@ -33,6 +34,14 @@ import { ExternalGamePlayerStatusToFriendlyPipe } from './pipes/external-game-pl import { FeedbackReportComponent } from './feedback-report/feedback-report.component'; import { GameBonusesConfigComponent } from './components/game-bonuses-config/game-bonuses-config.component'; import { GameCenterComponent } from './components/game-center/game-center.component'; +import { GameCenterDeployComponent } from './components/game-center/game-center-deploy/game-center-deploy.component'; +import { GameCenterObserveComponent } from './components/game-center/game-center-observe/game-center-observe.component'; +import { GameCenterPracticePlayerDetailComponent } from './components/game-center/game-center-practice-player-detail/game-center-practice-player-detail.component'; +import { GameCenterPracticeComponent } from './components/game-center/game-center-practice/game-center-practice.component'; +import { GameCenterSettingsComponent } from './components/game-center/game-center-settings/game-center-settings.component'; +import { GameCenterTeamContextMenuComponent } from './components/game-center/game-center-team-context-menu/game-center-team-context-menu.component'; +import { GameCenterTeamsComponent } from './components/game-center/game-center-teams/game-center-teams.component'; +import { GameCenterTicketsComponent } from './components/game-center/game-center-tickets/game-center-tickets.component'; import { GameClassificationToStringPipe } from './pipes/game-classification-to-string.pipe'; import { GameDesignerComponent } from './game-designer/game-designer.component'; import { GameEditorComponent } from './game-editor/game-editor.component'; @@ -54,11 +63,11 @@ import { SupportReportLegacyComponent } from './support-report-legacy/support-re import { SyncStartTeamPlayerReadyCountPipe } from './pipes/sync-start-team-player-ready-count.pipe'; import { SystemNotificationsModule } from '@/system-notifications/system-notifications.module'; import { TeamAdminContextMenuComponent } from './components/team-admin-context-menu/team-admin-context-menu.component'; +import { TeamListCardComponent } from './components/team-list-card/team-list-card.component'; import { TeamObserverComponent } from './team-observer/team-observer.component'; import { UserApiKeysComponent } from './user-api-keys/user-api-keys.component'; import { UserRegistrarComponent } from './user-registrar/user-registrar.component'; import { UserReportComponent } from './user-report/user-report.component'; -import { AdminSystemNotificationsComponent } from '@/system-notifications/components/admin-system-notifications/admin-system-notifications.component'; import { EventHorizonModule } from '@/event-horizon/event-horizon.module'; import { SupportSettingsComponent } from './components/support-settings/support-settings.component'; import { FeedbackEditorComponent } from './components/feedback-editor/feedback-editor.component'; @@ -70,10 +79,6 @@ import { SyncStartGameStateDescriptionPipe } from './pipes/sync-start-game-state import { ExternalGameHostPickerComponent } from './components/external-game-host-picker/external-game-host-picker.component'; import { ExternalHostEditorComponent } from './components/external-host-editor/external-host-editor.component'; import { DeleteExternalGameHostModalComponent } from './components/delete-external-game-host-modal/delete-external-game-host-modal.component'; -import { GameCenterPlayersComponent } from './components/game-center/game-center-players/game-center-players.component'; -import { GameCenterTeamContextMenuComponent } from './components/game-center/game-center-team-context-menu/game-center-team-context-menu.component'; -import { GameCenterSettingsComponent } from './components/game-center/game-center-settings/game-center-settings.component'; -import { GameCenterTicketsComponent } from './components/game-center/game-center-tickets/game-center-tickets.component'; @NgModule({ declarations: [ @@ -96,6 +101,14 @@ import { GameCenterTicketsComponent } from './components/game-center/game-center ExternalTeamChallengesToIsPredeployablePipe, FeedbackReportComponent, GameCenterComponent, + GameCenterDeployComponent, + GameCenterPracticeComponent, + GameCenterPracticePlayerDetailComponent, + GameCenterSettingsComponent, + GameCenterObserveComponent, + GameCenterTeamContextMenuComponent, + GameCenterTeamsComponent, + GameCenterTicketsComponent, GameClassificationToStringPipe, GameDesignerComponent, GameEditorComponent, @@ -132,10 +145,7 @@ import { GameCenterTicketsComponent } from './components/game-center/game-center ExternalGameHostPickerComponent, ExternalHostEditorComponent, DeleteExternalGameHostModalComponent, - GameCenterPlayersComponent, - GameCenterTeamContextMenuComponent, - GameCenterSettingsComponent, - GameCenterTicketsComponent, + TeamListCardComponent, ], imports: [ CommonModule, @@ -147,12 +157,14 @@ import { GameCenterTicketsComponent } from './components/game-center/game-center { path: '', pathMatch: 'full', redirectTo: 'dashboard' }, { path: 'dashboard', component: DashboardComponent }, { path: 'designer/:id', component: GameEditorComponent }, + { + path: "game/:gameId/:selectedTab", + component: GameCenterComponent + }, { path: 'game/:gameId', component: GameCenterComponent, - children: [ - { path: "teams", component: PlayerRegistrarComponent } - ] + pathMatch: 'full' }, { path: "game/:gameId/external", pathMatch: 'full', component: ExternalGameAdminComponent @@ -177,7 +189,7 @@ import { GameCenterTicketsComponent } from './components/game-center/game-center { path: 'report/support', component: SupportReportLegacyComponent }, { path: 'report/participation', component: ParticipationReportComponent }, { path: "notifications", component: AdminSystemNotificationsComponent }, - { path: "support/settings", component: SupportSettingsComponent }, + { path: "support/settings", component: SupportSettingsComponent, title: "Admin | Support" }, { path: 'support', component: ChallengeBrowserComponent } ] }, diff --git a/projects/gameboard-ui/src/app/admin/challenge-observer/challenge-observer.component.html b/projects/gameboard-ui/src/app/admin/challenge-observer/challenge-observer.component.html index 84387398..9ce2e4e0 100644 --- a/projects/gameboard-ui/src/app/admin/challenge-observer/challenge-observer.component.html +++ b/projects/gameboard-ui/src/app/admin/challenge-observer/challenge-observer.component.html @@ -1,7 +1,7 @@ - + Back @@ -9,12 +9,14 @@ Refresh -
+ + +
diff --git a/projects/gameboard-ui/src/app/admin/challenge-observer/challenge-observer.component.ts b/projects/gameboard-ui/src/app/admin/challenge-observer/challenge-observer.component.ts index 6209d53f..2a0bb037 100644 --- a/projects/gameboard-ui/src/app/admin/challenge-observer/challenge-observer.component.ts +++ b/projects/gameboard-ui/src/app/admin/challenge-observer/challenge-observer.component.ts @@ -2,11 +2,11 @@ // Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. import { KeyValue } from '@angular/common'; -import { Component, OnDestroy } from '@angular/core'; +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { faArrowLeft, faSyncAlt, faTv, faExternalLinkAlt, faExpandAlt, faUser, faThLarge, faMinusSquare, faPlusSquare, faCompressAlt, faSortAlphaDown, faSortAmountDownAlt, faAngleDoubleUp, faWindowRestore } from '@fortawesome/free-solid-svg-icons'; import { combineLatest, timer, BehaviorSubject, Observable, Subscription } from 'rxjs'; -import { debounceTime, tap, switchMap, map } from 'rxjs/operators'; +import { debounceTime, tap, switchMap, map, filter } from 'rxjs/operators'; import { ConsoleActor, ObserveChallenge, ObserveVM } from '../../api/board-models'; import { BoardService } from '../../api/board.service'; import { Game } from '../../api/game-models'; @@ -20,7 +20,9 @@ import { MatchesTermPipe } from '@/utility/pipes/matches-term.pipe'; styleUrls: ['./challenge-observer.component.scss'], providers: [MatchesTermPipe] }) -export class ChallengeObserverComponent implements OnDestroy { +export class ChallengeObserverComponent implements OnInit, OnDestroy { + @Input() gameId?: string; + refresh$ = new BehaviorSubject(true); game?: Game; table: Map = new Map(); // table of player challenges to display @@ -49,17 +51,18 @@ export class ChallengeObserverComponent implements OnDestroy { faAngleDoubleUp = faAngleDoubleUp; faWindowRestore = faWindowRestore; + protected isLegacyMode = false; protected searchText = ""; constructor( route: ActivatedRoute, private api: BoardService, private gameApi: GameService, - private conf: ConfigService, - private matchesTerm: MatchesTermPipe + private conf: ConfigService ) { this.mksHost = conf.mkshost; this.gameData = route.params.pipe( + filter(a => !!a.id), switchMap(a => this.gameApi.retrieve(a.id)) ).subscribe(game => this.game = game); @@ -69,7 +72,7 @@ export class ChallengeObserverComponent implements OnDestroy { timer(0, 60_000) // *every 60 sec* refresh challenge data (score/duration updates and new deploys) ]).pipe( debounceTime(500), - tap(([a, b, c]) => this.gid = a.id), + tap(([a, b, c]) => this.gid = this.gameId || a.id), tap(() => this.isLoading = true), switchMap(() => this.api.consoles(this.gid)), ).subscribe(data => { @@ -87,7 +90,7 @@ export class ChallengeObserverComponent implements OnDestroy { timer(0, 10_000) // *every 10 sec* refresh which users are one which consoles ]).pipe( debounceTime(500), - tap(([a, b, c]) => this.gid = a.id), + tap(([a, b, c]) => this.gid = this.gameId || a.id), switchMap(() => this.api.consoleActors(this.gid)), map(data => data.reduce((map, item) => { const key = `${item.challengeId}#${item.vmName}`; @@ -105,6 +108,10 @@ export class ChallengeObserverComponent implements OnDestroy { ); } + public ngOnInit() { + this.isLegacyMode == !this.gameId; + } + updateTable(data: ObserveChallenge[]) { for (let updatedChallenge of data) { if (this.table.has(updatedChallenge.id)) { diff --git a/projects/gameboard-ui/src/app/admin/components/admin-enroll-team-modal/admin-enroll-team-modal.component.html b/projects/gameboard-ui/src/app/admin/components/admin-enroll-team-modal/admin-enroll-team-modal.component.html index f7a1246a..dd6cccec 100644 --- a/projects/gameboard-ui/src/app/admin/components/admin-enroll-team-modal/admin-enroll-team-modal.component.html +++ b/projects/gameboard-ui/src/app/admin/components/admin-enroll-team-modal/admin-enroll-team-modal.component.html @@ -1,12 +1,7 @@ - + + diff --git a/projects/gameboard-ui/src/app/admin/components/challenge-spec-editor/challenge-spec-editor.component.html b/projects/gameboard-ui/src/app/admin/components/challenge-spec-editor/challenge-spec-editor.component.html index 9e8ce143..38d45c10 100644 --- a/projects/gameboard-ui/src/app/admin/components/challenge-spec-editor/challenge-spec-editor.component.html +++ b/projects/gameboard-ui/src/app/admin/components/challenge-spec-editor/challenge-spec-editor.component.html @@ -30,7 +30,6 @@

{{ spec.name }}

-
@@ -44,7 +43,7 @@

{{ spec.name }}

+ whatItIs="Hidden challenge specs are invisible to players and don't contribute to their score. They also don't appear in reports.">
diff --git a/projects/gameboard-ui/src/app/admin/components/extend-teams-modal/extend-teams-modal.component.html b/projects/gameboard-ui/src/app/admin/components/extend-teams-modal/extend-teams-modal.component.html index ab64d326..1ff45795 100644 --- a/projects/gameboard-ui/src/app/admin/components/extend-teams-modal/extend-teams-modal.component.html +++ b/projects/gameboard-ui/src/app/admin/components/extend-teams-modal/extend-teams-modal.component.html @@ -9,6 +9,14 @@ diff --git a/projects/gameboard-ui/src/app/admin/components/extend-teams-modal/extend-teams-modal.component.ts b/projects/gameboard-ui/src/app/admin/components/extend-teams-modal/extend-teams-modal.component.ts index 5c5cc46a..bbb7f57e 100644 --- a/projects/gameboard-ui/src/app/admin/components/extend-teams-modal/extend-teams-modal.component.ts +++ b/projects/gameboard-ui/src/app/admin/components/extend-teams-modal/extend-teams-modal.component.ts @@ -1,9 +1,10 @@ import { Component, OnInit } from '@angular/core'; -import { firstValueFrom } from 'rxjs'; +import { firstValueFrom, map, tap } from 'rxjs'; import { TeamService } from '@/api/team.service'; import { ModalConfirmService } from '@/services/modal-confirm.service'; import { ToastService } from '@/utility/services/toast.service'; import { Team } from '@/api/player-models'; +import { DateTime } from 'luxon'; @Component({ selector: 'app-extend-teams-modal', @@ -20,6 +21,7 @@ export class ExtendTeamsModalComponent implements OnInit { teamIds: string[] = []; protected errors: any[] = []; + protected ineligibleTeamIds: string[] = []; protected isWorking = false; protected modalTitle = "Extend Sessions"; protected apiTeams: Team[] = []; @@ -38,14 +40,18 @@ export class ExtendTeamsModalComponent implements OnInit { throw new Error("Can't open the Extend modal - no teams supplied."); } - this.apiTeams = await firstValueFrom(this.teamService.search(this.teamIds)); + this.apiTeams = await firstValueFrom(this.teamService.search(this.teamIds).pipe( + map(teams => teams.filter(t => t.sessionEnd.getFullYear() !== 0)) + )); + this.ineligibleTeamIds = this.teamIds.filter(tId => this.apiTeams.map(t => t.teamId).indexOf(tId) < 0); this.modalTitle = this.apiTeams.length === 1 ? `Extend Session: ${this.apiTeams[0].approvedName}` : "Extend Sessions"; } - async extend(teamIds: string[], extensionDurationInMinutes: number) { + async extend(extensionDurationInMinutes: number) { this.isWorking = true; try { + const teamIds = this.apiTeams.map(t => t.teamId); const result = await firstValueFrom(this.teamService.adminExtendSession({ teamIds, extensionDurationInMinutes })); this.modalService.hide(); this.toastService.showMessage(`Extended sessions by ${extensionDurationInMinutes} minutes for ${result.teams.length} teams.`); diff --git a/projects/gameboard-ui/src/app/admin/components/external-game-admin/external-game-admin.component.ts b/projects/gameboard-ui/src/app/admin/components/external-game-admin/external-game-admin.component.ts index 77db8f36..430312c0 100644 --- a/projects/gameboard-ui/src/app/admin/components/external-game-admin/external-game-admin.component.ts +++ b/projects/gameboard-ui/src/app/admin/components/external-game-admin/external-game-admin.component.ts @@ -1,8 +1,8 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { ExternalGameDeployStatus } from '@/api/game-models'; -import { Subject, combineLatest, debounceTime, filter, firstValueFrom, map, timer } from 'rxjs'; import { DateTime } from 'luxon'; +import { Subject, combineLatest, debounceTime, filter, firstValueFrom, map, timer } from 'rxjs'; +import { ExternalGameDeployStatus } from '@/api/game-models'; import { SimpleEntity, SimpleSponsor } from '@/api/models'; import { fa } from '@/services/font-awesome.service'; import { AppTitleService } from '@/services/app-title.service'; @@ -55,10 +55,10 @@ export interface ExternalGameAdminContext { styleUrls: ['./external-game-admin.component.scss'], providers: [UnsubscriberService] }) -export class ExternalGameAdminComponent implements OnInit { +export class ExternalGameAdminComponent implements OnInit, OnChanges { + @Input() gameId?: string; private autoUpdateInterval = 30000; private forceRefresh$ = new Subject(); - private gameId?: string; protected ctx?: ExternalGameAdminContext; protected errors: any[] = []; @@ -93,6 +93,16 @@ export class ExternalGameAdminComponent implements OnInit { this.forceRefresh$.next(); } + ngOnChanges(changes: SimpleChanges) { + if (changes?.gameId?.currentValue) { + const paramGameId = this.route.snapshot.paramMap?.get("gameId"); + + if (paramGameId !== changes.gameId.currentValue) { + throw new Error("The gameId detected in the route param map is different than the component's input gameId. This will cause the component to misbehave."); + } + } + } + protected async handlePreDeployAllClick(gameId: string) { try { this.bindOverallDeployStatus("deploying"); diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-deploy/game-center-deploy.component.html b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-deploy/game-center-deploy.component.html new file mode 100644 index 00000000..6a40fcd1 --- /dev/null +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-deploy/game-center-deploy.component.html @@ -0,0 +1 @@ +

game-center-deploy works!

diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-players/game-center-players.component.scss b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-deploy/game-center-deploy.component.scss similarity index 100% rename from projects/gameboard-ui/src/app/admin/components/game-center/game-center-players/game-center-players.component.scss rename to projects/gameboard-ui/src/app/admin/components/game-center/game-center-deploy/game-center-deploy.component.scss diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-deploy/game-center-deploy.component.ts b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-deploy/game-center-deploy.component.ts new file mode 100644 index 00000000..0f951d82 --- /dev/null +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-deploy/game-center-deploy.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-game-center-deploy', + templateUrl: './game-center-deploy.component.html', + styleUrls: ['./game-center-deploy.component.scss'] +}) +export class GameCenterDeployComponent { + +} diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-observe/game-center-observe.component.html b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-observe/game-center-observe.component.html new file mode 100644 index 00000000..705b64d5 --- /dev/null +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-observe/game-center-observe.component.html @@ -0,0 +1,11 @@ +
+ + +
+ +
+ + +
diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-observe/game-center-observe.component.ts b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-observe/game-center-observe.component.ts new file mode 100644 index 00000000..c2c8c92f --- /dev/null +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-observe/game-center-observe.component.ts @@ -0,0 +1,11 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'app-game-center-observe', + templateUrl: './game-center-observe.component.html', +}) +export class GameCenterObserveComponent { + @Input() gameId?: string; + + protected observeBy: "challenge" | "team" = "challenge"; +} diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-players/game-center-players.component.html b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-players/game-center-players.component.html deleted file mode 100644 index dd8f8425..00000000 --- a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-players/game-center-players.component.html +++ /dev/null @@ -1,45 +0,0 @@ - -
    -
  • -
    -
    -
    -
    -
    - {{team.rank}} -
    - - -
    -
    {{ team.name }}
    -
    {{ team.captain.sponsor.name }}
    -
    -
    - - -
    - -
      -
    • -
    • -
    -
    -
    -
  • -
-
- - - - - - -

No players match your search.

-
- - - Finding players... - diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-players/game-center-players.component.ts b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-players/game-center-players.component.ts deleted file mode 100644 index 6af56b66..00000000 --- a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-players/game-center-players.component.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Component, Input, OnInit } from '@angular/core'; -import { GameCenterTeamsResults } from '@/api/admin.models'; -import { AdminService } from '@/api/admin.service'; -import { Game } from '@/api/game-models'; -import { GameService } from '@/api/game.service'; -import { firstValueFrom } from 'rxjs'; - -@Component({ - selector: 'app-game-center-players', - templateUrl: './game-center-players.component.html', - styleUrls: ['./game-center-players.component.scss'] -}) -export class GameCenterPlayersComponent implements OnInit { - @Input() gameId?: string; - - protected game?: Game; - protected isLoading = false; - protected results?: GameCenterTeamsResults; - - constructor( - private adminService: AdminService, - private gameService: GameService) { } - - async ngOnInit(): Promise { - if (!this.gameId) - throw new Error("Component requires a gameId"); - - this.game = await firstValueFrom(this.gameService.retrieve(this.gameId)); - await this.load(); - } - - private async load() { - this.isLoading = true; - this.results = await this.adminService.getGameCenterTeams(this.gameId!, { sort: "rank" }); - this.isLoading = false; - } -} diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-practice-player-detail/game-center-practice-player-detail.component.html b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-practice-player-detail/game-center-practice-player-detail.component.html new file mode 100644 index 00000000..32e76e37 --- /dev/null +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-practice-player-detail/game-center-practice-player-detail.component.html @@ -0,0 +1,51 @@ + +
+
+
Total Challenges Attempted
+
{{ ctx.uniqueChallengeSpecs }}
+
+
+
Total Attempts
+
{{ ctx.totalAttempts }}
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
ChallengeAttemptsLast AttemptBest Score
{{ spec.name }}{{ spec.attemptCount}} + {{ spec.lastAttemptDate | epochMsToDateTime | shortdate }} +
{{ spec.lastAttemptDate | epochMsToDateTime | friendlyTime }}
+
+ +
+ {{ spec.bestAttempt.result | challengeResultPretty }} +
+
({{ spec.bestAttempt.score}} points)
+
+
+
+ + + Loading player data... + diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-practice-player-detail/game-center-practice-player-detail.component.scss b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-practice-player-detail/game-center-practice-player-detail.component.scss new file mode 100644 index 00000000..6a5c733b --- /dev/null +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-practice-player-detail/game-center-practice-player-detail.component.scss @@ -0,0 +1,8 @@ +@import "../../../../../scss/variables"; + +pre code { + background-color: $info; + border-radius: 4px; + color: #fff; + padding: 4px; +} diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-practice-player-detail/game-center-practice-player-detail.component.ts b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-practice-player-detail/game-center-practice-player-detail.component.ts new file mode 100644 index 00000000..68edb117 --- /dev/null +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-practice-player-detail/game-center-practice-player-detail.component.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; +import { GameCenterPracticeChallengeSpec } from '../game-center.models'; +import { SimpleEntity } from '@/api/models'; + +export interface GameCenterPracticePlayerDetailContext { + id: string; + name: string; + game: SimpleEntity; + totalAttempts: number; + uniqueChallengeSpecs: number; + challengeSpecs: GameCenterPracticeChallengeSpec[] +} + +@Component({ + selector: 'app-game-center-practice-player-detail', + templateUrl: './game-center-practice-player-detail.component.html', + styleUrls: ['./game-center-practice-player-detail.component.scss'] +}) +export class GameCenterPracticePlayerDetailComponent { + ctx?: GameCenterPracticePlayerDetailContext; +} diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-practice/game-center-practice.component.html b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-practice/game-center-practice.component.html new file mode 100644 index 00000000..f34eb9bb --- /dev/null +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-practice/game-center-practice.component.html @@ -0,0 +1,55 @@ +
+ + + + + + + + + +
+ + +
+
    +
  • + +
    + {{ user.totalAttempts }} {{ "attempt" | pluralizer:user.totalAttempts }} +
    + ({{ user.uniqueChallengeSpecs }} {{ "challenge" | pluralizer:user.uniqueChallengeSpecs }}) +
    +
    +
    + Playing "{{ user.activeChallenge.name }}" now +
    + + remaining +
    +
    +
    + +
    +
    +
  • +
+
+
+ + + Loading practice data... + + + +
No one has attempted challenges from this game in the Practice Area yet. +
+
diff --git a/projects/gameboard-ui/src/app/support/components/ticket-label-picker/ticket-label-picker.component.scss b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-practice/game-center-practice.component.scss similarity index 100% rename from projects/gameboard-ui/src/app/support/components/ticket-label-picker/ticket-label-picker.component.scss rename to projects/gameboard-ui/src/app/admin/components/game-center/game-center-practice/game-center-practice.component.scss diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-practice/game-center-practice.component.ts b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-practice/game-center-practice.component.ts new file mode 100644 index 00000000..2f451903 --- /dev/null +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-practice/game-center-practice.component.ts @@ -0,0 +1,88 @@ +import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { AdminService } from '@/api/admin.service'; +import { fa } from "@/services/font-awesome.service"; +import { TeamListCardContext } from '../../team-list-card/team-list-card.component'; +import { GameCenterPracticeContext, GameCenterPracticeContextUser, GameCenterPracticeSessionStatus, GameCenterPracticeSort } from '../game-center.models'; +import { ModalConfirmService } from '@/services/modal-confirm.service'; +import { GameCenterPracticePlayerDetailComponent } from '../game-center-practice-player-detail/game-center-practice-player-detail.component'; +import { debounceTime, Subject } from 'rxjs'; +import { UnsubscriberService } from '@/services/unsubscriber.service'; + +interface GameCenterPracticeFilterSettings { + searchTerm?: string; + sessionStatus?: GameCenterPracticeSessionStatus; + sort?: GameCenterPracticeSort; +} + +@Component({ + selector: 'app-game-center-practice', + templateUrl: './game-center-practice.component.html', + styleUrls: ['./game-center-practice.component.scss'] +}) +export class GameCenterPracticeComponent implements OnChanges { + @Input() gameId?: string; + + protected ctx?: GameCenterPracticeContext; + protected fa = fa; + protected filterSettings: GameCenterPracticeFilterSettings = { sort: "name" }; + protected isLoading = false; + protected teamCardContexts: { [userId: string]: TeamListCardContext } = {}; + protected searchInput$ = new Subject(); + protected selectedUserIds: string[] = []; + + constructor( + unsub: UnsubscriberService, + private adminService: AdminService, + private modalService: ModalConfirmService) { + unsub.add(this.searchInput$.pipe(debounceTime(500)).subscribe(async searchTerm => { + this.filterSettings.searchTerm = searchTerm; + await this.load(); + })); + } + + async ngOnChanges(changes: SimpleChanges): Promise { + await this.load(); + } + + protected async handleClearAllFilters() { + this.filterSettings = { sort: "name" }; + await this.load(); + } + + protected async handleSearch(value: string) { + this.filterSettings.searchTerm = value; + + } + + protected handleUserDetailClick(user: GameCenterPracticeContextUser) { + if (!this.ctx) + throw new Error("Context is required."); + + this.modalService.openComponent({ + content: GameCenterPracticePlayerDetailComponent, + context: { + ctx: { + ...user, + game: this.ctx.game + } + }, + modalClasses: ["modal-lg"] + }); + } + + protected async load(): Promise { + if (!this.gameId) + throw new Error("GameId is required."); + + this.ctx = await this.adminService.getGameCenterPracticeContext(this.gameId, this.filterSettings); + this.teamCardContexts = {}; + for (let user of this.ctx.users) { + this.teamCardContexts[user.id] = { + id: user.id, + name: user.name, + captain: { id: user.id, name: user.name, sponsor: user.sponsor }, + players: [] + }; + } + } +} diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-settings/game-center-settings.component.html b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-settings/game-center-settings.component.html index dfa81797..0ab7647c 100644 --- a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-settings/game-center-settings.component.html +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-settings/game-center-settings.component.html @@ -1,16 +1,8 @@
-

- {{game.name}} - - - -

- {{game.id}} - - - Lobby - + + +
diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-team-context-menu/game-center-team-context-menu.component.html b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-team-context-menu/game-center-team-context-menu.component.html index a4f7fd48..d123dc5e 100644 --- a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-team-context-menu/game-center-team-context-menu.component.html +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-team-context-menu/game-center-team-context-menu.component.html @@ -1,14 +1,9 @@ -
+
- - -
  • + + + + + + + + +
  • + + +
    + +
    + + + + + + + + + + + +
    + + +
      +
    • + +
      +
      + + {{ team.score.totalScore }} points +
      + +
      + ({{ team.challengesCompleteCount }} complete / {{ team.challengesPartialCount }} partial) +
      +
      + +
      + +
      Playing now
      + ({{ + team.session.timeRemainingMs | countdown }} + remaining) +
      + + +
      Not playing yet
      + + + (Registered {{ + team.registeredOn | epochMsToDateTime | + shortdate }}) + +
      + + + Finished + + {{ team.session.end | epochMsToDateTime | shortdate }} + + +
      + +
      + +
      +
      +
    • +
    +
    + + + +
    +
    {{player.name}}
    +
    + {{player.sponsor.name}} +
    +
    +
    +
    + + +

    No players match your search.

    +
    + + + Finding players... + diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-teams/game-center-teams.component.scss b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-teams/game-center-teams.component.scss new file mode 100644 index 00000000..140e7bc1 --- /dev/null +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-teams/game-center-teams.component.scss @@ -0,0 +1,6 @@ +@import "../../../../../scss/variables"; + +.score-popover { + background-color: $gray-900; + padding: 1rem; +} diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-teams/game-center-teams.component.ts b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-teams/game-center-teams.component.ts new file mode 100644 index 00000000..7ba6996e --- /dev/null +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-teams/game-center-teams.component.ts @@ -0,0 +1,238 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { debounceTime, firstValueFrom, map, Subject } from 'rxjs'; +import { AdminService } from '@/api/admin.service'; +import { fa } from '@/services/font-awesome.service'; +import { Game } from '@/api/game-models'; +import { GameService } from '@/api/game.service'; +import { ModalConfirmService } from '@/services/modal-confirm.service'; +import { ToastService } from '@/utility/services/toast.service'; +import { AdminEnrollTeamModalComponent } from '../../admin-enroll-team-modal/admin-enroll-team-modal.component'; +import { ManageManualChallengeBonusesModalComponent } from '../../manage-manual-challenge-bonuses-modal/manage-manual-challenge-bonuses-modal.component'; +import { SimpleEntity } from '@/api/models'; +import { GameCenterTeamsAdvancementFilter, GameCenterTeamSessionStatus, GameCenterTeamsResults, GameCenterTeamsSort } from '../game-center.models'; +import { UnsubscriberService } from '@/services/unsubscriber.service'; +import { unique } from 'projects/gameboard-ui/src/tools'; +import { ScoreboardTeamDetailModalComponent } from '@/scoreboard/components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component'; +import { PlayerService } from '@/api/player.service'; +import { ClipboardService } from '@/utility/services/clipboard.service'; +import { ExtendTeamsModalComponent } from '../../extend-teams-modal/extend-teams-modal.component'; +import { LocalStorageService, StorageKey } from '@/services/local-storage.service'; +import { NowService } from '@/services/now.service'; + +interface GameCenterTeamsFilterSettings { + advancement?: GameCenterTeamsAdvancementFilter; + searchTerm?: string; + sort?: GameCenterTeamsSort; + sessionStatus?: GameCenterTeamSessionStatus; +} + +@Component({ + selector: 'app-game-center-teams', + templateUrl: './game-center-teams.component.html', + styleUrls: ['./game-center-teams.component.scss'] +}) +export class GameCenterTeamsComponent implements OnInit { + @Input() gameId?: string; + + protected fa = fa; + protected game?: Game; + protected isLoading = false; + protected results?: GameCenterTeamsResults; + protected selectedTeamIds: string[] = []; + + // search/sort/filter + protected filterSettings: GameCenterTeamsFilterSettings = { sort: 'rank' }; + protected searchInput$ = new Subject(); + + constructor( + private adminService: AdminService, + private clipboardService: ClipboardService, + private gameService: GameService, + private localStorageClient: LocalStorageService, + private modalService: ModalConfirmService, + private nowService: NowService, + private playerService: PlayerService, + private toastService: ToastService, + private unsub: UnsubscriberService) { + + this.unsub.add(this.searchInput$.pipe( + debounceTime(300) + ).subscribe(async event => { + this.filterSettings.searchTerm = (event.target as HTMLInputElement).value; + this.load(); + }) + ); + } + + async ngOnInit(): Promise { + if (!this.gameId) + throw new Error("Component requires a gameId"); + + this.game = await firstValueFrom(this.gameService.retrieve(this.gameId)); + this.filterSettings = this.localStorageClient.getAs(StorageKey.GameCenterTeamsFilterSettings, this.filterSettings); + await this.load(); + } + + protected async handleClearAllFilters() { + this.filterSettings.advancement = undefined; + this.filterSettings.searchTerm = ""; + this.filterSettings.sort = "rank"; + this.filterSettings.sessionStatus = undefined; + await this.load(); + } + + protected async handleDeployGameResources() { + if (!this.results || !this.gameId) + return; + + const teamIds = this.selectedTeamIds.length ? this.selectedTeamIds : this.results.teams.items.map(t => t.id); + const invalidTeamNames: string[] = []; + const validTeamIds: string[] = []; + + const nowish = this.nowService.nowToMsEpoch(); + for (const team of this.results.teams.items) { + if (this.selectedTeamIds.length && this.selectedTeamIds.indexOf(team.id) < 0) + continue; + + if (team.session.end && team.session.end < nowish) + invalidTeamNames.push(team.name); + else + validTeamIds.push(team.id); + } + + if (!validTeamIds.length) { + this.modalService.openConfirm({ + bodyContent: "All selected teams have finished their sessions, so no resources can be deployed for them.", + hideCancel: true, + title: "All teams finished", + subtitle: this.game?.name + }); + + return; + } + + let appendInvalidTeamsClause = ""; + if (invalidTeamNames.length) { + appendInvalidTeamsClause = `\n\nSessions for some teams have ended, so their resources won't be deployed:\n\n${invalidTeamNames.map(tId => `- ${tId}\n`)}`; + } + + + this.modalService.openConfirm({ + bodyContent: `Are you sure you want to deploy resources for ${validTeamIds.length} teams?${appendInvalidTeamsClause}`, + onConfirm: async () => { + if (!validTeamIds.length) + return; + + await this.gameService.deployResources(this.gameId!, validTeamIds); + this.toastService.showMessage(`Deploying resources for **${validTeamIds.length} ${this.game?.isTeamGame ? "team" : "player"}(s)**.`) + this.selectedTeamIds = []; + }, + renderBodyAsMarkdown: true, + subtitle: this.game?.name, + title: "Deploy game resources" + }); + } + + protected async handleExportCsvData(selectedTeamIds: string[]) { + if (!this.gameId) + throw new Error("GameId is required."); + + const hdr = 'GameId,TeamId,TeamName,PlayerId,UserId,UserName,Rank,Score,Time,Correct,Partial,SessionBegin,SessionEnd\n'; + var players = await this.adminService.getPlayersExport(this.gameId, selectedTeamIds.length ? selectedTeamIds : undefined); + this.clipboardService.copy(hdr + players + .map(p => `${p.game.id},${p.team.id},${p.name.replace(',', '-')},${p.id},${p.user.id},${p.user.name.replace(',', '-')},${p.rank},${p.score},${p.timeMs},${p.solvesCorrectCount},${p.solvesPartialCount},${p.session.start || ""},${p.session.end || ""}`) + .join("\n") + ); + + this.toastService.showMessage(`Copied CSV data for **${this.selectedTeamIds.length ? players.length : "all"} players** to your clipboard.`); + } + + protected handleExportMailMetaData(): void { + if (!this.gameId) + throw new Error("Requires gameId"); + + this.playerService.getTeams(this.gameId) + .pipe( + map(r => r.filter(s => this.selectedTeamIds?.length === 0 || this.selectedTeamIds.indexOf(s.id) >= 0)) + ) + .subscribe(data => { + this.clipboardService.copy(JSON.stringify(data, null, 2)); + }); + + this.toastService.showMessage(`Copied mail metadata for **${this.selectedTeamIds.length ? this.selectedTeamIds.length : "all"} ${this.game?.isTeamGame ? "teams" : "players"}** to your clipboard.`); + } + + protected handleExtendClick(selectedTeamIds: string[]) { + if (!this.game) + throw new Error("Game is required"); + + this.modalService.openComponent({ + content: ExtendTeamsModalComponent, + context: { + game: this.game, + teamIds: selectedTeamIds + }, + modalClasses: ["modal-lg"] + }); + } + + protected async handleRerankClick(gameId: string) { + await firstValueFrom(this.gameService.rerank(gameId)); + await this.load(); + this.toastService.showMessage(`${this.game?.name} has been **reranked**!`); + } + + protected handleTeamScoreClick(teamId: string) { + this.modalService.openComponent({ + content: ScoreboardTeamDetailModalComponent, + context: { teamId }, + modalClasses: ["modal-lg"] + }); + } + + protected handleTeamSelected(event: { isSelected: boolean, teamId: string }) { + if (event.isSelected) + this.selectedTeamIds = unique([...this.selectedTeamIds.concat(event.teamId)]); + else + this.selectedTeamIds = [...this.selectedTeamIds.filter(id => id !== event.teamId)]; + } + + protected manageManualBonuses(team: SimpleEntity) { + this.modalService.openComponent({ + content: ManageManualChallengeBonusesModalComponent, + context: { + gameName: this.game?.name, + playerName: team.name, + teamId: team.id + } + }); + } + + protected handlePlayerAddClick(game: Game) { + this.modalService.openComponent({ + content: AdminEnrollTeamModalComponent, + context: { + game: game, + onConfirm: async result => { + this.toastService.showMessage(`Enrolled **${result.name}** in the game.`); + await this.load(); + } + }, + modalClasses: ["modal-xl"] + }); + } + + protected async load() { + if (!this.gameId) + throw new Error("gameId is required."); + + this.isLoading = true; + this.results = await this.adminService.getGameCenterTeams(this.gameId, this.filterSettings); + this.isLoading = false; + this.updateFilterConfig(); + } + + private updateFilterConfig() { + this.localStorageClient.add(StorageKey.GameCenterTeamsFilterSettings, this.filterSettings); + } +} diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-tickets/game-center-tickets.component.html b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-tickets/game-center-tickets.component.html index 2d5b522e..1eb28872 100644 --- a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-tickets/game-center-tickets.component.html +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-tickets/game-center-tickets.component.html @@ -1,16 +1,5 @@ - + diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-tickets/game-center-tickets.component.ts b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-tickets/game-center-tickets.component.ts index d2e4207e..49e81675 100644 --- a/projects/gameboard-ui/src/app/admin/components/game-center/game-center-tickets/game-center-tickets.component.ts +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center-tickets/game-center-tickets.component.ts @@ -1,4 +1,4 @@ -import { Ticket, TicketSummary } from '@/api/support-models'; +import { TicketSummary } from '@/api/support-models'; import { SupportService } from '@/api/support.service'; import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { firstValueFrom } from 'rxjs'; diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center.component.html b/projects/gameboard-ui/src/app/admin/components/game-center/game-center.component.html index 38f99054..3bf98b2e 100644 --- a/projects/gameboard-ui/src/app/admin/components/game-center/game-center.component.html +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center.component.html @@ -1,7 +1,7 @@
    -
    +

    {{ ctx.name }}

    @@ -11,49 +11,81 @@

    Game

    -

    - {{ ctx | gameClassificationToString }} -

    +
    +
    +

    + {{ ctx | gameClassificationToString }} +

    +

    + {{ ctx.executionWindow.start | friendlyDateAndTime }} + — + {{ ctx.executionWindow.end | friendlyDateAndTime }} +

    -

    - {{ ctx.executionWindow.start | friendlyDateAndTime }} - — - {{ ctx.executionWindow.end | friendlyDateAndTime }} -

    - -
    -
    - Registration Available +
    +
    Unpublished
    +
    + Registration Available +
    +
    Live
    -
    Live
    - - NOTE: This feature is under active development, and some things might not as expected just - yet. Stay tuned as we continue to improve the game management process! 🚀 - +
    + + + + + + + +
    - - + + + + + + + + + + + - - + + - - + + - - + + - -

    This feature is in development. Check back soon!

    + + - - + +
    diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center.component.scss b/projects/gameboard-ui/src/app/admin/components/game-center/game-center.component.scss index 5524c5dd..169c1da9 100644 --- a/projects/gameboard-ui/src/app/admin/components/game-center/game-center.component.scss +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center.component.scss @@ -1,3 +1,7 @@ app-game-card-image { max-width: 120px; } + +app-big-stat { + margin-bottom: 1.2rem; +} diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center.component.ts b/projects/gameboard-ui/src/app/admin/components/game-center/game-center.component.ts index 71f9f7e3..b1c757b6 100644 --- a/projects/gameboard-ui/src/app/admin/components/game-center/game-center.component.ts +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center.component.ts @@ -1,11 +1,14 @@ -import { GameCenterContext } from '@/api/admin.models'; import { AdminService } from '@/api/admin.service'; import { Game } from '@/api/game-models'; import { GameService } from '@/api/game.service'; +import { fa } from '@/services/font-awesome.service'; import { UnsubscriberService } from '@/services/unsubscriber.service'; import { Component } from '@angular/core'; +import { firstValueFrom, interval } from 'rxjs'; +import { GameCenterContext, GameCenterTab } from './game-center.models'; +import { AppTitleService } from '@/services/app-title.service'; +import { RouterService } from '@/services/router.service'; import { ActivatedRoute } from '@angular/router'; -import { firstValueFrom } from 'rxjs'; @Component({ selector: 'app-game-center', @@ -14,24 +17,65 @@ import { firstValueFrom } from 'rxjs'; providers: [UnsubscriberService] }) export class GameCenterComponent { + protected fa = fa; protected game?: Game; protected gameCenterCtx?: GameCenterContext; + protected selectedTab: GameCenterTab = "settings"; constructor( - route: ActivatedRoute, unsub: UnsubscriberService, + private activatedRoute: ActivatedRoute, private adminService: AdminService, - private gameService: GameService) { - unsub.add( - route.paramMap.subscribe(paramMap => this.load(paramMap.get("gameId"))) - ); + private appTitle: AppTitleService, + private gameService: GameService, + private routerService: RouterService) { + unsub.add(this.activatedRoute.paramMap.subscribe(async paramMap => { + const gameId = paramMap.get("gameId") || this.gameCenterCtx?.id; + if (gameId && gameId != this.gameCenterCtx?.id) + await this.load(gameId); + })); + + unsub.add(interval(30000).subscribe(async () => { + if (this.game && this.game?.isLive) + this.gameCenterCtx = await this.adminService.getGameCenterContext(this.game.id); + })); + + // listen for updates from the game service + // rather than actually reload, we just pick off matching properties between the ctx and the game + // (arguably should restructure ctx to use the same model) + unsub.add(gameService.gameUpdated$.subscribe(async game => { + if (game.id === this.gameCenterCtx?.id) { + this.gameCenterCtx = { + ...this.gameCenterCtx, + ...game, + isPractice: game.isPracticeMode + }; + } + })); + + // route changes + unsub.add(this.activatedRoute.paramMap.subscribe(params => { + this.selectedTab = params.get("selectedTab") as GameCenterTab || "settings"; + })); + } + + protected handleSelect(tab: GameCenterTab) { + if (!this.gameCenterCtx) + return; + + this.routerService.toGameCenter(this.gameCenterCtx.id, tab); } private async load(gameId: string | null) { - if (gameId === null || gameId == this.game?.id) + if (!gameId) return; - this.game = await firstValueFrom(this.gameService.retrieve(gameId)); - this.gameCenterCtx = await this.adminService.getGameCenterContext(gameId); + if (gameId) + this.gameCenterCtx = await this.adminService.getGameCenterContext(gameId); + + if (gameId !== this.game?.id) + this.game = await firstValueFrom(this.gameService.retrieve(gameId)); + + this.appTitle.set(this.game.name); } } diff --git a/projects/gameboard-ui/src/app/admin/components/game-center/game-center.models.ts b/projects/gameboard-ui/src/app/admin/components/game-center/game-center.models.ts new file mode 100644 index 00000000..e65001d0 --- /dev/null +++ b/projects/gameboard-ui/src/app/admin/components/game-center/game-center.models.ts @@ -0,0 +1,139 @@ +import { DateTime } from "luxon"; +import { PagedArray, SimpleEntity, SimpleSponsor } from "@/api/models"; +import { Score } from "@/services/scoring/scoring.models"; +import { ChallengeResult } from "@/api/board-models"; + +export type GameCenterPracticeSessionStatus = "playing" | "notPlaying"; +export type GameCenterPracticeSort = "attemptCount" | "name"; + +export type GameCenterTab = "settings" | "challenges" | "teams" | "deployment" | "practice" | "observe" | "tickets" | "scoreboard"; +export type GameCenterTeamsAdvancementFilter = "advancedFromPreviousGame" | "advancedToNextGame" +export type GameCenterTeamsSort = "name" | "rank" | "timeRemaining" | "timeSinceStart"; +export type GameCenterTeamSessionStatus = "complete" | "notStarted" | "playing"; + +export interface GameCenterContext { + id: string; + name: string; + logo?: string; + competition: string | null; + season: string | null; + track: string | null; + executionWindow: { + start: DateTime, + end: DateTime + }, + hasScoreboard: boolean; + isExternal: boolean; + isLive: boolean; + isPractice: boolean; + isPublished: boolean; + isRegistrationActive: boolean; + isTeamGame: boolean; + stats: { + attemptCountPractice: number; + playerCountActive: number; + playerCountCompetitive: number; + playerCountPractice: number; + playerCountTotal: number; + teamCountActive: number; + teamCountCompetitive: number; + teamCountPractice: number; + teamCountNotStarted: number; + teamCountTotal: number; + topScore?: number; + topScoreTeamName?: string; + }; + + challengeCount: number; + openTicketCount: number; + pointsAvailable: number; +} + +export interface GameCenterPracticeContext { + game: SimpleEntity; + users: GameCenterPracticeContextUser[]; +} + +export interface GameCenterPracticeChallengeSpec { + id: string; + name: string; + tag?: string; + activeAttempt: { + attemptTimestamp: number; + result: ChallengeResult; + score: number; + } + attemptCount: number; + lastAttemptDate?: number; + bestAttempt?: { + attemptTimestamp: number; + result: ChallengeResult; + score: number; + } +} + +export interface GameCenterPracticeContextUser { + id: string; + name: string; + sponsor: SimpleSponsor; + totalAttempts: number; + activeChallenge?: SimpleEntity; + activeChallengeEndTimestamp?: number; + uniqueChallengeSpecs: number; + challengeSpecs: GameCenterPracticeChallengeSpec[] +} + +export interface GameCenterTeamsRequestArgs { + advancement?: GameCenterTeamsAdvancementFilter; + searchTerm?: string; + sessionStatus?: GameCenterTeamSessionStatus; + sort?: GameCenterTeamsSort; +} + +export interface GameCenterTeamsResults { + playerCountActive: number; + playerCountTotal: number; + teamCountActive: number; + teamCountTotal: number; + teams: PagedArray; +} + +export interface GameCenterTeamsResultsTeam { + id: string; + name: string; + + captain: GameCenterTeamsPlayer; + challengesCompleteCount: number; + challengesPartialCount: number; + challengesRemainingCount: number; + isExtended: boolean; + isReady: boolean; + players: GameCenterTeamsPlayer[]; + rank?: number; + registeredOn: number; + score: Score; + session: GameCenterTeamsSession; + ticketCount: number; +} + +export interface GameCenterTeamsPlayer { + id: string; + name: string; + isReady: boolean; + sponsor: SimpleSponsor; +} + +export interface GameCenterTeamsSession { + start?: number; + end?: number; + timeRemainingMs?: number; + timeSinceStartMs?: number; +} + +export type GameCenterTeamsStatus = "complete" | "notStarted" | "playing"; + +export interface GetGameCenterPracticeContextRequest { + searchTerm?: string; + sessionStatus?: GameCenterPracticeSessionStatus; + sort?: GameCenterPracticeSort; +} diff --git a/projects/gameboard-ui/src/app/admin/components/site-overview-stats/site-overview-stats.component.html b/projects/gameboard-ui/src/app/admin/components/site-overview-stats/site-overview-stats.component.html index 66da1b8f..f4e4b36c 100644 --- a/projects/gameboard-ui/src/app/admin/components/site-overview-stats/site-overview-stats.component.html +++ b/projects/gameboard-ui/src/app/admin/components/site-overview-stats/site-overview-stats.component.html @@ -12,6 +12,7 @@ (click)="showTeamsModal()">
  • - +
  • diff --git a/projects/gameboard-ui/src/app/admin/components/site-overview-stats/site-overview-stats.component.ts b/projects/gameboard-ui/src/app/admin/components/site-overview-stats/site-overview-stats.component.ts index 8c3d2680..f5047ad8 100644 --- a/projects/gameboard-ui/src/app/admin/components/site-overview-stats/site-overview-stats.component.ts +++ b/projects/gameboard-ui/src/app/admin/components/site-overview-stats/site-overview-stats.component.ts @@ -36,7 +36,7 @@ export class SiteOverviewStatsComponent implements OnInit { context: { playerMode: playerMode == "practice" ? PlayerMode.practice : PlayerMode.competition }, - modalClasses: ["modal-dialog-centered", "modal-xl"] + modalClasses: ["modal-xl"] }); } @@ -44,7 +44,7 @@ export class SiteOverviewStatsComponent implements OnInit { this.modalService.openComponent({ content: ActiveTeamsModalComponent, context: {}, - modalClasses: ["modal-dialog-centered", "modal-xl"] + modalClasses: ["modal-xl"] }); } } diff --git a/projects/gameboard-ui/src/app/admin/components/team-admin-context-menu/team-admin-context-menu.component.html b/projects/gameboard-ui/src/app/admin/components/team-admin-context-menu/team-admin-context-menu.component.html index 9bba1740..04afadc1 100644 --- a/projects/gameboard-ui/src/app/admin/components/team-admin-context-menu/team-admin-context-menu.component.html +++ b/projects/gameboard-ui/src/app/admin/components/team-admin-context-menu/team-admin-context-menu.component.html @@ -1,69 +1,64 @@ -
    +
    diff --git a/projects/gameboard-ui/src/app/admin/player-registrar/player-registrar.component.ts b/projects/gameboard-ui/src/app/admin/player-registrar/player-registrar.component.ts index 208dc596..87406b76 100644 --- a/projects/gameboard-ui/src/app/admin/player-registrar/player-registrar.component.ts +++ b/projects/gameboard-ui/src/app/admin/player-registrar/player-registrar.component.ts @@ -1,9 +1,8 @@ // Copyright 2021 Carnegie Mellon University. All Rights Reserved. // Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. -import { Component } from '@angular/core'; +import { Component, Input } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { BsModalService } from 'ngx-bootstrap/modal'; import { asyncScheduler, BehaviorSubject, combineLatest, firstValueFrom, interval, Observable, scheduled, timer } from 'rxjs'; import { debounceTime, filter, first, map, mergeAll, switchMap, tap } from 'rxjs/operators'; import { Game } from '../../api/game-models'; @@ -13,16 +12,14 @@ import { PlayerService } from '../../api/player.service'; import { fa } from '@/services/font-awesome.service'; import { ModalConfirmService } from '../../services/modal-confirm.service'; import { ClipboardService } from '../../utility/services/clipboard.service'; -import { ManageManualChallengeBonusesModalComponent } from '../components/manage-manual-challenge-bonuses-modal/manage-manual-challenge-bonuses-modal.component'; import { TeamService } from '@/api/team.service'; -import { TeamAdminContextMenuSessionResetRequest } from '../components/team-admin-context-menu/team-admin-context-menu.component'; +import { TeamAdminContextMenuTeam } from '../components/team-admin-context-menu/team-admin-context-menu.component'; import { AppTitleService } from '@/services/app-title.service'; import { UnsubscriberService } from '@/services/unsubscriber.service'; import { ExtendTeamsModalComponent } from '../components/extend-teams-modal/extend-teams-modal.component'; import { unique } from 'projects/gameboard-ui/src/tools'; import { ToastService } from '@/utility/services/toast.service'; import { AdminEnrollTeamModalComponent } from '../components/admin-enroll-team-modal/admin-enroll-team-modal.component'; -import { GameSessionService } from '@/services/game-session.service'; @Component({ selector: 'app-player-registrar', @@ -31,6 +28,7 @@ import { GameSessionService } from '@/services/game-session.service'; providers: [UnsubscriberService] }) export class PlayerRegistrarComponent { + @Input() gameId?: string; refresh$ = new BehaviorSubject(true); game!: Game; ctx$: Observable<{ game: Game, advanceTargetGames: Game[], players: Player[] }>; @@ -66,8 +64,6 @@ export class PlayerRegistrarComponent { constructor( route: ActivatedRoute, private gameapi: GameService, - private gameSessionService: GameSessionService, - private bsModalService: BsModalService, private modalConfirmService: ModalConfirmService, private api: PlayerService, private clipboard: ClipboardService, @@ -79,8 +75,8 @@ export class PlayerRegistrarComponent { const game$ = route.params.pipe( debounceTime(500), - filter(p => !!p.id), - switchMap(p => gameapi.retrieve(p.id)), + filter(p => !!this.gameId || !!p.id), + switchMap(p => gameapi.retrieve(this.gameId || p.id)), tap(r => this.game = r), tap(r => this.teamView = r.allowTeam ? 'collapse' : ''), tap(r => this.title.set(`Players: ${r.name}`)) @@ -95,7 +91,7 @@ export class PlayerRegistrarComponent { ]).pipe( tap(() => this.isLoading = true), debounceTime(500), - tap(([a, b, c]) => this.search.gid = a.id), + tap(([a, b, c]) => this.search.gid = (this.gameId || a.id)), switchMap(() => this.api.list(this.search)), tap(r => this.source = r), tap(() => this.isLoading = false), @@ -106,9 +102,7 @@ export class PlayerRegistrarComponent { const players$ = scheduled([ fetch$, interval(1000).pipe(map(() => this.source)) - ], asyncScheduler).pipe( - mergeAll() - ); + ], asyncScheduler).pipe(mergeAll()); this.ctx$ = combineLatest([ game$, @@ -290,56 +284,7 @@ export class PlayerRegistrarComponent { }); } - protected handlePlayerChange(player: Player) { - this.refresh$.next(true); - } - - protected confirmReset(request: TeamAdminContextMenuSessionResetRequest) { - // this is all really window-dressing around the "reset" operation, which we describe as an unenroll if the team's session hasn't started - const isUnenroll = request.resetType === "unenrollAndArchiveChallenges" && this.gameSessionService.canUnenroll(request.player); - let confirmMessage = `Are you sure you want to unenroll ${this.game.allowTeam ? " team" : ""} **${request.player.approvedName}** from the game?`; - let confirmTitle = `Unenroll ${request.player.approvedName}?`; - let confirmToast = `**${request.player.approvedName}** has been unenrolled.`; - - if (!isUnenroll) { - confirmMessage = `Are you sure you want to reset the session for ${this.game.allowTeam ? " team" : ""} **${request.player.approvedName}**?`; - confirmTitle = `Reset ${request.player.approvedName}'s session?`; - confirmToast = `${this.game.allowTeam ? "Team " : ""}**${request.player.approvedName}**'s session has been reset.`; - - // accommodate various "types" of reset that can happen (e.g. keep challenges, don't keep challenges, destroy the universe and the unenroll) - if (request.resetType === "preserveChallenges") - confirmMessage += " Their challenges **won't** be archived automatically, and they'll remain enrolled in the game."; - else if (request.resetType == "unenrollAndArchiveChallenges") - confirmMessage += " Their challenges will be archived, and they'll be unenrolled from the game."; - } - - this.modalConfirmService.openConfirm({ - bodyContent: confirmMessage, - renderBodyAsMarkdown: true, - title: confirmTitle, - onConfirm: () => { - this.resetSession(request); - this.toastService.showMessage(confirmToast); - }, - confirmButtonText: "Yes, reset", - cancelButtonText: "No, don't reset" - }); - } - - protected manageManualBonuses(player: Player) { - this.bsModalService.show(ManageManualChallengeBonusesModalComponent, { - class: "modal-xl", - initialState: { - gameName: player.gameName, - playerName: player.approvedName, - teamId: player.teamId - } - }); - } - - private async resetSession(request: TeamAdminContextMenuSessionResetRequest): Promise { - this.isLoading = true; - await firstValueFrom(this.teamService.resetSession({ teamId: request.player.teamId, resetType: request.resetType })); + protected handleTeamUpdated(team: TeamAdminContextMenuTeam) { this.refresh$.next(true); } } diff --git a/projects/gameboard-ui/src/app/admin/practice/practice-settings/practice-settings.component.html b/projects/gameboard-ui/src/app/admin/practice/practice-settings/practice-settings.component.html index d3bcce6c..faff2fc5 100644 --- a/projects/gameboard-ui/src/app/admin/practice/practice-settings/practice-settings.component.html +++ b/projects/gameboard-ui/src/app/admin/practice/practice-settings/practice-settings.component.html @@ -73,7 +73,7 @@

    Session Limits

    diff --git a/projects/gameboard-ui/src/app/admin/practice/practice-settings/practice-settings.component.ts b/projects/gameboard-ui/src/app/admin/practice/practice-settings/practice-settings.component.ts index a91f99b3..72d05407 100644 --- a/projects/gameboard-ui/src/app/admin/practice/practice-settings/practice-settings.component.ts +++ b/projects/gameboard-ui/src/app/admin/practice/practice-settings/practice-settings.component.ts @@ -59,7 +59,7 @@ export class PracticeSettingsComponent implements OnInit { this.modalService.open({ title: "Creating a certificate template", bodyContent: this.certificateHtmlPlaceholder, - modalClasses: ["modal-lg", "modal-dialog-centered"], + modalClasses: ["modal-lg"], renderBodyAsMarkdown: true }); } diff --git a/projects/gameboard-ui/src/app/admin/team-observer/team-observer.component.html b/projects/gameboard-ui/src/app/admin/team-observer/team-observer.component.html index b9a8fe8d..c8a0cc6f 100644 --- a/projects/gameboard-ui/src/app/admin/team-observer/team-observer.component.html +++ b/projects/gameboard-ui/src/app/admin/team-observer/team-observer.component.html @@ -1,4 +1,4 @@ - + Back @@ -7,12 +7,14 @@ Refresh -
    + +

    Observe {{game.allowTeam ? 'Teams' : 'Players'}}

    Observe Challenges
    +
    diff --git a/projects/gameboard-ui/src/app/admin/team-observer/team-observer.component.ts b/projects/gameboard-ui/src/app/admin/team-observer/team-observer.component.ts index c8bffa70..2048e09b 100644 --- a/projects/gameboard-ui/src/app/admin/team-observer/team-observer.component.ts +++ b/projects/gameboard-ui/src/app/admin/team-observer/team-observer.component.ts @@ -1,9 +1,9 @@ import { KeyValue } from '@angular/common'; -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { faArrowLeft, faSyncAlt, faTv, faExternalLinkAlt, faExpandAlt, faUser, faThLarge, faMinusSquare, faPlusSquare, faCompressAlt, faSortAlphaDown, faSortAmountDownAlt, faAngleDoubleUp, faUsers, faWindowMaximize, faBullseye, faWindowRestore } from '@fortawesome/free-solid-svg-icons'; import { combineLatest, timer, BehaviorSubject, Observable, Subscription } from 'rxjs'; -import { debounceTime, tap, switchMap, map } from 'rxjs/operators'; +import { debounceTime, tap, switchMap, map, filter } from 'rxjs/operators'; import { ConsoleActor } from '../../api/board-models'; import { BoardService } from '../../api/board.service'; import { Game } from '../../api/game-models'; @@ -17,7 +17,9 @@ import { ConfigService } from '../../utility/config.service'; templateUrl: './team-observer.component.html', styleUrls: ['./team-observer.component.scss'] }) -export class TeamObserverComponent implements OnDestroy { +export class TeamObserverComponent implements OnInit, OnDestroy { + @Input() gameId?: string; + refresh$ = new BehaviorSubject(true); game?: Game; // game info like team or individual table: Map = new Map(); // table of teams to display @@ -48,6 +50,7 @@ export class TeamObserverComponent implements OnDestroy { faAngleDoubleUp = faAngleDoubleUp; faWindowRestore = faWindowRestore; + protected isLegacyMode = false; protected searchText = ""; constructor( @@ -59,6 +62,7 @@ export class TeamObserverComponent implements OnDestroy { ) { this.mksHost = conf.mkshost; this.gameData = route.params.pipe( + filter(a => !!a.id), switchMap(a => this.gameApi.retrieve(a.id)) ).subscribe(game => this.game = game); this.tableData = combineLatest([ @@ -67,7 +71,7 @@ export class TeamObserverComponent implements OnDestroy { timer(0, 60_000) // *every 60 sec* refresh challenge data (score/duration updates and new deploys) ]).pipe( debounceTime(500), - tap(([a, b, c]) => this.gid = a.id), + tap(([a, b, c]) => this.gid = this.gameId || a.id), tap(() => this.isLoading = true), switchMap(() => this.playerApi.observeTeams(this.gid)) // tomorrow do this instead of players ).subscribe(data => { @@ -88,9 +92,11 @@ export class TeamObserverComponent implements OnDestroy { switchMap(() => this.api.consoleActors(this.gid)), map(data => new Map(data.map(i => [i.userId, i]))) ); - this.term$ = this.typing$.pipe( - debounceTime(500) - ); + this.term$ = this.typing$.pipe(debounceTime(500)); + } + + public ngOnInit(): void { + this.isLegacyMode = !this.gameId; } updateTable(data: Team[]) { diff --git a/projects/gameboard-ui/src/app/api/admin.models.ts b/projects/gameboard-ui/src/app/api/admin.models.ts index 955d421f..e2f9e1f7 100644 --- a/projects/gameboard-ui/src/app/api/admin.models.ts +++ b/projects/gameboard-ui/src/app/api/admin.models.ts @@ -1,6 +1,6 @@ import { DateTime } from "luxon"; import { GameEngineType } from "./spec-models"; -import { PagedArray, SimpleEntity, SimpleSponsor } from "./models"; +import { DateTimeRange, SimpleEntity } from "./models"; export interface AppActiveChallengeSpec { id: string; @@ -54,69 +54,24 @@ export interface AppActiveTeam { score: number; } -export interface GameCenterContext { +export interface GetPlayersCsvExportResponsePlayer { id: string; name: string; - logo?: string; - competition: string | null; - season: string | null; - track: string | null; - executionWindow: { - start: DateTime, - end: DateTime - }, - isExternal: boolean; - isLive: boolean; - isPractice: boolean; - isRegistrationActive: boolean; - isTeamGame: boolean; - - challengeCount: number; - openTicketCount: number; - pointsAvailable: number; -} - -export interface GameCenterTeamsRequestArgs { - sort: "name" | "rank" | "timeRemaining" | "timeSinceStart"; -} - -export interface GameCenterTeamsResults { - teams: PagedArray; -} - -export interface GameCenterTeamsResultsTeam { - id: string; - name: string; - - captain: GameCenterTeamsPlayer; - challengesCompleteCount: number; - challengesPartialCount: number; - challengesRemainingCount: number; - isExtended: boolean; - isReady: boolean; - players: GameCenterTeamsPlayer[]; + game: SimpleEntity; rank?: number; - registeredOn?: DateTime; - session: GameCenterTeamsSession; - ticketCount: number; -} - -export interface GameCenterTeamsPlayer { - id: string; - name: string; - isReady: boolean; - sponsor: SimpleSponsor; + score?: number; + session: DateTimeRange; + solvesCorrectCount: number; + solvesPartialCount: number; + team: SimpleEntity; + timeMs: number; + user: SimpleEntity; } -export interface GameCenterTeamsSession { - start?: number; - end?: number; - timeRemainingMs?: number; - timeSinceStartMs?: number; +export interface GetPlayersCsvExportResponse { + players: GetPlayersCsvExportResponsePlayer[]; } -export type GameCenterTeamsStatus = "complete" | "notStarted" | "playing"; - export interface GetSiteOverviewStatsResponse { activeCompetitiveChallenges: number; activePracticeChallenges: number; diff --git a/projects/gameboard-ui/src/app/api/admin.service.ts b/projects/gameboard-ui/src/app/api/admin.service.ts index 4b822d6d..85fbf1e1 100644 --- a/projects/gameboard-ui/src/app/api/admin.service.ts +++ b/projects/gameboard-ui/src/app/api/admin.service.ts @@ -1,10 +1,11 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; +import { DateTime } from 'luxon'; import { Observable, firstValueFrom, map, tap } from 'rxjs'; import { ApiUrlService } from '@/services/api-url.service'; -import { GameCenterContext, GameCenterTeamsRequestArgs, GameCenterTeamsResults, GetAppActiveChallengesResponse, GetAppActiveTeamsResponse, GetSiteOverviewStatsResponse, SendAnnouncement } from './admin.models'; +import { GetAppActiveChallengesResponse, GetAppActiveTeamsResponse, GetPlayersCsvExportResponse, GetPlayersCsvExportResponsePlayer, GetSiteOverviewStatsResponse, SendAnnouncement } from './admin.models'; import { PlayerMode } from './player-models'; -import { DateTime } from 'luxon'; +import { GameCenterContext, GameCenterPracticeContext, GameCenterPracticeSessionStatus, GameCenterTeamsRequestArgs, GameCenterTeamsResults, GetGameCenterPracticeContextRequest } from '@/admin/components/game-center/game-center.models'; @Injectable({ providedIn: 'root' }) export class AdminService { @@ -39,6 +40,19 @@ export class AdminService { ); } + async getPlayersExport(gameId: string, teamIds?: string[]): Promise { + return firstValueFrom(this.http.get(this.apiUrl.build(`admin/games/${gameId}/players/export`, { gameId, teamIds })).pipe( + map(res => { + for (const player of res.players) { + player.session.start = player.session.start ? DateTime.fromISO(player.session.start.toString()) : undefined; + player.session.end = player.session.end ? DateTime.fromISO(player.session.end.toString()) : undefined; + } + + return res.players; + }) + )); + } + async getGameCenterContext(gameId: string): Promise { return firstValueFrom(this.http.get(this.apiUrl.build(`admin/games/${gameId}/game-center`)).pipe( tap(ctx => { @@ -48,15 +62,12 @@ export class AdminService { )); } + async getGameCenterPracticeContext(gameId: string, args: GetGameCenterPracticeContextRequest): Promise { + return firstValueFrom(this.http.get(this.apiUrl.build(`admin/games/${gameId}/game-center/practice`, args))); + } + async getGameCenterTeams(gameId: string, args: GameCenterTeamsRequestArgs): Promise { - return firstValueFrom(this.http.get(this.apiUrl.build(`admin/games/${gameId}/game-center/teams`, args)).pipe( - tap(results => { - for (const team of results.teams.items) { - if (team.registeredOn) - team.registeredOn = DateTime.fromJSDate(new Date(team.registeredOn?.toString())); - } - }) - )); + return firstValueFrom(this.http.get(this.apiUrl.build(`admin/games/${gameId}/game-center/teams`, args))); } getOverallSiteStats(): Observable { diff --git a/projects/gameboard-ui/src/app/api/game.service.ts b/projects/gameboard-ui/src/app/api/game.service.ts index f930741b..2ec175b1 100644 --- a/projects/gameboard-ui/src/app/api/game.service.ts +++ b/projects/gameboard-ui/src/app/api/game.service.ts @@ -3,7 +3,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { Observable, firstValueFrom, of } from 'rxjs'; +import { Observable, Subject, firstValueFrom, of } from 'rxjs'; import { map, tap } from 'rxjs/operators'; import { SyncStartGameState } from '../game/game.models'; import { ConfigService } from '../utility/config.service'; @@ -14,9 +14,13 @@ import { Spec } from './spec-models'; @Injectable({ providedIn: 'root' }) export class GameService { + url = ''; private cache: CachedGame[] = []; + private _gameUpdated$ = new Subject(); + public gameUpdated$ = this._gameUpdated$.asObservable(); + constructor( private http: HttpClient, private config: ConfigService @@ -61,7 +65,8 @@ export class GameService { public update(model: ChangedGame): Observable { return this.http.put(`${this.url}/game`, model).pipe( - tap(m => this.removeCache(model.id)) + tap(m => this.removeCache(model.id)), + tap(m => this._gameUpdated$.next(m)) ); } diff --git a/projects/gameboard-ui/src/app/api/models.ts b/projects/gameboard-ui/src/app/api/models.ts index 8027213a..85964ac2 100644 --- a/projects/gameboard-ui/src/app/api/models.ts +++ b/projects/gameboard-ui/src/app/api/models.ts @@ -5,6 +5,17 @@ import { SortDirection } from "@/core/models/sort-direction"; import { Game } from "./game-models"; import { Player } from "./player-models"; import { ApiUser } from "./user-models"; +import { DateTime } from "luxon"; + +export interface DateTimeRange { + start?: DateTime; + end?: DateTime; +} + +export interface EpochTimeRange { + start?: number; + end?: number; +} export interface Search { term?: string; diff --git a/projects/gameboard-ui/src/app/api/support.service.ts b/projects/gameboard-ui/src/app/api/support.service.ts index f5b9c6ce..5e5821d0 100644 --- a/projects/gameboard-ui/src/app/api/support.service.ts +++ b/projects/gameboard-ui/src/app/api/support.service.ts @@ -12,6 +12,7 @@ import { AttachmentFile, ChangedTicket, NewTicket, NewTicketComment, SupportSett import { UserSummary } from './user-models'; import { Search } from './models'; import { ApiUrlService } from '@/services/api-url.service'; +import { cloneNonNullAndDefinedProperties } from '@/tools/object-tools.lib'; @Injectable({ providedIn: 'root' }) export class SupportService { @@ -29,7 +30,8 @@ export class SupportService { } public list(search: any): Observable { - return this.http.get(`${this.url}/ticket/list`, { params: search }).pipe( + const params = !search ? {} : cloneNonNullAndDefinedProperties(search); + return this.http.get(`${this.url}/ticket/list`, { params }).pipe( map(r => this.transform(r)) ); } diff --git a/projects/gameboard-ui/src/app/app-routing.module.ts b/projects/gameboard-ui/src/app/app-routing.module.ts index bf0233fb..d205a729 100644 --- a/projects/gameboard-ui/src/app/app-routing.module.ts +++ b/projects/gameboard-ui/src/app/app-routing.module.ts @@ -46,12 +46,10 @@ const routes: Routes = [ loadChildren: () => import('./home/home.module').then(m => m.HomeModule) }, { path: '', component: HomePageComponent, pathMatch: 'full' } - ]; @NgModule({ imports: [RouterModule.forRoot(routes)], - exports: [RouterModule], providers: [ { provide: TitleStrategy, diff --git a/projects/gameboard-ui/src/app/core/components/avatar/avatar.component.ts b/projects/gameboard-ui/src/app/core/components/avatar/avatar.component.ts index 17f1b19a..17acf94a 100644 --- a/projects/gameboard-ui/src/app/core/components/avatar/avatar.component.ts +++ b/projects/gameboard-ui/src/app/core/components/avatar/avatar.component.ts @@ -7,13 +7,15 @@ export type AvatarSize = 'tiny' | 'small' | 'medium' | 'large'; selector: 'app-avatar', styleUrls: ['./avatar.component.scss'], template: ` -
    + + + +
    `, }) export class AvatarComponent implements OnChanges { @Input() imageUrl?: SafeUrl; @Input() size: AvatarSize = "medium"; - @Input() tooltip = ""; @ViewChild("searchBox") searchBox?: ElementRef; diff --git a/projects/gameboard-ui/src/app/core/components/big-stat/big-stat.component.html b/projects/gameboard-ui/src/app/core/components/big-stat/big-stat.component.html index 58df1c5f..cd963114 100644 --- a/projects/gameboard-ui/src/app/core/components/big-stat/big-stat.component.html +++ b/projects/gameboard-ui/src/app/core/components/big-stat/big-stat.component.html @@ -1,9 +1,9 @@
    -
    +
    {{ value === undefined || value === null ? "--" : value }}
    {{ label }}
    -
    +
    {{subLabel}}
    diff --git a/projects/gameboard-ui/src/app/core/components/big-stat/big-stat.component.scss b/projects/gameboard-ui/src/app/core/components/big-stat/big-stat.component.scss index 09ce11b5..0c9e20f2 100644 --- a/projects/gameboard-ui/src/app/core/components/big-stat/big-stat.component.scss +++ b/projects/gameboard-ui/src/app/core/components/big-stat/big-stat.component.scss @@ -1,8 +1,17 @@ @import "../../../../scss/variables"; +.big-stat-component { + padding: 4px 0; +} + .value { font-size: 4rem; font-weight: bold; + line-height: 4.2rem; +} + +.stat-button .value { + line-height: unset; } .stat-button { @@ -12,3 +21,7 @@ border-bottom: dotted 3px $info; } } + +.label { + padding: 0 !important; +} diff --git a/projects/gameboard-ui/src/app/core/components/big-stat/big-stat.component.ts b/projects/gameboard-ui/src/app/core/components/big-stat/big-stat.component.ts index 55cfee32..5b5f5bcb 100644 --- a/projects/gameboard-ui/src/app/core/components/big-stat/big-stat.component.ts +++ b/projects/gameboard-ui/src/app/core/components/big-stat/big-stat.component.ts @@ -7,7 +7,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; }) export class BigStatComponent { @Input() label = ""; - @Input() value: string | number = ""; - @Input() subLabel = ""; + @Input() value: string | number | undefined = ""; + @Input() subLabel? = ""; @Input() isClickable = false; } diff --git a/projects/gameboard-ui/src/app/core/components/long-content-hider/long-content-hider.component.html b/projects/gameboard-ui/src/app/core/components/long-content-hider/long-content-hider.component.html index 11bd0fff..b03a7dec 100644 --- a/projects/gameboard-ui/src/app/core/components/long-content-hider/long-content-hider.component.html +++ b/projects/gameboard-ui/src/app/core/components/long-content-hider/long-content-hider.component.html @@ -12,4 +12,4 @@
    -
    \ No newline at end of file +
    diff --git a/projects/gameboard-ui/src/app/core/components/long-content-hider/long-content-hider.component.ts b/projects/gameboard-ui/src/app/core/components/long-content-hider/long-content-hider.component.ts index 7987f1ab..a6e6924b 100644 --- a/projects/gameboard-ui/src/app/core/components/long-content-hider/long-content-hider.component.ts +++ b/projects/gameboard-ui/src/app/core/components/long-content-hider/long-content-hider.component.ts @@ -7,39 +7,43 @@ import { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular }) export class LongContentHiderComponent implements AfterViewInit { @Input() defaultExpanded = false; - @Input() maxHeightCollapsed: string = "15rem"; + @Input() maxHeightCollapsed = "15rem"; @ViewChild("contentContainer") contentContainer!: ElementRef; - protected isExpandEnabled = true; - protected isExpanded = false; - private nativeElement!: HTMLParagraphElement; + private nativeElement!: HTMLDivElement; - public ngAfterViewInit(): void { - this.nativeElement = this.contentContainer.nativeElement as HTMLParagraphElement; + protected isExpanded = false; + protected isExpandEnabled = true; + public ngOnInit() { if (this.defaultExpanded) { - this.toggleExpanded(); + this.isExpanded = true; } + } + public ngAfterViewInit(): void { + this.nativeElement = this.contentContainer.nativeElement as HTMLDivElement; + } + + public ngAfterContentChecked() { // determine if we need to show the expand/collapse control at all this.setIsExpandEnabled(); } + protected setIsExpandEnabled() { + // if the client height (the space the element is actually taking up) is equal to the scroll height (the amount of space + // the element WANTS to take up), we don't need the expand/collapse controls. + this.isExpandEnabled = this.nativeElement && this.nativeElement.clientHeight < this.nativeElement.scrollHeight; + } + protected toggleExpanded() { + if (!this.isExpandEnabled) + throw new Error(`Can't toggle visibility of a ${LongContentHiderComponent.name} - expand is disabled.`); + if (!this.nativeElement) { throw new Error(`Can't toggle visibility of a ${LongContentHiderComponent.name} - not resolved.`); } this.isExpanded = !this.isExpanded; } - - // if the client height (the space the element is actually taking up) is equal to the scroll height (the amount of space - // the element WANTS to take up), we don't need the expand/collapse controls. - protected setIsExpandEnabled() { - this.isExpandEnabled = this.nativeElement && this.nativeElement.clientHeight < this.nativeElement.scrollHeight; - - if (!this.isExpandEnabled) { - this.isExpanded = true; - } - } } diff --git a/projects/gameboard-ui/src/app/core/components/observer-console/observer-console.component.html b/projects/gameboard-ui/src/app/core/components/observer-console/observer-console.component.html new file mode 100644 index 00000000..5ff28020 --- /dev/null +++ b/projects/gameboard-ui/src/app/core/components/observer-console/observer-console.component.html @@ -0,0 +1 @@ +

    observer-console works!

    diff --git a/projects/gameboard-ui/src/app/core/components/observer-console/observer-console.component.scss b/projects/gameboard-ui/src/app/core/components/observer-console/observer-console.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/projects/gameboard-ui/src/app/core/components/observer-console/observer-console.component.ts b/projects/gameboard-ui/src/app/core/components/observer-console/observer-console.component.ts new file mode 100644 index 00000000..d35b0bc6 --- /dev/null +++ b/projects/gameboard-ui/src/app/core/components/observer-console/observer-console.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-observer-console', + templateUrl: './observer-console.component.html', + styleUrls: ['./observer-console.component.scss'] +}) +export class ObserverConsoleComponent { + +} diff --git a/projects/gameboard-ui/src/app/support/components/ticket-label-picker/ticket-label-picker.component.html b/projects/gameboard-ui/src/app/core/components/ticket-label-picker/ticket-label-picker.component.html similarity index 100% rename from projects/gameboard-ui/src/app/support/components/ticket-label-picker/ticket-label-picker.component.html rename to projects/gameboard-ui/src/app/core/components/ticket-label-picker/ticket-label-picker.component.html diff --git a/projects/gameboard-ui/src/app/core/components/ticket-label-picker/ticket-label-picker.component.scss b/projects/gameboard-ui/src/app/core/components/ticket-label-picker/ticket-label-picker.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/projects/gameboard-ui/src/app/support/components/ticket-label-picker/ticket-label-picker.component.ts b/projects/gameboard-ui/src/app/core/components/ticket-label-picker/ticket-label-picker.component.ts similarity index 90% rename from projects/gameboard-ui/src/app/support/components/ticket-label-picker/ticket-label-picker.component.ts rename to projects/gameboard-ui/src/app/core/components/ticket-label-picker/ticket-label-picker.component.ts index ea2d6e6d..04362369 100644 --- a/projects/gameboard-ui/src/app/support/components/ticket-label-picker/ticket-label-picker.component.ts +++ b/projects/gameboard-ui/src/app/core/components/ticket-label-picker/ticket-label-picker.component.ts @@ -1,7 +1,7 @@ import { Component, EventEmitter, OnInit, Output } from '@angular/core'; import { fa } from "@/services/font-awesome.service"; import { ModalConfirmService } from '@/services/modal-confirm.service'; -import { TicketLabelPickerModalComponent } from '../ticket-label-picker-modal/ticket-label-picker-modal.component'; +import { TicketLabelPickerModalComponent } from '../../../support/components/ticket-label-picker-modal/ticket-label-picker-modal.component'; @Component({ selector: 'app-ticket-label-picker', diff --git a/projects/gameboard-ui/src/app/support/ticket-list/ticket-list.component.html b/projects/gameboard-ui/src/app/core/components/ticket-list/ticket-list.component.html similarity index 96% rename from projects/gameboard-ui/src/app/support/ticket-list/ticket-list.component.html rename to projects/gameboard-ui/src/app/core/components/ticket-list/ticket-list.component.html index 0a8fb245..6ae8c784 100644 --- a/projects/gameboard-ui/src/app/support/ticket-list/ticket-list.component.html +++ b/projects/gameboard-ui/src/app/core/components/ticket-list/ticket-list.component.html @@ -7,7 +7,7 @@

    {{ctx.canManage ? 'Tickets' : 'My Tickets'}}

    - + Create Ticket
    @@ -44,6 +44,8 @@

    {{ctx.canManage ? 'Tickets' : 'My Tickets'}}

    +
    +
    {{ctx.canManage ? 'Tickets' : 'My Tickets'}} - {{ticket.fullKey}} + {{ticket.fullKey}}
    - + {{ticket.summary || "No summary"}} diff --git a/projects/gameboard-ui/src/app/support/ticket-list/ticket-list.component.scss b/projects/gameboard-ui/src/app/core/components/ticket-list/ticket-list.component.scss similarity index 100% rename from projects/gameboard-ui/src/app/support/ticket-list/ticket-list.component.scss rename to projects/gameboard-ui/src/app/core/components/ticket-list/ticket-list.component.scss diff --git a/projects/gameboard-ui/src/app/support/ticket-list/ticket-list.component.ts b/projects/gameboard-ui/src/app/core/components/ticket-list/ticket-list.component.ts similarity index 88% rename from projects/gameboard-ui/src/app/support/ticket-list/ticket-list.component.ts rename to projects/gameboard-ui/src/app/core/components/ticket-list/ticket-list.component.ts index 5aa9ed2b..23e540ff 100644 --- a/projects/gameboard-ui/src/app/support/ticket-list/ticket-list.component.ts +++ b/projects/gameboard-ui/src/app/core/components/ticket-list/ticket-list.component.ts @@ -1,16 +1,16 @@ -import { Component, OnDestroy } from '@angular/core'; +import { Component, Input, OnDestroy } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; import { BehaviorSubject, Observable, timer, combineLatest, Subscription } from 'rxjs'; import { debounceTime, switchMap, map, tap, first } from 'rxjs/operators'; -import { ReportService } from '../../api/report.service'; -import { TicketNotification, TicketSummary } from '../../api/support-models'; -import { SupportService } from '../../api/support.service'; -import { ConfigService } from '../../utility/config.service'; -import { NotificationService } from '../../services/notification.service'; -import { UserService as LocalUserService } from '../../utility/user.service'; -import { ToastService } from '../../utility/services/toast.service'; -import { ClipboardService } from '../../utility/services/clipboard.service'; import { fa } from '@/services/font-awesome.service'; -import { ActivatedRoute } from '@angular/router'; +import { ReportService } from '@/api/report.service'; +import { TicketNotification, TicketSummary } from '@/api/support-models'; +import { SupportService } from '@/api/support.service'; +import { ConfigService } from '@/utility/config.service'; +import { NotificationService } from '@/services/notification.service'; +import { UserService as LocalUserService } from '@/utility/user.service'; +import { ToastService } from '@/utility/services/toast.service'; +import { ClipboardService } from '@/utility/services/clipboard.service'; @Component({ selector: 'app-ticket-list', @@ -18,6 +18,9 @@ import { ActivatedRoute } from '@angular/router'; styleUrls: ['./ticket-list.component.scss'] }) export class TicketListComponent implements OnDestroy { + @Input() gameId?: string; + @Input() isReadOnly = false; + subs: Subscription[] = []; refresh$ = new BehaviorSubject(true); ctx$: Observable<{ tickets: TicketSummary[]; nextTicket: TicketSummary[]; canManage: boolean; }>; @@ -28,6 +31,7 @@ export class TicketListComponent implements OnDestroy { assignFilter: string = "Any Assignment"; curOrderItem: string = "created"; + hideTitle = false; isDescending: boolean = true; hoverOrderItem: string = ""; showHoverCaret: boolean = false; @@ -54,6 +58,7 @@ export class TicketListComponent implements OnDestroy { this.statusFilter = config.local.ticketFilter || "Any Status"; this.assignFilter = config.local.ticketType || "Any Assignment"; this.curOrderItem = config.local.ticketOrder || "created"; + this.hideTitle = !!this.gameId; this.isDescending = config.local.ticketOrderDesc || true; const canManage$ = local.user$.pipe( @@ -77,6 +82,7 @@ export class TicketListComponent implements OnDestroy { switchMap(() => api.list({ term: this.searchText, filter: [this.statusFilter.toLowerCase(), this.assignFilter.toLowerCase(), this.selectedLabels], + gameId: this.gameId, withAllLabels: this.selectedLabels.join(","), take: this.take, skip: this.skip, diff --git a/projects/gameboard-ui/src/app/core/core.module.ts b/projects/gameboard-ui/src/app/core/core.module.ts index 2e93510e..540d9019 100644 --- a/projects/gameboard-ui/src/app/core/core.module.ts +++ b/projects/gameboard-ui/src/app/core/core.module.ts @@ -30,6 +30,7 @@ import { markedOptionsFactory } from './config/marked.config'; // internal components/pipes/directives import { AbsoluteValuePipe } from './pipes/absolute-value.pipe'; import { AddDurationPipe } from './pipes/add-duration.pipe'; +import { AgedDatePipe } from './pipes/aged-date.pipe'; import { ApiDatePipe } from './pipes/api-date.pipe'; import { ApiUrlPipe } from './pipes/api-url.pipe'; import { ArrayContainsPipe } from './pipes/array-contains.pipe'; @@ -52,12 +53,14 @@ import { CountdownComponent } from './components/countdown/countdown.component'; import { CountdownPipe } from './pipes/countdown.pipe'; import { CumulativeTimeClockComponent } from './components/cumulative-time-clock/cumulative-time-clock.component'; import { DateToCountdownPipe } from './pipes/date-to-countdown.pipe'; +import { DateTimeIsFuturePipe } from './pipes/datetime-is-future.pipe'; import { DateTimeIsPastPipe } from './pipes/datetime-is-past.pipe'; import { DatetimeToDatePipe } from './pipes/datetime-to-date.pipe'; import { DateToDatetimePipe } from './pipes/date-to-datetime.pipe'; import { DelimitedPipe } from './pipes/delimited.pipe'; import { DoughnutChartComponent } from './components/doughnut-chart/doughnut-chart.component'; import { DropzoneComponent } from './components/dropzone/dropzone.component'; +import { EpochMsToDateTimePipe as EpochMsToDateTimePipe } from './pipes/epoch-ms-to-datetime.pipe'; import { ErrorDivComponent } from './components/error-div/error-div.component'; import { FeedbackFormComponent } from './components/feedback-form/feedback-form.component'; import { FilterPipe } from './pipes/filter.pipe'; @@ -101,12 +104,14 @@ import { SponsorLogoFileNamesToUrisPipe } from './pipes/sponsor-logo-file-names- import { StatusLightComponent } from './components/status-light/status-light.component'; import { SumArrayPipe } from './pipes/sum-array.pipe'; import { TextToColorPipe } from './pipes/text-to-color.pipe'; +import { TicketListComponent } from './components/ticket-list/ticket-list.component'; import { TicketStatusBadgePipe } from './pipes/ticket-status-badge.pipe'; import { ToSupportCodePipe } from './pipes/to-support-code.pipe'; import { ToggleClassPipe } from './pipes/toggle-class.pipe'; import { ToggleSwitchComponent } from './components/toggle-switch/toggle-switch.component'; import { ToTemplateContextPipe } from './pipes/to-template-context.pipe'; import { TrimPipe } from './pipes/trim.pipe'; +import { UntilDateTimePipe } from './pipes/until-datetime.pipe'; import { UrlRewritePipe } from './pipes/url-rewrite.pipe'; import { WhatsThisComponent } from './components/whats-this/whats-this.component'; import { WhitespacePipe } from './pipes/whitespace.pipe'; @@ -114,10 +119,13 @@ import { YamlBlockComponent } from './components/yaml-block/yaml-block.component import { YamlPipe } from './pipes/yaml.pipe'; import { ApiStatusInterceptor } from '@/api-status.interceptor'; import { AuthInterceptor } from '@/utility/auth.interceptor'; +import { ObserverConsoleComponent } from './components/observer-console/observer-console.component'; +import { TicketLabelPickerComponent } from './components/ticket-label-picker/ticket-label-picker.component'; const PUBLIC_DECLARATIONS = [ AbsoluteValuePipe, AddDurationPipe, + AgedDatePipe, ApiDatePipe, ApiUrlPipe, ArrayContainsPipe, @@ -138,11 +146,13 @@ const PUBLIC_DECLARATIONS = [ CumulativeTimeClockComponent, DateToCountdownPipe, DateToDatetimePipe, + DateTimeIsFuturePipe, DateTimeIsPastPipe, DatetimeToDatePipe, DelimitedPipe, DoughnutChartComponent, DropzoneComponent, + EpochMsToDateTimePipe, ErrorDivComponent, FeedbackFormComponent, FriendlyDateAndTimePipe, @@ -162,11 +172,13 @@ const PUBLIC_DECLARATIONS = [ PlayerAvatarListComponent, PlayerStatusComponent, NumbersToPercentage, + ObserverConsoleComponent, QueryParamModelDirective, RefreshIframeOnReconnectDirective, RelativeUrlsPipe, RenderLinksInTextComponent, SpinnerComponent, + TicketListComponent, ToggleSwitchComponent, TrimPipe, UrlRewritePipe, @@ -198,7 +210,10 @@ const PUBLIC_DECLARATIONS = [ ToggleClassPipe, ToSupportCodePipe, ToTemplateContextPipe, + TicketLabelPickerComponent, + TicketListComponent, TicketStatusBadgePipe, + UntilDateTimePipe, UrlRewritePipe, WhatsThisComponent, WhitespacePipe, diff --git a/projects/gameboard-ui/src/app/utility/pipes/aged-date.pipe.ts b/projects/gameboard-ui/src/app/core/pipes/aged-date.pipe.ts similarity index 100% rename from projects/gameboard-ui/src/app/utility/pipes/aged-date.pipe.ts rename to projects/gameboard-ui/src/app/core/pipes/aged-date.pipe.ts diff --git a/projects/gameboard-ui/src/app/core/pipes/date-to-datetime.pipe.ts b/projects/gameboard-ui/src/app/core/pipes/date-to-datetime.pipe.ts index d2e794e5..ee127fc2 100644 --- a/projects/gameboard-ui/src/app/core/pipes/date-to-datetime.pipe.ts +++ b/projects/gameboard-ui/src/app/core/pipes/date-to-datetime.pipe.ts @@ -3,11 +3,13 @@ import { DateTime } from 'luxon'; @Pipe({ name: 'dateToDateTime' }) export class DateToDatetimePipe implements PipeTransform { - transform(value: Date): DateTime { + transform(value: Date): DateTime | null { if (!value) return value; + const asDate = new Date(value); + // using the date constructor to guard against stringified dates coming in - return DateTime.fromJSDate(new Date(value)); + return asDate.getFullYear() <= 1 ? null : DateTime.fromJSDate(asDate); } } diff --git a/projects/gameboard-ui/src/app/core/pipes/datetime-is-future.pipe.ts b/projects/gameboard-ui/src/app/core/pipes/datetime-is-future.pipe.ts new file mode 100644 index 00000000..a85ed09f --- /dev/null +++ b/projects/gameboard-ui/src/app/core/pipes/datetime-is-future.pipe.ts @@ -0,0 +1,12 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { DateTime } from 'luxon'; + +@Pipe({ name: 'datetimeIsFuture' }) +export class DateTimeIsFuturePipe implements PipeTransform { + transform(value: DateTime | null): boolean { + if (!value) + return false; + + return value.diffNow().toMillis() > 0; + } +} diff --git a/projects/gameboard-ui/src/app/core/pipes/datetime-is-past.pipe.ts b/projects/gameboard-ui/src/app/core/pipes/datetime-is-past.pipe.ts index 1201fbbe..ac174bdf 100644 --- a/projects/gameboard-ui/src/app/core/pipes/datetime-is-past.pipe.ts +++ b/projects/gameboard-ui/src/app/core/pipes/datetime-is-past.pipe.ts @@ -3,7 +3,10 @@ import { DateTime } from 'luxon'; @Pipe({ name: 'dateTimeIsPast' }) export class DateTimeIsPastPipe implements PipeTransform { - transform(value: DateTime): boolean { + transform(value: DateTime | null): boolean { + if (!value) + return false; + return value.diffNow().toMillis() < 0; } } diff --git a/projects/gameboard-ui/src/app/core/pipes/epoch-ms-to-datetime.pipe.ts b/projects/gameboard-ui/src/app/core/pipes/epoch-ms-to-datetime.pipe.ts new file mode 100644 index 00000000..41b38ef3 --- /dev/null +++ b/projects/gameboard-ui/src/app/core/pipes/epoch-ms-to-datetime.pipe.ts @@ -0,0 +1,12 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { DateTime } from 'luxon'; + +@Pipe({ name: 'epochMsToDateTime' }) +export class EpochMsToDateTimePipe implements PipeTransform { + transform(value: number | null | undefined): DateTime | null { + if (value === null || value === undefined) + return null; + + return DateTime.fromJSDate(new Date(value)); + } +} diff --git a/projects/gameboard-ui/src/app/core/pipes/friendly-date-and-time.pipe.ts b/projects/gameboard-ui/src/app/core/pipes/friendly-date-and-time.pipe.ts index 79a01422..e8400e25 100644 --- a/projects/gameboard-ui/src/app/core/pipes/friendly-date-and-time.pipe.ts +++ b/projects/gameboard-ui/src/app/core/pipes/friendly-date-and-time.pipe.ts @@ -6,7 +6,7 @@ import { DateTime } from 'luxon'; export class FriendlyDateAndTimePipe implements PipeTransform { constructor(private friendlyDates: FriendlyDatesService) { } - transform(value?: Date | DateTime): string | null { + transform(value?: Date | DateTime | null): string | null { if (!value) return null; diff --git a/projects/gameboard-ui/src/app/core/pipes/friendly-time.pipe.ts b/projects/gameboard-ui/src/app/core/pipes/friendly-time.pipe.ts index 75621ec5..ff08bb56 100644 --- a/projects/gameboard-ui/src/app/core/pipes/friendly-time.pipe.ts +++ b/projects/gameboard-ui/src/app/core/pipes/friendly-time.pipe.ts @@ -1,4 +1,5 @@ import { Pipe, PipeTransform } from '@angular/core'; +import { DateTime } from 'luxon'; import { FriendlyDatesService } from '@/services/friendly-dates.service'; @Pipe({ name: 'friendlyTime' }) @@ -6,7 +7,7 @@ export class FriendlyTimePipe implements PipeTransform { constructor(private friendlyDates: FriendlyDatesService) { } - transform(value?: Date): string { + transform(value: DateTime | Date | null | undefined): string { return this.friendlyDates.toFriendlyTime(value); } } diff --git a/projects/gameboard-ui/src/app/core/pipes/until-datetime.pipe.ts b/projects/gameboard-ui/src/app/core/pipes/until-datetime.pipe.ts new file mode 100644 index 00000000..56fb696b --- /dev/null +++ b/projects/gameboard-ui/src/app/core/pipes/until-datetime.pipe.ts @@ -0,0 +1,18 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { DateTime } from 'luxon'; + +@Pipe({ + name: 'untilDateTime' +}) +export class UntilDateTimePipe implements PipeTransform { + + transform(value: DateTime | null): string | null { + if (!value) + return null; + + return value + .diffNow() + .shiftTo("hours", "minutes") + .toHuman({ listStyle: "narrow", maximumFractionDigits: 0 }); + } +} diff --git a/projects/gameboard-ui/src/app/game/player-session/player-session.component.ts b/projects/gameboard-ui/src/app/game/player-session/player-session.component.ts index 6798c5eb..140aa31f 100644 --- a/projects/gameboard-ui/src/app/game/player-session/player-session.component.ts +++ b/projects/gameboard-ui/src/app/game/player-session/player-session.component.ts @@ -148,7 +148,7 @@ export class PlayerSessionComponent implements OnDestroy { private async doReset(p: Player) { try { - const team = await firstValueFrom(this.teamService.get(p.teamId)); + await firstValueFrom(this.teamService.get(p.teamId)); } catch (err: any) { if (err.status == 400) { diff --git a/projects/gameboard-ui/src/app/home/landing/landing.component.ts b/projects/gameboard-ui/src/app/home/landing/landing.component.ts index 1c288f5e..000deb0d 100644 --- a/projects/gameboard-ui/src/app/home/landing/landing.component.ts +++ b/projects/gameboard-ui/src/app/home/landing/landing.component.ts @@ -1,7 +1,7 @@ // Copyright 2021 Carnegie Mellon University. All Rights Reserved. // Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. -import { Component, OnInit } from '@angular/core'; +import { Component } from '@angular/core'; import { Router } from '@angular/router'; import { faGamepad, faPlusSquare, faSearch, faUserPlus } from '@fortawesome/free-solid-svg-icons'; import { BehaviorSubject, Observable } from 'rxjs'; @@ -15,7 +15,7 @@ import { GameService } from '../../api/game.service'; templateUrl: './landing.component.html', styleUrls: ['./landing.component.scss'] }) -export class LandingComponent implements OnInit { +export class LandingComponent { refresh$ = new BehaviorSubject(true); featured$: Observable; ongoing$: Observable; @@ -63,9 +63,6 @@ export class LandingComponent implements OnInit { ); } - ngOnInit(): void { - } - selected(game: Game | BoardGame): void { this.router.navigate(['/game', game.id]); } diff --git a/projects/gameboard-ui/src/app/reports/components/reports/challenges-report/challenges-report.component.ts b/projects/gameboard-ui/src/app/reports/components/reports/challenges-report/challenges-report.component.ts index 0376a27a..77f9eda1 100644 --- a/projects/gameboard-ui/src/app/reports/components/reports/challenges-report/challenges-report.component.ts +++ b/projects/gameboard-ui/src/app/reports/components/reports/challenges-report/challenges-report.component.ts @@ -68,7 +68,7 @@ export class ChallengesReportComponent extends ReportComponentBase({ content: SpecQuestionPerformanceModalComponent, context: { specId: spec.id }, - modalClasses: ["modal-lg", "modal-dialog-centered"] + modalClasses: ["modal-lg"] }); } diff --git a/projects/gameboard-ui/src/app/reports/components/reports/enrollment-report/enrollment-report-trend/enrollment-report-trend.component.ts b/projects/gameboard-ui/src/app/reports/components/reports/enrollment-report/enrollment-report-trend/enrollment-report-trend.component.ts index 1b547e66..c3d61e76 100644 --- a/projects/gameboard-ui/src/app/reports/components/reports/enrollment-report/enrollment-report-trend/enrollment-report-trend.component.ts +++ b/projects/gameboard-ui/src/app/reports/components/reports/enrollment-report/enrollment-report-trend/enrollment-report-trend.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input, OnInit, SimpleChanges } from '@angular/core'; import { EnrollmentReportFlatParameters } from '../enrollment-report.models'; import { EnrollmentReportService } from '../enrollment-report.service'; import { LineChartConfig } from '@/core/components/line-chart/line-chart.component'; @@ -15,7 +15,10 @@ export class EnrollmentReportTrendComponent implements OnInit { constructor(private enrollmentReportService: EnrollmentReportService) { } - async ngOnInit(): Promise { + async ngOnChanges(changes: SimpleChanges) { + if (!changes.parameters) + return; + const lineChartResults = await this.enrollmentReportService.getTrendData(this.parameters); this.chartConfig = { type: 'line', @@ -55,4 +58,8 @@ export class EnrollmentReportTrendComponent implements OnInit { } }; } + + async ngOnInit(): Promise { + + } } diff --git a/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/challenge-detail-modal/challenge-detail-modal.component.html b/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/challenge-detail-modal/challenge-detail-modal.component.html new file mode 100644 index 00000000..7c315c06 --- /dev/null +++ b/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/challenge-detail-modal/challenge-detail-modal.component.html @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Best Attempt
    PlayerAttemptsDateTimeScoreResult
    + +
    +
    {{ player.user.name }}
    +
    {{ player.user.sponsor.name }}
    +
    +
    {{ player.attemptCount }}{{ player.bestAttemptDate | friendlyDateAndTime }}{{ player.bestAttemptDurationMs | msToDuration }}{{ player.bestAttemptScore | number:"1.0-2" }} + {{ player.bestAttemptResult | challengeResultPretty }} +
    + +
    +
    + +
    +
    + + + Loading challenge data... + + + +

    + There aren't any users with matching data for this practice challenge. +

    +
    diff --git a/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/challenge-detail-modal/challenge-detail-modal.component.scss b/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/challenge-detail-modal/challenge-detail-modal.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/challenge-detail-modal/challenge-detail-modal.component.ts b/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/challenge-detail-modal/challenge-detail-modal.component.ts new file mode 100644 index 00000000..7601cd1a --- /dev/null +++ b/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/challenge-detail-modal/challenge-detail-modal.component.ts @@ -0,0 +1,51 @@ +import { Component, OnInit } from '@angular/core'; +import { PracticeModeReportChallengeDetail, PracticeModeReportChallengeDetailParameters, PracticeModeReportFlatParameters } from '../practice-mode-report.models'; +import { PracticeModeReportService } from '../practice-mode-report.service'; +import { PagingArgs } from '@/api/models'; +import { ChallengeResult } from '@/api/board-models'; + +@Component({ + selector: 'app-challenge-detail-modal', + templateUrl: './challenge-detail-modal.component.html', + styleUrls: ['./challenge-detail-modal.component.scss'] +}) +export class ChallengeDetailModalComponent implements OnInit { + challengeSpecId?: string; + challengeDetailParameters?: PracticeModeReportChallengeDetailParameters; + parameters?: PracticeModeReportFlatParameters; + + protected errors: string[] = []; + protected isLoading = false; + protected pagingArgs?: PagingArgs; + protected results?: PracticeModeReportChallengeDetail; + protected subtitleComputed = "Practice Mode Performance"; + + constructor(private reportService: PracticeModeReportService) { } + + async ngOnInit() { + await this.load(this.challengeSpecId, this.parameters, this.challengeDetailParameters); + } + + protected async handlePaging(pagingArgs: PagingArgs) { + await this.load(this.challengeSpecId, this.parameters, this.challengeDetailParameters, pagingArgs); + } + + private async load(specId?: string, parameters?: PracticeModeReportFlatParameters, challengeDetailParameters?: PracticeModeReportChallengeDetailParameters, pagingArgs?: PagingArgs) { + this.errors = []; + + if (!specId) { + this.errors.push("Whoops. Something went wrong. Please contact your administrator."); + throw new Error("ChallengeSpecId is required."); + } + + this.isLoading = true; + this.results = await this.reportService.getChallengeDetail(specId, parameters, challengeDetailParameters, pagingArgs); + this.isLoading = false; + + this.pagingArgs = this.results.paging; + + if (this.challengeDetailParameters?.playersWithSolveType) { + this.subtitleComputed = `Practice Mode Performance (best attempt: ${this.challengeDetailParameters.playersWithSolveType})`; + } + } +} diff --git a/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/practice-mode-report-by-challenge/practice-mode-report-by-challenge.component.html b/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/practice-mode-report-by-challenge/practice-mode-report-by-challenge.component.html index 604061ee..04e42d36 100644 --- a/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/practice-mode-report-by-challenge/practice-mode-report-by-challenge.component.html +++ b/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/practice-mode-report-by-challenge/practice-mode-report-by-challenge.component.html @@ -54,7 +54,8 @@ -

    {{ record.overallPerformance.players.length }}

    +

    {{ + record.overallPerformance.players.length }}

    {{ record.overallPerformance.totalAttempts }}

    @@ -80,13 +81,28 @@ -

    {{ record.overallPerformance.percentageCompleteSolved | percent }}

    +
    + {{ record.overallPerformance.completeSolves }} + + ({{ record.overallPerformance.percentageCompleteSolved | percent }}) + +
    -

    {{ record.overallPerformance.percentagePartiallySolved | percent }}

    +
    + {{ record.overallPerformance.partialSolves }} + + ({{ record.overallPerformance.percentagePartiallySolved | percent }}) + +
    -

    {{ record.overallPerformance.percentageZeroScoreSolved | percent }}

    +
    + {{ record.overallPerformance.zeroScoreSolves }} + + ({{ record.overallPerformance.percentageZeroScoreSolved | percent }}) + +
    diff --git a/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/practice-mode-report-by-challenge/practice-mode-report-by-challenge.component.ts b/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/practice-mode-report-by-challenge/practice-mode-report-by-challenge.component.ts index 642fd26f..b861e4d1 100644 --- a/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/practice-mode-report-by-challenge/practice-mode-report-by-challenge.component.ts +++ b/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/practice-mode-report-by-challenge/practice-mode-report-by-challenge.component.ts @@ -8,6 +8,8 @@ import { ModalConfirmService } from '@/services/modal-confirm.service'; import { SponsorChallengePerformanceComponent, SponsorChallengePerformanceModalContext } from '../sponsor-challenge-performance/sponsor-challenge-performance.component'; import { PagingArgs, SimpleEntity } from '@/api/models'; import { LogService } from '@/services/log.service'; +import { ChallengeDetailModalComponent } from '../challenge-detail-modal/challenge-detail-modal.component'; +import { ChallengeResult } from '@/api/board-models'; @Component({ selector: 'app-practice-mode-report-by-challenge', @@ -35,6 +37,31 @@ export class PracticeModeReportByChallengeComponent implements OnChanges { this.overallStatsUpdate.emit(this.results.overallStats); } + handlePlayersClicked(specId: string) { + this.modalService.openComponent({ + content: ChallengeDetailModalComponent, + context: { + challengeSpecId: specId, + parameters: this.parameters || undefined + }, + modalClasses: ["modal-xl"] + }); + } + + handleSolveTypeClicked(specId: string, type: ChallengeResult) { + this.modalService.openComponent({ + content: ChallengeDetailModalComponent, + context: { + challengeSpecId: specId, + challengeDetailParameters: { + playersWithSolveType: type + }, + parameters: this.parameters || undefined, + }, + modalClasses: ["modal-xl"] + }); + } + handleSponsorsClicked(challenge: SimpleEntity, sponsorPerformance: PracticeModeReportSponsorPerformance[]) { if (!this.sponsorPerformanceTemplate) { throw new Error("Couldn't resolve the sponsor performance template."); @@ -48,7 +75,7 @@ export class PracticeModeReportByChallengeComponent implements OnChanges { sponsorPerformance } }, - modalClasses: ["modal-dialog-centered", "modal-xl"] + modalClasses: ["modal-xl"] }); } diff --git a/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/practice-mode-report-by-player-mode-performance/practice-mode-report-by-player-mode-performance.component.ts b/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/practice-mode-report-by-player-mode-performance/practice-mode-report-by-player-mode-performance.component.ts index b373d239..12d59d22 100644 --- a/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/practice-mode-report-by-player-mode-performance/practice-mode-report-by-player-mode-performance.component.ts +++ b/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/practice-mode-report-by-player-mode-performance/practice-mode-report-by-player-mode-performance.component.ts @@ -39,7 +39,7 @@ export class PracticeModeReportByPlayerModePerformanceComponent implements OnCha this.modalService.openComponent({ content: PlayerModePerformanceSummaryComponent, context: { context: { ...event } }, - modalClasses: ["modal-dialog-centered", "modal-lg"] + modalClasses: ["modal-lg"] }); } diff --git a/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/practice-mode-report-by-user/practice-mode-report-by-user.component.ts b/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/practice-mode-report-by-user/practice-mode-report-by-user.component.ts index 13f1fc35..898215f0 100644 --- a/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/practice-mode-report-by-user/practice-mode-report-by-user.component.ts +++ b/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/practice-mode-report-by-user/practice-mode-report-by-user.component.ts @@ -64,7 +64,7 @@ export class PracticeModeReportByUserComponent implements OnChanges { })) } }, - modalClasses: ["modal-dialog-centered", "modal-md"] + modalClasses: ["modal-md"] }); } } diff --git a/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/practice-mode-report.models.ts b/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/practice-mode-report.models.ts index 36a451c3..ef3d4692 100644 --- a/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/practice-mode-report.models.ts +++ b/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/practice-mode-report.models.ts @@ -1,6 +1,7 @@ import { ChallengeResult } from "@/api/board-models"; -import { PagingArgs, SimpleEntity } from "@/api/models"; -import { ReportDateRange, ReportGame, ReportSponsor, ReportTeam } from "@/reports/reports-models"; +import { PagingResults, PlayerWithSponsor, SimpleEntity, SimpleSponsor } from "@/api/models"; +import { ReportGame, ReportSponsor, ReportTeam } from "@/reports/reports-models"; +import { DateTime } from "luxon"; export enum PracticeModeReportGrouping { challenge = "challenge", @@ -29,6 +30,10 @@ export interface PracticeModeReportFlatParameters { grouping: PracticeModeReportGrouping; } +export interface PracticeModeReportChallengeDetailParameters { + playersWithSolveType?: ChallengeResult; +} + export interface PracticeModeReportRecord { } export interface PracticeModeReportByUserRecord extends PracticeModeReportRecord { @@ -124,3 +129,21 @@ export interface PracticeModeReportPlayerModeSummaryChallenge { pctAvailablePointsEarned: number; scorePercentile: number; } + +export interface PracticeModeReportChallengeDetail { + game: SimpleEntity; + spec: SimpleEntity; + paging: PagingResults; + users: PracticeModeReportChallengeDetailUser[]; +} + +export interface PracticeModeReportChallengeDetailUser { + user: PlayerWithSponsor; + sponsor: SimpleSponsor; + attemptCount: number; + bestAttemptDate: DateTime; + bestAttemptDurationMs: number; + bestAttemptResult: ChallengeResult; + bestAttemptScore: number; + lastAttemptDate: DateTime; +} diff --git a/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/practice-mode-report.service.ts b/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/practice-mode-report.service.ts index 36748344..64ae79cc 100644 --- a/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/practice-mode-report.service.ts +++ b/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/practice-mode-report.service.ts @@ -1,10 +1,12 @@ import { ApiUrlService } from '@/services/api-url.service'; import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { PracticeModeReportFlatParameters, PracticeModeReportByUserRecord, PracticeModeReportByChallengeRecord, PracticeModeReportGrouping, PracticeModeReportByPlayerModePerformanceRecord, PracticeModeReportPlayerModeSummary, PracticeModeReportOverallStats } from './practice-mode-report.models'; -import { Observable, map } from 'rxjs'; +import { PracticeModeReportFlatParameters, PracticeModeReportByUserRecord, PracticeModeReportByChallengeRecord, PracticeModeReportGrouping, PracticeModeReportByPlayerModePerformanceRecord, PracticeModeReportPlayerModeSummary, PracticeModeReportOverallStats, PracticeModeReportChallengeDetail, PracticeModeReportChallengeDetailParameters } from './practice-mode-report.models'; +import { Observable, firstValueFrom, map } from 'rxjs'; import { ReportResultsWithOverallStats } from '../../../reports-models'; import { ReportsService } from '../../../reports.service'; +import { DateTime } from 'luxon'; +import { PagingArgs } from '@/api/models'; @Injectable({ providedIn: 'root' }) export class PracticeModeReportService { @@ -19,6 +21,22 @@ export class PracticeModeReportService { return this.http.get>(this.apiUrl.build("reports/practice-area", finalParams)); } + getChallengeDetail(challengeSpecId: string, parameters: PracticeModeReportFlatParameters | null | undefined, challengeDetailParameters: PracticeModeReportChallengeDetailParameters | null | undefined, paging: PagingArgs | null | undefined): Promise { + return firstValueFrom(this.http.get(this.apiUrl.build(`reports/practice-area/challenge-spec/${challengeSpecId}`, { ...parameters, ...challengeDetailParameters, ...paging })).pipe( + map(result => { + return { + ...result, + users: result.users.map(u => { + u.lastAttemptDate = DateTime.fromJSDate(new Date(u.lastAttemptDate.toString())); + u.bestAttemptDate = DateTime.fromJSDate(new Date(u.bestAttemptDate.toString())); + + return u; + }) + }; + }) + )); + } + getByPlayerModePerformance(parameters: PracticeModeReportFlatParameters | null): Observable> { const finalParams = { ... (parameters || {}), ...{ grouping: PracticeModeReportGrouping.playerModePerformance } }; diff --git a/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/sponsor-challenge-performance/sponsor-challenge-performance.component.html b/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/sponsor-challenge-performance/sponsor-challenge-performance.component.html index e8edf78d..76b47cf6 100644 --- a/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/sponsor-challenge-performance/sponsor-challenge-performance.component.html +++ b/projects/gameboard-ui/src/app/reports/components/reports/practice-mode-report/sponsor-challenge-performance/sponsor-challenge-performance.component.html @@ -25,13 +25,13 @@

    Sponsor Performance

    Sponsor - Player Count - Total Attempts - High - Avg - Complete - Partial - Zero + Player Count + Total Attempts + High + Avg + Complete + Partial + Zero @@ -63,13 +63,28 @@

    Sponsor Performance

    -

    {{ sponsorPerformance.performance.percentageCompleteSolved | percent }}

    +
    + {{ sponsorPerformance.performance.completeSolves }} + + ({{ sponsorPerformance.performance.percentageCompleteSolved | percent }}) + +
    -

    {{ sponsorPerformance.performance.percentagePartiallySolved | percent }}

    +
    + {{ sponsorPerformance.performance.partialSolves }} + + ({{ sponsorPerformance.performance.percentagePartiallySolved | percent }}) + +
    -

    {{ sponsorPerformance.performance.percentageZeroScoreSolved | percent }}

    +
    + {{ sponsorPerformance.performance.zeroScoreSolves }} + + ({{ sponsorPerformance.performance.percentageZeroScoreSolved | percent }}) + +
    diff --git a/projects/gameboard-ui/src/app/reports/components/reports/site-usage-report/site-usage-report.component.html b/projects/gameboard-ui/src/app/reports/components/reports/site-usage-report/site-usage-report.component.html index 15fa682c..41c76113 100644 --- a/projects/gameboard-ui/src/app/reports/components/reports/site-usage-report/site-usage-report.component.html +++ b/projects/gameboard-ui/src/app/reports/components/reports/site-usage-report/site-usage-report.component.html @@ -18,16 +18,18 @@

    Players & Sponsors

    label="Practice-Only Players">
    -

    Challenges

    +

    Challenges & Engagement

    + -
    diff --git a/projects/gameboard-ui/src/app/reports/components/reports/site-usage-report/site-usage-report.models.ts b/projects/gameboard-ui/src/app/reports/components/reports/site-usage-report/site-usage-report.models.ts index b625581b..1105232a 100644 --- a/projects/gameboard-ui/src/app/reports/components/reports/site-usage-report/site-usage-report.models.ts +++ b/projects/gameboard-ui/src/app/reports/components/reports/site-usage-report/site-usage-report.models.ts @@ -10,6 +10,8 @@ export interface SiteUsageReport { deployedChallengesCompetitiveCount: number; deployedChallengesPracticeCount: number; deployedChallengesSpecCount: number; + competitivePlayDurationHours: number; + practicePlayDurationHours: number; practiceUsersWithNoCompetitiveCount: number; sponsorCount: number; userCount: number; diff --git a/projects/gameboard-ui/src/app/reports/reports.module.ts b/projects/gameboard-ui/src/app/reports/reports.module.ts index beab52dc..4fc61b8d 100644 --- a/projects/gameboard-ui/src/app/reports/reports.module.ts +++ b/projects/gameboard-ui/src/app/reports/reports.module.ts @@ -4,6 +4,7 @@ import { RouterModule } from '@angular/router'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { CoreModule } from '../core/core.module'; import { ChallengeAttemptSummaryComponent } from './components/challenge-attempt-summary/challenge-attempt-summary.component'; +import { ChallengeDetailModalComponent } from './components/reports/practice-mode-report/challenge-detail-modal/challenge-detail-modal.component'; import { ChallengeOrGameFieldComponent } from './components/challenge-or-game-field/challenge-or-game-field.component'; import { ChallengesReportComponent } from './components/reports/challenges-report/challenges-report.component'; import { ChallengesReportSummaryToStatsPipe } from './components/reports/challenges-report/challenges-report-summary-to-stats.pipe'; @@ -56,6 +57,7 @@ import { SpecQuestionPerformanceModalComponent } from './components/spec-questio ArrayFieldToClassPipe, CountToTooltipClassPipe, ChallengeAttemptSummaryComponent, + ChallengeDetailModalComponent, ChallengesReportComponent, ChallengesReportSummaryToStatsPipe, EnrollmentReportComponent, @@ -98,7 +100,7 @@ import { SpecQuestionPerformanceModalComponent } from './components/spec-questio SiteUsageReportSponsorsModalComponent, SiteUsageReportChallengesListComponent, SortHeaderComponent, - SpecQuestionPerformanceModalComponent + SpecQuestionPerformanceModalComponent, ], imports: [ CommonModule, diff --git a/projects/gameboard-ui/src/app/scoreboard/components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component.ts b/projects/gameboard-ui/src/app/scoreboard/components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component.ts index 3dccb6e7..cf054559 100644 --- a/projects/gameboard-ui/src/app/scoreboard/components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component.ts +++ b/projects/gameboard-ui/src/app/scoreboard/components/scoreboard-team-detail-modal/scoreboard-team-detail-modal.component.ts @@ -52,19 +52,23 @@ export class ScoreboardTeamDetailModalComponent implements OnInit { // the scoring endpoint currently doesn't provide the total of challenge manual bonuses // vs team manual bonuses if (this.hasManualTeamBonuses) { - this.challengeManualBonusTotal = this - .context - .score - .challenges - .map(c => c.score.manualBonusScore) - .reduce((accumulator, nextValue) => accumulator + nextValue); + this.challengeManualBonusTotal = 0; + + if (this.context.score.challenges?.length) { + this.challengeManualBonusTotal = this + .context + .score + .challenges + .map(c => c.score.manualBonusScore) + .reduce((accumulator, nextValue) => (accumulator || 0) + nextValue); + } this.teamManualBonusTotal = this .context .score .manualBonuses .map(b => b.pointValue) - .reduce((accumulator, nextValue) => accumulator + nextValue); + .reduce((accumulator, nextValue) => (accumulator || 0) + nextValue); } } diff --git a/projects/gameboard-ui/src/app/scoreboard/components/scoreboard/scoreboard.component.html b/projects/gameboard-ui/src/app/scoreboard/components/scoreboard/scoreboard.component.html index 29e29243..7c636c7d 100644 --- a/projects/gameboard-ui/src/app/scoreboard/components/scoreboard/scoreboard.component.html +++ b/projects/gameboard-ui/src/app/scoreboard/components/scoreboard/scoreboard.component.html @@ -1,5 +1,5 @@ - + This game is still being played. Check back after the round ends ({{ scoreboardData.game.isLiveUntil?.toJSDate() | friendlyDateAndTime }}) to see who came out on top and view complete score breakdowns for diff --git a/projects/gameboard-ui/src/app/scoreboard/components/scoreboard/scoreboard.component.ts b/projects/gameboard-ui/src/app/scoreboard/components/scoreboard/scoreboard.component.ts index c9010580..ea16f283 100644 --- a/projects/gameboard-ui/src/app/scoreboard/components/scoreboard/scoreboard.component.ts +++ b/projects/gameboard-ui/src/app/scoreboard/components/scoreboard/scoreboard.component.ts @@ -12,6 +12,7 @@ import { ScoreboardTeamDetailModalComponent } from '../scoreboard-team-detail-mo }) export class ScoreboardComponent implements OnInit, OnDestroy { @Input() gameId?: string; + @Input() suppressLiveGameBanner = false; protected canViewAllScores = false; protected cumulativeTimeTooltip = "Cumulative Time is only used for tiebreaking purposes. When a challenge is started, a timer tracks how long it takes to solve that challenge. The sum time of all successfully solved challenges is the value in this column."; @@ -48,8 +49,7 @@ export class ScoreboardComponent implements OnInit, OnDestroy { content: ScoreboardTeamDetailModalComponent, context: { teamId: teamData.score.teamId }, modalClasses: [ - teamData.players.length > 1 ? "modal-xl" : "modal-lg", - "modal-dialog-centered" + teamData.players.length > 1 ? "modal-xl" : "modal-lg" ] }); } diff --git a/projects/gameboard-ui/src/app/scoreboard/pipes/score-to-tooltip.pipe.ts b/projects/gameboard-ui/src/app/scoreboard/pipes/score-to-tooltip.pipe.ts index 33258423..7a33401f 100644 --- a/projects/gameboard-ui/src/app/scoreboard/pipes/score-to-tooltip.pipe.ts +++ b/projects/gameboard-ui/src/app/scoreboard/pipes/score-to-tooltip.pipe.ts @@ -1,22 +1,47 @@ -import { DenormalizedTeamScore } from '@/services/scoring/scoring.models'; +import { DenormalizedTeamScore, Score } from '@/services/scoring/scoring.models'; import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ name: 'scoreToTooltip' }) export class ScoreToTooltipPipe implements PipeTransform { - transform(value: DenormalizedTeamScore): string { - if (!value || value.scoreOverall === 0 || value.scoreOverall === value.scoreChallenge) + transform(value: DenormalizedTeamScore | Score): string { + if (!value) + return ""; + + if ("scoreOverall" in value) { + const denormalizedScore = value as DenormalizedTeamScore; + return this.buildTooltip({ + totalScore: denormalizedScore.scoreOverall, + completionScore: denormalizedScore.scoreChallenge, + autoBonusScore: denormalizedScore.scoreAutoBonus, + manualBonusScore: denormalizedScore.scoreManualBonus, + advancedScore: denormalizedScore.scoreAdvanced, + }); + } + + const score = value as Score; + return this.buildTooltip({ + totalScore: score.totalScore, + completionScore: score.completionScore, + autoBonusScore: score.bonusScore, + manualBonusScore: score.manualBonusScore, + advancedScore: score.advancedScore + }); + } + + private buildTooltip(score: { totalScore: number, completionScore: number, autoBonusScore: number, manualBonusScore: number, advancedScore?: number }) { + if (!score.totalScore || score.totalScore === 0 || score.totalScore == score.completionScore) return ""; let previousGameClause = ""; - if (value.scoreAdvanced || 0) { - previousGameClause = ` + ${value.scoreAdvanced || 0} previous`; + if (score.advancedScore || 0) { + previousGameClause = ` + ${score.advancedScore || 0} previous`; } let bonusClause = ""; - const bonusPoints = (value.scoreAutoBonus || 0) || (value.scoreManualBonus || 0); + const bonusPoints = (score.autoBonusScore || 0) || (score.manualBonusScore || 0); if (bonusPoints) bonusClause = ` + ${bonusPoints} bonus `; - return `${value.scoreChallenge}${previousGameClause}${bonusClause} (click for details)`; + return `${score.completionScore}${previousGameClause}${bonusClause} (click for details)`; } } diff --git a/projects/gameboard-ui/src/app/scoreboard/scoreboard.module.ts b/projects/gameboard-ui/src/app/scoreboard/scoreboard.module.ts index 4807e92d..db16e49f 100644 --- a/projects/gameboard-ui/src/app/scoreboard/scoreboard.module.ts +++ b/projects/gameboard-ui/src/app/scoreboard/scoreboard.module.ts @@ -9,14 +9,14 @@ import { ScoreToTooltipPipe } from './pipes/score-to-tooltip.pipe'; const PUBLIC_DECLARATIONS = [ ScoreboardComponent, - ScoreboardTeamDetailModalComponent + ScoreboardTeamDetailModalComponent, + ScoreToTooltipPipe ]; @NgModule({ declarations: [ ...PUBLIC_DECLARATIONS, - ChallengeBonusesToTooltip, - ScoreToTooltipPipe + ChallengeBonusesToTooltip ], imports: [ CommonModule, diff --git a/projects/gameboard-ui/src/app/services/font-awesome.service.ts b/projects/gameboard-ui/src/app/services/font-awesome.service.ts index 207f2d17..27d1adfc 100644 --- a/projects/gameboard-ui/src/app/services/font-awesome.service.ts +++ b/projects/gameboard-ui/src/app/services/font-awesome.service.ts @@ -21,9 +21,11 @@ import { faClock, faCloudUploadAlt, faComments, + faComputer, faCopy, faEdit, faEllipsisVertical, + faEnvelope, faEraser, faExclamationCircle, faExclamationTriangle, @@ -90,9 +92,11 @@ export const fa = { clock: faClock, cloudUploadAlt: faCloudUploadAlt, comments: faComments, + computer: faComputer, copy: faCopy, edit: faEdit, ellipsisVertical: faEllipsisVertical, + envelope: faEnvelope, eraser: faEraser, exclamationCircle: faExclamationCircle, exclamationTriangle: faExclamationTriangle, diff --git a/projects/gameboard-ui/src/app/services/friendly-dates.service.ts b/projects/gameboard-ui/src/app/services/friendly-dates.service.ts index bda31a71..b81baf7c 100644 --- a/projects/gameboard-ui/src/app/services/friendly-dates.service.ts +++ b/projects/gameboard-ui/src/app/services/friendly-dates.service.ts @@ -19,7 +19,7 @@ export class FriendlyDatesService { }); } - toFriendlyTime(date?: string | Date | DateTime) { + toFriendlyTime(date?: null | string | Date | DateTime) { const transformed = this.transformDateInput(date); if (!transformed) return ""; @@ -34,7 +34,7 @@ export class FriendlyDatesService { return `${this.toFriendlyDate(date)} @ ${this.toFriendlyTime(date)}`; } - private transformDateInput(input?: string | Date | DateTime): Date | null { + private transformDateInput(input?: null | string | Date | DateTime): Date | null { if (!input) return null; diff --git a/projects/gameboard-ui/src/app/services/game-session.service.ts b/projects/gameboard-ui/src/app/services/game-session.service.ts index 934597ab..b98b8874 100644 --- a/projects/gameboard-ui/src/app/services/game-session.service.ts +++ b/projects/gameboard-ui/src/app/services/game-session.service.ts @@ -48,6 +48,10 @@ export class GameSessionService { return session ? session.isBefore : true; } + public canUnenrollSessionWithDateTIme(session: { start: DateTime | null, end: DateTime | null }) { + return session.start === null || this.nowService.nowToDateTime() < session.start; + } + public getCumulativeTime(session: TimeWindow) { const now = this.nowService.now(); diff --git a/projects/gameboard-ui/src/app/services/local-storage.service.ts b/projects/gameboard-ui/src/app/services/local-storage.service.ts index 247f768d..da046b04 100644 --- a/projects/gameboard-ui/src/app/services/local-storage.service.ts +++ b/projects/gameboard-ui/src/app/services/local-storage.service.ts @@ -3,6 +3,7 @@ import { Injectable } from '@angular/core'; export enum StorageKey { ExternalGameOidc = "oidcLink", ExternalGameUrl = "gameServerUrl", + GameCenterTeamsFilterSettings = "gameCenterTeamsFilterSettings", Gameboard = "gameboard", } @@ -11,15 +12,15 @@ export class LocalStorageService { readonly Client: Storage = window.localStorage; private _client = this.Client; - - add(key: StorageKey, value: string, throwIfExists = false): void { + add(key: StorageKey, value: T, throwIfExists = false): void { const finalKey = this.prependKey(key); if (throwIfExists && this._client.getItem(finalKey) !== null) { throw new Error(`Storage key ${finalKey} already exists in local storage.`); } - this._client.setItem(finalKey, value); + const isObjectValue = typeof value == "object"; + this._client.setItem(finalKey, isObjectValue ? JSON.stringify(value) : value.toString()); } /** @@ -54,6 +55,16 @@ export class LocalStorageService { return value; } + getAs(key: StorageKey, defaultValue: T): T { + const value = this._client.getItem(this.prependKey(key)); + + if (value === null) { + return defaultValue; + } + + return JSON.parse(value) as T; + } + getArbitrary = (key: string, throwIfNotExists = false) => this.get(key as StorageKey, throwIfNotExists); has(key: string): boolean { diff --git a/projects/gameboard-ui/src/app/services/modal-confirm.service.ts b/projects/gameboard-ui/src/app/services/modal-confirm.service.ts index 94f6fac8..352cf806 100644 --- a/projects/gameboard-ui/src/app/services/modal-confirm.service.ts +++ b/projects/gameboard-ui/src/app/services/modal-confirm.service.ts @@ -48,7 +48,7 @@ export class ModalConfirmService implements OnDestroy { return this.bsModalService.show(config.content, { initialState: config.context as unknown as Partial, - class: config.modalClasses?.join(" ") || "modal-dialog-centered", + class: ["modal-dialog-centered", ...(config.modalClasses || [])].join(" "), focus: true, ignoreBackdropClick: config.ignoreBackdropClick || false, scrollable: true diff --git a/projects/gameboard-ui/src/app/services/now.service.ts b/projects/gameboard-ui/src/app/services/now.service.ts index a5d5b96d..804f9a3d 100644 --- a/projects/gameboard-ui/src/app/services/now.service.ts +++ b/projects/gameboard-ui/src/app/services/now.service.ts @@ -10,4 +10,8 @@ export class NowService { nowToDateTime(): DateTime { return DateTime.now(); } + + nowToMsEpoch(): number { + return DateTime.now().toUnixInteger() * 1000; + } } diff --git a/projects/gameboard-ui/src/app/services/router.service.ts b/projects/gameboard-ui/src/app/services/router.service.ts index 176dc18b..71532d86 100644 --- a/projects/gameboard-ui/src/app/services/router.service.ts +++ b/projects/gameboard-ui/src/app/services/router.service.ts @@ -9,6 +9,7 @@ import { PlayerMode } from '@/api/player-models'; import { ConfigService } from '@/utility/config.service'; import { UserService as LocalUser } from '@/utility/user.service'; import { slug } from "@/tools/functions"; +import { GameCenterTab } from '@/admin/components/game-center/game-center.models'; export interface QueryParamsUpdate { parameters?: Params, @@ -107,6 +108,10 @@ export class RouterService implements OnDestroy { return this.router.navigateByUrl(this.getCertificatePrintableUrl(mode, challengeSpecOrGameId)); } + public toGameCenter(gameId: string, selectedTab?: GameCenterTab) { + return this.router.navigateByUrl(`/admin/game/${gameId}` + ('/' + selectedTab)); + } + public toReport(key: ReportKey, query: T | null = null): Promise { return this.router.navigateByUrl(this.getReportRoute(key, query)); } diff --git a/projects/gameboard-ui/src/app/sponsors/components/sponsor-admin-entry/sponsor-admin-entry.component.ts b/projects/gameboard-ui/src/app/sponsors/components/sponsor-admin-entry/sponsor-admin-entry.component.ts index 722e4c46..8cbf8193 100644 --- a/projects/gameboard-ui/src/app/sponsors/components/sponsor-admin-entry/sponsor-admin-entry.component.ts +++ b/projects/gameboard-ui/src/app/sponsors/components/sponsor-admin-entry/sponsor-admin-entry.component.ts @@ -29,7 +29,7 @@ export class SponsorAdminEntryComponent { editingSponsor: sponsor, onSave: () => this.requestRefresh.emit() }, - modalClasses: ["modal-dialog-centered", "modal-xl"] + modalClasses: ["modal-xl"] }); } } diff --git a/projects/gameboard-ui/src/app/support/components/ticket-list-page/ticket-list-page.component.html b/projects/gameboard-ui/src/app/support/components/ticket-list-page/ticket-list-page.component.html new file mode 100644 index 00000000..6fb0df44 --- /dev/null +++ b/projects/gameboard-ui/src/app/support/components/ticket-list-page/ticket-list-page.component.html @@ -0,0 +1 @@ + diff --git a/projects/gameboard-ui/src/app/support/components/ticket-list-page/ticket-list-page.component.scss b/projects/gameboard-ui/src/app/support/components/ticket-list-page/ticket-list-page.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/projects/gameboard-ui/src/app/support/components/ticket-list-page/ticket-list-page.component.ts b/projects/gameboard-ui/src/app/support/components/ticket-list-page/ticket-list-page.component.ts new file mode 100644 index 00000000..a50fdb71 --- /dev/null +++ b/projects/gameboard-ui/src/app/support/components/ticket-list-page/ticket-list-page.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-ticket-list-page', + templateUrl: './ticket-list-page.component.html', + styleUrls: ['./ticket-list-page.component.scss'] +}) +export class TicketListPageComponent { + +} diff --git a/projects/gameboard-ui/src/app/support/components/ticket-support-tools/ticket-support-tools.component.ts b/projects/gameboard-ui/src/app/support/components/ticket-support-tools/ticket-support-tools.component.ts index eb590165..6e45ecc5 100644 --- a/projects/gameboard-ui/src/app/support/components/ticket-support-tools/ticket-support-tools.component.ts +++ b/projects/gameboard-ui/src/app/support/components/ticket-support-tools/ticket-support-tools.component.ts @@ -113,7 +113,7 @@ export class TicketSupportToolsComponent implements OnInit { this.modalService.openComponent({ content: EventHorizonModalComponent, context: this.context, - modalClasses: ["modal-lg", "modal-dialog-centered"], + modalClasses: ["modal-lg"], ignoreBackdropClick: true }); } diff --git a/projects/gameboard-ui/src/app/support/support-routing.module.ts b/projects/gameboard-ui/src/app/support/support-routing.module.ts new file mode 100644 index 00000000..a9bf5bac --- /dev/null +++ b/projects/gameboard-ui/src/app/support/support-routing.module.ts @@ -0,0 +1,25 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; +import { SupportPageComponent } from './support-page/support-page.component'; +import { TicketFormComponent } from './ticket-form/ticket-form.component'; +import { TicketListPageComponent } from './components/ticket-list-page/ticket-list-page.component'; +import { TicketDetailsComponent } from './ticket-details/ticket-details.component'; + +@NgModule({ + declarations: [], + imports: [ + CommonModule, + RouterModule.forChild([ + { + path: '', component: SupportPageComponent, children: [ + { path: 'create', component: TicketFormComponent, title: "New Ticket" }, + { path: 'tickets', component: TicketListPageComponent, title: "Support" }, + { path: 'tickets/:id', component: TicketDetailsComponent }, + { path: '', pathMatch: 'full', redirectTo: 'tickets' }, + ] + }, + ]), + ] +}) +export class SupportRoutingModule { } diff --git a/projects/gameboard-ui/src/app/support/support.module.ts b/projects/gameboard-ui/src/app/support/support.module.ts index 1813f150..ecf27b34 100644 --- a/projects/gameboard-ui/src/app/support/support.module.ts +++ b/projects/gameboard-ui/src/app/support/support.module.ts @@ -3,34 +3,32 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { RouterModule } from '@angular/router'; -import { UtilityModule } from '../utility/utility.module'; -import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { FormsModule } from '@angular/forms'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { AlertModule } from 'ngx-bootstrap/alert'; import { ButtonsModule } from 'ngx-bootstrap/buttons'; import { MarkdownModule } from 'ngx-markdown'; +import { ModalModule } from 'ngx-bootstrap/modal'; +import { TooltipModule } from 'ngx-bootstrap/tooltip'; +import { CoreModule } from '@/core/core.module'; +import { UtilityModule } from '../utility/utility.module'; import { TicketFormComponent } from './ticket-form/ticket-form.component'; import { TicketDetailsComponent } from './ticket-details/ticket-details.component'; -import { TicketListComponent } from './ticket-list/ticket-list.component'; import { SupportPageComponent } from './support-page/support-page.component'; -import { ModalModule } from 'ngx-bootstrap/modal'; -import { TooltipModule } from 'ngx-bootstrap/tooltip'; -import { CoreModule } from '../core/core.module'; import { TicketSupportToolsComponent } from './components/ticket-support-tools/ticket-support-tools.component'; import { EventHorizonModule } from '@/event-horizon/event-horizon.module'; -import { TicketLabelPickerComponent } from './components/ticket-label-picker/ticket-label-picker.component'; import { TicketLabelPickerModalComponent } from './components/ticket-label-picker-modal/ticket-label-picker-modal.component'; +import { TicketListPageComponent } from './components/ticket-list-page/ticket-list-page.component'; +import { RouterModule } from '@angular/router'; @NgModule({ declarations: [ SupportPageComponent, TicketDetailsComponent, TicketFormComponent, - TicketListComponent, TicketSupportToolsComponent, - TicketLabelPickerComponent, TicketLabelPickerModalComponent, + TicketListPageComponent, ], imports: [ CommonModule, @@ -38,10 +36,10 @@ import { TicketLabelPickerModalComponent } from './components/ticket-label-picke RouterModule.forChild([ { path: '', component: SupportPageComponent, children: [ - { path: '', pathMatch: 'full', redirectTo: 'tickets' }, { path: 'create', component: TicketFormComponent, title: "New Ticket" }, - { path: 'tickets', component: TicketListComponent, title: "Support" }, - { path: 'tickets/:id', component: TicketDetailsComponent } + { path: 'tickets', component: TicketListPageComponent, title: "Support" }, + { path: 'tickets/:id', component: TicketDetailsComponent }, + { path: '', pathMatch: 'full', redirectTo: 'tickets' }, ] }, ]), @@ -54,6 +52,6 @@ import { TicketLabelPickerModalComponent } from './components/ticket-label-picke ModalModule, TooltipModule, EventHorizonModule - ] + ], }) export class SupportModule { } diff --git a/projects/gameboard-ui/src/app/tools/object-tools.lib.ts b/projects/gameboard-ui/src/app/tools/object-tools.lib.ts index 7d2b1a8c..d47be8c5 100644 --- a/projects/gameboard-ui/src/app/tools/object-tools.lib.ts +++ b/projects/gameboard-ui/src/app/tools/object-tools.lib.ts @@ -2,9 +2,6 @@ export function arraysEqual(a: Array, b: Array): boolean { if (a.length !== b.length) return false; - const aSorted = [...a.sort()]; - const bSorted = [...b.sort()]; - for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) return false; @@ -30,6 +27,17 @@ export function deepEquals(obj1: T1, obj2: T2): bo return JSON.stringify(finalObj1) === JSON.stringify(finalObj2); } +export function cloneNonNullAndDefinedProperties(input: T): T { + const retVal = {} as T; + + for (let property in input) { + if (input[property] !== null && input[property] !== undefined) + retVal[property] = input[property]; + } + + return retVal; +} + export function isEmpty(obj: any): boolean { for (const prop in obj) { if (Object.prototype.hasOwnProperty.call(obj, prop)) { diff --git a/projects/gameboard-ui/src/app/utility/config.service.ts b/projects/gameboard-ui/src/app/utility/config.service.ts index 128caf4d..a1f8b5ee 100644 --- a/projects/gameboard-ui/src/app/utility/config.service.ts +++ b/projects/gameboard-ui/src/app/utility/config.service.ts @@ -177,7 +177,7 @@ export class ConfigService { storeLocal(model: LocalAppSettings): void { try { - this.storage.add(StorageKey.Gameboard, JSON.stringify(model)); + this.storage.add(StorageKey.Gameboard, model); } catch (e) { this.log.logError("Couldn't save app settings to local storage", model); } diff --git a/projects/gameboard-ui/src/app/utility/utility.module.ts b/projects/gameboard-ui/src/app/utility/utility.module.ts index 47610a20..b0dafc89 100644 --- a/projects/gameboard-ui/src/app/utility/utility.module.ts +++ b/projects/gameboard-ui/src/app/utility/utility.module.ts @@ -16,7 +16,6 @@ import { ModalModule } from 'ngx-bootstrap/modal'; import { TooltipModule } from 'ngx-bootstrap/tooltip'; // internal components/services -import { AgedDatePipe } from './pipes/aged-date.pipe'; import { ClipspanComponent } from './components/clipspan/clipspan.component'; import { GameCardComponent } from './components/game-card/game-card.component'; import { ImageManagerComponent } from './components/image-manager/image-manager.component'; @@ -44,7 +43,6 @@ const components = [ LoginComponent, MessageBoardComponent, InplaceEditorComponent, - AgedDatePipe, UntilPipe, ShortTimePipe, UntagPipe, diff --git a/projects/gameboard-ui/src/styles.scss b/projects/gameboard-ui/src/styles.scss index a3514d99..e73a72e0 100644 --- a/projects/gameboard-ui/src/styles.scss +++ b/projects/gameboard-ui/src/styles.scss @@ -175,6 +175,7 @@ th[align="center"] { table.gameboard-table { width: 100%; + margin-bottom: 0; thead { tr > th:not(:first-child) { @@ -227,15 +228,16 @@ table.gameboard-table { } th { - padding-left: 4px; + padding-left: 6px; } + } - td.numeric-col, - th.numeric-col, - th.date-col { - padding: 0; - text-align: center; - } + td.numeric-col, + td.date-col, + th.numeric-col, + th.date-col { + padding: 0; + text-align: center; } tbody td { @@ -416,6 +418,14 @@ th[align="left"] { min-height: 85vh; } +.flex-basis-10 { + flex-basis: 10%; +} + +.flex-basis-15 { + flex-basis: 15%; +} + .flex-basis-25 { flex-basis: 25%; } @@ -436,6 +446,13 @@ th[align="left"] { text-transform: capitalize; } +.text-dashed-underline { + text-decoration: underline; + text-decoration-style: dotted !important; + text-decoration-color: #fff !important; + text-decoration-thickness: 2px !important; +} + .text-upper { text-transform: uppercase; }