mirror of
https://github.com/HangarMC/Hangar.git
synced 2025-02-23 15:12:52 +08:00
implement validation framework, validate new project page
This commit is contained in:
parent
18ae7783d6
commit
58a2eb77c3
@ -19,7 +19,7 @@ Stuff that needs to be done before I consider this a successful POC
|
||||
- [x] date formatting needs to go thru i18n
|
||||
- [x] investigate why eslint/prettier don't auto fix
|
||||
- [x] actually implement page transitions (as opposed to popping up below the page)
|
||||
- [ ] validation of forms/inputs etc
|
||||
- [ ] validation of forms/inputs etc (groundwork is done, just needs to be added everywhere...)
|
||||
- [ ] check that we have loading states everywhere, on like buttons and whatever
|
||||
- [x] add header calls to all pages
|
||||
|
||||
|
@ -89,6 +89,5 @@
|
||||
"vite-plugin-pwa": "^0.11.13",
|
||||
"vite-plugin-vue-layouts": "^0.6.0",
|
||||
"vite-plugin-windicss": "^1.8.3"
|
||||
},
|
||||
"//": "VITE NEEDS TO BE 2.7 FOR NOW! https://github.com/frandiox/vite-ssr/discussions/143"
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,18 @@
|
||||
<script lang="ts" setup>
|
||||
import Popper from "vue3-popper";
|
||||
import { computed } from "vue";
|
||||
import { ErrorObject } from "@vuelidate/core";
|
||||
import { computed, Ref } from "vue";
|
||||
import { isErrorObject } from "~/composables/useValidationHelpers";
|
||||
|
||||
const props = defineProps<{
|
||||
errorMessages?: string[];
|
||||
errorMessages?: string[] | ErrorObject[];
|
||||
}>();
|
||||
|
||||
const formattedError = computed<string>(() => {
|
||||
const formattedError = computed<string | Ref<string>>(() => {
|
||||
if (!props.errorMessages || props.errorMessages.length === 0) {
|
||||
return "";
|
||||
}
|
||||
return props.errorMessages[0];
|
||||
return isErrorObject(props.errorMessages[0]) ? props.errorMessages[0].$message : props.errorMessages[0];
|
||||
});
|
||||
|
||||
const hasError = computed<boolean>(() => {
|
||||
|
@ -1,24 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useValidation } from "~/composables/useValidationHelpers";
|
||||
import { ValidationRule } from "@vuelidate/core";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: boolean | boolean[]): void;
|
||||
}>();
|
||||
const internalVal = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emit("update:modelValue", v),
|
||||
set: (val) => emit("update:modelValue", val),
|
||||
});
|
||||
const props = defineProps<{
|
||||
modelValue: boolean | boolean[];
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
errorMessages?: string[];
|
||||
rules?: ValidationRule<string | undefined>[];
|
||||
}>();
|
||||
|
||||
const { v, errors, hasError } = useValidation(props.label, props.rules, internalVal, props.errorMessages);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<label class="group relative pl-30px customCheckboxContainer w-max" :cursor="disabled ? 'auto' : 'pointer'">
|
||||
<template v-if="props.label">{{ props.label }}</template>
|
||||
<input v-model="internalVal" type="checkbox" class="hidden" v-bind="$attrs" :disabled="disabled" />
|
||||
<input v-model="internalVal" type="checkbox" class="hidden" v-bind="$attrs" :disabled="disabled" @blur="v.$touch()" />
|
||||
<span
|
||||
class="absolute top-5px left-0 h-15px w-15px rounded bg-gray-300"
|
||||
after="absolute hidden content-DEFAULT top-1px left-5px border-solid w-6px h-12px border-r-3px border-b-3px border-white"
|
||||
|
@ -1,5 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import { useValidation } from "~/composables/useValidationHelpers";
|
||||
import { ValidationRule } from "@vuelidate/core";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", date: string): void;
|
||||
@ -10,10 +12,16 @@ const date = computed({
|
||||
});
|
||||
const props = defineProps<{
|
||||
modelValue: string;
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
errorMessages?: string[];
|
||||
rules?: ValidationRule<string | undefined>[];
|
||||
}>();
|
||||
|
||||
const { v, errors, hasError } = useValidation(props.label, props.rules, date, props.errorMessages);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- todo make fancy -->
|
||||
<input v-model="date" type="date" v-bind="$attrs" />
|
||||
<input v-model="date" type="date" v-bind="$attrs" :disabled="disabled" />
|
||||
</template>
|
||||
|
@ -1,5 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import { useValidation } from "~/composables/useValidationHelpers";
|
||||
import { ValidationRule } from "@vuelidate/core";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", file: string): void;
|
||||
@ -10,11 +12,17 @@ const file = computed({
|
||||
});
|
||||
const props = defineProps<{
|
||||
modelValue: string;
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
showSize?: boolean;
|
||||
errorMessages?: string[];
|
||||
rules?: ValidationRule<string | undefined>[];
|
||||
}>();
|
||||
|
||||
const { v, errors, hasError } = useValidation(props.label, props.rules, file, props.errorMessages);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- todo make fancy, implement functionality -->
|
||||
<input type="file" v-bind="$attrs" />
|
||||
<input type="file" v-bind="$attrs" :disabled="disabled" />
|
||||
</template>
|
||||
|
@ -1,23 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useValidation } from "~/composables/useValidationHelpers";
|
||||
import { ValidationRule } from "@vuelidate/core";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value?: string): void;
|
||||
}>();
|
||||
const internalVal = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emit("update:modelValue", v),
|
||||
set: (val) => emit("update:modelValue", val),
|
||||
});
|
||||
const props = defineProps<{
|
||||
modelValue?: string;
|
||||
label?: string;
|
||||
errorMessages?: string[];
|
||||
rules?: ValidationRule<string | undefined>[];
|
||||
}>();
|
||||
|
||||
const { v, errors, hasError } = useValidation(props.label, props.rules, internalVal, props.errorMessages);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<label class="group relative cursor-pointer pl-30px customCheckboxContainer w-max">
|
||||
<template v-if="props.label">{{ props.label }}</template>
|
||||
<input v-model="internalVal" type="radio" class="hidden" v-bind="$attrs" />
|
||||
<input v-model="internalVal" type="radio" class="hidden" v-bind="$attrs" @blur="v.$touch()" />
|
||||
<span
|
||||
class="absolute top-5px left-0 h-15px w-15px rounded-full bg-gray-300"
|
||||
after="absolute hidden content-DEFAULT top-1px left-5px border-solid w-6px h-12px border-r-3px border-b-3px border-white rounded-full"
|
||||
|
@ -2,13 +2,15 @@
|
||||
import { computed } from "vue";
|
||||
import { FloatingLabel, inputClasses } from "~/composables/useInputHelper";
|
||||
import ErrorTooltip from "~/components/design/ErrorTooltip.vue";
|
||||
import { useValidation } from "~/composables/useValidationHelpers";
|
||||
import { ValidationRule } from "@vuelidate/core";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: object | string | boolean | number | null | undefined): void;
|
||||
}>();
|
||||
const internalVal = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emit("update:modelValue", v),
|
||||
set: (val) => emit("update:modelValue", val),
|
||||
});
|
||||
|
||||
export interface Option {
|
||||
@ -25,6 +27,7 @@ const props = withDefaults(
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
errorMessages?: string[];
|
||||
rules?: ValidationRule<string | undefined>[];
|
||||
}>(),
|
||||
{
|
||||
modelValue: "",
|
||||
@ -32,19 +35,17 @@ const props = withDefaults(
|
||||
itemText: "text",
|
||||
label: "",
|
||||
errorMessages: () => [],
|
||||
rules: () => [],
|
||||
}
|
||||
);
|
||||
|
||||
// TODO proper validation
|
||||
const error = computed<boolean>(() => {
|
||||
return props.errorMessages ? props.errorMessages.length > 0 : false;
|
||||
});
|
||||
const { v, errors, hasError } = useValidation(props.label, props.rules, internalVal, props.errorMessages);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ErrorTooltip :error-messages="errorMessages" class="w-full">
|
||||
<label class="relative flex" :class="{ filled: internalVal, error: error }">
|
||||
<select v-model="internalVal" :disabled="disabled" :class="inputClasses">
|
||||
<ErrorTooltip :error-messages="errors" class="w-full">
|
||||
<label class="relative flex" :class="{ filled: internalVal, error: hasError }">
|
||||
<select v-model="internalVal" :disabled="disabled" :class="inputClasses" @blur="v.$touch()">
|
||||
<option v-for="val in values" :key="val[itemValue] || val" :value="val[itemValue] || val" class="dark:bg-[#191e28]">
|
||||
{{ val[itemText] || val }}
|
||||
</option>
|
||||
|
@ -2,6 +2,8 @@
|
||||
import { computed, ref } from "vue";
|
||||
import { FloatingLabel, inputClasses } from "~/composables/useInputHelper";
|
||||
import ErrorTooltip from "~/components/design/ErrorTooltip.vue";
|
||||
import { useValidation } from "~/composables/useValidationHelpers";
|
||||
import { ValidationRule } from "@vuelidate/core";
|
||||
|
||||
const tag = ref<string>("");
|
||||
const emit = defineEmits<{
|
||||
@ -14,20 +16,19 @@ const tags = computed({
|
||||
const props = defineProps<{
|
||||
modelValue: string[];
|
||||
label?: string;
|
||||
errorMessages?: string[];
|
||||
counter?: boolean;
|
||||
maxlength?: number;
|
||||
errorMessages?: string[];
|
||||
rules?: ValidationRule<string | undefined>[];
|
||||
}>();
|
||||
|
||||
// TODO proper validation
|
||||
const error = computed<boolean>(() => {
|
||||
return props.errorMessages ? props.errorMessages.length > 0 : false;
|
||||
});
|
||||
const { v, errors, hasError } = useValidation(props.label, props.rules, tags, props.errorMessages);
|
||||
|
||||
if (!tags.value) tags.value = [];
|
||||
|
||||
function remove(t: string) {
|
||||
tags.value = tags.value.filter((v) => v != t);
|
||||
v.value.$touch();
|
||||
}
|
||||
|
||||
function add() {
|
||||
@ -39,14 +40,14 @@ function add() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ErrorTooltip :error-messages="errorMessages" class="w-full">
|
||||
<div class="relative flex items-center pointer-events-none text-left" :class="{ filled: (modelValue && modelValue.length) || tag, error: error }">
|
||||
<ErrorTooltip :error-messages="errors" class="w-full">
|
||||
<div class="relative flex items-center pointer-events-none text-left" :class="{ filled: (modelValue && modelValue.length) || tag, error: hasError }">
|
||||
<div :class="inputClasses" class="flex">
|
||||
<span v-for="t in tags" :key="t" class="bg-primary-light-400 rounded-4xl px-1 py-1 mx-1 h-30px inline-flex items-center" dark="text-black">
|
||||
{{ t }}
|
||||
<button class="text-gray-400 ml-1 inline-flex pointer-events-auto" hover="text-gray-500" @click="remove(t)"><icon-mdi-close-circle /></button>
|
||||
</span>
|
||||
<input v-model="tag" type="text" class="pointer-events-auto outline-none bg-transparent flex-grow" @keydown.enter="add" />
|
||||
<input v-model="tag" type="text" class="pointer-events-auto outline-none bg-transparent flex-grow" @keydown.enter="add" @blur="v.$touch()" />
|
||||
<floating-label :label="label" />
|
||||
</div>
|
||||
<span v-if="counter && maxlength">{{ tags?.length || 0 }}/{{ maxlength }}</span>
|
||||
|
@ -2,32 +2,32 @@
|
||||
import { computed } from "vue";
|
||||
import { FloatingLabel, inputClasses } from "~/composables/useInputHelper";
|
||||
import ErrorTooltip from "~/components/design/ErrorTooltip.vue";
|
||||
import { ValidationRule } from "@vuelidate/core";
|
||||
import { useValidation } from "~/composables/useValidationHelpers";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value?: string): void;
|
||||
}>();
|
||||
const value = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emit("update:modelValue", v),
|
||||
set: (val) => emit("update:modelValue", val),
|
||||
});
|
||||
const props = defineProps<{
|
||||
modelValue?: string;
|
||||
label?: string;
|
||||
errorMessages?: string[];
|
||||
counter?: boolean;
|
||||
maxlength?: number;
|
||||
errorMessages?: string[];
|
||||
rules?: ValidationRule<string | undefined>[];
|
||||
}>();
|
||||
|
||||
// TODO proper validation
|
||||
const error = computed<boolean>(() => {
|
||||
return props.errorMessages ? props.errorMessages.length > 0 : false;
|
||||
});
|
||||
const { v, errors, hasError } = useValidation(props.label, props.rules, value, props.errorMessages);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ErrorTooltip :error-messages="errorMessages" class="w-full">
|
||||
<label class="relative flex" :class="{ filled: modelValue, error: error }">
|
||||
<input v-model="value" type="text" :class="inputClasses" v-bind="$attrs" :maxlength="maxlength" />
|
||||
<ErrorTooltip :error-messages="errors" class="w-full">
|
||||
<label class="relative flex" :class="{ filled: modelValue, error: hasError }">
|
||||
<input v-model="value" type="text" :class="inputClasses" v-bind="$attrs" :maxlength="maxlength" @blur="v.$touch()" />
|
||||
<span v-if="counter && maxlength" class="inline-flex items-center ml-2">{{ value?.length || 0 }}/{{ maxlength }}</span>
|
||||
<span v-else-if="counter">{{ value?.length || 0 }}</span>
|
||||
<floating-label :label="label" /> </label
|
||||
|
@ -2,31 +2,32 @@
|
||||
import { computed } from "vue";
|
||||
import { FloatingLabel, inputClasses } from "~/composables/useInputHelper";
|
||||
import ErrorTooltip from "~/components/design/ErrorTooltip.vue";
|
||||
import { useValidation } from "~/composables/useValidationHelpers";
|
||||
import { ValidationRule } from "@vuelidate/core";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: string): void;
|
||||
}>();
|
||||
const value = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emit("update:modelValue", v),
|
||||
set: (val) => emit("update:modelValue", val),
|
||||
});
|
||||
const props = defineProps<{
|
||||
modelValue: string;
|
||||
label?: string;
|
||||
errorMessages?: string[];
|
||||
counter?: boolean;
|
||||
maxlength?: number;
|
||||
errorMessages?: string[];
|
||||
rules?: ValidationRule<string | undefined>[];
|
||||
}>();
|
||||
// TODO proper validation
|
||||
const error = computed<boolean>(() => {
|
||||
return props.errorMessages ? props.errorMessages.length > 0 : false;
|
||||
});
|
||||
|
||||
const { v, errors, hasError } = useValidation(props.label, props.rules, value, props.errorMessages);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ErrorTooltip :error-messages="errorMessages" class="w-full">
|
||||
<label class="relative flex" :class="{ filled: modelValue, error: error }">
|
||||
<textarea v-model="value" :class="inputClasses" v-bind="$attrs" :maxlength="maxlength" />
|
||||
<ErrorTooltip :error-messages="errors" class="w-full">
|
||||
<label class="relative flex" :class="{ filled: modelValue, error: hasError }">
|
||||
<textarea v-model="value" :class="inputClasses" v-bind="$attrs" :maxlength="maxlength" @blur="v.$touch()" />
|
||||
<floating-label :label="label" />
|
||||
</label>
|
||||
<div v-if="counter" class="mt-1 mb-2">
|
||||
|
@ -22,6 +22,7 @@ export const FloatingLabel = defineComponent({
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
render() {
|
||||
|
100
frontend-new/src/composables/useValidationHelpers.ts
Normal file
100
frontend-new/src/composables/useValidationHelpers.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import { ErrorObject, useVuelidate, ValidationRule } from "@vuelidate/core";
|
||||
import { computed, Ref } from "vue";
|
||||
import * as validators from "@vuelidate/validators";
|
||||
import { createI18nMessage, helpers, ValidatorWrapper } from "@vuelidate/validators";
|
||||
import { I18n } from "~/i18n";
|
||||
import { useInternalApi } from "~/composables/useApi";
|
||||
|
||||
export function isErrorObject(errorObject: string | ErrorObject): errorObject is ErrorObject {
|
||||
return (<ErrorObject>errorObject).$message !== undefined;
|
||||
}
|
||||
|
||||
export function constructValidators<T>(rules: ValidationRule<T | undefined>[] | undefined, name: string) {
|
||||
// no clue why this cast is needed
|
||||
return rules ? { [name]: rules } : { [name]: {} };
|
||||
}
|
||||
|
||||
export function useValidation<T>(
|
||||
name: string | undefined,
|
||||
rules: ValidationRule<T | undefined>[] | undefined,
|
||||
state: Ref,
|
||||
errorMessages?: string[] | undefined
|
||||
) {
|
||||
const n = name || "val";
|
||||
const v = useVuelidate(constructValidators(rules, n), { [n]: state });
|
||||
const errors = computed(() => {
|
||||
const e = [];
|
||||
if (errorMessages) {
|
||||
e.push(...errorMessages);
|
||||
}
|
||||
if (v.value.$errors) {
|
||||
e.push(...v.value.$errors);
|
||||
}
|
||||
return e;
|
||||
});
|
||||
const hasError = computed(() => (errorMessages && errorMessages.length) || v.value.$error);
|
||||
|
||||
return { v, errors, hasError };
|
||||
}
|
||||
|
||||
export const withI18nMessage = <T extends ValidationRule | ValidatorWrapper>(validator: T, overrideMsg?: string): T => {
|
||||
return createI18nMessage({
|
||||
t: (_, params) => {
|
||||
let msg = "validation." + params.type;
|
||||
const response = params.response;
|
||||
if (response && response.$message) {
|
||||
msg = response.$message;
|
||||
}
|
||||
if (overrideMsg) {
|
||||
msg = overrideMsg;
|
||||
}
|
||||
console.log("translate", msg, overrideMsg, params);
|
||||
return I18n.value.global.t(msg, params);
|
||||
},
|
||||
})(validator, { withArguments: true });
|
||||
};
|
||||
|
||||
function withOverrideMessage<T extends ValidationRule | ValidatorWrapper>(rule: T) {
|
||||
return (overrideMsg?: string) => {
|
||||
return withI18nMessage(rule, overrideMsg);
|
||||
};
|
||||
}
|
||||
|
||||
// basic
|
||||
export const required = withOverrideMessage(validators.required);
|
||||
export const requiredIf = withOverrideMessage(validators.requiredIf);
|
||||
export const minLength = withOverrideMessage(validators.minLength);
|
||||
export const maxLength = withOverrideMessage(validators.maxLength);
|
||||
export const url = withOverrideMessage(validators.url);
|
||||
|
||||
// custom
|
||||
export const validName = withOverrideMessage(
|
||||
(ownerId: string) =>
|
||||
helpers.withParams(
|
||||
{ ownerId, type: "validName" },
|
||||
helpers.withAsync(async (value: string) => {
|
||||
if (!helpers.req(value)) {
|
||||
return { $valid: true };
|
||||
}
|
||||
try {
|
||||
await useInternalApi("projects/validateName", false, "get", {
|
||||
userId: ownerId,
|
||||
value: value,
|
||||
});
|
||||
return { $valid: true };
|
||||
} catch (e) {
|
||||
return !e.response?.data.isHangarApiException ? { $valid: false } : { $valid: false, $message: e.response?.data.message };
|
||||
}
|
||||
})
|
||||
) as ValidationRule<{ ownerId: string }>
|
||||
);
|
||||
|
||||
export const pattern = withOverrideMessage(
|
||||
(regex: string) =>
|
||||
helpers.withParams({ regex, type: "pattern" }, (value: string) => {
|
||||
if (!helpers.req(value)) {
|
||||
return { $valid: true };
|
||||
}
|
||||
return { $valid: new RegExp(regex).test(value) };
|
||||
}) as ValidationRule<{ regex: string }>
|
||||
);
|
@ -2,6 +2,7 @@ import type { App } from "vue";
|
||||
import { createI18n } from "vue-i18n";
|
||||
import { DATE_FORMATS } from "./date-formats";
|
||||
import { DEFAULT_LOCALE, SUPPORTED_LOCALES } from "./locales";
|
||||
import { ref } from "vue";
|
||||
|
||||
export { DEFAULT_LOCALE, SUPPORTED_LOCALES, SUPPORTED_LANGUAGES, extractLocaleFromPath } from "./locales";
|
||||
|
||||
@ -26,6 +27,8 @@ export async function loadAsyncLanguage(i18n: any, locale = DEFAULT_LOCALE) {
|
||||
}
|
||||
}
|
||||
|
||||
export const I18n = ref();
|
||||
|
||||
export async function installI18n(app: App, locale = "") {
|
||||
locale = SUPPORTED_LOCALES.includes(locale) ? locale : DEFAULT_LOCALE;
|
||||
try {
|
||||
@ -44,6 +47,7 @@ export async function installI18n(app: App, locale = "") {
|
||||
});
|
||||
|
||||
app.use(i18n);
|
||||
I18n.value = i18n;
|
||||
} catch (error) {
|
||||
console.log("installI18n error", error);
|
||||
|
||||
@ -55,5 +59,6 @@ export async function installI18n(app: App, locale = "") {
|
||||
datetimeFormats: DATE_FORMATS,
|
||||
});
|
||||
app.use(i18n);
|
||||
I18n.value = i18n;
|
||||
}
|
||||
}
|
||||
|
@ -863,11 +863,12 @@
|
||||
"hangarAuth": "This only change the locale for your current browser (as a cookie). Click here to change your lang on paper auth for all paper services"
|
||||
},
|
||||
"validation": {
|
||||
"required": "{0} is required",
|
||||
"maxLength": "Maximum length is {0}",
|
||||
"required": "{property} is required",
|
||||
"requiredIf": "{property} is required",
|
||||
"maxLength": "Maximum length is {max}",
|
||||
"minLength": "Minimum length is {0}",
|
||||
"invalidFormat": "{0} is invalid",
|
||||
"invalidUrl": "Invalid URL format"
|
||||
"pattern": "{property} is invalid",
|
||||
"url": "Invalid URL format"
|
||||
},
|
||||
"prompts": {
|
||||
"confirm": "Got it!",
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { ProjectOwner, ProjectSettingsForm } from "hangar-internal";
|
||||
import { ProjectCategory } from "~/types/enums";
|
||||
import { handleRequestError } from "~/composables/useErrorHandling";
|
||||
import { computed, reactive, Ref, ref, watch } from "vue";
|
||||
import { computed, Ref, ref } from "vue";
|
||||
import { useInternalApi } from "~/composables/useApi";
|
||||
import { useContext } from "vite-ssr/vue";
|
||||
import { useBackendDataStore } from "~/store/backendData";
|
||||
@ -19,6 +19,8 @@ import Tabs, { Tab } from "~/components/design/Tabs.vue";
|
||||
import Button from "~/components/design/Button.vue";
|
||||
import Markdown from "~/components/Markdown.vue";
|
||||
import InputTextarea from "~/components/ui/InputTextarea.vue";
|
||||
import { useVuelidate } from "@vuelidate/core";
|
||||
import { required, maxLength, validName, pattern, url, requiredIf } from "~/composables/useValidationHelpers";
|
||||
|
||||
interface NewProjectForm extends ProjectSettingsForm {
|
||||
ownerId: ProjectOwner["userId"];
|
||||
@ -35,7 +37,6 @@ const settings = useSettingsStore();
|
||||
|
||||
// TODO move to useApi
|
||||
const projectOwners = await useInternalApi<ProjectOwner[]>("projects/possibleOwners");
|
||||
const nameErrors: Ref<string[]> = ref([]);
|
||||
const projectCreationErrors: Ref<string[]> = ref([]);
|
||||
const projectLoading = ref(true);
|
||||
const form = ref<NewProjectForm>({
|
||||
@ -55,6 +56,13 @@ const converter = ref({
|
||||
loading: false,
|
||||
});
|
||||
|
||||
const rules = {
|
||||
name: {
|
||||
required,
|
||||
},
|
||||
};
|
||||
const v = useVuelidate(rules, form);
|
||||
|
||||
const isCustomLicense = computed(() => form.value.settings.license.type === "(custom)");
|
||||
|
||||
const selectedStep = ref("tos");
|
||||
@ -109,23 +117,6 @@ function createProject() {
|
||||
});
|
||||
}
|
||||
|
||||
const projectName = computed(() => form.value.name);
|
||||
watch(projectName, (newName) => {
|
||||
nameErrors.value = [];
|
||||
if (!newName) {
|
||||
return;
|
||||
}
|
||||
useInternalApi("projects/validateName", false, "get", {
|
||||
userId: form.value.ownerId,
|
||||
value: newName,
|
||||
}).catch((e) => {
|
||||
if (!e.response?.data.isHangarApiException) {
|
||||
return handleRequestError(e, ctx, i18n);
|
||||
}
|
||||
nameErrors.value.push(i18n.t(e.response?.data.message));
|
||||
});
|
||||
});
|
||||
|
||||
function retry() {
|
||||
if (!form.value.pageContent) {
|
||||
form.value.pageContent = "# " + form.value.name + " \nWelcome to your new project!";
|
||||
@ -155,14 +146,37 @@ function retry() {
|
||||
<!-- todo i18n -->
|
||||
<p class="basis-full mb-4">Please provide the basic settings for this project</p>
|
||||
<div class="basis-full md:basis-6/12">
|
||||
<InputSelect v-model="form.ownerId" :values="projectOwners" item-value="id" item-text="name" :label="i18n.t('project.new.step2.userSelect')" />
|
||||
<InputSelect
|
||||
v-model="form.ownerId"
|
||||
:values="projectOwners"
|
||||
item-value="id"
|
||||
item-text="name"
|
||||
:label="i18n.t('project.new.step2.userSelect')"
|
||||
:rules="[required()]"
|
||||
/>
|
||||
</div>
|
||||
<div class="basis-full md:basis-6/12 mt-4 md:mt-0">
|
||||
<InputText v-model.trim="form.name" :error-messages="nameErrors" :label="i18n.t('project.new.step2.projectName')" />
|
||||
<InputText
|
||||
v-model.trim="form.name"
|
||||
:label="i18n.t('project.new.step2.projectName')"
|
||||
:rules="[
|
||||
required(),
|
||||
maxLength()(backendData.validations.project.name.max),
|
||||
pattern()(backendData.validations.project.name.regex),
|
||||
validName()(form.ownerId),
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
<div class="basis-full md:basis-8/12 mt-4">
|
||||
<InputText v-model.trim="form.description" :label="i18n.t('project.new.step2.projectSummary')" :rules="[required()]" />
|
||||
</div>
|
||||
<div class="basis-full md:basis-8/12 mt-4"><InputText v-model.trim="form.description" :label="i18n.t('project.new.step2.projectSummary')" /></div>
|
||||
<div class="basis-full md:basis-4/12 mt-4">
|
||||
<InputSelect v-model="form.category" :values="backendData.categoryOptions" :label="i18n.t('project.new.step2.projectCategory')" />
|
||||
<InputSelect
|
||||
v-model="form.category"
|
||||
:values="backendData.categoryOptions"
|
||||
:label="i18n.t('project.new.step2.projectCategory')"
|
||||
:rules="[required()]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -175,10 +189,10 @@ function retry() {
|
||||
<hr />
|
||||
</div>
|
||||
<div class="flex flex-wrap">
|
||||
<div class="basis-full mt-4"><InputText v-model.trim="form.settings.homepage" :label="i18n.t('project.new.step3.homepage')" /></div>
|
||||
<div class="basis-full mt-4"><InputText v-model.trim="form.settings.issues" :label="i18n.t('project.new.step3.issues')" /></div>
|
||||
<div class="basis-full mt-4"><InputText v-model.trim="form.settings.source" :label="i18n.t('project.new.step3.source')" /></div>
|
||||
<div class="basis-full mt-4"><InputText v-model.trim="form.settings.support" :label="i18n.t('project.new.step3.support')" /></div>
|
||||
<div class="basis-full mt-4"><InputText v-model.trim="form.settings.homepage" :label="i18n.t('project.new.step3.homepage')" :rules="[url()]" /></div>
|
||||
<div class="basis-full mt-4"><InputText v-model.trim="form.settings.issues" :label="i18n.t('project.new.step3.issues')" :rules="[url()]" /></div>
|
||||
<div class="basis-full mt-4"><InputText v-model.trim="form.settings.source" :label="i18n.t('project.new.step3.source')" :rules="[url()]" /></div>
|
||||
<div class="basis-full mt-4"><InputText v-model.trim="form.settings.support" :label="i18n.t('project.new.step3.support')" :rules="[url()]" /></div>
|
||||
</div>
|
||||
<div class="text-lg mt-6">
|
||||
<IconMdiLicense />
|
||||
@ -187,13 +201,18 @@ function retry() {
|
||||
</div>
|
||||
<div class="flex flex-wrap">
|
||||
<div class="basis-full mt-4" :md="isCustomLicense ? 'basis-4/12' : 'basis-6/12'">
|
||||
<InputSelect v-model="form.settings.license.type" :values="backendData.licenseOptions" :label="i18n.t('project.new.step3.type')" />
|
||||
<InputSelect
|
||||
v-model="form.settings.license.type"
|
||||
:values="backendData.licenseOptions"
|
||||
:label="i18n.t('project.new.step3.type')"
|
||||
:rules="[required()]"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="isCustomLicense" class="basis-full md:basis-8/12 mt-4">
|
||||
<InputText v-model.trim="form.settings.license.name" :label="i18n.t('project.new.step3.customName')" />
|
||||
<InputText v-model.trim="form.settings.license.name" :label="i18n.t('project.new.step3.customName')" :rules="[requiredIf()(isCustomLicense)]" />
|
||||
</div>
|
||||
<div class="basis-full mt-4" :md="isCustomLicense ? 'basis-full' : 'basis-6/12'">
|
||||
<InputText v-model.trim="form.settings.license.url" :label="i18n.t('project.new.step3.url')" />
|
||||
<InputText v-model.trim="form.settings.license.url" :label="i18n.t('project.new.step3.url')" :rules="[url()]" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-lg mt-6">
|
||||
@ -202,7 +221,14 @@ function retry() {
|
||||
<hr />
|
||||
</div>
|
||||
<div class="flex">
|
||||
<div class="mt-4 basis-full"><InputTag v-model="form.settings.keywords" :label="i18n.t('project.new.step3.keywords')" /></div>
|
||||
<div class="mt-4 basis-full">
|
||||
<InputTag
|
||||
v-model="form.settings.keywords"
|
||||
:label="i18n.t('project.new.step3.keywords')"
|
||||
:rules="[maxLength()(backendData.validations.project.keywords.max)]"
|
||||
:maxlength="backendData.validations.project.keywords.max"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #import>
|
||||
|
Loading…
Reference in New Issue
Block a user