Introduce basic user and profile management
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
318
js/src/views/Admin/AdminProfile.vue
Normal file
318
js/src/views/Admin/AdminProfile.vue
Normal file
@@ -0,0 +1,318 @@
|
||||
<template>
|
||||
<div v-if="person" class="section">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link :to="{ name: RouteName.ADMIN }">{{ $t("Admin") }}</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.PROFILES,
|
||||
}"
|
||||
>{{ $t("Profiles") }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.PROFILES,
|
||||
params: { id: person.id },
|
||||
}"
|
||||
>{{ person.name || person.preferredUsername }}</router-link
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<article class="media">
|
||||
<figure class="media-left" v-if="person.avatar">
|
||||
<p class="image is-48x48">
|
||||
<img :src="person.avatar.url" alt="" />
|
||||
</p>
|
||||
</figure>
|
||||
<div class="media-content">
|
||||
<div class="content">
|
||||
<strong v-if="person.name">{{ person.name }}</strong>
|
||||
<small>@{{ usernameWithDomain(person) }}</small>
|
||||
<p v-html="person.summary" />
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<table v-if="metadata.length > 0" class="table is-fullwidth">
|
||||
<tbody>
|
||||
<tr v-for="{ key, value, link } in metadata" :key="key">
|
||||
<td>{{ key }}</td>
|
||||
<td v-if="link">
|
||||
<router-link :to="link">
|
||||
{{ value }}
|
||||
</router-link>
|
||||
</td>
|
||||
<td v-else>{{ value }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="buttons">
|
||||
<b-button
|
||||
@click="suspendProfile"
|
||||
v-if="person.domain && !person.suspended"
|
||||
type="is-primary"
|
||||
>{{ $t("Suspend") }}</b-button
|
||||
>
|
||||
<b-button
|
||||
@click="unsuspendProfile"
|
||||
v-if="person.domain && person.suspended"
|
||||
type="is-primary"
|
||||
>{{ $t("Unsuspend") }}</b-button
|
||||
>
|
||||
</div>
|
||||
<section>
|
||||
<h2 class="subtitle">
|
||||
{{
|
||||
$tc("{number} organized events", person.organizedEvents.total, {
|
||||
number: person.organizedEvents.total,
|
||||
})
|
||||
}}
|
||||
</h2>
|
||||
<b-table
|
||||
:data="person.organizedEvents.elements"
|
||||
:loading="$apollo.queries.person.loading"
|
||||
paginated
|
||||
backend-pagination
|
||||
:total="person.organizedEvents.total"
|
||||
:per-page="EVENTS_PER_PAGE"
|
||||
@page-change="onOrganizedEventsPageChange"
|
||||
>
|
||||
<template slot-scope="props">
|
||||
<b-table-column field="beginsOn" :label="$t('Begins on')">
|
||||
{{ props.row.beginsOn | formatDateTimeString }}
|
||||
</b-table-column>
|
||||
<b-table-column field="title" :label="$t('Title')">
|
||||
<router-link :to="{ name: RouteName.EVENT, params: { uuid: props.row.uuid } }">
|
||||
{{ props.row.title }}
|
||||
</router-link>
|
||||
</b-table-column>
|
||||
</template>
|
||||
<template slot="empty">
|
||||
<section class="section">
|
||||
<div class="content has-text-grey has-text-centered">
|
||||
<p>{{ $t("Nothing to see here") }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</b-table>
|
||||
</section>
|
||||
<section>
|
||||
<h2 class="subtitle">
|
||||
{{
|
||||
$tc("{number} participations", person.participations.total, {
|
||||
number: person.participations.total,
|
||||
})
|
||||
}}
|
||||
</h2>
|
||||
<b-table
|
||||
:data="person.participations.elements.map((participation) => participation.event)"
|
||||
:loading="$apollo.queries.person.loading"
|
||||
paginated
|
||||
backend-pagination
|
||||
:total="person.participations.total"
|
||||
:per-page="EVENTS_PER_PAGE"
|
||||
@page-change="onParticipationsPageChange"
|
||||
>
|
||||
<template slot-scope="props">
|
||||
<b-table-column field="beginsOn" :label="$t('Begins on')">
|
||||
{{ props.row.beginsOn | formatDateTimeString }}
|
||||
</b-table-column>
|
||||
<b-table-column field="title" :label="$t('Title')">
|
||||
<router-link :to="{ name: RouteName.EVENT, params: { uuid: props.row.uuid } }">
|
||||
{{ props.row.title }}
|
||||
</router-link>
|
||||
</b-table-column>
|
||||
</template>
|
||||
<template slot="empty">
|
||||
<section class="section">
|
||||
<div class="content has-text-grey has-text-centered">
|
||||
<p>{{ $t("Nothing to see here") }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</b-table>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from "vue-property-decorator";
|
||||
import { GET_PERSON, SUSPEND_PROFILE, UNSUSPEND_PROFILE } from "../../graphql/actor";
|
||||
import { IPerson } from "../../types/actor";
|
||||
import { usernameWithDomain } from "../../types/actor/actor.model";
|
||||
import RouteName from "../../router/name";
|
||||
import { IEvent } from "../../types/event.model";
|
||||
|
||||
const EVENTS_PER_PAGE = 10;
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
person: {
|
||||
query: GET_PERSON,
|
||||
variables() {
|
||||
return {
|
||||
actorId: this.id,
|
||||
organizedEventsPage: 1,
|
||||
organizedEventsLimit: EVENTS_PER_PAGE,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.id;
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class AdminProfile extends Vue {
|
||||
@Prop({ required: true }) id!: String;
|
||||
|
||||
person!: IPerson;
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
EVENTS_PER_PAGE = EVENTS_PER_PAGE;
|
||||
|
||||
organizedEventsPage = 1;
|
||||
participationsPage = 1;
|
||||
|
||||
get metadata(): Array<object> {
|
||||
if (!this.person) return [];
|
||||
const res: object[] = [
|
||||
{
|
||||
key: this.$t("Status") as string,
|
||||
value: this.person.suspended ? this.$t("Suspended") : this.$t("Active"),
|
||||
},
|
||||
{
|
||||
key: this.$t("Domain") as string,
|
||||
value: this.person.domain ? this.person.domain : this.$t("Local"),
|
||||
},
|
||||
];
|
||||
if (!this.person.domain && this.person.user) {
|
||||
res.push({
|
||||
key: this.$t("User") as string,
|
||||
link: { name: RouteName.ADMIN_USER_PROFILE, params: { id: this.person.user.id } },
|
||||
value: this.person.user.email,
|
||||
});
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async suspendProfile() {
|
||||
this.$apollo.mutate<{ suspendProfile: { id: string } }>({
|
||||
mutation: SUSPEND_PROFILE,
|
||||
variables: {
|
||||
id: this.id,
|
||||
},
|
||||
update: (store, { data }) => {
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
if (!profileData) return;
|
||||
const { person } = profileData;
|
||||
person.suspended = true;
|
||||
person.avatar = null;
|
||||
person.name = "";
|
||||
person.summary = "";
|
||||
store.writeQuery({
|
||||
query: GET_PERSON,
|
||||
variables: {
|
||||
actorId: profileId,
|
||||
},
|
||||
data: { person },
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async unsuspendProfile() {
|
||||
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(page: number) {
|
||||
this.organizedEventsPage = page;
|
||||
await this.$apollo.queries.person.fetchMore({
|
||||
variables: {
|
||||
actorId: this.id,
|
||||
organizedEventsPage: this.organizedEventsPage,
|
||||
organizedEventsLimit: EVENTS_PER_PAGE,
|
||||
},
|
||||
updateQuery: (previousResult, { fetchMoreResult }) => {
|
||||
if (!fetchMoreResult) return previousResult;
|
||||
const newOrganizedEvents = fetchMoreResult.person.organizedEvents.elements;
|
||||
return {
|
||||
person: {
|
||||
...previousResult.person,
|
||||
organizedEvents: {
|
||||
__typename: previousResult.person.organizedEvents.__typename,
|
||||
total: previousResult.person.organizedEvents.total,
|
||||
elements: [...previousResult.person.organizedEvents.elements, ...newOrganizedEvents],
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async onParticipationsPageChange(page: number) {
|
||||
this.participationsPage = page;
|
||||
await this.$apollo.queries.person.fetchMore({
|
||||
variables: {
|
||||
actorId: this.id,
|
||||
participationPage: this.participationsPage,
|
||||
participationLimit: EVENTS_PER_PAGE,
|
||||
},
|
||||
updateQuery: (previousResult, { fetchMoreResult }) => {
|
||||
if (!fetchMoreResult) return previousResult;
|
||||
const newParticipations = fetchMoreResult.person.participations.elements;
|
||||
return {
|
||||
person: {
|
||||
...previousResult.person,
|
||||
participations: {
|
||||
__typename: previousResult.person.participations.__typename,
|
||||
total: previousResult.person.participations.total,
|
||||
elements: [...previousResult.person.participations.elements, ...newParticipations],
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
table,
|
||||
section {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
</style>
|
||||
165
js/src/views/Admin/AdminUserProfile.vue
Normal file
165
js/src/views/Admin/AdminUserProfile.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<div v-if="user" class="section">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link :to="{ name: RouteName.ADMIN }">{{ $t("Admin") }}</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.USERS,
|
||||
}"
|
||||
>{{ $t("Users") }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.ADMIN_USER_PROFILE,
|
||||
params: { id: user.id },
|
||||
}"
|
||||
>{{ user.email }}</router-link
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<table v-if="metadata.length > 0" class="table is-fullwidth">
|
||||
<tbody>
|
||||
<tr v-for="{ key, value, link, elements } in metadata" :key="key">
|
||||
<td>{{ key }}</td>
|
||||
<td v-if="elements && elements.length > 0">
|
||||
<ul v-for="{ value, link: elementLink, active } in elements" :key="value">
|
||||
<li>
|
||||
<router-link :to="elementLink">
|
||||
<span v-if="active">{{ $t("{profile} (by default)", { profile: value }) }}</span>
|
||||
<span v-else>{{ value }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td v-else-if="elements">
|
||||
{{ $t("None") }}
|
||||
</td>
|
||||
<td v-else-if="link">
|
||||
<router-link :to="link">
|
||||
{{ value }}
|
||||
</router-link>
|
||||
</td>
|
||||
<td v-else>{{ value }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="buttons">
|
||||
<b-button @click="deleteAccount" v-if="!user.disabled" type="is-primary">{{
|
||||
$t("Suspend")
|
||||
}}</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from "vue-property-decorator";
|
||||
import { GET_USER, DELETE_ACCOUNT } from "../../graphql/user";
|
||||
import { usernameWithDomain } from "../../types/actor/actor.model";
|
||||
import RouteName from "../../router/name";
|
||||
import { IUser, ICurrentUserRole } from "../../types/current-user.model";
|
||||
import { IPerson } from "../../types/actor";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
user: {
|
||||
query: GET_USER,
|
||||
variables() {
|
||||
return {
|
||||
id: this.id,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.id;
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class AdminUserProfile extends Vue {
|
||||
@Prop({ required: true }) id!: String;
|
||||
|
||||
user!: IUser;
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
get metadata(): Array<object> {
|
||||
if (!this.user) return [];
|
||||
return [
|
||||
{
|
||||
key: this.$i18n.t("Email"),
|
||||
value: this.user.email,
|
||||
},
|
||||
{
|
||||
key: this.$i18n.t("Language"),
|
||||
value: this.user.locale,
|
||||
},
|
||||
{
|
||||
key: this.$i18n.t("Role"),
|
||||
value: this.roleName(this.user.role),
|
||||
},
|
||||
{
|
||||
key: this.$i18n.t("Login status"),
|
||||
value: this.user.disabled ? this.$i18n.t("Disabled") : this.$t("Activated"),
|
||||
},
|
||||
{
|
||||
key: this.$i18n.t("Profiles"),
|
||||
elements: this.user.actors.map((actor: IPerson) => {
|
||||
return {
|
||||
link: { name: RouteName.ADMIN_PROFILE, params: { id: actor.id } },
|
||||
value: actor.name
|
||||
? `${actor.name} (${actor.preferredUsername})`
|
||||
: actor.preferredUsername,
|
||||
active: this.user.defaultActor ? actor.id === this.user.defaultActor.id : false,
|
||||
};
|
||||
}),
|
||||
},
|
||||
{
|
||||
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: this.$i18n.t("Participations"),
|
||||
value: this.user.participations.total,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteAccount() {
|
||||
await this.$apollo.mutate<{ suspendProfile: { id: string } }>({
|
||||
mutation: DELETE_ACCOUNT,
|
||||
variables: {
|
||||
userId: this.id,
|
||||
},
|
||||
});
|
||||
return this.$router.push({ name: RouteName.USERS });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
table {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
</style>
|
||||
@@ -16,15 +16,17 @@
|
||||
</div>
|
||||
<div class="tile is-parent is-vertical">
|
||||
<article class="tile is-child box">
|
||||
<p class="dashboard-number">{{ dashboard.numberOfUsers }}</p>
|
||||
<p>{{ $t("Users") }}</p>
|
||||
<router-link :to="{ name: RouteName.USERS }">
|
||||
<p class="dashboard-number">{{ dashboard.numberOfUsers }}</p>
|
||||
<p>{{ $t("Users") }}</p>
|
||||
</router-link>
|
||||
</article>
|
||||
<router-link :to="{ name: RouteName.REPORTS }">
|
||||
<article class="tile is-child box">
|
||||
<article class="tile is-child box">
|
||||
<router-link :to="{ name: RouteName.REPORTS }">
|
||||
<p class="dashboard-number">{{ dashboard.numberOfReports }}</p>
|
||||
<p>{{ $t("Opened reports") }}</p>
|
||||
</article>
|
||||
</router-link>
|
||||
</router-link>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tile is-parent" v-if="dashboard.lastPublicEventPublished">
|
||||
@@ -80,4 +82,10 @@ export default class Dashboard extends Vue {
|
||||
font-weight: 700;
|
||||
line-height: 1.125;
|
||||
}
|
||||
|
||||
article.tile {
|
||||
a {
|
||||
color: #4a4a4a;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
138
js/src/views/Admin/Profiles.vue
Normal file
138
js/src/views/Admin/Profiles.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<div v-if="persons">
|
||||
<b-switch v-model="local">{{ $t("Local") }}</b-switch>
|
||||
<b-switch v-model="suspended">{{ $t("Suspended") }}</b-switch>
|
||||
<b-table
|
||||
:data="persons.elements"
|
||||
:loading="$apollo.queries.persons.loading"
|
||||
paginated
|
||||
backend-pagination
|
||||
backend-filtering
|
||||
:total="persons.total"
|
||||
:per-page="PROFILES_PER_PAGE"
|
||||
@page-change="onPageChange"
|
||||
@filters-change="onFiltersChange"
|
||||
>
|
||||
<template slot-scope="props">
|
||||
<b-table-column field="preferredUsername" :label="$t('Username')" searchable>
|
||||
<template slot="searchable" slot-scope="props">
|
||||
<b-input
|
||||
v-model="props.filters.preferredUsername"
|
||||
placeholder="Search..."
|
||||
icon="magnify"
|
||||
size="is-small"
|
||||
/>
|
||||
</template>
|
||||
<router-link :to="{ name: RouteName.ADMIN_PROFILE, 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" />
|
||||
</p>
|
||||
</figure>
|
||||
<div class="media-content">
|
||||
<div class="content">
|
||||
<strong v-if="props.row.name">{{ props.row.name }}</strong
|
||||
><br v-if="props.row.name" />
|
||||
<small>@{{ props.row.preferredUsername }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</router-link>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="domain" :label="$t('Domain')" searchable>
|
||||
<template slot="searchable" slot-scope="props">
|
||||
<b-input
|
||||
v-model="props.filters.domain"
|
||||
placeholder="Search..."
|
||||
icon="magnify"
|
||||
size="is-small"
|
||||
/>
|
||||
</template>
|
||||
{{ props.row.domain }}
|
||||
</b-table-column>
|
||||
</template>
|
||||
<template slot="empty">
|
||||
<section class="section">
|
||||
<div class="content has-text-grey has-text-centered">
|
||||
<p>{{ $t("No profile matches the filters") }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</b-table>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Watch } from "vue-property-decorator";
|
||||
import { LIST_PROFILES } from "../../graphql/actor";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
const PROFILES_PER_PAGE = 10;
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
persons: {
|
||||
query: LIST_PROFILES,
|
||||
variables() {
|
||||
return {
|
||||
preferredUsername: this.preferredUsername,
|
||||
name: this.name,
|
||||
domain: this.domain,
|
||||
local: this.local,
|
||||
suspended: this.suspended,
|
||||
page: 1,
|
||||
limit: PROFILES_PER_PAGE,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class Profiles extends Vue {
|
||||
page = 1;
|
||||
preferredUsername = "";
|
||||
name = "";
|
||||
domain = "";
|
||||
local = true;
|
||||
suspended = false;
|
||||
|
||||
PROFILES_PER_PAGE = PROFILES_PER_PAGE;
|
||||
RouteName = RouteName;
|
||||
|
||||
async onPageChange(page: number) {
|
||||
this.page = page;
|
||||
await this.$apollo.queries.persons.fetchMore({
|
||||
variables: {
|
||||
preferredUsername: this.preferredUsername,
|
||||
name: this.name,
|
||||
domain: this.domain,
|
||||
local: this.local,
|
||||
suspended: this.suspended,
|
||||
page: this.page,
|
||||
limit: PROFILES_PER_PAGE,
|
||||
},
|
||||
updateQuery: (previousResult, { fetchMoreResult }) => {
|
||||
if (!fetchMoreResult) return previousResult;
|
||||
const newProfiles = fetchMoreResult.persons.elements;
|
||||
return {
|
||||
persons: {
|
||||
__typename: previousResult.persons.__typename,
|
||||
total: previousResult.persons.total,
|
||||
elements: [...previousResult.persons.elements, ...newProfiles],
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onFiltersChange({ preferredUsername, domain }: { preferredUsername: string; domain: string }) {
|
||||
this.preferredUsername = preferredUsername;
|
||||
this.domain = domain;
|
||||
}
|
||||
|
||||
@Watch("domain")
|
||||
domainNotLocal() {
|
||||
this.local = this.domain === "";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
132
js/src/views/Admin/Users.vue
Normal file
132
js/src/views/Admin/Users.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<div v-if="users">
|
||||
<b-table
|
||||
:data="users.elements"
|
||||
:loading="$apollo.queries.users.loading"
|
||||
paginated
|
||||
backend-pagination
|
||||
backend-filtering
|
||||
detailed
|
||||
:show-detail-icon="true"
|
||||
:total="users.total"
|
||||
:per-page="USERS_PER_PAGE"
|
||||
:has-detailed-visible="(row => row.actors.length > 0)"
|
||||
@page-change="onPageChange"
|
||||
@filters-change="onFiltersChange"
|
||||
>
|
||||
<template slot-scope="props">
|
||||
<b-table-column field="id" width="40" numeric>
|
||||
{{ props.row.id }}
|
||||
</b-table-column>
|
||||
<b-table-column field="email" :label="$t('Email')" searchable>
|
||||
<template slot="searchable" slot-scope="props">
|
||||
<b-input
|
||||
v-model="props.filters.email"
|
||||
placeholder="Search..."
|
||||
icon="magnify"
|
||||
size="is-small"
|
||||
/>
|
||||
</template>
|
||||
<router-link
|
||||
:to="{ name: RouteName.ADMIN_USER_PROFILE, params: { id: props.row.id } }"
|
||||
:class="{ disabled: props.row.disabled }"
|
||||
>
|
||||
{{ props.row.email }}
|
||||
</router-link>
|
||||
</b-table-column>
|
||||
<b-table-column field="confirmedAt" :label="$t('Confirmed at')" :centered="true">
|
||||
{{ props.row.confirmedAt | formatDateTimeString }}
|
||||
</b-table-column>
|
||||
<b-table-column field="locale" :label="$t('Language')" :centered="true">
|
||||
{{ props.row.locale }}
|
||||
</b-table-column>
|
||||
</template>
|
||||
|
||||
<template slot="detail" slot-scope="props">
|
||||
<router-link
|
||||
v-for="actor in props.row.actors"
|
||||
:key="actor.id"
|
||||
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: actor.id } }"
|
||||
>
|
||||
<article class="media">
|
||||
<figure class="media-left">
|
||||
<p class="image is-64x64">
|
||||
<img :src="actor.avatar.url" />
|
||||
</p>
|
||||
</figure>
|
||||
<div class="media-content">
|
||||
<div class="content">
|
||||
<strong v-if="actor.name">{{ actor.name }}</strong>
|
||||
<small>@{{ actor.preferredUsername }}</small>
|
||||
<p>{{ actor.summary }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</router-link>
|
||||
</template>
|
||||
</b-table>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import { LIST_USERS } from "../../graphql/user";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
const USERS_PER_PAGE = 10;
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
users: {
|
||||
query: LIST_USERS,
|
||||
variables() {
|
||||
return {
|
||||
email: this.email,
|
||||
page: 1,
|
||||
limit: USERS_PER_PAGE,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class Users extends Vue {
|
||||
page = 1;
|
||||
email = "";
|
||||
|
||||
USERS_PER_PAGE = USERS_PER_PAGE;
|
||||
RouteName = RouteName;
|
||||
|
||||
async onPageChange(page: number) {
|
||||
this.page = page;
|
||||
await this.$apollo.queries.users.fetchMore({
|
||||
variables: {
|
||||
email: this.email,
|
||||
page: this.page,
|
||||
limit: USERS_PER_PAGE,
|
||||
},
|
||||
updateQuery: (previousResult, { fetchMoreResult }) => {
|
||||
if (!fetchMoreResult) return previousResult;
|
||||
const newFollowings = fetchMoreResult.users.elements;
|
||||
return {
|
||||
users: {
|
||||
__typename: previousResult.users.__typename,
|
||||
total: previousResult.users.total,
|
||||
elements: [...previousResult.users.elements, ...newFollowings],
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onFiltersChange({ email }: { email: string }) {
|
||||
this.email = email;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../variables.scss";
|
||||
a.disabled {
|
||||
color: $danger;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
</style>
|
||||
@@ -9,7 +9,11 @@
|
||||
tag="span"
|
||||
path="{actor} closed {report}"
|
||||
>
|
||||
<span slot="actor">@{{ log.actor.preferredUsername }}</span>
|
||||
<router-link
|
||||
slot="actor"
|
||||
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.actor.id } }"
|
||||
>@{{ log.actor.preferredUsername }}</router-link
|
||||
>
|
||||
<router-link
|
||||
:to="{ name: RouteName.REPORT, params: { reportId: log.object.id } }"
|
||||
slot="report"
|
||||
@@ -21,7 +25,11 @@
|
||||
tag="span"
|
||||
path="{actor} reopened {report}"
|
||||
>
|
||||
<span slot="actor">@{{ log.actor.preferredUsername }}</span>
|
||||
<router-link
|
||||
slot="actor"
|
||||
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.actor.id } }"
|
||||
>@{{ log.actor.preferredUsername }}</router-link
|
||||
>
|
||||
<router-link
|
||||
:to="{ name: RouteName.REPORT, params: { reportId: log.object.id } }"
|
||||
slot="report"
|
||||
@@ -33,7 +41,11 @@
|
||||
tag="span"
|
||||
path="{actor} marked {report} as resolved"
|
||||
>
|
||||
<span slot="actor">@{{ log.actor.preferredUsername }}</span>
|
||||
<router-link
|
||||
slot="actor"
|
||||
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.actor.id } }"
|
||||
>@{{ log.actor.preferredUsername }}</router-link
|
||||
>
|
||||
<router-link
|
||||
:to="{ name: RouteName.REPORT, params: { reportId: log.object.id } }"
|
||||
slot="report"
|
||||
@@ -45,7 +57,11 @@
|
||||
tag="span"
|
||||
path="{actor} added a note on {report}"
|
||||
>
|
||||
<span slot="actor">@{{ log.actor.preferredUsername }}</span>
|
||||
<router-link
|
||||
slot="actor"
|
||||
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.actor.id } }"
|
||||
>@{{ log.actor.preferredUsername }}</router-link
|
||||
>
|
||||
<router-link
|
||||
v-if="log.object.report"
|
||||
:to="{ name: RouteName.REPORT, params: { reportId: log.object.report.id } }"
|
||||
@@ -59,8 +75,28 @@
|
||||
tag="span"
|
||||
path='{actor} deleted an event named "{title}"'
|
||||
>
|
||||
<span slot="actor">@{{ log.actor.preferredUsername }}</span>
|
||||
<span slot="title">{{ log.object.title }}</span>
|
||||
<router-link
|
||||
slot="actor"
|
||||
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.actor.id } }"
|
||||
>@{{ log.actor.preferredUsername }}</router-link
|
||||
>
|
||||
<b slot="title">{{ log.object.title }}</b>
|
||||
</i18n>
|
||||
<i18n
|
||||
v-else-if="log.action === ActionLogAction.ACTOR_SUSPENSION"
|
||||
tag="span"
|
||||
path="{actor} suspended profile {profile}"
|
||||
>
|
||||
<router-link
|
||||
slot="actor"
|
||||
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.actor.id } }"
|
||||
>@{{ log.actor.preferredUsername }}</router-link
|
||||
>
|
||||
<router-link
|
||||
slot="profile"
|
||||
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.object.id } }"
|
||||
>{{ displayNameAndUsername(log.object) }}
|
||||
</router-link>
|
||||
</i18n>
|
||||
<br />
|
||||
<small>{{ log.insertedAt | formatDateTimeString }}</small>
|
||||
@@ -78,6 +114,7 @@ import { IActionLog, ActionLogAction } from "@/types/report.model";
|
||||
import { LOGS } from "@/graphql/report";
|
||||
import ReportCard from "@/components/Report/ReportCard.vue";
|
||||
import RouteName from "../../router/name";
|
||||
import { displayNameAndUsername } from "../../types/actor";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@@ -95,6 +132,8 @@ export default class ReportList extends Vue {
|
||||
ActionLogAction = ActionLogAction;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
displayNameAndUsername = displayNameAndUsername;
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
<td>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.PROFILE,
|
||||
params: { name: report.reported.preferredUsername },
|
||||
name: RouteName.ADMIN_PROFILE,
|
||||
params: { id: report.reported.id },
|
||||
}"
|
||||
>
|
||||
<img
|
||||
@@ -53,8 +53,8 @@
|
||||
<td v-else>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.PROFILE,
|
||||
params: { name: report.reporter.preferredUsername },
|
||||
name: RouteName.ADMIN_PROFILE,
|
||||
params: { id: report.reporter.id },
|
||||
}"
|
||||
>
|
||||
<img
|
||||
@@ -139,7 +139,7 @@
|
||||
>
|
||||
</div>
|
||||
|
||||
<ul v-for="comment in report.comments" v-if="report.comments.length > 0">
|
||||
<ul v-for="comment in report.comments" v-if="report.comments.length > 0" :key="comment.id">
|
||||
<li>
|
||||
<div class="box" v-if="comment">
|
||||
<article class="media">
|
||||
@@ -173,11 +173,9 @@
|
||||
</ul>
|
||||
|
||||
<h2 class="title" v-if="report.notes.length > 0">{{ $t("Notes") }}</h2>
|
||||
<div class="box note" v-for="note in report.notes" :id="`note-${note.id}`">
|
||||
<div class="box note" v-for="note in report.notes" :id="`note-${note.id}`" :key="note.id">
|
||||
<p>{{ note.content }}</p>
|
||||
<router-link
|
||||
:to="{ name: RouteName.PROFILE, params: { name: note.moderator.preferredUsername } }"
|
||||
>
|
||||
<router-link :to="{ name: RouteName.ADMIN_PROFILE, params: { id: note.moderator.id } }">
|
||||
<img alt class="image" :src="note.moderator.avatar.url" v-if="note.moderator.avatar" />
|
||||
@{{ note.moderator.preferredUsername }}
|
||||
</router-link>
|
||||
|
||||
@@ -122,6 +122,14 @@ export default class Settings extends Vue {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: this.$t("Users") as string,
|
||||
to: { name: RouteName.USERS } as Route,
|
||||
},
|
||||
{
|
||||
title: this.$t("Profiles") as string,
|
||||
to: { name: RouteName.PROFILES } as Route,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user