Merge branch 'improve-groups' into 'master'
Improve and activate groups See merge request framasoft/mobilizon!568
This commit is contained in:
commit
692a0e670a
@ -17,7 +17,7 @@ config :mobilizon, :instance,
|
||||
description: "Change this to a proper description of your instance",
|
||||
hostname: "localhost",
|
||||
registrations_open: false,
|
||||
registration_email_whitelist: [],
|
||||
registration_email_allowlist: [],
|
||||
demo: false,
|
||||
repository: Mix.Project.config()[:source_url],
|
||||
allow_relay: true,
|
||||
@ -29,8 +29,7 @@ config :mobilizon, :instance,
|
||||
email_from: "noreply@localhost",
|
||||
email_reply_to: "noreply@localhost"
|
||||
|
||||
# Groups are to be activated with Mobilizon 1.0.0
|
||||
config :mobilizon, :groups, enabled: false
|
||||
config :mobilizon, :groups, enabled: true
|
||||
|
||||
config :mobilizon, :events, creation: true
|
||||
|
||||
|
@ -76,7 +76,7 @@ export default class App extends Vue {
|
||||
|
||||
currentUser!: ICurrentUser;
|
||||
|
||||
async created() {
|
||||
async created(): Promise<void> {
|
||||
if (await this.initializeCurrentUser()) {
|
||||
await initializeCurrentActor(this.$apollo.provider.defaultClient);
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { IntrospectionFragmentMatcher, NormalizedCacheObject } from "apollo-cache-inmemory";
|
||||
import { IError, errors, defaultError, refreshSuggestion } from "@/utils/errors";
|
||||
import { AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN } from "@/constants";
|
||||
import { REFRESH_TOKEN } from "@/graphql/auth";
|
||||
import { saveTokenData } from "@/utils/auth";
|
||||
@ -24,18 +23,6 @@ export const fragmentMatcher = new IntrospectionFragmentMatcher({
|
||||
},
|
||||
});
|
||||
|
||||
export const computeErrorMessage = (message: any) => {
|
||||
const error: IError = errors.reduce((acc, errorLocal) => {
|
||||
if (RegExp(errorLocal.match).test(message)) {
|
||||
return errorLocal;
|
||||
}
|
||||
return acc;
|
||||
}, defaultError);
|
||||
|
||||
if (error.value === null) return null;
|
||||
return error.suggestRefresh === false ? error.value : `${error.value}<br>${refreshSuggestion}`;
|
||||
};
|
||||
|
||||
export async function refreshAccessToken(
|
||||
apolloClient: ApolloClient<NormalizedCacheObject>
|
||||
): Promise<boolean> {
|
||||
|
@ -101,6 +101,7 @@
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Mixins } from "vue-property-decorator";
|
||||
import { SnackbarProgrammatic as Snackbar } from "buefy";
|
||||
import { ACCEPT_RELAY, REJECT_RELAY, RELAY_FOLLOWERS } from "../../graphql/admin";
|
||||
import { Paginate } from "../../types/paginate";
|
||||
import { IFollower } from "../../types/actor/follower.model";
|
||||
@ -125,38 +126,46 @@ export default class Followers extends Mixins(RelayMixin) {
|
||||
|
||||
RelayMixin = RelayMixin;
|
||||
|
||||
async acceptRelays() {
|
||||
async acceptRelays(): Promise<void> {
|
||||
await this.checkedRows.forEach((row: IFollower) => {
|
||||
this.acceptRelay(`${row.actor.preferredUsername}@${row.actor.domain}`);
|
||||
});
|
||||
}
|
||||
|
||||
async rejectRelays() {
|
||||
async rejectRelays(): Promise<void> {
|
||||
await this.checkedRows.forEach((row: IFollower) => {
|
||||
this.rejectRelay(`${row.actor.preferredUsername}@${row.actor.domain}`);
|
||||
});
|
||||
}
|
||||
|
||||
async acceptRelay(address: string) {
|
||||
await this.$apollo.mutate({
|
||||
mutation: ACCEPT_RELAY,
|
||||
variables: {
|
||||
address,
|
||||
},
|
||||
});
|
||||
await this.$apollo.queries.relayFollowers.refetch();
|
||||
this.checkedRows = [];
|
||||
async acceptRelay(address: string): Promise<void> {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: ACCEPT_RELAY,
|
||||
variables: {
|
||||
address,
|
||||
},
|
||||
});
|
||||
await this.$apollo.queries.relayFollowers.refetch();
|
||||
this.checkedRows = [];
|
||||
} catch (e) {
|
||||
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
|
||||
}
|
||||
}
|
||||
|
||||
async rejectRelay(address: string) {
|
||||
await this.$apollo.mutate({
|
||||
mutation: REJECT_RELAY,
|
||||
variables: {
|
||||
address,
|
||||
},
|
||||
});
|
||||
await this.$apollo.queries.relayFollowers.refetch();
|
||||
this.checkedRows = [];
|
||||
async rejectRelay(address: string): Promise<void> {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: REJECT_RELAY,
|
||||
variables: {
|
||||
address,
|
||||
},
|
||||
});
|
||||
await this.$apollo.queries.relayFollowers.refetch();
|
||||
this.checkedRows = [];
|
||||
} catch (e) {
|
||||
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
|
||||
}
|
||||
}
|
||||
|
||||
get checkedRowsHaveAtLeastOneToApprove(): boolean {
|
||||
|
@ -99,6 +99,7 @@
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Mixins } from "vue-property-decorator";
|
||||
import { SnackbarProgrammatic as Snackbar } from "buefy";
|
||||
import { ADD_RELAY, RELAY_FOLLOWINGS, REMOVE_RELAY } from "../../graphql/admin";
|
||||
import { IFollower } from "../../types/actor/follower.model";
|
||||
import { Paginate } from "../../types/paginate";
|
||||
@ -125,34 +126,42 @@ export default class Followings extends Mixins(RelayMixin) {
|
||||
|
||||
RelayMixin = RelayMixin;
|
||||
|
||||
async followRelay(e: Event) {
|
||||
async followRelay(e: Event): Promise<void> {
|
||||
e.preventDefault();
|
||||
await this.$apollo.mutate({
|
||||
mutation: ADD_RELAY,
|
||||
variables: {
|
||||
address: this.newRelayAddress,
|
||||
},
|
||||
// TODO: Handle cache update properly without refreshing
|
||||
});
|
||||
await this.$apollo.queries.relayFollowings.refetch();
|
||||
this.newRelayAddress = "";
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: ADD_RELAY,
|
||||
variables: {
|
||||
address: this.newRelayAddress,
|
||||
},
|
||||
// TODO: Handle cache update properly without refreshing
|
||||
});
|
||||
await this.$apollo.queries.relayFollowings.refetch();
|
||||
this.newRelayAddress = "";
|
||||
} catch (e) {
|
||||
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
|
||||
}
|
||||
}
|
||||
|
||||
async removeRelays() {
|
||||
async removeRelays(): Promise<void> {
|
||||
await this.checkedRows.forEach((row: IFollower) => {
|
||||
this.removeRelay(`${row.targetActor.preferredUsername}@${row.targetActor.domain}`);
|
||||
});
|
||||
}
|
||||
|
||||
async removeRelay(address: string) {
|
||||
await this.$apollo.mutate({
|
||||
mutation: REMOVE_RELAY,
|
||||
variables: {
|
||||
address,
|
||||
},
|
||||
});
|
||||
await this.$apollo.queries.relayFollowings.refetch();
|
||||
this.checkedRows = [];
|
||||
async removeRelay(address: string): Promise<void> {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: REMOVE_RELAY,
|
||||
variables: {
|
||||
address,
|
||||
},
|
||||
});
|
||||
await this.$apollo.queries.relayFollowings.refetch();
|
||||
this.checkedRows = [];
|
||||
} catch (e) {
|
||||
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -126,6 +126,7 @@
|
||||
import { Component, Prop, Vue, Ref } from "vue-property-decorator";
|
||||
import EditorComponent from "@/components/Editor.vue";
|
||||
import TimeAgo from "javascript-time-ago";
|
||||
import { SnackbarProgrammatic as Snackbar } from "buefy";
|
||||
import { CommentModel, IComment } from "../../types/comment.model";
|
||||
import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
|
||||
import { IPerson, usernameWithDomain } from "../../types/actor";
|
||||
@ -171,7 +172,7 @@ export default class Comment extends Vue {
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
|
||||
async mounted() {
|
||||
async mounted(): Promise<void> {
|
||||
const localeName = this.$i18n.locale;
|
||||
const locale = await import(`javascript-time-ago/locale/${localeName}`);
|
||||
TimeAgo.addLocale(locale);
|
||||
@ -183,7 +184,7 @@ export default class Comment extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
async createReplyToComment(comment: IComment) {
|
||||
async createReplyToComment(comment: IComment): Promise<void> {
|
||||
if (this.replyTo) {
|
||||
this.replyTo = false;
|
||||
this.newComment = new CommentModel();
|
||||
@ -196,7 +197,7 @@ export default class Comment extends Vue {
|
||||
this.commentEditor.replyToComment(comment);
|
||||
}
|
||||
|
||||
replyToComment() {
|
||||
replyToComment(): void {
|
||||
this.newComment.inReplyToComment = this.comment;
|
||||
this.newComment.originComment = this.comment.originComment || this.comment;
|
||||
this.newComment.actor = this.currentActor;
|
||||
@ -205,7 +206,7 @@ export default class Comment extends Vue {
|
||||
this.replyTo = false;
|
||||
}
|
||||
|
||||
async fetchReplies() {
|
||||
async fetchReplies(): Promise<void> {
|
||||
const parentId = this.comment.id;
|
||||
const { data } = await this.$apollo.query<{ thread: IComment[] }>({
|
||||
query: FETCH_THREAD_REPLIES,
|
||||
@ -267,7 +268,7 @@ export default class Comment extends Vue {
|
||||
return this.commentId;
|
||||
}
|
||||
|
||||
reportModal() {
|
||||
reportModal(): void {
|
||||
if (!this.comment.actor) return;
|
||||
this.$buefy.modal.open({
|
||||
parent: this,
|
||||
@ -281,7 +282,7 @@ export default class Comment extends Vue {
|
||||
});
|
||||
}
|
||||
|
||||
async reportComment(content: string, forward: boolean) {
|
||||
async reportComment(content: string, forward: boolean): Promise<void> {
|
||||
try {
|
||||
if (!this.comment.actor) return;
|
||||
await this.$apollo.mutate<IReport>({
|
||||
@ -303,8 +304,8 @@ export default class Comment extends Vue {
|
||||
position: "is-bottom-right",
|
||||
duration: 5000,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} catch (e) {
|
||||
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -51,6 +51,7 @@
|
||||
import { Prop, Vue, Component, Watch } from "vue-property-decorator";
|
||||
import Comment from "@/components/Comment/Comment.vue";
|
||||
import IdentityPickerWrapper from "@/views/Account/IdentityPickerWrapper.vue";
|
||||
import { SnackbarProgrammatic as Snackbar } from "buefy";
|
||||
import { CommentModel, IComment } from "../../types/comment.model";
|
||||
import {
|
||||
CREATE_COMMENT_FROM_EVENT,
|
||||
@ -100,11 +101,11 @@ export default class CommentTree extends Vue {
|
||||
CommentModeration = CommentModeration;
|
||||
|
||||
@Watch("currentActor")
|
||||
watchCurrentActor(currentActor: IPerson) {
|
||||
watchCurrentActor(currentActor: IPerson): void {
|
||||
this.newComment.actor = currentActor;
|
||||
}
|
||||
|
||||
async createCommentForEvent(comment: IComment) {
|
||||
async createCommentForEvent(comment: IComment): Promise<void> {
|
||||
try {
|
||||
if (!comment.actor) return;
|
||||
await this.$apollo.mutate({
|
||||
@ -190,74 +191,78 @@ export default class CommentTree extends Vue {
|
||||
// and reset the new comment field
|
||||
this.newComment = new CommentModel();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
|
||||
}
|
||||
}
|
||||
|
||||
async deleteComment(comment: IComment) {
|
||||
await this.$apollo.mutate({
|
||||
mutation: DELETE_COMMENT,
|
||||
variables: {
|
||||
commentId: comment.id,
|
||||
actorId: this.currentActor.id,
|
||||
},
|
||||
update: (store, { data }) => {
|
||||
if (data == null) return;
|
||||
const deletedCommentId = data.deleteComment.id;
|
||||
async deleteComment(comment: IComment): Promise<void> {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: DELETE_COMMENT,
|
||||
variables: {
|
||||
commentId: comment.id,
|
||||
actorId: this.currentActor.id,
|
||||
},
|
||||
update: (store, { data }) => {
|
||||
if (data == null) return;
|
||||
const deletedCommentId = data.deleteComment.id;
|
||||
|
||||
const commentsData = store.readQuery<{ event: IEvent }>({
|
||||
query: COMMENTS_THREADS,
|
||||
variables: {
|
||||
eventUUID: this.event.uuid,
|
||||
},
|
||||
});
|
||||
if (!commentsData) return;
|
||||
const { event } = commentsData;
|
||||
const { comments: oldComments } = event;
|
||||
|
||||
if (comment.originComment) {
|
||||
// we have deleted a reply to a thread
|
||||
const localData = store.readQuery<{ thread: IComment[] }>({
|
||||
query: FETCH_THREAD_REPLIES,
|
||||
const commentsData = store.readQuery<{ event: IEvent }>({
|
||||
query: COMMENTS_THREADS,
|
||||
variables: {
|
||||
threadId: comment.originComment.id,
|
||||
eventUUID: this.event.uuid,
|
||||
},
|
||||
});
|
||||
if (!localData) return;
|
||||
const { thread: oldReplyList } = localData;
|
||||
const replies = oldReplyList.filter((reply) => reply.id !== deletedCommentId);
|
||||
if (!commentsData) return;
|
||||
const { event } = commentsData;
|
||||
const { comments: oldComments } = event;
|
||||
|
||||
if (comment.originComment) {
|
||||
// we have deleted a reply to a thread
|
||||
const localData = store.readQuery<{ thread: IComment[] }>({
|
||||
query: FETCH_THREAD_REPLIES,
|
||||
variables: {
|
||||
threadId: comment.originComment.id,
|
||||
},
|
||||
});
|
||||
if (!localData) return;
|
||||
const { thread: oldReplyList } = localData;
|
||||
const replies = oldReplyList.filter((reply) => reply.id !== deletedCommentId);
|
||||
store.writeQuery({
|
||||
query: FETCH_THREAD_REPLIES,
|
||||
variables: {
|
||||
threadId: comment.originComment.id,
|
||||
},
|
||||
data: { thread: replies },
|
||||
});
|
||||
|
||||
const { originComment } = comment;
|
||||
|
||||
const parentCommentIndex = oldComments.findIndex(
|
||||
(oldComment) => oldComment.id === originComment.id
|
||||
);
|
||||
const parentComment = oldComments[parentCommentIndex];
|
||||
parentComment.replies = replies;
|
||||
parentComment.totalReplies -= 1;
|
||||
oldComments.splice(parentCommentIndex, 1, parentComment);
|
||||
event.comments = oldComments;
|
||||
} else {
|
||||
// we have deleted a thread itself
|
||||
event.comments = oldComments.filter((reply) => reply.id !== deletedCommentId);
|
||||
}
|
||||
store.writeQuery({
|
||||
query: FETCH_THREAD_REPLIES,
|
||||
query: COMMENTS_THREADS,
|
||||
variables: {
|
||||
threadId: comment.originComment.id,
|
||||
eventUUID: this.event.uuid,
|
||||
},
|
||||
data: { thread: replies },
|
||||
data: { event },
|
||||
});
|
||||
|
||||
const { originComment } = comment;
|
||||
|
||||
const parentCommentIndex = oldComments.findIndex(
|
||||
(oldComment) => oldComment.id === originComment.id
|
||||
);
|
||||
const parentComment = oldComments[parentCommentIndex];
|
||||
parentComment.replies = replies;
|
||||
parentComment.totalReplies -= 1;
|
||||
oldComments.splice(parentCommentIndex, 1, parentComment);
|
||||
event.comments = oldComments;
|
||||
} else {
|
||||
// we have deleted a thread itself
|
||||
event.comments = oldComments.filter((reply) => reply.id !== deletedCommentId);
|
||||
}
|
||||
store.writeQuery({
|
||||
query: COMMENTS_THREADS,
|
||||
variables: {
|
||||
eventUUID: this.event.uuid,
|
||||
},
|
||||
data: { event },
|
||||
});
|
||||
},
|
||||
});
|
||||
// this.comments = this.comments.filter(commentItem => commentItem.id !== comment.id);
|
||||
},
|
||||
});
|
||||
// this.comments = this.comments.filter(commentItem => commentItem.id !== comment.id);
|
||||
} catch (e) {
|
||||
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
|
||||
}
|
||||
}
|
||||
|
||||
get orderedComments(): IComment[] {
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div v-if="editor">
|
||||
<div
|
||||
class="editor"
|
||||
:class="{ mode_description: isDescriptionMode }"
|
||||
:class="{ short_mode: isShortMode, comment_mode: isCommentMode }"
|
||||
id="tiptab-editor"
|
||||
:data-actor-id="currentActor && currentActor.id"
|
||||
>
|
||||
@ -211,6 +211,7 @@ import tippy, { Instance, sticky } from "tippy.js";
|
||||
import { SEARCH_PERSONS } from "../graphql/search";
|
||||
import { Actor, IActor, IPerson } from "../types/actor";
|
||||
import Image from "./Editor/Image";
|
||||
import MaxSize from "./Editor/MaxSize";
|
||||
import { UPLOAD_PICTURE } from "../graphql/upload";
|
||||
import { listenFileUpload } from "../utils/upload";
|
||||
import { CURRENT_ACTOR_CLIENT } from "../graphql/actor";
|
||||
@ -229,6 +230,8 @@ export default class EditorComponent extends Vue {
|
||||
|
||||
@Prop({ required: false, default: "description" }) mode!: string;
|
||||
|
||||
@Prop({ required: false, default: 100_000_000 }) maxSize!: number;
|
||||
|
||||
currentActor!: IPerson;
|
||||
|
||||
editor: Editor | null = null;
|
||||
@ -254,6 +257,10 @@ export default class EditorComponent extends Vue {
|
||||
return this.mode === "comment";
|
||||
}
|
||||
|
||||
get isShortMode(): boolean {
|
||||
return this.isBasicMode;
|
||||
}
|
||||
|
||||
get hasResults(): boolean {
|
||||
return this.filteredActors.length > 0;
|
||||
}
|
||||
@ -386,6 +393,7 @@ export default class EditorComponent extends Vue {
|
||||
showOnlyWhenEditable: false,
|
||||
}),
|
||||
new Image(),
|
||||
new MaxSize({ maxSize: this.maxSize }),
|
||||
],
|
||||
onUpdate: ({ getHTML }: { getHTML: Function }) => {
|
||||
this.$emit("input", getHTML());
|
||||
@ -395,7 +403,7 @@ export default class EditorComponent extends Vue {
|
||||
}
|
||||
|
||||
@Watch("value")
|
||||
onValueChanged(val: string) {
|
||||
onValueChanged(val: string): void {
|
||||
if (!this.editor) return;
|
||||
if (val !== this.editor.getHTML()) {
|
||||
this.editor.setContent(val);
|
||||
@ -420,7 +428,7 @@ export default class EditorComponent extends Vue {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
upHandler() {
|
||||
upHandler(): void {
|
||||
this.navigatedActorIndex =
|
||||
(this.navigatedActorIndex + this.filteredActors.length - 1) % this.filteredActors.length;
|
||||
}
|
||||
@ -429,11 +437,11 @@ export default class EditorComponent extends Vue {
|
||||
* navigate to the next item
|
||||
* if it's the last item, navigate to the first one
|
||||
*/
|
||||
downHandler() {
|
||||
downHandler(): void {
|
||||
this.navigatedActorIndex = (this.navigatedActorIndex + 1) % this.filteredActors.length;
|
||||
}
|
||||
|
||||
enterHandler() {
|
||||
enterHandler(): void {
|
||||
const actor = this.filteredActors[this.navigatedActorIndex];
|
||||
if (actor) {
|
||||
this.selectActor(actor);
|
||||
@ -445,7 +453,7 @@ export default class EditorComponent extends Vue {
|
||||
* so it's important to pass also the position of your suggestion text
|
||||
* @param actor IActor
|
||||
*/
|
||||
selectActor(actor: IActor) {
|
||||
selectActor(actor: IActor): void {
|
||||
const actorModel = new Actor(actor);
|
||||
this.insertMention({
|
||||
range: this.suggestionRange,
|
||||
@ -460,7 +468,7 @@ export default class EditorComponent extends Vue {
|
||||
}
|
||||
|
||||
/** We use this to programatically insert an actor mention when creating a reply to comment */
|
||||
replyToComment(comment: IComment) {
|
||||
replyToComment(comment: IComment): void {
|
||||
if (!comment.actor) return;
|
||||
const actorModel = new Actor(comment.actor);
|
||||
if (!this.editor) return;
|
||||
@ -476,11 +484,12 @@ export default class EditorComponent extends Vue {
|
||||
* tiptap provides a virtualNode object for using popper.js (or tippy.js) for popups
|
||||
* @param node
|
||||
*/
|
||||
renderPopup(node: Element) {
|
||||
renderPopup(node: Element): void {
|
||||
if (this.popup) {
|
||||
return;
|
||||
}
|
||||
this.popup = tippy("#mobilizon", {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
getReferenceClientRect: node.getBoundingClientRect,
|
||||
appendTo: () => document.body,
|
||||
@ -497,8 +506,9 @@ export default class EditorComponent extends Vue {
|
||||
}) as Instance[];
|
||||
}
|
||||
|
||||
destroyPopup() {
|
||||
destroyPopup(): void {
|
||||
if (this.popup) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
this.popup[0].destroy();
|
||||
this.popup = null;
|
||||
@ -512,7 +522,7 @@ export default class EditorComponent extends Vue {
|
||||
* Show a file prompt, upload picture and insert it into editor
|
||||
* @param command
|
||||
*/
|
||||
async showImagePrompt(command: Function) {
|
||||
async showImagePrompt(command: Function): Promise<void> {
|
||||
const image = await listenFileUpload();
|
||||
const { data } = await this.$apollo.mutate({
|
||||
mutation: UPLOAD_PICTURE,
|
||||
@ -527,7 +537,7 @@ export default class EditorComponent extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
beforeDestroy() {
|
||||
beforeDestroy(): void {
|
||||
if (!this.editor) return;
|
||||
this.destroyPopup();
|
||||
this.editor.destroy();
|
||||
@ -577,9 +587,19 @@ $color-white: #eee;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
&.mode_description {
|
||||
.editor__content div.ProseMirror {
|
||||
min-height: 10rem;
|
||||
}
|
||||
|
||||
&.short_mode {
|
||||
div.ProseMirror {
|
||||
min-height: 10rem;
|
||||
min-height: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.comment_mode {
|
||||
div.ProseMirror {
|
||||
min-height: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
33
js/src/components/Editor/MaxSize.ts
Normal file
33
js/src/components/Editor/MaxSize.ts
Normal file
@ -0,0 +1,33 @@
|
||||
// @ts-nocheck
|
||||
import { Extension, Plugin } from "tiptap";
|
||||
|
||||
export default class MaxSize extends Extension {
|
||||
get name() {
|
||||
return "maxSize";
|
||||
}
|
||||
|
||||
get defaultOptions() {
|
||||
return {
|
||||
maxSize: null,
|
||||
};
|
||||
}
|
||||
|
||||
get plugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
appendTransaction: (transactions, oldState, newState) => {
|
||||
const max = this.options.maxSize;
|
||||
const oldLength = oldState.doc.content.size;
|
||||
const newLength = newState.doc.content.size;
|
||||
|
||||
if (newLength > max && newLength > oldLength) {
|
||||
let newTr = newState.tr;
|
||||
newTr.insertText("", max + 1, newLength);
|
||||
|
||||
return newTr;
|
||||
}
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
102
js/src/components/Event/OrganizerPicker.vue
Normal file
102
js/src/components/Event/OrganizerPicker.vue
Normal file
@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div class="list is-hoverable">
|
||||
<b-radio-button
|
||||
v-model="currentActor"
|
||||
:native-value="availableActor"
|
||||
class="list-item"
|
||||
v-for="availableActor in actualAvailableActors"
|
||||
:class="{ 'is-active': availableActor.id === currentActor.id }"
|
||||
:key="availableActor.id"
|
||||
>
|
||||
<div class="media">
|
||||
<figure class="image is-48x48" v-if="availableActor.avatar">
|
||||
<img class="media-left is-rounded" :src="availableActor.avatar.url" alt="" />
|
||||
</figure>
|
||||
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
|
||||
<div class="media-content">
|
||||
<h3>{{ availableActor.name }}</h3>
|
||||
<small>{{ `@${availableActor.preferredUsername}` }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</b-radio-button>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||
import { IMember, IPerson, MemberRole, IActor, Actor } from "@/types/actor";
|
||||
import { PERSON_MEMBERSHIPS } from "@/graphql/actor";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
groupMemberships: {
|
||||
query: PERSON_MEMBERSHIPS,
|
||||
variables() {
|
||||
return {
|
||||
id: this.identity.id,
|
||||
};
|
||||
},
|
||||
update: (data) => data.person.memberships,
|
||||
skip() {
|
||||
return !this.identity.id;
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class OrganizerPicker extends Vue {
|
||||
@Prop() value!: IActor;
|
||||
|
||||
@Prop() identity!: IPerson;
|
||||
|
||||
@Prop({ required: false, default: false }) restrictModeratorLevel!: boolean;
|
||||
|
||||
groupMemberships: Paginate<IMember> = { elements: [], total: 0 };
|
||||
|
||||
currentActor: IActor = this.value;
|
||||
|
||||
Actor = Actor;
|
||||
|
||||
get actualMemberships(): IMember[] {
|
||||
if (this.restrictModeratorLevel) {
|
||||
return this.groupMemberships.elements.filter((membership: IMember) =>
|
||||
[MemberRole.ADMINISTRATOR, MemberRole.MODERATOR, MemberRole.CREATOR].includes(
|
||||
membership.role
|
||||
)
|
||||
);
|
||||
}
|
||||
return this.groupMemberships.elements;
|
||||
}
|
||||
|
||||
get actualAvailableActors(): IActor[] {
|
||||
return [this.identity, ...this.actualMemberships.map((member) => member.parent)];
|
||||
}
|
||||
|
||||
@Watch("currentActor")
|
||||
async fetchMembersForGroup(): Promise<void> {
|
||||
this.$emit("input", this.currentActor);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
/deep/ .list-item {
|
||||
box-sizing: content-box;
|
||||
|
||||
label.b-radio {
|
||||
padding: 0.85rem 0;
|
||||
|
||||
.media {
|
||||
padding: 0.25rem 0;
|
||||
align-items: center;
|
||||
|
||||
figure.image,
|
||||
span.icon.media-left {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
span.icon.media-left {
|
||||
margin-left: -0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
199
js/src/components/Event/OrganizerPickerWrapper.vue
Normal file
199
js/src/components/Event/OrganizerPickerWrapper.vue
Normal file
@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<div class="organizer-picker">
|
||||
<!-- If we have a current actor (inline) -->
|
||||
<div v-if="inline && currentActor.id" class="inline box" @click="isComponentModalActive = true">
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-48x48" v-if="currentActor.avatar">
|
||||
<img
|
||||
class="image is-rounded"
|
||||
:src="currentActor.avatar.url"
|
||||
:alt="currentActor.avatar.alt"
|
||||
/>
|
||||
</figure>
|
||||
<b-icon v-else size="is-large" icon="account-circle" />
|
||||
</div>
|
||||
<div class="media-content" v-if="currentActor.name">
|
||||
<p class="is-4">{{ currentActor.name }}</p>
|
||||
<p class="is-6 has-text-grey">{{ `@${currentActor.preferredUsername}` }}</p>
|
||||
</div>
|
||||
<div class="media-content" v-else>
|
||||
{{ `@${currentActor.preferredUsername}` }}
|
||||
</div>
|
||||
<b-button type="is-text" @click="isComponentModalActive = true">
|
||||
{{ $t("Change") }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- If we have a current actor -->
|
||||
<span v-else-if="currentActor.id" class="block" @click="isComponentModalActive = true">
|
||||
<img
|
||||
class="image is-48x48"
|
||||
v-if="currentActor.avatar"
|
||||
:src="currentActor.avatar.url"
|
||||
:alt="currentActor.avatar.alt"
|
||||
/>
|
||||
<b-icon v-else size="is-large" icon="account-circle" />
|
||||
</span>
|
||||
<!-- If we have no current actor -->
|
||||
<div v-if="groupMemberships.total === 0 || !currentActor.id" class="box">
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-48x48" v-if="identity.avatar">
|
||||
<img class="image is-rounded" :src="identity.avatar.url" :alt="identity.avatar.alt" />
|
||||
</figure>
|
||||
<b-icon v-else size="is-large" icon="account-circle" />
|
||||
</div>
|
||||
<div class="media-content" v-if="identity.name">
|
||||
<p class="is-4">{{ identity.name }}</p>
|
||||
<p class="is-6 has-text-grey">{{ `@${identity.preferredUsername}` }}</p>
|
||||
</div>
|
||||
<div class="media-content" v-else>
|
||||
{{ `@${identity.preferredUsername}` }}
|
||||
</div>
|
||||
<b-button type="is-text" @click="isComponentModalActive = true">
|
||||
{{ $t("Change") }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
<b-modal :active.sync="isComponentModalActive" has-modal-card>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">{{ $t("Pick a profile or a group") }}</p>
|
||||
</header>
|
||||
<section class="modal-card-body">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<organizer-picker
|
||||
v-model="currentActor"
|
||||
:identity.sync="identity"
|
||||
@input="relay"
|
||||
:restrict-moderator-level="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div v-if="actorMembersForCurrentActor.length > 0">
|
||||
<p>{{ $t("Add a contact") }}</p>
|
||||
<p class="field" v-for="actor in actorMembersForCurrentActor" :key="actor.id">
|
||||
<b-checkbox v-model="actualContacts" :native-value="actor.id">
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-48x48" v-if="actor.avatar">
|
||||
<img
|
||||
class="image is-rounded"
|
||||
:src="actor.avatar.url"
|
||||
:alt="actor.avatar.alt"
|
||||
/>
|
||||
</figure>
|
||||
<b-icon v-else size="is-large" icon="account-circle" />
|
||||
</div>
|
||||
<div class="media-content" v-if="actor.name">
|
||||
<p class="is-4">{{ actor.name }}</p>
|
||||
<p class="is-6 has-text-grey">{{ `@${actor.preferredUsername}` }}</p>
|
||||
</div>
|
||||
<div class="media-content" v-else>
|
||||
{{ `@${actor.preferredUsername}` }}
|
||||
</div>
|
||||
</div>
|
||||
</b-checkbox>
|
||||
</p>
|
||||
</div>
|
||||
<div v-else class="content has-text-grey has-text-centered">
|
||||
<p>{{ $t("Your profile will be shown as contact.") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<footer class="modal-card-foot">
|
||||
<button class="button is-primary" type="button" @click="pickActor">
|
||||
{{ $t("Pick") }}
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||
import { IActor, IGroup, IMember, IPerson } from "../../types/actor";
|
||||
import OrganizerPicker from "./OrganizerPicker.vue";
|
||||
import { PERSON_MEMBERSHIPS_WITH_MEMBERS } from "../../graphql/actor";
|
||||
import { Paginate } from "../../types/paginate";
|
||||
|
||||
@Component({
|
||||
components: { OrganizerPicker },
|
||||
apollo: {
|
||||
groupMemberships: {
|
||||
query: PERSON_MEMBERSHIPS_WITH_MEMBERS,
|
||||
variables() {
|
||||
return {
|
||||
id: this.identity.id,
|
||||
};
|
||||
},
|
||||
update: (data) => data.person.memberships,
|
||||
skip() {
|
||||
return !this.identity.id;
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class OrganizerPickerWrapper extends Vue {
|
||||
@Prop({ type: Object, required: true }) value!: IActor;
|
||||
|
||||
@Prop({ default: true, type: Boolean }) inline!: boolean;
|
||||
|
||||
@Prop({ type: Object, required: true }) identity!: IPerson;
|
||||
|
||||
isComponentModalActive = false;
|
||||
|
||||
currentActor: IActor = this.value;
|
||||
|
||||
groupMemberships: Paginate<IMember> = { elements: [], total: 0 };
|
||||
|
||||
@Prop({ type: Array, required: false, default: () => [] }) contacts!: IActor[];
|
||||
|
||||
actualContacts: (string | undefined)[] = this.contacts.map(({ id }) => id);
|
||||
|
||||
@Watch("contacts")
|
||||
updateActualContacts(contacts: IActor[]): void {
|
||||
this.actualContacts = contacts.map(({ id }) => id);
|
||||
}
|
||||
|
||||
@Watch("value")
|
||||
updateCurrentActor(value: IGroup): void {
|
||||
this.currentActor = value;
|
||||
}
|
||||
|
||||
async relay(group: IGroup): Promise<void> {
|
||||
this.currentActor = group;
|
||||
}
|
||||
|
||||
pickActor(): void {
|
||||
this.$emit(
|
||||
"update:contacts",
|
||||
this.actorMembersForCurrentActor.filter(({ id }) => this.actualContacts.includes(id))
|
||||
);
|
||||
this.$emit("input", this.currentActor);
|
||||
this.isComponentModalActive = false;
|
||||
}
|
||||
|
||||
get actorMembersForCurrentActor(): IActor[] {
|
||||
const currentMembership = this.groupMemberships.elements.find(
|
||||
({ parent: { id } }) => id === this.currentActor.id
|
||||
);
|
||||
if (currentMembership) {
|
||||
return currentMembership.parent.members.elements.map(({ actor }) => actor);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.group-picker {
|
||||
.block,
|
||||
.no-group,
|
||||
.inline {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -10,7 +10,7 @@
|
||||
<router-link :to="{ name: RouteName.TERMS }">{{ $t("Terms") }}</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://framagit.org/framasoft/mobilizon/blob/master/LICENSE">
|
||||
<a hreflang="en" href="https://framagit.org/framasoft/mobilizon/blob/master/LICENSE">
|
||||
{{ $t("License") }}
|
||||
</a>
|
||||
</li>
|
||||
|
@ -1,32 +1,52 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-48x48">
|
||||
<img src="https://bulma.io/images/placeholders/96x96.png" alt="Placeholder image" />
|
||||
</figure>
|
||||
<div>
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-48x48" v-if="member.parent.avatar">
|
||||
<img class="is-rounded" :src="member.parent.avatar.url" alt="" />
|
||||
</figure>
|
||||
<b-icon v-else size="is-large" icon="account-group" />
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: usernameWithDomain(member.parent) },
|
||||
}"
|
||||
>
|
||||
<h3>{{ member.parent.name }}</h3>
|
||||
<p class="is-6 has-text-grey">
|
||||
<span v-if="member.parent.domain">{{
|
||||
`@${member.parent.preferredUsername}@${member.parent.domain}`
|
||||
}}</span>
|
||||
<span v-else>{{ `@${member.parent.preferredUsername}` }}</span>
|
||||
<b-taglist>
|
||||
<b-tag type="is-info" v-if="member.role === MemberRole.ADMINISTRATOR">{{
|
||||
$t("Administrator")
|
||||
}}</b-tag>
|
||||
<b-tag type="is-info" v-else-if="member.role === MemberRole.MODERATOR">{{
|
||||
$t("Moderator")
|
||||
}}</b-tag>
|
||||
</b-taglist>
|
||||
</p>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: usernameWithDomain(member.parent) },
|
||||
}"
|
||||
>
|
||||
<h3>{{ member.parent.name }}</h3>
|
||||
<p class="is-6 has-text-grey">
|
||||
<span v-if="member.parent.domain">{{
|
||||
`@${member.parent.preferredUsername}@${member.parent.domain}`
|
||||
}}</span>
|
||||
<span v-else>{{ `@${member.parent.preferredUsername}` }}</span>
|
||||
</p>
|
||||
<b-tag type="is-info">{{ member.role }}</b-tag>
|
||||
</router-link>
|
||||
<div class="content" v-if="member.parent.summary">
|
||||
<p>{{ member.parent.summary }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>{{ member.parent.summary }}</p>
|
||||
<div>
|
||||
<b-dropdown aria-role="list" position="is-bottom-left">
|
||||
<b-icon icon="dots-horizontal" slot="trigger" />
|
||||
|
||||
<b-dropdown-item aria-role="listitem" @click="$emit('leave')">
|
||||
<b-icon icon="exit-to-app" />
|
||||
{{ $t("Leave") }}
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -34,7 +54,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { IMember, usernameWithDomain } from "@/types/actor";
|
||||
import { IMember, MemberRole, usernameWithDomain } from "@/types/actor";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component
|
||||
@ -44,5 +64,21 @@ export default class GroupMemberCard extends Vue {
|
||||
RouteName = RouteName;
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
|
||||
MemberRole = MemberRole;
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.card-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& > div:first-child {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
& > div:last-child {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -7,7 +7,7 @@
|
||||
<div class="list is-hoverable">
|
||||
<a
|
||||
class="list-item"
|
||||
v-for="groupMembership in groupMemberships.elements"
|
||||
v-for="groupMembership in actualMemberships"
|
||||
:class="{ 'is-active': groupMembership.parent.id === currentGroup.id }"
|
||||
@click="changeCurrentGroup(groupMembership.parent)"
|
||||
:key="groupMembership.id"
|
||||
@ -36,7 +36,7 @@
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { IGroup, IMember, IPerson, Group } from "@/types/actor";
|
||||
import { IGroup, IMember, IPerson, Group, MemberRole } from "@/types/actor";
|
||||
import { PERSON_MEMBERSHIPS } from "@/graphql/actor";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
|
||||
@ -61,15 +61,28 @@ export default class GroupPicker extends Vue {
|
||||
|
||||
@Prop() identity!: IPerson;
|
||||
|
||||
@Prop({ required: false, default: false }) restrictModeratorLevel!: boolean;
|
||||
|
||||
groupMemberships: Paginate<IMember> = { elements: [], total: 0 };
|
||||
|
||||
currentGroup: IGroup = this.value;
|
||||
|
||||
Group = Group;
|
||||
|
||||
changeCurrentGroup(group: IGroup) {
|
||||
changeCurrentGroup(group: IGroup): void {
|
||||
this.currentGroup = group;
|
||||
this.$emit("input", group);
|
||||
}
|
||||
|
||||
get actualMemberships(): IMember[] {
|
||||
if (this.restrictModeratorLevel) {
|
||||
return this.groupMemberships.elements.filter((membership: IMember) =>
|
||||
[MemberRole.ADMINISTRATOR, MemberRole.MODERATOR, MemberRole.CREATOR].includes(
|
||||
membership.role
|
||||
)
|
||||
);
|
||||
}
|
||||
return this.groupMemberships.elements;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -14,7 +14,11 @@
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-48x48" v-if="currentGroup.avatar">
|
||||
<img class="image" :src="currentGroup.avatar.url" :alt="currentGroup.avatar.alt" />
|
||||
<img
|
||||
class="image is-rounded"
|
||||
:src="currentGroup.avatar.url"
|
||||
:alt="currentGroup.avatar.alt"
|
||||
/>
|
||||
</figure>
|
||||
<b-icon v-else size="is-large" icon="account-circle" />
|
||||
</div>
|
||||
@ -46,7 +50,12 @@
|
||||
</p>
|
||||
</div>
|
||||
<b-modal :active.sync="isComponentModalActive" has-modal-card>
|
||||
<group-picker v-model="currentGroup" :identity.sync="identity" @input="relay" />
|
||||
<group-picker
|
||||
v-model="currentGroup"
|
||||
:identity.sync="identity"
|
||||
@input="relay"
|
||||
:restrict-moderator-level="true"
|
||||
/>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
@ -88,11 +97,11 @@ export default class GroupPickerWrapper extends Vue {
|
||||
groupMemberships: Paginate<IMember> = { elements: [], total: 0 };
|
||||
|
||||
@Watch("value")
|
||||
updateCurrentGroup(value: IGroup) {
|
||||
updateCurrentGroup(value: IGroup): void {
|
||||
this.currentGroup = value;
|
||||
}
|
||||
|
||||
relay(group: IGroup) {
|
||||
relay(group: IGroup): void {
|
||||
this.currentGroup = group;
|
||||
this.$emit("input", group);
|
||||
this.isComponentModalActive = false;
|
||||
|
@ -14,6 +14,7 @@ import { ACCEPT_INVITATION, REJECT_INVITATION } from "@/graphql/member";
|
||||
import { IMember } from "@/types/actor";
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import InvitationCard from "@/components/Group/InvitationCard.vue";
|
||||
import { SnackbarProgrammatic as Snackbar } from "buefy";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@ -23,27 +24,35 @@ import InvitationCard from "@/components/Group/InvitationCard.vue";
|
||||
export default class Invitations extends Vue {
|
||||
@Prop({ required: true, type: Array }) invitations!: IMember;
|
||||
|
||||
async acceptInvitation(id: string) {
|
||||
const { data } = await this.$apollo.mutate<{ acceptInvitation: IMember }>({
|
||||
mutation: ACCEPT_INVITATION,
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
if (data) {
|
||||
this.$emit("acceptInvitation", data.acceptInvitation);
|
||||
async acceptInvitation(id: string): Promise<void> {
|
||||
try {
|
||||
const { data } = await this.$apollo.mutate<{ acceptInvitation: IMember }>({
|
||||
mutation: ACCEPT_INVITATION,
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
if (data) {
|
||||
this.$emit("acceptInvitation", data.acceptInvitation);
|
||||
}
|
||||
} catch (e) {
|
||||
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
|
||||
}
|
||||
}
|
||||
|
||||
async rejectInvitation(id: string) {
|
||||
const { data } = await this.$apollo.mutate<{ rejectInvitation: IMember }>({
|
||||
mutation: REJECT_INVITATION,
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
if (data) {
|
||||
this.$emit("rejectInvitation", data.rejectInvitation);
|
||||
async rejectInvitation(id: string): Promise<void> {
|
||||
try {
|
||||
const { data } = await this.$apollo.mutate<{ rejectInvitation: IMember }>({
|
||||
mutation: REJECT_INVITATION,
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
if (data) {
|
||||
this.$emit("rejectInvitation", data.rejectInvitation);
|
||||
}
|
||||
} catch (e) {
|
||||
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { SnackbarProgrammatic as Snackbar } from "buefy";
|
||||
import RouteName from "../../router/name";
|
||||
import { IParticipant } from "../../types/event.model";
|
||||
import { CONFIRM_PARTICIPATION } from "../../graphql/event";
|
||||
@ -31,11 +32,11 @@ export default class ConfirmParticipation extends Vue {
|
||||
|
||||
failed = false;
|
||||
|
||||
async created() {
|
||||
async created(): Promise<void> {
|
||||
await this.validateAction();
|
||||
}
|
||||
|
||||
async validateAction() {
|
||||
async validateAction(): Promise<void> {
|
||||
try {
|
||||
const { data } = await this.$apollo.mutate<{
|
||||
confirmParticipation: IParticipant;
|
||||
@ -56,6 +57,8 @@ export default class ConfirmParticipation extends Vue {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
Snackbar.open({ message: err.message, type: "is-danger", position: "is-bottom" });
|
||||
this.failed = true;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
|