@@ -83,6 +83,7 @@ import { IStatistics } from "../../types/statistics.model";
|
||||
})
|
||||
export default class AboutInstance extends Vue {
|
||||
config!: IConfig;
|
||||
|
||||
statistics!: IStatistics;
|
||||
|
||||
get isContactEmail(): boolean {
|
||||
@@ -97,7 +98,8 @@ export default class AboutInstance extends Vue {
|
||||
if (!this.config.contact) return null;
|
||||
if (this.isContactEmail) {
|
||||
return { uri: `mailto:${this.config.contact}`, text: this.config.contact };
|
||||
} else if (this.isContactURL) {
|
||||
}
|
||||
if (this.isContactURL) {
|
||||
return {
|
||||
uri: this.config.contact,
|
||||
text: this.urlToHostname(this.config.contact) || (this.$t("Contact") as string),
|
||||
|
||||
@@ -160,7 +160,7 @@ const EVENTS_PER_PAGE = 10;
|
||||
},
|
||||
})
|
||||
export default class AdminProfile extends Vue {
|
||||
@Prop({ required: true }) id!: String;
|
||||
@Prop({ required: true }) id!: string;
|
||||
|
||||
person!: IPerson;
|
||||
|
||||
@@ -171,6 +171,7 @@ export default class AdminProfile extends Vue {
|
||||
EVENTS_PER_PAGE = EVENTS_PER_PAGE;
|
||||
|
||||
organizedEventsPage = 1;
|
||||
|
||||
participationsPage = 1;
|
||||
|
||||
get metadata(): Array<object> {
|
||||
|
||||
@@ -81,7 +81,7 @@ import { IPerson } from "../../types/actor";
|
||||
},
|
||||
})
|
||||
export default class AdminUserProfile extends Vue {
|
||||
@Prop({ required: true }) id!: String;
|
||||
@Prop({ required: true }) id!: string;
|
||||
|
||||
user!: IUser;
|
||||
|
||||
|
||||
@@ -105,13 +105,19 @@ const PROFILES_PER_PAGE = 10;
|
||||
})
|
||||
export default class Profiles extends Vue {
|
||||
page = 1;
|
||||
|
||||
preferredUsername = "";
|
||||
|
||||
name = "";
|
||||
|
||||
domain = "";
|
||||
|
||||
local = true;
|
||||
|
||||
suspended = false;
|
||||
|
||||
PROFILES_PER_PAGE = PROFILES_PER_PAGE;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
async onPageChange(page: number) {
|
||||
|
||||
@@ -270,6 +270,7 @@ export default class Settings extends Vue {
|
||||
adminSettings!: IAdminSettings;
|
||||
|
||||
InstanceTermsType = InstanceTermsType;
|
||||
|
||||
InstancePrivacyType = InstancePrivacyType;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
@@ -109,9 +109,11 @@ const USERS_PER_PAGE = 10;
|
||||
})
|
||||
export default class Users extends Vue {
|
||||
page = 1;
|
||||
|
||||
email = "";
|
||||
|
||||
USERS_PER_PAGE = USERS_PER_PAGE;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
async onPageChange(page: number) {
|
||||
|
||||
@@ -1,243 +0,0 @@
|
||||
<template>
|
||||
<div class="container section" v-if="conversation">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link :to="{ name: RouteName.MY_GROUPS }">{{ $t("My groups") }}</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: conversation.actor.preferredUsername },
|
||||
}"
|
||||
>{{ `@${conversation.actor.preferredUsername}` }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.CONVERSATION_LIST,
|
||||
params: { preferredUsername: conversation.actor.preferredUsername },
|
||||
}"
|
||||
>{{ $t("Discussions") }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<router-link :to="{ name: RouteName.CONVERSATION, params: { id: conversation.id } }">{{
|
||||
conversation.title
|
||||
}}</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<section>
|
||||
<div class="conversation-title">
|
||||
<h2 class="title" v-if="!editTitleMode">
|
||||
{{ conversation.title }}
|
||||
<span
|
||||
@click="
|
||||
() => {
|
||||
newTitle = conversation.title;
|
||||
editTitleMode = true;
|
||||
}
|
||||
"
|
||||
>
|
||||
<b-icon icon="pencil" />
|
||||
</span>
|
||||
</h2>
|
||||
<form v-else @submit.prevent="updateConversation" class="title-edit">
|
||||
<b-input :value="conversation.title" v-model="newTitle" />
|
||||
<div class="buttons">
|
||||
<b-button type="is-primary" native-type="submit" icon-right="check" />
|
||||
<b-button
|
||||
@click="
|
||||
() => {
|
||||
editTitleMode = false;
|
||||
newTitle = '';
|
||||
}
|
||||
"
|
||||
icon-right="close"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<conversation-comment
|
||||
v-for="comment in conversation.comments.elements"
|
||||
:key="comment.id"
|
||||
:comment="comment"
|
||||
/>
|
||||
<b-button
|
||||
v-if="conversation.comments.elements.length < conversation.comments.total"
|
||||
@click="loadMoreComments"
|
||||
>Fetch more</b-button
|
||||
>
|
||||
<form @submit.prevent="reply">
|
||||
<b-field :label="$t('Text')">
|
||||
<editor v-model="newComment" />
|
||||
</b-field>
|
||||
<b-button native-type="submit" type="is-primary">{{ $t("Reply") }}</b-button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import {
|
||||
GET_CONVERSATION,
|
||||
REPLY_TO_CONVERSATION,
|
||||
UPDATE_CONVERSATION,
|
||||
} from "@/graphql/conversation";
|
||||
import { IConversation } from "@/types/conversations";
|
||||
import ConversationComment from "@/components/Conversation/ConversationComment.vue";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
conversation: {
|
||||
query: GET_CONVERSATION,
|
||||
variables() {
|
||||
return {
|
||||
id: this.id,
|
||||
page: 1,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.id;
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
ConversationComment,
|
||||
editor: () => import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
|
||||
},
|
||||
})
|
||||
export default class Conversation extends Vue {
|
||||
@Prop({ type: String, required: true }) id!: string;
|
||||
|
||||
conversation!: IConversation;
|
||||
|
||||
newComment = "";
|
||||
|
||||
newTitle = "";
|
||||
|
||||
editTitleMode = false;
|
||||
|
||||
page = 1;
|
||||
|
||||
hasMoreComments = true;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
async reply() {
|
||||
await this.$apollo.mutate({
|
||||
mutation: REPLY_TO_CONVERSATION,
|
||||
variables: {
|
||||
conversationId: this.conversation.id,
|
||||
text: this.newComment,
|
||||
},
|
||||
update: (store, { data: { replyToConversation } }) => {
|
||||
const conversationData = store.readQuery<{
|
||||
conversation: IConversation;
|
||||
}>({
|
||||
query: GET_CONVERSATION,
|
||||
variables: {
|
||||
id: this.id,
|
||||
page: this.page,
|
||||
},
|
||||
});
|
||||
if (!conversationData) return;
|
||||
const { conversation } = conversationData;
|
||||
conversation.lastComment = replyToConversation.lastComment;
|
||||
conversation.comments.elements.push(replyToConversation.lastComment);
|
||||
conversation.comments.total += 1;
|
||||
store.writeQuery({
|
||||
query: GET_CONVERSATION,
|
||||
variables: { id: this.id, page: this.page },
|
||||
data: { conversation },
|
||||
});
|
||||
},
|
||||
});
|
||||
this.newComment = "";
|
||||
}
|
||||
|
||||
async loadMoreComments() {
|
||||
this.page += 1;
|
||||
try {
|
||||
console.log(this.$apollo.queries.conversation);
|
||||
await this.$apollo.queries.conversation.fetchMore({
|
||||
// New variables
|
||||
variables: {
|
||||
id: this.id,
|
||||
page: this.page,
|
||||
},
|
||||
// Transform the previous result with new data
|
||||
updateQuery: (previousResult, { fetchMoreResult }) => {
|
||||
if (!fetchMoreResult) return previousResult;
|
||||
const newComments = fetchMoreResult.conversation.comments.elements;
|
||||
this.hasMoreComments = newComments.length === 1;
|
||||
const { conversation } = previousResult;
|
||||
conversation.comments.elements = [
|
||||
...previousResult.conversation.comments.elements,
|
||||
...newComments,
|
||||
];
|
||||
|
||||
return { conversation };
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async updateConversation() {
|
||||
await this.$apollo.mutate({
|
||||
mutation: UPDATE_CONVERSATION,
|
||||
variables: {
|
||||
conversationId: this.conversation.id,
|
||||
title: this.newTitle,
|
||||
},
|
||||
update: (store, { data: { updateConversation } }) => {
|
||||
const conversationData = store.readQuery<{
|
||||
conversation: IConversation;
|
||||
}>({
|
||||
query: GET_CONVERSATION,
|
||||
variables: {
|
||||
id: this.id,
|
||||
page: this.page,
|
||||
},
|
||||
});
|
||||
if (!conversationData) return;
|
||||
const { conversation } = conversationData;
|
||||
conversation.title = updateConversation.title;
|
||||
store.writeQuery({
|
||||
query: GET_CONVERSATION,
|
||||
variables: { id: this.id, page: this.page },
|
||||
data: { conversation },
|
||||
});
|
||||
},
|
||||
});
|
||||
this.editTitleMode = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
div.container.section {
|
||||
background: white;
|
||||
|
||||
div.conversation-title {
|
||||
margin-bottom: 0.75rem;
|
||||
|
||||
h2.title {
|
||||
span {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
form.title-edit {
|
||||
div.control {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -2,13 +2,13 @@
|
||||
<section class="section container">
|
||||
<h1>{{ $t("Create a discussion") }}</h1>
|
||||
|
||||
<form @submit.prevent="createConversation">
|
||||
<form @submit.prevent="createDiscussion">
|
||||
<b-field :label="$t('Title')">
|
||||
<b-input aria-required="true" required v-model="conversation.title" />
|
||||
<b-input aria-required="true" required v-model="discussion.title" />
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('Text')">
|
||||
<editor v-model="conversation.text" />
|
||||
<editor v-model="discussion.text" />
|
||||
</b-field>
|
||||
|
||||
<button class="button is-primary" type="submit">{{ $t("Create the discussion") }}</button>
|
||||
@@ -20,7 +20,7 @@
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { IGroup, IPerson } from "@/types/actor";
|
||||
import { CURRENT_ACTOR_CLIENT, FETCH_GROUP } from "@/graphql/actor";
|
||||
import { CREATE_CONVERSATION } from "@/graphql/conversation";
|
||||
import { CREATE_DISCUSSION } from "@/graphql/discussion";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
@@ -41,36 +41,45 @@ import RouteName from "../../router/name";
|
||||
},
|
||||
},
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
||||
// @ts-ignore
|
||||
title: this.$t("Create a discussion") as string,
|
||||
// all titles will be injected into this template
|
||||
titleTemplate: "%s | Mobilizon",
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class CreateConversation extends Vue {
|
||||
export default class CreateDiscussion extends Vue {
|
||||
@Prop({ type: String, required: true }) preferredUsername!: string;
|
||||
|
||||
group!: IGroup;
|
||||
|
||||
currentActor!: IPerson;
|
||||
|
||||
conversation = { title: "", text: "" };
|
||||
discussion = { title: "", text: "" };
|
||||
|
||||
async createConversation() {
|
||||
async createDiscussion() {
|
||||
try {
|
||||
const { data } = await this.$apollo.mutate({
|
||||
mutation: CREATE_CONVERSATION,
|
||||
mutation: CREATE_DISCUSSION,
|
||||
variables: {
|
||||
title: this.conversation.title,
|
||||
text: this.conversation.text,
|
||||
title: this.discussion.title,
|
||||
text: this.discussion.text,
|
||||
actorId: this.group.id,
|
||||
creatorId: this.currentActor.id,
|
||||
},
|
||||
// update: (store, { data: { createConversation } }) => {
|
||||
// update: (store, { data: { createDiscussion } }) => {
|
||||
// // TODO: update group list cache
|
||||
// },
|
||||
});
|
||||
|
||||
await this.$router.push({
|
||||
name: RouteName.CONVERSATION,
|
||||
name: RouteName.DISCUSSION,
|
||||
params: {
|
||||
id: data.createConversation.id,
|
||||
slug: data.createConversation.slug,
|
||||
id: data.createDiscussion.id,
|
||||
slug: data.createDiscussion.slug,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
350
js/src/views/Discussions/Discussion.vue
Normal file
350
js/src/views/Discussions/Discussion.vue
Normal file
@@ -0,0 +1,350 @@
|
||||
<template>
|
||||
<div class="container section" v-if="discussion">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link :to="{ name: RouteName.MY_GROUPS }">{{ $t("My groups") }}</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link
|
||||
v-if="discussion.actor"
|
||||
:to="{
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: usernameWithDomain(discussion.actor) },
|
||||
}"
|
||||
>{{ discussion.actor.name }}</router-link
|
||||
>
|
||||
<b-skeleton v-else animated />
|
||||
</li>
|
||||
<li>
|
||||
<router-link
|
||||
v-if="discussion.actor"
|
||||
:to="{
|
||||
name: RouteName.DISCUSSION_LIST,
|
||||
params: { preferredUsername: usernameWithDomain(discussion.actor) },
|
||||
}"
|
||||
>{{ $t("Discussions") }}</router-link
|
||||
>
|
||||
<b-skeleton animated v-else />
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<router-link :to="{ name: RouteName.DISCUSSION, params: { id: discussion.id } }">{{
|
||||
discussion.title
|
||||
}}</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<section>
|
||||
<div class="discussion-title">
|
||||
<h2 class="title" v-if="discussion.title && !editTitleMode">
|
||||
{{ discussion.title }}
|
||||
<span
|
||||
@click="
|
||||
() => {
|
||||
newTitle = discussion.title;
|
||||
editTitleMode = true;
|
||||
}
|
||||
"
|
||||
>
|
||||
<b-icon icon="pencil" />
|
||||
</span>
|
||||
</h2>
|
||||
<b-skeleton v-else-if="!editTitleMode" height="50px" animated />
|
||||
<form v-else @submit.prevent="updateDiscussion" class="title-edit">
|
||||
<b-input :value="discussion.title" v-model="newTitle" />
|
||||
<div class="buttons">
|
||||
<b-button type="is-primary" native-type="submit" icon-right="check" />
|
||||
<b-button
|
||||
@click="
|
||||
() => {
|
||||
editTitleMode = false;
|
||||
newTitle = '';
|
||||
}
|
||||
"
|
||||
icon-right="close"
|
||||
/>
|
||||
<b-button
|
||||
@click="deleteConversation"
|
||||
type="is-danger"
|
||||
native-type="button"
|
||||
icon-left="delete"
|
||||
>{{ $t("Delete conversation") }}</b-button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<discussion-comment
|
||||
v-for="comment in discussion.comments.elements"
|
||||
:key="comment.id"
|
||||
:comment="comment"
|
||||
/>
|
||||
<b-button
|
||||
v-if="discussion.comments.elements.length < discussion.comments.total"
|
||||
@click="loadMoreComments"
|
||||
>{{ $t("Fetch more") }}</b-button
|
||||
>
|
||||
<form @submit.prevent="reply">
|
||||
<b-field :label="$t('Text')">
|
||||
<editor v-model="newComment" />
|
||||
</b-field>
|
||||
<b-button native-type="submit" type="is-primary">{{ $t("Reply") }}</b-button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import {
|
||||
GET_DISCUSSION,
|
||||
REPLY_TO_DISCUSSION,
|
||||
UPDATE_DISCUSSION,
|
||||
DELETE_DISCUSSION,
|
||||
DISCUSSION_COMMENT_CHANGED,
|
||||
} from "@/graphql/discussion";
|
||||
import { IDiscussion, Discussion } from "@/types/discussions";
|
||||
import { usernameWithDomain } from "@/types/actor";
|
||||
import DiscussionComment from "@/components/Discussion/DiscussionComment.vue";
|
||||
import { GraphQLError } from "graphql";
|
||||
import RouteName from "../../router/name";
|
||||
import { IComment } from "../../types/comment.model";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
discussion: {
|
||||
query: GET_DISCUSSION,
|
||||
variables() {
|
||||
return {
|
||||
slug: this.slug,
|
||||
page: 1,
|
||||
limit: this.COMMENTS_PER_PAGE,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.slug;
|
||||
},
|
||||
error({ graphQLErrors }) {
|
||||
this.handleErrors(graphQLErrors);
|
||||
},
|
||||
update: (data) => new Discussion(data.discussion),
|
||||
subscribeToMore: {
|
||||
document: DISCUSSION_COMMENT_CHANGED,
|
||||
variables() {
|
||||
return {
|
||||
slug: this.slug,
|
||||
};
|
||||
},
|
||||
updateQuery: (previousResult, { subscriptionData }) => {
|
||||
const previousDiscussion = previousResult.discussion;
|
||||
console.log("updating subscription with ", subscriptionData);
|
||||
if (
|
||||
!previousDiscussion.comments.elements.find(
|
||||
(comment: IComment) =>
|
||||
comment.id === subscriptionData.data.discussionCommentChanged.lastComment.id
|
||||
)
|
||||
) {
|
||||
previousDiscussion.lastComment =
|
||||
subscriptionData.data.discussionCommentChanged.lastComment;
|
||||
previousDiscussion.comments.elements.push(
|
||||
subscriptionData.data.discussionCommentChanged.lastComment
|
||||
);
|
||||
previousDiscussion.comments.total += 1;
|
||||
}
|
||||
|
||||
return previousDiscussion;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
DiscussionComment,
|
||||
editor: () => import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
||||
// @ts-ignore
|
||||
title: this.discussion.title,
|
||||
// all titles will be injected into this template
|
||||
titleTemplate: "%s | Mobilizon",
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class discussion extends Vue {
|
||||
@Prop({ type: String, required: true }) slug!: string;
|
||||
|
||||
discussion: IDiscussion = new Discussion();
|
||||
|
||||
newComment = "";
|
||||
|
||||
newTitle = "";
|
||||
|
||||
editTitleMode = false;
|
||||
|
||||
page = 1;
|
||||
|
||||
hasMoreComments = true;
|
||||
|
||||
COMMENTS_PER_PAGE = 10;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
|
||||
async reply() {
|
||||
await this.$apollo.mutate({
|
||||
mutation: REPLY_TO_DISCUSSION,
|
||||
variables: {
|
||||
discussionId: this.discussion.id,
|
||||
text: this.newComment,
|
||||
},
|
||||
update: (store, { data: { replyToDiscussion } }) => {
|
||||
const discussionData = store.readQuery<{
|
||||
discussion: IDiscussion;
|
||||
}>({
|
||||
query: GET_DISCUSSION,
|
||||
variables: {
|
||||
slug: this.slug,
|
||||
page: this.page,
|
||||
},
|
||||
});
|
||||
if (!discussionData) return;
|
||||
const { discussion } = discussionData;
|
||||
discussion.lastComment = replyToDiscussion.lastComment;
|
||||
discussion.comments.elements.push(replyToDiscussion.lastComment);
|
||||
discussion.comments.total += 1;
|
||||
store.writeQuery({
|
||||
query: GET_DISCUSSION,
|
||||
variables: { slug: this.slug, page: this.page },
|
||||
data: { discussion },
|
||||
});
|
||||
},
|
||||
// We don't need to handle cache update since there's the subscription that handles this for us
|
||||
});
|
||||
this.newComment = "";
|
||||
}
|
||||
|
||||
async loadMoreComments() {
|
||||
if (!this.hasMoreComments) return;
|
||||
this.page += 1;
|
||||
try {
|
||||
await this.$apollo.queries.discussion.fetchMore({
|
||||
// New variables
|
||||
variables: {
|
||||
slug: this.slug,
|
||||
page: this.page,
|
||||
limit: this.COMMENTS_PER_PAGE,
|
||||
},
|
||||
// Transform the previous result with new data
|
||||
updateQuery: (previousResult, { fetchMoreResult }) => {
|
||||
if (!fetchMoreResult) return previousResult;
|
||||
const newComments = fetchMoreResult.discussion.comments.elements;
|
||||
this.hasMoreComments = newComments.length === 1;
|
||||
const { discussion } = previousResult;
|
||||
discussion.comments.elements = [
|
||||
...previousResult.discussion.comments.elements,
|
||||
...newComments,
|
||||
];
|
||||
|
||||
return { discussion };
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async updateDiscussion() {
|
||||
await this.$apollo.mutate({
|
||||
mutation: UPDATE_DISCUSSION,
|
||||
variables: {
|
||||
discussionId: this.discussion.id,
|
||||
title: this.newTitle,
|
||||
},
|
||||
update: (store, { data: { updateDiscussion } }) => {
|
||||
const discussionData = store.readQuery<{
|
||||
discussion: IDiscussion;
|
||||
}>({
|
||||
query: GET_DISCUSSION,
|
||||
variables: {
|
||||
slug: this.slug,
|
||||
page: this.page,
|
||||
},
|
||||
});
|
||||
if (!discussionData) return;
|
||||
const { discussion } = discussionData;
|
||||
discussion.title = updateDiscussion.title;
|
||||
store.writeQuery({
|
||||
query: GET_DISCUSSION,
|
||||
variables: { slug: this.slug, page: this.page },
|
||||
data: { discussion },
|
||||
});
|
||||
},
|
||||
});
|
||||
this.editTitleMode = false;
|
||||
}
|
||||
|
||||
async deleteConversation() {
|
||||
await this.$apollo.mutate({
|
||||
mutation: DELETE_DISCUSSION,
|
||||
variables: {
|
||||
discussionId: this.discussion.id,
|
||||
},
|
||||
});
|
||||
if (this.discussion.actor) {
|
||||
return this.$router.push({
|
||||
name: RouteName.DISCUSSION_LIST,
|
||||
params: { preferredUsername: usernameWithDomain(this.discussion.actor) },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async handleErrors(errors: GraphQLError[]) {
|
||||
if (errors[0].message.includes("No such discussion")) {
|
||||
await this.$router.push({ name: RouteName.PAGE_NOT_FOUND });
|
||||
}
|
||||
}
|
||||
|
||||
mounted() {
|
||||
window.addEventListener("scroll", this.handleScroll);
|
||||
}
|
||||
|
||||
destroyed() {
|
||||
window.removeEventListener("scroll", this.handleScroll);
|
||||
}
|
||||
|
||||
handleScroll() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
div.container.section {
|
||||
background: white;
|
||||
padding: 1rem 5% 4rem;
|
||||
|
||||
div.discussion-title {
|
||||
margin-bottom: 0.75rem;
|
||||
|
||||
h2.title {
|
||||
span {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
form.title-edit {
|
||||
div.control {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -17,7 +17,7 @@
|
||||
<li class="is-active">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.CONVERSATION_LIST,
|
||||
name: RouteName.DISCUSSION_LIST,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>{{ $t("Discussions") }}</router-link
|
||||
@@ -26,17 +26,17 @@
|
||||
</ul>
|
||||
</nav>
|
||||
<section>
|
||||
<div v-if="group.conversations.elements.length > 0">
|
||||
<conversation-list-item
|
||||
:conversation="conversation"
|
||||
v-for="conversation in group.conversations.elements"
|
||||
:key="conversation.id"
|
||||
<div v-if="group.discussions.elements.length > 0">
|
||||
<discussion-list-item
|
||||
:discussion="discussion"
|
||||
v-for="discussion in group.discussions.elements"
|
||||
:key="discussion.id"
|
||||
/>
|
||||
</div>
|
||||
<b-button
|
||||
tag="router-link"
|
||||
:to="{
|
||||
name: RouteName.CREATE_CONVERSATION,
|
||||
name: RouteName.CREATE_DISCUSSION,
|
||||
params: { preferredUsername: this.preferredUsername },
|
||||
}"
|
||||
>{{ $t("New discussion") }}</b-button
|
||||
@@ -48,11 +48,11 @@
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { FETCH_GROUP } from "@/graphql/actor";
|
||||
import { IGroup, usernameWithDomain } from "@/types/actor";
|
||||
import ConversationListItem from "@/components/Conversation/ConversationListItem.vue";
|
||||
import DiscussionListItem from "@/components/Discussion/DiscussionListItem.vue";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
components: { ConversationListItem },
|
||||
components: { DiscussionListItem },
|
||||
apollo: {
|
||||
group: {
|
||||
query: FETCH_GROUP,
|
||||
@@ -66,8 +66,17 @@ import RouteName from "../../router/name";
|
||||
},
|
||||
},
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
||||
// @ts-ignore
|
||||
title: this.$t("Discussions") as string,
|
||||
// all titles will be injected into this template
|
||||
titleTemplate: "%s | Mobilizon",
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class ConversationsList extends Vue {
|
||||
export default class DiscussionsList extends Vue {
|
||||
@Prop({ type: String, required: true }) preferredUsername!: string;
|
||||
|
||||
group!: IGroup;
|
||||
@@ -777,10 +777,8 @@ export default class Event extends EventMixin {
|
||||
let reporterId = null;
|
||||
if (this.currentActor.id) {
|
||||
reporterId = this.currentActor.id;
|
||||
} else {
|
||||
if (this.config.anonymous.reports.allowed) {
|
||||
reporterId = this.config.anonymous.actorId;
|
||||
}
|
||||
} else if (this.config.anonymous.reports.allowed) {
|
||||
reporterId = this.config.anonymous.actorId;
|
||||
}
|
||||
if (!reporterId) return;
|
||||
try {
|
||||
|
||||
@@ -171,7 +171,7 @@ export default class MyEvents extends Vue {
|
||||
|
||||
static monthlyParticipations(
|
||||
participations: IParticipant[],
|
||||
revertSort: boolean = false
|
||||
revertSort = false
|
||||
): Map<string, Participant[]> {
|
||||
const res = participations.filter(
|
||||
({ event, role }) => event.beginsOn != null && role !== ParticipantRole.REJECTED
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
<span v-if="props.row.actor.name">{{ props.row.actor.name }}</span
|
||||
><br />
|
||||
<span class="is-size-7 has-text-grey"
|
||||
>@{{ props.row.actor.preferredUsername }}</span
|
||||
>@{{ usernameWithDomain(props.row.actor) }}</span
|
||||
>
|
||||
</span>
|
||||
<span v-else>
|
||||
@@ -184,6 +184,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch, Ref } from "vue-property-decorator";
|
||||
import { DataProxy } from "apollo-cache";
|
||||
import {
|
||||
IEvent,
|
||||
IEventParticipantStats,
|
||||
@@ -192,13 +193,11 @@ import {
|
||||
ParticipantRole,
|
||||
} from "../../types/event.model";
|
||||
import { PARTICIPANTS, UPDATE_PARTICIPANT } from "../../graphql/event";
|
||||
import ParticipantCard from "../../components/Account/ParticipantCard.vue";
|
||||
import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
|
||||
import { IPerson } from "../../types/actor";
|
||||
import { IPerson, usernameWithDomain } from "../../types/actor";
|
||||
import { CONFIG } from "../../graphql/config";
|
||||
import { IConfig } from "../../types/config.model";
|
||||
import { Paginate } from "../../types/paginate";
|
||||
import { DataProxy } from "apollo-cache";
|
||||
import { nl2br } from "../../utils/html";
|
||||
import { asyncForEach } from "../../utils/asyncForEach";
|
||||
import RouteName from "../../router/name";
|
||||
@@ -207,9 +206,6 @@ const PARTICIPANTS_PER_PAGE = 10;
|
||||
const MESSAGE_ELLIPSIS_LENGTH = 130;
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
ParticipantCard,
|
||||
},
|
||||
apollo: {
|
||||
currentActor: {
|
||||
query: CURRENT_ACTOR_CLIENT,
|
||||
@@ -259,6 +255,8 @@ export default class Participants extends Vue {
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
|
||||
@Ref("queueTable") readonly queueTable!: any;
|
||||
|
||||
mounted() {
|
||||
|
||||
@@ -1,60 +1,71 @@
|
||||
<template>
|
||||
<div class="container is-widescreen">
|
||||
<div
|
||||
v-if="group && groupMemberships && groupMemberships.includes(group.id)"
|
||||
class="block-container"
|
||||
>
|
||||
<div class="block-column">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs" v-if="group">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link :to="{ name: RouteName.MY_GROUPS }">{{ $t("My groups") }}</router-link>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: usernameWithDomain(group.preferredUsername) },
|
||||
}"
|
||||
>{{ group.name }}</router-link
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<section class="presentation">
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-128x128" v-if="group.avatar">
|
||||
<img :src="group.avatar.url" />
|
||||
</figure>
|
||||
<b-icon v-else size="is-large" icon="account-group" />
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<h1>{{ group.name }}</h1>
|
||||
<small class="has-text-grey">@{{ usernameWithDomain(group) }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="members">
|
||||
<figure
|
||||
class="image is-48x48"
|
||||
:title="
|
||||
$t(`@{username} ({role})`, {
|
||||
username: usernameWithDomain(member.actor),
|
||||
role: member.role,
|
||||
})
|
||||
"
|
||||
v-for="member in group.members.elements"
|
||||
:key="member.actor.id"
|
||||
<div class="header">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link :to="{ name: RouteName.MY_GROUPS }">{{ $t("My groups") }}</router-link>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<router-link
|
||||
v-if="group.preferredUsername"
|
||||
:to="{
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>{{ group.name }}</router-link
|
||||
>
|
||||
<img
|
||||
class="is-rounded"
|
||||
:src="member.actor.avatar.url"
|
||||
v-if="member.actor.avatar"
|
||||
alt
|
||||
/>
|
||||
<b-skeleton v-else :animated="true"></b-skeleton>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<header class="block-container presentation">
|
||||
<div class="block-column media">
|
||||
<div class="media-left">
|
||||
<figure class="image rounded is-128x128" v-if="group.avatar">
|
||||
<img :src="group.avatar.url" />
|
||||
</figure>
|
||||
<b-icon v-else size="is-large" icon="account-group" />
|
||||
</div>
|
||||
</section>
|
||||
<div class="media-content">
|
||||
<h1 v-if="group.name">{{ group.name }}</h1>
|
||||
<b-skeleton v-else :animated="true" />
|
||||
<small class="has-text-grey" v-if="group.preferredUsername"
|
||||
>@{{ usernameWithDomain(group) }}</small
|
||||
>
|
||||
<b-skeleton v-else :animated="true" />
|
||||
<br />
|
||||
<router-link
|
||||
v-if="isCurrentActorAGroupAdmin"
|
||||
:to="{
|
||||
name: RouteName.GROUP_PUBLIC_SETTINGS,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
class="button is-outlined"
|
||||
>{{ $t("Group settings") }}</router-link
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="block-column members">
|
||||
<figure
|
||||
class="image is-48x48"
|
||||
:title="
|
||||
$t(`@{username} ({role})`, {
|
||||
username: usernameWithDomain(member.actor),
|
||||
role: member.role,
|
||||
})
|
||||
"
|
||||
v-for="member in group.members.elements"
|
||||
:key="member.actor.id"
|
||||
>
|
||||
<img class="is-rounded" :src="member.actor.avatar.url" v-if="member.actor.avatar" alt />
|
||||
<b-icon v-else size="is-large" icon="account-circle" />
|
||||
</figure>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
<div v-if="isCurrentActorAGroupMember" class="block-container">
|
||||
<div class="block-column">
|
||||
<section>
|
||||
<subtitle>{{ $t("Upcoming events") }}</subtitle>
|
||||
<div class="organized-events-wrapper" v-if="group.organizedEvents.total > 0">
|
||||
@@ -92,8 +103,17 @@
|
||||
<section>
|
||||
<subtitle>{{ $t("Public page") }}</subtitle>
|
||||
<p>{{ $t("Followed by {count} persons", { count: group.members.total }) }}</p>
|
||||
<b-button type="is-light">{{ $t("Edit biography") }}</b-button>
|
||||
<b-button type="is-primary">{{ $t("Post a public message") }}</b-button>
|
||||
<div v-if="group.posts.total > 0" class="posts-wrapper">
|
||||
<post-list-item v-for="post in group.posts.elements" :key="post.id" :post="post" />
|
||||
</div>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.POST_CREATE,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
class="button is-primary"
|
||||
>{{ $t("Post a public message") }}</router-link
|
||||
>
|
||||
</section>
|
||||
<section>
|
||||
<subtitle>{{ $t("Ongoing tasks") }}</subtitle>
|
||||
@@ -122,15 +142,15 @@
|
||||
</section>
|
||||
<section>
|
||||
<subtitle>{{ $t("Discussions") }}</subtitle>
|
||||
<conversation-list-item
|
||||
v-if="group.conversations.total > 0"
|
||||
v-for="conversation in group.conversations.elements"
|
||||
:key="conversation.id"
|
||||
:conversation="conversation"
|
||||
<discussion-list-item
|
||||
v-if="group.discussions.total > 0"
|
||||
v-for="discussion in group.discussions.elements"
|
||||
:key="discussion.id"
|
||||
:discussion="discussion"
|
||||
/>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.CONVERSATION_LIST,
|
||||
name: RouteName.DISCUSSION_LIST,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>{{ $t("View all discussions") }}</router-link
|
||||
@@ -138,24 +158,13 @@
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="group">
|
||||
<section class="presentation">
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-128x128" v-if="group.avatar">
|
||||
<img :src="group.avatar.url" alt />
|
||||
</figure>
|
||||
<b-icon v-else size="is-large" icon="account-group" />
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<h2>{{ group.name }}</h2>
|
||||
<small class="has-text-grey">@{{ usernameWithDomain(group) }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<b-message v-else-if="!group && $apollo.loading === false" type="is-danger">
|
||||
{{ $t("No group found") }}
|
||||
</b-message>
|
||||
<div v-else class="public-container">
|
||||
<section>
|
||||
<subtitle>{{ $t("Upcoming events") }}</subtitle>
|
||||
<div class="organized-events-wrapper" v-if="group.organizedEvents.total > 0">
|
||||
<div class="organized-events-wrapper" v-if="group && group.organizedEvents.total > 0">
|
||||
<EventMinimalistCard
|
||||
v-for="event in group.organizedEvents.elements"
|
||||
:event="event"
|
||||
@@ -164,16 +173,24 @@
|
||||
/>
|
||||
<router-link :to="{}">{{ $t("View all upcoming events") }}</router-link>
|
||||
</div>
|
||||
<span v-else>{{ $t("No public upcoming events") }}</span>
|
||||
<span v-else-if="group">{{ $t("No public upcoming events") }}</span>
|
||||
<b-skeleton animated v-else></b-skeleton>
|
||||
</section>
|
||||
<!-- {{ group }}-->
|
||||
<section>
|
||||
<subtitle>{{ $t("Latest posts") }}</subtitle>
|
||||
<div v-if="group && group.posts.elements">
|
||||
<router-link
|
||||
v-for="post in group.posts.elements"
|
||||
:key="post.id"
|
||||
:to="{ name: RouteName.POST, params: { slug: post.slug } }"
|
||||
>
|
||||
{{ post.title }}
|
||||
</router-link>
|
||||
</div>
|
||||
<b-skeleton animated v-else></b-skeleton>
|
||||
</section>
|
||||
</div>
|
||||
<b-message v-else-if="!group && $apollo.loading === false" type="is-danger">
|
||||
{{ $t("No group found") }}
|
||||
</b-message>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -181,11 +198,19 @@
|
||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||
import EventCard from "@/components/Event/EventCard.vue";
|
||||
import { FETCH_GROUP, CURRENT_ACTOR_CLIENT, PERSON_MEMBERSHIPS } from "@/graphql/actor";
|
||||
import { IActor, IGroup, IPerson, usernameWithDomain } from "@/types/actor";
|
||||
import {
|
||||
IActor,
|
||||
IGroup,
|
||||
IPerson,
|
||||
usernameWithDomain,
|
||||
Group as GroupModel,
|
||||
MemberRole,
|
||||
} from "@/types/actor";
|
||||
import Subtitle from "@/components/Utils/Subtitle.vue";
|
||||
import CompactTodo from "@/components/Todo/CompactTodo.vue";
|
||||
import EventMinimalistCard from "@/components/Event/EventMinimalistCard.vue";
|
||||
import ConversationListItem from "@/components/Conversation/ConversationListItem.vue";
|
||||
import DiscussionListItem from "@/components/Discussion/DiscussionListItem.vue";
|
||||
import PostListItem from "@/components/Post/PostListItem.vue";
|
||||
import ResourceItem from "@/components/Resource/ResourceItem.vue";
|
||||
import FolderItem from "@/components/Resource/FolderItem.vue";
|
||||
import RouteName from "../../router/name";
|
||||
@@ -214,7 +239,8 @@ import RouteName from "../../router/name";
|
||||
currentActor: CURRENT_ACTOR_CLIENT,
|
||||
},
|
||||
components: {
|
||||
ConversationListItem,
|
||||
DiscussionListItem,
|
||||
PostListItem,
|
||||
EventMinimalistCard,
|
||||
CompactTodo,
|
||||
Subtitle,
|
||||
@@ -243,7 +269,7 @@ export default class Group extends Vue {
|
||||
|
||||
person!: IPerson;
|
||||
|
||||
group!: IGroup;
|
||||
group: IGroup = new GroupModel();
|
||||
|
||||
loading = true;
|
||||
|
||||
@@ -272,18 +298,63 @@ export default class Group extends Vue {
|
||||
if (!this.person || !this.person.id) return undefined;
|
||||
return this.person.memberships.elements.map(({ parent: { id } }) => id);
|
||||
}
|
||||
|
||||
get isCurrentActorAGroupMember(): boolean {
|
||||
return this.groupMemberships != undefined && this.groupMemberships.includes(this.group.id);
|
||||
}
|
||||
|
||||
get isCurrentActorAGroupAdmin(): boolean {
|
||||
return (
|
||||
this.person &&
|
||||
this.person.memberships.elements.some(
|
||||
({ parent: { id }, role }) => id === this.group.id && role === MemberRole.ADMINISTRATOR
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "../../variables.scss";
|
||||
|
||||
div.container {
|
||||
background: white;
|
||||
margin-bottom: 3rem;
|
||||
padding: 2rem 0;
|
||||
|
||||
.header,
|
||||
.public-container {
|
||||
margin: auto 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.block-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&.presentation {
|
||||
border: 2px solid $purple-2;
|
||||
padding: 10px 0;
|
||||
|
||||
h1 {
|
||||
color: $purple-1;
|
||||
font-size: 2rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.button.is-outlined {
|
||||
border-color: $purple-2;
|
||||
}
|
||||
}
|
||||
|
||||
.members {
|
||||
display: flex;
|
||||
|
||||
figure:not(:first-child) {
|
||||
margin-left: -10px;
|
||||
}
|
||||
}
|
||||
|
||||
.block-column {
|
||||
flex: 1;
|
||||
margin: 0 2rem;
|
||||
@@ -293,10 +364,8 @@ div.container {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.presentation {
|
||||
.members {
|
||||
display: flex;
|
||||
}
|
||||
.posts-wrapper {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.organized-events-wrapper {
|
||||
|
||||
@@ -3,15 +3,31 @@
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link :to="{ name: RouteName.GROUP }">{{ group.name }}</router-link>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>{{ group.name }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: RouteName.GROUP_SETTINGS }">{{ $t("Settings") }}</router-link>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.GROUP_SETTINGS,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>{{ $t("Settings") }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<router-link :to="{ name: RouteName.GROUP_MEMBERS_SETTINGS }">{{
|
||||
$t("Members")
|
||||
}}</router-link>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.GROUP_MEMBERS_SETTINGS,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>{{ $t("Members") }}</router-link
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
@@ -29,26 +45,127 @@
|
||||
</b-field>
|
||||
</form>
|
||||
<h1>{{ $t("Group Members") }} ({{ group.members.total }})</h1>
|
||||
<b-field :label="$t('Status')" horizontal>
|
||||
<b-select v-model="roles">
|
||||
<option value="">
|
||||
{{ $t("Everything") }}
|
||||
</option>
|
||||
<option :value="MemberRole.ADMINISTRATOR">
|
||||
{{ $t("Administrator") }}
|
||||
</option>
|
||||
<option :value="MemberRole.MODERATOR">
|
||||
{{ $t("Moderator") }}
|
||||
</option>
|
||||
<option :value="MemberRole.MEMBER">
|
||||
{{ $t("Member") }}
|
||||
</option>
|
||||
<option :value="MemberRole.INVITED">
|
||||
{{ $t("Invited") }}
|
||||
</option>
|
||||
<option :value="MemberRole.NOT_APPROVED">
|
||||
{{ $t("Not approved") }}
|
||||
</option>
|
||||
<option :value="MemberRole.REJECTED">
|
||||
{{ $t("Rejected") }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<b-table
|
||||
:data="group.members.elements"
|
||||
ref="queueTable"
|
||||
:loading="this.$apollo.loading"
|
||||
paginated
|
||||
backend-pagination
|
||||
:pagination-simple="true"
|
||||
:aria-next-label="$t('Next page')"
|
||||
:aria-previous-label="$t('Previous page')"
|
||||
:aria-page-label="$t('Page')"
|
||||
:aria-current-label="$t('Current page')"
|
||||
:total="group.members.total"
|
||||
:per-page="MEMBERS_PER_PAGE"
|
||||
backend-sorting
|
||||
:default-sort-direction="'desc'"
|
||||
:default-sort="['insertedAt', 'desc']"
|
||||
@page-change="(newPage) => (page = newPage)"
|
||||
@sort="(field, order) => $emit('sort', field, order)"
|
||||
>
|
||||
<template slot-scope="props">
|
||||
<b-table-column field="actor.preferredUsername" :label="$t('Member')">
|
||||
<article class="media">
|
||||
<figure class="media-left image is-48x48" v-if="props.row.actor.avatar">
|
||||
<img class="is-rounded" :src="props.row.actor.avatar.url" alt="" />
|
||||
</figure>
|
||||
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
|
||||
<div class="media-content">
|
||||
<div class="content">
|
||||
<span v-if="props.row.actor.name">{{ props.row.actor.name }}</span
|
||||
><br />
|
||||
<span class="is-size-7 has-text-grey"
|
||||
>@{{ usernameWithDomain(props.row.actor) }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</b-table-column>
|
||||
<b-table-column field="role" :label="$t('Role')">
|
||||
<b-tag type="is-primary" v-if="props.row.role === MemberRole.ADMINISTRATOR">
|
||||
{{ $t("Administrator") }}
|
||||
</b-tag>
|
||||
<b-tag type="is-primary" v-else-if="props.row.role === MemberRole.MODERATOR">
|
||||
{{ $t("Moderator") }}
|
||||
</b-tag>
|
||||
<b-tag v-else-if="props.row.role === MemberRole.MEMBER">
|
||||
{{ $t("Member") }}
|
||||
</b-tag>
|
||||
<b-tag type="is-warning" v-else-if="props.row.role === MemberRole.NOT_APPROVED">
|
||||
{{ $t("Not approved") }}
|
||||
</b-tag>
|
||||
<b-tag type="is-danger" v-else-if="props.row.role === MemberRole.REJECTED">
|
||||
{{ $t("Rejected") }}
|
||||
</b-tag>
|
||||
<b-tag type="is-danger" v-else-if="props.row.role === MemberRole.INVITED">
|
||||
{{ $t("Invited") }}
|
||||
</b-tag>
|
||||
</b-table-column>
|
||||
<b-table-column field="insertedAt" :label="$t('Date')">
|
||||
<span class="has-text-centered">
|
||||
{{ props.row.insertedAt | formatDateString }}<br />{{
|
||||
props.row.insertedAt | formatTimeString
|
||||
}}
|
||||
</span>
|
||||
</b-table-column>
|
||||
</template>
|
||||
<template slot="empty">
|
||||
<section class="section">
|
||||
<div class="content has-text-grey has-text-centered">
|
||||
<p>{{ $t("No member matches the filters") }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</b-table>
|
||||
<pre>{{ group.members }}</pre>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import { Component, Vue, Watch } from "vue-property-decorator";
|
||||
import RouteName from "../../router/name";
|
||||
import { FETCH_GROUP } from "../../graphql/actor";
|
||||
import { INVITE_MEMBER } from "../../graphql/member";
|
||||
import { IGroup } from "../../types/actor";
|
||||
import { IMember } from "../../types/actor/group.model";
|
||||
import { INVITE_MEMBER, GROUP_MEMBERS } from "../../graphql/member";
|
||||
import { IGroup, usernameWithDomain } from "../../types/actor";
|
||||
import { IMember, MemberRole } from "../../types/actor/group.model";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
group: {
|
||||
query: FETCH_GROUP,
|
||||
query: GROUP_MEMBERS,
|
||||
// fetchPolicy: "network-only",
|
||||
variables() {
|
||||
return {
|
||||
name: this.$route.params.preferredUsername,
|
||||
page: 1,
|
||||
limit: this.MEMBERS_PER_PAGE,
|
||||
roles: this.roles,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
@@ -66,6 +183,23 @@ export default class GroupMembers extends Vue {
|
||||
|
||||
newMemberUsername = "";
|
||||
|
||||
MemberRole = MemberRole;
|
||||
|
||||
roles: MemberRole | "" = "";
|
||||
|
||||
page = 1;
|
||||
|
||||
MEMBERS_PER_PAGE = 10;
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
|
||||
mounted() {
|
||||
const roleQuery = this.$route.query.role as string;
|
||||
if (Object.values(MemberRole).includes(roleQuery as MemberRole)) {
|
||||
this.roles = roleQuery as MemberRole;
|
||||
}
|
||||
}
|
||||
|
||||
async inviteMember() {
|
||||
await this.$apollo.mutate<{ inviteMember: IMember }>({
|
||||
mutation: INVITE_MEMBER,
|
||||
@@ -75,5 +209,32 @@ export default class GroupMembers extends Vue {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@Watch("page")
|
||||
loadMoreMembers() {
|
||||
this.$apollo.queries.event.fetchMore({
|
||||
// New variables
|
||||
variables: {
|
||||
page: this.page,
|
||||
limit: this.MEMBERS_PER_PAGE,
|
||||
},
|
||||
// Transform the previous result with new data
|
||||
updateQuery: (previousResult, { fetchMoreResult }) => {
|
||||
const oldMembers = previousResult.group.members;
|
||||
const newMembers = fetchMoreResult.group.members;
|
||||
|
||||
return {
|
||||
group: {
|
||||
...previousResult.event,
|
||||
members: {
|
||||
elements: [...oldMembers.elements, ...newMembers.elements],
|
||||
total: newMembers.total,
|
||||
__typename: oldMembers.__typename,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
91
js/src/views/Group/GroupSettings.vue
Normal file
91
js/src/views/Group/GroupSettings.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div>
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>{{ group.name }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.GROUP_SETTINGS,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>{{ $t("Settings") }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.GROUP_PUBLIC_SETTINGS,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>{{ $t("Group settings") }}</router-link
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<section class="container section">
|
||||
<form @submit.prevent="updateGroup">
|
||||
<b-field :label="$t('Group name')">
|
||||
<b-input v-model="group.name" />
|
||||
</b-field>
|
||||
<b-field :label="$t('Group short description')">
|
||||
<b-input type="textarea" v-model="group.summary"
|
||||
/></b-field>
|
||||
<b-button native-type="submit" type="is-primary">{{ $t("Update group") }}</b-button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import RouteName from "../../router/name";
|
||||
import { FETCH_GROUP, UPDATE_GROUP } from "../../graphql/actor";
|
||||
import { IGroup, usernameWithDomain } from "../../types/actor";
|
||||
import { IMember, Group } from "../../types/actor/group.model";
|
||||
import { Paginate } from "../../types/paginate";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
group: {
|
||||
query: FETCH_GROUP,
|
||||
variables() {
|
||||
return {
|
||||
name: this.$route.params.preferredUsername,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.$route.params.preferredUsername;
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class GroupSettings extends Vue {
|
||||
group: IGroup = new Group();
|
||||
|
||||
loading = true;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
newMemberUsername = "";
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
|
||||
async updateGroup() {
|
||||
await this.$apollo.mutate<{ updateGroup: IGroup }>({
|
||||
mutation: UPDATE_GROUP,
|
||||
variables: {
|
||||
...this.group,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -36,7 +36,7 @@ import { ACCEPT_INVITATION } from "../../graphql/member";
|
||||
InvitationCard,
|
||||
},
|
||||
apollo: {
|
||||
paginatedGroups: {
|
||||
membershipsPages: {
|
||||
query: LOGGED_USER_MEMBERSHIPS,
|
||||
fetchPolicy: "network-only",
|
||||
variables: {
|
||||
@@ -57,18 +57,22 @@ import { ACCEPT_INVITATION } from "../../graphql/member";
|
||||
},
|
||||
})
|
||||
export default class MyEvents extends Vue {
|
||||
paginatedGroups!: Paginate<IMember>;
|
||||
membershipsPages!: Paginate<IMember>;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
get invitations() {
|
||||
if (!this.paginatedGroups) return [];
|
||||
return this.paginatedGroups.elements.filter((member) => member.role === MemberRole.INVITED);
|
||||
if (!this.membershipsPages) return [];
|
||||
return this.membershipsPages.elements.filter(
|
||||
(member: IMember) => member.role === MemberRole.INVITED
|
||||
);
|
||||
}
|
||||
|
||||
get memberships() {
|
||||
if (!this.paginatedGroups) return [];
|
||||
return this.paginatedGroups.elements.filter((member) => member.role !== MemberRole.INVITED);
|
||||
if (!this.membershipsPages) return [];
|
||||
return this.membershipsPages.elements.filter(
|
||||
(member: IMember) => member.role !== MemberRole.INVITED
|
||||
);
|
||||
}
|
||||
|
||||
async acceptInvitation(id: string) {
|
||||
|
||||
@@ -315,7 +315,7 @@ export default class Report extends Vue {
|
||||
this.$buefy.dialog.confirm({
|
||||
title: this.$t("Deleting event") as string,
|
||||
message: this.$t(
|
||||
"Are you sure you want to <b>delete</b> this event? This action cannot be undone. You may want to engage the conversation with the event creator or edit its event instead."
|
||||
"Are you sure you want to <b>delete</b> this event? This action cannot be undone. You may want to engage the discussion with the event creator or edit its event instead."
|
||||
) as string,
|
||||
confirmText: this.$t("Delete Event") as string,
|
||||
type: "is-danger",
|
||||
|
||||
217
js/src/views/Posts/Edit.vue
Normal file
217
js/src/views/Posts/Edit.vue
Normal file
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<form @submit.prevent="publish(false)">
|
||||
<div class="container section">
|
||||
<h1 class="title" v-if="isUpdate === true">
|
||||
{{ $t("Edit post") }}
|
||||
</h1>
|
||||
<h1 class="title" v-else>
|
||||
{{ $t("Add a new post") }}
|
||||
</h1>
|
||||
<subtitle>{{ $t("General information") }}</subtitle>
|
||||
<picture-upload v-model="pictureFile" :textFallback="$t('Headline picture')" />
|
||||
|
||||
<b-field :label="$t('Title')">
|
||||
<b-input size="is-large" aria-required="true" required v-model="post.title" />
|
||||
</b-field>
|
||||
|
||||
<tag-input v-model="post.tags" :data="tags" path="title" />
|
||||
|
||||
<div class="field">
|
||||
<label class="label">{{ $t("Post") }}</label>
|
||||
<editor v-model="post.body" />
|
||||
</div>
|
||||
<subtitle>{{ $t("Who can view this post") }}</subtitle>
|
||||
<div class="field">
|
||||
<b-radio
|
||||
v-model="post.visibility"
|
||||
name="postVisibility"
|
||||
:native-value="PostVisibility.PUBLIC"
|
||||
>{{ $t("Visible everywhere on the web") }}</b-radio
|
||||
>
|
||||
</div>
|
||||
<div class="field">
|
||||
<b-radio
|
||||
v-model="post.visibility"
|
||||
name="postVisibility"
|
||||
:native-value="PostVisibility.UNLISTED"
|
||||
>{{ $t("Only accessible through link") }}</b-radio
|
||||
>
|
||||
</div>
|
||||
<div class="field">
|
||||
<b-radio
|
||||
v-model="post.visibility"
|
||||
name="postVisibility"
|
||||
:native-value="PostVisibility.PRIVATE"
|
||||
>{{ $t("Only accessible to members of the group") }}</b-radio
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="navbar">
|
||||
<div class="container">
|
||||
<div class="navbar-menu">
|
||||
<div class="navbar-end">
|
||||
<span class="navbar-item">
|
||||
<b-button type="is-text" @click="$router.go(-1)">{{ $t("Cancel") }}</b-button>
|
||||
</span>
|
||||
<span class="navbar-item">
|
||||
<b-button type="is-danger is-outlined" @click="deletePost">{{
|
||||
$t("Delete post")
|
||||
}}</b-button>
|
||||
</span>
|
||||
<!-- If an post has been published we can't make it draft anymore -->
|
||||
<span class="navbar-item" v-if="post.draft === true">
|
||||
<b-button type="is-primary" outlined @click="publish(true)">{{
|
||||
$t("Save draft")
|
||||
}}</b-button>
|
||||
</span>
|
||||
<span class="navbar-item">
|
||||
<b-button type="is-primary" native-type="submit">
|
||||
<span v-if="isUpdate === false || post.draft === true">{{ $t("Publish") }}</span>
|
||||
|
||||
<span v-else>{{ $t("Update post") }}</span>
|
||||
</b-button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</form>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from "vue-property-decorator";
|
||||
import { CURRENT_ACTOR_CLIENT, FETCH_GROUP } from "../../graphql/actor";
|
||||
import { TAGS } from "../../graphql/tags";
|
||||
import { CONFIG } from "../../graphql/config";
|
||||
import { FETCH_POST, CREATE_POST, UPDATE_POST, DELETE_POST } from "../../graphql/post";
|
||||
|
||||
import { IPost, PostVisibility } from "../../types/post.model";
|
||||
import Editor from "../../components/Editor.vue";
|
||||
import { IGroup } from "../../types/actor";
|
||||
import TagInput from "../../components/Event/TagInput.vue";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
currentActor: CURRENT_ACTOR_CLIENT,
|
||||
tags: TAGS,
|
||||
config: CONFIG,
|
||||
post: {
|
||||
query: FETCH_POST,
|
||||
variables() {
|
||||
return {
|
||||
slug: this.slug,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.slug;
|
||||
},
|
||||
},
|
||||
group: {
|
||||
query: FETCH_GROUP,
|
||||
variables() {
|
||||
return {
|
||||
name: this.preferredUsername,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.preferredUsername;
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
Editor,
|
||||
TagInput,
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
||||
// @ts-ignore
|
||||
title: this.isUpdate
|
||||
? (this.$t("Edit post") as string)
|
||||
: (this.$t("Add a new post") as string),
|
||||
// all titles will be injected into this template
|
||||
titleTemplate: "%s | Mobilizon",
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class EditPost extends Vue {
|
||||
@Prop({ required: false, type: String }) slug: undefined | string;
|
||||
|
||||
@Prop({ required: false, type: String }) preferredUsername!: string;
|
||||
|
||||
@Prop({ type: Boolean, default: false }) isUpdate!: boolean;
|
||||
|
||||
post: IPost = {
|
||||
title: "",
|
||||
body: "",
|
||||
local: true,
|
||||
draft: true,
|
||||
visibility: PostVisibility.PUBLIC,
|
||||
tags: [],
|
||||
};
|
||||
|
||||
group!: IGroup;
|
||||
|
||||
PostVisibility = PostVisibility;
|
||||
|
||||
async publish(draft: boolean) {
|
||||
if (this.isUpdate) {
|
||||
const { data } = await this.$apollo.mutate({
|
||||
mutation: UPDATE_POST,
|
||||
variables: {
|
||||
id: this.post.id,
|
||||
title: this.post.title,
|
||||
body: this.post.body,
|
||||
tags: (this.post.tags || []).map(({ title }) => title),
|
||||
visibility: this.post.visibility,
|
||||
draft,
|
||||
},
|
||||
});
|
||||
if (data && data.updatePost) {
|
||||
return this.$router.push({ name: RouteName.POST, params: { slug: data.updatePost.slug } });
|
||||
}
|
||||
} else {
|
||||
const { data } = await this.$apollo.mutate({
|
||||
mutation: CREATE_POST,
|
||||
variables: {
|
||||
...this.post,
|
||||
tags: (this.post.tags || []).map(({ title }) => title),
|
||||
attributedToId: this.group.id,
|
||||
draft,
|
||||
},
|
||||
});
|
||||
if (data && data.createPost) {
|
||||
return this.$router.push({ name: RouteName.POST, params: { slug: data.createPost.slug } });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async deletePost() {
|
||||
const { data } = await this.$apollo.mutate({
|
||||
mutation: DELETE_POST,
|
||||
variables: {
|
||||
id: this.post.id,
|
||||
},
|
||||
});
|
||||
if (data && this.post.attributedTo) {
|
||||
return this.$router.push({
|
||||
name: RouteName.POSTS,
|
||||
params: { preferredUsername: this.post.attributedTo.preferredUsername },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
form {
|
||||
nav.navbar {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
min-height: 2rem;
|
||||
|
||||
.container {
|
||||
min-height: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
86
js/src/views/Posts/List.vue
Normal file
86
js/src/views/Posts/List.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div>
|
||||
<section class="section container">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs" v-if="group">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link :to="{ name: RouteName.MY_GROUPS }">{{ $t("My groups") }}</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link
|
||||
v-if="group"
|
||||
:to="{
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>{{ group.name || group.preferredUsername }}</router-link
|
||||
>
|
||||
<b-skeleton v-else :animated="true"></b-skeleton>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<router-link
|
||||
v-if="group"
|
||||
:to="{
|
||||
name: RouteName.POSTS,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>{{ $t("Posts") }}</router-link
|
||||
>
|
||||
<b-skeleton v-else :animated="true"></b-skeleton>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div v-if="group">
|
||||
<router-link
|
||||
v-for="post in group.posts.elements"
|
||||
:key="post.id"
|
||||
:to="{ name: RouteName.POST, params: { slug: post.slug } }"
|
||||
>
|
||||
{{ post.title }}
|
||||
</router-link>
|
||||
</div>
|
||||
<b-skeleton v-else :animated="true"></b-skeleton>
|
||||
</section>
|
||||
<pre>{{ group }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from "vue-property-decorator";
|
||||
import { FETCH_GROUP_POSTS } from "../../graphql/post";
|
||||
import { Paginate } from "../../types/paginate";
|
||||
import { IPost } from "../../types/post.model";
|
||||
import { IGroup, usernameWithDomain } from "../../types/actor";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
group: {
|
||||
query: FETCH_GROUP_POSTS,
|
||||
variables() {
|
||||
return {
|
||||
preferredUsername: this.preferredUsername,
|
||||
};
|
||||
},
|
||||
// update(data) {
|
||||
// console.log(data);
|
||||
// return data.group.posts;
|
||||
// },
|
||||
skip() {
|
||||
return !this.preferredUsername;
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class PostList extends Vue {
|
||||
@Prop({ required: true, type: String }) preferredUsername!: string;
|
||||
|
||||
group!: IGroup;
|
||||
|
||||
posts!: Paginate<IPost>;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
}
|
||||
</script>
|
||||
184
js/src/views/Posts/Post.vue
Normal file
184
js/src/views/Posts/Post.vue
Normal file
@@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<div>
|
||||
<article class="container" v-if="post">
|
||||
<section class="heading-section">
|
||||
<h1 class="title">{{ post.title }}</h1>
|
||||
<i18n tag="span" path="By {author}" class="authors">
|
||||
<router-link
|
||||
slot="author"
|
||||
:to="{
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: usernameWithDomain(post.attributedTo) },
|
||||
}"
|
||||
>{{ post.attributedTo.name }}</router-link
|
||||
>
|
||||
</i18n>
|
||||
<p class="published" v-if="!post.draft">{{ post.publishAt | formatDateTimeString }}</p>
|
||||
<p class="buttons" v-if="isCurrentActorMember">
|
||||
<b-tag type="is-warning" size="is-medium" v-if="post.draft">{{ $t("Draft") }}</b-tag>
|
||||
<router-link
|
||||
:to="{ name: RouteName.POST_EDIT, params: { slug: post.slug } }"
|
||||
tag="button"
|
||||
class="button is-text"
|
||||
>{{ $t("Edit") }}</router-link
|
||||
>
|
||||
</p>
|
||||
</section>
|
||||
<section v-html="post.body" class="content" />
|
||||
<section class="tags">
|
||||
<router-link
|
||||
v-for="tag in post.tags"
|
||||
:key="tag.title"
|
||||
:to="{ name: RouteName.TAG, params: { tag: tag.title } }"
|
||||
>
|
||||
<tag>{{ tag.title }}</tag>
|
||||
</router-link>
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from "vue-property-decorator";
|
||||
import Editor from "@/components/Editor.vue";
|
||||
import { GraphQLError } from "graphql";
|
||||
import { CURRENT_ACTOR_CLIENT, PERSON_MEMBERSHIPS, FETCH_GROUP } from "../../graphql/actor";
|
||||
import { TAGS } from "../../graphql/tags";
|
||||
import { CONFIG } from "../../graphql/config";
|
||||
import { FETCH_POST, CREATE_POST } from "../../graphql/post";
|
||||
|
||||
import { IPost, PostVisibility } from "../../types/post.model";
|
||||
import { IGroup, IMember, usernameWithDomain } from "../../types/actor";
|
||||
import RouteName from "../../router/name";
|
||||
import Tag from "../../components/Tag.vue";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
currentActor: CURRENT_ACTOR_CLIENT,
|
||||
memberships: {
|
||||
query: PERSON_MEMBERSHIPS,
|
||||
variables() {
|
||||
return {
|
||||
id: this.currentActor.id,
|
||||
};
|
||||
},
|
||||
update: (data) => data.person.memberships.elements,
|
||||
skip() {
|
||||
return !this.currentActor || !this.currentActor.id;
|
||||
},
|
||||
},
|
||||
post: {
|
||||
query: FETCH_POST,
|
||||
variables() {
|
||||
return {
|
||||
slug: this.slug,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.slug;
|
||||
},
|
||||
error({ graphQLErrors }) {
|
||||
this.handleErrors(graphQLErrors);
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
Tag,
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
||||
// @ts-ignore
|
||||
title: this.post ? this.post.title : "",
|
||||
// all titles will be injected into this template
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
||||
// @ts-ignore
|
||||
titleTemplate: this.post ? "%s | Mobilizon" : "Mobilizon",
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class Post extends Vue {
|
||||
@Prop({ required: true, type: String }) slug!: string;
|
||||
|
||||
post!: IPost;
|
||||
|
||||
memberships!: IMember[];
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
|
||||
get isCurrentActorMember(): boolean {
|
||||
if (!this.post.attributedTo || !this.memberships) return false;
|
||||
return this.memberships.map(({ parent: { id } }) => id).includes(this.post.attributedTo.id);
|
||||
}
|
||||
|
||||
async handleErrors(errors: GraphQLError[]) {
|
||||
if (errors[0].message.includes("No such post")) {
|
||||
await this.$router.push({ name: RouteName.PAGE_NOT_FOUND });
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "../../variables.scss";
|
||||
|
||||
article {
|
||||
section.heading-section {
|
||||
text-align: center;
|
||||
|
||||
h1.title {
|
||||
margin: 0 auto;
|
||||
padding-top: 3rem;
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.authors {
|
||||
margin-top: 2rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.published {
|
||||
margin-top: 1rem;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
&::after {
|
||||
height: 0.4rem;
|
||||
margin-bottom: 2rem;
|
||||
content: " ";
|
||||
display: block;
|
||||
width: 100%;
|
||||
background-color: $purple-1;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
section.content {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
section.tags {
|
||||
padding-bottom: 5rem;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
span {
|
||||
&.tag {
|
||||
margin: 0 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: $white;
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
padding: 0 3rem;
|
||||
}
|
||||
</style>
|
||||
@@ -118,7 +118,7 @@
|
||||
</div>
|
||||
</transition-group>
|
||||
</draggable>
|
||||
<div class="content has-text-centered has-text-grey">
|
||||
<div class="content has-text-centered has-text-grey" v-if="resource.children.total === 0">
|
||||
<p>{{ $t("No resources in this folder") }}</p>
|
||||
</div>
|
||||
</section>
|
||||
@@ -470,12 +470,12 @@ export default class Resources extends Mixins(ResourceMixin) {
|
||||
|
||||
handleRename(resource: IResource) {
|
||||
this.renameModal = true;
|
||||
this.updatedResource = Object.assign({}, resource);
|
||||
this.updatedResource = { ...resource };
|
||||
}
|
||||
|
||||
handleMove(resource: IResource) {
|
||||
this.moveModal = true;
|
||||
this.updatedResource = Object.assign({}, resource);
|
||||
this.updatedResource = { ...resource };
|
||||
}
|
||||
|
||||
async moveResource(resource: IResource, oldParent: IResource | undefined) {
|
||||
|
||||
Reference in New Issue
Block a user