mirror of
https://github.com/HangarMC/Hangar.git
synced 2025-02-17 15:01:42 +08:00
fix remaining type errors, fix org loading, fix project settings, watch for key changes in data fetching
This commit is contained in:
parent
089a3e396e
commit
b90bd46e74
@ -119,7 +119,7 @@ export default defineNuxtConfig({
|
||||
typedPages: true,
|
||||
},
|
||||
typescript: {
|
||||
// typeCheck: "build", // TODO enable typechecking on build
|
||||
typeCheck: "build",
|
||||
tsConfig: {
|
||||
include: ["./types/typed-router.d.ts"],
|
||||
compilerOptions: {
|
||||
|
@ -20,7 +20,7 @@ const sorters = [
|
||||
{ id: "-newest", label: i18n.t("project.sorting.newest") },
|
||||
];
|
||||
|
||||
const toArray = (input: unknown) => (Array.isArray(input) ? input : input ? [input] : []);
|
||||
const toArray = (input: (string | null)[] | string | null): string[] => (Array.isArray(input) ? (input as string[]) : input ? [input!] : []);
|
||||
const filters = ref({
|
||||
versions: toArray(route.query.version),
|
||||
categories: toArray(route.query.category),
|
||||
@ -38,12 +38,12 @@ const query = ref<string>((route.query.query as string) || "");
|
||||
|
||||
const requestParams = computed(() => {
|
||||
const limit = 10;
|
||||
const params: Record<string, any> = {
|
||||
const params: ReturnType<Parameters<typeof useProjects>[0]> = {
|
||||
limit,
|
||||
offset: page.value * limit,
|
||||
version: filters.value.versions,
|
||||
category: filters.value.categories,
|
||||
platform: filters.value.platform !== null ? [filters.value.platform] : [],
|
||||
platform: filters.value.platform ? [filters.value.platform] : [],
|
||||
tag: filters.value.tags,
|
||||
};
|
||||
if (query.value) {
|
||||
@ -55,7 +55,7 @@ const requestParams = computed(() => {
|
||||
|
||||
return params;
|
||||
});
|
||||
const { projects } = useProjects(() => requestParams.value, router);
|
||||
const { projects, refreshProjects } = useProjects(() => requestParams.value, router);
|
||||
|
||||
// if somebody set page too high, lets reset it back
|
||||
watch(projects, () => {
|
||||
@ -63,6 +63,12 @@ watch(projects, () => {
|
||||
page.value = 0;
|
||||
}
|
||||
});
|
||||
// for some reason the watch in useProjects doesn't work for filters 🤷♂️
|
||||
watch(
|
||||
() => filters.value.versions,
|
||||
() => refreshProjects(),
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
function versions(platform: Platform): PlatformVersion[] {
|
||||
const platformData = useBackendData.platforms?.get(platform);
|
||||
@ -76,9 +82,9 @@ function versions(platform: Platform): PlatformVersion[] {
|
||||
function updatePlatform(platform: any) {
|
||||
filters.value.platform = platform;
|
||||
|
||||
const allowedVersion: PlatformVersion[] = versions(platform);
|
||||
const allowedVersion = versions(platform);
|
||||
filters.value.versions = filters.value.versions.filter((existingVersion) => {
|
||||
return allowedVersion.find((allowedNewVersion) => allowedNewVersion === existingVersion);
|
||||
return allowedVersion.find((allowedNewVersion) => allowedNewVersion.version === existingVersion);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,10 @@ function remove(index: number) {
|
||||
model.value.splice(index, 1);
|
||||
}
|
||||
function add() {
|
||||
if (!model.value) {
|
||||
model.value = [{ id: 0, name: "", url: "" }];
|
||||
return;
|
||||
}
|
||||
let nextId = Math.max(...model.value.map((l) => l.id)) + 1;
|
||||
if (nextId === -Infinity) {
|
||||
nextId = 0;
|
||||
|
@ -50,10 +50,28 @@ export function usePossibleAlts(user: () => string) {
|
||||
return { possibleAlts, possibleAltsStatus };
|
||||
}
|
||||
|
||||
export function useProjects(params: () => { member?: string; limit?: number; offset?: number; q?: string; owner?: string }, router?: Router) {
|
||||
const { data: projects, status: projectsStatus } = useData(
|
||||
export function useProjects(
|
||||
params: () => {
|
||||
member?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
query?: string;
|
||||
owner?: string;
|
||||
version?: string[];
|
||||
category?: string[];
|
||||
platform?: Platform[];
|
||||
tag?: string[];
|
||||
sort?: string;
|
||||
},
|
||||
router?: Router
|
||||
) {
|
||||
const {
|
||||
data: projects,
|
||||
status: projectsStatus,
|
||||
refresh: refreshProjects,
|
||||
} = useData(
|
||||
params,
|
||||
(p) => "projects:" + p,
|
||||
(p) => "projects:" + (p.member || p.owner || "main") + ":" + p.offset,
|
||||
(p) => useApi<PaginatedResultProject>("projects", "get", { ...p }),
|
||||
true,
|
||||
() => false,
|
||||
@ -63,7 +81,7 @@ export function useProjects(params: () => { member?: string; limit?: number; off
|
||||
}
|
||||
}
|
||||
);
|
||||
return { projects, projectsStatus };
|
||||
return { projects, projectsStatus, refreshProjects };
|
||||
}
|
||||
|
||||
export function useStarred(user: () => string) {
|
||||
@ -214,8 +232,8 @@ export function usePossiblePerms(user: () => string) {
|
||||
export function useAdminStats(params: () => { from: string; to: string }) {
|
||||
const { data: adminStats, status: adminStatsStatus } = useData(
|
||||
params,
|
||||
() => "adminStats:" + params().from + params().to,
|
||||
(params) => useInternalApi<DayStats[]>("admin/stats", "get", params)
|
||||
(p) => "adminStats:" + p.from + ":" + p.to,
|
||||
(p) => useInternalApi<DayStats[]>("admin/stats", "get", p)
|
||||
);
|
||||
return { adminStats, adminStatsStatus };
|
||||
}
|
||||
@ -272,7 +290,7 @@ export function useUser(userName: () => string) {
|
||||
export function useUsers(params: () => { query?: string; limit?: number; offset?: number; sort?: string[] }) {
|
||||
const { data: users, status: usersStatus } = useData(
|
||||
params,
|
||||
() => "users",
|
||||
(p) => "users:" + p.query + ":" + p.offset + ":" + p.sort,
|
||||
(p) => useApi<PaginatedResultUser>("users", "get", p)
|
||||
);
|
||||
return { users, usersStatus };
|
||||
@ -284,7 +302,7 @@ export function useActionLogs(
|
||||
) {
|
||||
const { data: actionLogs, status: actionLogsStatus } = useData(
|
||||
params,
|
||||
() => "actionLogs",
|
||||
(p) => "actionLogs:" + p.offset + ":" + p.sort + ":" + p.user + ":" + p.logAction + ":" + p.authorName + ":" + p.projectSlug,
|
||||
(p) => useInternalApi<PaginatedResultHangarLoggedAction>("admin/log", "get", p),
|
||||
true,
|
||||
() => false,
|
||||
@ -300,7 +318,7 @@ export function useActionLogs(
|
||||
export function useStaff(params: () => { offset?: number; limit?: number; sort?: string[]; query?: string }) {
|
||||
const { data: staff, status: staffStatus } = useData(
|
||||
params,
|
||||
() => "staff",
|
||||
(p) => "staff:" + p.offset + ":" + p.sort + ":" + p.query,
|
||||
(p) => useApi<PaginatedResultUser>("staff", "GET", p)
|
||||
);
|
||||
return { staff, staffStatus };
|
||||
@ -309,7 +327,7 @@ export function useStaff(params: () => { offset?: number; limit?: number; sort?:
|
||||
export function useAuthors(params: () => { offset?: number; limit?: number; sort?: string[]; query?: string }) {
|
||||
const { data: authors, status: authorStatus } = useData(
|
||||
params,
|
||||
() => "authors",
|
||||
(p) => "authors:" + p.offset + ":" + p.sort + ":" + p.query,
|
||||
(p) => useApi<PaginatedResultUser>("authors", "GET", p)
|
||||
);
|
||||
return { authors, authorStatus };
|
||||
@ -375,7 +393,7 @@ export function useProjectVersions(
|
||||
) {
|
||||
const { data: versions, status: versionsStatus } = useData(
|
||||
params,
|
||||
(p) => "versions:" + p.project,
|
||||
(p) => "versions:" + p.project + ":" + p.data.offset + ":" + p.data.channel + ":" + p.data.platform + ":" + p.data.includeHiddenChannels,
|
||||
(p) => useApi<PaginatedResultVersion>(`projects/${p.project}/versions`, "GET", p.data),
|
||||
true,
|
||||
() => false,
|
||||
|
@ -1,43 +1,49 @@
|
||||
import type { RouteLocationNormalized } from "vue-router";
|
||||
import type { ExtendedProjectPage, HangarOrganization, HangarProject, HangarVersion, User } from "~/types/backend";
|
||||
|
||||
type dataLoaders = "user" | "project" | "version" | "organization" | "page";
|
||||
type routeParams = "user" | "project" | "version" | "organization" | "page";
|
||||
type routeParams = "user" | "project" | "version" | "page";
|
||||
type DataLoaderTypes = {
|
||||
user: User;
|
||||
project: HangarProject;
|
||||
version: HangarVersion;
|
||||
organization: HangarOrganization;
|
||||
page: ExtendedProjectPage;
|
||||
};
|
||||
|
||||
// TODO check every handling of the reject stuff (for both composables)
|
||||
|
||||
export function useDataLoader<T>(key: dataLoaders) {
|
||||
const data = useState<T | undefined>(key);
|
||||
export function useDataLoader<K extends keyof DataLoaderTypes>(key: K) {
|
||||
const data = useState<DataLoaderTypes[K] | undefined>(key);
|
||||
|
||||
function loader(
|
||||
param: routeParams,
|
||||
to: RouteLocationNormalized,
|
||||
from: RouteLocationNormalized,
|
||||
loader: (param: string) => Promise<T>,
|
||||
loader: (param: string) => Promise<DataLoaderTypes[K]>,
|
||||
promises: Promise<any>[]
|
||||
) {
|
||||
const meta = to.meta["dataLoader_" + key];
|
||||
if (meta) {
|
||||
const oldParam = key in from.params ? (from.params[param as never] as string) : undefined;
|
||||
const newParam = key in to.params ? (to.params[param as never] as string) : undefined;
|
||||
const oldParam = param in from.params ? (from.params[param as never] as string) : undefined;
|
||||
const newParam = param in to.params ? (to.params[param as never] as string) : undefined;
|
||||
if (data.value && oldParam === newParam) {
|
||||
console.log("skip loading", key); // TODO test this
|
||||
return newParam;
|
||||
} else if (newParam) {
|
||||
promises.push(
|
||||
new Promise<void>(async (resolve, reject) => {
|
||||
console.log("load loading", param);
|
||||
console.log("load loading", key);
|
||||
const result = await loader(newParam).catch(reject);
|
||||
// await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
if (result) {
|
||||
data.value = result;
|
||||
console.log("load loaded", param);
|
||||
console.log("load loaded", key);
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
);
|
||||
return newParam;
|
||||
}
|
||||
console.log("dataLoader " + key + " is miss configured for " + to.path + "!");
|
||||
console.warn("dataLoader " + key + " is miss configured for " + to.path + "! (no param " + param + ")");
|
||||
return undefined;
|
||||
} else {
|
||||
data.value = undefined;
|
||||
@ -57,8 +63,13 @@ export function useData<T, P extends Record<string, unknown> | string>(
|
||||
callback: (params: P) => void = () => {},
|
||||
defaultValue: T | undefined = undefined
|
||||
) {
|
||||
// TODO make this reactive somehow
|
||||
const data = useState<T | undefined>(key(params()));
|
||||
// state tracking is twofold.
|
||||
// `state` is used store data in the nuxt payload, so it will be shared between server and client side and on client side navigation
|
||||
const state = useState<Record<string, T | undefined>>("useData", () => ({}));
|
||||
// `data` is used to store a reference into the state, using the current key. it points to the data we want to return
|
||||
// we are not using a computed here, since consumers might manually want to update the data. this kinda corrupts the cache, but we can't do much about it
|
||||
const data = ref<T | undefined>();
|
||||
|
||||
const status = ref<"idle" | "loading" | "success" | "error">("idle");
|
||||
let promise: Promise<void> | undefined;
|
||||
|
||||
@ -66,17 +77,18 @@ export function useData<T, P extends Record<string, unknown> | string>(
|
||||
return load(params());
|
||||
}
|
||||
|
||||
function setState(newState?: T) {
|
||||
state.value[key(params())] = newState;
|
||||
data.value = newState;
|
||||
}
|
||||
|
||||
if (import.meta.server && !server) {
|
||||
return { data, status, refresh };
|
||||
}
|
||||
|
||||
function load(params: P) {
|
||||
status.value = "loading";
|
||||
if (defaultValue) {
|
||||
data.value = defaultValue;
|
||||
} else {
|
||||
data.value = undefined;
|
||||
}
|
||||
setState(defaultValue || undefined);
|
||||
|
||||
if (skip(params)) {
|
||||
console.log("skip", key(params));
|
||||
@ -89,7 +101,7 @@ export function useData<T, P extends Record<string, unknown> | string>(
|
||||
//await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
console.log("loaded", key(params));
|
||||
if (result) {
|
||||
data.value = result;
|
||||
setState(result);
|
||||
status.value = "success";
|
||||
callback(params);
|
||||
resolve();
|
||||
@ -99,9 +111,13 @@ export function useData<T, P extends Record<string, unknown> | string>(
|
||||
});
|
||||
}
|
||||
|
||||
// load initial state
|
||||
data.value = state.value[key(params())];
|
||||
// if we have no state, queue a load
|
||||
if (!data.value) {
|
||||
promise = load(params());
|
||||
|
||||
// if on server (and we dont wanna skip server fetching, we need await the promise onServerPrefetch)
|
||||
if (import.meta.server && server && promise) {
|
||||
onServerPrefetch(async () => {
|
||||
console.log("server prefetch", key(params()));
|
||||
@ -110,6 +126,22 @@ export function useData<T, P extends Record<string, unknown> | string>(
|
||||
}
|
||||
}
|
||||
|
||||
// when the key changes, we move the data from the old key to the new key
|
||||
watch(
|
||||
() => key(params()),
|
||||
(newKey, oldKey) => {
|
||||
if (newKey === oldKey) {
|
||||
return;
|
||||
}
|
||||
const oldState = state.value[oldKey];
|
||||
state.value[newKey] = oldState;
|
||||
state.value[oldKey] = undefined;
|
||||
data.value = oldState;
|
||||
console.log("watchKey", newKey, oldKey);
|
||||
}
|
||||
);
|
||||
|
||||
// when the params change, we load the new data
|
||||
watchDebounced(
|
||||
params,
|
||||
(newParams, oldParams) => {
|
||||
|
@ -73,11 +73,11 @@ function _handleRequestError(err: AxiosError | H3Error | unknown, i18n?: Compose
|
||||
});
|
||||
} else if ("response" in err && typeof err.response?.data === "object" && err.response.data) {
|
||||
_handleErrorResponse(err.response.data, i18n);
|
||||
} else if (err.cause?.response?.data) {
|
||||
_handleErrorResponse(err.cause.response.data, i18n);
|
||||
} else if (err.cause?.statusCode) {
|
||||
} else if ((err.cause as any)?.response?.data) {
|
||||
_handleErrorResponse((err.cause as any).response.data, i18n);
|
||||
} else if ((err.cause as any)?.statusCode) {
|
||||
// this error was rethrown, lets inform nuxt
|
||||
showError(err.cause);
|
||||
showError(err.cause as any);
|
||||
} else {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
|
@ -91,9 +91,12 @@ export const maxFileSize = withOverrideMessage((maxSize: number) =>
|
||||
})
|
||||
);
|
||||
|
||||
export const noDuplicated = withOverrideMessage((elements: any[] | (() => any[])) =>
|
||||
export const noDuplicated = withOverrideMessage((elements: any[] | (() => any[] | undefined) | undefined) =>
|
||||
helpers.withParams({ elements, type: "noDuplicated" }, () => {
|
||||
const els = typeof elements === "function" ? elements() : unref(elements);
|
||||
if (!els) {
|
||||
return { $valid: true };
|
||||
}
|
||||
return { $valid: new Set(els).size === els.length };
|
||||
})
|
||||
);
|
||||
|
@ -7,6 +7,10 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||
console.log("hit vite path???????????????????????", to.fullPath);
|
||||
return;
|
||||
}
|
||||
// don't call on router.replace when we just update the query
|
||||
if (import.meta.client && to.path === from?.path) {
|
||||
return;
|
||||
}
|
||||
|
||||
await useAuth.updateUser();
|
||||
await loadRoutePerms(to);
|
||||
|
@ -2,12 +2,15 @@ import type { ExtendedProjectPage, HangarOrganization, HangarProject, HangarVers
|
||||
import { useDataLoader } from "~/composables/useDataLoader";
|
||||
import { isAxiosError } from "axios";
|
||||
|
||||
// this middleware takes care of fetching the "important" data for pages, like user/project/org/version/page, based on route params
|
||||
// it also handles 404s and redirects to the proper casing
|
||||
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||
// don't call on router.replace when we just update the query
|
||||
if (import.meta.client && to.path === from?.path) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if we are in an error state, dont waste time loading this data
|
||||
const error = useError();
|
||||
if (error.value) {
|
||||
return;
|
||||
@ -22,13 +25,7 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||
const projectName = projectLoader("project", to, from, async (projectName) => useInternalApi<HangarProject>("projects/project/" + projectName), promises);
|
||||
|
||||
const { loader: organizationLoader } = useDataLoader<HangarOrganization>("organization");
|
||||
organizationLoader(
|
||||
"organization",
|
||||
to,
|
||||
from,
|
||||
async (organizationName) => useInternalApi<HangarOrganization>("organizations/org/" + organizationName),
|
||||
promises
|
||||
);
|
||||
organizationLoader("user", to, from, async (organizationName) => useInternalApi<HangarOrganization>("organizations/org/" + organizationName), promises);
|
||||
|
||||
const { loader: versionLoader, data: version } = useDataLoader<HangarVersion>("version");
|
||||
const versionName = versionLoader(
|
||||
|
@ -1,11 +1,12 @@
|
||||
<script lang="ts" setup>
|
||||
import { useDataLoader } from "~/composables/useDataLoader";
|
||||
import type { User } from "~/types/backend";
|
||||
|
||||
const { data: user } = useDataLoader<User>("user");
|
||||
const { data: user } = useDataLoader("user");
|
||||
const { data: org } = useDataLoader("organization");
|
||||
|
||||
definePageMeta({
|
||||
dataLoader_user: true,
|
||||
dataLoader_organization: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -14,8 +15,7 @@ definePageMeta({
|
||||
<router-view v-slot="{ Component }">
|
||||
<Suspense>
|
||||
<div>
|
||||
<!-- todo fix org -->
|
||||
<component :is="Component" :user="user" :organization="undefined" />
|
||||
<component :is="Component" :user="user" :organization="org" />
|
||||
</div>
|
||||
</Suspense>
|
||||
</router-view>
|
||||
|
@ -1,12 +1,12 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HangarProject, HangarProjectPage, User } from "~/types/backend";
|
||||
import type { HangarProjectPage, User } from "~/types/backend";
|
||||
import { useDataLoader } from "~/composables/useDataLoader";
|
||||
|
||||
defineProps<{
|
||||
user?: User;
|
||||
}>();
|
||||
|
||||
const { data: project } = useDataLoader<HangarProject>("project");
|
||||
const { data: project } = useDataLoader("project");
|
||||
|
||||
definePageMeta({
|
||||
dataLoader_project: true,
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { type ExtendedProjectPage, type HangarProject, NamedPermission, type User } from "~/types/backend";
|
||||
import { type HangarProject, NamedPermission, type User } from "~/types/backend";
|
||||
import { useDataLoader } from "~/composables/useDataLoader";
|
||||
|
||||
const props = defineProps<{
|
||||
@ -9,7 +9,7 @@ const props = defineProps<{
|
||||
|
||||
const route = useRoute("user-project-pages-page");
|
||||
|
||||
const { data: page } = useDataLoader<ExtendedProjectPage>("page");
|
||||
const { data: page } = useDataLoader("page");
|
||||
|
||||
definePageMeta({
|
||||
dataLoader_page: true,
|
||||
|
@ -4,7 +4,16 @@ import { useVuelidate } from "@vuelidate/core";
|
||||
import { Cropper, type CropperResult } from "vue-advanced-cropper";
|
||||
import type { Tab } from "~/types/components/design/Tabs";
|
||||
import InputText from "~/components/ui/InputText.vue";
|
||||
import { type HangarProject, type HangarUser, NamedPermission, type PaginatedResultUser, Tag, Visibility } from "~/types/backend";
|
||||
import {
|
||||
Category,
|
||||
type HangarProject,
|
||||
type HangarUser,
|
||||
NamedPermission,
|
||||
type PaginatedResultUser,
|
||||
type ProjectSettings,
|
||||
Tag,
|
||||
Visibility,
|
||||
} from "~/types/backend";
|
||||
|
||||
import "vue-advanced-cropper/dist/style.css";
|
||||
|
||||
@ -34,16 +43,27 @@ if (hasPerms(NamedPermission.IsSubjectOwner) || hasPerms(NamedPermission.DeleteP
|
||||
}
|
||||
|
||||
const form = reactive({
|
||||
settings: cloneDeep(props.project.settings),
|
||||
description: props.project.description,
|
||||
category: props.project.category,
|
||||
});
|
||||
if (!form.settings.license.type) {
|
||||
form.settings.license.type = "Unspecified";
|
||||
}
|
||||
if (!form.settings.links) {
|
||||
form.settings.links = [];
|
||||
}
|
||||
settings: undefined,
|
||||
description: undefined,
|
||||
category: undefined,
|
||||
} as { settings?: ProjectSettings; description?: string; category?: Category });
|
||||
|
||||
watch(
|
||||
() => props.project,
|
||||
(val) => {
|
||||
form.settings = cloneDeep(val?.settings);
|
||||
form.description = val?.description;
|
||||
form.category = val?.category;
|
||||
|
||||
if (form.settings && !form.settings?.license?.type) {
|
||||
form.settings.license.type = "Unspecified";
|
||||
}
|
||||
if (form.settings && !form.settings?.links) {
|
||||
form.settings.links = [];
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const hasCustomIcon = computed(() => props.project?.avatarUrl?.includes("project"));
|
||||
const projectIcon = ref<File | null>(null);
|
||||
@ -84,8 +104,8 @@ const loading = reactive({
|
||||
transfer: false,
|
||||
});
|
||||
|
||||
const isCustomLicense = computed(() => form.settings.license.type === "Other");
|
||||
const isUnspecifiedLicense = computed(() => form.settings.license.type === "Unspecified");
|
||||
const isCustomLicense = computed(() => form.settings?.license?.type === "Other");
|
||||
const isUnspecifiedLicense = computed(() => form.settings?.license?.type === "Unspecified");
|
||||
|
||||
watch(route, (val) => (selectedTab.value = val.params.slug?.[0] || "general"), { deep: true });
|
||||
watch(selectedTab, (val) => router.replace("/" + route.params.user + "/" + route.params.project + "/settings/" + val));
|
||||
@ -106,10 +126,10 @@ async function save() {
|
||||
if (!(await v.value.$validate())) return;
|
||||
loading.save = true;
|
||||
try {
|
||||
if (!isCustomLicense.value) {
|
||||
if (form.settings && !isCustomLicense.value) {
|
||||
form.settings.license.name = undefined as unknown as string;
|
||||
}
|
||||
if (isUnspecifiedLicense.value) {
|
||||
if (form.settings && isUnspecifiedLicense.value) {
|
||||
form.settings.license.url = undefined;
|
||||
}
|
||||
|
||||
@ -252,35 +272,44 @@ useHead(useSeo(i18n.t("project.settings.title") + " | " + props.project?.name, p
|
||||
</ProjectSettingsSection>
|
||||
<ProjectSettingsSection title="project.settings.keywords" description="project.settings.keywordsSub">
|
||||
<InputTag
|
||||
v-if="form.settings"
|
||||
v-model="form.settings.keywords"
|
||||
counter
|
||||
:maxlength="useBackendData.validations?.project?.keywords?.max || 5"
|
||||
:tag-maxlength="useBackendData.validations?.project?.keywordName?.max || 16"
|
||||
:label="i18n.t('project.new.step3.keywords')"
|
||||
:rules="[maxLength()(useBackendData.validations?.project?.keywords?.max || 5), noDuplicated()(() => form.settings.keywords)]"
|
||||
:rules="[maxLength()(useBackendData.validations?.project?.keywords?.max || 5), noDuplicated()(() => form.settings?.keywords)]"
|
||||
/>
|
||||
</ProjectSettingsSection>
|
||||
<ProjectSettingsSection title="project.settings.tags.title" description="project.settings.tagsSub">
|
||||
<InputCheckbox v-for="tag in Object.values(Tag)" :key="tag" v-model="form.settings.tags" :value="tag">
|
||||
<template #label>
|
||||
<IconMdiPuzzleOutline v-if="tag === Tag.ADDON" />
|
||||
<IconMdiBookshelf v-else-if="tag === Tag.LIBRARY" />
|
||||
<IconMdiLeaf v-else-if="tag === Tag.SUPPORTS_FOLIA" />
|
||||
<span class="ml-1">{{ i18n.t("project.settings.tags." + tag + ".title") }}</span>
|
||||
<Tooltip>
|
||||
<template #content> {{ i18n.t("project.settings.tags." + tag + ".description") }} </template>
|
||||
<IconMdiHelpCircleOutline class="ml-1 text-gray-500 dark:text-gray-400 text-sm" />
|
||||
</Tooltip>
|
||||
</template>
|
||||
</InputCheckbox>
|
||||
<template v-if="form.settings">
|
||||
<InputCheckbox v-for="tag in Object.values(Tag)" :key="tag" v-model="form.settings.tags" :value="tag">
|
||||
<template #label>
|
||||
<IconMdiPuzzleOutline v-if="tag === Tag.ADDON" />
|
||||
<IconMdiBookshelf v-else-if="tag === Tag.LIBRARY" />
|
||||
<IconMdiLeaf v-else-if="tag === Tag.SUPPORTS_FOLIA" />
|
||||
<span class="ml-1">{{ i18n.t("project.settings.tags." + tag + ".title") }}</span>
|
||||
<Tooltip>
|
||||
<template #content> {{ i18n.t("project.settings.tags." + tag + ".description") }} </template>
|
||||
<IconMdiHelpCircleOutline class="ml-1 text-gray-500 dark:text-gray-400 text-sm" />
|
||||
</Tooltip>
|
||||
</template>
|
||||
</InputCheckbox>
|
||||
</template>
|
||||
</ProjectSettingsSection>
|
||||
<ProjectSettingsSection title="project.settings.license" description="project.settings.licenseSub">
|
||||
<div class="flex md:gap-2 lt-md:flex-wrap">
|
||||
<div class="basis-full" :md="isCustomLicense ? 'basis-4/12' : 'basis-6/12'">
|
||||
<InputSelect v-model="form.settings.license.type" :values="useLicenseOptions" :label="i18n.t('project.settings.licenseType')" />
|
||||
<InputSelect
|
||||
v-if="form.settings"
|
||||
v-model="form.settings.license.type"
|
||||
:values="useLicenseOptions"
|
||||
:label="i18n.t('project.settings.licenseType')"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="isCustomLicense" class="basis-full md:basis-8/12">
|
||||
<InputText
|
||||
v-if="form.settings"
|
||||
v-model.trim="form.settings.license.name"
|
||||
:label="i18n.t('project.settings.licenseCustom')"
|
||||
:rules="[
|
||||
@ -291,7 +320,7 @@ useHead(useSeo(i18n.t("project.settings.title") + " | " + props.project?.name, p
|
||||
/>
|
||||
</div>
|
||||
<div v-if="!isUnspecifiedLicense" class="basis-full" :md="isCustomLicense ? 'basis-full' : 'basis-6/12'">
|
||||
<InputText v-model.trim="form.settings.license.url" :label="i18n.t('project.settings.licenseUrl')" :rules="[validUrl()]" />
|
||||
<InputText v-if="form.settings" v-model.trim="form.settings.license.url" :label="i18n.t('project.settings.licenseUrl')" :rules="[validUrl()]" />
|
||||
</div>
|
||||
</div>
|
||||
</ProjectSettingsSection>
|
||||
@ -342,7 +371,7 @@ useHead(useSeo(i18n.t("project.settings.title") + " | " + props.project?.name, p
|
||||
</template>
|
||||
<template #links>
|
||||
<ProjectSettingsSection title="project.settings.links.title" description="project.settings.links.sub">
|
||||
<ProjectLinksForm v-model="form.settings.links" />
|
||||
<ProjectLinksForm v-if="form.settings" v-model="form.settings.links" />
|
||||
</ProjectSettingsSection>
|
||||
</template>
|
||||
<template #management>
|
||||
|
@ -6,7 +6,7 @@ defineProps<{
|
||||
project?: HangarProject;
|
||||
}>();
|
||||
|
||||
const { data: version } = useDataLoader<HangarVersion>("version");
|
||||
const { data: version } = useDataLoader("version");
|
||||
|
||||
definePageMeta({
|
||||
dataLoader_version: true,
|
||||
|
Loading…
Reference in New Issue
Block a user