Migrate to Vue 3 and Vite

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2022-07-12 10:55:28 +02:00
parent 8f4099ee33
commit ee20e03cc2
464 changed files with 31515 additions and 32758 deletions

View File

@@ -1,11 +1,11 @@
<template>
<section class="section container">
<section class="container mx-auto">
<breadcrumbs-nav
v-if="group"
:links="[
{
name: RouteName.MY_GROUPS,
text: $t('My groups'),
text: t('My groups'),
},
{
name: RouteName.GROUP,
@@ -15,137 +15,122 @@
{
name: RouteName.DISCUSSION_LIST,
params: { preferredUsername: usernameWithDomain(group) },
text: $t('Discussions'),
text: t('Discussions'),
},
{
name: RouteName.CREATE_DISCUSSION,
params: { preferredUsername: usernameWithDomain(group) },
text: $t('Create'),
text: t('Create'),
},
]"
/>
<h1 class="title">{{ $t("Create a discussion") }}</h1>
<h1 class="title">{{ t("Create a discussion") }}</h1>
<form @submit.prevent="createDiscussion">
<b-field
:label="$t('Title')"
<o-field
:label="t('Title')"
label-for="discussion-title"
:message="errors.title"
:type="errors.title ? 'is-danger' : undefined"
>
<b-input
<o-input
aria-required="true"
required
v-model="discussion.title"
id="discussion-title"
/>
</b-field>
</o-field>
<b-field :label="$t('Text')">
<editor v-model="discussion.text" :aria-label="$t('Comment body')" />
</b-field>
<o-field :label="t('Text')">
<Editor
v-model="discussion.text"
:aria-label="t('Comment body')"
v-if="currentActor"
:current-actor="currentActor"
/>
</o-field>
<button class="button is-primary" type="submit">
{{ $t("Create the discussion") }}
</button>
<o-button class="mt-2" native-type="submit">
{{ t("Create the discussion") }}
</o-button>
</form>
</section>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import {
displayName,
IGroup,
IPerson,
usernameWithDomain,
} from "@/types/actor";
import { CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
import { FETCH_GROUP } from "@/graphql/group";
<script lang="ts" setup>
import { displayName, usernameWithDomain } from "@/types/actor";
import { CREATE_DISCUSSION } from "@/graphql/discussion";
import RouteName from "../../router/name";
import { computed, defineAsyncComponent, reactive, inject } from "vue";
import { useCurrentActorClient } from "@/composition/apollo/actor";
import { useGroup } from "@/composition/apollo/group";
import { useI18n } from "vue-i18n";
import { useMutation } from "@vue/apollo-composable";
import { IDiscussion } from "@/types/discussions";
import { useRouter } from "vue-router";
import { useHead } from "@vueuse/head";
import { Notifier } from "@/plugins/notifier";
import { AbsintheGraphQLError } from "@/types/errors.model";
@Component({
components: {
editor: () =>
import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
},
apollo: {
currentActor: CURRENT_ACTOR_CLIENT,
group: {
query: FETCH_GROUP,
variables() {
return {
name: this.preferredUsername,
};
},
skip() {
return !this.preferredUsername;
},
const Editor = defineAsyncComponent(() => import("@/components/Editor.vue"));
const props = defineProps<{ preferredUsername: string }>();
const { currentActor } = useCurrentActorClient();
const { group } = useGroup(props.preferredUsername);
const { t } = useI18n({ useScope: "global" });
useHead({
title: computed(() => t("Create a discussion")),
});
const discussion = reactive({ title: "", text: "" });
const errors = reactive({ title: "" });
const router = useRouter();
const notifier = inject<Notifier>("notifier");
const { mutate, onDone, onError } = useMutation<{
createDiscussion: IDiscussion;
}>(CREATE_DISCUSSION);
onDone(({ data }) => {
router.push({
name: RouteName.DISCUSSION,
params: {
id: data?.createDiscussion.id,
slug: data?.createDiscussion.slug,
},
},
metaInfo() {
return {
title: this.$t("Create a discussion") as string,
};
},
})
export default class CreateDiscussion extends Vue {
@Prop({ type: String, required: true }) preferredUsername!: string;
});
});
group!: IGroup;
currentActor!: IPerson;
discussion = { title: "", text: "" };
errors = { title: "" };
RouteName = RouteName;
usernameWithDomain = usernameWithDomain;
displayName = displayName;
async createDiscussion(): Promise<void> {
this.errors = { title: "" };
try {
if (!this.group.id || !this.currentActor.id) return;
const { data } = await this.$apollo.mutate({
mutation: CREATE_DISCUSSION,
variables: {
title: this.discussion.title,
text: this.discussion.text,
actorId: parseInt(this.group.id, 10),
},
});
await this.$router.push({
name: RouteName.DISCUSSION,
params: {
id: data.createDiscussion.id,
slug: data.createDiscussion.slug,
},
});
} catch (error: any) {
console.error(error);
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
if (error.graphQLErrors[0].field == "title") {
this.errors.title = error.graphQLErrors[0].message;
} else {
this.$notifier.error(error.graphQLErrors[0].message);
}
}
onError((error) => {
console.error(error);
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
const graphQLError = error.graphQLErrors[0] as AbsintheGraphQLError;
if (graphQLError.field == "title") {
errors.title = graphQLError.message;
} else {
notifier?.error(graphQLError.message);
}
}
}
});
const createDiscussion = async (): Promise<void> => {
errors.title = "";
if (!group.value?.id || !currentActor.value?.id) return;
mutate({
title: discussion.title,
text: discussion.text,
actorId: group.value.id,
});
};
</script>
<style lang="scss" scoped>
.container.section {
background: $white;
}
.markdown-render h1 {
font-size: 2em;
}

View File

@@ -1,5 +1,5 @@
<template>
<div class="container section" v-if="discussion">
<div class="container mx-auto" v-if="discussion">
<breadcrumbs-nav
v-if="group"
:links="[
@@ -24,57 +24,57 @@
},
]"
/>
<b-message v-if="error" type="is-danger">
<o-notification v-if="error" variant="danger">
{{ error }}
</b-message>
<section>
<div class="discussion-title" dir="auto">
<h1 class="title" v-if="discussion.title && !editTitleMode">
</o-notification>
<section v-if="currentActor">
<div class="flex items-center gap-2" dir="auto">
<h1 class="" v-if="discussion.title && !editTitleMode">
{{ discussion.title }}
</h1>
<b-button
<o-button
icon-right="pencil"
size="is-small"
size="small"
:title="$t('Update discussion title')"
v-if="
discussion.creator &&
!editTitleMode &&
(currentActor.id === discussion.creator.id ||
(currentActor?.id === discussion.creator.id ||
isCurrentActorAGroupModerator)
"
@click="
() => {
newTitle = discussion.title;
newTitle = discussion?.title ?? '';
editTitleMode = true;
}
"
>
</b-button>
<b-skeleton
v-else-if="!editTitleMode && $apollo.loading"
</o-button>
<o-skeleton
v-else-if="!editTitleMode && discussionLoading"
height="50px"
animated
/>
<form
v-else-if="!$apollo.loading && !error"
v-else-if="!discussionLoading && !error"
@submit.prevent="updateDiscussion"
class="title-edit"
class="w-full"
>
<b-field :label="$t('Title')" label-for="discussion-title">
<b-input
<o-field :label="$t('Title')" label-for="discussion-title">
<o-input
:value="discussion.title"
v-model="newTitle"
id="discussion-title"
/>
</b-field>
<div class="buttons">
<b-button
type="is-primary"
</o-field>
<div class="flex gap-2 mt-2">
<o-button
variant="primary"
native-type="submit"
icon-right="check"
:title="$t('Update discussion title')"
/>
<b-button
<o-button
@click="
() => {
editTitleMode = false;
@@ -84,12 +84,12 @@
icon-right="close"
:title="$t('Cancel discussion title edition')"
/>
<b-button
<o-button
@click="openDeleteDiscussionConfirmation"
type="is-danger"
variant="danger"
native-type="button"
icon-left="delete"
>{{ $t("Delete conversation") }}</b-button
>{{ $t("Delete conversation") }}</o-button
>
</div>
</form>
@@ -97,31 +97,45 @@
<discussion-comment
v-for="comment in discussion.comments.elements"
:key="comment.id"
:comment="comment"
@update-comment="updateComment"
@delete-comment="deleteComment"
:model-value="comment"
:current-actor="currentActor"
@update:modelValue="
(comment: IComment) =>
updateComment({
commentId: comment.id as string,
text: comment.text,
})
"
@delete-comment="(comment: IComment) => deleteComment({
commentId: comment.id as string,
})"
/>
<b-button
<o-button
v-if="discussion.comments.elements.length < discussion.comments.total"
@click="loadMoreComments"
>{{ $t("Fetch more") }}</b-button
>{{ $t("Fetch more") }}</o-button
>
<form @submit.prevent="reply" v-if="!error">
<b-field :label="$t('Text')">
<editor v-model="newComment" :aria-label="$t('Comment body')" />
</b-field>
<b-button
<o-field :label="$t('Text')">
<Editor
v-model="newComment"
:aria-label="$t('Comment body')"
v-if="currentActor"
:currentActor="currentActor"
/>
</o-field>
<o-button
class="my-2"
native-type="submit"
:disabled="['<p></p>', ''].includes(newComment)"
type="is-primary"
>{{ $t("Reply") }}</b-button
variant="primary"
>{{ $t("Reply") }}</o-button
>
</form>
</section>
</div>
</template>
<script lang="ts">
import { Component, Prop } from "vue-property-decorator";
<script lang="ts" setup>
import {
GET_DISCUSSION,
REPLY_TO_DISCUSSION,
@@ -130,311 +144,312 @@ import {
DISCUSSION_COMMENT_CHANGED,
} from "@/graphql/discussion";
import { IDiscussion } from "@/types/discussions";
import { Discussion as DiscussionModel } from "@/types/discussions";
import { displayName, usernameWithDomain } from "@/types/actor";
import { displayName, IPerson, usernameWithDomain } from "@/types/actor";
import DiscussionComment from "@/components/Discussion/DiscussionComment.vue";
import { GraphQLError } from "graphql";
import { DELETE_COMMENT, UPDATE_COMMENT } from "@/graphql/comment";
import RouteName from "../../router/name";
import { IComment } from "../../types/comment.model";
import { ApolloCache, FetchResult, gql, Reference } from "@apollo/client/core";
import { mixins } from "vue-class-component";
import GroupMixin from "@/mixins/group";
import { ApolloCache, FetchResult, gql } from "@apollo/client/core";
import { useMutation, useQuery } from "@vue/apollo-composable";
import {
defineAsyncComponent,
onMounted,
onUnmounted,
ref,
computed,
inject,
} from "vue";
import { useHead } from "@vueuse/head";
import { useRouter } from "vue-router";
import { useCurrentActorClient } from "@/composition/apollo/actor";
import { AbsintheGraphQLError } from "@/types/errors.model";
import { useGroup } from "@/composition/apollo/group";
import { MemberRole } from "@/types/enums";
import { PERSON_MEMBERSHIPS } from "@/graphql/actor";
import { Dialog } from "@/plugins/dialog";
import { useI18n } from "vue-i18n";
@Component({
apollo: {
discussion: {
const props = defineProps<{ slug: string }>();
const page = ref(1);
const COMMENTS_PER_PAGE = 10;
const { currentActor } = useCurrentActorClient();
const {
result: discussionResult,
onError: onDiscussionError,
subscribeToMore,
fetchMore,
loading: discussionLoading,
} = useQuery<{ discussion: IDiscussion }>(
GET_DISCUSSION,
() => ({
slug: props.slug,
page: page.value,
limit: COMMENTS_PER_PAGE,
}),
() => ({
enabled: props.slug !== undefined,
})
);
subscribeToMore({
document: DISCUSSION_COMMENT_CHANGED,
variables: {
slug: props.slug,
page: page.value,
limit: COMMENTS_PER_PAGE,
},
updateQuery(
previousResult: any,
{ subscriptionData }: { subscriptionData: any }
) {
const previousDiscussion = previousResult.discussion;
const lastComment =
subscriptionData.data.discussionCommentChanged.lastComment;
hasMoreComments.value = !previousDiscussion.comments.elements.some(
(comment: IComment) => comment.id === lastComment.id
);
if (hasMoreComments.value) {
return {
discussion: {
...previousDiscussion,
lastComment: lastComment,
comments: {
elements: [
...previousDiscussion.comments.elements.filter(
({ id }: { id: string }) => id !== lastComment.id
),
lastComment,
],
total: previousDiscussion.comments.total + 1,
},
},
};
}
return previousDiscussion;
},
});
const discussion = computed(() => discussionResult.value?.discussion);
const { group } = useGroup(usernameWithDomain(discussion.value?.actor));
const Editor = defineAsyncComponent(() => import("@/components/Editor.vue"));
useHead({
title: computed(() => discussion.value?.title ?? ""),
});
const newComment = ref("");
const newTitle = ref("");
const editTitleMode = ref(false);
const hasMoreComments = ref(true);
const error = ref<string | null>(null);
const { mutate: replyToDiscussionMutation } = useMutation(REPLY_TO_DISCUSSION);
const reply = () => {
if (newComment.value === "") return;
replyToDiscussionMutation({
discussionId: discussion.value?.id,
text: newComment.value,
});
newComment.value = "";
};
const { mutate: updateComment } = useMutation<
{ updateComment: IComment },
{ commentId: string; text: string }
>(UPDATE_COMMENT, () => ({
update: (
store: ApolloCache<{ deleteComment: IComment }>,
{ data }: FetchResult
) => {
if (!data || !data.deleteComment) return;
const discussionData = store.readQuery<{
discussion: IDiscussion;
}>({
query: GET_DISCUSSION,
variables() {
return {
slug: this.slug,
page: 1,
limit: this.COMMENTS_PER_PAGE,
};
variables: {
slug: props.slug,
page: page.value,
},
skip() {
return !this.slug;
},
error({ graphQLErrors }) {
this.handleErrors(graphQLErrors);
},
subscribeToMore: {
document: DISCUSSION_COMMENT_CHANGED,
variables() {
return {
slug: this.$route.params.slug,
page: this.page,
limit: this.COMMENTS_PER_PAGE,
};
},
updateQuery: function (
previousResult: any,
{ subscriptionData }: { subscriptionData: any }
) {
const previousDiscussion = previousResult.discussion;
const lastComment =
subscriptionData.data.discussionCommentChanged.lastComment;
this.hasMoreComments = !previousDiscussion.comments.elements.some(
(comment: IComment) => comment.id === lastComment.id
);
if (this.hasMoreComments) {
return {
discussion: {
...previousDiscussion,
lastComment: lastComment,
comments: {
elements: [
...previousDiscussion.comments.elements.filter(
({ id }: { id: string }) => id !== lastComment.id
),
lastComment,
],
total: previousDiscussion.comments.total + 1,
},
},
};
});
if (!discussionData) return;
const { discussion: discussionCached } = discussionData;
const index = discussionCached.comments.elements.findIndex(
({ id }) => id === data.deleteComment.id
);
if (index > -1) {
discussionCached.comments.elements.splice(index, 1);
discussionCached.comments.total -= 1;
}
store.writeQuery({
query: GET_DISCUSSION,
variables: { slug: props.slug, page: page.value },
data: { discussion: discussionCached },
});
},
}));
const { mutate: deleteComment } = useMutation<
{ deleteComment: { id: string } },
{ commentId: string }
>(DELETE_COMMENT, () => ({
update: (store: ApolloCache<{ deleteComment: IComment }>, { data }) => {
const id = data?.deleteComment?.id;
if (!id) return;
store.writeFragment({
id: `Comment:${id}`,
fragment: gql`
fragment CommentDeleted on Comment {
deletedAt
actor {
id
}
return previousDiscussion;
},
},
},
},
components: {
DiscussionComment,
editor: () =>
import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
},
metaInfo() {
return {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
title: this.discussion.title,
};
},
})
export default class Discussion extends mixins(GroupMixin) {
@Prop({ type: String, required: true }) slug!: string;
discussion: IDiscussion = new DiscussionModel();
newComment = "";
newTitle = "";
editTitleMode = false;
page = 1;
hasMoreComments = true;
COMMENTS_PER_PAGE = 10;
RouteName = RouteName;
usernameWithDomain = usernameWithDomain;
displayName = displayName;
error: string | null = null;
async reply(): Promise<void> {
if (this.newComment === "") return;
await this.$apollo.mutate({
mutation: REPLY_TO_DISCUSSION,
variables: {
discussionId: this.discussion.id,
text: this.newComment,
},
});
this.newComment = "";
}
async updateComment(comment: IComment): Promise<void> {
await this.$apollo.mutate<{ deleteComment: IComment }>({
mutation: UPDATE_COMMENT,
variables: {
commentId: comment.id,
text: comment.text,
},
update: (
store: ApolloCache<{ deleteComment: IComment }>,
{ data }: FetchResult
) => {
if (!data || !data.deleteComment) return;
const discussionData = store.readQuery<{
discussion: IDiscussion;
}>({
query: GET_DISCUSSION,
variables: {
slug: this.slug,
page: this.page,
},
});
if (!discussionData) return;
const { discussion: discussionCached } = discussionData;
const index = discussionCached.comments.elements.findIndex(
({ id }) => id === data.deleteComment.id
);
if (index > -1) {
discussionCached.comments.elements.splice(index, 1);
discussionCached.comments.total -= 1;
text
}
store.writeQuery({
query: GET_DISCUSSION,
variables: { slug: this.slug, page: this.page },
data: { discussion: discussionCached },
});
`,
data: {
deletedAt: new Date(),
text: "",
actor: null,
},
});
}
},
}));
async deleteComment(comment: IComment): Promise<void> {
await this.$apollo.mutate<{ deleteComment: IComment }>({
mutation: DELETE_COMMENT,
const loadMoreComments = async (): Promise<void> => {
if (!hasMoreComments.value) return;
page.value++;
try {
await fetchMore({
// New variables
variables: {
commentId: comment.id,
slug: props.slug,
page: page.value,
limit: COMMENTS_PER_PAGE,
},
update: (store: ApolloCache<{ deleteComment: IComment }>) => {
store.writeFragment({
id: store.identify(comment as unknown as Reference),
fragment: gql`
fragment CommentDeleted on Comment {
deletedAt
actor {
id
}
text
}
`,
data: {
deletedAt: new Date(),
text: "",
actor: null,
},
});
});
hasMoreComments.value = !discussion.value?.comments.elements
.map(({ id }) => id)
.includes(discussion.value?.lastComment?.id);
} catch (e) {
console.error(e);
}
};
const { mutate: updateDiscussionMutation } = useMutation<{
updateDiscussion: IDiscussion;
}>(UPDATE_DISCUSSION);
const updateDiscussion = async (): Promise<void> => {
updateDiscussionMutation({
discussionId: discussion.value?.id,
title: newTitle.value,
});
editTitleMode.value = false;
};
const { t } = useI18n({ useScope: "global" });
const dialog = inject<Dialog>("dialog");
const openDeleteDiscussionConfirmation = (): void => {
dialog?.confirm({
type: "is-danger",
title: t("Delete this discussion"),
message: t("Are you sure you want to delete this entire discussion?"),
confirmText: t("Delete discussion"),
cancelText: t("Cancel"),
onConfirm: () =>
deleteConversation({
discussionId: discussion.value?.id,
}),
});
};
const router = useRouter();
const { mutate: deleteConversation, onDone: deleteConversationDone } =
useMutation(DELETE_DISCUSSION);
deleteConversationDone(() => {
if (discussion.value?.actor) {
router.push({
name: RouteName.DISCUSSION_LIST,
params: {
preferredUsername: usernameWithDomain(discussion.value.actor),
},
});
}
});
async loadMoreComments(): Promise<void> {
if (!this.hasMoreComments) return;
this.page++;
try {
await this.$apollo.queries.discussion.fetchMore({
// New variables
variables: {
slug: this.slug,
page: this.page,
limit: this.COMMENTS_PER_PAGE,
},
});
this.hasMoreComments = !this.discussion.comments.elements
.map(({ id }) => id)
.includes(this.discussion?.lastComment?.id);
} catch (e) {
console.error(e);
}
}
onDiscussionError((discussionError) =>
handleErrors(discussionError.graphQLErrors as AbsintheGraphQLError[])
);
async updateDiscussion(): Promise<void> {
await this.$apollo.mutate<{ updateDiscussion: IDiscussion }>({
mutation: UPDATE_DISCUSSION,
variables: {
discussionId: this.discussion.id,
title: this.newTitle,
},
});
this.editTitleMode = false;
const handleErrors = async (errors: AbsintheGraphQLError[]): Promise<void> => {
if (errors[0].message.includes("No such discussion")) {
await router.push({ name: RouteName.PAGE_NOT_FOUND });
}
if (errors[0].code === "unauthorized") {
error.value = errors[0].message;
}
};
openDeleteDiscussionConfirmation(): void {
this.$buefy.dialog.confirm({
type: "is-danger",
title: this.$t("Delete this discussion") as string,
message: this.$t(
"Are you sure you want to delete this entire discussion?"
) as string,
confirmText: this.$t("Delete discussion") as string,
cancelText: this.$t("Cancel") as string,
onConfirm: () => this.deleteConversation(),
});
}
onMounted(() => {
window.addEventListener("scroll", handleScroll);
});
async deleteConversation(): Promise<void> {
await this.$apollo.mutate({
mutation: DELETE_DISCUSSION,
variables: {
discussionId: this.discussion.id,
},
});
if (this.discussion.actor) {
this.$router.push({
name: RouteName.DISCUSSION_LIST,
params: {
preferredUsername: usernameWithDomain(this.discussion.actor),
},
});
}
}
onUnmounted(() => {
window.removeEventListener("scroll", handleScroll);
});
async handleErrors(errors: GraphQLError[]): Promise<void> {
if (errors[0].message.includes("No such discussion")) {
await this.$router.push({ name: RouteName.PAGE_NOT_FOUND });
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (errors[0].code === "unauthorized") {
this.error = errors[0].message;
}
const handleScroll = (): void => {
const scrollTop =
(document.documentElement && document.documentElement.scrollTop) ||
document.body.scrollTop;
const scrollHeight =
(document.documentElement && document.documentElement.scrollHeight) ||
document.body.scrollHeight;
const clientHeight =
document.documentElement.clientHeight || window.innerHeight;
const scrolledToBottom =
Math.ceil(scrollTop + clientHeight + 800) >= scrollHeight;
if (scrolledToBottom) {
loadMoreComments();
}
};
mounted(): void {
window.addEventListener("scroll", this.handleScroll);
}
const isCurrentActorAGroupModerator = computed((): boolean => {
return hasCurrentActorThisRole([
MemberRole.MODERATOR,
MemberRole.ADMINISTRATOR,
]);
});
destroyed(): void {
window.removeEventListener("scroll", this.handleScroll);
}
const { result: membershipsResult } = useQuery<{
person: Pick<IPerson, "memberships">;
}>(
PERSON_MEMBERSHIPS,
() => ({ id: currentActor.value?.id }),
() => ({ enabled: currentActor.value?.id !== undefined })
);
const memberships = computed(() => membershipsResult.value?.person.memberships);
handleScroll(): void {
const scrollTop =
(document.documentElement && document.documentElement.scrollTop) ||
document.body.scrollTop;
const scrollHeight =
(document.documentElement && document.documentElement.scrollHeight) ||
document.body.scrollHeight;
const clientHeight =
document.documentElement.clientHeight || window.innerHeight;
const scrolledToBottom =
Math.ceil(scrollTop + clientHeight + 800) >= scrollHeight;
if (scrolledToBottom) {
this.loadMoreComments();
}
}
}
const hasCurrentActorThisRole = (givenRole: string | string[]): boolean => {
const roles = Array.isArray(givenRole)
? givenRole
: ([givenRole] as MemberRole[]);
return (
(memberships.value?.total ?? 0) > 0 &&
roles.includes(memberships.value?.elements[0].role as MemberRole)
);
};
</script>
<style lang="scss" scoped>
@use "@/styles/_mixins" as *;
div.container.section {
background: white;
padding: 1rem 5% 4rem;
div.discussion-title {
margin-bottom: 1.75rem;
display: flex;
align-items: center;
h1.title {
margin-bottom: 0;
@include margin-right(10px);
}
form.title-edit {
flex: 1;
div.control {
margin-bottom: 0.75rem;
}
}
}
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div class="container section" v-if="group">
<div class="container mx-auto section" v-if="group">
<breadcrumbs-nav
:links="[
{
@@ -26,13 +26,13 @@
)
}}
</p>
<b-button
<o-button
tag="router-link"
:to="{
name: RouteName.CREATE_DISCUSSION,
params: { preferredUsername },
}"
>{{ $t("New discussion") }}</b-button
>{{ $t("New discussion") }}</o-button
>
<div v-if="group.discussions.elements.length > 0">
<discussion-list-item
@@ -40,7 +40,7 @@
v-for="discussion in group.discussions.elements"
:key="discussion.id"
/>
<b-pagination
<o-pagination
class="discussion-pagination"
:total="group.discussions.total"
v-model="page"
@@ -50,13 +50,13 @@
:aria-page-label="$t('Page')"
:aria-current-label="$t('Current page')"
>
</b-pagination>
</o-pagination>
</div>
<empty-content v-else icon="chat">
{{ $t("There's no discussions yet") }}
</empty-content>
</section>
<section class="section" v-else-if="!$apollo.loading">
<section class="section" v-else-if="!groupLoading && !personLoading">
<empty-content icon="chat">
{{ $t("Only group members can access discussions") }}
<template #desc>
@@ -70,157 +70,59 @@
</section>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { FETCH_GROUP } from "@/graphql/group";
import {
displayName,
IActor,
IGroup,
IPerson,
usernameWithDomain,
} from "@/types/actor";
<script lang="ts" setup>
import { displayName, usernameWithDomain } from "@/types/actor";
import DiscussionListItem from "@/components/Discussion/DiscussionListItem.vue";
import RouteName from "../../router/name";
import { MemberRole } from "@/types/enums";
import {
CURRENT_ACTOR_CLIENT,
GROUP_MEMBERSHIP_SUBSCRIPTION_CHANGED,
PERSON_STATUS_GROUP,
} from "@/graphql/actor";
import { IMember } from "@/types/actor/member.model";
import EmptyContent from "@/components/Utils/EmptyContent.vue";
import VueRouter from "vue-router";
const { isNavigationFailure, NavigationFailureType } = VueRouter;
import { useGroup } from "@/composition/apollo/group";
import { usePersonStatusGroup } from "@/composition/apollo/actor";
import { useI18n } from "vue-i18n";
import { useRouteQuery, integerTransformer } from "vue-use-route-query";
import { useHead } from "@vueuse/head";
import { computed } from "vue";
const page = useRouteQuery("page", 1, integerTransformer);
const DISCUSSIONS_PER_PAGE = 10;
@Component({
components: { DiscussionListItem, EmptyContent },
apollo: {
group: {
query: FETCH_GROUP,
fetchPolicy: "cache-and-network",
variables() {
return {
name: this.preferredUsername,
discussionsPage: this.page,
discussionsLimit: DISCUSSIONS_PER_PAGE,
};
},
skip() {
return !this.preferredUsername;
},
},
person: {
query: PERSON_STATUS_GROUP,
fetchPolicy: "cache-and-network",
variables() {
return {
id: this.currentActor.id,
group: this.preferredUsername,
};
},
subscribeToMore: {
document: GROUP_MEMBERSHIP_SUBSCRIPTION_CHANGED,
variables() {
return {
actorId: this.currentActor.id,
group: this.preferredUsername,
};
},
skip() {
return (
!this.currentActor ||
!this.currentActor.id ||
!this.preferredUsername
);
},
},
skip() {
return (
!this.currentActor || !this.currentActor.id || !this.preferredUsername
);
},
},
currentActor: CURRENT_ACTOR_CLIENT,
},
metaInfo() {
return {
title: this.$t("Discussions") as string,
};
},
})
export default class DiscussionsList extends Vue {
@Prop({ type: String, required: true }) preferredUsername!: string;
const props = defineProps<{ preferredUsername: string }>();
person!: IPerson;
const { group, loading: groupLoading } = useGroup(props.preferredUsername, {
discussionsPage: page.value,
discussionsLimit: DISCUSSIONS_PER_PAGE,
});
group!: IGroup;
const { person, loading: personLoading } = usePersonStatusGroup(
props.preferredUsername
);
currentActor!: IActor;
const { t } = useI18n({ useScope: "global" });
RouteName = RouteName;
useHead({
title: computed(() => t("Discussions")),
});
usernameWithDomain = usernameWithDomain;
displayName = displayName;
const groupMemberships = computed((): (string | undefined)[] => {
if (!person.value || !person.value.id) return [];
return person.value.memberships.elements
.filter(
(membership: IMember) =>
![
MemberRole.REJECTED,
MemberRole.NOT_APPROVED,
MemberRole.INVITED,
].includes(membership.role)
)
.map(({ parent: { id } }) => id);
});
DISCUSSIONS_PER_PAGE = DISCUSSIONS_PER_PAGE;
get page(): number {
return parseInt((this.$route.query.page as string) || "1", 10);
}
set page(page: number) {
this.pushRouter(RouteName.DISCUSSION_LIST, {
page: page.toString(),
});
}
get groupMemberships(): (string | undefined)[] {
if (!this.person || !this.person.id) return [];
return this.person.memberships.elements
.filter(
(membership: IMember) =>
![
MemberRole.REJECTED,
MemberRole.NOT_APPROVED,
MemberRole.INVITED,
].includes(membership.role)
)
.map(({ parent: { id } }) => id);
}
get isCurrentActorAGroupMember(): boolean {
return (
this.groupMemberships !== undefined &&
this.groupMemberships.includes(this.group.id)
);
}
protected async pushRouter(
routeName: string,
args: Record<string, string>
): Promise<void> {
try {
await this.$router.push({
name: routeName,
query: { ...this.$route.query, ...args },
});
} catch (e) {
if (isNavigationFailure(e, NavigationFailureType.redirected)) {
throw Error(e.toString());
}
}
}
}
const isCurrentActorAGroupMember = computed((): boolean => {
return (
groupMemberships.value !== undefined &&
groupMemberships.value.includes(group.value?.id)
);
});
</script>
<style lang="scss">
div.container.section {
background: white;
.discussion-pagination {
margin-top: 1rem;
}
}
</style>