mirror of
https://github.com/HangarMC/Hangar.git
synced 2025-01-24 14:24:47 +08:00
page list collapsing
auto expands also refactor page stuff into composable
This commit is contained in:
parent
e6ac39603e
commit
2334e6b698
@ -1,17 +1,43 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
items: Record<string, unknown>;
|
||||
itemKey: string;
|
||||
clazz?: string;
|
||||
open: string[];
|
||||
}>();
|
||||
|
||||
// TODO make collapsable
|
||||
const expanded = ref<Record<string, boolean>>({});
|
||||
watch(
|
||||
props.open,
|
||||
(val) => {
|
||||
if (val) {
|
||||
for (let item of val) {
|
||||
expanded.value[item] = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-for="item in items" :key="item[itemKey]" :class="props.clazz">
|
||||
<IconMdiMenuDown
|
||||
v-if="item.children && item.children.length > 0"
|
||||
:class="'cursor-pointer transform transition-transform ' + (expanded[item[itemKey]] ? 'rotate-0' : '-rotate-90')"
|
||||
@click="expanded[item[itemKey]] = !expanded[item[itemKey]]"
|
||||
/>
|
||||
<span v-else class="pl-4" />
|
||||
<slot name="item" :item="item"></slot>
|
||||
<TreeView v-if="item.children && item.children.length > 0" :key="item[itemKey]" :items="item.children" :item-key="itemKey" clazz="pl-2">
|
||||
<TreeView
|
||||
v-if="expanded[item[itemKey]] && item.children && item.children.length > 0"
|
||||
:key="item[itemKey]"
|
||||
:items="item.children"
|
||||
:item-key="itemKey"
|
||||
:open="open"
|
||||
clazz="pl-2"
|
||||
>
|
||||
<template #item="inner">
|
||||
<slot name="item" :item="inner.item"></slot>
|
||||
</template>
|
||||
|
@ -7,7 +7,7 @@ const classes = "color-primary font-bold hover:(underline)";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-link v-if="to" :to="to" :class="classes" v-bind="$attrs">
|
||||
<router-link v-if="to" :to="to" :class="classes" v-bind="$attrs" active-class="underline">
|
||||
<slot></slot>
|
||||
</router-link>
|
||||
<a v-else :href="href" :class="classes" v-bind="$attrs">
|
||||
|
@ -50,7 +50,7 @@ function createPage() {
|
||||
<Button class="mt-2 ml-2" @click="createPage">{{ i18n.t("general.create") }}</Button>
|
||||
</template>
|
||||
<template #activator="{ on }">
|
||||
<Button v-bind="$attrs" class="mr-1" v-on="on">
|
||||
<Button v-bind="$attrs" class="mr-1 h-[32px]" small v-on="on">
|
||||
<IconMdiPlus />
|
||||
</Button>
|
||||
</template>
|
||||
|
@ -11,6 +11,7 @@ import { useRoute } from "vue-router";
|
||||
|
||||
const props = defineProps<{
|
||||
project: HangarProject;
|
||||
open: string[];
|
||||
}>();
|
||||
|
||||
const i18n = useI18n();
|
||||
@ -24,7 +25,7 @@ const route = useRoute();
|
||||
{{ i18n.t("page.plural") }}
|
||||
</template>
|
||||
|
||||
<TreeView :items="project.pages" item-key="slug">
|
||||
<TreeView :items="project.pages" item-key="slug" :open="open">
|
||||
<template #item="{ item }">
|
||||
<Link v-if="item.home" :to="`/${route.params.user}/${route.params.project}`" exact><IconMdiHome /> {{ item.name }}</Link>
|
||||
<Link v-else :to="`/${route.params.user}/${route.params.project}/pages/${item.slug}`" exact> {{ item.name }}</Link>
|
||||
|
62
frontend-new/src/composables/useProjectPage.ts
Normal file
62
frontend-new/src/composables/useProjectPage.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { ref, watch } from "vue";
|
||||
import { useInternalApi } from "~/composables/useApi";
|
||||
import { handleRequestError } from "~/composables/useErrorHandling";
|
||||
import { RouteLocationNormalizedLoaded, Router, useRoute, useRouter } from "vue-router";
|
||||
import { Context } from "vite-ssr/vue";
|
||||
import { Composer, VueMessageType } from "vue-i18n";
|
||||
import { HangarProject } from "hangar-internal";
|
||||
import { usePage } from "~/composables/useApiHelper";
|
||||
import { useErrorRedirect } from "~/composables/useErrorRedirect";
|
||||
|
||||
export async function useProjectPage(
|
||||
route: RouteLocationNormalizedLoaded,
|
||||
router: Router,
|
||||
ctx: Context,
|
||||
i18n: Composer<unknown, unknown, unknown, VueMessageType>,
|
||||
project: HangarProject
|
||||
) {
|
||||
const page = await usePage(route.params.user as string, route.params.project as string).catch((e) => handleRequestError(e, ctx, i18n));
|
||||
if (!page) {
|
||||
await useRouter().push(useErrorRedirect(useRoute(), 404, "Not found"));
|
||||
}
|
||||
|
||||
const editingPage = ref<boolean>(false);
|
||||
const open = ref<string[]>([]);
|
||||
|
||||
watch(
|
||||
route,
|
||||
() => {
|
||||
const slugs = route.fullPath.split("/").slice(4);
|
||||
if (slugs.length) {
|
||||
for (let i = 0; i < slugs.length; i++) {
|
||||
const slug = slugs.slice(0, i + 1).join("/");
|
||||
if (!open.value.includes(slug)) {
|
||||
open.value.push(slug);
|
||||
}
|
||||
}
|
||||
} else if (project.pages.length === 1) {
|
||||
open.value.push(project.pages[0].slug);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
async function savePage(content: string) {
|
||||
if (!page) return;
|
||||
await useInternalApi(`pages/save/${project.id}/${page.value?.id}`, true, "post", {
|
||||
content,
|
||||
}).catch((e) => handleRequestError(e, ctx, i18n, "page.new.error.save"));
|
||||
// todo page saving
|
||||
//page.value?.contents = content;
|
||||
editingPage.value = false;
|
||||
}
|
||||
|
||||
async function deletePage() {
|
||||
if (!page) return;
|
||||
await useInternalApi(`pages/delete/${project.id}/${page.value?.id}`, true, "post").catch((e) => handleRequestError(e, ctx, i18n, "page.new.error.save"));
|
||||
// todo page deleting
|
||||
//this.$refs.editor.loading.delete = false;
|
||||
await router.replace(`/${route.params.user}/${route.params.project}`);
|
||||
}
|
||||
return { editingPage, open, page, savePage, deletePage };
|
||||
}
|
@ -8,13 +8,11 @@ import MemberList from "~/components/projects/MemberList.vue";
|
||||
import MarkdownEditor from "~/components/MarkdownEditor.vue";
|
||||
import { hasPerms } from "~/composables/usePerm";
|
||||
import { NamedPermission } from "~/types/enums";
|
||||
import { usePage } from "~/composables/useApiHelper";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useContext } from "vite-ssr/vue";
|
||||
import { handleRequestError } from "~/composables/useErrorHandling";
|
||||
import { ref } from "vue";
|
||||
import Markdown from "~/components/Markdown.vue";
|
||||
import ProjectPageList from "~/components/projects/ProjectPageList.vue";
|
||||
import { useProjectPage } from "~/composables/useProjectPage";
|
||||
|
||||
const props = defineProps<{
|
||||
user: User;
|
||||
@ -23,12 +21,8 @@ const props = defineProps<{
|
||||
const i18n = useI18n();
|
||||
const route = useRoute();
|
||||
const context = useContext();
|
||||
const page = await usePage(route.params.user as string, route.params.project as string).catch((e) => handleRequestError(e, context, i18n));
|
||||
const editingPage = ref(false);
|
||||
|
||||
function savePage() {
|
||||
// TODO mixin?
|
||||
}
|
||||
const router = useRouter();
|
||||
const { editingPage, open, savePage, page } = await useProjectPage(route, router, context, i18n, props.project);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -52,7 +46,7 @@ function savePage() {
|
||||
<template #header>{{ i18n.t("project.promotedVersions") }}</template>
|
||||
<template #default>Promoted versions go here</template>
|
||||
</Card>
|
||||
<ProjectPageList :project="project" />
|
||||
<ProjectPageList :project="project" :open="open" />
|
||||
<MemberList :project="project" />
|
||||
</section>
|
||||
</div>
|
||||
|
@ -1,21 +1,16 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useContext } from "vite-ssr/vue";
|
||||
import { usePage, useStaff } from "~/composables/useApiHelper";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useErrorRedirect } from "~/composables/useErrorRedirect";
|
||||
import { User } from "hangar-api";
|
||||
import { HangarProject } from "hangar-internal";
|
||||
import ProjectPageList from "~/components/projects/ProjectPageList.vue";
|
||||
import Markdown from "~/components/Markdown.vue";
|
||||
import MarkdownEditor from "~/components/MarkdownEditor.vue";
|
||||
import { useInternalApi } from "~/composables/useApi";
|
||||
import { handleRequestError } from "~/composables/useErrorHandling";
|
||||
import { ref } from "vue";
|
||||
import { hasPerms } from "~/composables/usePerm";
|
||||
import { NamedPermission } from "~/types/enums";
|
||||
import Card from "~/components/design/Card.vue";
|
||||
import Settings from "~/pages/[user]/[project]/settings.vue";
|
||||
import { useProjectPage } from "~/composables/useProjectPage";
|
||||
|
||||
const props = defineProps<{
|
||||
user: User;
|
||||
@ -26,29 +21,8 @@ const i18n = useI18n();
|
||||
const ctx = useContext();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const page = await usePage(route.params.user as string, route.params.project as string, route.params.all as string);
|
||||
if (!page) {
|
||||
await useRouter().push(useErrorRedirect(useRoute(), 404, "Not found"));
|
||||
}
|
||||
const editingPage = ref(false);
|
||||
|
||||
async function savePage(content: string) {
|
||||
await useInternalApi(`pages/save/${props.project.id}/${page.value?.id}`, true, "post", {
|
||||
content,
|
||||
}).catch((e) => handleRequestError(e, ctx, i18n, "page.new.error.save"));
|
||||
// todo page saving
|
||||
//page.value?.contents = content;
|
||||
editingPage.value = false;
|
||||
}
|
||||
|
||||
async function deletePage() {
|
||||
await useInternalApi(`pages/delete/${props.project.id}/${page.value?.id}`, true, "post").catch((e) =>
|
||||
handleRequestError(e, ctx, i18n, "page.new.error.save")
|
||||
);
|
||||
// todo page deleting
|
||||
//this.$refs.editor.loading.delete = false;
|
||||
await router.replace(`/${route.params.user}/${route.params.project}`);
|
||||
}
|
||||
const { editingPage, open, savePage, deletePage, page } = await useProjectPage(route, router, ctx, i18n, props.project);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -68,7 +42,7 @@ async function deletePage() {
|
||||
</Card>
|
||||
</section>
|
||||
<section class="basis-full md:basis-3/12 flex-grow">
|
||||
<ProjectPageList :project="project" />
|
||||
<ProjectPageList :project="project" :open="open" />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
1
frontend-new/src/types/generated/icons.d.ts
vendored
1
frontend-new/src/types/generated/icons.d.ts
vendored
@ -16,6 +16,7 @@ declare module "vue" {
|
||||
IconMdiKeyOutline: typeof import("~icons/mdi/key-outline")["default"];
|
||||
IconMdiListStatus: typeof import("~icons/mdi/list-status")["default"];
|
||||
IconMdiMenu: typeof import("~icons/mdi/menu")["default"];
|
||||
IconMdiMenuDown: typeof import("~icons/mdi/menu-down")["default"];
|
||||
IconMdiOpenInNew: typeof import("~icons/mdi/open-in-new")["default"];
|
||||
IconMdiPencil: typeof import("~icons/mdi/pencil")["default"];
|
||||
IconMdiPlay: typeof import("~icons/mdi/play")["default"];
|
||||
|
Loading…
Reference in New Issue
Block a user