Migrate to Vue 3 and Vite
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
@@ -2,10 +2,10 @@
|
||||
<div v-if="group" class="section">
|
||||
<breadcrumbs-nav
|
||||
:links="[
|
||||
{ name: RouteName.ADMIN, text: $t('Admin') },
|
||||
{ name: RouteName.ADMIN, text: t('Admin') },
|
||||
{
|
||||
name: RouteName.ADMIN_GROUPS,
|
||||
text: $t('Groups'),
|
||||
text: t('Groups'),
|
||||
},
|
||||
{
|
||||
name: RouteName.PROFILES,
|
||||
@@ -39,7 +39,7 @@
|
||||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
<table v-if="metadata.length > 0" class="table is-fullwidth">
|
||||
<table v-if="metadata.length > 0" class="table w-full">
|
||||
<tbody>
|
||||
<tr v-for="{ key, value, link } in metadata" :key="key">
|
||||
<td>{{ key }}</td>
|
||||
@@ -52,52 +52,64 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="buttons">
|
||||
<b-button
|
||||
<div class="flex gap-1">
|
||||
<o-button
|
||||
@click="confirmSuspendProfile"
|
||||
v-if="!group.suspended"
|
||||
type="is-primary"
|
||||
>{{ $t("Suspend") }}</b-button
|
||||
variant="primary"
|
||||
>{{ t("Suspend") }}</o-button
|
||||
>
|
||||
<b-button
|
||||
@click="unsuspendProfile"
|
||||
<o-button
|
||||
@click="
|
||||
unsuspendProfile({
|
||||
id,
|
||||
})
|
||||
"
|
||||
v-if="group.suspended"
|
||||
type="is-primary"
|
||||
>{{ $t("Unsuspend") }}</b-button
|
||||
variant="primary"
|
||||
>{{ t("Unsuspend") }}</o-button
|
||||
>
|
||||
<b-button
|
||||
@click="refreshProfile"
|
||||
<o-button
|
||||
@click="
|
||||
refreshProfile({
|
||||
actorId: id,
|
||||
})
|
||||
"
|
||||
v-if="group.domain"
|
||||
type="is-primary"
|
||||
variant="primary"
|
||||
outlined
|
||||
>{{ $t("Refresh profile") }}</b-button
|
||||
>{{ t("Refresh profile") }}</o-button
|
||||
>
|
||||
</div>
|
||||
<section>
|
||||
<h2 class="subtitle">
|
||||
<h2>
|
||||
{{
|
||||
$tc("{number} members", group.members.total, {
|
||||
number: group.members.total,
|
||||
})
|
||||
t(
|
||||
"{number} members",
|
||||
{
|
||||
number: group.members.total,
|
||||
},
|
||||
group.members.total
|
||||
)
|
||||
}}
|
||||
</h2>
|
||||
<b-table
|
||||
<o-table
|
||||
:data="group.members.elements"
|
||||
:loading="$apollo.queries.group.loading"
|
||||
:loading="loading"
|
||||
paginated
|
||||
backend-pagination
|
||||
:current-page.sync="membersPage"
|
||||
:aria-next-label="$t('Next page')"
|
||||
:aria-previous-label="$t('Previous page')"
|
||||
:aria-page-label="$t('Page')"
|
||||
:aria-current-label="$t('Current page')"
|
||||
v-model:current-page="membersPage"
|
||||
: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"
|
||||
@page-change="onMembersPageChange"
|
||||
>
|
||||
<b-table-column
|
||||
<o-table-column
|
||||
field="actor.preferredUsername"
|
||||
:label="$t('Member')"
|
||||
:label="t('Member')"
|
||||
v-slot="props"
|
||||
>
|
||||
<article class="media">
|
||||
@@ -111,14 +123,14 @@
|
||||
alt=""
|
||||
/>
|
||||
</figure>
|
||||
<b-icon
|
||||
<o-icon
|
||||
class="media-left"
|
||||
v-else
|
||||
size="is-large"
|
||||
size="large"
|
||||
icon="account-circle"
|
||||
/>
|
||||
<div class="media-content">
|
||||
<div class="content">
|
||||
<div class="prose dark:prose-invert">
|
||||
<span v-if="props.row.actor.name">{{
|
||||
props.row.actor.name
|
||||
}}</span
|
||||
@@ -132,163 +144,166 @@
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</b-table-column>
|
||||
<b-table-column field="role" :label="$t('Role')" v-slot="props">
|
||||
</o-table-column>
|
||||
<o-table-column field="role" :label="t('Role')" v-slot="props">
|
||||
<b-tag
|
||||
type="is-primary"
|
||||
variant="primary"
|
||||
v-if="props.row.role === MemberRole.ADMINISTRATOR"
|
||||
>
|
||||
{{ $t("Administrator") }}
|
||||
{{ t("Administrator") }}
|
||||
</b-tag>
|
||||
<b-tag
|
||||
type="is-primary"
|
||||
variant="primary"
|
||||
v-else-if="props.row.role === MemberRole.MODERATOR"
|
||||
>
|
||||
{{ $t("Moderator") }}
|
||||
{{ t("Moderator") }}
|
||||
</b-tag>
|
||||
<b-tag v-else-if="props.row.role === MemberRole.MEMBER">
|
||||
{{ $t("Member") }}
|
||||
{{ t("Member") }}
|
||||
</b-tag>
|
||||
<b-tag
|
||||
type="is-warning"
|
||||
variant="warning"
|
||||
v-else-if="props.row.role === MemberRole.NOT_APPROVED"
|
||||
>
|
||||
{{ $t("Not approved") }}
|
||||
{{ t("Not approved") }}
|
||||
</b-tag>
|
||||
<b-tag
|
||||
type="is-danger"
|
||||
variant="danger"
|
||||
v-else-if="props.row.role === MemberRole.REJECTED"
|
||||
>
|
||||
{{ $t("Rejected") }}
|
||||
{{ t("Rejected") }}
|
||||
</b-tag>
|
||||
<b-tag
|
||||
type="is-danger"
|
||||
variant="danger"
|
||||
v-else-if="props.row.role === MemberRole.INVITED"
|
||||
>
|
||||
{{ $t("Invited") }}
|
||||
{{ t("Invited") }}
|
||||
</b-tag>
|
||||
</b-table-column>
|
||||
<b-table-column field="insertedAt" :label="$t('Date')" v-slot="props">
|
||||
</o-table-column>
|
||||
<o-table-column field="insertedAt" :label="t('Date')" v-slot="props">
|
||||
<span class="has-text-centered">
|
||||
{{ props.row.insertedAt | formatDateString }}<br />{{
|
||||
props.row.insertedAt | formatTimeString
|
||||
{{ formatDateString(props.row.insertedAt) }}<br />{{
|
||||
formatTimeString(props.row.insertedAt)
|
||||
}}
|
||||
</span>
|
||||
</b-table-column>
|
||||
<template slot="empty">
|
||||
</o-table-column>
|
||||
<template #empty>
|
||||
<empty-content icon="account-group" :inline="true">
|
||||
{{ $t("No members found") }}
|
||||
{{ t("No members found") }}
|
||||
</empty-content>
|
||||
</template>
|
||||
</b-table>
|
||||
</o-table>
|
||||
</section>
|
||||
<section>
|
||||
<h2 class="subtitle">
|
||||
<h2>
|
||||
{{
|
||||
$tc("{number} organized events", group.organizedEvents.total, {
|
||||
number: group.organizedEvents.total,
|
||||
})
|
||||
t(
|
||||
"{number} organized events",
|
||||
{
|
||||
number: group.organizedEvents.total,
|
||||
},
|
||||
group.organizedEvents.total
|
||||
)
|
||||
}}
|
||||
</h2>
|
||||
<b-table
|
||||
<o-table
|
||||
:data="group.organizedEvents.elements"
|
||||
:loading="$apollo.queries.group.loading"
|
||||
:loading="loading"
|
||||
paginated
|
||||
backend-pagination
|
||||
:current-page.sync="organizedEventsPage"
|
||||
:aria-next-label="$t('Next page')"
|
||||
:aria-previous-label="$t('Previous page')"
|
||||
:aria-page-label="$t('Page')"
|
||||
:aria-current-label="$t('Current page')"
|
||||
v-model:current-page="organizedEventsPage"
|
||||
: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.organizedEvents.total"
|
||||
:per-page="EVENTS_PER_PAGE"
|
||||
@page-change="onOrganizedEventsPageChange"
|
||||
>
|
||||
<b-table-column field="title" :label="$t('Title')" v-slot="props">
|
||||
<o-table-column field="title" :label="t('Title')" v-slot="props">
|
||||
<router-link
|
||||
:to="{ name: RouteName.EVENT, params: { uuid: props.row.uuid } }"
|
||||
>
|
||||
{{ props.row.title }}
|
||||
<b-tag type="is-info" v-if="props.row.draft">{{
|
||||
$t("Draft")
|
||||
<b-tag variant="info" v-if="props.row.draft">{{
|
||||
t("Draft")
|
||||
}}</b-tag>
|
||||
</router-link>
|
||||
</b-table-column>
|
||||
<b-table-column
|
||||
field="beginsOn"
|
||||
:label="$t('Begins on')"
|
||||
v-slot="props"
|
||||
>
|
||||
{{ props.row.beginsOn | formatDateTimeString }}
|
||||
</b-table-column>
|
||||
<template slot="empty">
|
||||
</o-table-column>
|
||||
<o-table-column field="beginsOn" :label="t('Begins on')" v-slot="props">
|
||||
{{ formatDateTimeString(props.row.beginsOn) }}
|
||||
</o-table-column>
|
||||
<template #empty>
|
||||
<empty-content icon="account-group" :inline="true">
|
||||
{{ $t("No organized events found") }}
|
||||
{{ t("No organized events found") }}
|
||||
</empty-content>
|
||||
</template>
|
||||
</b-table>
|
||||
</o-table>
|
||||
</section>
|
||||
<section>
|
||||
<h2 class="subtitle">
|
||||
<h2>
|
||||
{{
|
||||
$tc("{number} posts", group.posts.total, {
|
||||
number: group.posts.total,
|
||||
})
|
||||
t(
|
||||
"{number} posts",
|
||||
{
|
||||
number: group.posts.total,
|
||||
},
|
||||
group.posts.total
|
||||
)
|
||||
}}
|
||||
</h2>
|
||||
<b-table
|
||||
<o-table
|
||||
:data="group.posts.elements"
|
||||
:loading="$apollo.queries.group.loading"
|
||||
:loading="loading"
|
||||
paginated
|
||||
backend-pagination
|
||||
:current-page.sync="postsPage"
|
||||
:aria-next-label="$t('Next page')"
|
||||
:aria-previous-label="$t('Previous page')"
|
||||
:aria-page-label="$t('Page')"
|
||||
:aria-current-label="$t('Current page')"
|
||||
v-model:current-page="postsPage"
|
||||
: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.posts.total"
|
||||
:per-page="POSTS_PER_PAGE"
|
||||
@page-change="onPostsPageChange"
|
||||
>
|
||||
<b-table-column field="title" :label="$t('Title')" v-slot="props">
|
||||
<o-table-column field="title" :label="t('Title')" v-slot="props">
|
||||
<router-link
|
||||
:to="{ name: RouteName.POST, params: { slug: props.row.slug } }"
|
||||
>
|
||||
{{ props.row.title }}
|
||||
<b-tag type="is-info" v-if="props.row.draft">{{
|
||||
$t("Draft")
|
||||
<b-tag variant="info" v-if="props.row.draft">{{
|
||||
t("Draft")
|
||||
}}</b-tag>
|
||||
</router-link>
|
||||
</b-table-column>
|
||||
<b-table-column
|
||||
</o-table-column>
|
||||
<o-table-column
|
||||
field="publishAt"
|
||||
:label="$t('Publication date')"
|
||||
:label="t('Publication date')"
|
||||
v-slot="props"
|
||||
>
|
||||
{{ props.row.publishAt | formatDateTimeString }}
|
||||
</b-table-column>
|
||||
<template slot="empty">
|
||||
{{ formatDateTimeString(props.row.publishAt) }}
|
||||
</o-table-column>
|
||||
<template #empty>
|
||||
<empty-content icon="bullhorn" :inline="true">
|
||||
{{ $t("No posts found") }}
|
||||
{{ t("No posts found") }}
|
||||
</empty-content>
|
||||
</template>
|
||||
</b-table>
|
||||
</o-table>
|
||||
</section>
|
||||
</div>
|
||||
<empty-content v-else-if="!$apollo.loading" icon="account-multiple">
|
||||
{{ $t("This group was not found") }}
|
||||
<empty-content v-else-if="!loading" icon="account-multiple">
|
||||
{{ t("This group was not found") }}
|
||||
<template #desc>
|
||||
<b-button
|
||||
<o-button
|
||||
type="is-text"
|
||||
tag="router-link"
|
||||
:to="{ name: RouteName.ADMIN_GROUPS }"
|
||||
>{{ $t("Back to group list") }}</b-button
|
||||
>{{ t("Back to group list") }}</o-button
|
||||
>
|
||||
</template>
|
||||
</empty-content>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from "vue-property-decorator";
|
||||
<script lang="ts" setup>
|
||||
import { GET_GROUP, REFRESH_PROFILE } from "@/graphql/group";
|
||||
import { formatBytes } from "@/utils/datetime";
|
||||
import { MemberRole } from "@/types/enums";
|
||||
@@ -303,273 +318,216 @@ import RouteName from "../../router/name";
|
||||
import ActorCard from "../../components/Account/ActorCard.vue";
|
||||
import EmptyContent from "../../components/Utils/EmptyContent.vue";
|
||||
import { ApolloCache, FetchResult } from "@apollo/client/core";
|
||||
import VueRouter from "vue-router";
|
||||
const { isNavigationFailure, NavigationFailureType } = VueRouter;
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { computed, inject } from "vue";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import { integerTransformer, useRouteQuery } from "vue-use-route-query";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {
|
||||
formatTimeString,
|
||||
formatDateString,
|
||||
formatDateTimeString,
|
||||
} from "@/filters/datetime";
|
||||
import { Dialog } from "@/plugins/dialog";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
|
||||
const EVENTS_PER_PAGE = 10;
|
||||
const POSTS_PER_PAGE = 10;
|
||||
const MEMBERS_PER_PAGE = 10;
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
group: {
|
||||
query: GET_GROUP,
|
||||
fetchPolicy: "cache-and-network",
|
||||
variables() {
|
||||
return {
|
||||
id: this.id,
|
||||
organizedEventsPage: this.organizedEventsPage,
|
||||
organizedEventsLimit: EVENTS_PER_PAGE,
|
||||
postsPage: this.postsPage,
|
||||
postsLimit: POSTS_PER_PAGE,
|
||||
membersLimit: MEMBERS_PER_PAGE,
|
||||
membersPage: this.membersPage,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.id;
|
||||
},
|
||||
update: (data) => data.getGroup,
|
||||
const props = defineProps<{ id: string }>();
|
||||
|
||||
const organizedEventsPage = useRouteQuery(
|
||||
"organizedEventsPage",
|
||||
1,
|
||||
integerTransformer
|
||||
);
|
||||
const membersPage = useRouteQuery("membersPage", 1, integerTransformer);
|
||||
const postsPage = useRouteQuery("postsPage", 1, integerTransformer);
|
||||
|
||||
const {
|
||||
result: groupResult,
|
||||
loading,
|
||||
fetchMore,
|
||||
} = useQuery(
|
||||
GET_GROUP,
|
||||
() => ({
|
||||
id: props.id,
|
||||
organizedEventsPage: organizedEventsPage.value,
|
||||
organizedEventsLimit: EVENTS_PER_PAGE,
|
||||
postsPage: postsPage.value,
|
||||
postsLimit: POSTS_PER_PAGE,
|
||||
membersLimit: MEMBERS_PER_PAGE,
|
||||
membersPage: membersPage.value,
|
||||
}),
|
||||
() => ({
|
||||
enabled: props.id !== undefined,
|
||||
})
|
||||
);
|
||||
|
||||
const group = computed(() => groupResult.value?.getGroup);
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
useHead({
|
||||
title: computed(() => displayName(group.value)),
|
||||
});
|
||||
|
||||
const metadata = computed((): Array<Record<string, string>> => {
|
||||
if (!group.value) return [];
|
||||
const res: Record<string, string>[] = [
|
||||
{
|
||||
key: t("Status") as string,
|
||||
value: (group.value.suspended ? t("Suspended") : t("Active")) as string,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
ActorCard,
|
||||
EmptyContent,
|
||||
},
|
||||
metaInfo() {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const { group } = this;
|
||||
return {
|
||||
title: group ? group.name || usernameWithDomain(group) : "",
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class AdminGroupProfile extends Vue {
|
||||
@Prop({ required: true }) id!: string;
|
||||
{
|
||||
key: t("Domain") as string,
|
||||
value: (group.value.domain ? group.value.domain : t("Local")) as string,
|
||||
},
|
||||
{
|
||||
key: t("Uploaded media size") as string,
|
||||
value: formatBytes(group.value.mediaSize),
|
||||
},
|
||||
];
|
||||
return res;
|
||||
});
|
||||
|
||||
group!: IGroup;
|
||||
const dialog = inject<Dialog>("dialog");
|
||||
const notifier = inject<Notifier>("notifier");
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
const confirmSuspendProfile = (): void => {
|
||||
const message = (
|
||||
group.value.domain
|
||||
? t(
|
||||
"Are you sure you want to <b>suspend</b> this group? As this group originates from instance {instance}, this will only remove local members and delete the local data, as well as rejecting all the future data.",
|
||||
{ instance: group.value.domain }
|
||||
)
|
||||
: t(
|
||||
"Are you sure you want to <b>suspend</b> this group? All members - including remote ones - will be notified and removed from the group, and <b>all of the group data (events, posts, discussions, todos…) will be irretrievably destroyed</b>."
|
||||
)
|
||||
) as string;
|
||||
|
||||
displayName = displayName;
|
||||
dialog?.confirm({
|
||||
title: t("Suspend group") as string,
|
||||
message,
|
||||
confirmText: t("Suspend group") as string,
|
||||
cancelText: t("Cancel") as string,
|
||||
type: "danger",
|
||||
hasIcon: true,
|
||||
onConfirm: () =>
|
||||
suspendProfile({
|
||||
id: props.id,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
RouteName = RouteName;
|
||||
const { mutate: suspendProfile, onError: onSuspendProfileError } = useMutation<{
|
||||
suspendProfile: { id: string };
|
||||
}>(SUSPEND_PROFILE, () => ({
|
||||
update: (
|
||||
store: ApolloCache<{ suspendProfile: { id: string } }>,
|
||||
{ data }: FetchResult
|
||||
) => {
|
||||
if (data == null) return;
|
||||
const profileId = props.id;
|
||||
|
||||
EVENTS_PER_PAGE = EVENTS_PER_PAGE;
|
||||
|
||||
POSTS_PER_PAGE = POSTS_PER_PAGE;
|
||||
|
||||
MEMBERS_PER_PAGE = MEMBERS_PER_PAGE;
|
||||
|
||||
MemberRole = MemberRole;
|
||||
|
||||
get organizedEventsPage(): number {
|
||||
return parseInt(
|
||||
(this.$route.query.organizedEventsPage as string) || "1",
|
||||
10
|
||||
);
|
||||
}
|
||||
|
||||
set organizedEventsPage(page: number) {
|
||||
this.pushRouter({ organizedEventsPage: page.toString() });
|
||||
}
|
||||
|
||||
get membersPage(): number {
|
||||
return parseInt((this.$route.query.membersPage as string) || "1", 10);
|
||||
}
|
||||
|
||||
set membersPage(page: number) {
|
||||
this.pushRouter({ membersPage: page.toString() });
|
||||
}
|
||||
|
||||
get postsPage(): number {
|
||||
return parseInt((this.$route.query.postsPage as string) || "1", 10);
|
||||
}
|
||||
|
||||
set postsPage(page: number) {
|
||||
this.pushRouter({ postsPage: page.toString() });
|
||||
}
|
||||
|
||||
get metadata(): Array<Record<string, string>> {
|
||||
if (!this.group) return [];
|
||||
const res: Record<string, string>[] = [
|
||||
{
|
||||
key: this.$t("Status") as string,
|
||||
value: (this.group.suspended
|
||||
? this.$t("Suspended")
|
||||
: this.$t("Active")) as string,
|
||||
},
|
||||
{
|
||||
key: this.$t("Domain") as string,
|
||||
value: (this.group.domain
|
||||
? this.group.domain
|
||||
: this.$t("Local")) as string,
|
||||
},
|
||||
{
|
||||
key: this.$i18n.t("Uploaded media size") as string,
|
||||
value: formatBytes(this.group.mediaSize),
|
||||
},
|
||||
];
|
||||
return res;
|
||||
}
|
||||
|
||||
confirmSuspendProfile(): void {
|
||||
const message = (
|
||||
this.group.domain
|
||||
? this.$t(
|
||||
"Are you sure you want to <b>suspend</b> this group? As this group originates from instance {instance}, this will only remove local members and delete the local data, as well as rejecting all the future data.",
|
||||
{ instance: this.group.domain }
|
||||
)
|
||||
: this.$t(
|
||||
"Are you sure you want to <b>suspend</b> this group? All members - including remote ones - will be notified and removed from the group, and <b>all of the group data (events, posts, discussions, todos…) will be irretrievably destroyed</b>."
|
||||
)
|
||||
) as string;
|
||||
|
||||
this.$buefy.dialog.confirm({
|
||||
title: this.$t("Suspend group") as string,
|
||||
message,
|
||||
confirmText: this.$t("Suspend group") as string,
|
||||
cancelText: this.$t("Cancel") as string,
|
||||
type: "is-danger",
|
||||
hasIcon: true,
|
||||
onConfirm: () => this.suspendProfile(),
|
||||
});
|
||||
}
|
||||
|
||||
async suspendProfile(): Promise<void> {
|
||||
try {
|
||||
await this.$apollo.mutate<{ suspendProfile: { id: string } }>({
|
||||
mutation: SUSPEND_PROFILE,
|
||||
variables: {
|
||||
id: this.id,
|
||||
},
|
||||
update: (
|
||||
store: ApolloCache<{ suspendProfile: { id: string } }>,
|
||||
{ data }: FetchResult
|
||||
) => {
|
||||
if (data == null) return;
|
||||
const profileId = this.id;
|
||||
|
||||
const profileData = store.readQuery<{ getGroup: IGroup }>({
|
||||
query: GET_GROUP,
|
||||
variables: {
|
||||
id: profileId,
|
||||
organizedEventsPage: this.organizedEventsPage,
|
||||
organizedEventsLimit: EVENTS_PER_PAGE,
|
||||
postsPage: this.postsPage,
|
||||
postsLimit: POSTS_PER_PAGE,
|
||||
},
|
||||
});
|
||||
|
||||
if (!profileData) return;
|
||||
store.writeQuery({
|
||||
query: GET_GROUP,
|
||||
variables: {
|
||||
id: profileId,
|
||||
},
|
||||
data: {
|
||||
getGroup: {
|
||||
...profileData.getGroup,
|
||||
suspended: true,
|
||||
avatar: null,
|
||||
name: "",
|
||||
summary: "",
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.$notifier.error(this.$t("Error while suspending group") as string);
|
||||
}
|
||||
}
|
||||
|
||||
async unsuspendProfile(): Promise<void> {
|
||||
try {
|
||||
const profileID = this.id;
|
||||
await this.$apollo.mutate<{ unsuspendProfile: { id: string } }>({
|
||||
mutation: UNSUSPEND_PROFILE,
|
||||
variables: {
|
||||
id: this.id,
|
||||
},
|
||||
refetchQueries: [
|
||||
{
|
||||
query: GET_GROUP,
|
||||
variables: {
|
||||
id: profileID,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.$notifier.error(this.$t("Error while suspending group") as string);
|
||||
}
|
||||
}
|
||||
|
||||
async refreshProfile(): Promise<void> {
|
||||
try {
|
||||
this.$apollo.mutate<{ refreshProfile: IActor }>({
|
||||
mutation: REFRESH_PROFILE,
|
||||
variables: {
|
||||
actorId: this.id,
|
||||
},
|
||||
});
|
||||
this.$notifier.success(
|
||||
this.$t("Triggered profile refreshment") as string
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.$notifier.error(this.$t("Error while suspending group") as string);
|
||||
}
|
||||
}
|
||||
|
||||
async onOrganizedEventsPageChange(page: number): Promise<void> {
|
||||
this.organizedEventsPage = page;
|
||||
await this.$apollo.queries.group.fetchMore({
|
||||
const profileData = store.readQuery<{ getGroup: IGroup }>({
|
||||
query: GET_GROUP,
|
||||
variables: {
|
||||
actorId: this.id,
|
||||
organizedEventsPage: this.organizedEventsPage,
|
||||
id: profileId,
|
||||
organizedEventsPage: organizedEventsPage.value,
|
||||
organizedEventsLimit: EVENTS_PER_PAGE,
|
||||
postsPage: postsPage.value,
|
||||
postsLimit: POSTS_PER_PAGE,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async onMembersPageChange(page: number): Promise<void> {
|
||||
this.membersPage = page;
|
||||
await this.$apollo.queries.group.fetchMore({
|
||||
if (!profileData) return;
|
||||
store.writeQuery({
|
||||
query: GET_GROUP,
|
||||
variables: {
|
||||
actorId: this.id,
|
||||
memberPage: this.membersPage,
|
||||
memberLimit: EVENTS_PER_PAGE,
|
||||
id: profileId,
|
||||
},
|
||||
data: {
|
||||
getGroup: {
|
||||
...profileData.getGroup,
|
||||
suspended: true,
|
||||
avatar: null,
|
||||
name: "",
|
||||
summary: "",
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
async onPostsPageChange(page: number): Promise<void> {
|
||||
this.postsPage = page;
|
||||
await this.$apollo.queries.group.fetchMore({
|
||||
variables: {
|
||||
actorId: this.id,
|
||||
postsPage: this.postsPage,
|
||||
postLimit: POSTS_PER_PAGE,
|
||||
onSuspendProfileError((e) => {
|
||||
console.error(e);
|
||||
notifier?.error(t("Error while suspending group"));
|
||||
});
|
||||
|
||||
const { mutate: unsuspendProfile, onError: onUnsuspendProfileError } =
|
||||
useMutation(UNSUSPEND_PROFILE, () => ({
|
||||
refetchQueries: [
|
||||
{
|
||||
query: GET_GROUP,
|
||||
variables: {
|
||||
id: props.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
],
|
||||
}));
|
||||
|
||||
private async pushRouter(args: Record<string, string>): Promise<void> {
|
||||
try {
|
||||
await this.$router.push({
|
||||
name: RouteName.ADMIN_GROUP_PROFILE,
|
||||
query: { ...this.$route.query, ...args },
|
||||
});
|
||||
} catch (e) {
|
||||
if (isNavigationFailure(e, NavigationFailureType.redirected)) {
|
||||
throw Error(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
onUnsuspendProfileError((e) => {
|
||||
console.error(e);
|
||||
notifier?.error(t("Error while suspending group"));
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: refreshProfile,
|
||||
onDone: onRefreshProfileDone,
|
||||
onError: onRefreshProfileError,
|
||||
} = useMutation<{ refreshProfile: IActor }>(REFRESH_PROFILE);
|
||||
|
||||
onRefreshProfileDone(() => {
|
||||
notifier?.success(t("Triggered profile refreshment"));
|
||||
});
|
||||
|
||||
onRefreshProfileError((e) => {
|
||||
console.error(e);
|
||||
notifier?.error(t("Error while suspending group"));
|
||||
});
|
||||
|
||||
const onOrganizedEventsPageChange = async (page: number): Promise<void> => {
|
||||
organizedEventsPage.value = page;
|
||||
await fetchMore({
|
||||
variables: {
|
||||
id: props.id,
|
||||
organizedEventsPage: organizedEventsPage.value,
|
||||
organizedEventsLimit: EVENTS_PER_PAGE,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onMembersPageChange = async (page: number): Promise<void> => {
|
||||
membersPage.value = page;
|
||||
await fetchMore({
|
||||
variables: {
|
||||
id: props.id,
|
||||
membersPage: membersPage.value,
|
||||
membersLimit: EVENTS_PER_PAGE,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onPostsPageChange = async (page: number): Promise<void> => {
|
||||
postsPage.value = page;
|
||||
await fetchMore({
|
||||
variables: {
|
||||
id: props.id,
|
||||
postsPage: postsPage.value,
|
||||
postsLimit: POSTS_PER_PAGE,
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
/>
|
||||
</div>
|
||||
<section class="mt-4 mb-3">
|
||||
<h2 class="text-lg font-bold">{{ $t("Details") }}</h2>
|
||||
<h2 class="">{{ $t("Details") }}</h2>
|
||||
<div class="flex flex-col">
|
||||
<div class="overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div class="inline-block py-2 min-w-full sm:px-2 lg:px-8">
|
||||
@@ -62,19 +62,19 @@
|
||||
</div>
|
||||
</section>
|
||||
<section class="mt-4 mb-3">
|
||||
<h2 class="text-lg font-bold">{{ $t("Actions") }}</h2>
|
||||
<h2 class="">{{ $t("Actions") }}</h2>
|
||||
<div class="buttons" v-if="person.domain">
|
||||
<b-button
|
||||
@click="suspendProfile"
|
||||
<o-button
|
||||
@click="suspendProfile({ id })"
|
||||
v-if="person.domain && !person.suspended"
|
||||
type="is-primary"
|
||||
>{{ $t("Suspend") }}</b-button
|
||||
variant="primary"
|
||||
>{{ $t("Suspend") }}</o-button
|
||||
>
|
||||
<b-button
|
||||
@click="unsuspendProfile"
|
||||
<o-button
|
||||
@click="unsuspendProfile({ id })"
|
||||
v-if="person.domain && person.suspended"
|
||||
type="is-primary"
|
||||
>{{ $t("Unsuspend") }}</b-button
|
||||
variant="primary"
|
||||
>{{ $t("Unsuspend") }}</o-button
|
||||
>
|
||||
</div>
|
||||
<p v-else></p>
|
||||
@@ -83,8 +83,8 @@
|
||||
class="p-4 mb-4 text-sm text-blue-700 bg-blue-100 rounded-lg"
|
||||
role="alert"
|
||||
>
|
||||
<i18n
|
||||
path="This profile is located on this instance, so you need to {access_the_corresponding_account} to suspend it."
|
||||
<i18n-t
|
||||
keypath="This profile is located on this instance, so you need to {access_the_corresponding_account} to suspend it."
|
||||
>
|
||||
<template #access_the_corresponding_account>
|
||||
<router-link
|
||||
@@ -96,17 +96,17 @@
|
||||
>{{ $t("access the corresponding account") }}</router-link
|
||||
>
|
||||
</template>
|
||||
</i18n>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</section>
|
||||
<section class="mt-4 mb-3">
|
||||
<h2 class="text-lg font-bold">{{ $t("Organized events") }}</h2>
|
||||
<b-table
|
||||
<h2 class="">{{ $t("Organized events") }}</h2>
|
||||
<o-table
|
||||
:data="person.organizedEvents.elements"
|
||||
:loading="$apollo.queries.person.loading"
|
||||
:loading="loading"
|
||||
paginated
|
||||
backend-pagination
|
||||
:current-page.sync="organizedEventsPage"
|
||||
v-model:current-page="organizedEventsPage"
|
||||
:aria-next-label="$t('Next page')"
|
||||
:aria-previous-label="$t('Previous page')"
|
||||
:aria-page-label="$t('Page')"
|
||||
@@ -115,39 +115,39 @@
|
||||
:per-page="EVENTS_PER_PAGE"
|
||||
@page-change="onOrganizedEventsPageChange"
|
||||
>
|
||||
<b-table-column
|
||||
<o-table-column
|
||||
field="beginsOn"
|
||||
:label="$t('Begins on')"
|
||||
v-slot="props"
|
||||
>
|
||||
{{ props.row.beginsOn | formatDateTimeString }}
|
||||
</b-table-column>
|
||||
<b-table-column field="title" :label="$t('Title')" v-slot="props">
|
||||
{{ formatDateTimeString(props.row.beginsOn) }}
|
||||
</o-table-column>
|
||||
<o-table-column field="title" :label="$t('Title')" v-slot="props">
|
||||
<router-link
|
||||
:to="{ name: RouteName.EVENT, params: { uuid: props.row.uuid } }"
|
||||
>
|
||||
{{ props.row.title }}
|
||||
</router-link>
|
||||
</b-table-column>
|
||||
<template slot="empty">
|
||||
</o-table-column>
|
||||
<template #empty>
|
||||
<empty-content icon="account-group" :inline="true">
|
||||
{{ $t("No organized events listed") }}
|
||||
</empty-content>
|
||||
</template>
|
||||
</b-table>
|
||||
</o-table>
|
||||
</section>
|
||||
<section class="mt-4 mb-3">
|
||||
<h2 class="text-lg font-bold">{{ $t("Participations") }}</h2>
|
||||
<b-table
|
||||
<h2 class="">{{ $t("Participations") }}</h2>
|
||||
<o-table
|
||||
:data="
|
||||
person.participations.elements.map(
|
||||
(participation) => participation.event
|
||||
)
|
||||
"
|
||||
:loading="$apollo.loading"
|
||||
:loading="loading"
|
||||
paginated
|
||||
backend-pagination
|
||||
:current-page.sync="participationsPage"
|
||||
v-model:current-page="participationsPage"
|
||||
:aria-next-label="$t('Next page')"
|
||||
:aria-previous-label="$t('Previous page')"
|
||||
:aria-page-label="$t('Page')"
|
||||
@@ -156,35 +156,35 @@
|
||||
:per-page="EVENTS_PER_PAGE"
|
||||
@page-change="onParticipationsPageChange"
|
||||
>
|
||||
<b-table-column
|
||||
<o-table-column
|
||||
field="beginsOn"
|
||||
:label="$t('Begins on')"
|
||||
v-slot="props"
|
||||
>
|
||||
{{ props.row.beginsOn | formatDateTimeString }}
|
||||
</b-table-column>
|
||||
<b-table-column field="title" :label="$t('Title')" v-slot="props">
|
||||
{{ formatDateTimeString(props.row.beginsOn) }}
|
||||
</o-table-column>
|
||||
<o-table-column field="title" :label="$t('Title')" v-slot="props">
|
||||
<router-link
|
||||
:to="{ name: RouteName.EVENT, params: { uuid: props.row.uuid } }"
|
||||
>
|
||||
{{ props.row.title }}
|
||||
</router-link>
|
||||
</b-table-column>
|
||||
<template slot="empty">
|
||||
</o-table-column>
|
||||
<template #empty>
|
||||
<empty-content icon="account-group" :inline="true">
|
||||
{{ $t("No participations listed") }}
|
||||
</empty-content>
|
||||
</template>
|
||||
</b-table>
|
||||
</o-table>
|
||||
</section>
|
||||
<section class="mt-4 mb-3">
|
||||
<h2 class="text-lg font-bold">{{ $t("Memberships") }}</h2>
|
||||
<b-table
|
||||
<h2 class="">{{ $t("Memberships") }}</h2>
|
||||
<o-table
|
||||
:data="person.memberships.elements"
|
||||
:loading="$apollo.loading"
|
||||
:loading="loading"
|
||||
paginated
|
||||
backend-pagination
|
||||
:current-page.sync="membershipsPage"
|
||||
v-model:current-page="membershipsPage"
|
||||
:aria-next-label="$t('Next page')"
|
||||
:aria-previous-label="$t('Previous page')"
|
||||
:aria-page-label="$t('Page')"
|
||||
@@ -193,30 +193,24 @@
|
||||
:per-page="EVENTS_PER_PAGE"
|
||||
@page-change="onMembershipsPageChange"
|
||||
>
|
||||
<b-table-column
|
||||
<o-table-column
|
||||
field="parent.preferredUsername"
|
||||
:label="$t('Group')"
|
||||
v-slot="props"
|
||||
>
|
||||
<article class="media">
|
||||
<figure
|
||||
class="media-left image is-48x48"
|
||||
v-if="props.row.parent.avatar"
|
||||
>
|
||||
<article class="flex gap-2">
|
||||
<figure class="" v-if="props.row.parent.avatar">
|
||||
<img
|
||||
class="is-rounded"
|
||||
class="rounded-full"
|
||||
:src="props.row.parent.avatar.url"
|
||||
alt=""
|
||||
width="48"
|
||||
height="48"
|
||||
/>
|
||||
</figure>
|
||||
<b-icon
|
||||
class="media-left"
|
||||
v-else
|
||||
size="is-large"
|
||||
icon="account-circle"
|
||||
/>
|
||||
<div class="media-content">
|
||||
<div class="content">
|
||||
<AccountCircle v-else :size="48" />
|
||||
<div class="">
|
||||
<div class="prose dark:prose-invert">
|
||||
<span v-if="props.row.parent.name">{{
|
||||
props.row.parent.name
|
||||
}}</span
|
||||
@@ -227,16 +221,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</b-table-column>
|
||||
<b-table-column field="role" :label="$t('Role')" v-slot="props">
|
||||
</o-table-column>
|
||||
<o-table-column field="role" :label="$t('Role')" v-slot="props">
|
||||
<b-tag
|
||||
type="is-primary"
|
||||
variant="primary"
|
||||
v-if="props.row.role === MemberRole.ADMINISTRATOR"
|
||||
>
|
||||
{{ $t("Administrator") }}
|
||||
</b-tag>
|
||||
<b-tag
|
||||
type="is-primary"
|
||||
variant="primary"
|
||||
v-else-if="props.row.role === MemberRole.MODERATOR"
|
||||
>
|
||||
{{ $t("Moderator") }}
|
||||
@@ -245,301 +239,253 @@
|
||||
{{ $t("Member") }}
|
||||
</b-tag>
|
||||
<b-tag
|
||||
type="is-warning"
|
||||
variant="warning"
|
||||
v-else-if="props.row.role === MemberRole.NOT_APPROVED"
|
||||
>
|
||||
{{ $t("Not approved") }}
|
||||
</b-tag>
|
||||
<b-tag
|
||||
type="is-danger"
|
||||
variant="danger"
|
||||
v-else-if="props.row.role === MemberRole.REJECTED"
|
||||
>
|
||||
{{ $t("Rejected") }}
|
||||
</b-tag>
|
||||
<b-tag
|
||||
type="is-danger"
|
||||
variant="danger"
|
||||
v-else-if="props.row.role === MemberRole.INVITED"
|
||||
>
|
||||
{{ $t("Invited") }}
|
||||
</b-tag>
|
||||
</b-table-column>
|
||||
<b-table-column field="insertedAt" :label="$t('Date')" v-slot="props">
|
||||
</o-table-column>
|
||||
<o-table-column field="insertedAt" :label="$t('Date')" v-slot="props">
|
||||
<span class="has-text-centered">
|
||||
{{ props.row.insertedAt | formatDateString }}<br />{{
|
||||
props.row.insertedAt | formatTimeString
|
||||
{{ formatDateString(props.row.insertedAt) }}<br />{{
|
||||
formatTimeString(props.row.insertedAt)
|
||||
}}
|
||||
</span>
|
||||
</b-table-column>
|
||||
<template slot="empty">
|
||||
</o-table-column>
|
||||
<template #empty>
|
||||
<empty-content icon="account-group" :inline="true">
|
||||
{{ $t("No memberships found") }}
|
||||
</empty-content>
|
||||
</template>
|
||||
</b-table>
|
||||
</o-table>
|
||||
</section>
|
||||
</div>
|
||||
<empty-content v-else-if="!$apollo.loading" icon="account">
|
||||
<empty-content v-else-if="!loading" icon="account">
|
||||
{{ $t("This profile was not found") }}
|
||||
<template #desc>
|
||||
<b-button
|
||||
<o-button
|
||||
type="is-text"
|
||||
tag="router-link"
|
||||
:to="{ name: RouteName.PROFILES }"
|
||||
>{{ $t("Back to profile list") }}</b-button
|
||||
>{{ $t("Back to profile list") }}</o-button
|
||||
>
|
||||
</template>
|
||||
</empty-content>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from "vue-property-decorator";
|
||||
<script lang="ts" setup>
|
||||
import { formatBytes } from "@/utils/datetime";
|
||||
import {
|
||||
GET_PERSON,
|
||||
SUSPEND_PROFILE,
|
||||
UNSUSPEND_PROFILE,
|
||||
} from "../../graphql/actor";
|
||||
import { IPerson } from "../../types/actor";
|
||||
import { displayName, usernameWithDomain } from "../../types/actor/actor.model";
|
||||
import RouteName from "../../router/name";
|
||||
import ActorCard from "../../components/Account/ActorCard.vue";
|
||||
import EmptyContent from "../../components/Utils/EmptyContent.vue";
|
||||
} from "@/graphql/actor";
|
||||
import { IPerson } from "@/types/actor";
|
||||
import { displayName, usernameWithDomain } from "@/types/actor/actor.model";
|
||||
import RouteName from "@/router/name";
|
||||
import ActorCard from "@/components/Account/ActorCard.vue";
|
||||
import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
||||
import { ApolloCache, FetchResult } from "@apollo/client/core";
|
||||
import VueRouter from "vue-router";
|
||||
import { MemberRole } from "@/types/enums";
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
const { isNavigationFailure, NavigationFailureType } = VueRouter;
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { integerTransformer, useRouteQuery } from "vue-use-route-query";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {
|
||||
formatDateString,
|
||||
formatTimeString,
|
||||
formatDateTimeString,
|
||||
} from "@/filters/datetime";
|
||||
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
||||
|
||||
const EVENTS_PER_PAGE = 10;
|
||||
const PARTICIPATIONS_PER_PAGE = 10;
|
||||
const MEMBERSHIPS_PER_PAGE = 10;
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
person: {
|
||||
query: GET_PERSON,
|
||||
fetchPolicy: "cache-and-network",
|
||||
variables() {
|
||||
return {
|
||||
actorId: this.id,
|
||||
organizedEventsPage: this.organizedEventsPage,
|
||||
organizedEventsLimit: EVENTS_PER_PAGE,
|
||||
participationsPage: this.participationsPage,
|
||||
participationLimit: PARTICIPATIONS_PER_PAGE,
|
||||
membershipsPage: this.membershipsPage,
|
||||
membershipsLimit: MEMBERSHIPS_PER_PAGE,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.id;
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
ActorCard,
|
||||
EmptyContent,
|
||||
},
|
||||
metaInfo() {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const { person } = this;
|
||||
return {
|
||||
title: person ? person.name || usernameWithDomain(person) : "",
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class AdminProfile extends Vue {
|
||||
@Prop({ required: true }) id!: string;
|
||||
const props = defineProps<{ id: string }>();
|
||||
|
||||
person!: IPerson;
|
||||
const organizedEventsPage = useRouteQuery(
|
||||
"organizedEventsPage",
|
||||
1,
|
||||
integerTransformer
|
||||
);
|
||||
const participationsPage = useRouteQuery(
|
||||
"participationsPage",
|
||||
1,
|
||||
integerTransformer
|
||||
);
|
||||
const membershipsPage = useRouteQuery("membershipsPage", 1, integerTransformer);
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
const {
|
||||
result: personResult,
|
||||
fetchMore,
|
||||
loading,
|
||||
} = useQuery<{ person: IPerson }>(GET_PERSON, () => ({
|
||||
actorId: props.id,
|
||||
organizedEventsPage: organizedEventsPage.value,
|
||||
organizedEventsLimit: EVENTS_PER_PAGE,
|
||||
participationsPage: participationsPage.value,
|
||||
participationLimit: PARTICIPATIONS_PER_PAGE,
|
||||
membershipsPage: membershipsPage.value,
|
||||
membershipsLimit: MEMBERSHIPS_PER_PAGE,
|
||||
}));
|
||||
|
||||
displayName = displayName;
|
||||
const person = computed(() => personResult.value?.person);
|
||||
|
||||
RouteName = RouteName;
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
EVENTS_PER_PAGE = EVENTS_PER_PAGE;
|
||||
useHead({
|
||||
title: computed(() => displayName(person.value)),
|
||||
});
|
||||
|
||||
PARTICIPATIONS_PER_PAGE = PARTICIPATIONS_PER_PAGE;
|
||||
|
||||
MEMBERSHIPS_PER_PAGE = MEMBERSHIPS_PER_PAGE;
|
||||
|
||||
MemberRole = MemberRole;
|
||||
|
||||
get organizedEventsPage(): number {
|
||||
return parseInt(
|
||||
(this.$route.query.organizedEventsPage as string) || "1",
|
||||
10
|
||||
);
|
||||
}
|
||||
|
||||
set organizedEventsPage(page: number) {
|
||||
this.pushRouter({ organizedEventsPage: page.toString() });
|
||||
}
|
||||
|
||||
get participationsPage(): number {
|
||||
return parseInt(
|
||||
(this.$route.query.participationsPage as string) || "1",
|
||||
10
|
||||
);
|
||||
}
|
||||
|
||||
set participationsPage(page: number) {
|
||||
this.pushRouter({ participationsPage: page.toString() });
|
||||
}
|
||||
|
||||
get membershipsPage(): number {
|
||||
return parseInt((this.$route.query.membershipsPage as string) || "1", 10);
|
||||
}
|
||||
|
||||
set membershipsPage(page: number) {
|
||||
this.pushRouter({ membershipsPage: page.toString() });
|
||||
}
|
||||
|
||||
get metadata(): Array<Record<string, unknown>> {
|
||||
if (!this.person) return [];
|
||||
const res: Record<string, unknown>[] = [
|
||||
const metadata = computed(
|
||||
(): Array<{
|
||||
key: string;
|
||||
value: string;
|
||||
link?: { name: string; params: Record<string, any> };
|
||||
}> => {
|
||||
if (!person.value) return [];
|
||||
const res: {
|
||||
key: string;
|
||||
value: string;
|
||||
link?: { name: string; params: Record<string, any> };
|
||||
}[] = [
|
||||
{
|
||||
key: this.$t("Status") as string,
|
||||
value: this.person.suspended ? this.$t("Suspended") : this.$t("Active"),
|
||||
key: t("Status"),
|
||||
value: person.value.suspended ? t("Suspended") : t("Active"),
|
||||
},
|
||||
{
|
||||
key: this.$t("Domain") as string,
|
||||
value: this.person.domain ? this.person.domain : this.$t("Local"),
|
||||
link: this.person.domain
|
||||
key: t("Domain"),
|
||||
value: person.value.domain ? person.value.domain : t("Local"),
|
||||
link: person.value.domain
|
||||
? {
|
||||
name: RouteName.INSTANCE,
|
||||
params: { domain: this.person.domain },
|
||||
params: { domain: person.value.domain },
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
key: this.$i18n.t("Uploaded media size"),
|
||||
value: formatBytes(this.person.mediaSize),
|
||||
key: t("Uploaded media size"),
|
||||
value: formatBytes(person.value.mediaSize ?? 0),
|
||||
},
|
||||
];
|
||||
if (!this.person.domain && this.person.user) {
|
||||
if (!person.value.domain && person.value.user) {
|
||||
res.push({
|
||||
key: this.$t("User") as string,
|
||||
key: t("User"),
|
||||
link: {
|
||||
name: RouteName.ADMIN_USER_PROFILE,
|
||||
params: { id: this.person.user.id },
|
||||
params: { id: person.value.user.id },
|
||||
},
|
||||
value: this.person.user.email,
|
||||
value: person.value.user.email,
|
||||
});
|
||||
}
|
||||
return res;
|
||||
}
|
||||
);
|
||||
|
||||
async suspendProfile(): Promise<void> {
|
||||
this.$apollo.mutate<{ suspendProfile: { id: string } }>({
|
||||
mutation: SUSPEND_PROFILE,
|
||||
const { mutate: suspendProfile } = useMutation<
|
||||
{
|
||||
suspendProfile: { id: string };
|
||||
},
|
||||
{ id: string }
|
||||
>(SUSPEND_PROFILE, () => ({
|
||||
update: (
|
||||
store: ApolloCache<{ suspendProfile: { id: string } }>,
|
||||
{ data }: FetchResult
|
||||
) => {
|
||||
if (data == null) return;
|
||||
const profileId = props.id;
|
||||
|
||||
const profileData = store.readQuery<{ person: IPerson }>({
|
||||
query: GET_PERSON,
|
||||
variables: {
|
||||
id: this.id,
|
||||
},
|
||||
update: (
|
||||
store: ApolloCache<{ suspendProfile: { id: string } }>,
|
||||
{ data }: FetchResult
|
||||
) => {
|
||||
if (data == null) return;
|
||||
const profileId = this.id;
|
||||
|
||||
const profileData = store.readQuery<{ person: IPerson }>({
|
||||
query: GET_PERSON,
|
||||
variables: {
|
||||
actorId: profileId,
|
||||
organizedEventsPage: 1,
|
||||
organizedEventsLimit: EVENTS_PER_PAGE,
|
||||
participationsPage: 1,
|
||||
participationLimit: PARTICIPATIONS_PER_PAGE,
|
||||
membershipsPage: 1,
|
||||
membershipsLimit: MEMBERSHIPS_PER_PAGE,
|
||||
},
|
||||
});
|
||||
|
||||
if (!profileData) return;
|
||||
const { person } = profileData;
|
||||
store.writeQuery({
|
||||
query: GET_PERSON,
|
||||
variables: {
|
||||
actorId: profileId,
|
||||
},
|
||||
data: {
|
||||
person: {
|
||||
...cloneDeep(person),
|
||||
participations: { total: 0, elements: [] },
|
||||
suspended: true,
|
||||
avatar: null,
|
||||
name: "",
|
||||
summary: "",
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async unsuspendProfile(): Promise<void> {
|
||||
const profileID = this.id;
|
||||
this.$apollo.mutate<{ unsuspendProfile: { id: string } }>({
|
||||
mutation: UNSUSPEND_PROFILE,
|
||||
variables: {
|
||||
id: this.id,
|
||||
},
|
||||
refetchQueries: [
|
||||
{
|
||||
query: GET_PERSON,
|
||||
variables: {
|
||||
actorId: profileID,
|
||||
organizedEventsPage: 1,
|
||||
organizedEventsLimit: EVENTS_PER_PAGE,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async onOrganizedEventsPageChange(): Promise<void> {
|
||||
await this.$apollo.queries.person.fetchMore({
|
||||
variables: {
|
||||
actorId: this.id,
|
||||
organizedEventsPage: this.organizedEventsPage,
|
||||
actorId: profileId,
|
||||
organizedEventsPage: 1,
|
||||
organizedEventsLimit: EVENTS_PER_PAGE,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async onParticipationsPageChange(): Promise<void> {
|
||||
await this.$apollo.queries.person.fetchMore({
|
||||
variables: {
|
||||
actorId: this.id,
|
||||
participationPage: this.participationsPage,
|
||||
participationsPage: 1,
|
||||
participationLimit: PARTICIPATIONS_PER_PAGE,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async onMembershipsPageChange(): Promise<void> {
|
||||
await this.$apollo.queries.person.fetchMore({
|
||||
variables: {
|
||||
actorId: this.id,
|
||||
membershipsPage: this.participationsPage,
|
||||
membershipsPage: 1,
|
||||
membershipsLimit: MEMBERSHIPS_PER_PAGE,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async pushRouter(args: Record<string, string>): Promise<void> {
|
||||
try {
|
||||
await this.$router.push({
|
||||
name: RouteName.ADMIN_PROFILE,
|
||||
query: { ...this.$route.query, ...args },
|
||||
});
|
||||
} catch (e) {
|
||||
if (isNavigationFailure(e, NavigationFailureType.redirected)) {
|
||||
throw Error(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!profileData) return;
|
||||
const { person } = profileData;
|
||||
store.writeQuery({
|
||||
query: GET_PERSON,
|
||||
variables: {
|
||||
actorId: profileId,
|
||||
},
|
||||
data: {
|
||||
person: {
|
||||
...cloneDeep(person),
|
||||
participations: { total: 0, elements: [] },
|
||||
suspended: true,
|
||||
avatar: null,
|
||||
name: "",
|
||||
summary: "",
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
const { mutate: unsuspendProfile } = useMutation<
|
||||
{ unsuspendProfile: { id: string } },
|
||||
{ id: string }
|
||||
>(UNSUSPEND_PROFILE, () => ({
|
||||
refetchQueries: [
|
||||
{
|
||||
query: GET_PERSON,
|
||||
variables: {
|
||||
actorId: props.id,
|
||||
organizedEventsPage: 1,
|
||||
organizedEventsLimit: EVENTS_PER_PAGE,
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
const onOrganizedEventsPageChange = async (): Promise<void> => {
|
||||
await fetchMore({
|
||||
variables: {
|
||||
actorId: props.id,
|
||||
organizedEventsPage: organizedEventsPage.value,
|
||||
organizedEventsLimit: EVENTS_PER_PAGE,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onParticipationsPageChange = async (): Promise<void> => {
|
||||
await fetchMore({
|
||||
variables: {
|
||||
actorId: props.id,
|
||||
participationPage: participationsPage.value,
|
||||
participationLimit: PARTICIPATIONS_PER_PAGE,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onMembershipsPageChange = async (): Promise<void> => {
|
||||
await fetchMore({
|
||||
variables: {
|
||||
actorId: props.id,
|
||||
membershipsPage: participationsPage.value,
|
||||
membershipsLimit: MEMBERSHIPS_PER_PAGE,
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
<div v-if="user" class="section">
|
||||
<breadcrumbs-nav
|
||||
:links="[
|
||||
{ name: RouteName.ADMIN, text: $t('Admin') },
|
||||
{ name: RouteName.ADMIN, text: t('Admin') },
|
||||
{
|
||||
name: RouteName.USERS,
|
||||
text: $t('Users'),
|
||||
text: t('Users'),
|
||||
},
|
||||
{
|
||||
name: RouteName.ADMIN_USER_PROFILE,
|
||||
@@ -16,7 +16,7 @@
|
||||
/>
|
||||
|
||||
<section>
|
||||
<h2 class="text-lg font-bold mb-3">{{ $t("Details") }}</h2>
|
||||
<h2 class="text-lg font-bold mb-3">{{ t("Details") }}</h2>
|
||||
<div class="flex flex-col">
|
||||
<div class="overflow-x-auto sm:-mx-6">
|
||||
<div class="inline-block py-2 min-w-full sm:px-2">
|
||||
@@ -25,19 +25,15 @@
|
||||
<tbody>
|
||||
<tr
|
||||
class="odd:bg-white even:bg-gray-50 border-b"
|
||||
v-for="{ key, value, link, type } in metadata"
|
||||
v-for="{ key, value, type } in metadata"
|
||||
:key="key"
|
||||
>
|
||||
<td class="py-4 px-2 whitespace-nowrap align-middle">
|
||||
{{ key }}
|
||||
</td>
|
||||
<td v-if="link" class="py-4 px-2 whitespace-nowrap">
|
||||
<router-link :to="link">
|
||||
{{ value }}
|
||||
</router-link>
|
||||
</td>
|
||||
|
||||
<td
|
||||
v-else-if="type === 'ip'"
|
||||
v-if="type === 'ip'"
|
||||
class="py-4 px-2 whitespace-nowrap"
|
||||
>
|
||||
<code>{{ value }}</code>
|
||||
@@ -65,72 +61,72 @@
|
||||
</td>
|
||||
<td
|
||||
v-if="type === 'email'"
|
||||
class="py-4 px-2 whitespace-nowrap flex flex flex-col items-start"
|
||||
class="py-4 px-2 whitespace-nowrap flex flex flex-col items-start gap-2"
|
||||
>
|
||||
<b-button
|
||||
size="is-small"
|
||||
<o-button
|
||||
size="small"
|
||||
v-if="!user.disabled"
|
||||
@click="isEmailChangeModalActive = true"
|
||||
type="is-text"
|
||||
icon-left="pencil"
|
||||
>{{ $t("Change email") }}</b-button
|
||||
>{{ t("Change email") }}</o-button
|
||||
>
|
||||
<b-button
|
||||
<o-button
|
||||
tag="router-link"
|
||||
:to="{
|
||||
name: RouteName.USERS,
|
||||
query: { emailFilter: `@${userEmailDomain}` },
|
||||
}"
|
||||
size="is-small"
|
||||
size="small"
|
||||
type="is-text"
|
||||
icon-left="magnify"
|
||||
>{{
|
||||
$t("Other users with the same email domain")
|
||||
}}</b-button
|
||||
t("Other users with the same email domain")
|
||||
}}</o-button
|
||||
>
|
||||
</td>
|
||||
<td
|
||||
v-else-if="type === 'confirmed'"
|
||||
class="py-4 px-2 whitespace-nowrap flex items-center"
|
||||
>
|
||||
<b-button
|
||||
size="is-small"
|
||||
<o-button
|
||||
size="small"
|
||||
v-if="!user.confirmedAt || user.disabled"
|
||||
@click="isConfirmationModalActive = true"
|
||||
type="is-text"
|
||||
icon-left="check"
|
||||
>{{ $t("Confirm user") }}</b-button
|
||||
>{{ t("Confirm user") }}</o-button
|
||||
>
|
||||
</td>
|
||||
<td
|
||||
v-else-if="type === 'role'"
|
||||
class="py-4 px-2 whitespace-nowrap flex items-center"
|
||||
>
|
||||
<b-button
|
||||
size="is-small"
|
||||
<o-button
|
||||
size="small"
|
||||
v-if="!user.disabled"
|
||||
@click="isRoleChangeModalActive = true"
|
||||
type="is-text"
|
||||
icon-left="chevron-double-up"
|
||||
>{{ $t("Change role") }}</b-button
|
||||
>{{ t("Change role") }}</o-button
|
||||
>
|
||||
</td>
|
||||
<td
|
||||
v-else-if="type === 'ip' && user.currentSignInIp"
|
||||
class="py-4 px-2 whitespace-nowrap flex items-center"
|
||||
>
|
||||
<b-button
|
||||
<o-button
|
||||
tag="router-link"
|
||||
:to="{
|
||||
name: RouteName.USERS,
|
||||
query: { ipFilter: user.currentSignInIp },
|
||||
}"
|
||||
size="is-small"
|
||||
size="small"
|
||||
type="is-text"
|
||||
icon-left="web"
|
||||
>{{
|
||||
$t("Other users with the same IP address")
|
||||
}}</b-button
|
||||
t("Other users with the same IP address")
|
||||
}}</o-button
|
||||
>
|
||||
</td>
|
||||
<td v-else></td>
|
||||
@@ -143,10 +139,10 @@
|
||||
</div>
|
||||
</section>
|
||||
<section class="my-4">
|
||||
<h2 class="text-lg font-bold mb-3">{{ $t("Profiles") }}</h2>
|
||||
<h2 class="text-lg font-bold mb-3">{{ t("Profiles") }}</h2>
|
||||
<div
|
||||
class="flex flex-wrap justify-center sm:justify-start gap-4"
|
||||
v-if="profiles.length > 0"
|
||||
v-if="profiles && profiles.length > 0"
|
||||
>
|
||||
<router-link
|
||||
v-for="profile in profiles"
|
||||
@@ -161,422 +157,370 @@
|
||||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
<empty-content v-else-if="!$apollo.loading" :inline="true" icon="account">
|
||||
{{ $t("This user doesn't have any profiles") }}
|
||||
<empty-content v-else-if="!loadingUser" :inline="true" icon="account">
|
||||
{{ t("This user doesn't have any profiles") }}
|
||||
</empty-content>
|
||||
</section>
|
||||
<section class="my-4">
|
||||
<h2 class="text-lg font-bold mb-3">{{ $t("Actions") }}</h2>
|
||||
<h2 class="text-lg font-bold mb-3">{{ t("Actions") }}</h2>
|
||||
<div class="buttons" v-if="!user.disabled">
|
||||
<b-button @click="suspendAccount" type="is-danger">{{
|
||||
$t("Suspend")
|
||||
}}</b-button>
|
||||
<o-button @click="suspendAccount" variant="danger">{{
|
||||
t("Suspend")
|
||||
}}</o-button>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="p-4 mb-4 text-sm text-red-700 bg-red-100 rounded-lg"
|
||||
role="alert"
|
||||
>
|
||||
{{ $t("The user has been disabled") }}
|
||||
{{ t("The user has been disabled") }}
|
||||
</div>
|
||||
</section>
|
||||
<b-modal
|
||||
:active="isEmailChangeModalActive"
|
||||
<o-modal
|
||||
v-model:active="isEmailChangeModalActive"
|
||||
trap-focus
|
||||
:destroy-on-hide="false"
|
||||
aria-role="dialog"
|
||||
:aria-label="t('Edit user email')"
|
||||
:close-button-aria-label="t('Close')"
|
||||
aria-modal
|
||||
>
|
||||
<form @submit.prevent="updateUserEmail">
|
||||
<div class="" style="width: auto">
|
||||
<header class="">
|
||||
<h2>{{ t("Change user email") }}</h2>
|
||||
</header>
|
||||
<section class="">
|
||||
<o-field :label="t('Previous email')">
|
||||
<o-input type="email" :value="user.email" disabled> </o-input>
|
||||
</o-field>
|
||||
<o-field :label="t('New email')">
|
||||
<o-input
|
||||
type="email"
|
||||
v-model="newUser.email"
|
||||
:placeholder="t(`new{'@'}email.com`)"
|
||||
required
|
||||
>
|
||||
</o-input>
|
||||
</o-field>
|
||||
<o-checkbox v-model="newUser.notify">{{
|
||||
t("Notify the user of the change")
|
||||
}}</o-checkbox>
|
||||
</section>
|
||||
<footer class="mt-2 flex gap-2">
|
||||
<o-button @click="isEmailChangeModalActive = false">{{
|
||||
t("Close")
|
||||
}}</o-button>
|
||||
<o-button native-type="submit" variant="primary">{{
|
||||
t("Change email")
|
||||
}}</o-button>
|
||||
</footer>
|
||||
</div>
|
||||
</form>
|
||||
</o-modal>
|
||||
<o-modal
|
||||
v-model:active="isRoleChangeModalActive"
|
||||
has-modal-card
|
||||
trap-focus
|
||||
:destroy-on-hide="false"
|
||||
aria-role="dialog"
|
||||
:aria-label="$t('Edit user email')"
|
||||
:close-button-aria-label="$t('Close')"
|
||||
:aria-label="t('Edit user email')"
|
||||
:close-button-aria-label="t('Close')"
|
||||
aria-modal
|
||||
>
|
||||
<template>
|
||||
<form @submit.prevent="updateUserEmail">
|
||||
<div class="modal-card" style="width: auto">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">{{ $t("Change user email") }}</p>
|
||||
<button
|
||||
type="button"
|
||||
class="delete"
|
||||
@click="isEmailChangeModalActive = false"
|
||||
/>
|
||||
</header>
|
||||
<section class="modal-card-body">
|
||||
<b-field :label="$t('Previous email')">
|
||||
<b-input type="email" :value="user.email" disabled> </b-input>
|
||||
</b-field>
|
||||
<b-field :label="$t('New email')">
|
||||
<b-input
|
||||
type="email"
|
||||
v-model="newUser.email"
|
||||
:placeholder="$t('new@email.com')"
|
||||
required
|
||||
>
|
||||
</b-input>
|
||||
</b-field>
|
||||
<b-checkbox v-model="newUser.notify">{{
|
||||
$t("Notify the user of the change")
|
||||
}}</b-checkbox>
|
||||
</section>
|
||||
<footer class="modal-card-foot">
|
||||
<b-button @click="isEmailChangeModalActive = false">{{
|
||||
$t("Close")
|
||||
}}</b-button>
|
||||
<b-button native-type="submit" type="is-primary">{{
|
||||
$t("Change email")
|
||||
}}</b-button>
|
||||
</footer>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
</b-modal>
|
||||
<b-modal
|
||||
:active="isRoleChangeModalActive"
|
||||
<form @submit.prevent="updateUserRole">
|
||||
<div>
|
||||
<header>
|
||||
<h2 class="modal-card-title">{{ t("Change user role") }}</h2>
|
||||
</header>
|
||||
<section>
|
||||
<o-field>
|
||||
<o-radio
|
||||
v-model="newUser.role"
|
||||
:native-value="ICurrentUserRole.ADMINISTRATOR"
|
||||
>
|
||||
{{ t("Administrator") }}
|
||||
</o-radio>
|
||||
</o-field>
|
||||
<o-field>
|
||||
<o-radio
|
||||
v-model="newUser.role"
|
||||
:native-value="ICurrentUserRole.MODERATOR"
|
||||
>
|
||||
{{ t("Moderator") }}
|
||||
</o-radio>
|
||||
</o-field>
|
||||
<o-field>
|
||||
<o-radio
|
||||
v-model="newUser.role"
|
||||
:native-value="ICurrentUserRole.USER"
|
||||
>
|
||||
{{ t("User") }}
|
||||
</o-radio>
|
||||
</o-field>
|
||||
<o-checkbox v-model="newUser.notify">{{
|
||||
t("Notify the user of the change")
|
||||
}}</o-checkbox>
|
||||
</section>
|
||||
<footer class="mt-2 flex gap-2">
|
||||
<o-button @click="isRoleChangeModalActive = false">{{
|
||||
t("Close")
|
||||
}}</o-button>
|
||||
<o-button native-type="submit" variant="primary">{{
|
||||
t("Change role")
|
||||
}}</o-button>
|
||||
</footer>
|
||||
</div>
|
||||
</form>
|
||||
</o-modal>
|
||||
<o-modal
|
||||
v-model:active="isConfirmationModalActive"
|
||||
has-modal-card
|
||||
trap-focus
|
||||
:destroy-on-hide="false"
|
||||
aria-role="dialog"
|
||||
:aria-label="$t('Edit user email')"
|
||||
:close-button-aria-label="$t('Close')"
|
||||
:aria-label="t('Edit user email')"
|
||||
:close-button-aria-label="t('Close')"
|
||||
aria-modal
|
||||
>
|
||||
<template>
|
||||
<form @submit.prevent="updateUserRole">
|
||||
<div class="modal-card" style="width: auto">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">{{ $t("Change user role") }}</p>
|
||||
<button
|
||||
type="button"
|
||||
class="delete"
|
||||
@click="isRoleChangeModalActive = false"
|
||||
/>
|
||||
</header>
|
||||
<section class="modal-card-body">
|
||||
<b-field>
|
||||
<b-radio
|
||||
v-model="newUser.role"
|
||||
:native-value="ICurrentUserRole.ADMINISTRATOR"
|
||||
>
|
||||
{{ $t("Administrator") }}
|
||||
</b-radio>
|
||||
</b-field>
|
||||
<b-field>
|
||||
<b-radio
|
||||
v-model="newUser.role"
|
||||
:native-value="ICurrentUserRole.MODERATOR"
|
||||
>
|
||||
{{ $t("Moderator") }}
|
||||
</b-radio>
|
||||
</b-field>
|
||||
<b-field>
|
||||
<b-radio
|
||||
v-model="newUser.role"
|
||||
:native-value="ICurrentUserRole.USER"
|
||||
>
|
||||
{{ $t("User") }}
|
||||
</b-radio>
|
||||
</b-field>
|
||||
<b-checkbox v-model="newUser.notify">{{
|
||||
$t("Notify the user of the change")
|
||||
}}</b-checkbox>
|
||||
</section>
|
||||
<footer class="modal-card-foot">
|
||||
<b-button @click="isRoleChangeModalActive = false">{{
|
||||
$t("Close")
|
||||
}}</b-button>
|
||||
<b-button native-type="submit" type="is-primary">{{
|
||||
$t("Change role")
|
||||
}}</b-button>
|
||||
</footer>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
</b-modal>
|
||||
<b-modal
|
||||
:active="isConfirmationModalActive"
|
||||
has-modal-card
|
||||
trap-focus
|
||||
:destroy-on-hide="false"
|
||||
aria-role="dialog"
|
||||
:aria-label="$t('Edit user email')"
|
||||
:close-button-aria-label="$t('Close')"
|
||||
aria-modal
|
||||
>
|
||||
<template>
|
||||
<form @submit.prevent="confirmUser">
|
||||
<div class="modal-card" style="width: auto">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">{{ $t("Confirm user") }}</p>
|
||||
<button
|
||||
type="button"
|
||||
class="delete"
|
||||
@click="isConfirmationModalActive = false"
|
||||
/>
|
||||
</header>
|
||||
<section class="modal-card-body">
|
||||
<b-checkbox v-model="newUser.notify">{{
|
||||
$t("Notify the user of the change")
|
||||
}}</b-checkbox>
|
||||
</section>
|
||||
<footer class="modal-card-foot">
|
||||
<b-button @click="isConfirmationModalActive = false">{{
|
||||
$t("Close")
|
||||
}}</b-button>
|
||||
<b-button native-type="submit" type="is-primary">{{
|
||||
$t("Confirm user")
|
||||
}}</b-button>
|
||||
</footer>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
</b-modal>
|
||||
<form @submit.prevent="confirmUser">
|
||||
<div>
|
||||
<header>
|
||||
<h2>{{ t("Confirm user") }}</h2>
|
||||
</header>
|
||||
<section>
|
||||
<o-checkbox v-model="newUser.notify">{{
|
||||
t("Notify the user of the change")
|
||||
}}</o-checkbox>
|
||||
</section>
|
||||
<footer>
|
||||
<o-button @click="isConfirmationModalActive = false">{{
|
||||
t("Close")
|
||||
}}</o-button>
|
||||
<o-button native-type="submit" variant="primary">{{
|
||||
t("Confirm user")
|
||||
}}</o-button>
|
||||
</footer>
|
||||
</div>
|
||||
</form>
|
||||
</o-modal>
|
||||
</div>
|
||||
<empty-content v-else-if="!$apollo.loading" icon="account">
|
||||
{{ $t("This user was not found") }}
|
||||
<empty-content v-else-if="!loadingUser" icon="account">
|
||||
{{ t("This user was not found") }}
|
||||
<template #desc>
|
||||
<b-button
|
||||
<o-button
|
||||
type="is-text"
|
||||
tag="router-link"
|
||||
:to="{ name: RouteName.USERS }"
|
||||
>{{ $t("Back to user list") }}</b-button
|
||||
>{{ t("Back to user list") }}</o-button
|
||||
>
|
||||
</template>
|
||||
</empty-content>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop, Watch } from "vue-property-decorator";
|
||||
<script lang="ts" setup>
|
||||
import { formatBytes } from "@/utils/datetime";
|
||||
import { ICurrentUserRole } from "@/types/enums";
|
||||
import { GET_USER, SUSPEND_USER } from "../../graphql/user";
|
||||
import { IActor, usernameWithDomain } from "../../types/actor/actor.model";
|
||||
import RouteName from "../../router/name";
|
||||
import { IUser } from "../../types/current-user.model";
|
||||
import EmptyContent from "../../components/Utils/EmptyContent.vue";
|
||||
import ActorCard from "../../components/Account/ActorCard.vue";
|
||||
import { ADMIN_UPDATE_USER, LANGUAGES_CODES } from "@/graphql/admin";
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { ILanguage } from "@/types/admin.model";
|
||||
import { computed, inject, reactive, ref, watch } from "vue";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { formatDateTimeString } from "@/filters/datetime";
|
||||
import { useRouter } from "vue-router";
|
||||
import { IPerson } from "@/types/actor";
|
||||
import { Dialog } from "@/plugins/dialog";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
user: {
|
||||
query: GET_USER,
|
||||
fetchPolicy: "cache-and-network",
|
||||
variables() {
|
||||
return {
|
||||
id: this.id,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.id;
|
||||
},
|
||||
},
|
||||
languages: {
|
||||
query: LANGUAGES_CODES,
|
||||
variables() {
|
||||
return {
|
||||
codes: [this.languageCode],
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.languageCode;
|
||||
},
|
||||
},
|
||||
},
|
||||
metaInfo() {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const { user } = this;
|
||||
return {
|
||||
title: user?.email,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
EmptyContent,
|
||||
ActorCard,
|
||||
},
|
||||
})
|
||||
export default class AdminUserProfile extends Vue {
|
||||
@Prop({ required: true }) id!: string;
|
||||
const props = defineProps<{ id: string }>();
|
||||
|
||||
user!: IUser;
|
||||
const { result: userResult, loading: loadingUser } = useQuery<{ user: IUser }>(
|
||||
GET_USER,
|
||||
() => ({
|
||||
id: props.id,
|
||||
})
|
||||
);
|
||||
|
||||
languages!: Array<{ code: string; name: string }>;
|
||||
const user = computed(() => userResult.value?.user);
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
const languageCode = computed(() => user.value?.locale);
|
||||
|
||||
RouteName = RouteName;
|
||||
const { result: languagesResult } = useQuery<{ languages: ILanguage[] }>(
|
||||
LANGUAGES_CODES,
|
||||
() => ({
|
||||
codes: languageCode.value,
|
||||
}),
|
||||
() => ({
|
||||
enabled: languageCode.value !== undefined,
|
||||
})
|
||||
);
|
||||
|
||||
ICurrentUserRole = ICurrentUserRole;
|
||||
const languages = computed(() => languagesResult.value?.languages);
|
||||
|
||||
isEmailChangeModalActive = false;
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
isRoleChangeModalActive = false;
|
||||
useHead({
|
||||
title: computed(() => user.value?.email ?? ""),
|
||||
});
|
||||
|
||||
isConfirmationModalActive = false;
|
||||
const isEmailChangeModalActive = ref(false);
|
||||
const isRoleChangeModalActive = ref(false);
|
||||
const isConfirmationModalActive = ref(false);
|
||||
|
||||
newUser = {
|
||||
email: "",
|
||||
role: this?.user?.role,
|
||||
confirm: false,
|
||||
notify: true,
|
||||
};
|
||||
const newUser = reactive({
|
||||
email: "",
|
||||
role: user.value?.role,
|
||||
confirm: false,
|
||||
notify: true,
|
||||
});
|
||||
|
||||
get metadata(): Array<Record<string, unknown>> {
|
||||
if (!this.user) return [];
|
||||
const metadata = computed(
|
||||
(): Array<{ key: string; value: string; type?: string }> => {
|
||||
if (!user.value) return [];
|
||||
return [
|
||||
{
|
||||
key: this.$i18n.t("Email"),
|
||||
value: this.user.email,
|
||||
key: t("Email"),
|
||||
value: user.value.email,
|
||||
type: "email",
|
||||
},
|
||||
{
|
||||
key: this.$i18n.t("Language"),
|
||||
value: this.languages
|
||||
? this.languages[0].name
|
||||
: this.$i18n.t("Unknown"),
|
||||
key: t("Language"),
|
||||
value: languages.value ? languages.value[0].name : t("Unknown"),
|
||||
},
|
||||
{
|
||||
key: this.$i18n.t("Role"),
|
||||
value: this.roleName(this.user.role),
|
||||
key: t("Role"),
|
||||
value: roleName(user.value.role),
|
||||
type: "role",
|
||||
},
|
||||
{
|
||||
key: this.$i18n.t("Login status"),
|
||||
value: this.user.disabled
|
||||
? this.$i18n.t("Disabled")
|
||||
: this.$t("Activated"),
|
||||
key: t("Login status"),
|
||||
value: user.value.disabled ? t("Disabled") : t("Activated"),
|
||||
},
|
||||
{
|
||||
key: this.$i18n.t("Confirmed"),
|
||||
value:
|
||||
this.$options.filters && this.user.confirmedAt
|
||||
? this.$options.filters.formatDateTimeString(this.user.confirmedAt)
|
||||
: this.$i18n.t("Not confirmed"),
|
||||
key: t("Confirmed"),
|
||||
value: user.value.confirmedAt
|
||||
? formatDateTimeString(user.value.confirmedAt)
|
||||
: t("Not confirmed"),
|
||||
type: "confirmed",
|
||||
},
|
||||
{
|
||||
key: this.$i18n.t("Last sign-in"),
|
||||
value:
|
||||
this.$options.filters && this.user.currentSignInAt
|
||||
? this.$options.filters.formatDateTimeString(
|
||||
this.user.currentSignInAt
|
||||
)
|
||||
: this.$t("Unknown"),
|
||||
key: t("Last sign-in"),
|
||||
value: user.value.currentSignInAt
|
||||
? formatDateTimeString(user.value.currentSignInAt)
|
||||
: t("Unknown"),
|
||||
},
|
||||
{
|
||||
key: this.$i18n.t("Last IP adress"),
|
||||
value: this.user.currentSignInIp || this.$t("Unknown"),
|
||||
type: this.user.currentSignInIp ? "ip" : undefined,
|
||||
key: t("Last IP adress"),
|
||||
value: user.value.currentSignInIp || t("Unknown"),
|
||||
type: user.value.currentSignInIp ? "ip" : undefined,
|
||||
},
|
||||
{
|
||||
key: this.$i18n.t("Total number of participations"),
|
||||
value: this.user.participations.total,
|
||||
key: t("Total number of participations"),
|
||||
value: user.value.participations.total.toString(),
|
||||
},
|
||||
{
|
||||
key: this.$i18n.t("Uploaded media total size"),
|
||||
value: formatBytes(
|
||||
this.user.mediaSize,
|
||||
2,
|
||||
this.$i18n.t("0 Bytes") as string
|
||||
),
|
||||
key: t("Uploaded media total size"),
|
||||
value: formatBytes(user.value.mediaSize, 2, t("0 Bytes")),
|
||||
},
|
||||
];
|
||||
}
|
||||
);
|
||||
|
||||
roleName(role: ICurrentUserRole): string {
|
||||
switch (role) {
|
||||
case ICurrentUserRole.ADMINISTRATOR:
|
||||
return this.$t("Administrator") as string;
|
||||
case ICurrentUserRole.MODERATOR:
|
||||
return this.$t("Moderator") as string;
|
||||
case ICurrentUserRole.USER:
|
||||
default:
|
||||
return this.$t("User") as string;
|
||||
}
|
||||
const roleName = (role: ICurrentUserRole): string => {
|
||||
switch (role) {
|
||||
case ICurrentUserRole.ADMINISTRATOR:
|
||||
return t("Administrator");
|
||||
case ICurrentUserRole.MODERATOR:
|
||||
return t("Moderator");
|
||||
case ICurrentUserRole.USER:
|
||||
default:
|
||||
return t("User");
|
||||
}
|
||||
};
|
||||
|
||||
async suspendAccount(): Promise<void> {
|
||||
this.$buefy.dialog.confirm({
|
||||
title: this.$t("Suspend the account?") as string,
|
||||
message: this.$t(
|
||||
"Do you really want to suspend this account? All of the user's profiles will be deleted."
|
||||
) as string,
|
||||
confirmText: this.$t("Suspend the account") as string,
|
||||
cancelText: this.$t("Cancel") as string,
|
||||
type: "is-danger",
|
||||
onConfirm: async () => {
|
||||
await this.$apollo.mutate<{ suspendProfile: { id: string } }>({
|
||||
mutation: SUSPEND_USER,
|
||||
variables: {
|
||||
userId: this.id,
|
||||
},
|
||||
});
|
||||
return this.$router.push({ name: RouteName.USERS });
|
||||
},
|
||||
});
|
||||
}
|
||||
const router = useRouter();
|
||||
|
||||
get profiles(): IActor[] {
|
||||
return this.user.actors;
|
||||
}
|
||||
const { mutate: suspendUser } = useMutation<
|
||||
{ suspendProfile: { id: string } },
|
||||
{ userId: string }
|
||||
>(SUSPEND_USER);
|
||||
|
||||
get languageCode(): string | undefined {
|
||||
return this.user?.locale;
|
||||
}
|
||||
const dialog = inject<Dialog>("dialog");
|
||||
|
||||
async confirmUser() {
|
||||
this.isConfirmationModalActive = false;
|
||||
await this.updateUser({
|
||||
confirmed: true,
|
||||
notify: this.newUser.notify,
|
||||
});
|
||||
}
|
||||
const suspendAccount = async (): Promise<void> => {
|
||||
dialog?.confirm({
|
||||
title: t("Suspend the account?"),
|
||||
message: t(
|
||||
"Do you really want to suspend this account? All of the user's profiles will be deleted."
|
||||
),
|
||||
confirmText: t("Suspend the account"),
|
||||
cancelText: t("Cancel"),
|
||||
type: "is-danger",
|
||||
onConfirm: async () => {
|
||||
suspendUser({
|
||||
userId: props.id,
|
||||
});
|
||||
return router.push({ name: RouteName.USERS });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
async updateUserRole() {
|
||||
this.isRoleChangeModalActive = false;
|
||||
await this.updateUser({
|
||||
role: this.newUser.role,
|
||||
notify: this.newUser.notify,
|
||||
});
|
||||
}
|
||||
const profiles = computed((): IPerson[] | undefined => {
|
||||
return user.value?.actors;
|
||||
});
|
||||
|
||||
async updateUserEmail() {
|
||||
this.isEmailChangeModalActive = false;
|
||||
await this.updateUser({
|
||||
email: this.newUser.email,
|
||||
notify: this.newUser.notify,
|
||||
});
|
||||
}
|
||||
const confirmUser = async () => {
|
||||
isConfirmationModalActive.value = false;
|
||||
await updateUser({
|
||||
id: props.id,
|
||||
confirmed: true,
|
||||
notify: newUser.notify,
|
||||
});
|
||||
};
|
||||
|
||||
async updateUser(properties: {
|
||||
const updateUserRole = async () => {
|
||||
isRoleChangeModalActive.value = false;
|
||||
await updateUser({
|
||||
id: props.id,
|
||||
role: newUser.role,
|
||||
notify: newUser.notify,
|
||||
});
|
||||
};
|
||||
|
||||
const updateUserEmail = async () => {
|
||||
isEmailChangeModalActive.value = false;
|
||||
await updateUser({
|
||||
id: props.id,
|
||||
email: newUser.email,
|
||||
notify: newUser.notify,
|
||||
});
|
||||
};
|
||||
|
||||
const { mutate: updateUser } = useMutation<
|
||||
{ adminUpdateUser: IUser },
|
||||
{
|
||||
id: string;
|
||||
email?: string;
|
||||
notify: boolean;
|
||||
confirmed?: boolean;
|
||||
role?: ICurrentUserRole;
|
||||
}) {
|
||||
await this.$apollo.mutate<{ adminUpdateUser: IUser }>({
|
||||
mutation: ADMIN_UPDATE_USER,
|
||||
variables: {
|
||||
id: this.id,
|
||||
...properties,
|
||||
},
|
||||
});
|
||||
}
|
||||
>(ADMIN_UPDATE_USER);
|
||||
|
||||
@Watch("user")
|
||||
resetCurrentUserRole(
|
||||
updatedUser: IUser | undefined,
|
||||
oldUser: IUser | undefined
|
||||
) {
|
||||
if (updatedUser?.role !== oldUser?.role) {
|
||||
this.newUser.role = updatedUser?.role;
|
||||
}
|
||||
watch(user, (updatedUser: IUser | undefined, oldUser: IUser | undefined) => {
|
||||
if (updatedUser?.role !== oldUser?.role) {
|
||||
newUser.role = updatedUser?.role;
|
||||
}
|
||||
});
|
||||
|
||||
get userEmailDomain(): string | undefined {
|
||||
if (this?.user?.email) {
|
||||
return this?.user?.email.split("@")[1];
|
||||
}
|
||||
return undefined;
|
||||
const userEmailDomain = computed((): string | undefined => {
|
||||
if (user.value?.email) {
|
||||
return user.value?.email.split("@")[1];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -7,25 +7,26 @@
|
||||
]"
|
||||
/>
|
||||
<section>
|
||||
<h1 class="title">{{ $t("Administration") }}</h1>
|
||||
<h1>{{ $t("Administration") }}</h1>
|
||||
<div class="tile is-ancestor" v-if="dashboard">
|
||||
<div class="tile is-vertical">
|
||||
<div class="tile">
|
||||
<div class="tile is-parent is-vertical is-6">
|
||||
<article class="tile is-child box">
|
||||
<p class="dashboard-number">{{ dashboard.numberOfEvents }}</p>
|
||||
<p
|
||||
v-html="
|
||||
$t(
|
||||
'Published events with <b>{comments}</b> comments and <b>{participations}</b> confirmed participations',
|
||||
{
|
||||
comments: dashboard.numberOfComments,
|
||||
participations:
|
||||
dashboard.numberOfConfirmedParticipationsToLocalEvents,
|
||||
}
|
||||
)
|
||||
"
|
||||
/>
|
||||
<i18n-t
|
||||
keypath="Published events with {comments} comments and {participations} confirmed participations"
|
||||
tag="p"
|
||||
>
|
||||
<template #comments>
|
||||
<b>{{ dashboard.numberOfComments }}</b>
|
||||
</template>
|
||||
<template #participations>
|
||||
<b>{{
|
||||
dashboard.numberOfConfirmedParticipationsToLocalEvents
|
||||
}}</b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</article>
|
||||
<article class="tile is-child box">
|
||||
<router-link :to="{ name: RouteName.ADMIN_GROUPS }">
|
||||
@@ -133,38 +134,31 @@
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
<script lang="ts" setup>
|
||||
import { DASHBOARD } from "@/graphql/admin";
|
||||
import { IDashboard } from "@/types/admin.model";
|
||||
import { usernameWithDomain } from "@/types/actor";
|
||||
import RouteName from "../../router/name";
|
||||
import RouteName from "@/router/name";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@vueuse/head";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
dashboard: {
|
||||
query: DASHBOARD,
|
||||
fetchPolicy: "cache-and-network",
|
||||
},
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t("Administration") as string,
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class Dashboard extends Vue {
|
||||
dashboard!: IDashboard;
|
||||
const { result: dashboardResult } = useQuery<{ dashboard: IDashboard }>(
|
||||
DASHBOARD
|
||||
);
|
||||
|
||||
RouteName = RouteName;
|
||||
const dashboard = computed(() => dashboardResult.value?.dashboard);
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
}
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
useHead({
|
||||
title: computed(() => t("Administration")),
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dashboard-number {
|
||||
color: #3c376e;
|
||||
font-size: 40px;
|
||||
font-weight: 700;
|
||||
line-height: 1.125;
|
||||
@@ -172,7 +166,6 @@ export default class Dashboard extends Vue {
|
||||
|
||||
.tile a,
|
||||
article.tile a {
|
||||
color: #4a4a4a;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,16 +17,18 @@
|
||||
>
|
||||
</div>
|
||||
<div v-if="groups">
|
||||
<b-switch v-model="local">{{ $t("Local") }}</b-switch>
|
||||
<b-switch v-model="suspended">{{ $t("Suspended") }}</b-switch>
|
||||
<b-table
|
||||
<div class="flex gap-2">
|
||||
<o-switch v-model="local">{{ $t("Local") }}</o-switch>
|
||||
<o-switch v-model="suspended">{{ $t("Suspended") }}</o-switch>
|
||||
</div>
|
||||
<o-table
|
||||
:data="groups.elements"
|
||||
:loading="$apollo.queries.groups.loading"
|
||||
:loading="loading"
|
||||
paginated
|
||||
backend-pagination
|
||||
backend-filtering
|
||||
:debounce-search="200"
|
||||
:current-page.sync="page"
|
||||
v-model:current-page="page"
|
||||
:aria-next-label="$t('Next page')"
|
||||
:aria-previous-label="$t('Previous page')"
|
||||
:aria-page-label="$t('Page')"
|
||||
@@ -36,13 +38,13 @@
|
||||
@page-change="onPageChange"
|
||||
@filters-change="onFiltersChange"
|
||||
>
|
||||
<b-table-column
|
||||
<o-table-column
|
||||
field="preferredUsername"
|
||||
:label="$t('Username')"
|
||||
searchable
|
||||
>
|
||||
<template #searchable="props">
|
||||
<b-input
|
||||
<o-input
|
||||
:aria-label="$t('Filter')"
|
||||
v-model="props.filters.preferredUsername"
|
||||
:placeholder="$t('Filter')"
|
||||
@@ -57,17 +59,19 @@
|
||||
params: { id: props.row.id },
|
||||
}"
|
||||
>
|
||||
<article class="media">
|
||||
<figure class="media-left" v-if="props.row.avatar">
|
||||
<p class="image is-48x48">
|
||||
<img
|
||||
:src="props.row.avatar.url"
|
||||
:alt="props.row.avatar.alt || ''"
|
||||
/>
|
||||
</p>
|
||||
<article class="flex gap-1">
|
||||
<figure class="" v-if="props.row.avatar">
|
||||
<img
|
||||
:src="props.row.avatar.url"
|
||||
:alt="props.row.avatar.alt || ''"
|
||||
width="48"
|
||||
height="48"
|
||||
class="rounded-full"
|
||||
/>
|
||||
</figure>
|
||||
<div class="media-content">
|
||||
<div class="content">
|
||||
<AccountGroup v-else :size="48" />
|
||||
<div class="">
|
||||
<div class="prose dark:prose-invert">
|
||||
<strong v-if="props.row.name">{{ props.row.name }}</strong
|
||||
><br v-if="props.row.name" />
|
||||
<small>@{{ props.row.preferredUsername }}</small>
|
||||
@@ -76,11 +80,11 @@
|
||||
</article>
|
||||
</router-link>
|
||||
</template>
|
||||
</b-table-column>
|
||||
</o-table-column>
|
||||
|
||||
<b-table-column field="domain" :label="$t('Domain')" searchable>
|
||||
<o-table-column field="domain" :label="$t('Domain')" searchable>
|
||||
<template #searchable="props">
|
||||
<b-input
|
||||
<o-input
|
||||
:aria-label="$t('Filter')"
|
||||
v-model="props.filters.domain"
|
||||
:placeholder="$t('Filter')"
|
||||
@@ -90,150 +94,100 @@
|
||||
<template v-slot:default="props">
|
||||
{{ props.row.domain }}
|
||||
</template>
|
||||
</b-table-column>
|
||||
<template slot="empty">
|
||||
</o-table-column>
|
||||
<template #empty>
|
||||
<empty-content icon="account-group" :inline="true">
|
||||
{{ $t("No group matches the filters") }}
|
||||
</empty-content>
|
||||
</template>
|
||||
</b-table>
|
||||
</o-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import { CONFIG } from "@/graphql/config";
|
||||
import { IConfig } from "@/types/config.model";
|
||||
<script lang="ts" setup>
|
||||
import { LIST_GROUPS } from "@/graphql/group";
|
||||
import RouteName from "../../router/name";
|
||||
import EmptyContent from "../../components/Utils/EmptyContent.vue";
|
||||
import VueRouter from "vue-router";
|
||||
const { isNavigationFailure, NavigationFailureType } = VueRouter;
|
||||
import { useRestrictions } from "@/composition/apollo/config";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import {
|
||||
booleanTransformer,
|
||||
integerTransformer,
|
||||
useRouteQuery,
|
||||
} from "vue-use-route-query";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import { computed } from "vue";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
import { IGroup } from "@/types/actor";
|
||||
import AccountGroup from "vue-material-design-icons/AccountGroup.vue";
|
||||
|
||||
const PROFILES_PER_PAGE = 10;
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
config: CONFIG,
|
||||
groups: {
|
||||
query: LIST_GROUPS,
|
||||
variables() {
|
||||
return {
|
||||
preferredUsername: this.preferredUsername,
|
||||
name: this.name,
|
||||
domain: this.domain,
|
||||
local: this.local,
|
||||
suspended: this.suspended,
|
||||
page: this.page,
|
||||
limit: PROFILES_PER_PAGE,
|
||||
};
|
||||
},
|
||||
const { restrictions } = useRestrictions();
|
||||
|
||||
const preferredUsername = useRouteQuery("preferredUsername", "");
|
||||
const name = useRouteQuery("name", "");
|
||||
const domain = useRouteQuery("domain", "");
|
||||
const local = useRouteQuery("local", false, booleanTransformer);
|
||||
const suspended = useRouteQuery("suspended", false, booleanTransformer);
|
||||
const page = useRouteQuery("page", 1, integerTransformer);
|
||||
|
||||
const {
|
||||
result: groupsResult,
|
||||
fetchMore,
|
||||
loading,
|
||||
} = useQuery<{
|
||||
groups: Paginate<IGroup>;
|
||||
}>(LIST_GROUPS, () => ({
|
||||
preferredUsername: preferredUsername.value,
|
||||
name: name.value,
|
||||
domain: domain.value,
|
||||
local: local.value,
|
||||
suspended: suspended.value,
|
||||
page: page.value,
|
||||
limit: PROFILES_PER_PAGE,
|
||||
}));
|
||||
|
||||
const groups = computed(() => groupsResult.value?.groups);
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
useHead({ title: computed(() => t("Groups")) });
|
||||
|
||||
const onPageChange = async (): Promise<void> => {
|
||||
await doFetchMore();
|
||||
};
|
||||
|
||||
const showCreateGroupsButton = computed((): boolean => {
|
||||
return !!restrictions.value?.onlyAdminCanCreateGroups;
|
||||
});
|
||||
|
||||
const onFiltersChange = ({
|
||||
preferredUsername: newPreferredUsername,
|
||||
domain: newDomain,
|
||||
}: {
|
||||
preferredUsername: string;
|
||||
domain: string;
|
||||
}): void => {
|
||||
preferredUsername.value = newPreferredUsername;
|
||||
domain.value = newDomain;
|
||||
doFetchMore();
|
||||
};
|
||||
|
||||
const doFetchMore = async (): Promise<void> => {
|
||||
await fetchMore({
|
||||
variables: {
|
||||
preferredUsername: preferredUsername.value,
|
||||
name: name.value,
|
||||
domain: domain.value,
|
||||
local: local.value,
|
||||
suspended: suspended.value,
|
||||
page: page.value,
|
||||
limit: PROFILES_PER_PAGE,
|
||||
},
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t("Groups") as string,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
EmptyContent,
|
||||
},
|
||||
})
|
||||
export default class GroupProfiles extends Vue {
|
||||
name = "";
|
||||
|
||||
PROFILES_PER_PAGE = PROFILES_PER_PAGE;
|
||||
|
||||
config!: IConfig;
|
||||
RouteName = RouteName;
|
||||
|
||||
async onPageChange(): Promise<void> {
|
||||
await this.doFetchMore();
|
||||
}
|
||||
|
||||
get page(): number {
|
||||
return parseInt((this.$route.query.page as string) || "1", 10);
|
||||
}
|
||||
|
||||
set page(page: number) {
|
||||
this.pushRouter({ page: page.toString() });
|
||||
}
|
||||
|
||||
get domain(): string {
|
||||
return (this.$route.query.domain as string) || "";
|
||||
}
|
||||
|
||||
set domain(domain: string) {
|
||||
this.pushRouter({ domain });
|
||||
}
|
||||
|
||||
get preferredUsername(): string {
|
||||
return (this.$route.query.preferredUsername as string) || "";
|
||||
}
|
||||
|
||||
set preferredUsername(preferredUsername: string) {
|
||||
this.pushRouter({ preferredUsername });
|
||||
}
|
||||
|
||||
get local(): boolean {
|
||||
return this.$route.query.local === "1";
|
||||
}
|
||||
|
||||
set local(local: boolean) {
|
||||
this.pushRouter({ local: local ? "1" : "0" });
|
||||
}
|
||||
|
||||
get suspended(): boolean {
|
||||
return this.$route.query.suspended === "1";
|
||||
}
|
||||
|
||||
set suspended(suspended: boolean) {
|
||||
this.pushRouter({ suspended: suspended ? "1" : "0" });
|
||||
}
|
||||
|
||||
get showCreateGroupsButton(): boolean {
|
||||
return !!this.config?.restrictions?.onlyAdminCanCreateGroups;
|
||||
}
|
||||
|
||||
onFiltersChange({
|
||||
preferredUsername,
|
||||
domain,
|
||||
}: {
|
||||
preferredUsername: string;
|
||||
domain: string;
|
||||
}): void {
|
||||
this.preferredUsername = preferredUsername;
|
||||
this.domain = domain;
|
||||
this.doFetchMore();
|
||||
}
|
||||
|
||||
private async pushRouter(args: Record<string, string>): Promise<void> {
|
||||
try {
|
||||
await this.$router.push({
|
||||
name: RouteName.ADMIN_GROUPS,
|
||||
query: { ...this.$route.query, ...args },
|
||||
});
|
||||
} catch (e) {
|
||||
if (isNavigationFailure(e, NavigationFailureType.redirected)) {
|
||||
throw Error(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async doFetchMore(): Promise<void> {
|
||||
await this.$apollo.queries.groups.fetchMore({
|
||||
variables: {
|
||||
preferredUsername: this.preferredUsername,
|
||||
name: this.name,
|
||||
domain: this.domain,
|
||||
local: this.local,
|
||||
suspended: this.suspended,
|
||||
page: this.page,
|
||||
limit: PROFILES_PER_PAGE,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
a.profile {
|
||||
|
||||
@@ -72,14 +72,22 @@
|
||||
v-if="instance.hasRelay"
|
||||
>
|
||||
<button
|
||||
@click="removeInstanceFollow"
|
||||
@click="
|
||||
removeInstanceFollow({
|
||||
address: instance?.relayAddress,
|
||||
})
|
||||
"
|
||||
v-if="instance.followedStatus == InstanceFollowStatus.APPROVED"
|
||||
class="bg-primary hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
|
||||
>
|
||||
{{ $t("Stop following instance") }}
|
||||
</button>
|
||||
<button
|
||||
@click="removeInstanceFollow"
|
||||
@click="
|
||||
removeInstanceFollow({
|
||||
address: instance?.relayAddress,
|
||||
})
|
||||
"
|
||||
v-else-if="instance.followedStatus == InstanceFollowStatus.PENDING"
|
||||
class="bg-primary hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
|
||||
>
|
||||
@@ -98,14 +106,22 @@
|
||||
</div>
|
||||
<div class="border bg-white p-6 shadow-md rounded-md flex flex-col gap-2">
|
||||
<button
|
||||
@click="acceptInstance"
|
||||
@click="
|
||||
acceptInstance({
|
||||
address: instance?.relayAddress,
|
||||
})
|
||||
"
|
||||
v-if="instance.followerStatus == InstanceFollowStatus.PENDING"
|
||||
class="bg-green-700 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
|
||||
>
|
||||
{{ $t("Accept follow") }}
|
||||
</button>
|
||||
<button
|
||||
@click="rejectInstance"
|
||||
@click="
|
||||
rejectInstance({
|
||||
address: instance?.relayAddress,
|
||||
})
|
||||
"
|
||||
v-if="instance.followerStatus != InstanceFollowStatus.NONE"
|
||||
class="bg-red-700 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
|
||||
>
|
||||
@@ -118,151 +134,123 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ACCEPT_RELAY,
|
||||
ADD_INSTANCE,
|
||||
INSTANCE,
|
||||
REJECT_RELAY,
|
||||
REMOVE_RELAY,
|
||||
} from "@/graphql/admin";
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { formatBytes } from "@/utils/datetime";
|
||||
import RouteName from "@/router/name";
|
||||
import { IInstance } from "@/types/instance.model";
|
||||
import { ApolloCache, gql, Reference } from "@apollo/client/core";
|
||||
import { InstanceFollowStatus } from "@/types/enums";
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { computed, inject } from "vue";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
instance: {
|
||||
query: INSTANCE,
|
||||
variables() {
|
||||
return {
|
||||
domain: this.domain,
|
||||
};
|
||||
},
|
||||
const props = defineProps<{ domain: string }>();
|
||||
|
||||
const { result: instanceResult } = useQuery<{ instance: IInstance }>(
|
||||
INSTANCE,
|
||||
() => ({ domain: props.domain })
|
||||
);
|
||||
|
||||
const instance = computed(() => instanceResult.value?.instance);
|
||||
|
||||
const notifier = inject<Notifier>("notifier");
|
||||
|
||||
const { mutate: acceptInstance, onError: onAcceptInstanceError } = useMutation(
|
||||
ACCEPT_RELAY,
|
||||
() => ({
|
||||
update(cache: ApolloCache<any>) {
|
||||
cache.writeFragment({
|
||||
id: cache.identify(instance as unknown as Reference),
|
||||
fragment: gql`
|
||||
fragment InstanceFollowerStatus on Instance {
|
||||
followerStatus
|
||||
}
|
||||
`,
|
||||
data: {
|
||||
followerStatus: InstanceFollowStatus.APPROVED,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class Instance extends Vue {
|
||||
@Prop({ type: String, required: true }) domain!: string;
|
||||
})
|
||||
);
|
||||
|
||||
instance!: IInstance;
|
||||
onAcceptInstanceError((error) => {
|
||||
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
||||
notifier?.error(error.graphQLErrors[0].message);
|
||||
}
|
||||
});
|
||||
|
||||
InstanceFollowStatus = InstanceFollowStatus;
|
||||
|
||||
formatBytes = formatBytes;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
async acceptInstance(): Promise<void> {
|
||||
try {
|
||||
const { instance } = this;
|
||||
await this.$apollo.mutate({
|
||||
mutation: ACCEPT_RELAY,
|
||||
variables: {
|
||||
address: this.instance.relayAddress,
|
||||
},
|
||||
update(cache: ApolloCache<any>) {
|
||||
cache.writeFragment({
|
||||
id: cache.identify(instance as unknown as Reference),
|
||||
fragment: gql`
|
||||
fragment InstanceFollowerStatus on Instance {
|
||||
followerStatus
|
||||
}
|
||||
`,
|
||||
data: {
|
||||
followerStatus: InstanceFollowStatus.APPROVED,
|
||||
},
|
||||
});
|
||||
/**
|
||||
* Reject instance follow
|
||||
*/
|
||||
const { mutate: rejectInstance, onError: onRejectInstanceError } = useMutation(
|
||||
REJECT_RELAY,
|
||||
() => ({
|
||||
update(cache: ApolloCache<any>) {
|
||||
cache.writeFragment({
|
||||
id: cache.identify(instance as unknown as Reference),
|
||||
fragment: gql`
|
||||
fragment InstanceFollowerStatus on Instance {
|
||||
followerStatus
|
||||
}
|
||||
`,
|
||||
data: {
|
||||
followerStatus: InstanceFollowStatus.NONE,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
||||
this.$notifier.error(error.graphQLErrors[0].message);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Reject instance follow
|
||||
*/
|
||||
async rejectInstance(): Promise<void> {
|
||||
try {
|
||||
const { instance } = this;
|
||||
await this.$apollo.mutate({
|
||||
mutation: REJECT_RELAY,
|
||||
variables: {
|
||||
address: this.instance.relayAddress,
|
||||
},
|
||||
update(cache: ApolloCache<any>) {
|
||||
cache.writeFragment({
|
||||
id: cache.identify(instance as unknown as Reference),
|
||||
fragment: gql`
|
||||
fragment InstanceFollowerStatus on Instance {
|
||||
followerStatus
|
||||
}
|
||||
`,
|
||||
data: {
|
||||
followerStatus: InstanceFollowStatus.NONE,
|
||||
},
|
||||
});
|
||||
onRejectInstanceError((error) => {
|
||||
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
||||
notifier?.error(error.graphQLErrors[0].message);
|
||||
}
|
||||
});
|
||||
|
||||
const { mutate: followInstanceMutation, onError: onFollowInstanceError } =
|
||||
useMutation(ADD_INSTANCE);
|
||||
|
||||
onFollowInstanceError((error) => {
|
||||
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
||||
notifier?.error(error.graphQLErrors[0].message);
|
||||
}
|
||||
});
|
||||
|
||||
const followInstance = async (e: Event): Promise<void> => {
|
||||
e.preventDefault();
|
||||
followInstanceMutation({ domain: props.domain });
|
||||
};
|
||||
|
||||
/**
|
||||
* Stop following instance
|
||||
*/
|
||||
const { mutate: removeInstanceFollow, onError: onRemoveInstanceFollowError } =
|
||||
useMutation(REJECT_RELAY, () => ({
|
||||
update(cache: ApolloCache<any>) {
|
||||
cache.writeFragment({
|
||||
id: cache.identify(instance as unknown as Reference),
|
||||
fragment: gql`
|
||||
fragment InstanceFollowedStatus on Instance {
|
||||
followedStatus
|
||||
}
|
||||
`,
|
||||
data: {
|
||||
followedStatus: InstanceFollowStatus.NONE,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
||||
this.$notifier.error(error.graphQLErrors[0].message);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
async followInstance(e: Event): Promise<void> {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await this.$apollo.mutate<{ addInstance: Instance }>({
|
||||
mutation: ADD_INSTANCE,
|
||||
variables: {
|
||||
domain: this.domain,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
||||
this.$notifier.error(error.graphQLErrors[0].message);
|
||||
}
|
||||
}
|
||||
onRemoveInstanceFollowError((error) => {
|
||||
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
||||
notifier?.error(error.graphQLErrors[0].message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop following instance
|
||||
*/
|
||||
async removeInstanceFollow(): Promise<void> {
|
||||
const { instance } = this;
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: REMOVE_RELAY,
|
||||
variables: {
|
||||
address: this.instance.relayAddress,
|
||||
},
|
||||
update(cache: ApolloCache<any>) {
|
||||
cache.writeFragment({
|
||||
id: cache.identify(instance as unknown as Reference),
|
||||
fragment: gql`
|
||||
fragment InstanceFollowedStatus on Instance {
|
||||
followedStatus
|
||||
}
|
||||
`,
|
||||
data: {
|
||||
followedStatus: InstanceFollowStatus.NONE,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
||||
this.$notifier.error(error.graphQLErrors[0].message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -9,57 +9,57 @@
|
||||
<section>
|
||||
<h1 class="title">{{ $t("Instances") }}</h1>
|
||||
<form @submit="followInstance" class="my-4">
|
||||
<b-field :label="$t('Follow a new instance')" horizontal>
|
||||
<b-field grouped group-multiline expanded size="is-large">
|
||||
<o-field :label="$t('Follow a new instance')" horizontal>
|
||||
<o-field grouped group-multiline expanded size="large">
|
||||
<p class="control">
|
||||
<b-input
|
||||
<o-input
|
||||
v-model="newRelayAddress"
|
||||
:placeholder="$t('Ex: mobilizon.fr')"
|
||||
/>
|
||||
</p>
|
||||
<p class="control">
|
||||
<b-button type="is-primary" native-type="submit">{{
|
||||
<o-button variant="primary" native-type="submit">{{
|
||||
$t("Add an instance")
|
||||
}}</b-button>
|
||||
<b-loading
|
||||
}}</o-button>
|
||||
<o-loading
|
||||
:is-full-page="true"
|
||||
v-model="followInstanceLoading"
|
||||
:can-cancel="false"
|
||||
/>
|
||||
</p>
|
||||
</b-field>
|
||||
</b-field>
|
||||
</o-field>
|
||||
</o-field>
|
||||
</form>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<b-field :label="$t('Follow status')">
|
||||
<b-radio-button
|
||||
<o-field :label="$t('Follow status')">
|
||||
<o-radio
|
||||
v-model="followStatus"
|
||||
:native-value="InstanceFilterFollowStatus.ALL"
|
||||
>{{ $t("All") }}</b-radio-button
|
||||
>{{ $t("All") }}</o-radio
|
||||
>
|
||||
<b-radio-button
|
||||
<o-radio
|
||||
v-model="followStatus"
|
||||
:native-value="InstanceFilterFollowStatus.FOLLOWING"
|
||||
>{{ $t("Following") }}</b-radio-button
|
||||
>{{ $t("Following") }}</o-radio
|
||||
>
|
||||
<b-radio-button
|
||||
<o-radio
|
||||
v-model="followStatus"
|
||||
:native-value="InstanceFilterFollowStatus.FOLLOWED"
|
||||
>{{ $t("Followed") }}</b-radio-button
|
||||
>{{ $t("Followed") }}</o-radio
|
||||
>
|
||||
</b-field>
|
||||
<b-field
|
||||
</o-field>
|
||||
<o-field
|
||||
:label="$t('Domain')"
|
||||
label-for="domain-filter"
|
||||
class="flex-auto"
|
||||
>
|
||||
<b-input
|
||||
<o-input
|
||||
id="domain-filter"
|
||||
:placeholder="$t('mobilizon-instance.tld')"
|
||||
:value="filterDomain"
|
||||
@input="debouncedUpdateDomainFilter"
|
||||
/>
|
||||
</b-field>
|
||||
</o-field>
|
||||
</div>
|
||||
<div v-if="instances && instances.elements.length > 0" class="mt-3">
|
||||
<router-link
|
||||
@@ -78,7 +78,7 @@
|
||||
src="../../assets/logo.svg"
|
||||
alt=""
|
||||
/>
|
||||
<b-icon
|
||||
<o-icon
|
||||
class="is-large"
|
||||
v-else
|
||||
custom-size="mdi-36px"
|
||||
@@ -90,7 +90,7 @@
|
||||
class="text-sm"
|
||||
v-if="instance.followedStatus === InstanceFollowStatus.APPROVED"
|
||||
>
|
||||
<b-icon icon="inbox-arrow-down" />
|
||||
<o-icon icon="inbox-arrow-down" />
|
||||
{{ $t("Followed") }}</span
|
||||
>
|
||||
<span
|
||||
@@ -99,21 +99,21 @@
|
||||
instance.followedStatus === InstanceFollowStatus.PENDING
|
||||
"
|
||||
>
|
||||
<b-icon icon="inbox-arrow-down" />
|
||||
<o-icon icon="inbox-arrow-down" />
|
||||
{{ $t("Followed, pending response") }}</span
|
||||
>
|
||||
<span
|
||||
class="text-sm"
|
||||
v-if="instance.followerStatus == InstanceFollowStatus.APPROVED"
|
||||
>
|
||||
<b-icon icon="inbox-arrow-up" />
|
||||
<o-icon icon="inbox-arrow-up" />
|
||||
{{ $t("Follows us") }}</span
|
||||
>
|
||||
<span
|
||||
class="text-sm"
|
||||
v-if="instance.followerStatus == InstanceFollowStatus.PENDING"
|
||||
>
|
||||
<b-icon icon="inbox-arrow-up" />
|
||||
<o-icon icon="inbox-arrow-up" />
|
||||
{{ $t("Follows us, pending approval") }}</span
|
||||
>
|
||||
</div>
|
||||
@@ -129,7 +129,7 @@
|
||||
</p>
|
||||
</div>
|
||||
</router-link>
|
||||
<b-pagination
|
||||
<o-pagination
|
||||
v-show="instances.total > INSTANCES_PAGE_LIMIT"
|
||||
:total="instances.total"
|
||||
v-model="instancePage"
|
||||
@@ -139,7 +139,7 @@
|
||||
:aria-page-label="$t('Page')"
|
||||
:aria-current-label="$t('Current page')"
|
||||
>
|
||||
</b-pagination>
|
||||
</o-pagination>
|
||||
</div>
|
||||
<div v-else-if="instances && instances.elements.length == 0">
|
||||
<empty-content icon="lan-disconnect" :inline="true">
|
||||
@@ -162,145 +162,107 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
<script lang="ts" setup>
|
||||
import { ADD_INSTANCE, INSTANCES } from "@/graphql/admin";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
import { IFollower } from "@/types/actor/follower.model";
|
||||
import RouteName from "../../router/name";
|
||||
import { IInstance } from "@/types/instance.model";
|
||||
import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
||||
import VueRouter from "vue-router";
|
||||
import { debounce } from "lodash";
|
||||
import debounce from "lodash/debounce";
|
||||
import {
|
||||
InstanceFilterFollowStatus,
|
||||
InstanceFollowStatus,
|
||||
} from "@/types/enums";
|
||||
const { isNavigationFailure, NavigationFailureType } = VueRouter;
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {
|
||||
enumTransformer,
|
||||
integerTransformer,
|
||||
useRouteQuery,
|
||||
} from "vue-use-route-query";
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { computed, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useHead } from "@vueuse/head";
|
||||
|
||||
const INSTANCES_PAGE_LIMIT = 10;
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
instances: {
|
||||
query: INSTANCES,
|
||||
fetchPolicy: "cache-and-network",
|
||||
variables() {
|
||||
return {
|
||||
page: this.instancePage,
|
||||
limit: INSTANCES_PAGE_LIMIT,
|
||||
filterDomain: this.filterDomain,
|
||||
filterFollowStatus: this.followStatus,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t("Federation") as string,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
EmptyContent,
|
||||
},
|
||||
})
|
||||
export default class Follows extends Vue {
|
||||
RouteName = RouteName;
|
||||
const instancePage = useRouteQuery("page", 1, integerTransformer);
|
||||
const filterDomain = useRouteQuery("filterDomain", "");
|
||||
const followStatus = useRouteQuery(
|
||||
"followStatus",
|
||||
InstanceFilterFollowStatus.ALL,
|
||||
enumTransformer(InstanceFilterFollowStatus)
|
||||
);
|
||||
|
||||
followInstanceLoading = false;
|
||||
const { result: instancesResult } = useQuery<{
|
||||
instances: Paginate<IInstance>;
|
||||
}>(INSTANCES, () => ({
|
||||
page: instancePage.value,
|
||||
limit: INSTANCES_PAGE_LIMIT,
|
||||
filterDomain: filterDomain.value,
|
||||
filterFollowStatus: followStatus.value,
|
||||
}));
|
||||
|
||||
newRelayAddress = "";
|
||||
const instances = computed(() => instancesResult.value?.instances);
|
||||
|
||||
instances!: Paginate<IInstance>;
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
useHead({
|
||||
title: computed(() => t("Federation")),
|
||||
});
|
||||
|
||||
instancePage = 1;
|
||||
const followInstanceLoading = ref(false);
|
||||
|
||||
relayFollowings: Paginate<IFollower> = { elements: [], total: 0 };
|
||||
const newRelayAddress = ref("");
|
||||
|
||||
relayFollowers: Paginate<IFollower> = { elements: [], total: 0 };
|
||||
// relayFollowings: Paginate<IFollower> = { elements: [], total: 0 };
|
||||
|
||||
InstanceFilterFollowStatus = InstanceFilterFollowStatus;
|
||||
// relayFollowers: Paginate<IFollower> = { elements: [], total: 0 };
|
||||
|
||||
InstanceFollowStatus = InstanceFollowStatus;
|
||||
const updateDomainFilter = (domain: string) => {
|
||||
filterDomain.value = domain;
|
||||
};
|
||||
|
||||
INSTANCES_PAGE_LIMIT = INSTANCES_PAGE_LIMIT;
|
||||
const debouncedUpdateDomainFilter = debounce(updateDomainFilter, 500);
|
||||
|
||||
data(): Record<string, unknown> {
|
||||
return {
|
||||
debouncedUpdateDomainFilter: debounce(this.updateDomainFilter, 500),
|
||||
};
|
||||
}
|
||||
const hasFilter = computed((): boolean => {
|
||||
return (
|
||||
followStatus.value !== InstanceFilterFollowStatus.ALL ||
|
||||
filterDomain.value !== ""
|
||||
);
|
||||
});
|
||||
|
||||
updateDomainFilter(domain: string) {
|
||||
this.filterDomain = domain;
|
||||
}
|
||||
const router = useRouter();
|
||||
|
||||
get filterDomain(): string {
|
||||
return (this.$route.query.domain as string) || "";
|
||||
}
|
||||
const { mutate, onDone, onError } = useMutation<{
|
||||
addInstance: IInstance;
|
||||
}>(ADD_INSTANCE);
|
||||
|
||||
set filterDomain(domain: string) {
|
||||
this.pushRouter({ domain });
|
||||
}
|
||||
onDone(({ data }) => {
|
||||
newRelayAddress.value = "";
|
||||
followInstanceLoading.value = false;
|
||||
router.push({
|
||||
name: RouteName.INSTANCE,
|
||||
params: { domain: data?.addInstance.domain },
|
||||
});
|
||||
});
|
||||
|
||||
get followStatus(): InstanceFilterFollowStatus {
|
||||
return (
|
||||
(this.$route.query.followStatus as InstanceFilterFollowStatus) ||
|
||||
InstanceFilterFollowStatus.ALL
|
||||
);
|
||||
}
|
||||
|
||||
set followStatus(followStatus: InstanceFilterFollowStatus) {
|
||||
this.pushRouter({ followStatus });
|
||||
}
|
||||
|
||||
get hasFilter(): boolean {
|
||||
return (
|
||||
this.followStatus !== InstanceFilterFollowStatus.ALL ||
|
||||
this.filterDomain !== ""
|
||||
);
|
||||
}
|
||||
|
||||
async followInstance(e: Event): Promise<void> {
|
||||
e.preventDefault();
|
||||
this.followInstanceLoading = true;
|
||||
const domain = this.newRelayAddress.trim(); // trim to fix copy and paste domain name spaces and tabs
|
||||
try {
|
||||
await this.$apollo.mutate<{ relayFollowings: Paginate<IFollower> }>({
|
||||
mutation: ADD_INSTANCE,
|
||||
variables: {
|
||||
domain,
|
||||
},
|
||||
});
|
||||
this.newRelayAddress = "";
|
||||
this.followInstanceLoading = false;
|
||||
this.$router.push({
|
||||
name: RouteName.INSTANCE,
|
||||
params: { domain },
|
||||
});
|
||||
} catch (error: any) {
|
||||
if (error.message) {
|
||||
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
||||
this.$notifier.error(error.graphQLErrors[0].message);
|
||||
}
|
||||
}
|
||||
this.followInstanceLoading = false;
|
||||
onError((error) => {
|
||||
if (error.message) {
|
||||
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
||||
notifier?.error(error.graphQLErrors[0].message);
|
||||
}
|
||||
}
|
||||
followInstanceLoading.value = false;
|
||||
});
|
||||
|
||||
private async pushRouter(args: Record<string, string>): Promise<void> {
|
||||
try {
|
||||
await this.$router.push({
|
||||
name: RouteName.INSTANCES,
|
||||
query: { ...this.$route.query, ...args },
|
||||
});
|
||||
} catch (e) {
|
||||
if (isNavigationFailure(e, NavigationFailureType.redirected)) {
|
||||
throw Error(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const followInstance = async (e: Event): Promise<void> => {
|
||||
e.preventDefault();
|
||||
followInstanceLoading.value = true;
|
||||
const domain = newRelayAddress.value.trim(); // trim to fix copy and paste domain name spaces and tabs
|
||||
mutate({
|
||||
domain,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.tab-item {
|
||||
|
||||
@@ -10,16 +10,16 @@
|
||||
]"
|
||||
/>
|
||||
<div v-if="persons">
|
||||
<b-switch v-model="local">{{ $t("Local") }}</b-switch>
|
||||
<b-switch v-model="suspended">{{ $t("Suspended") }}</b-switch>
|
||||
<b-table
|
||||
<o-switch v-model="local">{{ $t("Local") }}</o-switch>
|
||||
<o-switch v-model="suspended">{{ $t("Suspended") }}</o-switch>
|
||||
<o-table
|
||||
:data="persons.elements"
|
||||
:loading="$apollo.queries.persons.loading"
|
||||
:loading="loading"
|
||||
paginated
|
||||
backend-pagination
|
||||
backend-filtering
|
||||
:debounce-search="200"
|
||||
:current-page.sync="page"
|
||||
v-model:current-page="page"
|
||||
:aria-next-label="$t('Next page')"
|
||||
:aria-previous-label="$t('Previous page')"
|
||||
:aria-page-label="$t('Page')"
|
||||
@@ -29,13 +29,13 @@
|
||||
@page-change="onPageChange"
|
||||
@filters-change="onFiltersChange"
|
||||
>
|
||||
<b-table-column
|
||||
<o-table-column
|
||||
field="preferredUsername"
|
||||
:label="$t('Username')"
|
||||
searchable
|
||||
>
|
||||
<template #searchable="props">
|
||||
<b-input
|
||||
<o-input
|
||||
v-model="props.filters.preferredUsername"
|
||||
:aria-label="$t('Filter')"
|
||||
:placeholder="$t('Filter')"
|
||||
@@ -50,17 +50,18 @@
|
||||
params: { id: props.row.id },
|
||||
}"
|
||||
>
|
||||
<article class="media">
|
||||
<figure class="media-left" v-if="props.row.avatar">
|
||||
<p class="image is-48x48">
|
||||
<img
|
||||
:src="props.row.avatar.url"
|
||||
:alt="props.row.avatar.alt || ''"
|
||||
/>
|
||||
</p>
|
||||
<article class="flex gap-2">
|
||||
<figure class="" v-if="props.row.avatar">
|
||||
<img
|
||||
:src="props.row.avatar.url"
|
||||
:alt="props.row.avatar.alt || ''"
|
||||
width="48"
|
||||
height="48"
|
||||
/>
|
||||
</figure>
|
||||
<div class="media-content">
|
||||
<div class="content">
|
||||
<Account v-else :size="48" />
|
||||
<div class="">
|
||||
<div class="prose dark:prose-invert">
|
||||
<strong v-if="props.row.name">{{ props.row.name }}</strong
|
||||
><br v-if="props.row.name" />
|
||||
<small>@{{ props.row.preferredUsername }}</small>
|
||||
@@ -69,11 +70,11 @@
|
||||
</article>
|
||||
</router-link>
|
||||
</template>
|
||||
</b-table-column>
|
||||
</o-table-column>
|
||||
|
||||
<b-table-column field="domain" :label="$t('Domain')" searchable>
|
||||
<o-table-column field="domain" :label="$t('Domain')" searchable>
|
||||
<template #searchable="props">
|
||||
<b-input
|
||||
<o-input
|
||||
v-model="props.filters.domain"
|
||||
:aria-label="$t('Filter')"
|
||||
:placeholder="$t('Filter')"
|
||||
@@ -83,140 +84,79 @@
|
||||
<template v-slot:default="props">
|
||||
{{ props.row.domain }}
|
||||
</template>
|
||||
</b-table-column>
|
||||
<template slot="empty">
|
||||
</o-table-column>
|
||||
<template #empty>
|
||||
<empty-content icon="account" :inline="true">
|
||||
{{ $t("No profile matches the filters") }}
|
||||
</empty-content>
|
||||
</template>
|
||||
</b-table>
|
||||
</o-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import { LIST_PROFILES } from "../../graphql/actor";
|
||||
import RouteName from "../../router/name";
|
||||
import EmptyContent from "../../components/Utils/EmptyContent.vue";
|
||||
import VueRouter from "vue-router";
|
||||
const { isNavigationFailure, NavigationFailureType } = VueRouter;
|
||||
<script lang="ts" setup>
|
||||
import { LIST_PROFILES } from "@/graphql/actor";
|
||||
import RouteName from "@/router/name";
|
||||
import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { computed } from "vue";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import {
|
||||
useRouteQuery,
|
||||
booleanTransformer,
|
||||
integerTransformer,
|
||||
} from "vue-use-route-query";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
import { IPerson } from "@/types/actor/person.model";
|
||||
import Account from "vue-material-design-icons/Account.vue";
|
||||
|
||||
const PROFILES_PER_PAGE = 10;
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
persons: {
|
||||
query: LIST_PROFILES,
|
||||
fetchPolicy: "cache-and-network",
|
||||
variables() {
|
||||
return {
|
||||
preferredUsername: this.preferredUsername,
|
||||
name: this.name,
|
||||
domain: this.domain,
|
||||
local: this.local,
|
||||
suspended: this.suspended,
|
||||
page: this.page,
|
||||
limit: PROFILES_PER_PAGE,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
EmptyContent,
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t("Profiles") as string,
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class Profiles extends Vue {
|
||||
PROFILES_PER_PAGE = PROFILES_PER_PAGE;
|
||||
const preferredUsername = useRouteQuery("preferredUsername", "");
|
||||
const name = useRouteQuery("name", "");
|
||||
const domain = useRouteQuery("domain", "");
|
||||
const local = useRouteQuery("local", false, booleanTransformer);
|
||||
const suspended = useRouteQuery("suspended", false, booleanTransformer);
|
||||
const page = useRouteQuery("page", 1, integerTransformer);
|
||||
|
||||
RouteName = RouteName;
|
||||
const {
|
||||
result: personResult,
|
||||
loading,
|
||||
fetchMore,
|
||||
} = useQuery<{ persons: Paginate<IPerson> }>(LIST_PROFILES, () => ({
|
||||
preferredUsername: preferredUsername.value,
|
||||
name: name.value,
|
||||
domain: domain.value,
|
||||
local: local.value,
|
||||
suspended: suspended.value,
|
||||
page: page.value,
|
||||
limit: PROFILES_PER_PAGE,
|
||||
}));
|
||||
|
||||
async onPageChange(): Promise<void> {
|
||||
await this.doFetchMore();
|
||||
}
|
||||
const persons = computed(() => personResult.value?.persons);
|
||||
|
||||
get page(): number {
|
||||
return parseInt((this.$route.query.page as string) || "1", 10);
|
||||
}
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
set page(page: number) {
|
||||
this.pushRouter({ page: page.toString() });
|
||||
}
|
||||
useHead({
|
||||
title: computed(() => t("Profiles")),
|
||||
});
|
||||
|
||||
get domain(): string {
|
||||
return (this.$route.query.domain as string) || "";
|
||||
}
|
||||
const onPageChange = async (): Promise<void> => {
|
||||
await fetchMore();
|
||||
};
|
||||
|
||||
set domain(domain: string) {
|
||||
this.pushRouter({ domain });
|
||||
}
|
||||
|
||||
get preferredUsername(): string {
|
||||
return (this.$route.query.preferredUsername as string) || "";
|
||||
}
|
||||
|
||||
set preferredUsername(preferredUsername: string) {
|
||||
this.pushRouter({ preferredUsername });
|
||||
}
|
||||
|
||||
get local(): boolean {
|
||||
return this.$route.query.local === "1";
|
||||
}
|
||||
|
||||
set local(local: boolean) {
|
||||
this.pushRouter({ local: local ? "1" : "0" });
|
||||
}
|
||||
|
||||
get suspended(): boolean {
|
||||
return this.$route.query.suspended === "1";
|
||||
}
|
||||
|
||||
set suspended(suspended: boolean) {
|
||||
this.pushRouter({ suspended: suspended ? "1" : "0" });
|
||||
}
|
||||
|
||||
private async pushRouter(args: Record<string, string>): Promise<void> {
|
||||
try {
|
||||
await this.$router.push({
|
||||
name: RouteName.PROFILES,
|
||||
query: { ...this.$route.query, ...args },
|
||||
});
|
||||
} catch (e) {
|
||||
if (isNavigationFailure(e, NavigationFailureType.redirected)) {
|
||||
throw Error(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async doFetchMore(): Promise<void> {
|
||||
await this.$apollo.queries.persons.fetchMore({
|
||||
variables: {
|
||||
preferredUsername: this.preferredUsername,
|
||||
domain: this.domain,
|
||||
local: this.local,
|
||||
suspended: this.suspended,
|
||||
page: this.page,
|
||||
limit: PROFILES_PER_PAGE,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onFiltersChange({
|
||||
preferredUsername,
|
||||
domain,
|
||||
}: {
|
||||
preferredUsername: string;
|
||||
domain: string;
|
||||
}): void {
|
||||
this.preferredUsername = preferredUsername;
|
||||
this.domain = domain;
|
||||
this.doFetchMore();
|
||||
}
|
||||
}
|
||||
const onFiltersChange = ({
|
||||
preferredUsername: newPreferredUsername,
|
||||
domain: newDomain,
|
||||
}: {
|
||||
preferredUsername: string;
|
||||
domain: string;
|
||||
}): void => {
|
||||
preferredUsername.value = newPreferredUsername;
|
||||
domain.value = newDomain;
|
||||
fetchMore();
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
a.profile {
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
|
||||
<section v-if="settingsToWrite">
|
||||
<form @submit.prevent="updateSettings">
|
||||
<b-field :label="$t('Instance Name')" label-for="instance-name">
|
||||
<b-input v-model="settingsToWrite.instanceName" id="instance-name" />
|
||||
</b-field>
|
||||
<o-field :label="$t('Instance Name')" label-for="instance-name">
|
||||
<o-input v-model="settingsToWrite.instanceName" id="instance-name" />
|
||||
</o-field>
|
||||
<div class="field">
|
||||
<label class="label has-help" for="instance-description">{{
|
||||
$t("Instance Short Description")
|
||||
@@ -23,7 +23,7 @@
|
||||
)
|
||||
}}
|
||||
</small>
|
||||
<b-input
|
||||
<o-input
|
||||
type="textarea"
|
||||
v-model="settingsToWrite.instanceDescription"
|
||||
rows="2"
|
||||
@@ -41,7 +41,7 @@
|
||||
)
|
||||
}}
|
||||
</small>
|
||||
<b-input
|
||||
<o-input
|
||||
v-model="settingsToWrite.instanceSlogan"
|
||||
:placeholder="$t('Gather ⋅ Organize ⋅ Mobilize')"
|
||||
id="instance-slogan"
|
||||
@@ -54,16 +54,21 @@
|
||||
<small>
|
||||
{{ $t("Can be an email or a link, or just plain text.") }}
|
||||
</small>
|
||||
<b-input v-model="settingsToWrite.contact" id="instance-contact" />
|
||||
<o-input v-model="settingsToWrite.contact" id="instance-contact" />
|
||||
</div>
|
||||
<b-field :label="$t('Allow registrations')">
|
||||
<b-switch v-model="settingsToWrite.registrationsOpen">
|
||||
<p class="content" v-if="settingsToWrite.registrationsOpen">
|
||||
<o-field :label="$t('Allow registrations')">
|
||||
<o-switch v-model="settingsToWrite.registrationsOpen">
|
||||
<p
|
||||
class="prose dark:prose-invert"
|
||||
v-if="settingsToWrite.registrationsOpen"
|
||||
>
|
||||
{{ $t("Registration is allowed, anyone can register.") }}
|
||||
</p>
|
||||
<p class="content" v-else>{{ $t("Registration is closed.") }}</p>
|
||||
</b-switch>
|
||||
</b-field>
|
||||
<p class="prose dark:prose-invert" v-else>
|
||||
{{ $t("Registration is closed.") }}
|
||||
</p>
|
||||
</o-switch>
|
||||
</o-field>
|
||||
<div class="field">
|
||||
<label class="label has-help" for="instance-languages">{{
|
||||
$t("Instance languages")
|
||||
@@ -71,7 +76,7 @@
|
||||
<small>
|
||||
{{ $t("Main languages you/your moderators speak") }}
|
||||
</small>
|
||||
<b-taginput
|
||||
<o-taginput
|
||||
v-model="instanceLanguages"
|
||||
:data="filteredLanguages"
|
||||
autocomplete
|
||||
@@ -82,8 +87,8 @@
|
||||
@typing="getFilteredLanguages"
|
||||
id="instance-languages"
|
||||
>
|
||||
<template slot="empty">{{ $t("No languages found") }}</template>
|
||||
</b-taginput>
|
||||
<template #empty>{{ $t("No languages found") }}</template>
|
||||
</o-taginput>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label has-help" for="instance-long-description">{{
|
||||
@@ -96,7 +101,7 @@
|
||||
)
|
||||
}}
|
||||
</small>
|
||||
<b-input
|
||||
<o-input
|
||||
type="textarea"
|
||||
v-model="settingsToWrite.instanceLongDescription"
|
||||
rows="4"
|
||||
@@ -114,43 +119,43 @@
|
||||
)
|
||||
}}
|
||||
</small>
|
||||
<b-input
|
||||
<o-input
|
||||
type="textarea"
|
||||
v-model="settingsToWrite.instanceRules"
|
||||
id="instance-rules"
|
||||
/>
|
||||
</div>
|
||||
<b-field :label="$t('Instance Terms Source')">
|
||||
<o-field :label="$t('Instance Terms Source')">
|
||||
<div class="columns">
|
||||
<div class="column is-one-third-desktop">
|
||||
<fieldset>
|
||||
<legend>
|
||||
{{ $t("Choose the source of the instance's Terms") }}
|
||||
</legend>
|
||||
<b-field>
|
||||
<b-radio
|
||||
<o-field>
|
||||
<o-radio
|
||||
v-model="settingsToWrite.instanceTermsType"
|
||||
name="instanceTermsType"
|
||||
:native-value="InstanceTermsType.DEFAULT"
|
||||
>{{ $t("Default Mobilizon terms") }}</b-radio
|
||||
>{{ $t("Default Mobilizon terms") }}</o-radio
|
||||
>
|
||||
</b-field>
|
||||
<b-field>
|
||||
<b-radio
|
||||
</o-field>
|
||||
<o-field>
|
||||
<o-radio
|
||||
v-model="settingsToWrite.instanceTermsType"
|
||||
name="instanceTermsType"
|
||||
:native-value="InstanceTermsType.URL"
|
||||
>{{ $t("Custom URL") }}</b-radio
|
||||
>{{ $t("Custom URL") }}</o-radio
|
||||
>
|
||||
</b-field>
|
||||
<b-field>
|
||||
<b-radio
|
||||
</o-field>
|
||||
<o-field>
|
||||
<o-radio
|
||||
v-model="settingsToWrite.instanceTermsType"
|
||||
name="instanceTermsType"
|
||||
:native-value="InstanceTermsType.CUSTOM"
|
||||
>{{ $t("Custom text") }}</b-radio
|
||||
>{{ $t("Custom text") }}</o-radio
|
||||
>
|
||||
</b-field>
|
||||
</o-field>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="column">
|
||||
@@ -162,19 +167,20 @@
|
||||
"
|
||||
>
|
||||
<b>{{ $t("Default") }}</b>
|
||||
<i18n
|
||||
<i18n-t
|
||||
tag="p"
|
||||
class="content"
|
||||
path="The {default_terms} will be used. They will be translated in the user's language."
|
||||
class="prose dark:prose-invert"
|
||||
keypath="The {default_terms} will be used. They will be translated in the user's language."
|
||||
>
|
||||
<a
|
||||
slot="default_terms"
|
||||
href="https://demo.mobilizon.org/terms"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>{{ $t("default Mobilizon terms") }}</a
|
||||
>
|
||||
</i18n>
|
||||
<template #default_terms>
|
||||
<a
|
||||
href="https://demo.mobilizon.org/terms"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>{{ $t("default Mobilizon terms") }}</a
|
||||
>
|
||||
</template>
|
||||
</i18n-t>
|
||||
<b>{{
|
||||
$t(
|
||||
"NOTE! The default terms have not been checked over by a lawyer and thus are unlikely to provide full legal protection for all situations for an instance admin using them. They are also not specific to all countries and jurisdictions. If you are unsure, please check with a lawyer."
|
||||
@@ -188,7 +194,7 @@
|
||||
"
|
||||
>
|
||||
<b>{{ $t("URL") }}</b>
|
||||
<p class="content">
|
||||
<p class="prose dark:prose-invert">
|
||||
{{ $t("Set an URL to a page with your own terms.") }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -199,77 +205,78 @@
|
||||
"
|
||||
>
|
||||
<b>{{ $t("Custom") }}</b>
|
||||
<i18n
|
||||
<i18n-t
|
||||
tag="p"
|
||||
class="content"
|
||||
path="Enter your own terms. HTML tags allowed. The {mobilizon_terms} are provided as template."
|
||||
class="prose dark:prose-invert"
|
||||
keypath="Enter your own terms. HTML tags allowed. The {mobilizon_terms} are provided as template."
|
||||
>
|
||||
<a
|
||||
slot="mobilizon_terms"
|
||||
href="https://demo.mobilizon.org/terms"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
{{ $t("default Mobilizon terms") }}</a
|
||||
>
|
||||
</i18n>
|
||||
<template #mobilizon_terms>
|
||||
<a
|
||||
href="https://demo.mobilizon.org/terms"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
{{ $t("default Mobilizon terms") }}</a
|
||||
>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</b-field>
|
||||
<b-field
|
||||
</o-field>
|
||||
<o-field
|
||||
:label="$t('Instance Terms URL')"
|
||||
label-for="instanceTermsUrl"
|
||||
v-if="settingsToWrite.instanceTermsType === InstanceTermsType.URL"
|
||||
>
|
||||
<b-input
|
||||
<o-input
|
||||
type="URL"
|
||||
v-model="settingsToWrite.instanceTermsUrl"
|
||||
id="instanceTermsUrl"
|
||||
/>
|
||||
</b-field>
|
||||
<b-field
|
||||
</o-field>
|
||||
<o-field
|
||||
:label="$t('Instance Terms')"
|
||||
label-for="instanceTerms"
|
||||
v-if="settingsToWrite.instanceTermsType === InstanceTermsType.CUSTOM"
|
||||
>
|
||||
<b-input
|
||||
<o-input
|
||||
type="textarea"
|
||||
v-model="settingsToWrite.instanceTerms"
|
||||
id="instanceTerms"
|
||||
/>
|
||||
</b-field>
|
||||
<b-field :label="$t('Instance Privacy Policy Source')">
|
||||
</o-field>
|
||||
<o-field :label="$t('Instance Privacy Policy Source')">
|
||||
<div class="columns">
|
||||
<div class="column is-one-third-desktop">
|
||||
<fieldset>
|
||||
<legend>
|
||||
{{ $t("Choose the source of the instance's Privacy Policy") }}
|
||||
</legend>
|
||||
<b-field>
|
||||
<b-radio
|
||||
<o-field>
|
||||
<o-radio
|
||||
v-model="settingsToWrite.instancePrivacyPolicyType"
|
||||
name="instancePrivacyType"
|
||||
:native-value="InstancePrivacyType.DEFAULT"
|
||||
>{{ $t("Default Mobilizon privacy policy") }}</b-radio
|
||||
>{{ $t("Default Mobilizon privacy policy") }}</o-radio
|
||||
>
|
||||
</b-field>
|
||||
<b-field>
|
||||
<b-radio
|
||||
</o-field>
|
||||
<o-field>
|
||||
<o-radio
|
||||
v-model="settingsToWrite.instancePrivacyPolicyType"
|
||||
name="instancePrivacyType"
|
||||
:native-value="InstancePrivacyType.URL"
|
||||
>{{ $t("Custom URL") }}</b-radio
|
||||
>{{ $t("Custom URL") }}</o-radio
|
||||
>
|
||||
</b-field>
|
||||
<b-field>
|
||||
<b-radio
|
||||
</o-field>
|
||||
<o-field>
|
||||
<o-radio
|
||||
v-model="settingsToWrite.instancePrivacyPolicyType"
|
||||
name="instancePrivacyType"
|
||||
:native-value="InstancePrivacyType.CUSTOM"
|
||||
>{{ $t("Custom text") }}</b-radio
|
||||
>{{ $t("Custom text") }}</o-radio
|
||||
>
|
||||
</b-field>
|
||||
</o-field>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="column">
|
||||
@@ -281,19 +288,20 @@
|
||||
"
|
||||
>
|
||||
<b>{{ $t("Default") }}</b>
|
||||
<i18n
|
||||
<i18n-t
|
||||
tag="p"
|
||||
class="content"
|
||||
path="The {default_privacy_policy} will be used. They will be translated in the user's language."
|
||||
class="prose dark:prose-invert"
|
||||
keypath="The {default_privacy_policy} will be used. They will be translated in the user's language."
|
||||
>
|
||||
<a
|
||||
slot="default_privacy_policy"
|
||||
href="https://demo.mobilizon.org/privacy"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>{{ $t("default Mobilizon privacy policy") }}</a
|
||||
>
|
||||
</i18n>
|
||||
<template #default_privacy_policy>
|
||||
<a
|
||||
href="https://demo.mobilizon.org/privacy"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>{{ $t("default Mobilizon privacy policy") }}</a
|
||||
>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
<div
|
||||
class="notification"
|
||||
@@ -303,7 +311,7 @@
|
||||
"
|
||||
>
|
||||
<b>{{ $t("URL") }}</b>
|
||||
<p class="content">
|
||||
<p class="prose dark:prose-invert">
|
||||
{{ $t("Set an URL to a page with your own privacy policy.") }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -315,25 +323,26 @@
|
||||
"
|
||||
>
|
||||
<b>{{ $t("Custom") }}</b>
|
||||
<i18n
|
||||
<i18n-t
|
||||
tag="p"
|
||||
class="content"
|
||||
class="prose dark:prose-invert"
|
||||
path="Enter your own privacy policy. HTML tags allowed. The {mobilizon_privacy_policy} is provided as template."
|
||||
>
|
||||
<a
|
||||
slot="mobilizon_privacy_policy"
|
||||
href="https://demo.mobilizon.org/privacy"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
{{ $t("default Mobilizon privacy policy") }}</a
|
||||
>
|
||||
</i18n>
|
||||
<template #mobilizon_privacy_policy>
|
||||
<a
|
||||
href="https://demo.mobilizon.org/privacy"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
{{ $t("default Mobilizon privacy policy") }}</a
|
||||
>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</b-field>
|
||||
<b-field
|
||||
</o-field>
|
||||
<o-field
|
||||
:label="$t('Instance Privacy Policy URL')"
|
||||
label-for="instancePrivacyPolicyUrl"
|
||||
v-if="
|
||||
@@ -341,13 +350,13 @@
|
||||
InstancePrivacyType.URL
|
||||
"
|
||||
>
|
||||
<b-input
|
||||
<o-input
|
||||
type="URL"
|
||||
v-model="settingsToWrite.instancePrivacyPolicyUrl"
|
||||
id="instancePrivacyPolicyUrl"
|
||||
/>
|
||||
</b-field>
|
||||
<b-field
|
||||
</o-field>
|
||||
<o-field
|
||||
:label="$t('Instance Privacy Policy')"
|
||||
label-for="instancePrivacyPolicy"
|
||||
v-if="
|
||||
@@ -355,142 +364,147 @@
|
||||
InstancePrivacyType.CUSTOM
|
||||
"
|
||||
>
|
||||
<b-input
|
||||
<o-input
|
||||
type="textarea"
|
||||
v-model="settingsToWrite.instancePrivacyPolicy"
|
||||
id="instancePrivacyPolicy"
|
||||
/>
|
||||
</b-field>
|
||||
<b-button native-type="submit" type="is-primary">{{
|
||||
</o-field>
|
||||
<o-button native-type="submit" variant="primary">{{
|
||||
$t("Save")
|
||||
}}</b-button>
|
||||
}}</o-button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Watch } from "vue-property-decorator";
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ADMIN_SETTINGS,
|
||||
SAVE_ADMIN_SETTINGS,
|
||||
LANGUAGES,
|
||||
} from "@/graphql/admin";
|
||||
import { InstancePrivacyType, InstanceTermsType } from "@/types/enums";
|
||||
import { IAdminSettings, ILanguage } from "../../types/admin.model";
|
||||
import RouteName from "../../router/name";
|
||||
import { IAdminSettings, ILanguage } from "@/types/admin.model";
|
||||
import RouteName from "@/router/name";
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { ref, computed, watch, inject } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
adminSettings: ADMIN_SETTINGS,
|
||||
languages: LANGUAGES,
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t("Settings") as string,
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class Settings extends Vue {
|
||||
adminSettings: IAdminSettings = {
|
||||
instanceName: "",
|
||||
instanceDescription: "",
|
||||
instanceSlogan: "",
|
||||
instanceLongDescription: "",
|
||||
contact: "",
|
||||
instanceTerms: "",
|
||||
instanceTermsType: InstanceTermsType.DEFAULT,
|
||||
instanceTermsUrl: null,
|
||||
instancePrivacyPolicy: "",
|
||||
instancePrivacyPolicyType: InstanceTermsType.DEFAULT,
|
||||
instancePrivacyPolicyUrl: null,
|
||||
instanceRules: "",
|
||||
registrationsOpen: false,
|
||||
instanceLanguages: [],
|
||||
};
|
||||
const { result: adminSettingsResult } = useQuery<{
|
||||
adminSettings: IAdminSettings;
|
||||
}>(ADMIN_SETTINGS);
|
||||
const adminSettings = computed(
|
||||
() =>
|
||||
adminSettingsResult.value?.adminSettings ?? {
|
||||
instanceName: "",
|
||||
instanceDescription: "",
|
||||
instanceSlogan: "",
|
||||
instanceLongDescription: "",
|
||||
contact: "",
|
||||
instanceTerms: "",
|
||||
instanceTermsType: InstanceTermsType.DEFAULT,
|
||||
instanceTermsUrl: null,
|
||||
instancePrivacyPolicy: "",
|
||||
instancePrivacyPolicyType: InstanceTermsType.DEFAULT,
|
||||
instancePrivacyPolicyUrl: null,
|
||||
instanceRules: "",
|
||||
registrationsOpen: false,
|
||||
instanceLanguages: [],
|
||||
}
|
||||
);
|
||||
|
||||
settingsToWrite: IAdminSettings = { ...this.adminSettings };
|
||||
const { result: languageResult } = useQuery<{ languages: ILanguage[] }>(
|
||||
LANGUAGES
|
||||
);
|
||||
const languages = computed(() => languageResult.value?.languages);
|
||||
|
||||
@Watch("adminSettings")
|
||||
updateSettingsToWrite(): void {
|
||||
this.settingsToWrite = { ...this.adminSettings };
|
||||
}
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
useHead({
|
||||
title: computed(() => t("Settings")),
|
||||
});
|
||||
|
||||
languages!: ILanguage[];
|
||||
const settingsToWrite = ref<IAdminSettings>({ ...adminSettings });
|
||||
|
||||
filteredLanguages: string[] = [];
|
||||
watch(adminSettings, () => {
|
||||
// settingsToWrite.value = { ...adminSettings.value };
|
||||
});
|
||||
|
||||
InstanceTermsType = InstanceTermsType;
|
||||
const filteredLanguages = ref<string[]>([]);
|
||||
|
||||
InstancePrivacyType = InstancePrivacyType;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
get instanceLanguages(): string[] {
|
||||
const languageCodes = [...this.adminSettings.instanceLanguages] || [];
|
||||
const instanceLanguages = computed({
|
||||
get() {
|
||||
const languageCodes = [...adminSettings.value.instanceLanguages] || [];
|
||||
return languageCodes
|
||||
.map((code) => this.languageForCode(code))
|
||||
.map((code) => languageForCode(code))
|
||||
.filter((language) => language) as string[];
|
||||
}
|
||||
|
||||
set instanceLanguages(instanceLanguages: string[]) {
|
||||
const newInstanceLanguages = instanceLanguages
|
||||
},
|
||||
set(newInstanceLanguages: string[]) {
|
||||
const newFilteredInstanceLanguages = newInstanceLanguages
|
||||
.map((language) => {
|
||||
return this.codeForLanguage(language);
|
||||
return codeForLanguage(language);
|
||||
})
|
||||
.filter((code) => code !== undefined) as string[];
|
||||
this.adminSettings = {
|
||||
...this.adminSettings,
|
||||
instanceLanguages: newInstanceLanguages,
|
||||
};
|
||||
}
|
||||
// adminSettings = {
|
||||
// ...adminSettings,
|
||||
// instanceLanguages: newInstanceLanguages,
|
||||
// };
|
||||
},
|
||||
});
|
||||
|
||||
async updateSettings(): Promise<void> {
|
||||
const variables = { ...this.settingsToWrite };
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: SAVE_ADMIN_SETTINGS,
|
||||
variables,
|
||||
});
|
||||
this.$notifier.success(
|
||||
this.$t("Admin settings successfully saved.") as string
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.$notifier.error(this.$t("Failed to save admin settings") as string);
|
||||
}
|
||||
}
|
||||
const notifier = inject<Notifier>("notifier");
|
||||
|
||||
getFilteredLanguages(text: string): void {
|
||||
this.filteredLanguages = this.languages
|
||||
? this.languages
|
||||
.filter((language: ILanguage) => {
|
||||
return (
|
||||
language.name
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.indexOf(text.toLowerCase()) >= 0
|
||||
);
|
||||
})
|
||||
.map(({ name }) => name)
|
||||
: [];
|
||||
}
|
||||
const {
|
||||
mutate: saveAdminSettings,
|
||||
onDone: saveAdminSettingsDone,
|
||||
onError: saveAdminSettingsError,
|
||||
} = useMutation(SAVE_ADMIN_SETTINGS);
|
||||
|
||||
private codeForLanguage(language: string): string | undefined {
|
||||
if (this.languages) {
|
||||
const lang = this.languages.find(({ name }) => name === language);
|
||||
if (lang) return lang.code;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
saveAdminSettingsDone(() => {
|
||||
notifier?.success(t("Admin settings successfully saved.") as string);
|
||||
});
|
||||
|
||||
private languageForCode(codeGiven: string): string | undefined {
|
||||
if (this.languages) {
|
||||
const lang = this.languages.find(({ code }) => code === codeGiven);
|
||||
if (lang) return lang.name;
|
||||
}
|
||||
return undefined;
|
||||
saveAdminSettingsError((e) => {
|
||||
console.error(e);
|
||||
notifier?.error(t("Failed to save admin settings") as string);
|
||||
});
|
||||
|
||||
const updateSettings = async (): Promise<void> => {
|
||||
const variables = { ...settingsToWrite };
|
||||
saveAdminSettings(variables);
|
||||
};
|
||||
|
||||
const getFilteredLanguages = (text: string): void => {
|
||||
filteredLanguages.value = languages.value
|
||||
? languages.value
|
||||
.filter((language: ILanguage) => {
|
||||
return (
|
||||
language.name
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.indexOf(text.toLowerCase()) >= 0
|
||||
);
|
||||
})
|
||||
.map(({ name }) => name)
|
||||
: [];
|
||||
};
|
||||
|
||||
const codeForLanguage = (language: string): string | undefined => {
|
||||
if (languages.value) {
|
||||
const lang = languages.value.find(({ name }) => name === language);
|
||||
if (lang) return lang.code;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const languageForCode = (codeGiven: string): string | undefined => {
|
||||
if (languages.value) {
|
||||
const lang = languages.value.find(({ code }) => code === codeGiven);
|
||||
if (lang) return lang.name;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
label.label.has-help {
|
||||
|
||||
@@ -11,27 +11,27 @@
|
||||
/>
|
||||
<div v-if="users">
|
||||
<form @submit.prevent="activateFilters">
|
||||
<b-field class="mb-5" grouped group-multiline>
|
||||
<b-field :label="$t('Email')" expanded>
|
||||
<b-input trap-focus icon="email" v-model="emailFilterFieldValue" />
|
||||
</b-field>
|
||||
<b-field :label="$t('IP Address')" expanded>
|
||||
<b-input icon="web" v-model="ipFilterFieldValue" />
|
||||
</b-field>
|
||||
<o-field class="mb-5" grouped group-multiline>
|
||||
<o-field :label="$t('Email')" expanded>
|
||||
<o-input trap-focus icon="email" v-model="emailFilterFieldValue" />
|
||||
</o-field>
|
||||
<o-field :label="$t('IP Address')" expanded>
|
||||
<o-input icon="web" v-model="ipFilterFieldValue" />
|
||||
</o-field>
|
||||
<p class="control self-end mb-0">
|
||||
<b-button type="is-primary" native-type="submit">{{
|
||||
<o-button variant="primary" native-type="submit">{{
|
||||
$t("Filter")
|
||||
}}</b-button>
|
||||
}}</o-button>
|
||||
</p>
|
||||
</b-field>
|
||||
</o-field>
|
||||
</form>
|
||||
<b-table
|
||||
<o-table
|
||||
:data="users.elements"
|
||||
:loading="$apollo.queries.users.loading"
|
||||
:loading="usersLoading"
|
||||
paginated
|
||||
backend-pagination
|
||||
:debounce-search="500"
|
||||
:current-page.sync="page"
|
||||
v-model:current-page="page"
|
||||
:aria-next-label="$t('Next page')"
|
||||
:aria-previous-label="$t('Previous page')"
|
||||
:aria-page-label="$t('Page')"
|
||||
@@ -41,10 +41,10 @@
|
||||
:per-page="USERS_PER_PAGE"
|
||||
@page-change="onPageChange"
|
||||
>
|
||||
<b-table-column field="id" width="40" numeric v-slot="props">
|
||||
<o-table-column field="id" width="40" numeric v-slot="props">
|
||||
{{ props.row.id }}
|
||||
</b-table-column>
|
||||
<b-table-column field="email" :label="$t('Email')">
|
||||
</o-table-column>
|
||||
<o-table-column field="email" :label="$t('Email')">
|
||||
<template v-slot:default="props">
|
||||
<router-link
|
||||
:to="{
|
||||
@@ -56,8 +56,8 @@
|
||||
{{ props.row.email }}
|
||||
</router-link>
|
||||
</template>
|
||||
</b-table-column>
|
||||
<b-table-column
|
||||
</o-table-column>
|
||||
<o-table-column
|
||||
field="confirmedAt"
|
||||
:label="$t('Last seen on')"
|
||||
:centered="true"
|
||||
@@ -65,171 +65,129 @@
|
||||
>
|
||||
<template v-if="props.row.currentSignInAt">
|
||||
<time :datetime="props.row.currentSignInAt">
|
||||
{{ props.row.currentSignInAt | formatDateTimeString }}
|
||||
{{ formatDateTimeString(props.row.currentSignInAt) }}
|
||||
</time>
|
||||
</template>
|
||||
<template v-else-if="props.row.confirmedAt"> - </template>
|
||||
<template v-else>
|
||||
{{ $t("Not confirmed") }}
|
||||
</template>
|
||||
</b-table-column>
|
||||
<b-table-column
|
||||
</o-table-column>
|
||||
<o-table-column
|
||||
field="locale"
|
||||
:label="$t('Language')"
|
||||
:centered="true"
|
||||
v-slot="props"
|
||||
>
|
||||
{{ getLanguageNameForCode(props.row.locale) }}
|
||||
</b-table-column>
|
||||
</o-table-column>
|
||||
<template #empty>
|
||||
<empty-content
|
||||
v-if="!$apollo.loading && emailFilter"
|
||||
v-if="!usersLoading && emailFilter"
|
||||
:inline="true"
|
||||
icon="account"
|
||||
>
|
||||
{{ $t("No user matches the filters") }}
|
||||
<template #desc>
|
||||
<b-button type="is-primary" @click="resetFilters">
|
||||
<o-button variant="primary" @click="resetFilters">
|
||||
{{ $t("Reset filters") }}
|
||||
</b-button>
|
||||
</o-button>
|
||||
</template>
|
||||
</empty-content>
|
||||
</template>
|
||||
</b-table>
|
||||
</o-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
<script lang="ts" setup>
|
||||
import { LIST_USERS } from "../../graphql/user";
|
||||
import RouteName from "../../router/name";
|
||||
import VueRouter from "vue-router";
|
||||
import { LANGUAGES_CODES } from "@/graphql/admin";
|
||||
import { IUser } from "@/types/current-user.model";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
import EmptyContent from "../../components/Utils/EmptyContent.vue";
|
||||
const { isNavigationFailure, NavigationFailureType } = VueRouter;
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { ILanguage } from "@/types/admin.model";
|
||||
import { computed, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import { integerTransformer, useRouteQuery } from "vue-use-route-query";
|
||||
import { formatDateTimeString } from "@/filters/datetime";
|
||||
|
||||
const USERS_PER_PAGE = 10;
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
users: {
|
||||
query: LIST_USERS,
|
||||
fetchPolicy: "cache-and-network",
|
||||
variables() {
|
||||
return {
|
||||
email: this.emailFilter,
|
||||
currentSignInIp: this.ipFilter,
|
||||
page: this.page,
|
||||
limit: USERS_PER_PAGE,
|
||||
};
|
||||
},
|
||||
const emailFilter = useRouteQuery("emailFilter", "");
|
||||
const ipFilter = useRouteQuery("ipFilter", "");
|
||||
const page = useRouteQuery("page", 1, integerTransformer);
|
||||
|
||||
const languagesCodes = computed((): string[] => {
|
||||
return (users.value?.elements ?? []).map((user: IUser) => user.locale);
|
||||
});
|
||||
|
||||
const {
|
||||
result: usersResult,
|
||||
fetchMore,
|
||||
loading: usersLoading,
|
||||
} = useQuery<{ users: Paginate<IUser> }>(LIST_USERS, () => ({
|
||||
email: emailFilter.value,
|
||||
currentSignInIp: ipFilter.value,
|
||||
page: page.value,
|
||||
limit: USERS_PER_PAGE,
|
||||
}));
|
||||
|
||||
const users = computed(() => usersResult.value?.users);
|
||||
|
||||
const { result: languagesResult } = useQuery<{ languages: ILanguage[] }>(
|
||||
LANGUAGES_CODES,
|
||||
() => ({
|
||||
codes: languagesCodes.value,
|
||||
}),
|
||||
() => ({
|
||||
enabled: languagesCodes.value !== undefined,
|
||||
})
|
||||
);
|
||||
|
||||
const languages = computed(() => languagesResult.value?.languages);
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
useHead({
|
||||
title: computed(() => t("Users")),
|
||||
});
|
||||
|
||||
const emailFilterFieldValue = ref(emailFilter.value);
|
||||
const ipFilterFieldValue = ref(ipFilter.value);
|
||||
|
||||
const getLanguageNameForCode = (code: string): string => {
|
||||
return (
|
||||
(languages.value ?? []).find(({ code: languageCode }) => {
|
||||
return languageCode === code;
|
||||
})?.name || code
|
||||
);
|
||||
};
|
||||
|
||||
const onPageChange = async (newPage: number): Promise<void> => {
|
||||
page.value = newPage;
|
||||
await fetchMore({
|
||||
variables: {
|
||||
email: emailFilter.value,
|
||||
currentSignInIp: ipFilter.value,
|
||||
page: page.value,
|
||||
limit: USERS_PER_PAGE,
|
||||
},
|
||||
languages: {
|
||||
query: LANGUAGES_CODES,
|
||||
variables() {
|
||||
return {
|
||||
codes: this.languagesCodes,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return this.languagesCodes.length < 1;
|
||||
},
|
||||
},
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t("Users") as string,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
EmptyContent,
|
||||
},
|
||||
})
|
||||
export default class Users extends Vue {
|
||||
USERS_PER_PAGE = USERS_PER_PAGE;
|
||||
});
|
||||
};
|
||||
|
||||
RouteName = RouteName;
|
||||
const activateFilters = (): void => {
|
||||
emailFilter.value = emailFilterFieldValue.value;
|
||||
ipFilter.value = ipFilterFieldValue.value;
|
||||
};
|
||||
|
||||
users!: Paginate<IUser>;
|
||||
languages!: Array<{ code: string; name: string }>;
|
||||
|
||||
emailFilterFieldValue = this.emailFilter;
|
||||
ipFilterFieldValue = this.ipFilter;
|
||||
|
||||
get page(): number {
|
||||
return parseInt((this.$route.query.page as string) || "1", 10);
|
||||
}
|
||||
|
||||
set page(page: number) {
|
||||
this.pushRouter({ page: page.toString() });
|
||||
}
|
||||
|
||||
get emailFilter(): string {
|
||||
return (this.$route.query.emailFilter as string) || "";
|
||||
}
|
||||
|
||||
set emailFilter(emailFilter: string) {
|
||||
this.pushRouter({ emailFilter });
|
||||
}
|
||||
|
||||
get ipFilter(): string {
|
||||
return (this.$route.query.ipFilter as string) || "";
|
||||
}
|
||||
|
||||
set ipFilter(ipFilter: string) {
|
||||
this.pushRouter({ ipFilter });
|
||||
}
|
||||
|
||||
get languagesCodes(): string[] {
|
||||
return (this.users?.elements || []).map((user: IUser) => user.locale);
|
||||
}
|
||||
|
||||
getLanguageNameForCode(code: string): string {
|
||||
return (
|
||||
(this.languages || []).find(({ code: languageCode }) => {
|
||||
return languageCode === code;
|
||||
})?.name || code
|
||||
);
|
||||
}
|
||||
|
||||
async onPageChange(page: number): Promise<void> {
|
||||
this.page = page;
|
||||
await this.$apollo.queries.users.fetchMore({
|
||||
variables: {
|
||||
email: this.emailFilter,
|
||||
currentSignInIp: this.ipFilter,
|
||||
page: this.page,
|
||||
limit: USERS_PER_PAGE,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
activateFilters(): void {
|
||||
this.emailFilter = this.emailFilterFieldValue;
|
||||
this.ipFilter = this.ipFilterFieldValue;
|
||||
}
|
||||
|
||||
resetFilters(): void {
|
||||
this.emailFilterFieldValue = "";
|
||||
this.ipFilterFieldValue = "";
|
||||
this.activateFilters();
|
||||
}
|
||||
|
||||
private async pushRouter(args: Record<string, string>): Promise<void> {
|
||||
try {
|
||||
await this.$router.push({
|
||||
name: RouteName.USERS,
|
||||
query: { ...this.$route.query, ...args },
|
||||
});
|
||||
} catch (e) {
|
||||
if (isNavigationFailure(e, NavigationFailureType.redirected)) {
|
||||
throw Error(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const resetFilters = (): void => {
|
||||
emailFilterFieldValue.value = "";
|
||||
ipFilterFieldValue.value = "";
|
||||
activateFilters();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
Reference in New Issue
Block a user