forked from potsda.mn/mobilizon
411 lines
11 KiB
Vue
411 lines
11 KiB
Vue
<template>
|
|
<div>
|
|
<form @submit.prevent="publish(false)" v-if="isCurrentActorAGroupModerator">
|
|
<div class="container mx-auto">
|
|
<breadcrumbs-nav v-if="actualGroup" :links="breadcrumbLinks" />
|
|
<h1 v-if="isUpdate === true">
|
|
{{ t("Edit post") }}
|
|
</h1>
|
|
<h1 v-else>
|
|
{{ t("Add a new post") }}
|
|
</h1>
|
|
<h2>{{ t("General information") }}</h2>
|
|
<picture-upload
|
|
v-model="pictureFile"
|
|
:textFallback="t('Headline picture')"
|
|
:defaultImage="editablePost.picture"
|
|
/>
|
|
|
|
<o-field
|
|
:label="t('Title')"
|
|
label-for="post-title"
|
|
:type="errors.title ? 'is-danger' : null"
|
|
:message="errors.title"
|
|
>
|
|
<o-input
|
|
size="large"
|
|
aria-required="true"
|
|
required
|
|
v-model="editablePost.title"
|
|
id="post-title"
|
|
dir="auto"
|
|
expanded
|
|
/>
|
|
</o-field>
|
|
|
|
<tag-input v-model="editablePost.tags" />
|
|
|
|
<o-field :label="t('Post')">
|
|
<p v-if="errors.body" class="help is-danger">{{ errors.body }}</p>
|
|
<editor
|
|
class="w-full"
|
|
v-if="currentActor"
|
|
v-model="editablePost.body"
|
|
:aria-label="t('Post body')"
|
|
:current-actor="currentActor"
|
|
:placeholder="t('Write your post')"
|
|
:headingLevel="[2, 3, 4]"
|
|
/>
|
|
</o-field>
|
|
<h2 class="mt-2">{{ t("Who can view this post") }}</h2>
|
|
<fieldset>
|
|
<legend>
|
|
{{
|
|
t(
|
|
"When the post is private, you'll need to share the link around."
|
|
)
|
|
}}
|
|
</legend>
|
|
<div class="field">
|
|
<o-radio
|
|
v-model="editablePost.visibility"
|
|
name="postVisibility"
|
|
:native-value="PostVisibility.PUBLIC"
|
|
>{{ t("Visible everywhere on the web") }}</o-radio
|
|
>
|
|
</div>
|
|
<div class="field">
|
|
<o-radio
|
|
v-model="editablePost.visibility"
|
|
name="postVisibility"
|
|
:native-value="PostVisibility.UNLISTED"
|
|
>{{ t("Only accessible through link") }}</o-radio
|
|
>
|
|
</div>
|
|
<div class="field">
|
|
<o-radio
|
|
v-model="editablePost.visibility"
|
|
name="postVisibility"
|
|
:native-value="PostVisibility.PRIVATE"
|
|
>{{ t("Only accessible to members of the group") }}</o-radio
|
|
>
|
|
</div>
|
|
</fieldset>
|
|
</div>
|
|
<nav class="navbar">
|
|
<div class="container mx-auto">
|
|
<div class="navbar-menu flex flex-wrap py-2">
|
|
<div class="flex flex-wrap justify-end ml-auto gap-1">
|
|
<o-button variant="text" @click="$router.go(-1)">{{
|
|
t("Cancel")
|
|
}}</o-button>
|
|
<o-button
|
|
v-if="isUpdate"
|
|
variant="danger"
|
|
outlined
|
|
@click="openDeletePostModal"
|
|
>{{ t("Delete post") }}</o-button
|
|
>
|
|
<!-- If an post has been published we can't make it draft anymore -->
|
|
<o-button
|
|
variant="primary"
|
|
v-if="post?.draft === true"
|
|
outlined
|
|
@click="publish(true)"
|
|
>{{ t("Save draft") }}</o-button
|
|
>
|
|
<o-button variant="primary" native-type="submit">
|
|
<span v-if="isUpdate === false || post?.draft === true">{{
|
|
t("Publish")
|
|
}}</span>
|
|
|
|
<span v-else>{{ t("Update post") }}</span>
|
|
</o-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
</form>
|
|
<o-loading
|
|
v-else-if="postLoading"
|
|
:is-full-page="false"
|
|
v-model:active="postLoading"
|
|
:can-cancel="false"
|
|
></o-loading>
|
|
<div class="container mx-auto" v-else>
|
|
<o-notification variant="danger">
|
|
{{ t("Only group moderators can create, edit and delete posts.") }}
|
|
</o-notification>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<script lang="ts" setup>
|
|
import {
|
|
buildFileFromIMedia,
|
|
buildFileVariable,
|
|
readFileAsync,
|
|
} from "@/utils/image";
|
|
import { MemberRole, PostVisibility } from "@/types/enums";
|
|
import {
|
|
CREATE_POST,
|
|
DELETE_POST,
|
|
FETCH_POST,
|
|
UPDATE_POST,
|
|
} from "../../graphql/post";
|
|
|
|
import { IPost } from "../../types/post.model";
|
|
import Editor from "../../components/TextEditor.vue";
|
|
import { displayName, IActor, usernameWithDomain } from "../../types/actor";
|
|
import TagInput from "../../components/Event/TagInput.vue";
|
|
import RouteName from "../../router/name";
|
|
import PictureUpload from "../../components/PictureUpload.vue";
|
|
import { useGroup } from "@/composition/apollo/group";
|
|
import {
|
|
useCurrentActorClient,
|
|
usePersonStatusGroup,
|
|
} from "@/composition/apollo/actor";
|
|
import { useHead } from "@unhead/vue";
|
|
import { useI18n } from "vue-i18n";
|
|
import { computed, inject, onMounted, ref, watch } from "vue";
|
|
import { useRouter } from "vue-router";
|
|
import { useMutation, useQuery } from "@vue/apollo-composable";
|
|
import { Dialog } from "@/plugins/dialog";
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
slug?: string;
|
|
preferredUsername?: string;
|
|
isUpdate?: boolean;
|
|
}>(),
|
|
{ isUpdate: false }
|
|
);
|
|
const preferredUsername = computed(() => props.preferredUsername);
|
|
|
|
const { currentActor } = useCurrentActorClient();
|
|
const { group } = useGroup(preferredUsername);
|
|
|
|
const { result: postResult, loading: postLoading } = useQuery<{
|
|
post: IPost;
|
|
}>(FETCH_POST, () => ({ slug: props.slug }));
|
|
|
|
const post = computed(() => postResult.value?.post);
|
|
|
|
const pictureFile = ref<File | null>(null);
|
|
const errors = ref<Record<string, unknown>>({});
|
|
const editablePost = ref<IPost>({
|
|
title: "",
|
|
body: "",
|
|
local: true,
|
|
draft: true,
|
|
visibility: PostVisibility.PUBLIC,
|
|
tags: [],
|
|
});
|
|
|
|
onMounted(async () => {
|
|
pictureFile.value = await buildFileFromIMedia(post.value?.picture);
|
|
});
|
|
|
|
watch(post, async (newPost: IPost | undefined, oldPost: IPost | undefined) => {
|
|
if (oldPost?.picture !== newPost?.picture) {
|
|
pictureFile.value = await buildFileFromIMedia(post.value?.picture);
|
|
}
|
|
if (newPost) {
|
|
editablePost.value = { ...newPost };
|
|
}
|
|
});
|
|
|
|
const router = useRouter();
|
|
|
|
const { mutate: updatePost, onDone: onUpdateDone } = useMutation<{
|
|
updatePost: IPost;
|
|
}>(UPDATE_POST);
|
|
const {
|
|
mutate: createPost,
|
|
onDone: onCreateDone,
|
|
onError: onCreateError,
|
|
} = useMutation<{
|
|
createPost: IPost;
|
|
}>(CREATE_POST);
|
|
|
|
onUpdateDone(({ data }) => {
|
|
if (data && data.updatePost) {
|
|
router.push({
|
|
name: RouteName.POST,
|
|
params: { slug: data.updatePost.slug },
|
|
});
|
|
}
|
|
});
|
|
|
|
onCreateDone(({ data }) => {
|
|
if (data && data.createPost) {
|
|
router.push({
|
|
name: RouteName.POST,
|
|
params: { slug: data.createPost.slug },
|
|
});
|
|
}
|
|
});
|
|
|
|
onCreateError((error) => {
|
|
console.error(error);
|
|
errors.value = error.graphQLErrors.reduce(
|
|
(acc: { [key: string]: any }, localError: any) => {
|
|
acc[localError.field] = transformMessage(localError.message);
|
|
return acc;
|
|
},
|
|
{}
|
|
);
|
|
});
|
|
|
|
const publish = async (draft: boolean): Promise<void> => {
|
|
errors.value = {};
|
|
|
|
if (props.isUpdate) {
|
|
updatePost({
|
|
id: editablePost.value?.id,
|
|
title: editablePost.value?.title,
|
|
body: editablePost.value?.body,
|
|
tags: (editablePost.value?.tags || []).map(({ title }) => title),
|
|
visibility: editablePost.value?.visibility,
|
|
draft,
|
|
...(await buildPicture()),
|
|
});
|
|
} else {
|
|
createPost({
|
|
...editablePost.value,
|
|
...(await buildPicture()),
|
|
tags: (editablePost.value?.tags ?? []).map(({ title }) => title),
|
|
attributedToId: actualGroup.value.id,
|
|
draft,
|
|
});
|
|
}
|
|
};
|
|
|
|
const transformMessage = (message: string[] | string): string | undefined => {
|
|
if (Array.isArray(message) && message.length > 0) {
|
|
return message[0];
|
|
}
|
|
if (typeof message === "string") {
|
|
return message;
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
const buildPicture = async (): Promise<Record<string, unknown>> => {
|
|
let obj: { picture?: any } = {};
|
|
if (pictureFile.value) {
|
|
const pictureObj = buildFileVariable(pictureFile.value, "picture");
|
|
obj = { ...obj, ...pictureObj };
|
|
}
|
|
try {
|
|
if (editablePost.value?.picture && pictureFile.value) {
|
|
const oldPictureFile = (await buildFileFromIMedia(
|
|
editablePost.value.picture
|
|
)) as File;
|
|
const oldPictureFileContent = await readFileAsync(oldPictureFile);
|
|
const newPictureFileContent = await readFileAsync(
|
|
pictureFile.value as File
|
|
);
|
|
if (oldPictureFileContent === newPictureFileContent) {
|
|
obj.picture = { mediaId: editablePost.value.picture.id };
|
|
}
|
|
}
|
|
} catch (e: any) {
|
|
console.error(e);
|
|
}
|
|
return obj;
|
|
};
|
|
|
|
const actualGroup = computed((): IActor => {
|
|
if (!group.value?.id) {
|
|
return post.value?.attributedTo as IActor;
|
|
}
|
|
return group.value;
|
|
});
|
|
|
|
const actualPreferredUsername = computed(() =>
|
|
usernameWithDomain(actualGroup.value)
|
|
);
|
|
|
|
const { t } = useI18n({ useScope: "global" });
|
|
|
|
const breadcrumbLinks = computed(() => {
|
|
const links = [
|
|
{
|
|
name: RouteName.GROUP,
|
|
params: {
|
|
preferredUsername: usernameWithDomain(actualGroup.value),
|
|
},
|
|
text: displayName(actualGroup.value),
|
|
},
|
|
{
|
|
name: RouteName.POSTS,
|
|
params: {
|
|
preferredUsername: usernameWithDomain(actualGroup.value),
|
|
},
|
|
text: t("Posts"),
|
|
},
|
|
];
|
|
if (props.preferredUsername) {
|
|
links.push({
|
|
text: t("New post") as string,
|
|
name: RouteName.POST_EDIT,
|
|
params: { preferredUsername: usernameWithDomain(actualGroup.value) },
|
|
});
|
|
} else {
|
|
links.push({
|
|
text: t("Edit post") as string,
|
|
name: RouteName.POST_EDIT,
|
|
params: { preferredUsername: usernameWithDomain(actualGroup.value) },
|
|
});
|
|
}
|
|
return links;
|
|
});
|
|
|
|
const isCurrentActorAGroupModerator = computed((): boolean => {
|
|
return hasCurrentActorThisRole([
|
|
MemberRole.MODERATOR,
|
|
MemberRole.ADMINISTRATOR,
|
|
]);
|
|
});
|
|
|
|
const hasCurrentActorThisRole = (givenRole: string | string[]): boolean => {
|
|
const roles = Array.isArray(givenRole) ? givenRole : [givenRole];
|
|
return (
|
|
personMemberships.value?.total > 0 &&
|
|
roles.includes(personMemberships.value?.elements[0].role)
|
|
);
|
|
};
|
|
|
|
const { person } = usePersonStatusGroup(actualPreferredUsername);
|
|
|
|
const personMemberships = computed(
|
|
() => person.value?.memberships ?? { total: 0, elements: [] }
|
|
);
|
|
|
|
const dialog = inject<Dialog>("dialog");
|
|
|
|
const openDeletePostModal = async (): Promise<void> => {
|
|
dialog?.confirm({
|
|
variant: "danger",
|
|
title: t("Delete post"),
|
|
message: t(
|
|
"Are you sure you want to delete this post? This action cannot be reverted."
|
|
),
|
|
onConfirm: () =>
|
|
deletePost({
|
|
id: post.value?.id,
|
|
}),
|
|
});
|
|
};
|
|
|
|
const { mutate: deletePost, onDone: onDeletePostDone } =
|
|
useMutation(DELETE_POST);
|
|
|
|
onDeletePostDone(({ data }) => {
|
|
if (data && post.value?.attributedTo) {
|
|
router.push({
|
|
name: RouteName.POSTS,
|
|
params: {
|
|
preferredUsername: usernameWithDomain(post.value?.attributedTo),
|
|
},
|
|
});
|
|
}
|
|
});
|
|
|
|
useHead({
|
|
title: computed(() =>
|
|
props.isUpdate ? t("Edit post") : t("Add a new post")
|
|
),
|
|
});
|
|
</script>
|