feat!: start moving to nuxt

This commit is contained in:
MiniDigger | Martin 2022-12-16 14:57:01 +01:00
parent c287c12b0d
commit dad8789f7e
119 changed files with 4639 additions and 4457 deletions

View File

@ -62,7 +62,7 @@ jobs:
run: mvn --batch-mode --errors --fail-at-end --show-version --no-transfer-progress -Dmaven.repo.local=$GITHUB_WORKSPACE/.m2/repository install
- name: Install frontend deps
run: (cd frontend && pnpm install --frozen-lockfile && cd server && pnpm install --frozen-lockfile)
run: (cd frontend && pnpm install --frozen-lockfile)
- name: Lint frontend
run: (cd frontend && pnpm lint:eslint)
@ -87,7 +87,7 @@ jobs:
AUTH_HOST: "https://hangar-auth.benndorf.dev"
PUBLIC_HOST: "https://hangar.benndorf.dev"
DEBUG: "hangar:*"
run: (cd frontend && pnpm build && cd server && pnpm build)
run: (cd frontend && pnpm build)
- name: Login to GitHub Container Registry
uses: docker/login-action@v2

View File

@ -43,7 +43,7 @@ jobs:
- name: Install frontend deps
env:
CI: true
run: (cd frontend && pnpm install --frozen-lockfile && cd server && pnpm install --frozen-lockfile)
run: (cd frontend && pnpm install --frozen-lockfile)
- name: Lint frontend
env:
@ -58,5 +58,5 @@ jobs:
AUTH_HOST: "https://hangar-auth.benndorf.dev"
PUBLIC_HOST: "https://hangar.benndorf.dev"
DEBUG: "hangar:*"
run: (cd frontend && pnpm build && cd server && pnpm build)
run: (cd frontend && pnpm build)

View File

@ -6,7 +6,8 @@ ENV TERM xterm-256color
ENV HOST 0.0.0.0
EXPOSE 1337
ENV PORT=1337
ENTRYPOINT ["./entrypoint.sh"]
COPY --chown=node:node --chmod=744 /chart/dockerfiles/frontend/entrypoint.sh /hangar-frontend/entrypoint.sh
COPY --chown=node:node /frontend/ /hangar-frontend/
COPY --chown=node:node /frontend/.output /frontend/

View File

@ -1,4 +1,2 @@
#!/usr/bin/env sh
#ls -ahl
#ls -ahl /hangar-frontend/node_modules/.bin
node /hangar-frontend/server
node server/index.mjs

3
frontend/.gitignore vendored
View File

@ -4,3 +4,6 @@ dist
dist-ssr
node_modules
**/_*
.nuxt
src/server/middleware/@proxy/proxy.ts
.output

View File

@ -7,3 +7,5 @@ pnpm-lock.yaml
.vscode/**
.idea/**
server/**
.output/**
.nuxt/**

View File

@ -1,23 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
<link rel="icon" href="/favicon/favicon.svg" type="image/svg+xml" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" />
<link rel="mask-icon" href="/favicon/safari-pinned-tab.svg" color="#5bbad5" />
<!--<link rel="manifest" href="/manifest.webmanifest" /> TODO fix manifest -->
<meta name="apple-mobile-web-app-title" content="Hangar | PaperMC" />
<meta name="application-name" content="Hangar | PaperMC" />
<meta name="theme-color" content="#ffffff" />
</head>
<body class="background-body text-[#262626] dark:text-[#E0E6f0]">
<div id="app"></div>
<script>
window.global = window;
</script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

102
frontend/nuxt.config.ts Normal file
View File

@ -0,0 +1,102 @@
import path from "node:path";
import VueI18n from "@intlify/vite-plugin-vue-i18n";
import IconsResolver from "unplugin-icons/resolver";
import Icons from "unplugin-icons/vite";
import Components from "unplugin-vue-components/vite";
import { ProxyOptions } from "@nuxtjs-alt/proxy";
import prettier from "./src/lib/plugins/prettier";
const backendHost = process.env.BACKEND_HOST || "http://localhost:8080";
const authHost = process.env.AUTH_HOST || "http://localhost:3001";
// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
imports: {
autoImport: false,
},
srcDir: "src",
runtimeConfig: {
authHost,
backendHost,
},
modules: ["nuxt-windicss", "@pinia/nuxt", "@nuxtjs-alt/proxy", "unplugin-icons/nuxt"],
vite: {
plugins: [
// https://github.com/antfu/unplugin-vue-components
Components({
// we don't want to import components, just icons
dirs: ["none"],
// auto import icons
resolvers: [
// https://github.com/antfu/vite-plugin-icons
IconsResolver({
componentPrefix: "icon",
enabledCollections: ["mdi"],
}),
],
dts: "types/generated/icons.d.ts",
}),
// https://github.com/antfu/unplugin-icons
Icons({
autoInstall: true,
}),
// https://github.com/intlify/bundle-tools/tree/main/packages/vite-plugin-vue-i18n
VueI18n({
include: [path.resolve(__dirname, "src/locales/*.json")],
}),
// TODO fix this
// EslintPlugin({
// fix: true,
// }),
prettier(),
],
ssr: {
// Workaround until they support native ESM
noExternal: ["vue3-popper"],
},
},
experimental: {
writeEarlyHints: false,
},
proxy: {
enableProxy: true,
proxies: {
"/api/": defineProxyBackend(),
"/signup": defineProxyBackend(),
"/login": defineProxyBackend(),
"/logout": defineProxyBackend(),
"/handle-logout": defineProxyBackend(),
"/refresh": defineProxyBackend(),
"/invalidate": defineProxyBackend(),
"/v2/api-docs/": defineProxyBackend(),
"/robots.txt": defineProxyBackend(),
"/sitemap.xml": defineProxyBackend(),
"/global-sitemap.xml": defineProxyBackend(),
"/*/sitemap.xml": defineProxyBackend(),
"/statusz": defineProxyBackend(),
// auth
"/avatar": defineProxyAuth(),
"/oauth/logout": defineProxyAuth(),
},
},
});
function defineProxyAuth(): ProxyOptions {
return {
configure: (proxy, options) => {
options.target = process.env.AUTH_HOST || process.env.NITRO_AUTH_HOST || "http://localhost:3001";
},
changeOrigin: true,
};
}
function defineProxyBackend(): ProxyOptions {
return {
configure: (proxy, options) => {
options.target = process.env.BACKEND_HOST || process.env.NITRO_BACKEND_HOST || "http://localhost:8080";
},
changeOrigin: true,
};
}

View File

@ -4,26 +4,27 @@
"node": ">=16"
},
"scripts": {
"dev": "vite-ssr dev --port 3333",
"dev:spa": "vite --port 3333",
"build": "cross-env NODE_ENV=production vite-ssr build",
"preview": "vite-ssr --port 1337 --open",
"lint:eslint": "eslint --ext \".js,.vue,.ts,.json,.html\" --ignore-path .gitignore --ignore-pattern 'server/**' --fix .",
"build": "nuxt build",
"dev": "nuxt dev --port 3333",
"generate": "nuxt generate",
"preview": "nuxt preview",
"previewBuild": "nuxt build && nuxt preview",
"lint:eslint": "eslint --ext \".js,.vue,.ts,.html\" --ignore-path .gitignore --fix .",
"lint:prettier": "prettier -w .",
"prepare": "cd .. && husky install frontend/.husky"
},
"lint-staged": {
"*.{ts,js,vue,json,html}": [
"*.{ts,js,vue,html}": [
"prettier -c",
"eslint"
]
},
"eslintConfig": {
"extends": "./src/lib/config/vitessr.eslint.config.js"
"extends": "./src/lib/config/eslint.config.js"
},
"dependencies": {
"@headlessui/vue": "1.7.4",
"@nuxt/devalue": "2.0.0",
"@pinia/nuxt": "0.4.6",
"@vuelidate/core": "2.0.0",
"@vuelidate/validators": "2.0.0",
"@vueuse/components": "9.6.0",
@ -40,22 +41,21 @@
"jwt-decode": "3.1.2",
"lodash-es": "4.17.21",
"nprogress": "0.2.0",
"ohmyfetch": "0.4.21",
"pinia": "2.0.27",
"prism-theme-vars": "0.2.4",
"qs": "6.11.0",
"swagger-ui-dist": "4.15.5",
"universal-cookie": "4.0.4",
"vite-ssr": "0.16.0",
"vue": "3.2.45",
"vue-advanced-cropper": "2.8.6",
"vue-i18n": "9.2.2",
"vue-router": "4.1.6",
"vue3-popper": "1.5.0"
},
"devDependencies": {
"@iconify/json": "2.1.147",
"@iconify-json/mdi": "1.1.38",
"@intlify/vite-plugin-vue-i18n": "6.0.3",
"@nuxtjs-alt/proxy": "2.1.2",
"@nuxtjs/eslint-config-typescript": "^12.0.0",
"@types/accept-language-parser": "1.5.3",
"@types/chartist": "0.11.1",
"@types/debug": "4.1.7",
@ -67,11 +67,8 @@
"@types/prettier": "2.7.1",
"@types/qs": "6.9.7",
"@types/swagger-ui-dist": "3.30.1",
"@vitejs/plugin-vue": "3.2.0",
"@vue/compiler-sfc": "3.2.45",
"@vue/eslint-config-typescript": "11.0.2",
"@vue/server-renderer": "3.2.45",
"cross-env": "7.0.3",
"eslint": "8.29.0",
"eslint-config-prettier": "8.5.0",
"eslint-import-resolver-alias": "1.1.2",
@ -81,7 +78,8 @@
"eslint-plugin-vue": "9.8.0",
"husky": "8.0.2",
"lint-staged": "13.0.4",
"node-fetch": "3.3.0",
"nuxt": "3.0.0",
"nuxt-windicss": "2.6.0",
"pnpm": "7.18.0",
"prettier": "2.8.0",
"regenerator-runtime": "0.13.11",
@ -91,10 +89,6 @@
"unplugin-icons": "0.14.14",
"unplugin-vue-components": "0.22.11",
"vite": "3.2.4",
"vite-plugin-eslint": "1.8.1",
"vite-plugin-pages": "0.27.1",
"vite-plugin-pwa": "0.13.3",
"vite-plugin-vue-layouts": "0.7.0",
"vite-plugin-windicss": "1.8.8"
"vite-plugin-eslint": "1.8.1"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,22 +0,0 @@
const backendHost = process.env.BACKEND_HOST || "http://localhost:8080";
const authHost = process.env.AUTH_HOST || "http://localhost:3001";
exports["default"] = {
// backend
"/api/": backendHost,
"/signup": backendHost,
"/login": backendHost,
"/logout": backendHost,
"/handle-logout": backendHost,
"/refresh": backendHost,
"/invalidate": backendHost,
"/v2/api-docs/": backendHost,
"/robots.txt": backendHost,
"/sitemap.xml": backendHost,
"/global-sitemap.xml": backendHost,
"/*/sitemap.xml": backendHost,
"/statusz": backendHost,
// auth
"/avatar": authHost,
"/oauth/logout": authHost,
};

View File

@ -1,23 +0,0 @@
{
"private": true,
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node .",
"build:start": "pnpm run build && pnpm run start",
"build:all:start": "cd .. && pnpm run build && cd server && pnpm run build:start"
},
"dependencies": {
"compression": "1.7.4",
"express": "4.18.2",
"http-proxy-middleware": "2.0.6"
},
"devDependencies": {
"@types/compression": "1.7.2",
"@types/express": "4.17.14",
"@types/http-proxy": "1.17.9",
"@types/node": "18.11.10",
"http-proxy": "1.18.1",
"typescript": "4.9.3"
}
}

View File

@ -1,624 +0,0 @@
lockfileVersion: 5.4
specifiers:
'@types/compression': 1.7.2
'@types/express': 4.17.14
'@types/http-proxy': 1.17.9
'@types/node': 18.11.10
compression: 1.7.4
express: 4.18.2
http-proxy: 1.18.1
http-proxy-middleware: 2.0.6
typescript: 4.9.3
dependencies:
compression: 1.7.4
express: 4.18.2
http-proxy-middleware: 2.0.6_@types+express@4.17.14
devDependencies:
'@types/compression': 1.7.2
'@types/express': 4.17.14
'@types/http-proxy': 1.17.9
'@types/node': 18.11.10
http-proxy: 1.18.1
typescript: 4.9.3
packages:
/@types/body-parser/1.19.2:
resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==}
dependencies:
'@types/connect': 3.4.35
'@types/node': 18.11.10
/@types/compression/1.7.2:
resolution: {integrity: sha512-lwEL4M/uAGWngWFLSG87ZDr2kLrbuR8p7X+QZB1OQlT+qkHsCPDVFnHPyXf4Vyl4yDDorNY+mAhosxkCvppatg==}
dependencies:
'@types/express': 4.17.14
dev: true
/@types/connect/3.4.35:
resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==}
dependencies:
'@types/node': 18.11.10
/@types/express-serve-static-core/4.17.28:
resolution: {integrity: sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==}
dependencies:
'@types/node': 18.11.10
'@types/qs': 6.9.7
'@types/range-parser': 1.2.4
/@types/express/4.17.14:
resolution: {integrity: sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==}
dependencies:
'@types/body-parser': 1.19.2
'@types/express-serve-static-core': 4.17.28
'@types/qs': 6.9.7
'@types/serve-static': 1.13.10
/@types/http-proxy/1.17.9:
resolution: {integrity: sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==}
dependencies:
'@types/node': 18.11.10
/@types/mime/1.3.2:
resolution: {integrity: sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==}
/@types/node/18.11.10:
resolution: {integrity: sha512-juG3RWMBOqcOuXC643OAdSA525V44cVgGV6dUDuiFtss+8Fk5x1hI93Rsld43VeJVIeqlP9I7Fn9/qaVqoEAuQ==}
/@types/qs/6.9.7:
resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==}
/@types/range-parser/1.2.4:
resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==}
/@types/serve-static/1.13.10:
resolution: {integrity: sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==}
dependencies:
'@types/mime': 1.3.2
'@types/node': 18.11.10
/accepts/1.3.8:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
dependencies:
mime-types: 2.1.34
negotiator: 0.6.3
dev: false
/array-flatten/1.1.1:
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
dev: false
/body-parser/1.20.1:
resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
dependencies:
bytes: 3.1.2
content-type: 1.0.4
debug: 2.6.9
depd: 2.0.0
destroy: 1.2.0
http-errors: 2.0.0
iconv-lite: 0.4.24
on-finished: 2.4.1
qs: 6.11.0
raw-body: 2.5.1
type-is: 1.6.18
unpipe: 1.0.0
transitivePeerDependencies:
- supports-color
dev: false
/braces/3.0.2:
resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
engines: {node: '>=8'}
dependencies:
fill-range: 7.0.1
dev: false
/bytes/3.0.0:
resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==}
engines: {node: '>= 0.8'}
dev: false
/bytes/3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'}
dev: false
/call-bind/1.0.2:
resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
dependencies:
function-bind: 1.1.1
get-intrinsic: 1.1.2
dev: false
/compressible/2.0.18:
resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==}
engines: {node: '>= 0.6'}
dependencies:
mime-db: 1.51.0
dev: false
/compression/1.7.4:
resolution: {integrity: sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==}
engines: {node: '>= 0.8.0'}
dependencies:
accepts: 1.3.8
bytes: 3.0.0
compressible: 2.0.18
debug: 2.6.9
on-headers: 1.0.2
safe-buffer: 5.1.2
vary: 1.1.2
transitivePeerDependencies:
- supports-color
dev: false
/content-disposition/0.5.4:
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
engines: {node: '>= 0.6'}
dependencies:
safe-buffer: 5.2.1
dev: false
/content-type/1.0.4:
resolution: {integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==}
engines: {node: '>= 0.6'}
dev: false
/cookie-signature/1.0.6:
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
dev: false
/cookie/0.5.0:
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
engines: {node: '>= 0.6'}
dev: false
/debug/2.6.9:
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dependencies:
ms: 2.0.0
dev: false
/depd/2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
dev: false
/destroy/1.2.0:
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
dev: false
/ee-first/1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
dev: false
/encodeurl/1.0.2:
resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
engines: {node: '>= 0.8'}
dev: false
/escape-html/1.0.3:
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
dev: false
/etag/1.8.1:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
dev: false
/eventemitter3/4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
/express/4.18.2:
resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==}
engines: {node: '>= 0.10.0'}
dependencies:
accepts: 1.3.8
array-flatten: 1.1.1
body-parser: 1.20.1
content-disposition: 0.5.4
content-type: 1.0.4
cookie: 0.5.0
cookie-signature: 1.0.6
debug: 2.6.9
depd: 2.0.0
encodeurl: 1.0.2
escape-html: 1.0.3
etag: 1.8.1
finalhandler: 1.2.0
fresh: 0.5.2
http-errors: 2.0.0
merge-descriptors: 1.0.1
methods: 1.1.2
on-finished: 2.4.1
parseurl: 1.3.3
path-to-regexp: 0.1.7
proxy-addr: 2.0.7
qs: 6.11.0
range-parser: 1.2.1
safe-buffer: 5.2.1
send: 0.18.0
serve-static: 1.15.0
setprototypeof: 1.2.0
statuses: 2.0.1
type-is: 1.6.18
utils-merge: 1.0.1
vary: 1.1.2
transitivePeerDependencies:
- supports-color
dev: false
/fill-range/7.0.1:
resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
engines: {node: '>=8'}
dependencies:
to-regex-range: 5.0.1
dev: false
/finalhandler/1.2.0:
resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==}
engines: {node: '>= 0.8'}
dependencies:
debug: 2.6.9
encodeurl: 1.0.2
escape-html: 1.0.3
on-finished: 2.4.1
parseurl: 1.3.3
statuses: 2.0.1
unpipe: 1.0.0
transitivePeerDependencies:
- supports-color
dev: false
/follow-redirects/1.14.9:
resolution: {integrity: sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
/forwarded/0.2.0:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'}
dev: false
/fresh/0.5.2:
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
engines: {node: '>= 0.6'}
dev: false
/function-bind/1.1.1:
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
dev: false
/get-intrinsic/1.1.2:
resolution: {integrity: sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==}
dependencies:
function-bind: 1.1.1
has: 1.0.3
has-symbols: 1.0.3
dev: false
/has-symbols/1.0.3:
resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
engines: {node: '>= 0.4'}
dev: false
/has/1.0.3:
resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
engines: {node: '>= 0.4.0'}
dependencies:
function-bind: 1.1.1
dev: false
/http-errors/2.0.0:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'}
dependencies:
depd: 2.0.0
inherits: 2.0.4
setprototypeof: 1.2.0
statuses: 2.0.1
toidentifier: 1.0.1
dev: false
/http-proxy-middleware/2.0.6_@types+express@4.17.14:
resolution: {integrity: sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==}
engines: {node: '>=12.0.0'}
peerDependencies:
'@types/express': ^4.17.13
peerDependenciesMeta:
'@types/express':
optional: true
dependencies:
'@types/express': 4.17.14
'@types/http-proxy': 1.17.9
http-proxy: 1.18.1
is-glob: 4.0.3
is-plain-obj: 3.0.0
micromatch: 4.0.5
transitivePeerDependencies:
- debug
dev: false
/http-proxy/1.18.1:
resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==}
engines: {node: '>=8.0.0'}
dependencies:
eventemitter3: 4.0.7
follow-redirects: 1.14.9
requires-port: 1.0.0
transitivePeerDependencies:
- debug
/iconv-lite/0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
dependencies:
safer-buffer: 2.1.2
dev: false
/inherits/2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
dev: false
/ipaddr.js/1.9.1:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'}
dev: false
/is-extglob/2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
dev: false
/is-glob/4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
dependencies:
is-extglob: 2.1.1
dev: false
/is-number/7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
dev: false
/is-plain-obj/3.0.0:
resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==}
engines: {node: '>=10'}
dev: false
/media-typer/0.3.0:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'}
dev: false
/merge-descriptors/1.0.1:
resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==}
dev: false
/methods/1.1.2:
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
engines: {node: '>= 0.6'}
dev: false
/micromatch/4.0.5:
resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==}
engines: {node: '>=8.6'}
dependencies:
braces: 3.0.2
picomatch: 2.3.1
dev: false
/mime-db/1.51.0:
resolution: {integrity: sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==}
engines: {node: '>= 0.6'}
dev: false
/mime-types/2.1.34:
resolution: {integrity: sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==}
engines: {node: '>= 0.6'}
dependencies:
mime-db: 1.51.0
dev: false
/mime/1.6.0:
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
engines: {node: '>=4'}
hasBin: true
dev: false
/ms/2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
dev: false
/ms/2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
dev: false
/negotiator/0.6.3:
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
engines: {node: '>= 0.6'}
dev: false
/object-inspect/1.12.2:
resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==}
dev: false
/on-finished/2.4.1:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'}
dependencies:
ee-first: 1.1.1
dev: false
/on-headers/1.0.2:
resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==}
engines: {node: '>= 0.8'}
dev: false
/parseurl/1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
dev: false
/path-to-regexp/0.1.7:
resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
dev: false
/picomatch/2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
dev: false
/proxy-addr/2.0.7:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'}
dependencies:
forwarded: 0.2.0
ipaddr.js: 1.9.1
dev: false
/qs/6.11.0:
resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==}
engines: {node: '>=0.6'}
dependencies:
side-channel: 1.0.4
dev: false
/range-parser/1.2.1:
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
engines: {node: '>= 0.6'}
dev: false
/raw-body/2.5.1:
resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==}
engines: {node: '>= 0.8'}
dependencies:
bytes: 3.1.2
http-errors: 2.0.0
iconv-lite: 0.4.24
unpipe: 1.0.0
dev: false
/requires-port/1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
/safe-buffer/5.1.2:
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
dev: false
/safe-buffer/5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
dev: false
/safer-buffer/2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
dev: false
/send/0.18.0:
resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==}
engines: {node: '>= 0.8.0'}
dependencies:
debug: 2.6.9
depd: 2.0.0
destroy: 1.2.0
encodeurl: 1.0.2
escape-html: 1.0.3
etag: 1.8.1
fresh: 0.5.2
http-errors: 2.0.0
mime: 1.6.0
ms: 2.1.3
on-finished: 2.4.1
range-parser: 1.2.1
statuses: 2.0.1
transitivePeerDependencies:
- supports-color
dev: false
/serve-static/1.15.0:
resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==}
engines: {node: '>= 0.8.0'}
dependencies:
encodeurl: 1.0.2
escape-html: 1.0.3
parseurl: 1.3.3
send: 0.18.0
transitivePeerDependencies:
- supports-color
dev: false
/setprototypeof/1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
dev: false
/side-channel/1.0.4:
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
dependencies:
call-bind: 1.0.2
get-intrinsic: 1.1.2
object-inspect: 1.12.2
dev: false
/statuses/2.0.1:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'}
dev: false
/to-regex-range/5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
dependencies:
is-number: 7.0.0
dev: false
/toidentifier/1.0.1:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
dev: false
/type-is/1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
dependencies:
media-typer: 0.3.0
mime-types: 2.1.34
dev: false
/typescript/4.9.3:
resolution: {integrity: sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==}
engines: {node: '>=4.2.0'}
hasBin: true
dev: true
/unpipe/1.0.0:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'}
dev: false
/utils-merge/1.0.1:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'}
dev: false
/vary/1.1.2:
resolution: {integrity: sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=}
engines: {node: '>= 0.8'}
dev: false

View File

@ -1,58 +0,0 @@
import path from "path";
import express, { Request, Response } from "express";
import compression from "compression";
import { createProxyMiddleware, Options } from "http-proxy-middleware";
const dist = `../../dist`;
// This contains a list of static routes (assets)
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { ssr } = require(`${dist}/server/package.json`);
// The manifest is required for preloading assets
// eslint-disable-next-line @typescript-eslint/no-var-requires
const manifest = require(`${dist}/client/ssr-manifest.json`);
// This is the server renderer we just built
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { default: renderPage } = require(`${dist}/server`);
// vite config
// eslint-disable-next-line @typescript-eslint/no-var-requires
const proxyConfig = require(`${dist}/../proxy.config.ts`).default;
const server = express();
// gzip is cool
server.use(compression());
// Serve every static asset route
for (const asset of ssr.assets || []) {
server.use("/" + asset, express.static(path.join(__dirname, `${dist}/client/` + asset)));
}
// proxy
for (const proxy of Object.keys(proxyConfig)) {
server.use(createProxyMiddleware(proxy, {target: proxyConfig[proxy] } as Options))
}
// main
server.get("*", async (request: Request, response: Response) => {
const url = request.protocol + "://" + request.get("host") + request.originalUrl;
const { html, status, statusText, headers } = await renderPage(url, {
manifest,
preload: true,
request,
response,
});
response.contentType("text/html");
response.setHeader("X-Powered-By", "HangarSSR");
response.writeHead(status || 200, statusText || headers, headers);
response.end(html);
});
const port = 1337;
console.log(`Server started: http://localhost:${port}`);
server.listen(port);

View File

@ -1,16 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true,
"target": "es6",
"noImplicitAny": true,
"moduleResolution": "node",
"sourceMap": true,
"outDir": "dist",
"baseUrl": ".",
"paths": {
"*": ["node_modules/*"]
}
},
"include": ["src/**/*"]
}

View File

@ -1,30 +1,31 @@
<script setup lang="ts">
import { useSettingsStore } from "~/store/useSettingsStore";
import "./lib/styles/main.css";
import { useHead } from "@vueuse/head";
import { computed } from "vue";
import { useSettingsStore } from "~/store/useSettingsStore";
import { settingsLog } from "~/lib/composables/useLog";
import { useAuthStore } from "~/store/auth";
// eslint-disable-next-line import/no-unresolved
import "virtual:windi-devtools";
import { computed } from "vue";
import { useBackendDataStore } from "~/store/backendData";
import "regenerator-runtime/runtime"; // popper needs this?
const authStore = useAuthStore();
const settingsStore = useSettingsStore();
settingsStore.loadSettingsClient();
settingsStore.setupMobile();
await useBackendDataStore().initBackendData();
settingsLog("render for user", authStore.user?.name, "with darkmode", settingsStore.darkMode);
useHead({
htmlAttrs: {
class: computed(() => (settingsStore.darkMode ? "dark" : "light")),
},
bodyAttrs: {
class: "background-body text-[#262626] dark:text-[#E0E6f0]",
},
});
</script>
<template>
<router-view v-slot="{ Component }">
<Suspense>
<component :is="Component" />
<template #fallback> Loading... </template>
</Suspense>
</router-view>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>

View File

@ -1,11 +1,11 @@
<script lang="ts" setup>
import { useContext } from "vite-ssr/vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { useResolvedFlags, useUnresolvedFlags } from "~/composables/useApiHelper";
import { handleRequestError } from "~/composables/useErrorHandling";
import { ref } from "vue";
import { Flag, HangarFlagNotification } from "hangar-internal";
import { PaginatedResult } from "hangar-api";
import { useResolvedFlags, useUnresolvedFlags } from "~/composables/useApiHelper";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useInternalApi } from "~/composables/useApi";
import UserAvatar from "~/components/UserAvatar.vue";
import Card from "~/lib/components/design/Card.vue";
@ -13,26 +13,24 @@ import Button from "~/lib/components/design/Button.vue";
import VisibilityChangerModal from "~/components/modals/VisibilityChangerModal.vue";
import ReportNotificationModal from "~/components/modals/ReportNotificationModal.vue";
import Pagination from "~/lib/components/design/Pagination.vue";
import { PaginatedResult } from "hangar-api";
const props = defineProps<{
resolved: boolean;
}>();
const ctx = useContext();
const i18n = useI18n();
const route = useRoute();
const flags = await (props.resolved ? useResolvedFlags() : useUnresolvedFlags()).catch((e) => handleRequestError(e, ctx, i18n));
const flags = await (props.resolved ? useResolvedFlags() : useUnresolvedFlags()).catch((e) => handleRequestError(e, i18n));
const loading = ref<{ [key: number]: boolean }>({});
function resolve(flag: Flag) {
loading.value[flag.id] = true;
useInternalApi(`flags/${flag.id}/resolve/${props.resolved ? "false" : "true"}`, false, "POST")
.catch<any>((e) => handleRequestError(e, ctx, i18n))
.catch<any>((e) => handleRequestError(e, i18n))
.then(async () => {
if (flags && flags.value) {
const newFlags = await useInternalApi<PaginatedResult<Flag>>("flags/" + (props.resolved ? "resolved" : "unresolved"), false).catch((e) =>
handleRequestError(e, ctx, i18n)
handleRequestError(e, i18n)
);
if (newFlags) {
flags.value = newFlags;
@ -44,7 +42,7 @@ function resolve(flag: Flag) {
});
}
//TODO: bake into hangarflag?
// TODO: bake into hangarflag?
const notifications = ref<HangarFlagNotification[]>([]);
const currentId = ref(-1);
async function getNotifications(flag: Flag) {
@ -53,7 +51,7 @@ async function getNotifications(flag: Flag) {
}
notifications.value = (await useInternalApi<HangarFlagNotification[]>(`flags/${flag.id}/notifications`, false, "get").catch((e) =>
handleRequestError(e, ctx, i18n)
handleRequestError(e, i18n)
)) as HangarFlagNotification[];
currentId.value = flag.id;
}

View File

@ -1,12 +1,10 @@
<script lang="ts" setup>
import { computed, nextTick, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { useInternalApi } from "~/composables/useApi";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useContext } from "vite-ssr/vue";
import { useI18n } from "vue-i18n";
import Spinner from "~/lib/components/design/Spinner.vue";
const ctx = useContext();
const i18n = useI18n();
const props = withDefaults(
defineProps<{
@ -28,7 +26,7 @@ async function fetch() {
loading.value = true;
renderedMarkdown.value = await useInternalApi<string>("pages/render", false, "post", {
content: props.raw,
}).catch<any>((e) => handleRequestError(e, ctx, i18n));
}).catch<any>((e) => handleRequestError(e, i18n));
loading.value = false;
if (!import.meta.env.SSR) {
await nextTick(setupAdmonition);
@ -100,6 +98,6 @@ function setupAdmonition() {
</template>
<style lang="scss">
@import "/src/lib/assets/css/admonition.css";
@import "/src/lib/assets/css/markdown.scss";
@import "@/lib/assets/css/admonition.css";
@import "@/lib/assets/css/markdown.scss";
</style>

View File

@ -1,10 +1,10 @@
<script lang="ts" setup>
import { reactive, ref, watch } from "vue";
import { Pagination } from "hangar-api";
import { hasSlotContent } from "~/lib/composables/useSlot";
import Table from "~/lib/components/design/Table.vue";
import { reactive, ref, watch } from "vue";
import PaginationButtons from "~/lib/components/design/PaginationButtons.vue";
import PaginationComponent from "~/lib/components/design/Pagination.vue";
import { Pagination } from "hangar-api";
export interface Header {
name: string;

View File

@ -1,16 +1,16 @@
<script lang="ts" setup>
import { computed } from "vue";
const props = defineProps<{
name?: string;
color?: Color;
}>();
interface Color {
foreground?: string;
background?: string;
}
const props = defineProps<{
name?: string;
color?: Color;
}>();
const ccColor = computed(() => {
if (props.color?.foreground) {
return props.color;

View File

@ -25,10 +25,17 @@ const props = withDefaults(
const errored = ref(false);
const sizeClass = computed(() => {
if (props.size == "xs") return "w-32px h-32px";
else if (props.size == "sm") return "w-50px h-50px";
else if (props.size == "md") return "w-75px h-75px";
else if (props.size == "lg") return "w-100px h-100px";
switch (props.size) {
case "xs":
return "w-32px h-32px";
case "sm":
return "w-50px h-50px";
case "md":
return "w-75px h-75px";
case "lg":
return "w-100px h-100px";
// No default
}
return "w-200px h-200px";
});

View File

@ -1,12 +1,12 @@
<script lang="ts" setup>
import { Organization } from "hangar-internal";
import { User } from "hangar-api";
import { useI18n } from "vue-i18n";
import { computed } from "vue";
import UserAvatar from "~/components/UserAvatar.vue";
import { avatarUrl, forumUserUrl } from "~/composables/useUrlHelper";
import { useI18n } from "vue-i18n";
import Card from "~/lib/components/design/Card.vue";
import TaglineModal from "~/components/modals/TaglineModal.vue";
import { computed } from "vue";
import { NamedPermission } from "~/types/enums";
import { hasPerms } from "~/composables/usePerm";
import { useAuthStore } from "~/store/auth";

View File

@ -1,6 +1,8 @@
<script setup lang="ts">
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/vue";
import { useI18n } from "vue-i18n";
import { HangarNotification } from "hangar-internal";
import { ref } from "vue";
import { useSettingsStore } from "~/store/useSettingsStore";
import Announcement from "~/components/Announcement.vue";
import DropdownButton from "~/lib/components/design/DropdownButton.vue";
@ -37,18 +39,14 @@ import UserAvatar from "~/components/UserAvatar.vue";
import Button from "~/lib/components/design/Button.vue";
import { useRecentNotifications, useUnreadNotificationsCount } from "~/composables/useApiHelper";
import { handleRequestError } from "~/composables/useErrorHandling";
import { HangarNotification } from "hangar-internal";
import { useContext } from "vite-ssr/vue";
import { ref } from "vue";
import Link from "~/lib/components/design/Link.vue";
import { useInternalApi } from "~/composables/useApi";
import { useConfig } from "~/lib/composables/useConfig";
const settings = useSettingsStore();
const { t } = useI18n();
const backendData = useBackendDataStore();
const ctx = useContext();
const i18n = useI18n();
const t = i18n.t;
const authStore = useAuthStore();
const notifications = ref<HangarNotification[]>([]);
@ -63,7 +61,7 @@ if (authStore.user) {
unreadNotifications.value = v.value;
}
});
useRecentNotifications(true, 30)
useRecentNotifications(30)
.then((v) => {
if (v && v.value) {
// Only show notifications that are recent or unread (from the last 30 notifications)
@ -81,7 +79,7 @@ if (authStore.user) {
});
}
})
.catch((e) => handleRequestError(e, ctx, i18n));
.catch((e) => handleRequestError(e, i18n));
if (hasPerms(NamedPermission.MOD_NOTES_AND_FLAGS)) {
useInternalApi<number>("admin/approval/projectneedingapproval", false)
@ -90,21 +88,21 @@ if (authStore.user) {
projectApprovalQueue.value = v;
}
})
.catch((e) => handleRequestError(e, ctx, i18n));
.catch((e) => handleRequestError(e, i18n));
useInternalApi<number>("admin/approval/versionsneedingapproval", false)
.then((v) => {
if (v) {
versionApprovalQueue.value = v;
}
})
.catch((e) => handleRequestError(e, ctx, i18n));
.catch((e) => handleRequestError(e, i18n));
useInternalApi<number>("flags/unresolvedamount", false)
.then((v) => {
if (v) {
reportQueue.value = v;
}
})
.catch((e) => handleRequestError(e, ctx, i18n));
.catch((e) => handleRequestError(e, i18n));
}
}
@ -147,12 +145,12 @@ function markNotificationsRead() {
}
}
async function markNotificationRead(notification: HangarNotification) {
function markNotificationRead(notification: HangarNotification) {
if (!notification.read) {
notification.read = true;
unreadNotifications.value--;
loadedUnreadNotifications.value--;
useInternalApi(`notifications/${notification.id}`, true, "post").catch((e) => handleRequestError(e, ctx, i18n));
useInternalApi(`notifications/${notification.id}`, true, "post").catch((e) => handleRequestError(e, i18n));
}
}

View File

@ -1,15 +1,14 @@
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import Button from "~/lib/components/design/Button.vue";
import Modal from "~/lib/components/modals/Modal.vue";
import { ProjectChannel } from "hangar-internal";
import { computed, reactive, ref } from "vue";
import { useVuelidate } from "@vuelidate/core";
import Button from "~/lib/components/design/Button.vue";
import Modal from "~/lib/components/modals/Modal.vue";
import { useBackendDataStore } from "~/store/backendData";
import { useContext } from "vite-ssr/vue";
import InputText from "~/lib/components/ui/InputText.vue";
import { required } from "~/lib/composables/useValidationHelpers";
import { validChannelName, validChannelColor } from "~/composables/useHangarValidations";
import { useVuelidate } from "@vuelidate/core";
import InputCheckbox from "~/lib/components/ui/InputCheckbox.vue";
import { ChannelFlag } from "~/types/enums";
@ -23,7 +22,6 @@ const emit = defineEmits<{
}>();
const i18n = useI18n();
const ctx = useContext();
const v = useVuelidate();
const frozen = props.channel && props.channel.flags.includes(ChannelFlag.FROZEN);

View File

@ -1,19 +1,18 @@
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import { HangarProject, HangarVersion } from "hangar-internal";
import { computed, onMounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { DependencyVersion, PluginDependency } from "hangar-api";
import { cloneDeep } from "lodash-es";
import { hasPerms } from "~/composables/usePerm";
import Button from "~/lib/components/design/Button.vue";
import Modal from "~/lib/components/modals/Modal.vue";
import { HangarProject, HangarVersion } from "hangar-internal";
import { useContext } from "vite-ssr/vue";
import { computed, onMounted, ref, watch } from "vue";
import { NamedPermission, Platform } from "~/types/enums";
import { useBackendDataStore } from "~/store/backendData";
import { useRoute, useRouter } from "vue-router";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useInternalApi } from "~/composables/useApi";
import DependencyTable from "~/components/projects/DependencyTable.vue";
import { DependencyVersion, PluginDependency } from "hangar-api";
import { cloneDeep } from "lodash-es";
const props = defineProps<{
project: HangarProject;
@ -21,7 +20,6 @@ const props = defineProps<{
}>();
const i18n = useI18n();
const ctx = useContext();
const route = useRoute();
const router = useRouter();
const backendData = useBackendDataStore();
@ -72,7 +70,7 @@ async function save() {
});
await router.go(0);
} catch (e) {
handleRequestError(e, ctx, i18n);
handleRequestError(e, i18n);
}
loading.value = false;
}

View File

@ -1,8 +1,8 @@
<script lang="ts" setup>
import { TranslateResult, useI18n } from "vue-i18n";
import { type TranslateResult, useI18n } from "vue-i18n";
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch as Diff } from "diff-match-patch";
import Modal from "~/lib/components/modals/Modal.vue";
import { computed } from "vue";
import Modal from "~/lib/components/modals/Modal.vue";
const props = defineProps<{
title: string | TranslateResult;

View File

@ -1,19 +1,18 @@
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import { useRouter } from "vue-router";
import { ref } from "vue";
import { HangarProject } from "hangar-internal";
import { AxiosError } from "axios";
import Button from "~/lib/components/design/Button.vue";
import Modal from "~/lib/components/modals/Modal.vue";
import Tooltip from "~/lib/components/design/Tooltip.vue";
import { useContext } from "vite-ssr/vue";
import { useRouter } from "vue-router";
import InputRadio from "~/lib/components/ui/InputRadio.vue";
import { useBackendDataStore } from "~/store/backendData";
import { ref } from "vue";
import InputTextarea from "~/lib/components/ui/InputTextarea.vue";
import { required } from "~/lib/composables/useValidationHelpers";
import { useInternalApi } from "~/composables/useApi";
import { HangarProject } from "hangar-internal";
import { handleRequestError } from "~/composables/useErrorHandling";
import { AxiosError } from "axios";
import { useNotificationStore } from "~/lib/store/notification";
const props = defineProps<{
@ -22,7 +21,6 @@ const props = defineProps<{
}>();
const i18n = useI18n();
const ctx = useContext();
const router = useRouter();
const backendData = useBackendDataStore();
@ -40,7 +38,7 @@ async function submit(close: () => void) {
useNotificationStore().success(i18n.t("project.flag.flagSend"));
await router.go(0);
} catch (e) {
handleRequestError(e as AxiosError, ctx, i18n);
handleRequestError(e as AxiosError, i18n);
}
}
</script>

View File

@ -1,15 +1,14 @@
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import { AxiosError } from "axios";
import { User } from "hangar-api";
import { ref } from "vue";
import { useRouter } from "vue-router";
import Button from "~/lib/components/design/Button.vue";
import Modal from "~/lib/components/modals/Modal.vue";
import { useInternalApi } from "~/composables/useApi";
import { handleRequestError } from "~/composables/useErrorHandling";
import { AxiosError } from "axios";
import Tooltip from "~/lib/components/design/Tooltip.vue";
import { useContext } from "vite-ssr/vue";
import { User } from "hangar-api";
import { ref } from "vue";
import { useRouter } from "vue-router";
import InputTextarea from "~/lib/components/ui/InputTextarea.vue";
import { useNotificationStore } from "~/lib/store/notification";
@ -18,7 +17,6 @@ const props = defineProps<{
}>();
const i18n = useI18n();
const ctx = useContext();
const router = useRouter();
const comment = ref<string>("");
@ -32,7 +30,7 @@ async function confirm(close: () => void) {
router.go(0);
useNotificationStore().success(i18n.t(`author.lock.success${props.user.locked ? "Unlock" : "Lock"}`, [props.user.name]));
} catch (e) {
handleRequestError(e as AxiosError, ctx, i18n);
handleRequestError(e as AxiosError, i18n);
}
}
</script>

View File

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { TranslateResult, useI18n } from "vue-i18n";
import { type TranslateResult, useI18n } from "vue-i18n";
import Markdown from "~/components/Markdown.vue";
import Modal from "~/lib/components/modals/Modal.vue";

View File

@ -1,9 +1,8 @@
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import { useRouter } from "vue-router";
import Button from "~/lib/components/design/Button.vue";
import Modal from "~/lib/components/modals/Modal.vue";
import { useContext } from "vite-ssr/vue";
import { useRouter } from "vue-router";
import { useInternalApi } from "~/composables/useApi";
import { handleRequestError } from "~/composables/useErrorHandling";
@ -20,15 +19,14 @@ const props = withDefaults(
);
const i18n = useI18n();
const ctx = useContext();
const router = useRouter();
const name = props.organization ? props.author : props.slug;
async function leave() {
function leave() {
const url = props.organization ? `organizations/org/${props.author}/members/leave` : `projects/project/${props.author}/${props.slug}/members/leave`;
useInternalApi(url, true, "post")
.then(() => router.go(0))
.catch((e) => handleRequestError(e, ctx, i18n));
.catch((e) => handleRequestError(e, i18n));
}
</script>

View File

@ -1,17 +1,16 @@
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import { HangarProjectPage } from "hangar-internal";
import { computed, inject, ref, watch } from "vue";
import { AxiosError } from "axios";
import { useRoute, useRouter } from "vue-router";
import Button from "~/lib/components/design/Button.vue";
import Modal from "~/lib/components/modals/Modal.vue";
import { useBackendDataStore } from "~/store/backendData";
import { useContext } from "vite-ssr/vue";
import { HangarProjectPage } from "hangar-internal";
import { computed, inject, ref, watch } from "vue";
import InputText from "~/lib/components/ui/InputText.vue";
import InputSelect, { Option } from "~/lib/components/ui/InputSelect.vue";
import { useInternalApi } from "~/composables/useApi";
import { handleRequestError } from "~/composables/useErrorHandling";
import { AxiosError } from "axios";
import { useRoute, useRouter } from "vue-router";
const props = defineProps<{
projectId: number;
@ -19,7 +18,6 @@ const props = defineProps<{
}>();
const i18n = useI18n();
const ctx = useContext();
const route = useRoute();
const router = useRouter();
const backendData = useBackendDataStore();
@ -45,7 +43,7 @@ watch(name, async () => {
})
.catch((err: AxiosError) => {
if (!err.response?.data.isHangarApiException) {
return handleRequestError(err, ctx, i18n);
return handleRequestError(err, i18n);
}
nameErrorMessages.value.push(i18n.t(err.response.data.message));
})
@ -79,7 +77,7 @@ async function createPage() {
await router.push(`/${route.params.user}/${route.params.project}/pages/${slug}`);
} catch (e) {
handleRequestError(e, ctx, i18n);
handleRequestError(e, i18n);
}
loading.value = false;

View File

@ -1,10 +1,9 @@
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import Button from "~/lib/components/design/Button.vue";
import Modal from "~/lib/components/modals/Modal.vue";
import { useContext } from "vite-ssr/vue";
import { useRouter } from "vue-router";
import { ref } from "vue";
import Button from "~/lib/components/design/Button.vue";
import Modal from "~/lib/components/modals/Modal.vue";
import InputTextarea from "~/lib/components/ui/InputTextarea.vue";
import { useInternalApi } from "~/composables/useApi";
import { handleRequestError } from "~/composables/useErrorHandling";
@ -14,7 +13,6 @@ const props = defineProps<{
}>();
const i18n = useI18n();
const ctx = useContext();
const router = useRouter();
const comment = ref<string>("");
@ -24,7 +22,7 @@ async function deleteOrg() {
loading.value = true;
await useInternalApi(`organizations/org/${props.organization}/delete`, true, "post", {
content: comment.value,
}).catch((e) => handleRequestError(e, ctx, i18n));
}).catch((e) => handleRequestError(e, i18n));
await router.push("/");
loading.value = false;
}

View File

@ -1,14 +1,13 @@
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import { useRouter } from "vue-router";
import { ref } from "vue";
import { PaginatedResult, User } from "hangar-api";
import Button from "~/lib/components/design/Button.vue";
import Modal from "~/lib/components/modals/Modal.vue";
import { useContext } from "vite-ssr/vue";
import { useRouter } from "vue-router";
import { useApi, useInternalApi } from "~/composables/useApi";
import { handleRequestError } from "~/composables/useErrorHandling";
import InputAutocomplete from "~/lib/components/ui/InputAutocomplete.vue";
import { ref } from "vue";
import { PaginatedResult, User } from "hangar-api";
import { useNotificationStore } from "~/lib/store/notification";
const props = defineProps<{
@ -16,7 +15,6 @@ const props = defineProps<{
}>();
const i18n = useI18n();
const ctx = useContext();
const router = useRouter();
const notificationStore = useNotificationStore();
@ -37,7 +35,7 @@ async function transfer() {
loading.value = true;
await useInternalApi<string>(`organizations/org/${props.organization}/transfer`, true, "post", {
content: search.value,
}).catch((e) => handleRequestError(e, ctx, i18n));
}).catch((e) => handleRequestError(e, i18n));
notificationStore.success(i18n.t("organization.settings.success.transferRequest", [search.value]));
loading.value = false;
}

View File

@ -1,11 +1,10 @@
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import { computed, ref } from "vue";
import Markdown from "~/components/Markdown.vue";
import Button from "~/lib/components/design/Button.vue";
import Modal from "~/lib/components/modals/Modal.vue";
import { useInternalApi } from "~/composables/useApi";
import { computed, ref } from "vue";
import { useContext } from "vite-ssr/vue";
import { handleRequestError } from "~/composables/useErrorHandling";
import InputCheckbox from "~/lib/components/ui/InputCheckbox.vue";
@ -14,7 +13,6 @@ const props = defineProps<{
}>();
const i18n = useI18n();
const ctx = useContext();
const emit = defineEmits<{
(e: "update:modelValue", value: { [key: string]: boolean }): void;
@ -31,7 +29,7 @@ async function changeOrgVisibility(org: string) {
loading.value = true;
await useInternalApi<{ [key: string]: boolean }>(`organizations/${org}/userOrganizationsVisibility`, true, "POST", internalVisibility.value[org] as any, {
"Content-Type": "application/json",
}).catch((e) => handleRequestError(e, ctx, i18n));
}).catch((e) => handleRequestError(e, i18n));
loading.value = false;
}
</script>

View File

@ -1,13 +1,12 @@
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import { HangarProject, HangarVersion } from "hangar-internal";
import { computed, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import Button from "~/lib/components/design/Button.vue";
import Modal from "~/lib/components/modals/Modal.vue";
import { HangarProject, HangarVersion } from "hangar-internal";
import { useContext } from "vite-ssr/vue";
import { computed, ref } from "vue";
import { Platform } from "~/types/enums";
import { useBackendDataStore } from "~/store/backendData";
import { useRoute, useRouter } from "vue-router";
import InputCheckbox from "~/lib/components/ui/InputCheckbox.vue";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useInternalApi } from "~/composables/useApi";
@ -19,7 +18,6 @@ const props = defineProps<{
}>();
const i18n = useI18n();
const ctx = useContext();
const route = useRoute();
const router = useRouter();
const backendData = useBackendDataStore();
@ -34,13 +32,13 @@ const projectVersion = computed(() => {
const loading = ref(false);
const selectedVersions = ref(projectVersion.value?.platformDependencies[platform.value?.name.toUpperCase() as Platform]);
async function save() {
function save() {
loading.value = true;
useInternalApi(`versions/version/${props.project.id}/${projectVersion.value?.id}/savePlatformVersions`, true, "post", {
platform: platform.value?.name?.toUpperCase(),
versions: selectedVersions.value,
})
.catch((e) => handleRequestError(e, ctx, i18n))
.catch((e) => handleRequestError(e, i18n))
.then(async () => {
await router.go(0);
})

View File

@ -1,19 +1,18 @@
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import { computed, ref } from "vue";
import { useRouter } from "vue-router";
import { Flag } from "hangar-internal";
import { Project } from "hangar-api";
import Button from "~/lib/components/design/Button.vue";
import Modal from "~/lib/components/modals/Modal.vue";
import { Visibility } from "~/types/enums";
import InputRadio from "~/lib/components/ui/InputRadio.vue";
import { useBackendDataStore } from "~/store/backendData";
import { computed, ref } from "vue";
import InputTextarea from "~/lib/components/ui/InputTextarea.vue";
import { useInternalApi } from "~/composables/useApi";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useContext } from "vite-ssr/vue";
import { useNotificationStore } from "~/lib/store/notification";
import { useRouter } from "vue-router";
import { Flag } from "hangar-internal";
import { Project } from "hangar-api";
import InputCheckbox from "~/lib/components/ui/InputCheckbox.vue";
const props = defineProps<{
@ -22,7 +21,6 @@ const props = defineProps<{
}>();
const i18n = useI18n();
const ctx = useContext();
const backendData = useBackendDataStore();
const notification = useNotificationStore();
const router = useRouter();
@ -35,7 +33,7 @@ async function submit() {
warning: warning.value,
toReporter: props.sendToReporter,
content: content.value,
}).catch((e) => handleRequestError(e, ctx, i18n));
}).catch((e) => handleRequestError(e, i18n));
content.value = "";
router.go(0);
}

View File

@ -1,14 +1,13 @@
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import { ref } from "vue";
import { useRouter } from "vue-router";
import Button from "~/lib/components/design/Button.vue";
import Modal from "~/lib/components/modals/Modal.vue";
import { ref } from "vue";
import { useNotificationStore } from "~/lib/store/notification";
import InputText from "~/lib/components/ui/InputText.vue";
import { useInternalApi } from "~/composables/useApi";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useContext } from "vite-ssr/vue";
import { useRouter } from "vue-router";
import { useBackendDataStore } from "~/store/backendData";
const props = defineProps<{
@ -18,7 +17,6 @@ const props = defineProps<{
const newTagline = ref(props.tagline);
const ctx = useContext();
const router = useRouter();
const i18n = useI18n();
const backendData = useBackendDataStore();
@ -32,7 +30,7 @@ async function save() {
});
router.go(0);
} catch (e) {
handleRequestError(e, ctx, i18n);
handleRequestError(e, i18n);
}
loading.value = false;
}

View File

@ -1,17 +1,16 @@
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import { computed, ref } from "vue";
import { useRouter } from "vue-router";
import Button from "~/lib/components/design/Button.vue";
import Modal from "~/lib/components/modals/Modal.vue";
import { Visibility } from "~/types/enums";
import InputRadio from "~/lib/components/ui/InputRadio.vue";
import { useBackendDataStore } from "~/store/backendData";
import { computed, ref } from "vue";
import InputTextarea from "~/lib/components/ui/InputTextarea.vue";
import { useInternalApi } from "~/composables/useApi";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useContext } from "vite-ssr/vue";
import { useNotificationStore } from "~/lib/store/notification";
import { useRouter } from "vue-router";
const props = defineProps<{
type: "project" | "version";
@ -20,7 +19,6 @@ const props = defineProps<{
}>();
const i18n = useI18n();
const ctx = useContext();
const backendData = useBackendDataStore();
const notification = useNotificationStore();
const router = useRouter();
@ -36,7 +34,7 @@ async function submit(): Promise<void> {
await useInternalApi(props.postUrl, true, "post", {
visibility: visibility.value,
comment: setVisibility.value?.showModal ? reason.value : null,
}).catch((e) => handleRequestError(e, ctx, i18n));
}).catch((e) => handleRequestError(e, i18n));
reason.value = "";
if (setVisibility.value) {
notification.success(i18n.t("visibility.modal.success", [props.type, i18n.t(setVisibility.value?.title)]));

View File

@ -1,7 +1,7 @@
<script lang="ts" setup>
import { ProjectApproval } from "hangar-internal";
import Alert from "~/lib/components/design/Alert.vue";
import { useI18n } from "vue-i18n";
import Alert from "~/lib/components/design/Alert.vue";
import Markdown from "~/components/Markdown.vue";
import Link from "~/lib/components/design/Link.vue";
import VisibilityChangerModal from "~/components/modals/VisibilityChangerModal.vue";

View File

@ -1,16 +1,16 @@
<script lang="ts" setup>
import { DependencyVersion, PaginatedResult, PluginDependency, Project, ProjectNamespace } from "hangar-api";
import { useI18n } from "vue-i18n";
import { computed, ref } from "vue";
import { useRoute } from "vue-router";
import { Platform } from "~/types/enums";
import Table from "~/lib/components/design/Table.vue";
import { useI18n } from "vue-i18n";
import Button from "~/lib/components/design/Button.vue";
import InputCheckbox from "~/lib/components/ui/InputCheckbox.vue";
import InputText from "~/lib/components/ui/InputText.vue";
import { required } from "~/lib/composables/useValidationHelpers";
import { computed, ref } from "vue";
import InputAutocomplete from "~/lib/components/ui/InputAutocomplete.vue";
import { useApi } from "~/composables/useApi";
import { useRoute } from "vue-router";
import Tabs, { Tab } from "~/lib/components/design/Tabs.vue";
const route = useRoute();
@ -93,7 +93,7 @@ const selectedUploadTabs: Tab[] = [
{ value: "url", header: "URL" },
];
defineExpose({ dependencies, reset: reset });
defineExpose({ dependencies, reset });
</script>
<template>

View File

@ -1,14 +1,14 @@
<script lang="ts" setup>
import Button from "~/lib/components/design/Button.vue";
import { useI18n } from "vue-i18n";
import { HangarProject, PinnedVersion } from "hangar-internal";
import { computed } from "vue";
import { PlatformVersionDownload } from "hangar-api";
import Button from "~/lib/components/design/Button.vue";
import { Platform } from "~/types/enums";
import DropdownButton from "~/lib/components/design/DropdownButton.vue";
import { useBackendDataStore } from "~/store/backendData";
import DropdownItem from "~/lib/components/design/DropdownItem.vue";
import PlatformLogo from "~/components/logos/platforms/PlatformLogo.vue";
import { PlatformVersionDownload } from "hangar-api";
const i18n = useI18n();
const backendData = useBackendDataStore();

View File

@ -2,6 +2,8 @@
import { computed, handleError, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { JoinableMember } from "hangar-internal";
import { PaginatedResult, Role, User } from "hangar-api";
import { useRoute, useRouter } from "vue-router";
import { NamedPermission } from "~/types/enums";
import Card from "~/lib/components/design/Card.vue";
import UserAvatar from "~/components/UserAvatar.vue";
@ -11,10 +13,7 @@ import DropdownItem from "~/lib/components/design/DropdownItem.vue";
import { avatarUrl } from "~/composables/useUrlHelper";
import { hasPerms } from "~/composables/usePerm";
import { useBackendDataStore } from "~/store/backendData";
import { PaginatedResult, Role, User } from "hangar-api";
import { useRoute, useRouter } from "vue-router";
import { useApi, useInternalApi } from "~/composables/useApi";
import { useContext } from "vite-ssr/vue";
import IconMdiClock from "~icons/mdi/clock";
import Tooltip from "~/lib/components/design/Tooltip.vue";
import InputAutocomplete from "~/lib/components/ui/InputAutocomplete.vue";
@ -22,6 +21,11 @@ import { useAuthStore } from "~/store/auth";
import MemberLeaveModal from "~/components/modals/MemberLeaveModal.vue";
import { handleRequestError } from "~/composables/useErrorHandling";
interface EditableMember {
name: string;
roleId: number;
}
const props = withDefaults(
defineProps<{
members: JoinableMember[];
@ -54,7 +58,6 @@ const i18n = useI18n();
const store = useBackendDataStore();
const route = useRoute();
const router = useRouter();
const ctx = useContext();
const authStore = useAuthStore();
const roles: Role[] = props.organization ? store.orgRoles : store.projectRoles;
@ -88,7 +91,7 @@ function cancelTransfer() {
const url = props.organization ? `organizations/org/${props.author}/canceltransfer` : `projects/project/${props.author}/${props.slug}/canceltransfer`;
useInternalApi(url, true, "post")
.then(() => router.go(0))
.catch((e) => handleRequestError(e, ctx, i18n))
.catch((e) => handleRequestError(e, i18n))
.finally(() => (saving.value = false));
}
@ -104,7 +107,7 @@ function invite(member: string, role: Role) {
return "";
}
async function post(member: EditableMember, action: "edit" | "add" | "remove") {
function post(member: EditableMember, action: "edit" | "add" | "remove") {
addErrors.value = [];
if (member.name.length === 0) {
addErrors.value.push(i18n.t("general.error.nameEmpty"));
@ -144,11 +147,6 @@ async function doSearch(val: string) {
});
result.value = users.result.filter((u) => !props.members.some((m) => m.user.name === u.name)).map((u) => u.name);
}
interface EditableMember {
name: string;
roleId: number;
}
</script>
<template>

View File

@ -1,12 +1,12 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { Project } from "hangar-api";
import Card from "~/lib/components/design/Card.vue";
import Link from "~/lib/components/design/Link.vue";
import UserAvatar from "~/components/UserAvatar.vue";
import { projectIconUrl } from "~/composables/useUrlHelper";
import { lastUpdated } from "~/lib/composables/useTime";
import { useI18n } from "vue-i18n";
import Tooltip from "~/lib/components/design/Tooltip.vue";
import { Project } from "hangar-api";
import { Visibility } from "~/types/enums";
import CategoryLogo from "~/components/logos/categories/CategoryLogo.vue";

View File

@ -1,14 +1,15 @@
<script setup lang="ts">
import { computed, ref, Ref } from "vue";
import { computed, ref, type Ref } from "vue";
import { useI18n } from "vue-i18n";
import { HangarProject } from "hangar-internal";
import { AxiosError } from "axios";
import { useRouter } from "vue-router";
import UserAvatar from "~/components/UserAvatar.vue";
import Button from "~/lib/components/design/Button.vue";
import Card from "~/lib/components/design/Card.vue";
import { projectIconUrl } from "~/composables/useUrlHelper";
import { HangarProject } from "hangar-internal";
import { useInternalApi } from "~/composables/useApi";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useContext } from "vite-ssr/vue";
import Tooltip from "~/lib/components/design/Tooltip.vue";
import { useAuthStore } from "~/store/auth";
import { useNotificationStore } from "~/lib/store/notification";
@ -17,11 +18,8 @@ import Alert from "~/lib/components/design/Alert.vue";
import { hasPerms } from "~/composables/usePerm";
import { NamedPermission, Platform, ReviewState, Visibility } from "~/types/enums";
import Markdown from "~/components/Markdown.vue";
import { AxiosError } from "axios";
import { useRouter } from "vue-router";
import DownloadButton from "~/components/projects/DownloadButton.vue";
const ctx = useContext();
const i18n = useI18n();
const router = useRouter();
const notification = useNotificationStore();
@ -54,7 +52,7 @@ function toggleState(route: string, completedKey: string, revokedKey: string, va
notification.success(i18n.t("project.actions." + (value.value ? completedKey : revokedKey)));
})
.catch((err) => handleRequestError(err, ctx, i18n, i18n.t(`project.error.${route}`)));
.catch((err) => handleRequestError(err, i18n, i18n.t(`project.error.${route}`)));
}
function toggleStar() {
@ -71,7 +69,7 @@ async function sendForApproval() {
notification.success(i18n.t("projectApproval.sendForApproval"));
await router.go(0);
} catch (e) {
handleRequestError(e as AxiosError, ctx, i18n);
handleRequestError(e as AxiosError, i18n);
}
}

View File

@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed } from "vue";
import { useI18n } from "vue-i18n";
import { HangarProject } from "hangar-internal";
import { forumUrl } from "~/composables/useUrlHelper";
import Card from "~/lib/components/design/Card.vue";
import Link from "~/lib/components/design/Link.vue";
@ -8,7 +9,6 @@ import DropdownButton from "~/lib/components/design/DropdownButton.vue";
import DropdownItem from "~/lib/components/design/DropdownItem.vue";
import { hasPerms } from "~/composables/usePerm";
import { NamedPermission } from "~/types/enums";
import { HangarProject } from "hangar-internal";
import DonationModal from "~/components/donation/DonationModal.vue";
import VisibilityChangerModal from "~/components/modals/VisibilityChangerModal.vue";

View File

@ -1,6 +1,5 @@
<script setup lang="ts">
import { PaginatedResult, Project } from "hangar-api";
import { PropType } from "vue";
import { useI18n } from "vue-i18n";
import Pagination from "~/lib/components/design/Pagination.vue";
import ProjectCard from "~/components/projects/ProjectCard.vue";

View File

@ -1,10 +1,10 @@
<script setup lang="ts">
import ProjectNavItem from "~/components/projects/ProjectNavItem.vue";
import { computed } from "vue";
import { hasPerms } from "~/composables/usePerm";
import { NamedPermission } from "~/types/enums";
import { useI18n } from "vue-i18n";
import { HangarProject } from "hangar-internal";
import ProjectNavItem from "~/components/projects/ProjectNavItem.vue";
import { hasPerms } from "~/composables/usePerm";
import { NamedPermission } from "~/types/enums";
import { avatarUrl, linkout } from "~/composables/useUrlHelper";
const props = defineProps<{

View File

@ -12,7 +12,7 @@ const route = useRoute();
const selected = computed(() => {
const routerPath = route.fullPath.endsWith("/") ? route.fullPath.substr(0, route.fullPath.length - 1) : route.fullPath;
return routerPath == props.to;
return routerPath === props.to;
});
const clazz = computed(() => {

View File

@ -1,13 +1,13 @@
<script setup lang="ts">
import { HangarProject } from "hangar-internal";
import Card from "~/lib/components/design/Card.vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import Card from "~/lib/components/design/Card.vue";
import { hasPerms } from "~/composables/usePerm";
import { NamedPermission } from "~/types/enums";
import NewPageModal from "~/components/modals/NewPageModal.vue";
import TreeView from "~/lib/components/design/TreeView.vue";
import Link from "~/lib/components/design/Link.vue";
import { useRoute } from "vue-router";
const props = defineProps<{
project: HangarProject;

View File

@ -2,11 +2,10 @@
import { HangarProject, HangarProjectPage } from "hangar-internal";
import { useI18n } from "vue-i18n";
import { useRoute, useRouter } from "vue-router";
import { useProjectPage } from "~/composables/useProjectPage";
import { useContext } from "vite-ssr";
import { useHead } from "@vueuse/head";
import { useSeo } from "~/composables/useSeo";
import { inject } from "vue";
import { useProjectPage } from "~/composables/useProjectPage";
import { useSeo } from "~/composables/useSeo";
import { useInternalApi } from "~/composables/useApi";
import { handleRequestError } from "~/composables/useErrorHandling";
import { projectIconUrl } from "~/composables/useUrlHelper";
@ -17,12 +16,11 @@ const props = defineProps<{
const route = useRoute();
const router = useRouter();
const ctx = useContext();
const i18n = useI18n();
const updateProjectPages = inject<(pages: HangarProjectPage[]) => void>("updateProjectPages");
const { editingPage, changeEditingPage, page, savePage, deletePage } = await useProjectPage(route, router, ctx, i18n, props.project);
const { editingPage, changeEditingPage, page, savePage, deletePage } = await useProjectPage(route, router, i18n, props.project);
if (page) {
const title = page.value?.name === "Home" ? props.project.name : page.value?.name + " | " + props.project.name;
useHead(useSeo(title, props.project.description, route, projectIconUrl(props.project.namespace.owner, props.project.namespace.slug)));
@ -36,7 +34,7 @@ async function deletePageAndUpdateProject() {
updateProjectPages(await useInternalApi<HangarProjectPage[]>(`pages/list/${props.project.id}`, false, "get"));
}
} catch (e: any) {
handleRequestError(e, ctx, i18n);
handleRequestError(e, i18n);
}
}
</script>

View File

@ -1,7 +1,7 @@
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import { hasSlotContent } from "~/lib/composables/useSlot";
import { useSlots } from "vue";
import { hasSlotContent } from "~/lib/composables/useSlot";
const props = withDefaults(
defineProps<{

View File

@ -2,14 +2,13 @@ import type { AxiosError, AxiosRequestConfig } from "axios";
import type { HangarApiException } from "hangar-api";
import qs from "qs";
import Cookies from "universal-cookie";
import { isEmpty } from "lodash-es";
import type { Ref } from "vue";
import { useAxios } from "~/composables/useAxios";
import { useCookies } from "~/composables/useCookies";
import { Ref } from "vue";
import { authLog, fetchLog } from "~/lib/composables/useLog";
import { isEmpty } from "lodash-es";
import { useAuth } from "~/composables/useAuth";
import { useRequest, useResponse } from "~/composables/useResReq";
import { Context } from "vite-ssr/vue";
import { useRequestEvent } from "#imports";
function request<T>(url: string, method: AxiosRequestConfig["method"], data: object, headers: Record<string, string> = {}, retry = false): Promise<T> {
const cookies = useCookies();
@ -42,7 +41,7 @@ function request<T>(url: string, method: AxiosRequestConfig["method"], data: obj
resolve(data);
})
.catch(async (error: AxiosError) => {
const { trace, ...err } = error.response?.data as { trace: any };
const { trace, ...err } = (error.response?.data as { trace: any }) || {};
authLog("failed", err);
// do we have an expired token?
if (error.response?.status === 403) {
@ -78,7 +77,7 @@ function request<T>(url: string, method: AxiosRequestConfig["method"], data: obj
});
}
export async function useApi<T>(
export function useApi<T>(
url: string,
authed = true,
method: AxiosRequestConfig["method"] = "get",
@ -89,7 +88,7 @@ export async function useApi<T>(
return processAuthStuff(headers, authed, (headers) => request(`v1/${url}`, method, data, headers));
}
export async function useInternalApi<T = void>(
export function useInternalApi<T = void>(
url: string,
authed = true,
method: AxiosRequestConfig["method"] = "get",
@ -106,7 +105,7 @@ export async function processAuthStuff<T>(headers: Record<string, string>, authR
let token = useCookies().get("HangarAuth");
let refreshToken = useCookies().get("HangarAuth_REFRESH");
if (!token) {
const header = useResponse()?.getHeader("set-cookie") as string[];
const header = useRequestEvent().node.res?.getHeader("set-cookie") as string[];
if (header && header.join) {
const cookies = new Cookies(header.join("; "));
token = cookies.get("HangarAuth");
@ -127,6 +126,7 @@ export async function processAuthStuff<T>(headers: Record<string, string>, authR
} else {
authLog("could not refresh, invalidate");
await useAuth.invalidate();
// eslint-disable-next-line no-throw-literal
throw {
isAxiosError: true,
response: {
@ -146,6 +146,7 @@ export async function processAuthStuff<T>(headers: Record<string, string>, authR
} else {
authLog("can't forward token, no cookie");
if (authRequired) {
// eslint-disable-next-line no-throw-literal
throw {
isAxiosError: true,
response: {
@ -173,6 +174,7 @@ export async function processAuthStuff<T>(headers: Record<string, string>, authR
authLog("could not refresh, invalidate");
await useAuth.invalidate();
if (authRequired) {
// eslint-disable-next-line no-throw-literal
throw {
isAxiosError: true,
response: {
@ -193,7 +195,7 @@ export async function processAuthStuff<T>(headers: Record<string, string>, authR
}
function forwardHeader(): Record<string, string> {
const req: Context["request"] = useRequest();
const req = useRequestEvent().node.req;
const result: Record<string, string> = {};
if (!req) return result;

View File

@ -1,6 +1,4 @@
import { useApi, useInternalApi } from "~/composables/useApi";
import { PaginatedResult, Project, ProjectCompact, User, Version } from "hangar-api";
import { useInitialState } from "~/composables/useInitialState";
import {
Flag,
HangarNotification,
@ -17,151 +15,139 @@ import {
ReviewQueueEntry,
RoleTable,
} from "hangar-internal";
import { useApi, useInternalApi } from "~/composables/useApi";
import { useAsyncData } from "#imports";
export async function useProjects(params: Record<string, any> = { limit: 25, offset: 0 }, blocking = true) {
return useInitialState("useProjects", () => useApi<PaginatedResult<Project>>("projects", false, "get", params), blocking);
export async function useProjects(params: Record<string, any> = { limit: 25, offset: 0 }) {
return (await useAsyncData("useProjects", () => useApi<PaginatedResult<Project>>("projects", false, "get", params))).data;
}
export async function useUser(user: string, blocking = true) {
return useInitialState("useUser:" + user, () => useApi<User>("users/" + user, false), blocking);
export async function useUser(user: string) {
return (await useAsyncData("useUser:" + user, () => useApi<User>("users/" + user, false))).data;
}
export async function useOrganization(user: string, blocking = true) {
return useInitialState("useOrganization:" + user, () => useInternalApi<Organization>(`organizations/org/${user}`, false), blocking);
export async function useOrganization(user: string) {
return (await useAsyncData("useOrganization:" + user, () => useInternalApi<Organization>(`organizations/org/${user}`, false))).data;
}
export async function useProject(user: string, project: string, blocking = true) {
return useInitialState(
"useProject:" + user + ":" + project,
() => useInternalApi<HangarProject>("projects/project/" + user + "/" + project, false),
blocking
);
export async function useProject(user: string, project: string) {
return (await useAsyncData("useProject:" + user + ":" + project, () => useInternalApi<HangarProject>("projects/project/" + user + "/" + project, false)))
.data;
}
export async function useStargazers(user: string, project: string, blocking = true) {
return useInitialState(
"useStargazers:" + user + ":" + project,
() => useApi<PaginatedResult<User>>(`projects/${user}/${project}/stargazers`, false),
blocking
);
export async function useStargazers(user: string, project: string) {
return (await useAsyncData("useStargazers:" + user + ":" + project, () => useApi<PaginatedResult<User>>(`projects/${user}/${project}/stargazers`, false)))
.data;
}
export async function useWatchers(user: string, project: string, blocking = true) {
return useInitialState("useWatchers:" + user + ":" + project, () => useApi<PaginatedResult<User>>(`projects/${user}/${project}/watchers`, false), blocking);
export async function useWatchers(user: string, project: string) {
return (await useAsyncData("useWatchers:" + user + ":" + project, () => useApi<PaginatedResult<User>>(`projects/${user}/${project}/watchers`, false))).data;
}
export async function useStaff(blocking = true) {
return useInitialState("useStaff", () => useApi<PaginatedResult<User>>("staff", false), blocking);
export async function useStaff() {
return (await useAsyncData("useStaff", () => useApi<PaginatedResult<User>>("staff", false))).data;
}
export async function useAuthors(blocking = true) {
return useInitialState("useAuthors", () => useApi<PaginatedResult<User>>("authors", false), blocking);
export async function useAuthors() {
return (await useAsyncData("useAuthors", () => useApi<PaginatedResult<User>>("authors", false))).data;
}
export async function useInvites(blocking = true) {
return useInitialState("useInvites", () => useInternalApi<Invites>("invites", false), blocking);
export async function useInvites() {
return (await useAsyncData("useInvites", () => useInternalApi<Invites>("invites", false))).data;
}
export async function useNotifications(blocking = true) {
return useInitialState("useNotifications", () => useInternalApi<PaginatedResult<HangarNotification>>("notifications", false), blocking);
export async function useNotifications() {
return (await useAsyncData("useNotifications", () => useInternalApi<PaginatedResult<HangarNotification>>("notifications", false))).data;
}
export async function useUnreadNotifications(blocking = true) {
return useInitialState("useUnreadNotifications", () => useInternalApi<PaginatedResult<HangarNotification>>("unreadnotifications", false), blocking);
export async function useUnreadNotifications() {
return (await useAsyncData("useUnreadNotifications", () => useInternalApi<PaginatedResult<HangarNotification>>("unreadnotifications", false))).data;
}
export async function useReadNotifications(blocking = true) {
return useInitialState("useReadNotifications", () => useInternalApi<PaginatedResult<HangarNotification>>("readnotifications", false), blocking);
export async function useReadNotifications() {
return (await useAsyncData("useReadNotifications", () => useInternalApi<PaginatedResult<HangarNotification>>("readnotifications", false))).data;
}
export async function useRecentNotifications(blocking = true, amount: number) {
return useInitialState(
"useRecentNotifications:" + amount,
() => useInternalApi<HangarNotification[]>("recentnotifications?amount=" + amount, false),
blocking
);
export async function useRecentNotifications(amount: number) {
return (await useAsyncData("useRecentNotifications:" + amount, () => useInternalApi<HangarNotification[]>("recentnotifications?amount=" + amount, false)))
.data;
}
export async function useUnreadNotificationsCount(blocking = true) {
return useInitialState("useUnreadNotificationsCount", () => useInternalApi<number>("unreadcount", false), blocking);
export async function useUnreadNotificationsCount() {
return (await useAsyncData("useUnreadNotificationsCount", () => useInternalApi<number>("unreadcount", false))).data;
}
export async function useResolvedFlags(blocking = true) {
return useInitialState("useResolvedFlags", () => useInternalApi<PaginatedResult<Flag>>("flags/resolved", false), blocking);
export async function useResolvedFlags() {
return (await useAsyncData("useResolvedFlags", () => useInternalApi<PaginatedResult<Flag>>("flags/resolved", false))).data;
}
export async function useUnresolvedFlags(blocking = true) {
return useInitialState("useUnresolvedFlags", () => useInternalApi<PaginatedResult<Flag>>("flags/unresolved", false), blocking);
export async function useUnresolvedFlags() {
return (await useAsyncData("useUnresolvedFlags", () => useInternalApi<PaginatedResult<Flag>>("flags/unresolved", false))).data;
}
export async function useProjectFlags(projectId: number, blocking = true) {
return useInitialState("useProjectFlags:" + projectId, () => useInternalApi<Flag[]>("flags/" + projectId, false), blocking);
export async function useProjectFlags(projectId: number) {
return (await useAsyncData("useProjectFlags:" + projectId, () => useInternalApi<Flag[]>("flags/" + projectId, false))).data;
}
export async function useProjectNotes(projectId: number, blocking = true) {
return useInitialState("useProjectNotes:" + projectId, () => useInternalApi<Note[]>("projects/notes/" + projectId, false), blocking);
export async function useProjectNotes(projectId: number) {
return (await useAsyncData("useProjectNotes:" + projectId, () => useInternalApi<Note[]>("projects/notes/" + projectId, false))).data;
}
export async function useProjectChannels(user: string, project: string, blocking = true) {
return useInitialState("useProjectChannels:" + user + ":" + project, () => useInternalApi<ProjectChannel[]>(`channels/${user}/${project}`, false), blocking);
export async function useProjectChannels(user: string, project: string) {
return (await useAsyncData("useProjectChannels:" + user + ":" + project, () => useInternalApi<ProjectChannel[]>(`channels/${user}/${project}`, false))).data;
}
export async function useProjectVersions(user: string, project: string, blocking = true) {
return useInitialState(
"useProjectVersions:" + user + ":" + project,
() => useApi<PaginatedResult<Version>>(`projects/${user}/${project}/versions`, false),
blocking
);
export async function useProjectVersions(user: string, project: string) {
return (
await useAsyncData("useProjectVersions:" + user + ":" + project, () => useApi<PaginatedResult<Version>>(`projects/${user}/${project}/versions`, false))
).data;
}
export async function useProjectVersionsInternal(user: string, project: string, version: string, blocking = true) {
return useInitialState(
"useProjectVersionsInternal:" + user + ":" + project + ":" + version,
() => useInternalApi<HangarVersion>(`versions/version/${user}/${project}/versions/${version}`, false),
blocking
);
export async function useProjectVersionsInternal(user: string, project: string, version: string) {
return (
await useAsyncData("useProjectVersionsInternal:" + user + ":" + project + ":" + version, () =>
useInternalApi<HangarVersion>(`versions/version/${user}/${project}/versions/${version}`, false)
)
).data;
}
export async function usePage(user: string, project: string, path?: string, blocking = true) {
return useInitialState(
"usePage:" + user + ":" + project + ":" + path,
() => useInternalApi<ProjectPage>(`pages/page/${user}/${project}` + (path ? "/" + path : ""), false),
blocking
);
export async function usePage(user: string, project: string, path?: string) {
return (
await useAsyncData("usePage:" + user + ":" + project + ":" + path, () =>
useInternalApi<ProjectPage>(`pages/page/${user}/${project}` + (path ? "/" + path : ""), false)
)
).data;
}
export async function useHealthReport(blocking = true) {
return useInitialState("useHealthReport", () => useInternalApi<HealthReport>("admin/health", false), blocking);
export async function useHealthReport() {
return (await useAsyncData("useHealthReport", () => useInternalApi<HealthReport>("admin/health", false))).data;
}
export async function useActionLogs(blocking = true) {
return useInitialState("useActionLogs", () => useInternalApi<PaginatedResult<LoggedAction>>("admin/log/", false), blocking);
export async function useActionLogs() {
return (await useAsyncData("useActionLogs", () => useInternalApi<PaginatedResult<LoggedAction>>("admin/log/", false))).data;
}
export async function useVersionApprovals(blocking = true) {
return useInitialState(
"useVersionApprovals",
() => useInternalApi<{ underReview: ReviewQueueEntry[]; notStarted: ReviewQueueEntry[] }>("admin/approval/versions", false),
blocking
);
export async function useVersionApprovals() {
return (
await useAsyncData("useVersionApprovals", () =>
useInternalApi<{ underReview: ReviewQueueEntry[]; notStarted: ReviewQueueEntry[] }>("admin/approval/versions", false)
)
).data;
}
export async function usePossibleOwners(blocking = true) {
return useInitialState("usePossibleOwners", () => useInternalApi<ProjectOwner[]>("projects/possibleOwners"), blocking);
export async function usePossibleOwners() {
return (await useAsyncData("usePossibleOwners", () => useInternalApi<ProjectOwner[]>("projects/possibleOwners"))).data;
}
export async function useOrgVisibility(user: string, blocking = true) {
return useInitialState(
"useOrgVisibility:" + user,
() => useInternalApi<{ [key: string]: boolean }>(`organizations/${user}/userOrganizationsVisibility`, true),
blocking
);
export async function useOrgVisibility(user: string) {
return (
await useAsyncData("useOrgVisibility:" + user, () => useInternalApi<{ [key: string]: boolean }>(`organizations/${user}/userOrganizationsVisibility`, true))
).data;
}
export async function useUserData(user: string, blocking = true) {
return useInitialState(
"useUserData:" + user,
async () => {
export async function useUserData(user: string) {
return (
await useAsyncData("useUserData:" + user, async () => {
// noinspection ES6MissingAwait
const data = await Promise.all([
useApi<PaginatedResult<ProjectCompact>>(`users/${user}/starred`, false),
@ -171,7 +157,7 @@ export async function useUserData(user: string, blocking = true) {
}),
useInternalApi<{ [key: string]: RoleTable }>(`organizations/${user}/userOrganizations`, false),
useApi<ProjectCompact[]>(`users/${user}/pinned`, false),
] as Promise<any>[]);
]);
return {
starred: data[0] as PaginatedResult<ProjectCompact>,
watching: data[1] as PaginatedResult<ProjectCompact>,
@ -179,7 +165,6 @@ export async function useUserData(user: string, blocking = true) {
organizations: data[3] as { [key: string]: RoleTable },
pinned: data[4] as ProjectCompact[],
};
},
blocking
);
})
).data;
}

View File

@ -1,16 +1,14 @@
import { HangarUser } from "hangar-internal";
import { AxiosError, AxiosRequestHeaders } from "axios";
import Cookies from "universal-cookie";
import jwtDecode, { type JwtPayload } from "jwt-decode";
import { useAuthStore } from "~/store/auth";
import { useCookies } from "~/composables/useCookies";
import { useInternalApi } from "~/composables/useApi";
import { useAxios } from "~/composables/useAxios";
import { authLog } from "~/lib/composables/useLog";
import { HangarUser } from "hangar-internal";
import * as domain from "~/composables/useDomain";
import { Pinia } from "pinia";
import { AxiosError, AxiosRequestHeaders } from "axios";
import { useResponse } from "~/composables/useResReq";
import Cookies from "universal-cookie";
import jwtDecode, { JwtPayload } from "jwt-decode";
import { useConfig } from "~/lib/composables/useConfig";
import { useRequestEvent } from "#imports";
class Auth {
loginUrl(redirectUrl: string): string {
@ -20,7 +18,7 @@ class Auth {
return `/login?returnUrl=${useConfig().publicHost}${redirectUrl}`;
}
async logout() {
logout() {
location.replace(`/logout?returnUrl=${useConfig().publicHost}?loggedOut`);
}
@ -36,6 +34,7 @@ class Auth {
}
// TODO do we need to scope this to the user?
// TODO do we even need this anymore?
refreshPromise: Promise<boolean | string> | null = null;
async refreshToken() {
@ -70,7 +69,7 @@ class Auth {
resolve(false);
} else if (import.meta.env.SSR) {
if (response.headers["set-cookie"]) {
useResponse()?.setHeader("set-cookie", response.headers["set-cookie"]);
useRequestEvent().node.res?.setHeader("set-cookie", response.headers["set-cookie"]);
const token = new Cookies(response.headers["set-cookie"]?.join("; ")).get("HangarAuth");
if (token) {
authLog("got token");
@ -103,7 +102,7 @@ class Auth {
}
async invalidate() {
const store = useAuthStore(this.usePiniaIfPresent());
const store = useAuthStore();
store.$patch({
user: null,
authenticated: false,
@ -120,13 +119,13 @@ class Auth {
}
async updateUser(): Promise<void> {
const authStore = useAuthStore(this.usePiniaIfPresent());
const authStore = useAuthStore();
if (authStore.invalidated) {
authLog("no point in updating if we just invalidated");
return;
}
const user = await useInternalApi<HangarUser>("users/@me", true).catch(async (err) => {
authLog("no user");
const user = await useInternalApi<HangarUser>("users/@me", true).catch((err) => {
authLog("no user", Object.assign({}, err));
return this.invalidate();
});
if (user) {
@ -136,10 +135,6 @@ class Auth {
authLog("user is now " + authStore.user?.name);
}
}
usePiniaIfPresent() {
return import.meta.env.SSR ? domain.get<Pinia>("pinia") : null;
}
}
export const useAuth = new Auth();

View File

@ -1,11 +1,12 @@
import { createCookies, useCookies as cookies } from "@vueuse/integrations/useCookies";
import { useRequest, useResponse } from "~/composables/useResReq";
import * as cookie from "cookie";
import { useRequestEvent } from "#imports";
export const useCookies = () => {
if (import.meta.env.SSR) {
const req = useRequest();
const res = useResponse();
const event = useRequestEvent();
const req = event.node.req;
const res = event.node.res;
if (!req || !req.headers) {
console.error("req null?!");
console.trace();

View File

@ -1,52 +0,0 @@
import * as domain from "domain";
import { Context } from "vite-ssr/vue";
import { domainLog } from "~/lib/composables/useLog";
export function create(request: Context["request"], response: Context["response"]) {
if (!import.meta.env.SSR) return null;
domainLog("enter");
const d = domain.create();
d.add(request);
d.add(response);
d.on("error", (err) => {
domainLog("domain error!", err);
});
d.context = {};
d.enter();
set("req", request);
set("res", response);
return d;
}
export function exit(d: domain.Domain | null) {
if (!import.meta.env.SSR || !d) return;
d.context = null;
d.exit();
domainLog("exit");
}
export function set(key: string, value: unknown) {
if (!import.meta.env.SSR) return;
if (!domain.active) {
throw new Error("no active domain found to set key " + key);
}
if (!domain.active.context) {
throw new Error("no context found on domain to set key " + key);
}
domain.active.context[key] = value;
}
export function get<T>(key: string): T | null {
if (!import.meta.env.SSR) return null;
if (!domain.active) {
throw new Error("no active domain found to get key " + key);
}
if (!domain.active.context) {
throw new Error("no context found on domain to get key " + key);
}
return domain.active.context[key];
}
export function isActive() {
return import.meta.env.SSR && domain.active && domain.active.context;
}

View File

@ -1,20 +1,12 @@
import { AxiosError } from "axios";
import { HangarApiException, HangarValidationException, MultiHangarApiException } from "hangar-api";
import { Composer, UseI18nOptions } from "vue-i18n";
import { Context } from "vite-ssr/vue";
import { useNotificationStore } from "~/lib/store/notification";
import { useI18n } from "vue-i18n";
import { ref } from "vue";
import { useNotificationStore } from "~/lib/store/notification";
type I18nType = Composer<
NonNullable<UseI18nOptions["messages"]>,
NonNullable<UseI18nOptions["datetimeFormats"]>,
NonNullable<UseI18nOptions["numberFormats"]>,
NonNullable<UseI18nOptions["locale"]>
>;
export function handleRequestError(err: AxiosError, { writeResponse }: Context, i18n: I18nType, msg: string | undefined = undefined) {
export function handleRequestError(err: AxiosError, i18n: ReturnType<typeof useI18n>, msg: string | undefined = undefined) {
if (import.meta.env.SSR) {
_handleRequestError(err, writeResponse, i18n);
_handleRequestError(err, i18n);
return ref();
}
const notfication = useNotificationStore();
@ -44,7 +36,11 @@ export function handleRequestError(err: AxiosError, { writeResponse }: Context,
return ref();
}
function _handleRequestError(err: AxiosError, writeResponse: Context["writeResponse"], i18n: I18nType) {
function _handleRequestError(err: AxiosError, i18n: ReturnType<typeof useI18n>) {
function writeResponse(object: unknown) {
console.log("writeResponse", object);
// throw new Error("TODO: Implement me"); // TODO
}
if (!err.isAxiosError) {
// everything should be an AxiosError
writeResponse({
@ -79,7 +75,7 @@ function _handleRequestError(err: AxiosError, writeResponse: Context["writeRespo
}
}
function collectErrors(exception: HangarApiException | MultiHangarApiException, i18n: Context["app"]["i18n"]): string[] {
function collectErrors(exception: HangarApiException | MultiHangarApiException, i18n: ReturnType<typeof useI18n>): string[] {
if (!exception.isMultiException) {
return [i18n.te(exception.message) ? i18n.t(exception.message, [exception.messageArgs]) : exception.message];
} else {

View File

@ -12,7 +12,7 @@ export const validProjectName = withOverrideMessage((ownerId: () => string) =>
try {
await useInternalApi("projects/validateName", false, "get", {
userId: ownerId(),
value: value,
value,
});
return { $valid: true };
} catch (e: any) {
@ -71,9 +71,9 @@ export const validChannelName = withOverrideMessage((projectId: string, existing
}
try {
await useInternalApi("channels/checkName", true, "get", {
projectId: projectId,
projectId,
name: value,
existingName: existingName,
existingName,
});
return { $valid: true };
} catch (e: any) {
@ -92,9 +92,9 @@ export const validChannelColor = withOverrideMessage((projectId: string, existin
}
try {
await useInternalApi("channels/checkColor", true, "get", {
projectId: projectId,
projectId,
color: value,
existingColor: existingColor,
existingColor,
});
return { $valid: true };
} catch (e: any) {

View File

@ -1,49 +0,0 @@
import type { Ref } from "vue";
import { onDeactivated, onMounted, onUnmounted, ref } from "vue";
import { useContext } from "vite-ssr/vue";
import { initalStateLog } from "~/lib/composables/useLog";
export async function useInitialState<T>(key: string, handler: (type: "server" | "client") => Promise<T>, blocking = false): Promise<Ref<T | null>> {
const { initialState } = useContext();
const responseValue = ref(initialState[key] || null) as Ref<T | null>;
// remove data from initialState when component unmounts or deactivates
const removeState = () => {
if (!import.meta.env.SSR) {
initialState[key] = null;
}
};
onUnmounted(removeState);
onDeactivated(removeState);
if (import.meta.env.SSR) {
// if on server, block until data can be stored in initialState
initalStateLog("do request " + key);
const data = await handler("server");
initalStateLog("done " + key);
initialState[key] = data;
responseValue.value = data;
} else {
// if on client, check if we already have data and use that
if (initialState[key]) {
initalStateLog("found " + key);
responseValue.value = initialState[key];
} else {
// else do the request ourselves, blocking if needed
const fn = async () => {
responseValue.value = await handler("client");
initalStateLog("done " + key);
};
if (blocking) {
initalStateLog("block " + key);
await fn();
} else {
initalStateLog("onMounted " + key);
onMounted(fn);
}
}
}
return responseValue;
}

View File

@ -2,7 +2,7 @@ import { RouteLocationNormalizedLoaded } from "vue-router";
import { HangarProject } from "hangar-internal";
import { ref, watch } from "vue";
export async function useOpenProjectPages(route: RouteLocationNormalizedLoaded, project: HangarProject) {
export function useOpenProjectPages(route: RouteLocationNormalizedLoaded, project: HangarProject) {
const open = ref<string[]>([]);
watch(

View File

@ -1,23 +1,14 @@
import { ref } from "vue";
import { useInternalApi } from "~/composables/useApi";
import { handleRequestError } from "~/composables/useErrorHandling";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import { Context } from "vite-ssr/vue";
import { Composer, VueMessageType } from "vue-i18n";
import { useI18n } from "vue-i18n";
import { HangarProject } from "hangar-internal";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useInternalApi } from "~/composables/useApi";
import { usePage } from "~/composables/useApiHelper";
import { useErrorRedirect } from "~/lib/composables/useErrorRedirect";
export async function useProjectPage(
route: RouteLocationNormalizedLoaded,
router: Router,
ctx: Context,
i18n: Composer<unknown, unknown, unknown, VueMessageType>,
project: HangarProject
) {
const page = await usePage(route.params.user as string, route.params.project as string, route.params.all as string).catch((e) =>
handleRequestError(e, ctx, i18n)
);
export async function useProjectPage(route: RouteLocationNormalizedLoaded, router: Router, i18n: ReturnType<typeof useI18n>, project: HangarProject) {
const page = await usePage(route.params.user as string, route.params.project as string, route.params.all as string).catch((e) => handleRequestError(e, i18n));
if (!page) {
await router.replace(useErrorRedirect(route, 404, "Not found"));
}
@ -33,7 +24,7 @@ export async function useProjectPage(
if (!page) return;
await useInternalApi(`pages/save/${project.id}/${page.value?.id}`, true, "post", {
content,
}).catch((e) => handleRequestError(e, ctx, i18n, "page.new.error.save"));
}).catch((e) => handleRequestError(e, i18n, "page.new.error.save"));
if (page.value) {
page.value.contents = content;
}
@ -42,7 +33,7 @@ export async function useProjectPage(
async function deletePage() {
if (!page) return;
await useInternalApi(`pages/delete/${project.id}/${page.value?.id}`, true, "post").catch((e) => handleRequestError(e, ctx, i18n, "page.new.error.save"));
await useInternalApi(`pages/delete/${project.id}/${page.value?.id}`, true, "post").catch((e) => handleRequestError(e, i18n, "page.new.error.save"));
await router.replace(`/${route.params.user}/${route.params.project}`);
}

View File

@ -1,40 +0,0 @@
import { useContext } from "vite-ssr";
import * as domain from "~/composables/useDomain";
import { Context } from "vite-ssr/vue";
export const useRequest: () => Context["request"] | null = () => {
if (import.meta.env.SSR) {
if (domain.isActive()) {
return domain.get<Context["request"]>("req");
}
const ctx = useContext();
if (ctx) {
return ctx.request;
}
console.error("request null!");
console.trace();
return null;
}
console.error("useRequest called on client?!");
console.trace();
return null;
};
export const useResponse: () => Context["response"] | null = () => {
if (import.meta.env.SSR) {
if (domain.isActive()) {
return domain.get<Context["response"]>("res");
}
const ctx = useContext();
if (ctx) {
return ctx.response;
}
console.error("response null!");
console.trace();
return null;
}
console.error("useResponse called on client?!");
console.trace();
return null;
};

View File

@ -1,9 +0,0 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly HANGAR_CONFIG_ENV: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@ -1,10 +1,10 @@
<script setup lang="ts">
import { computed } from "vue";
import { useRoute } from "vue-router";
import Header from "~/components/layout/Header.vue";
import Footer from "~/components/layout/Footer.vue";
import Container from "~/lib/components/design/Container.vue";
import Notifications from "~/lib/components/design/Notifications.vue";
import { computed } from "vue";
import { useRoute } from "vue-router";
const route = useRoute();
const key = computed<string>(() => route.params.user as string);
@ -14,17 +14,7 @@ const key = computed<string>(() => route.params.user as string);
<main>
<Header />
<Container class="min-h-[80vh]">
<Suspense>
<router-view v-slot="{ Component }" v-bind="$attrs" :key="key">
<transition name="slide">
<!-- dummy diff to make the transition work on pages where template root has multiple elements -->
<div id="#page">
<component :is="Component" />
</div>
</transition>
</router-view>
<template #fallback> Loading... </template>
</Suspense>
<slot />
</Container>
<Notifications />
<Footer />

View File

@ -1,16 +1,60 @@
<template>
<main>
<div class="min-h-[60vh]">
<Suspense>
<router-view v-slot="{ Component }" v-bind="$attrs">
<transition name="slide">
<!-- dummy diff to make the transition work on pages where template root has multiple elements -->
<div id="#page">
<component :is="Component" />
</div>
</transition>
</router-view>
</Suspense>
</div>
<Header />
<Container class="min-h-[80vh]">
<Card max-width="400" class="mx-auto">
<h1>{{ error.statusCode }}</h1>
<p>{{ text }}</p>
<!-- todo nuxt button? -->
<Button nuxt to="/" color="secondary">
<IconMdiHome />
{{ t("general.home") }}
</Button>
</Card>
</Container>
<Notifications />
<Footer />
</main>
</template>
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import { type NuxtError } from "nuxt/app";
import { computed } from "#imports";
import Card from "~/lib/components/design/Card.vue";
import Button from "~/lib/components/design/Button.vue";
const props = defineProps<{
error: NuxtError;
}>();
const { t } = useI18n();
const text = computed(() => {
switch (props.error.statusCode) {
case 404:
return t("error.404");
case 401:
return t("error.401");
case 403:
return t("error.403");
default:
return t("error.unknown");
}
});
const head = computed(() => {
let title = t("error.unknown");
switch (props.error.statusCode) {
case 404:
title = t("error.404");
break;
case 401:
title = props.error.message!;
break;
}
return {
title,
};
});
</script>

View File

@ -1,25 +0,0 @@
<script setup lang="ts">
import Header from "~/components/layout/Header.vue";
import Footer from "~/components/layout/Footer.vue";
import Notifications from "~/lib/components/design/Notifications.vue";
</script>
<template>
<main>
<Header />
<div class="min-h-[60vh]">
<Suspense>
<router-view v-slot="{ Component }" v-bind="$attrs">
<transition name="slide">
<!-- dummy diff to make the transition work on pages where template root has multiple elements -->
<div id="#page">
<component :is="Component" />
</div>
</transition>
</router-view>
</Suspense>
</div>
<Notifications />
<Footer />
</main>
</template>

@ -1 +1 @@
Subproject commit 1eb7a8f6a223477b51e6b57ecec5094c185ab722
Subproject commit 5ceb57e631896689b945290997fbe58ec39940bf

View File

@ -1,88 +0,0 @@
import { createHead } from "@vueuse/head";
import { createPinia } from "pinia";
import { setupLayouts } from "virtual:generated-layouts";
import generatedRoutes from "virtual:generated-pages";
import viteSSR, { ClientOnly } from "vite-ssr";
import "windi.css";
import App from "~/App.vue";
import { installI18n } from "~/lib/i18n";
import "./lib/styles/main.css";
import { useBackendDataStore } from "~/store/backendData";
import devalue from "@nuxt/devalue";
import { settingsLog } from "~/lib/composables/useLog";
import * as domain from "~/composables/useDomain";
import "regenerator-runtime/runtime"; // popper needs this?
import { RouterScrollBehavior } from "vue-router";
const routes = setupLayouts(generatedRoutes);
// we need to override the path on the error route to have the patch math
const errorRoute = routes.find((r) => r.path === "/error");
if (errorRoute) {
errorRoute.path = "/:pathMatch(.*)*";
} else {
console.error("No error route?!");
}
const scrollBehavior: RouterScrollBehavior = (to, from, savedPosition) => {
if (savedPosition) {
return savedPosition;
} else if (to.hash) {
return { el: to.hash };
} else {
return { top: 0 };
}
};
const options: Parameters<typeof viteSSR>["1"] = {
routes,
pageProps: {
passToPage: false,
},
routerOptions: {
scrollBehavior,
},
transformState(state) {
return import.meta.env.SSR ? devalue(state) : state;
},
};
export default viteSSR(App, options, async (ctx) => {
const { app, initialState, initialRoute, request, response } = ctx;
app.component(ClientOnly.name, ClientOnly);
const d = domain.create(request, response);
const head = createHead();
const pinia = createPinia();
app.use(pinia).use(head);
domain.set("pinia", pinia);
// install all modules under `modules/`
for (const module of Object.values(import.meta.globEager("./modules/*.ts"))) {
await module.install?.(ctx);
}
if (!import.meta.env.SSR) {
pinia.state.value = initialState.pinia;
}
settingsLog("Load locale " + pinia.state.value.settings?.locale || "en");
// Load default language async to avoid bundling all languages
await installI18n(app, pinia.state.value.settings?.locale || "en");
// really don't need to do stuff for such meta routes
if (!initialRoute.fullPath.startsWith("/@vite")) {
await useBackendDataStore().initBackendData();
}
if (import.meta.env.SSR) {
initialState.pinia = pinia.state.value;
request.ctx = ctx;
request.pinia = pinia;
}
domain.exit(d);
return { head };
});

View File

@ -1,14 +0,0 @@
import NProgress from "nprogress";
import type { UserModule } from "~/types";
import { nextTick } from "vue";
export const install: UserModule = ({ isClient, router }) => {
if (isClient) {
router.beforeEach(() => {
NProgress.start();
});
router.afterEach(async () => {
await nextTick(() => NProgress.done());
});
}
};

View File

@ -1,20 +1,18 @@
<script lang="ts" setup>
import { useOrganization, useUser } from "~/composables/useApiHelper";
import { useRoute, useRouter } from "vue-router";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useContext } from "vite-ssr/vue";
import { useI18n } from "vue-i18n";
import { useOrganization, useUser } from "~/composables/useApiHelper";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useErrorRedirect } from "~/lib/composables/useErrorRedirect";
const ctx = useContext();
const i18n = useI18n();
const route = useRoute();
const user = await useUser(route.params.user as string).catch((e) => handleRequestError(e, ctx, i18n));
const user = await useUser(route.params.user as string).catch((e) => handleRequestError(e, i18n));
let organization = null;
if (!user || !user.value) {
await useRouter().replace(useErrorRedirect(useRoute(), 404, "Not found"));
} else if (user.value?.isOrganization) {
organization = await useOrganization(route.params.user as string).catch((e) => handleRequestError(e, ctx, i18n));
organization = await useOrganization(route.params.user as string).catch((e) => handleRequestError(e, i18n));
}
</script>

View File

@ -1,15 +1,14 @@
<script lang="ts" setup>
import { PropType, provide } from "vue";
import { type PropType, provide } from "vue";
import { User } from "hangar-api";
import { useContext } from "vite-ssr/vue";
import { useI18n } from "vue-i18n";
import { useProject } from "~/composables/useApiHelper";
import { useRoute, useRouter } from "vue-router";
import { HangarProjectPage } from "hangar-internal";
import { useProject } from "~/composables/useApiHelper";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useErrorRedirect } from "~/lib/composables/useErrorRedirect";
import ProjectHeader from "~/components/projects/ProjectHeader.vue";
import ProjectNav from "~/components/projects/ProjectNav.vue";
import { HangarProjectPage } from "hangar-internal";
defineProps({
user: {
@ -18,10 +17,9 @@ defineProps({
},
});
const ctx = useContext();
const i18n = useI18n();
const route = useRoute();
const project = await useProject(route.params.user as string, route.params.project as string).catch((e) => handleRequestError(e, ctx, i18n));
const project = await useProject(route.params.user as string, route.params.project as string).catch((e) => handleRequestError(e, i18n));
if (!project || !project.value) {
await useRouter().replace(useErrorRedirect(route, 404, "Not found"));
}

View File

@ -1,33 +1,36 @@
<script lang="ts" setup>
import Card from "~/lib/components/design/Card.vue";
import { User } from "hangar-api";
import { useI18n } from "vue-i18n";
import { HangarProject, ProjectChannel } from "hangar-internal";
import { useHead } from "@vueuse/head";
import { useRoute } from "vue-router";
import Card from "~/lib/components/design/Card.vue";
import { Header } from "~/components/SortableTable.vue";
import { ChannelFlag } from "~/types/enums";
import { useContext } from "vite-ssr/vue";
import { useProjectChannels } from "~/composables/useApiHelper";
import { handleRequestError } from "~/composables/useErrorHandling";
import { HangarProject, ProjectChannel } from "hangar-internal";
import { useInternalApi } from "~/composables/useApi";
import Table from "~/lib/components/design/Table.vue";
import Tag from "~/components/Tag.vue";
import Button from "~/lib/components/design/Button.vue";
import { useBackendDataStore } from "~/store/backendData";
import ChannelModal from "~/components/modals/ChannelModal.vue";
import { useHead } from "@vueuse/head";
import { useSeo } from "~/composables/useSeo";
import { projectIconUrl } from "~/composables/useUrlHelper";
import { useRoute } from "vue-router";
import { useNotificationStore } from "~/lib/store/notification";
import { definePageMeta } from "#imports";
definePageMeta({
projectPermsRequired: ["EDIT_CHANNELS"],
});
const props = defineProps<{
user: User;
project: HangarProject;
}>();
const i18n = useI18n();
const ctx = useContext();
const route = useRoute();
const channels = await useProjectChannels(props.project.namespace.owner, props.project.namespace.slug).catch((e) => handleRequestError(e, ctx, i18n));
const channels = await useProjectChannels(props.project.namespace.owner, props.project.namespace.slug).catch((e) => handleRequestError(e, i18n));
const validations = useBackendDataStore().validations;
const notifications = useNotificationStore();
@ -37,7 +40,7 @@ useHead(
async function refreshChannels() {
const newChannels = await useInternalApi<ProjectChannel[]>(`channels/${props.project.namespace.owner}/${props.project.namespace.slug}`, false).catch((e) =>
handleRequestError(e, ctx, i18n)
handleRequestError(e, i18n)
);
if (channels && newChannels) {
channels.value = newChannels;
@ -50,7 +53,7 @@ async function deleteChannel(channel: ProjectChannel) {
refreshChannels();
notifications.warn(i18n.t("channel.modal.success.deletedChannel", [channel.name]));
})
.catch((e) => handleRequestError(e, ctx, i18n));
.catch((e) => handleRequestError(e, i18n));
}
async function addChannel(channel: ProjectChannel) {
@ -63,7 +66,7 @@ async function addChannel(channel: ProjectChannel) {
refreshChannels();
notifications.success(i18n.t("channel.modal.success.addedChannel", [channel.name]));
})
.catch((e) => handleRequestError(e, ctx, i18n));
.catch((e) => handleRequestError(e, i18n));
}
async function editChannel(channel: ProjectChannel) {
@ -78,7 +81,7 @@ async function editChannel(channel: ProjectChannel) {
refreshChannels();
notifications.success(i18n.t("channel.modal.success.editedChannel", [channel.name]));
})
.catch((e) => handleRequestError(e, ctx, i18n));
.catch((e) => handleRequestError(e, i18n));
}
</script>
@ -134,8 +137,3 @@ async function editChannel(channel: ProjectChannel) {
</ChannelModal>
</Card>
</template>
<route lang="yaml">
meta:
requireProjectPerm: ["EDIT_CHANNELS"]
</route>

View File

@ -1,9 +1,9 @@
<script lang="ts" setup>
import { useHead } from "@vueuse/head";
import { useSeo } from "~/composables/useSeo";
import { projectIconUrl } from "~/composables/useUrlHelper";
import { useRoute } from "vue-router";
import { HangarProject } from "hangar-internal";
import { useSeo } from "~/composables/useSeo";
import { projectIconUrl } from "~/composables/useUrlHelper";
const route = useRoute();
const props = defineProps<{

View File

@ -1,27 +1,30 @@
<script lang="ts" setup>
import Card from "~/lib/components/design/Card.vue";
import Link from "~/lib/components/design/Link.vue";
import { User } from "hangar-api";
import { useI18n } from "vue-i18n";
import SortableTable, { Header } from "~/components/SortableTable.vue";
import Alert from "~/lib/components/design/Alert.vue";
import { useContext } from "vite-ssr/vue";
import { useProjectFlags } from "~/composables/useApiHelper";
import { handleRequestError } from "~/composables/useErrorHandling";
import { Flag, HangarProject } from "hangar-internal";
import { useHead } from "@vueuse/head";
import { useRoute } from "vue-router";
import Card from "~/lib/components/design/Card.vue";
import Link from "~/lib/components/design/Link.vue";
import SortableTable, { Header } from "~/components/SortableTable.vue";
import Alert from "~/lib/components/design/Alert.vue";
import { useProjectFlags } from "~/composables/useApiHelper";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useSeo } from "~/composables/useSeo";
import { projectIconUrl } from "~/composables/useUrlHelper";
import { useRoute } from "vue-router";
import { definePageMeta } from "#imports";
definePageMeta({
projectPermsRequired: ["MOD_NOTES_AND_FLAGS"],
});
const props = defineProps<{
user: User;
project: HangarProject;
}>();
const i18n = useI18n();
const ctx = useContext();
const route = useRoute();
const flags = await useProjectFlags(props.project.id).catch((e) => handleRequestError(e, ctx, i18n));
const flags = await useProjectFlags(props.project.id).catch((e) => handleRequestError(e, i18n));
const headers = [
{ title: "Submitter", name: "user" },
@ -65,8 +68,3 @@ useHead(useSeo("Flags | " + props.project.name, props.project.description, route
</SortableTable>
</Card>
</template>
<route lang="yaml">
meta:
requireGlobalPerm: ["MOD_NOTES_AND_FLAGS"]
</route>

View File

@ -1,18 +1,17 @@
<script lang="ts" setup>
import { User } from "hangar-api";
import Card from "~/lib/components/design/Card.vue";
import { useI18n } from "vue-i18n";
import ProjectInfo from "~/components/projects/ProjectInfo.vue";
import { HangarProject, PinnedVersion } from "hangar-internal";
import { useRoute, useRouter } from "vue-router";
import { ref } from "vue";
import Card from "~/lib/components/design/Card.vue";
import ProjectInfo from "~/components/projects/ProjectInfo.vue";
import MemberList from "~/components/projects/MemberList.vue";
import MarkdownEditor from "~/components/MarkdownEditor.vue";
import { hasPerms } from "~/composables/usePerm";
import { NamedPermission } from "~/types/enums";
import { useRoute, useRouter } from "vue-router";
import { useContext } from "vite-ssr/vue";
import Markdown from "~/components/Markdown.vue";
import ProjectPageList from "~/components/projects/ProjectPageList.vue";
import { ref } from "vue";
import { useInternalApi } from "~/composables/useApi";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useBackendDataStore } from "~/store/backendData";
@ -28,9 +27,7 @@ const props = defineProps<{
}>();
const i18n = useI18n();
const backendData = useBackendDataStore();
const ctx = useContext();
const route = useRoute();
const context = useContext();
const router = useRouter();
const openProjectPages = await useOpenProjectPages(route, props.project);
@ -44,7 +41,7 @@ function saveSponsors(content: string) {
sponsors.value = content;
editingSponsors.value = false;
})
.catch((e) => handleRequestError(e, ctx, i18n, "page.new.error.save"));
.catch((e) => handleRequestError(e, i18n, "page.new.error.save"));
}
function createPinnedVersionUrl(version: PinnedVersion): string {

View File

@ -1,31 +1,34 @@
<script lang="ts" setup>
import Card from "~/lib/components/design/Card.vue";
import Link from "~/lib/components/design/Link.vue";
import { User } from "hangar-api";
import { useI18n } from "vue-i18n";
import SortableTable, { Header } from "~/components/SortableTable.vue";
import Alert from "~/lib/components/design/Alert.vue";
import { useContext } from "vite-ssr/vue";
import { useProjectNotes } from "~/composables/useApiHelper";
import { handleRequestError } from "~/composables/useErrorHandling";
import { HangarProject, Note } from "hangar-internal";
import { ref } from "vue";
import { useHead } from "@vueuse/head";
import { useRoute } from "vue-router";
import Card from "~/lib/components/design/Card.vue";
import Link from "~/lib/components/design/Link.vue";
import SortableTable, { Header } from "~/components/SortableTable.vue";
import Alert from "~/lib/components/design/Alert.vue";
import { useProjectNotes } from "~/composables/useApiHelper";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useInternalApi } from "~/composables/useApi";
import InputText from "~/lib/components/ui/InputText.vue";
import Button from "~/lib/components/design/Button.vue";
import { useHead } from "@vueuse/head";
import { useSeo } from "~/composables/useSeo";
import { projectIconUrl } from "~/composables/useUrlHelper";
import { useRoute } from "vue-router";
import { definePageMeta } from "#imports";
definePageMeta({
projectPermsRequired: ["MOD_NOTES_AND_FLAGS"],
});
const props = defineProps<{
user: User;
project: HangarProject;
}>();
const i18n = useI18n();
const ctx = useContext();
const route = useRoute();
const notes = await useProjectNotes(props.project.id).catch((e) => handleRequestError(e, ctx, i18n));
const notes = await useProjectNotes(props.project.id).catch((e) => handleRequestError(e, i18n));
const text = ref("");
const loading = ref(false);
@ -44,9 +47,9 @@ async function addNote() {
loading.value = true;
await useInternalApi(`projects/notes/${props.project.id}`, true, "post", {
content: text.value,
}).catch((e) => handleRequestError(e, ctx, i18n));
}).catch((e) => handleRequestError(e, i18n));
text.value = "";
const newNotes = await useInternalApi<Note[]>("projects/notes/" + props.project.id, false).catch((e) => handleRequestError(e, ctx, i18n));
const newNotes = await useInternalApi<Note[]>("projects/notes/" + props.project.id, false).catch((e) => handleRequestError(e, i18n));
if (notes && newNotes) {
notes.value = newNotes;
}
@ -81,8 +84,3 @@ async function addNote() {
</SortableTable>
</Card>
</template>
<route lang="yaml">
meta:
requireGlobalPerm: ["MOD_NOTES_AND_FLAGS"]
</route>

View File

@ -1,6 +1,5 @@
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import { useContext } from "vite-ssr/vue";
import { useRoute, useRouter } from "vue-router";
import { User } from "hangar-api";
import { HangarProject } from "hangar-internal";
@ -19,7 +18,6 @@ const props = defineProps<{
}>();
const i18n = useI18n();
const ctx = useContext();
const route = useRoute();
const router = useRouter();

View File

@ -1,43 +1,46 @@
<script lang="ts" setup>
import { useHead } from "@vueuse/head";
import { useSeo } from "~/composables/useSeo";
import { projectIconUrl } from "~/composables/useUrlHelper";
import { useRoute, useRouter } from "vue-router";
import { HangarProject } from "hangar-internal";
import { useI18n } from "vue-i18n";
import { computed, onMounted, reactive, ref, watch } from "vue";
import { cloneDeep } from "lodash-es";
import { useVuelidate } from "@vuelidate/core";
import { Cropper, type CropperResult } from "vue-advanced-cropper";
import { PaginatedResult, User } from "hangar-api";
import { useSeo } from "~/composables/useSeo";
import { projectIconUrl } from "~/composables/useUrlHelper";
import Card from "~/lib/components/design/Card.vue";
import MemberList from "~/components/projects/MemberList.vue";
import { hasPerms } from "~/composables/usePerm";
import { NamedPermission, Visibility } from "~/types/enums";
import Button from "~/lib/components/design/Button.vue";
import Tabs from "~/lib/components/design/Tabs.vue";
import { computed, onMounted, reactive, ref, watch } from "vue";
import InputSelect from "~/lib/components/ui/InputSelect.vue";
import { useBackendDataStore } from "~/store/backendData";
import InputText from "~/lib/components/ui/InputText.vue";
import { cloneDeep } from "lodash-es";
import InputCheckbox from "~/lib/components/ui/InputCheckbox.vue";
import InputFile from "~/lib/components/ui/InputFile.vue";
import { useApi, useInternalApi } from "~/composables/useApi";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useContext } from "vite-ssr/vue";
import { useNotificationStore } from "~/lib/store/notification";
import InputTag from "~/lib/components/ui/InputTag.vue";
import TextAreaModal from "~/lib/components/modals/TextAreaModal.vue";
import ProjectSettingsSection from "~/components/projects/ProjectSettingsSection.vue";
import { maxLength, required, requiredIf, url } from "~/lib/composables/useValidationHelpers";
import { validProjectName } from "~/composables/useHangarValidations";
import { useVuelidate } from "@vuelidate/core";
import { Cropper, CropperResult } from "vue-advanced-cropper";
import "vue-advanced-cropper/dist/style.css";
import { PaginatedResult, User } from "hangar-api";
import InputAutocomplete from "~/lib/components/ui/InputAutocomplete.vue";
import { definePageMeta } from "#imports";
definePageMeta({
projectPermsRequired: ["EDIT_SUBJECT_SETTINGS"],
});
const route = useRoute();
const router = useRouter();
const i18n = useI18n();
const ctx = useContext();
const backendData = useBackendDataStore();
const v = useVuelidate();
const notificationStore = useNotificationStore();
@ -137,7 +140,7 @@ async function save() {
});
await router.go(0);
} catch (e: any) {
handleRequestError(e, ctx, i18n);
handleRequestError(e, i18n);
}
loading.save = false;
}
@ -150,7 +153,7 @@ async function transfer() {
});
notificationStore.success(i18n.t("project.settings.success.transferRequest", [search.value]));
} catch (e: any) {
handleRequestError(e, ctx, i18n);
handleRequestError(e, i18n);
}
loading.transfer = false;
}
@ -164,7 +167,7 @@ async function rename() {
notificationStore.success(i18n.t("project.settings.success.rename", [newName.value]));
await router.push("/" + route.params.user + "/" + newSlug);
} catch (e: any) {
handleRequestError(e, ctx, i18n);
handleRequestError(e, i18n);
}
loading.rename = false;
}
@ -181,7 +184,7 @@ async function softDelete(comment: string) {
await router.push("/");
}
} catch (e: any) {
handleRequestError(e, ctx, i18n);
handleRequestError(e, i18n);
}
}
@ -193,7 +196,7 @@ async function hardDelete(comment: string) {
notificationStore.success(i18n.t("project.settings.success.hardDelete"));
await router.push("/");
} catch (e: any) {
handleRequestError(e, ctx, i18n);
handleRequestError(e, i18n);
}
}
@ -215,7 +218,7 @@ async function uploadIcon() {
useNotificationStore().success(i18n.t("project.settings.success.changedIcon"));
}
} catch (e: any) {
handleRequestError(e, ctx, i18n);
handleRequestError(e, i18n);
}
loading.uploadIcon = false;
}
@ -232,7 +235,7 @@ async function resetIcon() {
projectIcon.value = null;
await loadIconIntoCropper();
} catch (e: any) {
handleRequestError(e, ctx, i18n);
handleRequestError(e, i18n);
}
loading.resetIcon = false;
}
@ -447,8 +450,3 @@ useHead(
/>
</div>
</template>
<route lang="yaml">
meta:
requireProjectPerm: ["EDIT_SUBJECT_SETTINGS"]
</route>

View File

@ -1,7 +1,8 @@
<script lang="ts" setup>
import { useRoute } from "vue-router";
import { useI18n } from "vue-i18n";
import { useContext } from "vite-ssr/vue";
import { useHead } from "@vueuse/head";
import { HangarProject } from "hangar-internal";
import { handleRequestError } from "~/composables/useErrorHandling";
import Card from "~/lib/components/design/Card.vue";
import PageTitle from "~/lib/components/design/PageTitle.vue";
@ -10,14 +11,11 @@ import { avatarUrl, projectIconUrl } from "~/composables/useUrlHelper";
import Alert from "~/lib/components/design/Alert.vue";
import { useStargazers } from "~/composables/useApiHelper";
import Link from "~/lib/components/design/Link.vue";
import { useHead } from "@vueuse/head";
import { useSeo } from "~/composables/useSeo";
import { HangarProject } from "hangar-internal";
const route = useRoute();
const i18n = useI18n();
const ctx = useContext();
const stargazers = await useStargazers(route.params.user as string, route.params.project as string).catch<any>((e) => handleRequestError(e, ctx, i18n));
const stargazers = await useStargazers(route.params.user as string, route.params.project as string).catch<any>((e) => handleRequestError(e, i18n));
const props = defineProps<{
project: HangarProject;

View File

@ -1,14 +1,12 @@
<script lang="ts" setup>
import { useProjectVersionsInternal } from "~/composables/useApiHelper";
import { useContext } from "vite-ssr/vue";
import { useI18n } from "vue-i18n";
import { useRoute, useRouter } from "vue-router";
import { handleRequestError } from "~/composables/useErrorHandling";
import { HangarProject } from "hangar-internal";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useProjectVersionsInternal } from "~/composables/useApiHelper";
import { useErrorRedirect } from "~/lib/composables/useErrorRedirect";
import { Platform } from "~/types/enums";
const ctx = useContext();
const i18n = useI18n();
const route = useRoute();
@ -17,7 +15,7 @@ const props = defineProps<{
}>();
const version = await useProjectVersionsInternal(route.params.user as string, route.params.project as string, route.params.version as string).catch((e) =>
handleRequestError(e, ctx, i18n)
handleRequestError(e, i18n)
);
if (!version || !version.value) {
await useRouter().replace(useErrorRedirect(route, 404, "Not found"));

View File

@ -1,15 +1,17 @@
<script lang="ts" setup>
import { NamedPermission, Platform, ReviewState, Visibility, PinnedStatus } from "~/types/enums";
import { HangarProject, HangarVersion, IPlatform } from "hangar-internal";
import { computed, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
import { useContext } from "vite-ssr/vue";
import { User } from "hangar-api";
import { useHead } from "@vueuse/head";
import { AxiosError } from "axios";
import { filesize } from "filesize";
import { NamedPermission, Platform, ReviewState, Visibility, PinnedStatus } from "~/types/enums";
import { useBackendDataStore } from "~/store/backendData";
import { lastUpdated } from "~/lib/composables/useTime";
import { useInternalApi } from "~/composables/useApi";
import { handleRequestError } from "~/composables/useErrorHandling";
import { User } from "hangar-api";
import { useErrorRedirect } from "~/lib/composables/useErrorRedirect";
import TagComponent from "~/components/Tag.vue";
import { hasPerms } from "~/composables/usePerm";
@ -18,24 +20,20 @@ import MarkdownEditor from "~/components/MarkdownEditor.vue";
import Markdown from "~/components/Markdown.vue";
import Card from "~/lib/components/design/Card.vue";
import Link from "~/lib/components/design/Link.vue";
import { useHead } from "@vueuse/head";
import { useSeo } from "~/composables/useSeo";
import { projectIconUrl } from "~/composables/useUrlHelper";
import { useNotificationStore } from "~/lib/store/notification";
import DropdownButton from "~/lib/components/design/DropdownButton.vue";
import DropdownItem from "~/lib/components/design/DropdownItem.vue";
import PlatformVersionEditModal from "~/components/modals/PlatformVersionEditModal.vue";
import { AxiosError } from "axios";
import Tooltip from "~/lib/components/design/Tooltip.vue";
import DownloadButton from "~/components/projects/DownloadButton.vue";
import PlatformLogo from "~/components/logos/platforms/PlatformLogo.vue";
import TextAreaModal from "~/lib/components/modals/TextAreaModal.vue";
import DependencyEditModal from "~/components/modals/DependencyEditModal.vue";
import { filesize } from "filesize";
const route = useRoute();
const i18n = useI18n();
const ctx = useContext();
const router = useRouter();
const backendData = useBackendDataStore();
const notification = useNotificationStore();
@ -102,7 +100,7 @@ async function savePage(content: string) {
}
editingPage.value = false;
} catch (err) {
handleRequestError(err as AxiosError, ctx, i18n, "page.new.error.save");
handleRequestError(err as AxiosError, i18n, "page.new.error.save");
}
}
@ -112,7 +110,7 @@ async function setPinned(value: boolean) {
notification.success(i18n.t(`version.page.pinned.request.${value}`));
router.go(0);
} catch (e) {
handleRequestError(e as AxiosError, ctx, i18n);
handleRequestError(e as AxiosError, i18n);
}
}
@ -124,7 +122,7 @@ async function deleteVersion(comment: string) {
notification.success(i18n.t("version.success.softDelete"));
await router.replace(`/${route.params.user}/${route.params.project}/versions`);
} catch (e) {
handleRequestError(e as AxiosError, ctx, i18n);
handleRequestError(e as AxiosError, i18n);
}
}
@ -141,7 +139,7 @@ async function hardDeleteVersion(comment: string) {
},
});
} catch (e) {
handleRequestError(e as AxiosError, ctx, i18n);
handleRequestError(e as AxiosError, i18n);
}
}
@ -151,7 +149,7 @@ async function restoreVersion() {
notification.success(i18n.t("version.success.restore"));
await router.replace(`/${route.params.user}/${route.params.project}/versions`);
} catch (e) {
handleRequestError(e as AxiosError, ctx, i18n);
handleRequestError(e as AxiosError, i18n);
}
}
</script>

View File

@ -1,34 +1,37 @@
<script lang="ts" setup>
import { useHead } from "@vueuse/head";
import { useSeo } from "~/composables/useSeo";
import { useRoute } from "vue-router";
import { Platform, ReviewAction, ReviewState } from "~/types/enums";
import { HangarProject, HangarReview, HangarReviewMessage, HangarVersion, IPlatform } from "hangar-internal";
import { useI18n } from "vue-i18n";
import { computed, reactive, ref } from "vue";
import { useVuelidate } from "@vuelidate/core";
import { useSeo } from "~/composables/useSeo";
import { Platform, ReviewAction, ReviewState } from "~/types/enums";
import { projectIconUrl } from "~/composables/useUrlHelper";
import Button from "~/lib/components/design/Button.vue";
import { useI18n } from "vue-i18n";
import InputCheckbox from "~/lib/components/ui/InputCheckbox.vue";
import InputTextarea from "~/lib/components/ui/InputTextarea.vue";
import Alert from "~/lib/components/design/Alert.vue";
import { computed, reactive, ref } from "vue";
import { useInternalApi } from "~/composables/useApi";
import { useAuthStore } from "~/store/auth";
import { prettyDate, prettyDateTime } from "~/lib/composables/useDate";
import { useBackendDataStore } from "~/store/backendData";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useContext } from "vite-ssr/vue";
import { useVuelidate } from "@vuelidate/core";
import Tag from "~/components/Tag.vue";
import Accordeon from "~/lib/components/design/Accordeon.vue";
import TextAreaModal from "~/lib/components/modals/TextAreaModal.vue";
import DownloadButton from "~/components/projects/DownloadButton.vue";
import { definePageMeta } from "#imports";
definePageMeta({
globalPermsRequired: ["REVIEWER"],
});
const route = useRoute();
const authStore = useAuthStore();
const backendDataStore = useBackendDataStore();
const i18n = useI18n();
const t = i18n.t;
const ctx = useContext();
const v = useVuelidate();
const props = defineProps<{
@ -315,7 +318,7 @@ function sendReviewRequest(
then();
refresh();
})
.catch((e) => handleRequestError(e, ctx, i18n))
.catch((e) => handleRequestError(e, i18n))
.finally(final);
}
@ -443,8 +446,3 @@ useHead(
</Alert>
</div>
</template>
<route lang="yaml">
meta:
requireGlobalPerm: ["REVIEWER"]
</route>

View File

@ -1,22 +1,21 @@
<script lang="ts" setup>
import Link from "~/lib/components/design/Link.vue";
import { useI18n } from "vue-i18n";
import { PaginatedResult, Version } from "hangar-api";
import { computed, reactive, watch } from "vue";
import { useRoute } from "vue-router";
import { HangarProject } from "hangar-internal";
import { useHead } from "@vueuse/head";
import Link from "~/lib/components/design/Link.vue";
import { hasPerms } from "~/composables/usePerm";
import { NamedPermission, Platform, Visibility } from "~/types/enums";
import Card from "~/lib/components/design/Card.vue";
import InputCheckbox from "~/lib/components/ui/InputCheckbox.vue";
import Tag from "~/components/Tag.vue";
import Button from "~/lib/components/design/Button.vue";
import { PaginatedResult, Version } from "hangar-api";
import { computed, reactive, watch } from "vue";
import { useBackendDataStore } from "~/store/backendData";
import { useProjectChannels, useProjectVersions } from "~/composables/useApiHelper";
import { useContext } from "vite-ssr/vue";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useRoute } from "vue-router";
import { useApi } from "~/composables/useApi";
import { HangarProject } from "hangar-internal";
import { useHead } from "@vueuse/head";
import { useSeo } from "~/composables/useSeo";
import { projectIconUrl } from "~/composables/useUrlHelper";
import Alert from "~/lib/components/design/Alert.vue";
@ -24,7 +23,6 @@ import Pagination from "~/lib/components/design/Pagination.vue";
import PlatformLogo from "~/components/logos/platforms/PlatformLogo.vue";
const i18n = useI18n();
const ctx = useContext();
const route = useRoute();
const backendData = useBackendDataStore();
@ -50,8 +48,8 @@ const requestOptions = computed(() => {
};
});
const channels = await useProjectChannels(route.params.user as string, route.params.project as string).catch((e) => handleRequestError(e, ctx, i18n));
const versions = await useProjectVersions(route.params.user as string, route.params.project as string).catch((e) => handleRequestError(e, ctx, i18n));
const channels = await useProjectChannels(route.params.user as string, route.params.project as string).catch((e) => handleRequestError(e, i18n));
const versions = await useProjectVersions(route.params.user as string, route.params.project as string).catch((e) => handleRequestError(e, i18n));
if (channels) {
filter.channels.push(...(channels.value?.map((c) => c.name) || []));
@ -77,7 +75,7 @@ watch(
false,
"get",
requestOptions.value
).catch((e) => handleRequestError(e, ctx, i18n));
).catch((e) => handleRequestError(e, i18n));
if (newVersions) {
versions.value = newVersions;
}

View File

@ -1,13 +1,15 @@
<script lang="ts" setup>
import { PluginDependency } from "hangar-api";
import { useHead } from "@vueuse/head";
import { useSeo } from "~/composables/useSeo";
import { projectIconUrl } from "~/composables/useUrlHelper";
import { HangarProject, IPlatform, PendingVersion, ProjectChannel } from "hangar-internal";
import { useRoute, useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
import { computed, reactive, type Ref, ref } from "vue";
import { remove } from "lodash-es";
import { type ValidationRule } from "@vuelidate/core";
import { useSeo } from "~/composables/useSeo";
import { projectIconUrl } from "~/composables/useUrlHelper";
import Steps, { Step } from "~/lib/components/design/Steps.vue";
import { computed, reactive, Ref, ref } from "vue";
import InputFile from "~/lib/components/ui/InputFile.vue";
import InputText from "~/lib/components/ui/InputText.vue";
import InputSelect from "~/lib/components/ui/InputSelect.vue";
@ -18,21 +20,22 @@ import { required, url as validUrl } from "~/lib/composables/useValidationHelper
import { useInternalApi } from "~/composables/useApi";
import { Platform } from "~/types/enums";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useContext } from "vite-ssr/vue";
import { formatSize } from "~/lib/composables/useFile";
import ChannelModal from "~/components/modals/ChannelModal.vue";
import { remove } from "lodash-es";
import { useBackendDataStore } from "~/store/backendData";
import DependencyTable from "~/components/projects/DependencyTable.vue";
import InputTag from "~/lib/components/ui/InputTag.vue";
import Tabs, { Tab } from "~/lib/components/design/Tabs.vue";
import PlatformLogo from "~/components/logos/platforms/PlatformLogo.vue";
import { useProjectChannels } from "~/composables/useApiHelper";
import { ValidationRule } from "@vuelidate/core";
import { definePageMeta } from "#imports";
definePageMeta({
projectPermsRequired: ["CREATE_VERSION"],
});
const route = useRoute();
const router = useRouter();
const ctx = useContext();
const i18n = useI18n();
const t = i18n.t;
const backendData = useBackendDataStore();
@ -45,7 +48,7 @@ const steps: Step[] = [
{
value: "artifact",
header: t("version.new.steps.1.header"),
beforeNext: async () => {
beforeNext: () => {
return createPendingVersion();
},
disableNext: computed(() => {
@ -211,7 +214,7 @@ async function createPendingVersion() {
);
pendingVersion.value = await useInternalApi<PendingVersion>(`versions/version/${props.project.id}/upload`, true, "post", formData).catch<any>((e) =>
handleRequestError(e, ctx, i18n)
handleRequestError(e, i18n)
);
loading.create = false;
@ -257,7 +260,7 @@ async function createVersion() {
await useInternalApi(`versions/version/${props.project.id}/create`, true, "post", pendingVersion.value);
await router.push(`/${route.params.user}/${route.params.project}/versions`);
} catch (e: any) {
handleRequestError(e, ctx, i18n);
handleRequestError(e, i18n);
} finally {
loading.submit = false;
}
@ -408,8 +411,3 @@ useHead(
</template>
</Steps>
</template>
<route lang="yaml">
meta:
requireProjectPerm: ["CREATE_VERSION"]
</route>

View File

@ -1,7 +1,8 @@
<script lang="ts" setup>
import { useRoute } from "vue-router";
import { useI18n } from "vue-i18n";
import { useContext } from "vite-ssr/vue";
import { useHead } from "@vueuse/head";
import { HangarProject } from "hangar-internal";
import { handleRequestError } from "~/composables/useErrorHandling";
import Card from "~/lib/components/design/Card.vue";
import PageTitle from "~/lib/components/design/PageTitle.vue";
@ -10,14 +11,11 @@ import { avatarUrl, projectIconUrl } from "~/composables/useUrlHelper";
import Alert from "~/lib/components/design/Alert.vue";
import { useWatchers } from "~/composables/useApiHelper";
import Link from "~/lib/components/design/Link.vue";
import { useHead } from "@vueuse/head";
import { useSeo } from "~/composables/useSeo";
import { HangarProject } from "hangar-internal";
const route = useRoute();
const i18n = useI18n();
const ctx = useContext();
const watchers = await useWatchers(route.params.user as string, route.params.project as string).catch<any>((e) => handleRequestError(e, ctx, i18n));
const watchers = await useWatchers(route.params.user as string, route.params.project as string).catch<any>((e) => handleRequestError(e, i18n));
const props = defineProps<{
project: HangarProject;

View File

@ -1,22 +1,21 @@
<script setup lang="ts">
import { User } from "hangar-api";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { useHead } from "@vueuse/head";
import { Organization } from "hangar-internal";
import { computed, type FunctionalComponent } from "vue";
import ProjectList from "~/components/projects/ProjectList.vue";
import Card from "~/lib/components/design/Card.vue";
import { useI18n } from "vue-i18n";
import { avatarUrl } from "~/composables/useUrlHelper";
import UserAvatar from "~/components/UserAvatar.vue";
import Link from "~/lib/components/design/Link.vue";
import MemberList from "~/components/projects/MemberList.vue";
import { useContext } from "vite-ssr/vue";
import { useOrgVisibility, useUserData } from "~/composables/useApiHelper";
import { useBackendDataStore } from "~/store/backendData";
import { useAuthStore } from "~/store/auth";
import { useRoute } from "vue-router";
import { useSeo } from "~/composables/useSeo";
import { useHead } from "@vueuse/head";
import { Organization } from "hangar-internal";
import UserHeader from "~/components/UserHeader.vue";
import { computed, FunctionalComponent } from "vue";
import { hasPerms } from "~/composables/usePerm";
import { NamedPermission } from "~/types/enums";
import Button from "~/lib/components/design/Button.vue";
@ -36,7 +35,6 @@ const props = defineProps<{
organization: Organization;
}>();
const i18n = useI18n();
const ctx = useContext();
const route = useRoute();
const { starred, watching, projects, organizations, pinned } = (await useUserData(props.user.name)).value || {};
@ -80,99 +78,104 @@ useHead(useSeo(props.user.name, props.user.name + " is an author on Hangar. " +
</script>
<template>
<UserHeader :user="user" :organization="organization" />
<div class="flex-basis-full flex flex-col gap-2 flex-grow md:max-w-2/3 md:min-w-1/3">
<div v-for="project in pinned" :key="project.namespace">
<ProjectCard :project="project"></ProjectCard>
</div>
</div>
<div class="flex gap-4 flex-basis-full flex-col md:flex-row">
<div>
<UserHeader :user="user" :organization="organization" />
<div class="flex-basis-full flex flex-col gap-2 flex-grow md:max-w-2/3 md:min-w-1/3">
<ProjectList :projects="projects"></ProjectList>
<div v-for="project in pinned" :key="project.namespace">
<ProjectCard :project="project"></ProjectCard>
</div>
</div>
<div class="flex-basis-full flex-grow md:max-w-1/3 md:min-w-1/3">
<Card v-if="buttons.length !== 0" class="mb-4 border-solid border-top-4 border-top-red-500 dark:border-top-red-500">
<template #header>{{ i18n.t("author.management") }}</template>
<template v-if="organization && hasPerms(NamedPermission.IS_SUBJECT_OWNER)">
<Tooltip :content="i18n.t('author.tooltips.transfer')">
<OrgTransferModal :organization="user.name" />
</Tooltip>
<Tooltip :content="i18n.t('author.tooltips.delete')">
<OrgDeleteModal :organization="user.name" />
<div class="flex gap-4 flex-basis-full flex-col md:flex-row">
<div class="flex-basis-full flex flex-col gap-2 flex-grow md:max-w-2/3 md:min-w-1/3">
<ProjectList :projects="projects"></ProjectList>
</div>
<div class="flex-basis-full flex-grow md:max-w-1/3 md:min-w-1/3">
<Card v-if="buttons.length !== 0" class="mb-4 border-solid border-top-4 border-top-red-500 dark:border-top-red-500">
<template #header>{{ i18n.t("author.management") }}</template>
<template v-if="organization && hasPerms(NamedPermission.IS_SUBJECT_OWNER)">
<Tooltip :content="i18n.t('author.tooltips.transfer')">
<OrgTransferModal :organization="user.name" />
</Tooltip>
<Tooltip :content="i18n.t('author.tooltips.delete')">
<OrgDeleteModal :organization="user.name" />
</Tooltip>
</template>
<Tooltip v-for="btn in buttons" :key="btn.name">
<template #content>
{{ i18n.t(`author.tooltips.${btn.name}`) }}
</template>
<Link v-bind="btn.attr">
<Button size="small" class="mr-1 inline-flex"><component :is="btn.icon" /></Button>
</Link>
</Tooltip>
<LockUserModal v-if="!isCurrentUser && !user.isOrganization && hasPerms(NamedPermission.IS_STAFF)" :user="user" />
</Card>
<template v-if="!user.isOrganization">
<Card class="mb-4" accent>
<template #header>
<div class="inline-flex w-full">
<span class="flex-grow">{{ i18n.t("author.orgs") }}</span>
<OrgVisibilityModal
v-if="organizationVisibility && organizations && Object.keys(organizations).length !== 0"
v-model="organizationVisibility"
/>
</div>
</template>
<ul>
<li v-for="(orgRole, orgName) in organizations" :key="orgName">
<router-link :to="'/' + orgName" class="flex items-center mb-2">
<UserAvatar :username="orgName" :avatar-url="avatarUrl(orgName)" size="xs" :disable-link="true" class="flex-shrink-0 mr-2" />
{{ orgName }} ({{ orgRole.role.title }})
<span class="flex-grow"></span>
<IconMdiEyeOffOutline v-if="organizationVisibility && organizationVisibility[orgName]" class="ml-1" />
</router-link>
</li>
</ul>
<span v-if="!organizations || Object.keys(organizations).length === 0">
{{ i18n.t("author.noOrgs", [props.user.name]) }}
</span>
</Card>
<Card class="mb-4" accent>
<template #header>{{ i18n.t("author.stars") }}</template>
<ul>
<li v-for="star in starred?.result" :key="star.name">
<Link :to="'/' + star.namespace.owner + '/' + star.namespace.slug">
{{ star.namespace.owner }}/<strong>{{ star.name }}</strong>
</Link>
</li>
<span v-if="!starred || starred?.result?.length === 0">
{{ i18n.t("author.noStarred", [props.user.name]) }}
</span>
</ul>
</Card>
<Card accent>
<template #header>{{ i18n.t("author.watching") }}</template>
<ul>
<li v-for="watch in watching?.result" :key="watch.name">
<Link :to="'/' + watch.namespace.owner + '/' + watch.namespace.slug"
>{{ watch.namespace.owner }}/<strong>{{ watch.name }}</strong></Link
>
</li>
<span v-if="!watching || watching?.result?.length === 0">
{{ i18n.t("author.noWatching", [props.user.name]) }}
</span>
</ul>
</Card>
</template>
<Tooltip v-for="btn in buttons" :key="btn.name">
<template #content>
{{ i18n.t(`author.tooltips.${btn.name}`) }}
</template>
<Link v-bind="btn.attr">
<Button size="small" class="mr-1 inline-flex"><component :is="btn.icon" /></Button>
</Link>
</Tooltip>
<LockUserModal v-if="!isCurrentUser && !user.isOrganization && hasPerms(NamedPermission.IS_STAFF)" :user="user" />
</Card>
<template v-if="!user.isOrganization">
<Card class="mb-4" accent>
<template #header>
<div class="inline-flex w-full">
<span class="flex-grow">{{ i18n.t("author.orgs") }}</span>
<OrgVisibilityModal v-if="organizationVisibility && organizations && Object.keys(organizations).length !== 0" v-model="organizationVisibility" />
</div>
</template>
<ul>
<li v-for="(orgRole, orgName) in organizations" :key="orgName">
<router-link :to="'/' + orgName" class="flex items-center mb-2">
<UserAvatar :username="orgName" :avatar-url="avatarUrl(orgName)" size="xs" :disable-link="true" class="flex-shrink-0 mr-2" />
{{ orgName }} ({{ orgRole.role.title }})
<span class="flex-grow"></span>
<IconMdiEyeOffOutline v-if="organizationVisibility && organizationVisibility[orgName]" class="ml-1" />
</router-link>
</li>
</ul>
<span v-if="!organizations || Object.keys(organizations).length === 0">
{{ i18n.t("author.noOrgs", [props.user.name]) }}
</span>
</Card>
<Card class="mb-4" accent>
<template #header>{{ i18n.t("author.stars") }}</template>
<ul>
<li v-for="star in starred.result" :key="star.name">
<Link :to="'/' + star.namespace.owner + '/' + star.namespace.slug">
{{ star.namespace.owner }}/<strong>{{ star.name }}</strong>
</Link>
</li>
<span v-if="!starred || starred.result.length === 0">
{{ i18n.t("author.noStarred", [props.user.name]) }}
</span>
</ul>
</Card>
<Card accent>
<template #header>{{ i18n.t("author.watching") }}</template>
<ul>
<li v-for="watch in watching.result" :key="watch.name">
<Link :to="'/' + watch.namespace.owner + '/' + watch.namespace.slug"
>{{ watch.namespace.owner }}/<strong>{{ watch.name }}</strong></Link
>
</li>
<span v-if="!watching || watching.result.length === 0">
{{ i18n.t("author.noWatching", [props.user.name]) }}
</span>
</ul>
</Card>
</template>
<MemberList v-else :members="organization.members" :roles="orgRoles" organization :author="user.name" :owner="organization.owner.userId" />
<MemberList v-else :members="organization.members" :roles="orgRoles" organization :author="user.name" :owner="organization.owner.userId" />
</div>
</div>
</div>
</template>

View File

@ -1,13 +1,14 @@
<script lang="ts" setup>
import PageTitle from "~/lib/components/design/PageTitle.vue";
import { useI18n } from "vue-i18n";
import InputText from "~/lib/components/ui/InputText.vue";
import Button from "~/lib/components/design/Button.vue";
import { reactive, ref } from "vue";
import { useContext } from "vite-ssr/vue";
import { useInternalApi } from "~/composables/useApi";
import { ApiKey, User } from "hangar-api";
import { useRoute } from "vue-router";
import { useHead } from "@vueuse/head";
import { useVuelidate } from "@vuelidate/core";
import PageTitle from "~/lib/components/design/PageTitle.vue";
import InputText from "~/lib/components/ui/InputText.vue";
import Button from "~/lib/components/design/Button.vue";
import { useInternalApi } from "~/composables/useApi";
import { handleRequestError } from "~/composables/useErrorHandling";
import InputCheckbox from "~/lib/components/ui/InputCheckbox.vue";
import Table from "~/lib/components/design/Table.vue";
@ -15,15 +16,18 @@ import Alert from "~/lib/components/design/Alert.vue";
import Card from "~/lib/components/design/Card.vue";
import { useSeo } from "~/composables/useSeo";
import { avatarUrl } from "~/composables/useUrlHelper";
import { useHead } from "@vueuse/head";
import { useNotificationStore } from "~/lib/store/notification";
import { maxLength, minLength, required } from "~/lib/composables/useValidationHelpers";
import { validApiKeyName } from "~/composables/useHangarValidations";
import { useVuelidate } from "@vuelidate/core";
import InputGroup from "~/lib/components/ui/InputGroup.vue";
import { NamedPermission } from "~/types/enums";
import { definePageMeta } from "#imports";
definePageMeta({
currentUserRequired: true,
globalPermsRequired: ["EDIT_API_KEYS"],
});
const ctx = useContext();
const i18n = useI18n();
const route = useRoute();
const notification = useNotificationStore();
@ -49,7 +53,7 @@ async function create() {
const key = await useInternalApi<string>(`api-keys/create-key/${route.params.user}`, true, "post", {
name: name.value,
permissions: selectedPerms.value,
}).catch((err) => handleRequestError(err, ctx, i18n));
}).catch((err) => handleRequestError(err, i18n));
if (key) {
apiKeys.value.unshift({
token: key,
@ -69,7 +73,7 @@ async function deleteKey(key: ApiKey) {
loadingDelete[key.name] = true;
await useInternalApi(`api-keys/delete-key/${route.params.user}`, true, "post", {
content: key.name,
}).catch((err) => handleRequestError(err, ctx, i18n));
}).catch((err) => handleRequestError(err, i18n));
apiKeys.value = apiKeys.value.filter((k) => k.name !== key.name);
notification.success(i18n.t("apiKeys.success.delete", [key.name]));
loadingDelete[key.name] = false;
@ -143,12 +147,6 @@ async function deleteKey(key: ApiKey) {
</div>
</template>
<route lang="yaml">
meta:
requireCurrentUser: true
requireGlobalPerm: ["EDIT_API_KEYS"]
</route>
<style lang="scss" scoped>
.autofix {
grid-template-columns: repeat(auto-fit, 250px);

View File

@ -2,5 +2,3 @@
<!-- only here to organize routes nicer -->
<router-view></router-view>
</template>
<route lang="yaml"></route>

View File

@ -1,23 +1,26 @@
<script lang="ts" setup>
import { useInternalApi } from "~/composables/useApi";
import { useRoute } from "vue-router";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useI18n } from "vue-i18n";
import { useContext } from "vite-ssr/vue";
import { FlagActivity, ReviewActivity } from "hangar-internal";
import { useHead } from "@vueuse/head";
import { useInternalApi } from "~/composables/useApi";
import { handleRequestError } from "~/composables/useErrorHandling";
import PageTitle from "~/lib/components/design/PageTitle.vue";
import Card from "~/lib/components/design/Card.vue";
import Table from "~/lib/components/design/Table.vue";
import Link from "~/lib/components/design/Link.vue";
import Alert from "~/lib/components/design/Alert.vue";
import { useHead } from "@vueuse/head";
import { useSeo } from "~/composables/useSeo";
import { definePageMeta } from "#imports";
definePageMeta({
globalPermsRequired: ["REVIEWER"],
});
const route = useRoute();
const i18n = useI18n();
const ctx = useContext();
const flagActivities = await useInternalApi<FlagActivity[]>(`admin/activity/${route.params.user}/flags`).catch((e) => handleRequestError(e, ctx, i18n));
const reviewActivities = await useInternalApi<ReviewActivity[]>(`admin/activity/${route.params.user}/reviews`).catch((e) => handleRequestError(e, ctx, i18n));
const flagActivities = await useInternalApi<FlagActivity[]>(`admin/activity/${route.params.user}/flags`).catch((e) => handleRequestError(e, i18n));
const reviewActivities = await useInternalApi<ReviewActivity[]>(`admin/activity/${route.params.user}/reviews`).catch((e) => handleRequestError(e, i18n));
useHead(useSeo(i18n.t("userActivity.title", [route.params.user]) + route.params.constructor, null, route, null));
@ -32,61 +35,58 @@ function getRouteParams(activity: ReviewActivity) {
</script>
<template>
<PageTitle>{{ i18n.t("userActivity.title", [route.params.user]) }}</PageTitle>
<div class="grid grid-cols-2 gap-4">
<Card>
<template #header>{{ i18n.t("userActivity.reviews") }}</template>
<div>
<PageTitle>{{ i18n.t("userActivity.title", [route.params.user]) }}</PageTitle>
<div class="grid grid-cols-2 gap-4">
<Card>
<template #header>{{ i18n.t("userActivity.reviews") }}</template>
<Table v-if="reviewActivities && reviewActivities.length">
<tbody>
<tr v-for="(activity, idx) in reviewActivities" :key="`review-${idx}`">
<td>{{ i18n.t("userActivity.reviewApproved") }}</td>
<td>{{ activity.endedAt ? i18n.d(activity.endedAt, "time") : "" }}</td>
<td>
{{ `${activity.namespace.owner}/${activity.namespace.slug}/${activity.versionString}: ${activity.platforms[0].toLowerCase()}` }}
</td>
<td>
<Link
:to="{
name: 'user-project-versions-version-platform-reviews',
params: getRouteParams(activity),
}"
>
<IconMdiListStatus class="float-left"></IconMdiListStatus>
{{ i18n.t("version.page.reviewLogs") }}
</Link>
</td>
</tr>
</tbody>
</Table>
<Alert v-else type="success">
{{ i18n.t("health.empty") }}
</Alert>
</Card>
<Card>
<template #header>{{ i18n.t("userActivity.flags") }}</template>
<Table v-if="reviewActivities && reviewActivities.length">
<tbody>
<tr v-for="(activity, idx) in reviewActivities" :key="`review-${idx}`">
<td>{{ i18n.t("userActivity.reviewApproved") }}</td>
<td>{{ activity.endedAt ? i18n.d(activity.endedAt, "time") : "" }}</td>
<td>
{{ `${activity.namespace.owner}/${activity.namespace.slug}/${activity.versionString}: ${activity.platforms[0].toLowerCase()}` }}
</td>
<td>
<Link
:to="{
name: 'user-project-versions-version-platform-reviews',
params: getRouteParams(activity),
}"
>
<IconMdiListStatus class="float-left"></IconMdiListStatus>
{{ i18n.t("version.page.reviewLogs") }}
</Link>
</td>
</tr>
</tbody>
</Table>
<Alert v-else type="success">
{{ i18n.t("health.empty") }}
</Alert>
</Card>
<Card>
<template #header>{{ i18n.t("userActivity.flags") }}</template>
<Table v-if="flagActivities && flagActivities.length">
<tbody>
<tr v-for="(activity, idx) in flagActivities" :key="`flag-${idx}`">
<td>{{ i18n.t("userActivity.flagResolved") }}</td>
<td>{{ activity.resolvedAt ? i18n.d(activity.resolvedAt, "time") : "" }}</td>
<td>
<Link :to="`/${activity.namespace.owner}/${activity.namespace.slug}`">
{{ `${activity.namespace.owner}/${activity.namespace.slug}` }}
</Link>
</td>
</tr>
</tbody>
</Table>
<Alert v-else type="success">
{{ i18n.t("health.empty") }}
</Alert>
</Card>
<Table v-if="flagActivities && flagActivities.length">
<tbody>
<tr v-for="(activity, idx) in flagActivities" :key="`flag-${idx}`">
<td>{{ i18n.t("userActivity.flagResolved") }}</td>
<td>{{ activity.resolvedAt ? i18n.d(activity.resolvedAt, "time") : "" }}</td>
<td>
<Link :to="`/${activity.namespace.owner}/${activity.namespace.slug}`">
{{ `${activity.namespace.owner}/${activity.namespace.slug}` }}
</Link>
</td>
</tr>
</tbody>
</Table>
<Alert v-else type="success">
{{ i18n.t("health.empty") }}
</Alert>
</Card>
</div>
</div>
</template>
<route lang="yaml">
meta:
requireGlobalPerm: ["REVIEWER"]
</route>

View File

@ -1,25 +1,28 @@
<script lang="ts" setup>
import { ProjectApproval } from "hangar-internal";
import { useI18n } from "vue-i18n";
import { useHead } from "@vueuse/head";
import { useRoute } from "vue-router";
import { useInternalApi } from "~/composables/useApi";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useContext } from "vite-ssr/vue";
import { useI18n } from "vue-i18n";
import Card from "~/lib/components/design/Card.vue";
import AdminProjectList from "~/components/projects/AdminProjectList.vue";
import { useHead } from "@vueuse/head";
import { useSeo } from "~/composables/useSeo";
import { useRoute } from "vue-router";
import { definePageMeta } from "#imports";
interface ApprovalProjects {
needsApproval: ProjectApproval[];
waitingProjects: ProjectApproval[];
}
const ctx = useContext();
definePageMeta({
globalPermsRequired: ["REVIEWER"],
});
const i18n = useI18n();
const route = useRoute();
const data: ApprovalProjects = (await useInternalApi<ApprovalProjects>("admin/approval/projects").catch((e) =>
handleRequestError(e, ctx, i18n)
handleRequestError(e, i18n)
)) as ApprovalProjects;
useHead(useSeo(i18n.t("projectApproval.title"), null, route, null));
@ -37,8 +40,3 @@ useHead(useSeo(i18n.t("projectApproval.title"), null, route, null));
</Card>
</div>
</template>
<route lang="yaml">
meta:
requireGlobalPerm: ["REVIEWER"]
</route>

View File

@ -1,23 +1,26 @@
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import SortableTable, { Header } from "~/components/SortableTable.vue";
import { Review, ReviewQueueEntry } from "hangar-internal";
import { useHead } from "@vueuse/head";
import { useRoute } from "vue-router";
import SortableTable, { Header } from "~/components/SortableTable.vue";
import { ReviewAction } from "~/types/enums";
import { useContext } from "vite-ssr/vue";
import { useVersionApprovals } from "~/composables/useApiHelper";
import { handleRequestError } from "~/composables/useErrorHandling";
import Card from "~/lib/components/design/Card.vue";
import Link from "~/lib/components/design/Link.vue";
import Tag from "~/components/Tag.vue";
import { useHead } from "@vueuse/head";
import { useSeo } from "~/composables/useSeo";
import { useRoute } from "vue-router";
import Button from "~/lib/components/design/Button.vue";
import { definePageMeta } from "#imports";
definePageMeta({
globalPermsRequired: ["REVIEWER"],
});
const i18n = useI18n();
const ctx = useContext();
const route = useRoute();
const data = await useVersionApprovals().catch((e) => handleRequestError(e, ctx, i18n));
const data = await useVersionApprovals().catch((e) => handleRequestError(e, i18n));
const actions = {
ongoing: [ReviewAction.START, ReviewAction.MESSAGE, ReviewAction.UNDO_APPROVAL, ReviewAction.REOPEN],
@ -88,99 +91,96 @@ function getCount(entry: ReviewQueueEntry, ..._actions: ReviewAction[]) {
</script>
<template>
<Card>
<template #header>{{ i18n.t("versionApproval.approvalQueue") }}</template>
<div>
<Card>
<template #header>{{ i18n.t("versionApproval.approvalQueue") }}</template>
<SortableTable v-if="data" :headers="notStartedHeaders" :items="data.notStarted">
<template #item_project="{ item }">
<Link :to="`/${item.namespace.owner}/${item.namespace.slug}`">
{{ `${item.namespace.owner}/${item.namespace.slug}` }}
</Link>
</template>
<template #item_date="{ item }">
<span class="start-date">{{ i18n.d(item.versionCreatedAt, "time") }}</span>
</template>
<template #item_version="{ item }">
<Link :to="{ name: 'user-project-versions-version-platform', params: getRouteParams(item) }">
<Tag :color="{ background: item.channelColor }" :name="item.channelName" :data="item.versionString" />
</Link>
</template>
<template #item_queuedBy="{ item }">
<Link :to="`/${item.versionAuthor}`">
{{ item.versionAuthor }}
</Link>
</template>
<template #item_startBtn="{ item }">
<Link :to="{ name: 'user-project-versions-version-platform-reviews', params: getRouteParams(item) }" nuxt>
<Button>
<IconMdiPlay />
{{ i18n.t("version.page.reviewStart") }}
</Button>
</Link>
</template>
</SortableTable>
</Card>
<SortableTable v-if="data" :headers="notStartedHeaders" :items="data.notStarted">
<template #item_project="{ item }">
<Link :to="`/${item.namespace.owner}/${item.namespace.slug}`">
{{ `${item.namespace.owner}/${item.namespace.slug}` }}
</Link>
</template>
<template #item_date="{ item }">
<span class="start-date">{{ i18n.d(item.versionCreatedAt, "time") }}</span>
</template>
<template #item_version="{ item }">
<Link :to="{ name: 'user-project-versions-version-platform', params: getRouteParams(item) }">
<Tag :color="{ background: item.channelColor }" :name="item.channelName" :data="item.versionString" />
</Link>
</template>
<template #item_queuedBy="{ item }">
<Link :to="`/${item.versionAuthor}`">
{{ item.versionAuthor }}
</Link>
</template>
<template #item_startBtn="{ item }">
<Link :to="{ name: 'user-project-versions-version-platform-reviews', params: getRouteParams(item) }" nuxt>
<Button>
<IconMdiPlay />
{{ i18n.t("version.page.reviewStart") }}
</Button>
</Link>
</template>
</SortableTable>
</Card>
<Card class="mt-4">
<template #header>{{ i18n.t("versionApproval.inReview") }}</template>
<Card class="mt-4">
<template #header>{{ i18n.t("versionApproval.inReview") }}</template>
<SortableTable v-if="data" :headers="underReviewHeaders" :items="data.underReview" expandable>
<template #item_project="{ item }">
<Link :to="`/${item.namespace.owner}/${item.namespace.slug}`">
{{ `${item.namespace.owner}/${item.namespace.slug}` }}
</Link>
</template>
<template #item_version="{ item }">
<Link :to="{ name: 'user-project-versions-version-platform', params: getRouteParams(item) }">
<Tag :color="{ background: item.channelColor }" :name="item.channelName" :data="item.versionString" />
</Link>
</template>
<template #item_queuedBy="{ item }">
<Link :to="`/${item.versionAuthor}`">
{{ item.versionAuthor }}
</Link>
<br />
<small>{{ i18n.d(item.versionCreatedAt, "time") }}</small>
</template>
<template #item_status="{ item }">
<span class="text-yellow-400">
{{ i18n.t("versionApproval.statuses.ongoing", [getOngoingCount(item)]) }}
</span>
<br />
<span class="text-red-400">
{{ i18n.t("versionApproval.statuses.stopped", [getStoppedCount(item)]) }}
</span>
<br />
<span class="text-green-400"> {{ i18n.t("versionApproval.statuses.approved", [getApprovedCount(item)]) }}</span>
</template>
<template #item_reviewLogs="{ item }">
<Link :to="{ name: 'user-project-versions-version-platform-reviews', params: getRouteParams(item) }">
<IconMdiListStatus />
{{ i18n.t("version.page.reviewLogs") }}
</Link>
</template>
<template #expanded-item="{ item, headers }">
<td :colspan="headers.length">
<ul>
<li v-for="entry in item.reviews" :key="entry.reviewerName" class="ml-4">
<span
class="font-bold mr-2"
:class="{ 'text-yellow-400': isOngoing(entry), 'text-red-400': isStopped(entry), 'text-green-400': isApproved(entry) }"
>{{ entry.reviewerName }}</span
>
<span>{{ i18n.t("versionApproval.started", [i18n.d(entry.reviewStarted, "time")]) }}</span>
<span v-if="entry.reviewEnded" class="ml-4" :class="{ 'text-red-400': isStopped(entry), 'text-green-400': isApproved(entry) }">{{
i18n.t("versionApproval.ended", [i18n.d(entry.reviewEnded, "time")])
}}</span>
</li>
</ul>
</td>
</template>
</SortableTable>
</Card>
<SortableTable v-if="data" :headers="underReviewHeaders" :items="data.underReview" expandable>
<template #item_project="{ item }">
<Link :to="`/${item.namespace.owner}/${item.namespace.slug}`">
{{ `${item.namespace.owner}/${item.namespace.slug}` }}
</Link>
</template>
<template #item_version="{ item }">
<Link :to="{ name: 'user-project-versions-version-platform', params: getRouteParams(item) }">
<Tag :color="{ background: item.channelColor }" :name="item.channelName" :data="item.versionString" />
</Link>
</template>
<template #item_queuedBy="{ item }">
<Link :to="`/${item.versionAuthor}`">
{{ item.versionAuthor }}
</Link>
<br />
<small>{{ i18n.d(item.versionCreatedAt, "time") }}</small>
</template>
<template #item_status="{ item }">
<span class="text-yellow-400">
{{ i18n.t("versionApproval.statuses.ongoing", [getOngoingCount(item)]) }}
</span>
<br />
<span class="text-red-400">
{{ i18n.t("versionApproval.statuses.stopped", [getStoppedCount(item)]) }}
</span>
<br />
<span class="text-green-400"> {{ i18n.t("versionApproval.statuses.approved", [getApprovedCount(item)]) }}</span>
</template>
<template #item_reviewLogs="{ item }">
<Link :to="{ name: 'user-project-versions-version-platform-reviews', params: getRouteParams(item) }">
<IconMdiListStatus />
{{ i18n.t("version.page.reviewLogs") }}
</Link>
</template>
<template #expanded-item="{ item, headers }">
<td :colspan="headers.length">
<ul>
<li v-for="entry in item.reviews" :key="entry.reviewerName" class="ml-4">
<span
class="font-bold mr-2"
:class="{ 'text-yellow-400': isOngoing(entry), 'text-red-400': isStopped(entry), 'text-green-400': isApproved(entry) }"
>{{ entry.reviewerName }}</span
>
<span>{{ i18n.t("versionApproval.started", [i18n.d(entry.reviewStarted, "time")]) }}</span>
<span v-if="entry.reviewEnded" class="ml-4" :class="{ 'text-red-400': isStopped(entry), 'text-green-400': isApproved(entry) }">{{
i18n.t("versionApproval.ended", [i18n.d(entry.reviewEnded, "time")])
}}</span>
</li>
</ul>
</td>
</template>
</SortableTable>
</Card>
</div>
</template>
<route lang="yaml">
meta:
requireGlobalPerm: ["REVIEWER"]
</route>

View File

@ -1,20 +1,23 @@
<script lang="ts" setup>
import { useContext } from "vite-ssr/vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { ref } from "vue";
import { useHead } from "@vueuse/head";
import { useUnresolvedFlags } from "~/composables/useApiHelper";
import { handleRequestError } from "~/composables/useErrorHandling";
import { ref } from "vue";
import PageTitle from "~/lib/components/design/PageTitle.vue";
import { useHead } from "@vueuse/head";
import { useSeo } from "~/composables/useSeo";
import Flags from "~/components/Flags.vue";
import Tabs, { Tab } from "~/lib/components/design/Tabs.vue";
import { definePageMeta } from "#imports";
definePageMeta({
globalPermsRequired: ["MOD_NOTES_AND_FLAGS"],
});
const ctx = useContext();
const i18n = useI18n();
const route = useRoute();
const flags = await useUnresolvedFlags().catch((e) => handleRequestError(e, ctx, i18n));
const flags = await useUnresolvedFlags().catch((e) => handleRequestError(e, i18n));
const loading = ref<{ [key: number]: boolean }>({});
const selectedTab = ref("unresolved");
@ -27,18 +30,15 @@ useHead(useSeo(i18n.t("flagReview.title"), null, route, null));
</script>
<template>
<PageTitle>{{ i18n.t("flagReview.title") }}</PageTitle>
<Tabs v-model="selectedTab" :tabs="selectedTabs" :vertical="false">
<template #unresolved>
<Flags :resolved="false"></Flags>
</template>
<template #resolved>
<Flags resolved></Flags>
</template>
</Tabs>
<div>
<PageTitle>{{ i18n.t("flagReview.title") }}</PageTitle>
<Tabs v-model="selectedTab" :tabs="selectedTabs" :vertical="false">
<template #unresolved>
<Flags :resolved="false"></Flags>
</template>
<template #resolved>
<Flags resolved></Flags>
</template>
</Tabs>
</div>
</template>
<route lang="yaml">
meta:
requireGlobalPerm: ["MOD_NOTES_AND_FLAGS"]
</route>

View File

@ -1,108 +1,108 @@
<script lang="ts" setup>
import { useContext } from "vite-ssr/vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { useHead } from "@vueuse/head";
import { useHealthReport } from "~/composables/useApiHelper";
import { handleRequestError } from "~/composables/useErrorHandling";
import Card from "~/lib/components/design/Card.vue";
import Link from "~/lib/components/design/Link.vue";
import PageTitle from "~/lib/components/design/PageTitle.vue";
import { useHead } from "@vueuse/head";
import { useSeo } from "~/composables/useSeo";
import { definePageMeta } from "#imports";
definePageMeta({
globalPermsRequired: ["VIEW_HEALTH"],
});
const ctx = useContext();
const i18n = useI18n();
const route = useRoute();
const healthReport = await useHealthReport().catch((e) => handleRequestError(e, ctx, i18n));
const healthReport = await useHealthReport().catch((e) => handleRequestError(e, i18n));
useHead(useSeo(i18n.t("health.title"), null, route, null));
</script>
<template>
<PageTitle>{{ i18n.t("health.title") }}</PageTitle>
<div class="grid gap-8 grid-cols-1 md:grid-cols-2">
<Card v-if="healthReport">
<template #header> {{ i18n.t("health.noTopicProject") }}</template>
<div>
<PageTitle>{{ i18n.t("health.title") }}</PageTitle>
<div class="grid gap-8 grid-cols-1 md:grid-cols-2">
<Card v-if="healthReport">
<template #header> {{ i18n.t("health.noTopicProject") }}</template>
<ul class="max-h-xs overflow-auto">
<li v-for="project in healthReport.noTopicProjects" :key="project.namespace.slug + project.namespace.owner">
<Link :to="'/' + project.namespace.owner + '/' + project.namespace.slug">
{{ project.namespace.owner + "/" + project.namespace.slug }}
</Link>
</li>
<li v-if="!healthReport.noTopicProjects || healthReport.noTopicProjects.length === 0">
{{ i18n.t("health.empty") }}
</li>
</ul>
</Card>
<Card v-if="healthReport">
<template #header> {{ i18n.t("health.erroredJobs") }}</template>
<ul class="max-h-xs overflow-auto">
<li v-for="project in healthReport.noTopicProjects" :key="project.namespace.slug + project.namespace.owner">
<Link :to="'/' + project.namespace.owner + '/' + project.namespace.slug">
{{ project.namespace.owner + "/" + project.namespace.slug }}
</Link>
</li>
<li v-if="!healthReport.noTopicProjects || healthReport.noTopicProjects.length === 0">
{{ i18n.t("health.empty") }}
</li>
</ul>
</Card>
<Card v-if="healthReport">
<template #header> {{ i18n.t("health.erroredJobs") }}</template>
<ul class="max-h-xs overflow-auto">
<li v-for="job in healthReport.erroredJobs" :key="job.jobType + new Date(job.lastUpdated).toISOString()">
{{ i18n.t("health.jobText", [job.jobType, job.lastErrorDescriptor, i18n.d(job.lastUpdated, "time")]) }}
</li>
<li v-if="!healthReport.erroredJobs || healthReport.erroredJobs.length === 0">
{{ i18n.t("health.empty") }}
</li>
</ul>
</Card>
<Card v-if="healthReport">
<template #header> {{ i18n.t("health.staleProjects") }}</template>
<ul class="max-h-xs overflow-auto">
<li v-for="job in healthReport.erroredJobs" :key="job.jobType + new Date(job.lastUpdated).toISOString()">
{{ i18n.t("health.jobText", [job.jobType, job.lastErrorDescriptor, i18n.d(job.lastUpdated, "time")]) }}
</li>
<li v-if="!healthReport.erroredJobs || healthReport.erroredJobs.length === 0">
{{ i18n.t("health.empty") }}
</li>
</ul>
</Card>
<Card v-if="healthReport">
<template #header> {{ i18n.t("health.staleProjects") }}</template>
<ul class="max-h-xs overflow-auto">
<li v-for="project in healthReport.staleProjects" :key="project.namespace.slug + project.namespace.owner">
<Link :to="'/' + project.namespace.owner + '/' + project.namespace.slug">
{{ project.namespace.owner + "/" + project.namespace.slug }}
</Link>
</li>
<li v-if="!healthReport.staleProjects || healthReport.staleProjects.length === 0">
{{ i18n.t("health.empty") }}
</li>
</ul>
</Card>
<Card v-if="healthReport">
<template #header> {{ i18n.t("health.notPublicProjects") }}</template>
<ul class="max-h-xs overflow-auto">
<li v-for="project in healthReport.staleProjects" :key="project.namespace.slug + project.namespace.owner">
<Link :to="'/' + project.namespace.owner + '/' + project.namespace.slug">
{{ project.namespace.owner + "/" + project.namespace.slug }}
</Link>
</li>
<li v-if="!healthReport.staleProjects || healthReport.staleProjects.length === 0">
{{ i18n.t("health.empty") }}
</li>
</ul>
</Card>
<Card v-if="healthReport">
<template #header> {{ i18n.t("health.notPublicProjects") }}</template>
<ul class="max-h-xs overflow-auto">
<li v-for="project in healthReport.nonPublicProjects" :key="project.namespace.slug + project.namespace.owner">
<Link :to="'/' + project.namespace.owner + '/' + project.namespace.slug">
<strong>{{ project.namespace.owner + "/" + project.namespace.slug }}</strong>
<small class="ml-1">{{ i18n.t("visibility.name." + project.visibility) }}</small>
</Link>
</li>
<li v-if="!healthReport.nonPublicProjects || healthReport.nonPublicProjects.length === 0">
{{ i18n.t("health.empty") }}
</li>
</ul>
</Card>
<Card>
<template #header>{{ i18n.t("health.noPlatform") }}</template>
<ul class="max-h-xs overflow-auto">
<li v-for="project in healthReport.nonPublicProjects" :key="project.namespace.slug + project.namespace.owner">
<Link :to="'/' + project.namespace.owner + '/' + project.namespace.slug">
<strong>{{ project.namespace.owner + "/" + project.namespace.slug }}</strong>
<small class="ml-1">{{ i18n.t("visibility.name." + project.visibility) }}</small>
</Link>
</li>
<li v-if="!healthReport.nonPublicProjects || healthReport.nonPublicProjects.length === 0">
{{ i18n.t("health.empty") }}
</li>
</ul>
</Card>
<Card>
<template #header>{{ i18n.t("health.noPlatform") }}</template>
<ul>
<li>TODO: Implementation</li>
<!--TODO idek what this is for?-->
<!--<li v-if="!healthReport.noPlatform || healthReport.noPlatform.length === 0">{{ i18n.t('health.empty') }}</li>-->
</ul>
</Card>
<Card v-if="healthReport">
<template #header> {{ i18n.t("health.missingFileProjects") }}</template>
<ul>
<li>TODO: Implementation</li>
<!--TODO idek what this is for?-->
<!--<li v-if="!healthReport.noPlatform || healthReport.noPlatform.length === 0">{{ i18n.t('health.empty') }}</li>-->
</ul>
</Card>
<Card v-if="healthReport">
<template #header> {{ i18n.t("health.missingFileProjects") }}</template>
<ul class="max-h-xs overflow-auto">
<li v-for="project in healthReport.missingFiles" :key="project.namespace.slug + project.namespace.owner">
<Link :to="'/' + project.namespace.owner + '/' + project.namespace.slug">
{{ project.namespace.owner + "/" + project.namespace.slug }}
</Link>
</li>
<li v-if="!healthReport.missingFiles || healthReport.missingFiles.length === 0">
{{ i18n.t("health.empty") }}
</li>
</ul>
</Card>
<ul class="max-h-xs overflow-auto">
<li v-for="project in healthReport.missingFiles" :key="project.namespace.slug + project.namespace.owner">
<Link :to="'/' + project.namespace.owner + '/' + project.namespace.slug">
{{ project.namespace.owner + "/" + project.namespace.slug }}
</Link>
</li>
<li v-if="!healthReport.missingFiles || healthReport.missingFiles.length === 0">
{{ i18n.t("health.empty") }}
</li>
</ul>
</Card>
</div>
</div>
</template>
<route lang="yaml">
meta:
requireGlobalPerm: ["VIEW_HEALTH"]
</route>

View File

@ -1,8 +1,8 @@
<script lang="ts" setup>
import PageTitle from "~/lib/components/design/PageTitle.vue";
import { useContext } from "vite-ssr/vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { useHead } from "@vueuse/head";
import PageTitle from "~/lib/components/design/PageTitle.vue";
import { useActionLogs } from "~/composables/useApiHelper";
import { handleRequestError } from "~/composables/useErrorHandling";
import Card from "~/lib/components/design/Card.vue";
@ -11,13 +11,16 @@ import Link from "~/lib/components/design/Link.vue";
import MarkdownModal from "~/components/modals/MarkdownModal.vue";
import DiffModal from "~/components/modals/DiffModal.vue";
import Button from "~/lib/components/design/Button.vue";
import { useHead } from "@vueuse/head";
import { useSeo } from "~/composables/useSeo";
import { definePageMeta } from "#imports";
definePageMeta({
globalPermsRequired: ["VIEW_LOGS"],
});
const ctx = useContext();
const i18n = useI18n();
const route = useRoute();
const loggedActions = await useActionLogs().catch((e) => handleRequestError(e, ctx, i18n));
const loggedActions = await useActionLogs().catch((e) => handleRequestError(e, i18n));
const headers = [
{ title: i18n.t("userActionLog.user"), name: "user", sortable: false },
@ -34,86 +37,83 @@ useHead(useSeo(i18n.t("userActionLog.title"), null, route, null));
</script>
<template>
<PageTitle>{{ i18n.t("userActionLog.title") }}</PageTitle>
<Card>
<SortableTable :headers="headers" :items="loggedActions?.result">
<template #item_user="{ item }">
<Link :to="'/' + item.userName">{{ item.userName }}</Link>
</template>
<template #item_time="{ item }">
{{ i18n.d(item.createdAt, "time") }}
</template>
<template #item_action="{ item }">
{{ i18n.t(item.action.description) }}
</template>
<template #item_context="{ item }">
<template v-if="item.page">
<Link :to="'/' + item.project.owner + '/' + item.project.slug + '/pages/' + item.page.slug">
{{ item.project.owner + "/" + item.project.slug + "/" + item.page.slug }}
</Link>
<div>
<PageTitle>{{ i18n.t("userActionLog.title") }}</PageTitle>
<Card>
<SortableTable :headers="headers" :items="loggedActions?.result">
<template #item_user="{ item }">
<Link :to="'/' + item.userName">{{ item.userName }}</Link>
</template>
<template v-else-if="item.version">
<Link :to="'/' + item.project.owner + '/' + item.project.slug + '/versions/' + item.version.versionString">
{{ `${item.project.owner}/${item.project.slug}/${item.version.versionString}` }}
</Link>
<template #item_time="{ item }">
{{ i18n.d(item.createdAt, "time") }}
</template>
<template v-else-if="item.project && item.project.owner">
<Link :to="'/' + item.project.owner + '/' + item.project.slug">{{ item.project.owner + "/" + item.project.slug }} </Link>
<template #item_action="{ item }">
{{ i18n.t(item.action.description) }}
</template>
<template v-else-if="item.subject">
<Link :to="'/' + item.subject.name">{{ item.subject.name }}</Link>
<template #item_context="{ item }">
<template v-if="item.page">
<Link :to="'/' + item.project.owner + '/' + item.project.slug + '/pages/' + item.page.slug">
{{ item.project.owner + "/" + item.project.slug + "/" + item.page.slug }}
</Link>
</template>
<template v-else-if="item.version">
<Link :to="'/' + item.project.owner + '/' + item.project.slug + '/versions/' + item.version.versionString">
{{ `${item.project.owner}/${item.project.slug}/${item.version.versionString}` }}
</Link>
</template>
<template v-else-if="item.project && item.project.owner">
<Link :to="'/' + item.project.owner + '/' + item.project.slug">{{ item.project.owner + "/" + item.project.slug }} </Link>
</template>
<template v-else-if="item.subject">
<Link :to="'/' + item.subject.name">{{ item.subject.name }}</Link>
</template>
</template>
</template>
<template #item_oldState="{ item }">
<template v-if="(item.contextType === 'PAGE' || item.action.pgLoggedAction === 'version_description_changed') && item.oldState">
<MarkdownModal :markdown="item.oldState" :title="i18n.t('userActionLog.markdownView')">
<template #activator="{ on }">
<Button size="small" v-on="on">
{{ i18n.t("userActionLog.markdownView") }}
</Button>
</template>
</MarkdownModal>
</template>
<template v-else-if="item.action.pgLoggedAction === 'project_icon_changed'">
<span v-if="item.oldState === '#empty'">default</span>
<img v-else class="inline-img" :src="'data:image/png;base64,' + item.oldState" alt="" />
</template>
<template v-else>
<span>{{ item.oldState && i18n.te(item.oldState) ? i18n.t(item.oldState) : item.oldState }}</span>
</template>
</template>
<template #item_newState="{ item }">
<template v-if="item.contextType === 'PAGE' || item.action.pgLoggedAction === 'version_description_changed'">
<div class="flex gap-2">
<MarkdownModal :markdown="item.newState" :title="i18n.t('userActionLog.markdownView')">
<template #item_oldState="{ item }">
<template v-if="(item.contextType === 'PAGE' || item.action.pgLoggedAction === 'version_description_changed') && item.oldState">
<MarkdownModal :markdown="item.oldState" :title="i18n.t('userActionLog.markdownView')">
<template #activator="{ on }">
<Button size="small" v-on="on">
{{ i18n.t("userActionLog.markdownView") }}
</Button>
</template>
</MarkdownModal>
<DiffModal :left="item.oldState" :right="item.newState" :title="i18n.t('userActionLog.diffView')">
<template #activator="{ on }">
<Button size="small" v-on="on">
{{ i18n.t("userActionLog.diffView") }}
</Button>
</template>
</DiffModal>
</div>
</template>
<template v-else-if="item.action.pgLoggedAction === 'project_icon_changed'">
<span v-if="item.oldState === '#empty'">default</span>
<img v-else class="inline-img" :src="'data:image/png;base64,' + item.oldState" alt="" />
</template>
<template v-else>
<span>{{ item.oldState && i18n.te(item.oldState) ? i18n.t(item.oldState) : item.oldState }}</span>
</template>
</template>
<template v-else-if="item.action.pgLoggedAction === 'project_icon_changed'">
<span v-if="item.newState === '#empty'">default</span>
<img v-else class="inline-img" :src="'data:image/png;base64,' + item.newState" alt="" />
<template #item_newState="{ item }">
<template v-if="item.contextType === 'PAGE' || item.action.pgLoggedAction === 'version_description_changed'">
<div class="flex gap-2">
<MarkdownModal :markdown="item.newState" :title="i18n.t('userActionLog.markdownView')">
<template #activator="{ on }">
<Button size="small" v-on="on">
{{ i18n.t("userActionLog.markdownView") }}
</Button>
</template>
</MarkdownModal>
<DiffModal :left="item.oldState" :right="item.newState" :title="i18n.t('userActionLog.diffView')">
<template #activator="{ on }">
<Button size="small" v-on="on">
{{ i18n.t("userActionLog.diffView") }}
</Button>
</template>
</DiffModal>
</div>
</template>
<template v-else-if="item.action.pgLoggedAction === 'project_icon_changed'">
<span v-if="item.newState === '#empty'">default</span>
<img v-else class="inline-img" :src="'data:image/png;base64,' + item.newState" alt="" />
</template>
<template v-else>
<span>{{ i18n.te(item.newState) ? i18n.t(item.newState) : item.newState }}</span>
</template>
</template>
<template v-else>
<span>{{ i18n.te(item.newState) ? i18n.t(item.newState) : item.newState }}</span>
</template>
</template>
</SortableTable>
</Card>
</SortableTable>
</Card>
</div>
</template>
<route lang="yaml">
meta:
requireGlobalPerm: ["VIEW_LOGS"]
</route>

View File

@ -1,20 +1,38 @@
<script lang="ts" setup>
import { useContext } from "vite-ssr/vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { handleRequestError } from "~/composables/useErrorHandling";
import { ref, watch } from "vue";
import Chartist, { IChartistSeriesData, ILineChartOptions } from "chartist";
import { useHead } from "@vueuse/head";
import { handleRequestError } from "~/composables/useErrorHandling";
import { fromISOString, toISODateString } from "~/lib/composables/useDate";
import { useInternalApi } from "~/composables/useApi";
import Chart from "~/components/Chart.vue";
import Chartist, { IChartistSeriesData, ILineChartOptions } from "chartist";
import PageTitle from "~/lib/components/design/PageTitle.vue";
import Card from "~/lib/components/design/Card.vue";
import InputDate from "~/lib/components/ui/InputDate.vue";
import { useHead } from "@vueuse/head";
import { useSeo } from "~/composables/useSeo";
import { definePageMeta } from "#imports";
definePageMeta({
globalPermsRequired: ["VIEW_STATS"],
});
interface DayStat {
x: Date;
y: number;
}
interface DayStats {
day: string;
flagsClosed: number;
flagsOpened: number;
reviews: number;
totalDownloads: number;
unsafeDownloads: number;
uploads: number;
}
const ctx = useContext();
const i18n = useI18n();
const route = useRoute();
@ -26,7 +44,7 @@ const endDate = ref<string>(toISODateString(now));
let data: DayStats[] = (await useInternalApi<DayStats[]>("admin/stats", true, "get", {
from: startDate.value,
to: endDate.value,
}).catch((e) => handleRequestError(e, ctx, i18n))) as DayStats[];
}).catch((e) => handleRequestError(e, i18n))) as DayStats[];
let reviews: DayStat[] = [];
let uploads: DayStat[] = [];
@ -104,7 +122,7 @@ async function updateDate() {
data = (await useInternalApi<DayStats[]>("admin/stats", true, "get", {
from: startDate.value,
to: endDate.value,
}).catch((e) => handleRequestError(e, ctx, i18n))) as DayStats[];
}).catch((e) => handleRequestError(e, i18n))) as DayStats[];
if (!data) {
return;
}
@ -130,48 +148,30 @@ async function updateDate() {
(flagData.value.series[0] as IChartistSeriesData).data = openedFlags;
(flagData.value.series[1] as IChartistSeriesData).data = closedFlags;
}
interface DayStat {
x: Date;
y: number;
}
interface DayStats {
day: string;
flagsClosed: number;
flagsOpened: number;
reviews: number;
totalDownloads: number;
unsafeDownloads: number;
uploads: number;
}
</script>
<template>
<PageTitle>{{ i18n.t("stats.title") }}</PageTitle>
<InputDate v-model="startDate" />
<InputDate v-model="endDate" />
<Card class="mt-4">
<template #header> {{ i18n.t("stats.plugins") }}</template>
<client-only>
<Chart id="stats" :data="pluginData" :options="options" bar-type="Line" />
</client-only>
</Card>
<Card class="mt-4">
<template #header>{{ i18n.t("stats.downloads") }}</template>
<client-only>
<Chart id="downloads" :data="downloadData" :options="options" bar-type="Line" />
</client-only>
</Card>
<Card class="mt-4">
<template #header>{{ i18n.t("stats.flags") }}</template>
<client-only>
<Chart id="flags" :data="flagData" :options="options" bar-type="Line" />
</client-only>
</Card>
<div>
<PageTitle>{{ i18n.t("stats.title") }}</PageTitle>
<InputDate v-model="startDate" />
<InputDate v-model="endDate" />
<Card class="mt-4">
<template #header> {{ i18n.t("stats.plugins") }}</template>
<client-only>
<Chart id="stats" :data="pluginData" :options="options" bar-type="Line" />
</client-only>
</Card>
<Card class="mt-4">
<template #header>{{ i18n.t("stats.downloads") }}</template>
<client-only>
<Chart id="downloads" :data="downloadData" :options="options" bar-type="Line" />
</client-only>
</Card>
<Card class="mt-4">
<template #header>{{ i18n.t("stats.flags") }}</template>
<client-only>
<Chart id="flags" :data="flagData" :options="options" bar-type="Line" />
</client-only>
</Card>
</div>
</template>
<route lang="yaml">
meta:
requireGlobalPerm: ["VIEW_STATS"]
</route>

View File

@ -1,18 +1,18 @@
<script lang="ts" setup>
import PageTitle from "~/lib/components/design/PageTitle.vue";
import { useI18n } from "vue-i18n";
import { PaginatedResult, Project, User } from "hangar-api";
import { useRoute, useRouter } from "vue-router";
import { OrganizationRoleTable } from "hangar-internal";
import { computed, ref } from "vue";
import { useHead } from "@vueuse/head";
import { AxiosError } from "axios";
import PageTitle from "~/lib/components/design/PageTitle.vue";
import Link from "~/lib/components/design/Link.vue";
import Card from "~/lib/components/design/Card.vue";
import { useApi, useInternalApi } from "~/composables/useApi";
import { PaginatedResult, Project, User } from "hangar-api";
import { useRoute, useRouter } from "vue-router";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useContext } from "vite-ssr/vue";
import { OrganizationRoleTable } from "hangar-internal";
import { computed, ref } from "vue";
import SortableTable, { Header } from "~/components/SortableTable.vue";
import InputCheckbox from "~/lib/components/ui/InputCheckbox.vue";
import { useHead } from "@vueuse/head";
import { useSeo } from "~/composables/useSeo";
import { authUrl, forumUserUrl } from "~/composables/useUrlHelper";
import { useUser } from "~/composables/useApiHelper";
@ -20,21 +20,24 @@ import Tag from "~/components/Tag.vue";
import InputSelect from "~/lib/components/ui/InputSelect.vue";
import { useBackendDataStore } from "~/store/backendData";
import Button from "~/lib/components/design/Button.vue";
import { AxiosError } from "axios";
import { definePageMeta } from "#imports";
definePageMeta({
globalPermsRequired: ["EDIT_ALL_USER_SETTINGS"],
});
const i18n = useI18n();
const route = useRoute();
const ctx = useContext();
const router = useRouter();
const backendData = useBackendDataStore();
const projects = await useApi<PaginatedResult<Project>>("projects", false, "get", {
owner: route.params.user,
}).catch((e) => handleRequestError(e, ctx, i18n));
}).catch((e) => handleRequestError(e, i18n));
const orgs = await useInternalApi<{ [key: string]: OrganizationRoleTable }>(`organizations/${route.params.user}/userOrganizations`, false).catch((e) =>
handleRequestError(e, ctx, i18n)
handleRequestError(e, i18n)
);
const user = await useUser(route.params.user as string).catch((e) => handleRequestError(e, ctx, i18n));
const user = await useUser(route.params.user as string).catch((e) => handleRequestError(e, i18n));
const projectsConfig = [
{ title: i18n.t("userAdmin.project"), name: "name" },
@ -69,7 +72,7 @@ async function processRole(add: boolean) {
user.value = await useApi<User>(("users/" + route.params.user) as string);
}
} catch (e) {
handleRequestError(e as AxiosError, ctx, i18n);
handleRequestError(e as AxiosError, i18n);
}
}
@ -77,96 +80,93 @@ useHead(useSeo(i18n.t("userAdmin.title") + " " + route.params.user, null, route,
</script>
<template>
<PageTitle
>{{ i18n.t("userAdmin.title") }}
<Link :to="'/' + $route.params.user">
{{ $route.params.user }}
</Link>
</PageTitle>
<div class="flex <md:flex-col mb-2 gap-2">
<Card class="basis-full md:basis-8/12">
<template #header>{{ i18n.t("userAdmin.roles") }}</template>
<div class="space-x-1">
<Tag v-for="role in user.roles" :key="role.value" :color="{ background: role.color }" :name="role.title" />
</div>
<div>
<PageTitle
>{{ i18n.t("userAdmin.title") }}
<Link :to="'/' + $route.params.user">
{{ $route.params.user }}
</Link>
</PageTitle>
<div class="flex <md:flex-col mb-2 gap-2">
<Card class="basis-full md:basis-8/12">
<template #header>{{ i18n.t("userAdmin.roles") }}</template>
<div class="space-x-1">
<Tag v-for="role in user.roles" :key="role.value" :color="{ background: role.color }" :name="role.title" />
</div>
<div class="flex mt-2">
<div class="flex-grow">
<InputSelect v-model="selectedRole" :values="backendData.globalRoles" item-text="title" item-value="value"></InputSelect>
<div class="flex mt-2">
<div class="flex-grow">
<InputSelect v-model="selectedRole" :values="backendData.globalRoles" item-text="title" item-value="value"></InputSelect>
</div>
<div>
<Button size="medium" :disabled="!selectedRole || user.roles.some((r) => r.value === selectedRole)" @click="processRole(true)">
{{ i18n.t("general.add") }}
</Button>
</div>
<div class="ml-2">
<Button size="medium" :disabled="!selectedRole || !user.roles.some((r) => r.value === selectedRole)" @click="processRole(false)">
{{ i18n.t("general.delete") }}
</Button>
</div>
</div>
<div>
<Button size="medium" :disabled="!selectedRole || user.roles.some((r) => r.value === selectedRole)" @click="processRole(true)">
{{ i18n.t("general.add") }}
</Button>
</div>
<div class="ml-2">
<Button size="medium" :disabled="!selectedRole || !user.roles.some((r) => r.value === selectedRole)" @click="processRole(false)">
{{ i18n.t("general.delete") }}
</Button>
</div>
</div>
</Card>
<Card class="basis-full md:basis-4/12">
<template #header>{{ i18n.t("userAdmin.sidebar") }}</template>
<ul>
<li>
<Link :href="_authUrl">{{ i18n.t("userAdmin.hangarAuth") }}</Link>
</li>
<li>
<Link :href="_forumUserUrl">{{ i18n.t("userAdmin.forum") }}</Link>
</li>
</ul>
</Card>
</div>
<Card md="mb-2">
<template #header>{{ i18n.t("userAdmin.organizations") }}</template>
<SortableTable :items="orgList" :headers="orgConfig">
<template #item_name="{ item }">
<Link :to="'/' + item.name">
{{ item.name }}
</Link>
</template>
<template #item_owner="{ item }">
<Link :to="'/' + orgs[item.name].ownerName">
{{ orgs[item.name].ownerName }}
</Link>
</template>
<template #item_role="{ item }">
{{ orgs[item.name].role.title }}
</template>
<template #item_accepted="{ item }">
<InputCheckbox v-model="orgs[item.name].accepted" :disabled="true" />
</template>
</SortableTable>
</Card>
<Card class="basis-full md:basis-4/12">
<template #header>{{ i18n.t("userAdmin.sidebar") }}</template>
<ul>
<li>
<Link :href="_authUrl">{{ i18n.t("userAdmin.hangarAuth") }}</Link>
</li>
<li>
<Link :href="_forumUserUrl">{{ i18n.t("userAdmin.forum") }}</Link>
</li>
</ul>
<Card md="col-start-1">
<template #header>{{ i18n.t("userAdmin.projects") }}</template>
<SortableTable v-if="projects" :items="projects.result" :headers="projectsConfig">
<template #item_name="{ item }">
<Link :to="'/' + item.namespace.owner + '/' + item.name">
{{ item.name }}
</Link>
</template>
<template #item_owner="{ item }">
<Link :to="'/' + item.namespace.owner">
{{ item.namespace.owner }}
</Link>
</template>
<template #item_role="{ item }">
<!-- todo add role -->
&lt;{{ item.name }}'s role&gt;
</template>
<template #item_accepted="{ item }">
<InputCheckbox :model-value="item.visibility === 'public'" :disabled="true" />
</template>
</SortableTable>
</Card>
</div>
<Card md="mb-2">
<template #header>{{ i18n.t("userAdmin.organizations") }}</template>
<SortableTable :items="orgList" :headers="orgConfig">
<template #item_name="{ item }">
<Link :to="'/' + item.name">
{{ item.name }}
</Link>
</template>
<template #item_owner="{ item }">
<Link :to="'/' + orgs[item.name].ownerName">
{{ orgs[item.name].ownerName }}
</Link>
</template>
<template #item_role="{ item }">
{{ orgs[item.name].role.title }}
</template>
<template #item_accepted="{ item }">
<InputCheckbox v-model="orgs[item.name].accepted" :disabled="true" />
</template>
</SortableTable>
</Card>
<Card md="col-start-1">
<template #header>{{ i18n.t("userAdmin.projects") }}</template>
<SortableTable v-if="projects" :items="projects.result" :headers="projectsConfig">
<template #item_name="{ item }">
<Link :to="'/' + item.namespace.owner + '/' + item.name">
{{ item.name }}
</Link>
</template>
<template #item_owner="{ item }">
<Link :to="'/' + item.namespace.owner">
{{ item.namespace.owner }}
</Link>
</template>
<template #item_role="{ item }">
<!-- todo add role -->
&lt;{{ item.name }}'s role&gt;
</template>
<template #item_accepted="{ item }">
<InputCheckbox :model-value="item.visibility === 'public'" :disabled="true" />
</template>
</SortableTable>
</Card>
</template>
<route lang="yaml">
meta:
requireGlobalPerm: ["EDIT_ALL_USER_SETTINGS"]
</route>

View File

@ -1,22 +1,25 @@
<script lang="ts" setup>
import { useContext } from "vite-ssr/vue";
import { useI18n } from "vue-i18n";
import { useRoute, useRouter } from "vue-router";
import { handleRequestError } from "~/composables/useErrorHandling";
import { computed, ref } from "vue";
import { cloneDeep, isEqual } from "lodash-es";
import { useHead } from "@vueuse/head";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useBackendDataStore } from "~/store/backendData";
import { useInternalApi } from "~/composables/useApi";
import { cloneDeep, isEqual } from "lodash-es";
import InputTag from "~/lib/components/ui/InputTag.vue";
import Button from "~/lib/components/design/Button.vue";
import PageTitle from "~/lib/components/design/PageTitle.vue";
import Card from "~/lib/components/design/Card.vue";
import Table from "~/lib/components/design/Table.vue";
import { useHead } from "@vueuse/head";
import { useSeo } from "~/composables/useSeo";
import { useNotificationStore } from "~/lib/store/notification";
import { definePageMeta } from "#imports";
definePageMeta({
globalPermsRequired: ["MANUAL_VALUE_CHANGES"],
});
const ctx = useContext();
const i18n = useI18n();
const route = useRoute();
const router = useRouter();
@ -41,7 +44,7 @@ async function save() {
router.go(0);
} catch (e: any) {
loading.value = false;
handleRequestError(e, ctx, i18n);
handleRequestError(e, i18n);
}
}
@ -53,35 +56,32 @@ const hasChanged = computed(() => !isEqual(platforms.value, originalPlatforms));
</script>
<template>
<PageTitle>{{ i18n.t("platformVersions.title") }}</PageTitle>
<Card>
<Table class="w-full">
<thead>
<tr>
<th>{{ i18n.t("platformVersions.platform") }}</th>
<th>{{ i18n.t("platformVersions.versions") }}</th>
</tr>
</thead>
<tbody>
<tr v-for="platform in platforms" :key="platform.name">
<td>{{ platform.name }}</td>
<td>
<InputTag v-model="platform.possibleVersions"></InputTag>
</td>
</tr>
</tbody>
</Table>
<div>
<PageTitle>{{ i18n.t("platformVersions.title") }}</PageTitle>
<Card>
<Table class="w-full">
<thead>
<tr>
<th>{{ i18n.t("platformVersions.platform") }}</th>
<th>{{ i18n.t("platformVersions.versions") }}</th>
</tr>
</thead>
<tbody>
<tr v-for="platform in platforms" :key="platform.name">
<td>{{ platform.name }}</td>
<td>
<InputTag v-model="platform.possibleVersions"></InputTag>
</td>
</tr>
</tbody>
</Table>
<template #footer>
<span class="flex justify-end">
<Button :disabled="!hasChanged" @click="reset">{{ i18n.t("general.reset") }}</Button>
<Button :disabled="loading || !hasChanged" class="ml-2" @click="save"> {{ i18n.t("platformVersions.saveChanges") }}</Button>
</span>
</template>
</Card>
<template #footer>
<span class="flex justify-end">
<Button :disabled="!hasChanged" @click="reset">{{ i18n.t("general.reset") }}</Button>
<Button :disabled="loading || !hasChanged" class="ml-2" @click="save"> {{ i18n.t("platformVersions.saveChanges") }}</Button>
</span>
</template>
</Card>
</div>
</template>
<route lang="yaml">
meta:
requireGlobalPerm: ["MANUAL_VALUE_CHANGES"]
</route>

View File

@ -1,9 +1,9 @@
<script lang="ts" setup>
import { onMounted } from "vue";
import { useHead } from "@vueuse/head";
import { useSeo } from "~/composables/useSeo";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { useSeo } from "~/composables/useSeo";
import { useAuthStore } from "~/store/auth";
const i18n = useI18n();

View File

@ -1,22 +1,20 @@
<script lang="ts" setup>
import { useContext } from "vite-ssr/vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { useHead } from "@vueuse/head";
import { PaginatedResult, User } from "hangar-api";
import { computed, ref } from "vue";
import { useAuthors } from "~/composables/useApiHelper";
import { handleRequestError } from "~/composables/useErrorHandling";
import SortableTable, { Header } from "~/components/SortableTable.vue";
import PageTitle from "~/lib/components/design/PageTitle.vue";
import UserAvatar from "~/components/UserAvatar.vue";
import { useHead } from "@vueuse/head";
import { useSeo } from "~/composables/useSeo";
import Link from "~/lib/components/design/Link.vue";
import { useApi } from "~/composables/useApi";
import { PaginatedResult, User } from "hangar-api";
import { computed, ref } from "vue";
const ctx = useContext();
const i18n = useI18n();
const route = useRoute();
const authors = await useAuthors().catch((e) => handleRequestError(e, ctx, i18n));
const authors = await useAuthors().catch((e) => handleRequestError(e, i18n));
const headers = [
{ name: "pic", title: "", sortable: false },
@ -30,7 +28,7 @@ const sort = ref<string[]>([]);
const requestParams = computed(() => {
const limit = 25;
return {
limit: limit,
limit,
offset: page.value * limit,
sort: sort.value,
};
@ -40,8 +38,8 @@ async function updateSort(col: string, sorter: Record<string, number>) {
sort.value = [...Object.keys(sorter)]
.map((k) => {
const val = sorter[k];
if (val == -1) return "-" + k;
if (val == 1) return k;
if (val === -1) return "-" + k;
if (val === 1) return k;
return null;
})
.filter((v) => v !== null) as string[];
@ -62,12 +60,14 @@ useHead(useSeo(i18n.t("pages.authorsTitle"), "Hangar Project Authors", route, nu
</script>
<template>
<PageTitle>Authors</PageTitle>
<SortableTable :headers="headers" :items="authors?.result" :server-pagination="authors?.pagination" @update:sort="updateSort" @update:page="updatePage">
<template #item_pic="{ item }"><UserAvatar :username="item.name" size="xs"></UserAvatar></template>
<template #item_joinDate="{ item }">{{ i18n.d(item?.joinDate, "date") }}</template>
<template #item_name="{ item }">
<Link :to="'/' + item.name">{{ item.name }}</Link>
</template>
</SortableTable>
<div>
<PageTitle>Authors</PageTitle>
<SortableTable :headers="headers" :items="authors?.result" :server-pagination="authors?.pagination" @update:sort="updateSort" @update:page="updatePage">
<template #item_pic="{ item }"><UserAvatar :username="item.name" size="xs"></UserAvatar></template>
<template #item_joinDate="{ item }">{{ i18n.d(item?.joinDate, "date") }}</template>
<template #item_name="{ item }">
<Link :to="'/' + item.name">{{ item.name }}</Link>
</template>
</SortableTable>
</div>
</template>

View File

@ -19,7 +19,3 @@ useHead(useSeo((route.params.status || 404) + " " + (route.params.msg || "Not fo
</template>
</div>
</template>
<route lang="yaml">
layout: error
</route>

Some files were not shown because too many files have changed in this diff Show More