page list collapsing

auto expands
also refactor page stuff into composable
This commit is contained in:
MiniDigger 2022-03-26 17:08:16 +01:00
parent e6ac39603e
commit 2334e6b698
8 changed files with 104 additions and 46 deletions

View File

@ -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>

View File

@ -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">

View File

@ -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>

View File

@ -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>

View 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 };
}

View File

@ -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>

View File

@ -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>

View File

@ -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"];