mirror of
https://github.com/HangarMC/Hangar.git
synced 2025-01-30 14:30:08 +08:00
implement settings page
it was painful but it is done, just needs a bit more styling
This commit is contained in:
parent
53580da032
commit
81f4c679cf
@ -153,10 +153,10 @@ once QA has passed, the checkboxes can be removed and the page can be ~~striked
|
||||
- [x] design
|
||||
- [ ] qa
|
||||
* settings
|
||||
- [ ] fetch
|
||||
- [ ] layout
|
||||
- [ ] functionality
|
||||
- [ ] design
|
||||
- [x] fetch
|
||||
- [x] layout
|
||||
- [x] functionality
|
||||
- [ ] design (needs styling the tabs)
|
||||
- [ ] qa
|
||||
* stars
|
||||
- [x] fetch
|
||||
@ -192,9 +192,9 @@ once QA has passed, the checkboxes can be removed and the page can be ~~striked
|
||||
- [ ] qa
|
||||
- [version]
|
||||
- [x] fetch
|
||||
- [ ] layout (do we wanna do tabs again?)
|
||||
- [x] layout
|
||||
- [x] functionality
|
||||
- [ ] design
|
||||
- [x] design
|
||||
- [ ] qa
|
||||
* [platform] (empty)
|
||||
- index
|
||||
@ -257,7 +257,7 @@ once QA has passed, the checkboxes can be removed and the page can be ~~striked
|
||||
- versions
|
||||
- [x] fetch
|
||||
- [x] layout
|
||||
- [ ] functionality (expansion missing)
|
||||
- [x] functionality
|
||||
- [x] design
|
||||
- [ ] qa
|
||||
- user (empty)
|
||||
|
@ -0,0 +1,39 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import Link from "~/components/design/Link.vue";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: string): void;
|
||||
}>();
|
||||
const internalValue = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit("update:modelValue", value),
|
||||
});
|
||||
|
||||
interface Tab {
|
||||
value: string;
|
||||
header: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string;
|
||||
tabs: Tab[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex gap-2">
|
||||
<div>
|
||||
<ul>
|
||||
<li v-for="tab in tabs" :key="tab.value">
|
||||
<Link :href="'#' + tab.value" @click.prevent="internalValue = tab.value">{{ tab.header }}</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<template v-for="tab in tabs" :key="tab.value">
|
||||
<slot v-if="internalValue === tab.value" :name="tab.value" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -0,0 +1,39 @@
|
||||
<script lang="ts" setup>
|
||||
import { TranslateResult, useI18n } from "vue-i18n";
|
||||
import Button from "~/components/design/Button.vue";
|
||||
import Modal from "~/components/modals/Modal.vue";
|
||||
import { ref } from "vue";
|
||||
import InputTextarea from "~/components/ui/InputTextarea.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
title: string | TranslateResult;
|
||||
label: string;
|
||||
submit: (msg: string) => Promise<void>;
|
||||
}>();
|
||||
|
||||
const message = ref("");
|
||||
const loading = ref(false);
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
async function _submit(close: () => void) {
|
||||
loading.value = true;
|
||||
await props.submit(message.value);
|
||||
loading.value = false;
|
||||
close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="props.title">
|
||||
<template #default="{ on }">
|
||||
<InputTextarea v-model.trim="message" :label="label" :rows="2" @keydown.enter.prevent="" />
|
||||
|
||||
<Button class="mt-2" v-on="on">{{ i18n.t("general.close") }}</Button>
|
||||
<Button class="mt-2 ml-2" :disabled="loading" @click="_submit(on.click)">{{ i18n.t("general.submit") }}</Button>
|
||||
</template>
|
||||
<template #activator="{ on }">
|
||||
<slot name="activator" :on="on"></slot>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
@ -53,8 +53,8 @@ async function submit(closeModal: () => void): Promise<void> {
|
||||
<Button class="mt-2 ml-2" @click="submit(on.click)">{{ i18n.t("general.submit") }}</Button>
|
||||
</template>
|
||||
<template #activator="{ on }">
|
||||
<Button v-bind="$attrs" class="mr-1" v-on="on">
|
||||
<IconMdiEye />
|
||||
<Button v-bind="$attrs" class="inline-flex items-center mr-1" v-on="on">
|
||||
<IconMdiEye class="mr-1" />
|
||||
{{ i18n.t("visibility.modal.activatorBtn") }}
|
||||
</Button>
|
||||
</template>
|
||||
|
@ -0,0 +1,20 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", file: string): void;
|
||||
}>();
|
||||
const file = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit("update:modelValue", value),
|
||||
});
|
||||
const props = defineProps<{
|
||||
modelValue: string;
|
||||
showSize?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- todo make fancy, implement functionality -->
|
||||
<input type="file" v-bind="$attrs" />
|
||||
</template>
|
@ -2,7 +2,7 @@
|
||||
import { computed } from "vue";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: object | string | boolean | number | null): void;
|
||||
(e: "update:modelValue", value: object | string | boolean | number | null | undefined): void;
|
||||
}>();
|
||||
const internalVal = computed({
|
||||
get: () => props.modelValue,
|
||||
@ -15,7 +15,7 @@ export interface Option {
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: object | string | boolean | number | null;
|
||||
modelValue: object | string | boolean | number | null | undefined;
|
||||
values: Option[];
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
|
@ -11,14 +11,19 @@ const tags = computed({
|
||||
});
|
||||
const props = defineProps<{
|
||||
modelValue: string[];
|
||||
label?: string;
|
||||
counter?: boolean;
|
||||
maxlength?: number;
|
||||
}>();
|
||||
|
||||
if (!tags.value) tags.value = [];
|
||||
|
||||
function remove(t: string) {
|
||||
tags.value = tags.value.filter((v) => v != t);
|
||||
}
|
||||
|
||||
function add() {
|
||||
if (tag.value) {
|
||||
if (tag.value && (!props.maxlength || tags.value.length < props.maxlength)) {
|
||||
tags.value.push(tag.value);
|
||||
tag.value = "";
|
||||
}
|
||||
@ -26,12 +31,15 @@ function add() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<label>
|
||||
<span v-if="label">{{ label }}</span>
|
||||
<span v-for="t in tags" :key="t" class="bg-gray-200 rounded-4xl px-2 py-1 mx-1 inline-flex items-center" dark="text-black">
|
||||
{{ t }}
|
||||
<button class="text-gray-400 ml-1 inline-flex" hover="text-gray-500" @click="remove(t)"><icon-mdi-close-circle /></button>
|
||||
</span>
|
||||
<!-- todo style the input -->
|
||||
<input v-model="tag" type="text" dark="text-black" @keydown.enter="add" />
|
||||
</div>
|
||||
<span v-if="counter && maxlength">{{ tags?.length || 0 }}/{{ maxlength }}</span>
|
||||
<span v-else-if="counter">{{ tags?.length || 0 }}</span>
|
||||
</label>
|
||||
</template>
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { computed } from "vue";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: string): void;
|
||||
(e: "update:modelValue", value?: string): void;
|
||||
}>();
|
||||
const value = computed({
|
||||
get: () => props.modelValue,
|
||||
|
@ -2,16 +2,187 @@
|
||||
import { useHead } from "@vueuse/head";
|
||||
import { useSeo } from "~/composables/useSeo";
|
||||
import { projectIconUrl } from "~/composables/useUrlHelper";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { HangarProject } from "hangar-internal";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import Card from "~/components/design/Card.vue";
|
||||
import MemberList from "~/components/projects/MemberList.vue";
|
||||
import VisibilityChangerModal from "~/components/modals/VisibilityChangerModal.vue";
|
||||
import { hasPerms } from "~/composables/usePerm";
|
||||
import { NamedPermission } from "~/types/enums";
|
||||
import Button from "~/components/design/Button.vue";
|
||||
import Tabs from "~/components/design/Tabs.vue";
|
||||
import { computed, reactive, ref, watch } from "vue";
|
||||
import InputSelect, { Option } from "~/components/ui/InputSelect.vue";
|
||||
import { useBackendDataStore } from "~/store/backendData";
|
||||
import InputText from "~/components/ui/InputText.vue";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import InputCheckbox from "~/components/ui/InputCheckbox.vue";
|
||||
import InputFile from "~/components/ui/InputFile.vue";
|
||||
import { useInternalApi } from "~/composables/useApi";
|
||||
import { handleRequestError } from "~/composables/useErrorHandling";
|
||||
import { useContext } from "vite-ssr/vue";
|
||||
import { useNotificationStore } from "~/store/notification";
|
||||
import InputTag from "~/components/ui/InputTag.vue";
|
||||
import TextAreaModal from "~/components/modals/TextAreaModal.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const i18n = useI18n();
|
||||
const ctx = useContext();
|
||||
const backendData = useBackendDataStore();
|
||||
const props = defineProps<{
|
||||
project: HangarProject;
|
||||
}>();
|
||||
|
||||
const selectedTab = ref(route.hash.substring(1) || "general");
|
||||
const tabs = [
|
||||
{ value: "general", header: i18n.t("project.settings.tabs.general") },
|
||||
{ value: "optional", header: i18n.t("project.settings.tabs.optional") },
|
||||
{ value: "management", header: i18n.t("project.settings.tabs.management") },
|
||||
{ value: "donation", header: i18n.t("project.settings.tabs.donation") },
|
||||
];
|
||||
|
||||
const form = reactive({
|
||||
settings: cloneDeep(props.project.settings),
|
||||
description: props.project.description,
|
||||
category: props.project.category,
|
||||
});
|
||||
const projectIcon = ref<File | null>(null);
|
||||
const newName = ref<string | null>("");
|
||||
const nameErrors = ref<string[]>([]);
|
||||
const loading = reactive({
|
||||
save: false,
|
||||
uploadIcon: false,
|
||||
resetIcon: false,
|
||||
rename: false,
|
||||
});
|
||||
|
||||
const isCustomLicense = computed(() => form.settings.license.type === "(custom)");
|
||||
const licenses = computed<Option[]>(() =>
|
||||
backendData.licenses.map<Option>((l) => {
|
||||
return { value: l, text: l };
|
||||
})
|
||||
);
|
||||
const categories = computed<Option[]>(() =>
|
||||
backendData.visibleCategories.map<Option>((c) => {
|
||||
return { value: c.apiName, text: i18n.t(c.title) };
|
||||
})
|
||||
);
|
||||
|
||||
watch(route, (val) => (selectedTab.value = val.hash.replace("#", "")), { deep: true });
|
||||
watch(selectedTab, (val) => history.replaceState({}, "", route.path + "#" + val));
|
||||
|
||||
watch(newName, async (val) => {
|
||||
if (!val) {
|
||||
nameErrors.value = [];
|
||||
} else {
|
||||
try {
|
||||
await useInternalApi("projects/validateName", false, "get", {
|
||||
userId: props.project.owner.userId,
|
||||
value: val,
|
||||
});
|
||||
nameErrors.value = [];
|
||||
} catch (err) {
|
||||
nameErrors.value = [];
|
||||
if (!err.response?.data.isHangarApiException) {
|
||||
return;
|
||||
}
|
||||
nameErrors.value.push(i18n.t(err.response.data.message));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function save() {
|
||||
loading.save = true;
|
||||
try {
|
||||
await useInternalApi(`projects/project/${route.params.user}/${route.params.project}/settings`, true, "post", {
|
||||
...form,
|
||||
});
|
||||
await router.go(0);
|
||||
} catch (e) {
|
||||
handleRequestError(e, ctx, i18n);
|
||||
}
|
||||
loading.save = false;
|
||||
}
|
||||
|
||||
async function rename() {
|
||||
loading.rename = true;
|
||||
try {
|
||||
const newSlug = await useInternalApi<string>(`projects/project/${route.params.user}/${route.params.project}/rename`, true, "post", {
|
||||
content: newName.value,
|
||||
});
|
||||
useNotificationStore().success(i18n.t("project.settings.success.rename", [newName.value]));
|
||||
await router.push(route.params.user + "/" + newSlug);
|
||||
} catch (e) {
|
||||
handleRequestError(e, ctx, i18n);
|
||||
}
|
||||
loading.rename = false;
|
||||
}
|
||||
|
||||
async function softDelete(comment: string) {
|
||||
try {
|
||||
await useInternalApi(`projects/project/${props.project.id}/manage/delete`, true, "post", {
|
||||
content: comment,
|
||||
});
|
||||
useNotificationStore().success(i18n.t("project.settings.success.softDelete"));
|
||||
await router.go(0);
|
||||
} catch (e) {
|
||||
handleRequestError(e, ctx, i18n);
|
||||
}
|
||||
}
|
||||
|
||||
async function hardDelete(comment: string) {
|
||||
try {
|
||||
await useInternalApi(`projects/project/${props.project.id}/manage/hardDelete`, true, "post", {
|
||||
content: comment,
|
||||
});
|
||||
useNotificationStore().success(i18n.t("project.settings.success.hardDelete"));
|
||||
await router.push("/");
|
||||
} catch (e) {
|
||||
handleRequestError(e, ctx, i18n);
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadIcon() {
|
||||
const data = new FormData();
|
||||
data.append("projectIcon", projectIcon.value!);
|
||||
loading.uploadIcon = true;
|
||||
try {
|
||||
await useInternalApi(`projects/project/${route.params.user}/${route.params.project}/saveIcon`, true, "post", data);
|
||||
} catch (e) {
|
||||
handleRequestError(e, ctx, i18n);
|
||||
}
|
||||
loading.uploadIcon = false;
|
||||
}
|
||||
|
||||
async function resetIcon() {
|
||||
loading.resetIcon = true;
|
||||
try {
|
||||
await useInternalApi(`projects/project/${route.params.user}/${route.params.project}/resetIcon`, true, "post");
|
||||
useNotificationStore().success(i18n.t("project.settings.success.resetIcon"));
|
||||
document
|
||||
.getElementById("project-icon-preview")!
|
||||
.setAttribute("src", `${projectIconUrl(props.project.namespace.owner, props.project.namespace.slug)}?noCache=${Math.random()}`);
|
||||
await router.go(0);
|
||||
} catch (e) {
|
||||
handleRequestError(e, ctx, i18n);
|
||||
}
|
||||
loading.resetIcon = false;
|
||||
}
|
||||
|
||||
function onFileChange() {
|
||||
if (projectIcon.value) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (ev) => {
|
||||
document.getElementById("project-icon-preview")!.setAttribute("src", ev.target!.result as string);
|
||||
};
|
||||
reader.readAsDataURL(projectIcon.value);
|
||||
} else {
|
||||
document.getElementById("project-icon-preview")!.setAttribute("src", projectIconUrl(props.project.namespace.owner, props.project.namespace.slug));
|
||||
}
|
||||
}
|
||||
|
||||
useHead(
|
||||
useSeo(
|
||||
i18n.t("project.settings.title") + " | " + props.project.name,
|
||||
@ -23,7 +194,217 @@ useHead(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>settings</h1>
|
||||
<div class="flex gap-4">
|
||||
<Card class="basis-full md:basis-9/12">
|
||||
<template #header>
|
||||
<div class="flex justify-between">
|
||||
{{ i18n.t("project.settings.title") }}
|
||||
<div class="text-lg">
|
||||
<VisibilityChangerModal
|
||||
v-if="hasPerms(NamedPermission.SEE_HIDDEN)"
|
||||
type="project"
|
||||
:prop-visibility="project.visibility"
|
||||
:post-url="`projects/visibility/${project.id}`"
|
||||
></VisibilityChangerModal>
|
||||
<Button class="inline-flex items-center ml-2" @click="save">
|
||||
<IconMdiCheck />
|
||||
{{ i18n.t("project.settings.save") }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- setting icons -->
|
||||
<Tabs v-model="selectedTab" :tabs="tabs">
|
||||
<template #general>
|
||||
<div>
|
||||
<h2 class="text-lg">{{ i18n.t("project.settings.category") }}</h2>
|
||||
<p>{{ i18n.t("project.settings.categorySub") }}</p>
|
||||
<InputSelect v-model="form.category" :values="categories" />
|
||||
</div>
|
||||
<hr class="my-1" />
|
||||
<div>
|
||||
<h2 class="text-lg">{{ i18n.t("project.settings.description") }}</h2>
|
||||
<p>{{ i18n.t("project.settings.descriptionSub") }}</p>
|
||||
<InputText v-model="form.description" counter :maxlength="backendData.validations?.project?.desc?.max" />
|
||||
</div>
|
||||
<hr class="my-1" />
|
||||
<div>
|
||||
<h2 class="text-lg">{{ i18n.t("project.settings.forum") }}</h2>
|
||||
<InputCheckbox v-model="form.settings.forumSync" :label="i18n.t('project.settings.forumSub')" />
|
||||
</div>
|
||||
<hr class="my-1" />
|
||||
<div>
|
||||
<h2 class="text-lg">{{ i18n.t("project.settings.icon") }}</h2>
|
||||
<div class="flex">
|
||||
<div>
|
||||
<p>{{ i18n.t("project.settings.iconSub") }}</p>
|
||||
<InputFile v-model="projectIcon" accept="image/png, image/jpeg" show-size @change="onFileChange" />
|
||||
<Button :disabled="!projectIcon || loading.uploadIcon" @click="uploadIcon">
|
||||
<IconMdiUpload />
|
||||
{{ i18n.t("project.settings.iconUpload") }}
|
||||
</Button>
|
||||
<Button :disabled="loading.resetIcon" @click="resetIcon">
|
||||
<IconMdiUpload />
|
||||
{{ i18n.t("project.settings.iconReset") }}
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<img
|
||||
id="project-icon-preview"
|
||||
width="150"
|
||||
height="150"
|
||||
alt="Project Icon"
|
||||
:src="projectIconUrl(project.namespace.owner, project.namespace.slug)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #optional>
|
||||
<div>
|
||||
<h2 class="text-lg">
|
||||
{{ i18n.t("project.settings.keywords") }} <small>{{ i18n.t("project.settings.optional") }}</small>
|
||||
</h2>
|
||||
<p>{{ i18n.t("project.settings.keywordsSub") }}</p>
|
||||
<InputTag
|
||||
v-model="form.settings.keywords"
|
||||
counter
|
||||
:maxlength="backendData.validations.project.keywords.max"
|
||||
:label="i18n.t('project.new.step3.keywords')"
|
||||
/>
|
||||
</div>
|
||||
<hr class="my-1" />
|
||||
<div>
|
||||
<h2 class="text-lg">
|
||||
{{ i18n.t("project.settings.homepage") }} <small>{{ i18n.t("project.settings.optional") }}</small>
|
||||
</h2>
|
||||
<p>{{ i18n.t("project.settings.homepageSub") }}</p>
|
||||
<InputText v-model.trim="form.settings.homepage" :label="i18n.t('project.new.step3.homepage')"></InputText>
|
||||
</div>
|
||||
<hr class="my-1" />
|
||||
<div>
|
||||
<h2 class="text-lg">
|
||||
{{ i18n.t("project.settings.issues") }} <small>{{ i18n.t("project.settings.optional") }}</small>
|
||||
</h2>
|
||||
<p>{{ i18n.t("project.settings.issuesSub") }}</p>
|
||||
<InputText v-model.trim="form.settings.issues" :label="i18n.t('project.new.step3.issues')" />
|
||||
</div>
|
||||
<hr class="my-1" />
|
||||
<div>
|
||||
<h2 class="text-lg">
|
||||
{{ i18n.t("project.settings.source") }} <small>{{ i18n.t("project.settings.optional") }}</small>
|
||||
</h2>
|
||||
<p>{{ i18n.t("project.settings.sourceSub") }}</p>
|
||||
<InputText v-model.trim="form.settings.source" :label="i18n.t('project.new.step3.source')" />
|
||||
</div>
|
||||
<hr class="my-1" />
|
||||
<div>
|
||||
<h2 class="text-lg">
|
||||
{{ i18n.t("project.settings.support") }} <small>{{ i18n.t("project.settings.optional") }}</small>
|
||||
</h2>
|
||||
<p>{{ i18n.t("project.settings.supportSub") }}</p>
|
||||
<InputText v-model.trim="form.settings.support" :label="i18n.t('project.new.step3.support')" />
|
||||
</div>
|
||||
<hr class="my-1" />
|
||||
<div>
|
||||
<h2 class="text-lg">
|
||||
{{ i18n.t("project.settings.license") }} <small>{{ i18n.t("project.settings.optional") }}</small>
|
||||
</h2>
|
||||
<p>{{ i18n.t("project.settings.licenseSub") }}</p>
|
||||
<div class="flex">
|
||||
<div class="basis-full" :md="isCustomLicense ? 'basis-4/12' : 'basis-6/12'">
|
||||
<InputSelect v-model="form.settings.license.type" :values="licenses" :label="i18n.t('project.settings.licenseType')" />
|
||||
</div>
|
||||
<div v-if="isCustomLicense" class="basis-full md:basis-8/12">
|
||||
<InputText v-model.trim="form.settings.license.name" :label="i18n.t('project.settings.licenseCustom')" />
|
||||
</div>
|
||||
<div class="basis-full" :md="isCustomLicense ? 'basis-full' : 'basis-6/12'">
|
||||
<InputText v-model.trim="form.settings.license.url" :label="i18n.t('project.settings.licenseUrl')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #management>
|
||||
<div>
|
||||
<h2 class="text-lg">{{ i18n.t("project.settings.rename") }}</h2>
|
||||
<p>{{ i18n.t("project.settings.renameSub") }}</p>
|
||||
<div class="flex">
|
||||
<InputText v-model.trim="newName" :error-messages="nameErrors" />
|
||||
<Button :disabled="!newName || loading.rename || nameErrors.length > 0" class="inline-flex items-center ml-2" @click="rename">
|
||||
<IconMdiRenameBox class="mr-2" />
|
||||
{{ i18n.t("project.settings.rename") }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="my-1" />
|
||||
<div v-if="hasPerms(NamedPermission.DELETE_PROJECT)">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink">
|
||||
<h2 class="text-lg">{{ i18n.t("project.settings.delete") }}</h2>
|
||||
<p>{{ i18n.t("project.settings.deleteSub") }}</p>
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<TextAreaModal :title="i18n.t('project.settings.delete')" :label="i18n.t('general.comment')" :submit="softDelete">
|
||||
<template #activator="{ on }">
|
||||
<Button v-on="on">{{ i18n.t("project.settings.delete") }}</Button>
|
||||
</template>
|
||||
</TextAreaModal>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr v-if="hasPerms(NamedPermission.HARD_DELETE_PROJECT)" class="my-1" />
|
||||
<div v-if="hasPerms(NamedPermission.HARD_DELETE_PROJECT)" class="bg-red-500 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink">
|
||||
<h2 class="text-lg">{{ i18n.t("project.settings.hardDelete") }}</h2>
|
||||
<p>{{ i18n.t("project.settings.hardDeleteSub") }}</p>
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<TextAreaModal :title="i18n.t('project.settings.hardDelete')" :label="i18n.t('general.comment')" :submit="hardDelete">
|
||||
<template #activator="{ on }">
|
||||
<Button v-on="on">{{ i18n.t("project.settings.hardDelete") }}</Button>
|
||||
</template>
|
||||
</TextAreaModal>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #donation>
|
||||
<div>
|
||||
<h2 class="text-lg">{{ i18n.t("project.settings.donation.enable") }}</h2>
|
||||
<p>{{ i18n.t("project.settings.donation.enableSub") }}</p>
|
||||
<InputCheckbox v-model="form.settings.donation.enable" />
|
||||
</div>
|
||||
<hr class="my-1" />
|
||||
<div>
|
||||
<h2 class="text-lg">{{ i18n.t("project.settings.donation.email") }}</h2>
|
||||
<p>{{ i18n.t("project.settings.donation.emailSub") }}</p>
|
||||
<InputText v-model="form.settings.donation.email" />
|
||||
</div>
|
||||
<hr class="my-1" />
|
||||
<div>
|
||||
<h2 class="text-lg">{{ i18n.t("project.settings.donation.defaultAmount") }}</h2>
|
||||
<p>{{ i18n.t("project.settings.donation.defaultAmountSub") }}</p>
|
||||
<InputText v-model.number="form.settings.donation.defaultAmount" type="number" />
|
||||
</div>
|
||||
<hr class="my-1" />
|
||||
<div>
|
||||
<h2 class="text-lg">{{ i18n.t("project.settings.donation.oneTimeAmounts") }}</h2>
|
||||
<p>{{ i18n.t("project.settings.donation.oneTimeAmountsSub") }}</p>
|
||||
<InputTag v-model="form.settings.donation.oneTimeAmounts" />
|
||||
</div>
|
||||
<hr class="my-1" />
|
||||
<div>
|
||||
<h2 class="text-lg">{{ i18n.t("project.settings.donation.monthlyAmounts") }}</h2>
|
||||
<p>{{ i18n.t("project.settings.donation.monthlyAmountsSub") }}</p>
|
||||
<InputTag v-model="form.settings.donation.monthlyAmounts" />
|
||||
</div>
|
||||
</template>
|
||||
</Tabs>
|
||||
</Card>
|
||||
<MemberList :model-value="project.members" class="basis-full md:basis-3/12" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
|
@ -73,15 +73,15 @@ export const useBackendDataStore = defineStore("backendData", () => {
|
||||
return convertToMap<Prompt, IPrompt>(promptsResult, (value) => value.name);
|
||||
}, prompts);
|
||||
|
||||
await fetchIfNeeded(async () => await useInternalApi<string[]>("data/licenses", false), licenses);
|
||||
await fetchIfNeeded(async () => useInternalApi<string[]>("data/licenses", false), licenses);
|
||||
|
||||
await fetchIfNeeded(async () => await useInternalApi<AnnouncementObject[]>("data/announcements", false), announcements);
|
||||
await fetchIfNeeded(async () => useInternalApi<AnnouncementObject[]>("data/announcements", false), announcements);
|
||||
|
||||
await fetchIfNeeded(async () => await useInternalApi<IVisibility[]>("data/visibilities", false), visibilities);
|
||||
await fetchIfNeeded(async () => useInternalApi<IVisibility[]>("data/visibilities", false), visibilities);
|
||||
|
||||
await fetchIfNeeded(async () => await useInternalApi("data/validations", false), validations);
|
||||
await fetchIfNeeded(async () => useInternalApi("data/validations", false), validations);
|
||||
|
||||
await fetchIfNeeded(async () => await useInternalApi("data/orgRoles", false), validations);
|
||||
await fetchIfNeeded(async () => useInternalApi("data/orgRoles", false), orgRoles);
|
||||
} catch (e) {
|
||||
console.error("ERROR FETCHING BACKEND DATA");
|
||||
console.error(e);
|
||||
|
10
frontend-new/src/types/generated/icons.d.ts
vendored
10
frontend-new/src/types/generated/icons.d.ts
vendored
@ -4,22 +4,15 @@
|
||||
|
||||
declare module "vue" {
|
||||
export interface GlobalComponents {
|
||||
IconMdiAccountArrowRight: typeof import("~icons/mdi/account-arrow-right")["default"];
|
||||
IconMdiCalendar: typeof import("~icons/mdi/calendar")["default"];
|
||||
IconMdiCheck: typeof import("~icons/mdi/check")["default"];
|
||||
IconMdiCheckboxBlankCircleOutline: typeof import("~icons/mdi/checkbox-blank-circle-outline")["default"];
|
||||
IconMdiCheckCircle: typeof import("~icons/mdi/check-circle")["default"];
|
||||
IconMdiCheckCircleOutline: typeof import("~icons/mdi/check-circle-outline")["default"];
|
||||
IconMdiClipboardOutline: typeof import("~icons/mdi/clipboard-outline")["default"];
|
||||
IconMdiClose: typeof import("~icons/mdi/close")["default"];
|
||||
IconMdiCloseCircle: typeof import("~icons/mdi/close-circle")["default"];
|
||||
IconMdiDelete: typeof import("~icons/mdi/delete")["default"];
|
||||
IconMdiDiamondStone: typeof import("~icons/mdi/diamond-stone")["default"];
|
||||
IconMdiDownload: typeof import("~icons/mdi/download")["default"];
|
||||
IconMdiEye: typeof import("~icons/mdi/eye")["default"];
|
||||
IconMdiFile: typeof import("~icons/mdi/file")["default"];
|
||||
IconMdiFileFind: typeof import("~icons/mdi/file-find")["default"];
|
||||
IconMdiFormatListNumbered: typeof import("~icons/mdi/format-list-numbered")["default"];
|
||||
IconMdiHome: typeof import("~icons/mdi/home")["default"];
|
||||
IconMdiKeyOutline: typeof import("~icons/mdi/key-outline")["default"];
|
||||
IconMdiListStatus: typeof import("~icons/mdi/list-status")["default"];
|
||||
@ -29,9 +22,10 @@ declare module "vue" {
|
||||
IconMdiPencil: typeof import("~icons/mdi/pencil")["default"];
|
||||
IconMdiPlay: typeof import("~icons/mdi/play")["default"];
|
||||
IconMdiPlus: typeof import("~icons/mdi/plus")["default"];
|
||||
IconMdiRenameBox: typeof import("~icons/mdi/rename-box")["default"];
|
||||
IconMdiSortVariant: typeof import("~icons/mdi/sort-variant")["default"];
|
||||
IconMdiStar: typeof import("~icons/mdi/star")["default"];
|
||||
IconMdiTag: typeof import("~icons/mdi/tag")["default"];
|
||||
IconMdiUpload: typeof import("~icons/mdi/upload")["default"];
|
||||
IconMdiWeatherNight: typeof import("~icons/mdi/weather-night")["default"];
|
||||
IconMdiWhiteBalanceSunny: typeof import("~icons/mdi/white-balance-sunny")["default"];
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user