Hangar/frontend/plugins/utils.ts

326 lines
12 KiB
TypeScript
Raw Normal View History

2021-02-06 00:56:21 +08:00
import { Context } from '@nuxt/types';
import { Inject } from '@nuxt/types/app';
import { AxiosError } from 'axios';
2021-02-11 18:39:29 +08:00
import filesize from 'filesize';
2021-02-11 19:48:52 +08:00
// TODO fix it complaining about no type declaration file
// @ts-ignore
2021-03-16 01:58:43 +08:00
// import { contrastRatio, HexToRGBA, parseHex } from 'vuetify/es5/util/colorUtils'; // TODO remove or fix
import { HangarApiException, HangarValidationException, MultiHangarApiException } from 'hangar-api';
2021-02-24 03:27:43 +08:00
import { HangarProject, HangarUser } from 'hangar-internal';
2021-02-11 14:17:29 +08:00
import { TranslateResult } from 'vue-i18n';
2021-02-24 03:27:43 +08:00
import { NamedPermission, Visibility } from '~/types/enums';
2021-02-06 00:56:21 +08:00
import { RootState } from '~/store';
2021-02-09 02:26:18 +08:00
import { NotifPayload } from '~/store/snackbar';
2021-02-07 11:22:37 +08:00
import { AuthState } from '~/store/auth';
2021-01-23 02:44:49 +08:00
// for page request errors
2021-02-13 14:36:53 +08:00
function handleRequestError(err: AxiosError, error: Context['error'], i18n: Context['app']['i18n']) {
if (!err.isAxiosError) {
// everything should be an AxiosError
error({
statusCode: 500,
});
console.log(err);
} else if (err.response) {
2021-02-10 12:40:34 +08:00
if (err.response.data.isHangarApiException) {
const data: HangarApiException = err.response.data.isMultiException ? err.response.data.exceptions[0] : err.response.data;
error({
statusCode: data.httpError.statusCode,
2021-02-13 14:36:53 +08:00
message: i18n.te(data.message) ? <string>i18n.t(data.message) : data.message,
});
2021-02-10 12:40:34 +08:00
} else if (err.response.data.isHangarValidationException) {
const data: HangarValidationException = err.response.data;
error({
statusCode: data.httpError.statusCode,
message: data.fieldErrors.map((f) => f.errorMsg).join(', '),
});
} else {
error({
statusCode: err.response.status,
message: err.response.statusText,
});
}
} else {
error({
message: "This shouldn't happen...",
});
console.log(err);
}
}
function collectErrors(exception: HangarApiException | MultiHangarApiException, i18n: Context['app']['i18n']): TranslateResult[] {
if (!exception.isMultiException) {
return [i18n.te(exception.message) ? i18n.t(exception.message, [exception.messageArgs]) : exception.message];
} else {
const res: TranslateResult[] = [];
for (const ex of exception.exceptions) {
res.push(i18n.te(ex.message) ? i18n.t(ex.message, ex.messageArgs) : ex.message);
}
return res;
}
}
2021-02-13 14:36:53 +08:00
const createUtil = ({ store, error, app: { i18n } }: Context) => {
2021-01-23 02:44:49 +08:00
class Util {
2021-02-24 03:27:43 +08:00
dummyUser(): HangarUser {
return {
2021-02-06 00:56:21 +08:00
name: 'Dummy',
2021-02-05 23:29:14 +08:00
id: 42,
tagline: null,
createdAt: this.prettyDate(new Date()),
roles: [],
headerData: {
2021-02-06 00:56:21 +08:00
globalPermission: '',
2021-02-08 06:06:06 +08:00
unreadNotifications: 1,
projectApprovals: 3,
reviewQueueCount: 0,
unresolvedFlags: 2,
2021-02-05 23:29:14 +08:00
},
2021-02-06 00:56:21 +08:00
joinDate: this.prettyDate(new Date()),
} as unknown as HangarUser;
2021-02-05 23:29:14 +08:00
}
2021-02-24 03:27:43 +08:00
dummyProject(): HangarProject {
return { namespace: { owner: 'test', slug: 'test2' }, visibility: Visibility.NEW } as unknown as HangarProject;
2021-02-24 03:27:43 +08:00
}
2021-02-08 06:06:06 +08:00
avatarUrl(name: string): string {
return `/avatar/${name}?size=120x120`;
2021-01-23 02:44:49 +08:00
}
projectUrl(owner: string, slug: string): string {
return `/api/internal/projects/project/${owner}/${slug}/icon`;
}
2021-02-05 23:29:14 +08:00
forumUrl(name: string): string {
2021-01-23 02:44:49 +08:00
return 'https://papermc.io/forums/u/' + name;
}
prettyDate(date: Date | string | number): string {
if (typeof date === 'string' || typeof date === 'number') {
2021-02-09 02:26:18 +08:00
date = new Date(date);
}
2021-02-05 04:30:47 +08:00
return date.toLocaleDateString(undefined, {
day: 'numeric',
month: 'long',
year: 'numeric',
});
2021-01-23 02:44:49 +08:00
}
2021-01-23 15:37:57 +08:00
2021-03-13 18:10:56 +08:00
prettyDateTime(date: Date | string | number): string {
if (typeof date === 'string' || typeof date === 'number') {
date = new Date(date);
}
return date.toLocaleDateString(undefined, {
day: 'numeric',
month: '2-digit',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
});
}
2021-04-05 04:58:21 +08:00
/**
* Requires yyyy-MM-DD format
* @param dateString
*/
fromISOString(dateString: string): Date {
const ds = dateString.split('-').map((s) => parseInt(s));
return new Date(ds[0], ds[1] - 1, ds[2]);
}
toISODateString(date: Date): string {
return `${date.getFullYear()}-${('0' + (date.getMonth() + 1)).slice(-2)}-${('0' + date.getDate()).slice(-2)}`;
}
2021-02-11 18:39:29 +08:00
formatSize(input: any) {
return filesize(input);
}
2021-03-16 01:58:43 +08:00
// TODO either fix or remove this
// white = HexToRGBA(parseHex('#ffffff'));
// black = HexToRGBA(parseHex('#000000'));
2021-02-11 19:48:52 +08:00
isDark(hex: string): boolean {
2021-03-16 01:58:43 +08:00
// const rgba = HexToRGBA(parseHex(hex));
// return contrastRatio(rgba, this.white) > 2;
return hex === null;
2021-02-11 19:48:52 +08:00
}
isLight(hex: string): boolean {
2021-03-16 01:58:43 +08:00
// const rgba = HexToRGBA(parseHex(hex));
// return contrastRatio(rgba, this.black) > 2;
return hex === null;
2021-02-11 19:48:52 +08:00
}
2021-02-05 08:22:22 +08:00
/**
* Checks if the supplier permission has all named permissions.
* @param namedPermission perms required
*/
2021-02-07 11:22:37 +08:00
hasPerms(...namedPermission: NamedPermission[]): boolean {
const perms = (store.state.auth as AuthState).routePermissions;
if (perms === null) return false;
const _perms: bigint = BigInt('0b' + perms);
let result = true;
2021-02-05 08:22:22 +08:00
for (const np of namedPermission) {
const perm = (store.state as RootState).permissions.get(np);
if (!perm) {
throw new Error(namedPermission + ' is not valid');
}
2021-02-07 11:22:37 +08:00
const val = BigInt('0b' + perm.permission.toString(2));
result = result && (_perms & val) === val;
2021-01-23 15:37:57 +08:00
}
2021-02-05 08:22:22 +08:00
return result;
}
2021-02-07 11:22:37 +08:00
isCurrentUser(id: number): boolean {
return (store.state.auth as AuthState).user?.id === id;
}
2021-02-07 13:44:53 +08:00
/**
* Used when an auth error should redirect to 404
* @param err the axios request error
*/
handlePageRequestError(err: AxiosError) {
2021-02-13 14:36:53 +08:00
handleRequestError(err, error, i18n);
}
/**
* Used for showing error popups. See handlePageRequestError to show an error page.
* @param err the axios request error
2021-02-13 14:36:53 +08:00
* @param msg optional error message (should be i18n)
*/
2021-02-13 14:36:53 +08:00
handleRequestError(err: AxiosError, msg: string | undefined = undefined) {
if (process.server) {
2021-02-13 14:36:53 +08:00
handleRequestError(err, error, i18n);
return;
}
if (!err.isAxiosError) {
// everything should be an AxiosError
error({
statusCode: 500,
});
console.log(err);
} else if (err.response) {
2021-02-10 12:40:34 +08:00
if (err.response.data.isHangarApiException) {
for (const errorMsg of collectErrors(err.response.data, i18n)) {
store.dispatch('snackbar/SHOW_NOTIF', {
message: msg ? `${i18n.t(msg)}: ${errorMsg}` : errorMsg,
color: 'error',
timeout: 3000,
} as NotifPayload);
}
2021-02-10 12:40:34 +08:00
} else if (err.response.data.isHangarValidationException) {
const data: HangarValidationException = err.response.data;
for (const fieldError of data.fieldErrors) {
store.dispatch('snackbar/SHOW_NOTIF', {
message: fieldError.errorMsg,
color: 'error',
timeout: 3000,
} as NotifPayload);
}
if (msg) {
store.dispatch('snackbar/SHOW_NOTIF', {
2021-02-13 14:36:53 +08:00
message: i18n.t(msg),
2021-02-10 12:40:34 +08:00
color: 'error',
timeout: 3000,
} as NotifPayload);
}
2021-02-05 08:22:22 +08:00
} else {
2021-02-09 02:26:18 +08:00
store.dispatch('snackbar/SHOW_NOTIF', {
2021-02-13 14:36:53 +08:00
message: msg ? `${i18n.t(msg)}: ${err.response.statusText}` : err.response.statusText,
2021-02-09 02:26:18 +08:00
color: 'error',
timeout: 2000,
2021-02-09 02:26:18 +08:00
} as NotifPayload);
2021-02-05 08:22:22 +08:00
}
2021-01-23 15:37:57 +08:00
} else {
console.log(err);
2021-01-23 15:37:57 +08:00
}
}
2021-02-05 14:51:51 +08:00
2021-03-18 07:55:48 +08:00
$vc = {
require:
(name: TranslateResult = 'Field') =>
(v: string) =>
!!v || i18n.t('validation.required', [name]),
maxLength: (maxLength: number) => (v: string | any[]) => {
return (
2021-03-20 09:56:08 +08:00
((v === null || typeof v === 'undefined' || typeof v === 'string') && !v) ||
(Array.isArray(v) && v.length === 0) ||
v.length <= maxLength ||
i18n.t('validation.maxLength', [maxLength])
);
},
minLength: (minLength: number) => (v: string | any[]) => {
return (
((v === null || typeof v === 'string') && !v) ||
(Array.isArray(v) && v.length === 0) ||
v.length >= minLength ||
i18n.t('validation.minLength', [minLength])
);
},
requireNonEmptyArray:
(name: TranslateResult = 'Field') =>
(v: any[]) =>
v.length > 0 || i18n.t('validation.required', [name]),
2021-03-26 11:06:52 +08:00
url: (v: string) => !v || new RegExp((store.state as RootState).validations.urlRegex).test(v) || i18n.t('validation.invalidUrl'),
regex:
(name: TranslateResult = 'Field', regexp: string) =>
(v: string) => {
return !v || new RegExp(regexp).test(v) || i18n.t('validation.invalidFormat', [name]);
},
requireNumberArray: () => (v: string | any[]) => {
return (
((v === null || typeof v === 'undefined' || typeof v === 'string') && !v) ||
(Array.isArray(v) && v.length === 0) ||
(v as any[]).some((i) => !isNaN(i)) ||
i18n.t('validation.numberArray')
);
},
2021-02-05 14:51:51 +08:00
};
2021-03-20 09:19:51 +08:00
error(err: TranslateResult | NotifPayload) {
2021-02-06 03:50:18 +08:00
if (typeof err === 'string') {
2021-02-09 02:26:18 +08:00
store.dispatch('snackbar/SHOW_NOTIF', { message: err, color: 'error' } as NotifPayload);
2021-02-06 03:50:18 +08:00
} else {
2021-02-09 02:26:18 +08:00
store.dispatch('snackbar/SHOW_NOTIF', err);
2021-02-06 03:50:18 +08:00
}
}
2021-02-09 02:26:18 +08:00
2021-03-20 09:19:51 +08:00
success(msg: TranslateResult) {
2021-02-09 02:26:18 +08:00
store.dispatch('snackbar/SHOW_NOTIF', { message: msg, color: 'success' } as NotifPayload);
}
2021-01-23 02:44:49 +08:00
}
return new Util();
};
type utilType = ReturnType<typeof createUtil>;
declare module 'vue/types/vue' {
interface Vue {
$util: utilType;
}
}
declare module '@nuxt/types' {
interface NuxtAppOptions {
$util: utilType;
}
interface Context {
$util: utilType;
}
}
declare module 'vuex/types/index' {
2021-01-31 10:00:11 +08:00
// eslint-disable-next-line no-unused-vars,@typescript-eslint/no-unused-vars
2021-01-23 02:44:49 +08:00
interface Store<S> {
$util: utilType;
}
}
export default (ctx: Context, inject: Inject) => {
inject('util', createUtil(ctx));
};