Fix settings menu

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2020-06-25 11:36:35 +02:00
parent f5543dac70
commit f35fcd761b
23 changed files with 1363 additions and 1291 deletions

View File

@ -17,6 +17,10 @@ a {
}
}
nav.breadcrumb ul li a {
text-decoration: none;
}
input.input {
border-color: $input-border-color !important;
}

View File

@ -1,24 +1,25 @@
<template>
<li class="setting-menu-item" :class="{ active: isActive }">
<router-link v-if="menuItem.to" :to="menuItem.to">
<span>{{ menuItem.title }}</span>
<router-link v-if="to" :to="to">
<span>{{ title }}</span>
</router-link>
<span v-else>{{ menuItem.title }}</span>
<span v-else>{{ title }}</span>
</li>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { ISettingMenuSection } from "@/types/setting-menu.model";
import { Route } from "vue-router";
@Component
export default class SettingMenuItem extends Vue {
@Prop({ required: true, type: Object }) menuItem!: ISettingMenuSection;
@Prop({ required: false, type: String }) title!: string;
@Prop({ required: true, type: Object }) to!: Route;
get isActive() {
if (!this.menuItem.to) return false;
if (this.menuItem.to.name === this.$route.name) {
if (this.menuItem.to.params) {
return this.menuItem.to.params.identityName === this.$route.params.identityName;
if (!this.to) return false;
if (this.to.name === this.$route.name) {
if (this.to.params) {
return this.to.params.identityName === this.$route.params.identityName;
}
return true;
}

View File

@ -1,27 +1,35 @@
<template>
<li :class="{ active: sectionActive }">
<router-link v-if="menuSection.to" :to="menuSection.to">{{ menuSection.title }}</router-link>
<b v-else>{{ menuSection.title }}</b>
<router-link v-if="to" :to="to">{{ title }}</router-link>
<b v-else>{{ title }}</b>
<ul>
<setting-menu-item :menu-item="item" v-for="item in menuSection.items" :key="item.title" />
<slot></slot>
</ul>
</li>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { ISettingMenuSection } from "@/types/setting-menu.model";
import SettingMenuItem from "@/components/Settings/SettingMenuItem.vue";
import { Route } from "vue-router";
@Component({
components: { SettingMenuItem },
})
export default class SettingMenuSection extends Vue {
@Prop({ required: true, type: Object }) menuSection!: ISettingMenuSection;
@Prop({ required: false, type: String }) title!: string;
@Prop({ required: true, type: Object }) to!: Route;
get sectionActive(): boolean | undefined {
return (
this.menuSection.items &&
this.menuSection.items.some(({ to }) => to && to.name === this.$route.name)
);
get sectionActive() {
if (this.$slots.default) {
return this.$slots.default.some(
({
componentOptions: {
// @ts-ignore
propsData: { to },
},
}) => to && to.name === this.$route.name
);
}
return false;
}
}
</script>

View File

@ -1,27 +1,88 @@
<template>
<aside>
<ul>
<SettingMenuSection :title="$t('Account')" :to="{ name: RouteName.ACCOUNT_SETTINGS }">
<SettingMenuItem
:title="this.$t('General')"
:to="{ name: RouteName.ACCOUNT_SETTINGS_GENERAL }"
/>
<SettingMenuItem :title="$t('Preferences')" :to="{ name: RouteName.PREFERENCES }" />
<SettingMenuItem
:title="this.$t('Email notifications')"
:to="{ name: RouteName.NOTIFICATIONS }"
/>
</SettingMenuSection>
<SettingMenuSection :title="$t('Profiles')" :to="{ name: RouteName.IDENTITIES }">
<SettingMenuItem
v-for="profile in identities"
:key="profile.preferredUsername"
:title="profile.preferredUsername"
:to="{
name: RouteName.UPDATE_IDENTITY,
params: { identityName: profile.preferredUsername },
}"
/>
<SettingMenuItem :title="$t('New profile')" :to="{ name: RouteName.CREATE_IDENTITY }" />
</SettingMenuSection>
<SettingMenuSection
v-for="section in menuValue"
:key="section.title"
:menu-section="section"
/>
v-if="
[ICurrentUserRole.MODERATOR, ICurrentUserRole.ADMINISTRATOR].includes(
this.currentUser.role
)
"
:title="$t('Moderation')"
:to="{ name: RouteName.MODERATION }"
>
<SettingMenuItem :title="$t('Reports')" :to="{ name: RouteName.REPORTS }" />
<SettingMenuItem :title="$t('Moderation log')" :to="{ name: RouteName.REPORT_LOGS }" />
<SettingMenuItem :title="$t('Users')" :to="{ name: RouteName.USERS }" />
<SettingMenuItem :title="$t('Profiles')" :to="{ name: RouteName.PROFILES }" />
</SettingMenuSection>
<SettingMenuSection
v-if="this.currentUser.role == ICurrentUserRole.ADMINISTRATOR"
:title="$t('Admin')"
:to="{ name: RouteName.ADMIN }"
>
<SettingMenuItem :title="$t('Dashboard')" :to="{ name: RouteName.ADMIN_DASHBOARD }" />
<SettingMenuItem
:title="$t('Instance settings')"
:to="{ name: RouteName.ADMIN_SETTINGS }"
/>
<SettingMenuItem :title="$t('Federation')" :to="{ name: RouteName.RELAYS }" />
</SettingMenuSection>
</ul>
</aside>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import SettingMenuSection from "@/components/Settings/SettingMenuSection.vue";
import { ISettingMenuSection } from "@/types/setting-menu.model";
import SettingMenuSection from "./SettingMenuSection.vue";
import SettingMenuItem from "./SettingMenuItem.vue";
import { IDENTITIES } from "../../graphql/actor";
import { IPerson, Person } from "../../types/actor";
import { CURRENT_USER_CLIENT } from "../../graphql/user";
import { ICurrentUser, ICurrentUserRole } from "../../types/current-user.model";
import RouteName from "../../router/name";
@Component({
components: { SettingMenuSection },
components: { SettingMenuSection, SettingMenuItem },
apollo: {
identities: {
query: IDENTITIES,
update: (data) => data.identities.map((identity: IPerson) => new Person(identity)),
},
currentUser: CURRENT_USER_CLIENT,
},
})
export default class SettingsMenu extends Vue {
@Prop({ required: true, type: Array }) menu!: ISettingMenuSection[];
profiles = [];
get menuValue() {
return this.menu;
}
currentUser!: ICurrentUser;
identities!: IPerson[];
ICurrentUserRole = ICurrentUserRole;
RouteName = RouteName;
}
</script>
<style lang="scss" scoped>

View File

@ -106,6 +106,7 @@ export const USER_SETTINGS_FRAGMENT = gql`
export const USER_SETTINGS = gql`
query UserSetting {
loggedUser {
id
locale
settings {
...UserSettingFragment

View File

@ -695,5 +695,6 @@
"contact uninformed": "contact uninformed",
"Can be an email or a link, or just plain text.": "Can be an email or a link, or just plain text.",
"No profiles found": "No profiles found",
"URL copied to clipboard": "URL copied to clipboard"
"URL copied to clipboard": "URL copied to clipboard",
"Report #{reportNumber}": "Report #{reportNumber}"
}

View File

@ -695,5 +695,6 @@
"A place for your code of conduct, rules or guidelines. You can use HTML tags.": "Une section appropriée pour votre code de conduite, règles ou lignes directrices. Vous pouvez utiliser des balises HTML.",
"contact uninformed": "contact non renseigné",
"Can be an email or a link, or just plain text.": "Peut être une adresse email ou bien un lien, ou alors du simple texte brut.",
"URL copied to clipboard": "URL copiée dans le presse-papiers"
"URL copied to clipboard": "URL copiée dans le presse-papiers",
"Report #{reportNumber}": "Signalement #{reportNumber}"
}

View File

@ -1,8 +0,0 @@
import { Route } from "vue-router";
export interface ISettingMenuSection {
title: string;
to: Route;
items?: ISettingMenuSection[];
parents?: ISettingMenuSection[];
}

View File

@ -1,66 +1,91 @@
<template>
<div class="root" v-if="identity">
<h1 class="title">
<span v-if="isUpdate">{{ identity.displayName() }}</span>
<span v-else>{{ $t("I create an identity") }}</span>
</h1>
<div>
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<router-link :to="{ name: RouteName.IDENTITIES }">{{ $t("Profiles") }}</router-link>
</li>
<li class="is-active" v-if="isUpdate && identity">
<router-link
:to="{
name: RouteName.UPDATE_IDENTITY,
params: { identityName: identity.preferredUsername },
}"
>{{ identity.name }}</router-link
>
</li>
<li class="is-active" v-else>
<router-link :to="{ name: RouteName.CREATE_IDENTITY }">{{
$t("New profile")
}}</router-link>
</li>
</ul>
</nav>
<div class="root" v-if="identity">
<h1 class="title">
<span v-if="isUpdate">{{ identity.displayName() }}</span>
<span v-else>{{ $t("I create an identity") }}</span>
</h1>
<picture-upload v-model="avatarFile" class="picture-upload" />
<picture-upload v-model="avatarFile" class="picture-upload" />
<b-field horizontal :label="$t('Display name')">
<b-input
aria-required="true"
required
v-model="identity.name"
@input="autoUpdateUsername($event)"
/>
</b-field>
<b-field
horizontal
custom-class="username-field"
expanded
:label="$t('Username')"
:message="message"
>
<b-field expanded>
<b-field horizontal :label="$t('Display name')">
<b-input
aria-required="true"
required
v-model="identity.preferredUsername"
:disabled="isUpdate"
:use-html5-validation="!isUpdate"
pattern="[a-z0-9_]+"
v-model="identity.name"
@input="autoUpdateUsername($event)"
/>
<p class="control">
<span class="button is-static">@{{ getInstanceHost }}</span>
</p>
</b-field>
</b-field>
<b-field horizontal :label="$t('Description')">
<b-input type="textarea" aria-required="false" v-model="identity.summary" />
</b-field>
<b-field
horizontal
custom-class="username-field"
expanded
:label="$t('Username')"
:message="message"
>
<b-field expanded>
<b-input
aria-required="true"
required
v-model="identity.preferredUsername"
:disabled="isUpdate"
:use-html5-validation="!isUpdate"
pattern="[a-z0-9_]+"
/>
<b-notification
type="is-danger"
has-icon
aria-close-label="Close notification"
role="alert"
:key="error"
v-for="error in errors"
>{{ error }}</b-notification
>
<p class="control">
<span class="button is-static">@{{ getInstanceHost }}</span>
</p>
</b-field>
</b-field>
<b-field class="submit">
<div class="control">
<button type="button" class="button is-primary" @click="submit()">{{ $t("Save") }}</button>
<b-field horizontal :label="$t('Description')">
<b-input type="textarea" aria-required="false" v-model="identity.summary" />
</b-field>
<b-notification
type="is-danger"
has-icon
aria-close-label="Close notification"
role="alert"
:key="error"
v-for="error in errors"
>{{ error }}</b-notification
>
<b-field class="submit">
<div class="control">
<button type="button" class="button is-primary" @click="submit()">
{{ $t("Save") }}
</button>
</div>
</b-field>
<div class="delete-identity" v-if="isUpdate">
<span @click="openDeleteIdentityConfirmation()">{{ $t("Delete this identity") }}</span>
</div>
</b-field>
<div class="delete-identity" v-if="isUpdate">
<span @click="openDeleteIdentityConfirmation()">{{ $t("Delete this identity") }}</span>
</div>
</div>
</template>
@ -148,6 +173,8 @@ export default class EditIdentity extends mixins(identityEditionMixin) {
private currentActor: IPerson | null = null;
RouteName = RouteName;
get message() {
if (this.isUpdate) return null;
return this.$t("Only alphanumeric characters and underscores are supported.");

View File

@ -1,53 +1,65 @@
<template>
<section>
<h1 class="title">{{ $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>{{ $t("Published events") }}</p>
</article>
<article class="tile is-child box">
<p class="dashboard-number">{{ dashboard.numberOfComments }}</p>
<p>{{ $t("Comments") }}</p>
</article>
<div>
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<router-link :to="{ name: RouteName.ADMIN }">{{ $t("Admin") }}</router-link>
</li>
<li class="is-active">
<router-link :to="{ name: RouteName.ADMIN_DASHBOARD }">{{ $t("Dashboard") }}</router-link>
</li>
</ul>
</nav>
<section>
<h1 class="title">{{ $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>{{ $t("Published events") }}</p>
</article>
<article class="tile is-child box">
<p class="dashboard-number">{{ dashboard.numberOfComments }}</p>
<p>{{ $t("Comments") }}</p>
</article>
</div>
<div class="tile is-parent is-vertical">
<article class="tile is-child box">
<router-link :to="{ name: RouteName.USERS }">
<p class="dashboard-number">{{ dashboard.numberOfUsers }}</p>
<p>{{ $t("Users") }}</p>
</router-link>
</article>
<article class="tile is-child box">
<router-link :to="{ name: RouteName.REPORTS }">
<p class="dashboard-number">{{ dashboard.numberOfReports }}</p>
<p>{{ $t("Opened reports") }}</p>
</router-link>
</article>
</div>
</div>
<div class="tile is-parent is-vertical">
<article class="tile is-child box">
<router-link :to="{ name: RouteName.USERS }">
<p class="dashboard-number">{{ dashboard.numberOfUsers }}</p>
<p>{{ $t("Users") }}</p>
</router-link>
</article>
<article class="tile is-child box">
<router-link :to="{ name: RouteName.REPORTS }">
<p class="dashboard-number">{{ dashboard.numberOfReports }}</p>
<p>{{ $t("Opened reports") }}</p>
</router-link>
</article>
<div class="tile is-parent" v-if="dashboard.lastPublicEventPublished">
<router-link
:to="{
name: RouteName.EVENT,
params: { uuid: dashboard.lastPublicEventPublished.uuid },
}"
>
<article class="tile is-child box">
<p class="dashboard-number">{{ $t("Last published event") }}</p>
<p class="subtitle">{{ dashboard.lastPublicEventPublished.title }}</p>
<figure class="image is-4by3" v-if="dashboard.lastPublicEventPublished.picture">
<img :src="dashboard.lastPublicEventPublished.picture.url" />
</figure>
</article>
</router-link>
</div>
</div>
<div class="tile is-parent" v-if="dashboard.lastPublicEventPublished">
<router-link
:to="{
name: RouteName.EVENT,
params: { uuid: dashboard.lastPublicEventPublished.uuid },
}"
>
<article class="tile is-child box">
<p class="dashboard-number">{{ $t("Last published event") }}</p>
<p class="subtitle">{{ dashboard.lastPublicEventPublished.title }}</p>
<figure class="image is-4by3" v-if="dashboard.lastPublicEventPublished.picture">
<img :src="dashboard.lastPublicEventPublished.picture.url" />
</figure>
</article>
</router-link>
</div>
</div>
</div>
</section>
</section>
</div>
</template>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";

View File

@ -1,40 +1,60 @@
<template>
<section>
<h1 class="title">{{ $t("Instances") }}</h1>
<div class="tabs is-boxed">
<div>
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<router-link
tag="li"
active-class="is-active"
:to="{ name: RouteName.RELAY_FOLLOWINGS }"
exact
>
<a>
<b-icon icon="inbox-arrow-down"></b-icon>
<span>
{{ $t("Followings") }}
<b-tag rounded>{{ relayFollowings.total }}</b-tag>
</span>
</a>
</router-link>
<router-link
tag="li"
active-class="is-active"
:to="{ name: RouteName.RELAY_FOLLOWERS }"
exact
>
<a>
<b-icon icon="inbox-arrow-up"></b-icon>
<span>
{{ $t("Followers") }}
<b-tag rounded>{{ relayFollowers.total }}</b-tag>
</span>
</a>
</router-link>
<li>
<router-link :to="{ name: RouteName.ADMIN }">{{ $t("Admin") }}</router-link>
</li>
<li>
<router-link :to="{ name: RouteName.RELAYS }">{{ $t("Federation") }}</router-link>
</li>
<li class="is-active" v-if="$route.name == RouteName.RELAY_FOLLOWINGS">
<router-link :to="{ name: RouteName.RELAY_FOLLOWINGS }">{{
$t("Followings")
}}</router-link>
</li>
<li class="is-active" v-if="$route.name == RouteName.RELAY_FOLLOWERS">
<router-link :to="{ name: RouteName.RELAY_FOLLOWERS }">{{ $t("Followers") }}</router-link>
</li>
</ul>
</div>
<router-view></router-view>
</section>
</nav>
<section>
<h1 class="title">{{ $t("Instances") }}</h1>
<div class="tabs is-boxed">
<ul>
<router-link
tag="li"
active-class="is-active"
:to="{ name: RouteName.RELAY_FOLLOWINGS }"
exact
>
<a>
<b-icon icon="inbox-arrow-down"></b-icon>
<span>
{{ $t("Followings") }}
<b-tag rounded>{{ relayFollowings.total }}</b-tag>
</span>
</a>
</router-link>
<router-link
tag="li"
active-class="is-active"
:to="{ name: RouteName.RELAY_FOLLOWERS }"
exact
>
<a>
<b-icon icon="inbox-arrow-up"></b-icon>
<span>
{{ $t("Followers") }}
<b-tag rounded>{{ relayFollowers.total }}</b-tag>
</span>
</a>
</router-link>
</ul>
</div>
<router-view></router-view>
</section>
</div>
</template>
<script lang="ts">

View File

@ -1,69 +1,81 @@
<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
class="profile"
: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>
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<router-link :to="{ name: RouteName.MODERATION }">{{ $t("Moderation") }}</router-link>
</li>
<li class="is-active">
<router-link :to="{ name: RouteName.PROFILES }">{{ $t("Profiles") }}</router-link>
</li>
</ul>
</nav>
<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
class="profile"
: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>
</div>
</article>
</router-link>
</b-table-column>
</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>
<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>
</div>
</template>
<script lang="ts">

View File

@ -1,241 +1,259 @@
<template>
<section v-if="adminSettings">
<form @submit.prevent="updateSettings">
<b-field :label="$t('Instance Name')">
<b-input v-model="adminSettings.instanceName" />
</b-field>
<div class="field">
<label class="label has-help">{{ $t("Instance Short Description") }}</label>
<small>
{{
$t(
"Displayed on homepage and meta tags. Describe what Mobilizon is and what makes this instance special in a single paragraph."
)
}}
</small>
<b-input type="textarea" v-model="adminSettings.instanceDescription" rows="2" />
</div>
<div class="field">
<label class="label has-help">{{ $t("Contact") }}</label>
<small>
{{ $t("Can be an email or a link, or just plain text.") }}
</small>
<b-input v-model="adminSettings.contact" />
</div>
<b-field :label="$t('Allow registrations')">
<b-switch v-model="adminSettings.registrationsOpen">
<p class="content" v-if="adminSettings.registrationsOpen">
{{ $t("Registration is allowed, anyone can register.") }}
</p>
<p class="content" v-else>{{ $t("Registration is closed.") }}</p>
</b-switch>
</b-field>
<div class="field">
<label class="label has-help">{{ $t("Instance Long Description") }}</label>
<small>
{{
$t(
"A place to explain who you are and the things that set your instance apart. You can use HTML tags."
)
}}
</small>
<b-input type="textarea" v-model="adminSettings.instanceLongDescription" rows="4" />
</div>
<div class="field">
<label class="label has-help">{{ $t("Instance Rules") }}</label>
<small>
{{ $t("A place for your code of conduct, rules or guidelines. You can use HTML tags.") }}
</small>
<b-input type="textarea" v-model="adminSettings.instanceRules" />
</div>
<b-field :label="$t('Instance Terms Source')">
<div class="columns">
<div class="column is-one-third-desktop">
<b-field>
<b-radio
v-model="adminSettings.instanceTermsType"
name="instanceTermsType"
:native-value="InstanceTermsType.DEFAULT"
>{{ $t("Default Mobilizon terms") }}</b-radio
>
</b-field>
<b-field>
<b-radio
v-model="adminSettings.instanceTermsType"
name="instanceTermsType"
:native-value="InstanceTermsType.URL"
>{{ $t("Custom URL") }}</b-radio
>
</b-field>
<b-field>
<b-radio
v-model="adminSettings.instanceTermsType"
name="instanceTermsType"
:native-value="InstanceTermsType.CUSTOM"
>{{ $t("Custom text") }}</b-radio
>
</b-field>
</div>
<div class="column">
<div
class="notification"
v-if="adminSettings.instanceTermsType === InstanceTermsType.DEFAULT"
>
<b>{{ $t("Default") }}</b>
<i18n
tag="p"
class="content"
path="The {default_terms} will be used. They will be translated in the user's language."
>
<a
slot="default_terms"
href="https://mobilizon.org/terms"
target="_blank"
rel="noopener"
>{{ $t("default Mobilizon terms") }}</a
>
</i18n>
<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."
)
}}</b>
</div>
<div
class="notification"
v-if="adminSettings.instanceTermsType === InstanceTermsType.URL"
>
<b>{{ $t("URL") }}</b>
<p class="content">{{ $t("Set an URL to a page with your own terms.") }}</p>
</div>
<div
class="notification"
v-if="adminSettings.instanceTermsType === InstanceTermsType.CUSTOM"
>
<b>{{ $t("Custom") }}</b>
<i18n
tag="p"
class="content"
path="Enter your own terms. HTML tags allowed. The {mobilizon_terms} are provided as template."
>
<a
slot="mobilizon_terms"
href="https://mobilizon.org/terms"
target="_blank"
rel="noopener"
>
{{ $t("default Mobilizon terms") }}</a
>
</i18n>
</div>
</div>
<div>
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<router-link :to="{ name: RouteName.ADMIN }">{{ $t("Admin") }}</router-link>
</li>
<li class="is-active">
<router-link :to="{ name: RouteName.ADMIN_SETTINGS }">{{
$t("Instance settings")
}}</router-link>
</li>
</ul>
</nav>
<section v-if="adminSettings">
<form @submit.prevent="updateSettings">
<b-field :label="$t('Instance Name')">
<b-input v-model="adminSettings.instanceName" />
</b-field>
<div class="field">
<label class="label has-help">{{ $t("Instance Short Description") }}</label>
<small>
{{
$t(
"Displayed on homepage and meta tags. Describe what Mobilizon is and what makes this instance special in a single paragraph."
)
}}
</small>
<b-input type="textarea" v-model="adminSettings.instanceDescription" rows="2" />
</div>
</b-field>
<b-field
:label="$t('Instance Terms URL')"
v-if="adminSettings.instanceTermsType === InstanceTermsType.URL"
>
<b-input type="URL" v-model="adminSettings.instanceTermsUrl" />
</b-field>
<b-field
:label="$t('Instance Terms')"
v-if="adminSettings.instanceTermsType === InstanceTermsType.CUSTOM"
>
<b-input type="textarea" v-model="adminSettings.instancePrivacyPolicy" />
</b-field>
<b-field :label="$t('Instance Privacy Policy Source')">
<div class="columns">
<div class="column is-one-third-desktop">
<b-field>
<b-radio
v-model="adminSettings.instancePrivacyPolicyType"
name="instancePrivacyType"
:native-value="InstancePrivacyType.DEFAULT"
>{{ $t("Default Mobilizon privacy policy") }}</b-radio
>
</b-field>
<b-field>
<b-radio
v-model="adminSettings.instancePrivacyPolicyType"
name="instancePrivacyType"
:native-value="InstancePrivacyType.URL"
>{{ $t("Custom URL") }}</b-radio
>
</b-field>
<b-field>
<b-radio
v-model="adminSettings.instancePrivacyPolicyType"
name="instancePrivacyType"
:native-value="InstancePrivacyType.CUSTOM"
>{{ $t("Custom text") }}</b-radio
>
</b-field>
</div>
<div class="column">
<div
class="notification"
v-if="adminSettings.instancePrivacyPolicyType === InstancePrivacyType.DEFAULT"
>
<b>{{ $t("Default") }}</b>
<i18n
tag="p"
class="content"
path="The {default_privacy_policy} will be used. They will be translated in the user's language."
>
<a
slot="default_privacy_policy"
href="https://mobilizon.org/terms"
target="_blank"
rel="noopener"
>{{ $t("default Mobilizon privacy policy") }}</a
>
</i18n>
</div>
<div
class="notification"
v-if="adminSettings.instancePrivacyPolicyType === InstancePrivacyType.URL"
>
<b>{{ $t("URL") }}</b>
<p class="content">{{ $t("Set an URL to a page with your own privacy policy.") }}</p>
</div>
<div
class="notification"
v-if="adminSettings.instancePrivacyPolicyType === InstancePrivacyType.CUSTOM"
>
<b>{{ $t("Custom") }}</b>
<i18n
tag="p"
class="content"
path="Enter your own privacy policy. HTML tags allowed. The {mobilizon_privacy_policy} is provided as template."
>
<a
slot="mobilizon_privacy_policy"
href="https://mobilizon.org/privacy"
target="_blank"
rel="noopener"
>
{{ $t("default Mobilizon privacy policy") }}</a
>
</i18n>
</div>
</div>
<div class="field">
<label class="label has-help">{{ $t("Contact") }}</label>
<small>
{{ $t("Can be an email or a link, or just plain text.") }}
</small>
<b-input v-model="adminSettings.contact" />
</div>
</b-field>
<b-field
:label="$t('Instance Privacy Policy URL')"
v-if="adminSettings.instancePrivacyPolicyType === InstancePrivacyType.URL"
>
<b-input type="URL" v-model="adminSettings.instancePrivacyPolicyUrl" />
</b-field>
<b-field
:label="$t('Instance Privacy Policy')"
v-if="adminSettings.instancePrivacyPolicyType === InstancePrivacyType.CUSTOM"
>
<b-input type="textarea" v-model="adminSettings.instancePrivacyPolicy" />
</b-field>
<b-button native-type="submit" type="is-primary">{{ $t("Save") }}</b-button>
</form>
</section>
<b-field :label="$t('Allow registrations')">
<b-switch v-model="adminSettings.registrationsOpen">
<p class="content" v-if="adminSettings.registrationsOpen">
{{ $t("Registration is allowed, anyone can register.") }}
</p>
<p class="content" v-else>{{ $t("Registration is closed.") }}</p>
</b-switch>
</b-field>
<div class="field">
<label class="label has-help">{{ $t("Instance Long Description") }}</label>
<small>
{{
$t(
"A place to explain who you are and the things that set your instance apart. You can use HTML tags."
)
}}
</small>
<b-input type="textarea" v-model="adminSettings.instanceLongDescription" rows="4" />
</div>
<div class="field">
<label class="label has-help">{{ $t("Instance Rules") }}</label>
<small>
{{
$t("A place for your code of conduct, rules or guidelines. You can use HTML tags.")
}}
</small>
<b-input type="textarea" v-model="adminSettings.instanceRules" />
</div>
<b-field :label="$t('Instance Terms Source')">
<div class="columns">
<div class="column is-one-third-desktop">
<b-field>
<b-radio
v-model="adminSettings.instanceTermsType"
name="instanceTermsType"
:native-value="InstanceTermsType.DEFAULT"
>{{ $t("Default Mobilizon terms") }}</b-radio
>
</b-field>
<b-field>
<b-radio
v-model="adminSettings.instanceTermsType"
name="instanceTermsType"
:native-value="InstanceTermsType.URL"
>{{ $t("Custom URL") }}</b-radio
>
</b-field>
<b-field>
<b-radio
v-model="adminSettings.instanceTermsType"
name="instanceTermsType"
:native-value="InstanceTermsType.CUSTOM"
>{{ $t("Custom text") }}</b-radio
>
</b-field>
</div>
<div class="column">
<div
class="notification"
v-if="adminSettings.instanceTermsType === InstanceTermsType.DEFAULT"
>
<b>{{ $t("Default") }}</b>
<i18n
tag="p"
class="content"
path="The {default_terms} will be used. They will be translated in the user's language."
>
<a
slot="default_terms"
href="https://mobilizon.org/terms"
target="_blank"
rel="noopener"
>{{ $t("default Mobilizon terms") }}</a
>
</i18n>
<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."
)
}}</b>
</div>
<div
class="notification"
v-if="adminSettings.instanceTermsType === InstanceTermsType.URL"
>
<b>{{ $t("URL") }}</b>
<p class="content">{{ $t("Set an URL to a page with your own terms.") }}</p>
</div>
<div
class="notification"
v-if="adminSettings.instanceTermsType === InstanceTermsType.CUSTOM"
>
<b>{{ $t("Custom") }}</b>
<i18n
tag="p"
class="content"
path="Enter your own terms. HTML tags allowed. The {mobilizon_terms} are provided as template."
>
<a
slot="mobilizon_terms"
href="https://mobilizon.org/terms"
target="_blank"
rel="noopener"
>
{{ $t("default Mobilizon terms") }}</a
>
</i18n>
</div>
</div>
</div>
</b-field>
<b-field
:label="$t('Instance Terms URL')"
v-if="adminSettings.instanceTermsType === InstanceTermsType.URL"
>
<b-input type="URL" v-model="adminSettings.instanceTermsUrl" />
</b-field>
<b-field
:label="$t('Instance Terms')"
v-if="adminSettings.instanceTermsType === InstanceTermsType.CUSTOM"
>
<b-input type="textarea" v-model="adminSettings.instancePrivacyPolicy" />
</b-field>
<b-field :label="$t('Instance Privacy Policy Source')">
<div class="columns">
<div class="column is-one-third-desktop">
<b-field>
<b-radio
v-model="adminSettings.instancePrivacyPolicyType"
name="instancePrivacyType"
:native-value="InstancePrivacyType.DEFAULT"
>{{ $t("Default Mobilizon privacy policy") }}</b-radio
>
</b-field>
<b-field>
<b-radio
v-model="adminSettings.instancePrivacyPolicyType"
name="instancePrivacyType"
:native-value="InstancePrivacyType.URL"
>{{ $t("Custom URL") }}</b-radio
>
</b-field>
<b-field>
<b-radio
v-model="adminSettings.instancePrivacyPolicyType"
name="instancePrivacyType"
:native-value="InstancePrivacyType.CUSTOM"
>{{ $t("Custom text") }}</b-radio
>
</b-field>
</div>
<div class="column">
<div
class="notification"
v-if="adminSettings.instancePrivacyPolicyType === InstancePrivacyType.DEFAULT"
>
<b>{{ $t("Default") }}</b>
<i18n
tag="p"
class="content"
path="The {default_privacy_policy} will be used. They will be translated in the user's language."
>
<a
slot="default_privacy_policy"
href="https://mobilizon.org/terms"
target="_blank"
rel="noopener"
>{{ $t("default Mobilizon privacy policy") }}</a
>
</i18n>
</div>
<div
class="notification"
v-if="adminSettings.instancePrivacyPolicyType === InstancePrivacyType.URL"
>
<b>{{ $t("URL") }}</b>
<p class="content">
{{ $t("Set an URL to a page with your own privacy policy.") }}
</p>
</div>
<div
class="notification"
v-if="adminSettings.instancePrivacyPolicyType === InstancePrivacyType.CUSTOM"
>
<b>{{ $t("Custom") }}</b>
<i18n
tag="p"
class="content"
path="Enter your own privacy policy. HTML tags allowed. The {mobilizon_privacy_policy} is provided as template."
>
<a
slot="mobilizon_privacy_policy"
href="https://mobilizon.org/privacy"
target="_blank"
rel="noopener"
>
{{ $t("default Mobilizon privacy policy") }}</a
>
</i18n>
</div>
</div>
</div>
</b-field>
<b-field
:label="$t('Instance Privacy Policy URL')"
v-if="adminSettings.instancePrivacyPolicyType === InstancePrivacyType.URL"
>
<b-input type="URL" v-model="adminSettings.instancePrivacyPolicyUrl" />
</b-field>
<b-field
:label="$t('Instance Privacy Policy')"
v-if="adminSettings.instancePrivacyPolicyType === InstancePrivacyType.CUSTOM"
>
<b-input type="textarea" v-model="adminSettings.instancePrivacyPolicy" />
</b-field>
<b-button native-type="submit" type="is-primary">{{ $t("Save") }}</b-button>
</form>
</section>
</div>
</template>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";

View File

@ -1,77 +1,89 @@
<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
class="user-profile"
: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">
<template v-if="props.row.confirmedAt">
{{ props.row.confirmedAt | formatDateTimeString }}
</template>
<template v-else>
{{ $t("Not confirmed") }}
</template>
</b-table-column>
<b-table-column field="locale" :label="$t('Language')" :centered="true">
{{ props.row.locale }}
</b-table-column>
</template>
<div>
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<router-link :to="{ name: RouteName.MODERATION }">{{ $t("Moderation") }}</router-link>
</li>
<li class="is-active">
<router-link :to="{ name: RouteName.USERS }">{{ $t("Users") }}</router-link>
</li>
</ul>
</nav>
<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
class="user-profile"
: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">
<template v-if="props.row.confirmedAt">
{{ props.row.confirmedAt | formatDateTimeString }}
</template>
<template v-else>
{{ $t("Not confirmed") }}
</template>
</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
class="profile"
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>
<template slot="detail" slot-scope="props">
<router-link
class="profile"
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>
</div>
</article>
</router-link>
</template>
</b-table>
</article>
</router-link>
</template>
</b-table>
</div>
</div>
</template>
<script lang="ts">

View File

@ -1,20 +1,37 @@
<template>
<section class="container section" v-if="group">
<form @submit.prevent="inviteMember">
<b-field :label="$t('Invite a new member')" custom-class="add-relay" horizontal>
<b-field grouped expanded size="is-large">
<p class="control">
<b-input v-model="newMemberUsername" :placeholder="$t('Ex: someone@mobilizon.org')" />
</p>
<p class="control">
<b-button type="is-primary" native-type="submit">{{ $t("Invite member") }}</b-button>
</p>
<div>
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<router-link :to="{ name: RouteName.GROUP }">{{ group.name }}</router-link>
</li>
<li>
<router-link :to="{ name: RouteName.GROUP_SETTINGS }">{{ $t("Settings") }}</router-link>
</li>
<li class="is-active">
<router-link :to="{ name: RouteName.GROUP_MEMBERS_SETTINGS }">{{
$t("Members")
}}</router-link>
</li>
</ul>
</nav>
<section class="container section" v-if="group">
<form @submit.prevent="inviteMember">
<b-field :label="$t('Invite a new member')" custom-class="add-relay" horizontal>
<b-field grouped expanded size="is-large">
<p class="control">
<b-input v-model="newMemberUsername" :placeholder="$t('Ex: someone@mobilizon.org')" />
</p>
<p class="control">
<b-button type="is-primary" native-type="submit">{{ $t("Invite member") }}</b-button>
</p>
</b-field>
</b-field>
</b-field>
</form>
<h1>{{ $t("Group Members") }} ({{ group.members.total }})</h1>
<pre>{{ group.members }}</pre>
</section>
</form>
<h1>{{ $t("Group Members") }} ({{ group.members.total }})</h1>
<pre>{{ group.members }}</pre>
</section>
</div>
</template>
<script lang="ts">

View File

@ -2,19 +2,21 @@
<aside class="section container">
<h1 class="title">{{ $t("Settings") }}</h1>
<div class="columns">
<SettingsMenu class="column is-one-quarter-desktop" :menu="menu" />
<aside class="column is-one-quarter-desktop">
<ul>
<SettingMenuSection :title="$t('Settings')" :to="{ name: RouteName.GROUP_SETTINGS }">
<SettingMenuItem
:title="this.$t('Public')"
:to="{ name: RouteName.GROUP_PUBLIC_SETTINGS }"
/>
<SettingMenuItem
:title="this.$t('Members')"
:to="{ name: RouteName.GROUP_MEMBERS_SETTINGS }"
/>
</SettingMenuSection>
</ul>
</aside>
<div class="column">
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li
v-for="route in routes.get($route.name)"
:class="{ 'is-active': route.to.name === $route.name }"
:key="route.title"
>
<router-link :to="{ name: route.to.name }">{{ route.title }}</router-link>
</li>
</ul>
</nav>
<router-view />
</div>
</div>
@ -22,70 +24,20 @@
</template>
<script lang="ts">
import { Component, Vue, Watch } from "vue-property-decorator";
import SettingsMenu from "@/components/Settings/SettingsMenu.vue";
import { ISettingMenuSection } from "@/types/setting-menu.model";
import { Route } from "vue-router";
import { IGroup, IPerson } from "@/types/actor";
import { FETCH_GROUP } from "@/graphql/actor";
import RouteName from "../../router/name";
import SettingMenuSection from "../../components/Settings/SettingMenuSection.vue";
import SettingMenuItem from "../../components/Settings/SettingMenuItem.vue";
@Component({
components: { SettingsMenu },
apollo: {
group: {
query: FETCH_GROUP,
},
},
components: { SettingMenuSection, SettingMenuItem },
})
export default class Settings extends Vue {
RouteName = RouteName;
menu: ISettingMenuSection[] = [];
group!: IGroup[];
mounted() {
this.menu = [
{
title: this.$t("Settings") as string,
to: { name: RouteName.GROUP_SETTINGS } as Route,
items: [
{
title: this.$t("Public") as string,
to: { name: RouteName.GROUP_PUBLIC_SETTINGS } as Route,
},
{
title: this.$t("Members") as string,
to: { name: RouteName.GROUP_MEMBERS_SETTINGS } as Route,
},
],
},
];
}
get routes(): Map<string, Route[]> {
return this.getPath(this.menu);
}
getPath(object: ISettingMenuSection[]) {
function iter(menu: ISettingMenuSection[] | ISettingMenuSection, acc: ISettingMenuSection[]) {
if (Array.isArray(menu)) {
return menu.forEach((item: ISettingMenuSection) => {
iter(item, acc.concat(item));
});
}
if (menu.items && menu.items.length > 0) {
return menu.items.forEach((item: ISettingMenuSection) => {
iter(item, acc.concat(item));
});
}
result.set(menu.to.name, acc);
}
const result = new Map();
iter(object, []);
return result;
}
}
</script>

View File

@ -1,146 +1,160 @@
<template>
<section>
<ul v-if="actionLogs.length > 0">
<li v-for="log in actionLogs" :key="log.id">
<div class="box">
<img class="image" :src="log.actor.avatar.url" v-if="log.actor.avatar" />
<i18n
v-if="log.action === ActionLogAction.REPORT_UPDATE_CLOSED"
tag="span"
path="{moderator} closed {report}"
>
<router-link
slot="moderator"
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.actor.id } }"
>@{{ log.actor.preferredUsername }}</router-link
<div>
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<router-link :to="{ name: RouteName.MODERATION }">{{ $t("Moderation") }}</router-link>
</li>
<li class="is-active">
<router-link :to="{ name: RouteName.REPORT_LOGS }">{{
$t("Moderation log")
}}</router-link>
</li>
</ul>
</nav>
<section>
<ul v-if="actionLogs.length > 0">
<li v-for="log in actionLogs" :key="log.id">
<div class="box">
<img class="image" :src="log.actor.avatar.url" v-if="log.actor.avatar" />
<i18n
v-if="log.action === ActionLogAction.REPORT_UPDATE_CLOSED"
tag="span"
path="{moderator} closed {report}"
>
<router-link
:to="{ name: RouteName.REPORT, params: { reportId: log.object.id } }"
slot="report"
>{{ $t("report #{report_number}", { report_number: log.object.id }) }}
</router-link>
</i18n>
<i18n
v-else-if="log.action === ActionLogAction.REPORT_UPDATE_OPENED"
tag="span"
path="{moderator} reopened {report}"
>
<router-link
slot="moderator"
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.actor.id } }"
>@{{ log.actor.preferredUsername }}</router-link
<router-link
slot="moderator"
: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"
>{{ $t("report #{report_number}", { report_number: log.object.id }) }}
</router-link>
</i18n>
<i18n
v-else-if="log.action === ActionLogAction.REPORT_UPDATE_OPENED"
tag="span"
path="{moderator} reopened {report}"
>
<router-link
:to="{ name: RouteName.REPORT, params: { reportId: log.object.id } }"
slot="report"
>{{ $t("report #{report_number}", { report_number: log.object.id }) }}
</router-link>
</i18n>
<i18n
v-else-if="log.action === ActionLogAction.REPORT_UPDATE_RESOLVED"
tag="span"
path="{moderator} marked {report} as resolved"
>
<router-link
slot="moderator"
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.actor.id } }"
>@{{ log.actor.preferredUsername }}</router-link
<router-link
slot="moderator"
: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"
>{{ $t("report #{report_number}", { report_number: log.object.id }) }}
</router-link>
</i18n>
<i18n
v-else-if="log.action === ActionLogAction.REPORT_UPDATE_RESOLVED"
tag="span"
path="{moderator} marked {report} as resolved"
>
<router-link
:to="{ name: RouteName.REPORT, params: { reportId: log.object.id } }"
slot="report"
>{{ $t("report #{report_number}", { report_number: log.object.id }) }}
</router-link>
</i18n>
<i18n
v-else-if="log.action === ActionLogAction.NOTE_CREATION"
tag="span"
path="{moderator} added a note on {report}"
>
<router-link
slot="moderator"
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.actor.id } }"
>@{{ log.actor.preferredUsername }}</router-link
<router-link
slot="moderator"
: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"
>{{ $t("report #{report_number}", { report_number: log.object.id }) }}
</router-link>
</i18n>
<i18n
v-else-if="log.action === ActionLogAction.NOTE_CREATION"
tag="span"
path="{moderator} added a note on {report}"
>
<router-link
v-if="log.object.report"
:to="{ name: RouteName.REPORT, params: { reportId: log.object.report.id } }"
slot="report"
>{{ $t("report #{report_number}", { report_number: log.object.report.id }) }}
</router-link>
<span v-else slot="report">{{ $t("a non-existent report") }}</span>
</i18n>
<i18n
v-else-if="log.action === ActionLogAction.EVENT_DELETION"
tag="span"
path='{moderator} deleted an event named "{title}"'
>
<router-link
slot="moderator"
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.actor.id } }"
>@{{ log.actor.preferredUsername }}</router-link
<router-link
slot="moderator"
: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 } }"
slot="report"
>{{ $t("report #{report_number}", { report_number: log.object.report.id }) }}
</router-link>
<span v-else slot="report">{{ $t("a non-existent report") }}</span>
</i18n>
<i18n
v-else-if="log.action === ActionLogAction.EVENT_DELETION"
tag="span"
path='{moderator} deleted an event named "{title}"'
>
<b slot="title">{{ log.object.title }}</b>
</i18n>
<i18n
v-else-if="log.action === ActionLogAction.ACTOR_SUSPENSION"
tag="span"
path="{moderator} suspended profile {profile}"
>
<router-link
slot="moderator"
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.actor.id } }"
>@{{ log.actor.preferredUsername }}</router-link
<router-link
slot="moderator"
: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="{moderator} suspended profile {profile}"
>
<router-link
slot="profile"
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.object.id } }"
>{{ displayNameAndUsername(log.object) }}
</router-link>
</i18n>
<i18n
v-else-if="log.action === ActionLogAction.ACTOR_UNSUSPENSION"
tag="span"
path="{moderator} has unsuspended profile {profile}"
>
<router-link
slot="moderator"
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.actor.id } }"
>@{{ log.actor.preferredUsername }}</router-link
<router-link
slot="moderator"
: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>
<i18n
v-else-if="log.action === ActionLogAction.ACTOR_UNSUSPENSION"
tag="span"
path="{moderator} has unsuspended profile {profile}"
>
<router-link
slot="profile"
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.object.id } }"
>{{ displayNameAndUsername(log.object) }}
</router-link>
</i18n>
<i18n
v-else-if="log.action === ActionLogAction.USER_DELETION"
tag="span"
path="{moderator} has deleted user {user}"
>
<router-link
slot="moderator"
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.actor.id } }"
>@{{ log.actor.preferredUsername }}</router-link
<router-link
slot="moderator"
: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>
<i18n
v-else-if="log.action === ActionLogAction.USER_DELETION"
tag="span"
path="{moderator} has deleted user {user}"
>
<router-link
v-if="log.object.confirmedAt"
slot="user"
:to="{ name: RouteName.ADMIN_USER_PROFILE, params: { id: log.object.id } }"
>{{ log.object.email }}
</router-link>
<b v-else slot="user">{{ log.object.email }}</b>
</i18n>
<br />
<small>{{ log.insertedAt | formatDateTimeString }}</small>
</div>
</li>
</ul>
<div v-else>
<b-message type="is-info">{{ $t("No moderation logs yet") }}</b-message>
</div>
</section>
<router-link
slot="moderator"
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.actor.id } }"
>@{{ log.actor.preferredUsername }}</router-link
>
<router-link
v-if="log.object.confirmedAt"
slot="user"
:to="{ name: RouteName.ADMIN_USER_PROFILE, params: { id: log.object.id } }"
>{{ log.object.email }}
</router-link>
<b v-else slot="user">{{ log.object.email }}</b>
</i18n>
<br />
<small>{{ log.insertedAt | formatDateTimeString }}</small>
</div>
</li>
</ul>
<div v-else>
<b-message type="is-info">{{ $t("No moderation logs yet") }}</b-message>
</div>
</section>
</div>
</template>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";

View File

@ -1,202 +1,219 @@
<template>
<section>
<b-message title="Error" type="is-danger" v-for="error in errors" :key="error">
{{ error }}
</b-message>
<div class="container" v-if="report">
<div class="buttons">
<b-button
v-if="report.status !== ReportStatusEnum.RESOLVED"
@click="updateReport(ReportStatusEnum.RESOLVED)"
type="is-primary"
>{{ $t("Mark as resolved") }}</b-button
>
<b-button
v-if="report.status !== ReportStatusEnum.OPEN"
@click="updateReport(ReportStatusEnum.OPEN)"
type="is-success"
>{{ $t("Reopen") }}</b-button
>
<b-button
v-if="report.status !== ReportStatusEnum.CLOSED"
@click="updateReport(ReportStatusEnum.CLOSED)"
type="is-danger"
>{{ $t("Close") }}</b-button
>
</div>
<div class="table-container">
<table class="table is-striped is-fullwidth">
<tbody>
<tr>
<td>{{ $t("Reported identity") }}</td>
<td>
<router-link
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: report.reported.id },
}"
>
<img
v-if="report.reported.avatar"
class="image"
:src="report.reported.avatar.url"
alt=""
/>
@{{ report.reported.preferredUsername }}
</router-link>
</td>
</tr>
<tr>
<td>{{ $t("Reported by") }}</td>
<td v-if="report.reporter.type === ActorType.APPLICATION">
{{ report.reporter.domain }}
</td>
<td v-else>
<router-link
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: report.reporter.id },
}"
>
<img
v-if="report.reporter.avatar"
class="image"
:src="report.reporter.avatar.url"
alt=""
/>
@{{ report.reporter.preferredUsername }}
</router-link>
</td>
</tr>
<tr>
<td>{{ $t("Reported") }}</td>
<td>{{ report.insertedAt | formatDateTimeString }}</td>
</tr>
<tr v-if="report.updatedAt !== report.insertedAt">
<td>{{ $t("Updated") }}</td>
<td>{{ report.updatedAt | formatDateTimeString }}</td>
</tr>
<tr>
<td>{{ $t("Status") }}</td>
<td>
<span v-if="report.status === ReportStatusEnum.OPEN">{{ $t("Open") }}</span>
<span v-else-if="report.status === ReportStatusEnum.CLOSED">
{{ $t("Closed") }}
</span>
<span v-else-if="report.status === ReportStatusEnum.RESOLVED">
{{ $t("Resolved") }}
</span>
<span v-else>{{ $t("Unknown") }}</span>
</td>
</tr>
<tr v-if="report.event && report.comments.length > 0">
<td>{{ $t("Event") }}</td>
<td>
<router-link :to="{ name: RouteName.EVENT, params: { uuid: report.event.uuid } }">
{{ report.event.title }}
</router-link>
<span class="is-pulled-right">
<!-- <b-button-->
<!-- tag="router-link"-->
<!-- type="is-primary"-->
<!-- :to="{ name: RouteName.EDIT_EVENT, params: {eventId: report.event.uuid } }"-->
<!-- icon-left="pencil"-->
<!-- size="is-small">{{ $t('Edit') }}</b-button>-->
<div>
<nav class="breadcrumb" aria-label="breadcrumbs" v-if="report">
<ul>
<li>
<router-link :to="{ name: RouteName.MODERATION }">{{ $t("Moderation") }}</router-link>
</li>
<li>
<router-link :to="{ name: RouteName.REPORTS }">{{ $t("Reports") }}</router-link>
</li>
<li class="is-active">
<router-link :to="{ name: RouteName.REPORT, params: { id: report.id } }">{{
$t("Report #{reportNumber}", { reportNumber: report.id })
}}</router-link>
</li>
</ul>
</nav>
<section>
<b-message title="Error" type="is-danger" v-for="error in errors" :key="error">
{{ error }}
</b-message>
<div class="container" v-if="report">
<div class="buttons">
<b-button
v-if="report.status !== ReportStatusEnum.RESOLVED"
@click="updateReport(ReportStatusEnum.RESOLVED)"
type="is-primary"
>{{ $t("Mark as resolved") }}</b-button
>
<b-button
v-if="report.status !== ReportStatusEnum.OPEN"
@click="updateReport(ReportStatusEnum.OPEN)"
type="is-success"
>{{ $t("Reopen") }}</b-button
>
<b-button
v-if="report.status !== ReportStatusEnum.CLOSED"
@click="updateReport(ReportStatusEnum.CLOSED)"
type="is-danger"
>{{ $t("Close") }}</b-button
>
</div>
<div class="table-container">
<table class="table is-striped is-fullwidth">
<tbody>
<tr>
<td>{{ $t("Reported identity") }}</td>
<td>
<router-link
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: report.reported.id },
}"
>
<img
v-if="report.reported.avatar"
class="image"
:src="report.reported.avatar.url"
alt=""
/>
@{{ report.reported.preferredUsername }}
</router-link>
</td>
</tr>
<tr>
<td>{{ $t("Reported by") }}</td>
<td v-if="report.reporter.type === ActorType.APPLICATION">
{{ report.reporter.domain }}
</td>
<td v-else>
<router-link
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: report.reporter.id },
}"
>
<img
v-if="report.reporter.avatar"
class="image"
:src="report.reporter.avatar.url"
alt=""
/>
@{{ report.reporter.preferredUsername }}
</router-link>
</td>
</tr>
<tr>
<td>{{ $t("Reported") }}</td>
<td>{{ report.insertedAt | formatDateTimeString }}</td>
</tr>
<tr v-if="report.updatedAt !== report.insertedAt">
<td>{{ $t("Updated") }}</td>
<td>{{ report.updatedAt | formatDateTimeString }}</td>
</tr>
<tr>
<td>{{ $t("Status") }}</td>
<td>
<span v-if="report.status === ReportStatusEnum.OPEN">{{ $t("Open") }}</span>
<span v-else-if="report.status === ReportStatusEnum.CLOSED">
{{ $t("Closed") }}
</span>
<span v-else-if="report.status === ReportStatusEnum.RESOLVED">
{{ $t("Resolved") }}
</span>
<span v-else>{{ $t("Unknown") }}</span>
</td>
</tr>
<tr v-if="report.event && report.comments.length > 0">
<td>{{ $t("Event") }}</td>
<td>
<router-link :to="{ name: RouteName.EVENT, params: { uuid: report.event.uuid } }">
{{ report.event.title }}
</router-link>
<span class="is-pulled-right">
<!-- <b-button-->
<!-- tag="router-link"-->
<!-- type="is-primary"-->
<!-- :to="{ name: RouteName.EDIT_EVENT, params: {eventId: report.event.uuid } }"-->
<!-- icon-left="pencil"-->
<!-- size="is-small">{{ $t('Edit') }}</b-button>-->
<b-button
type="is-danger"
@click="confirmEventDelete()"
icon-left="delete"
size="is-small"
>{{ $t("Delete") }}</b-button
>
</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="box report-content">
<p v-if="report.content" v-html="nl2br(report.content)" />
<p v-else>{{ $t("No comment") }}</p>
</div>
<div class="box" v-if="report.event && report.comments.length === 0">
<router-link :to="{ name: RouteName.EVENT, params: { uuid: report.event.uuid } }">
<h3 class="title">{{ report.event.title }}</h3>
<p v-html="report.event.description" />
</router-link>
<!-- <b-button-->
<!-- tag="router-link"-->
<!-- type="is-primary"-->
<!-- :to="{ name: RouteName.EDIT_EVENT, params: {eventId: report.event.uuid } }"-->
<!-- icon-left="pencil"-->
<!-- size="is-small">{{ $t('Edit') }}</b-button>-->
<b-button
type="is-danger"
@click="confirmEventDelete()"
icon-left="delete"
size="is-small"
>{{ $t("Delete") }}</b-button
>
</div>
<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">
<div class="media-left">
<figure class="image is-48x48" v-if="comment.actor && comment.actor.avatar">
<img :src="comment.actor.avatar.url" alt="Image" />
</figure>
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
</div>
<div class="media-content">
<div class="content">
<span v-if="comment.actor">
<strong>{{ comment.actor.name }}</strong>
<small>@{{ comment.actor.preferredUsername }}</small>
</span>
<span v-else>{{ $t("Unknown actor") }}</span>
<br />
<p v-html="comment.text" />
</div>
<b-button
type="is-danger"
@click="confirmEventDelete()"
@click="confirmCommentDelete(comment)"
icon-left="delete"
size="is-small"
>{{ $t("Delete") }}</b-button
>
</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="box report-content">
<p v-if="report.content" v-html="nl2br(report.content)" />
<p v-else>{{ $t("No comment") }}</p>
</div>
<div class="box" v-if="report.event && report.comments.length === 0">
<router-link :to="{ name: RouteName.EVENT, params: { uuid: report.event.uuid } }">
<h3 class="title">{{ report.event.title }}</h3>
<p v-html="report.event.description" />
</router-link>
<!-- <b-button-->
<!-- tag="router-link"-->
<!-- type="is-primary"-->
<!-- :to="{ name: RouteName.EDIT_EVENT, params: {eventId: report.event.uuid } }"-->
<!-- icon-left="pencil"-->
<!-- size="is-small">{{ $t('Edit') }}</b-button>-->
<b-button
type="is-danger"
@click="confirmEventDelete()"
icon-left="delete"
size="is-small"
>{{ $t("Delete") }}</b-button
>
</div>
<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">
<div class="media-left">
<figure class="image is-48x48" v-if="comment.actor && comment.actor.avatar">
<img :src="comment.actor.avatar.url" alt="Image" />
</figure>
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
</div>
<div class="media-content">
<div class="content">
<span v-if="comment.actor">
<strong>{{ comment.actor.name }}</strong>
<small>@{{ comment.actor.preferredUsername }}</small>
</span>
<span v-else>{{ $t("Unknown actor") }}</span>
<br />
<p v-html="comment.text" />
</div>
<b-button
type="is-danger"
@click="confirmCommentDelete(comment)"
icon-left="delete"
size="is-small"
>{{ $t("Delete") }}</b-button
>
</div>
</article>
</div>
</li>
</ul>
</article>
</div>
</li>
</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}`" :key="note.id">
<p>{{ note.content }}</p>
<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>
<br />
<small>
<a :href="`#note-${note.id}`" v-if="note.insertedAt">
{{ note.insertedAt | formatDateTimeString }}
</a>
</small>
<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}`" :key="note.id">
<p>{{ note.content }}</p>
<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>
<br />
<small>
<a :href="`#note-${note.id}`" v-if="note.insertedAt">
{{ note.insertedAt | formatDateTimeString }}
</a>
</small>
</div>
<form @submit="addNote()">
<b-field :label="$t('New note')" label-for="newNoteInput">
<b-input type="textarea" v-model="noteContent" id="newNoteInput"></b-input>
</b-field>
<b-button type="submit" @click="addNote">{{ $t("Add a note") }}</b-button>
</form>
</div>
<form @submit="addNote()">
<b-field :label="$t('New note')" label-for="newNoteInput">
<b-input type="textarea" v-model="noteContent" id="newNoteInput"></b-input>
</b-field>
<b-button type="submit" @click="addNote">{{ $t("Add a note") }}</b-button>
</form>
</div>
</section>
</section>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";

View File

@ -1,35 +1,47 @@
<template>
<section>
<b-field>
<b-radio-button v-model="filterReports" :native-value="ReportStatusEnum.OPEN">{{
$t("Open")
}}</b-radio-button>
<b-radio-button v-model="filterReports" :native-value="ReportStatusEnum.RESOLVED">{{
$t("Resolved")
}}</b-radio-button>
<b-radio-button v-model="filterReports" :native-value="ReportStatusEnum.CLOSED">{{
$t("Closed")
}}</b-radio-button>
</b-field>
<ul v-if="reports.length > 0">
<li v-for="report in reports" :key="report.id">
<router-link :to="{ name: RouteName.REPORT, params: { reportId: report.id } }">
<report-card :report="report" />
</router-link>
</li>
</ul>
<div v-else>
<b-message v-if="filterReports === ReportStatusEnum.OPEN" type="is-info">
{{ $t("No open reports yet") }}
</b-message>
<b-message v-if="filterReports === ReportStatusEnum.RESOLVED" type="is-info">
{{ $t("No resolved reports yet") }}
</b-message>
<b-message v-if="filterReports === ReportStatusEnum.CLOSED" type="is-info">
{{ $t("No closed reports yet") }}
</b-message>
</div>
</section>
<div>
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<router-link :to="{ name: RouteName.MODERATION }">{{ $t("Moderation") }}</router-link>
</li>
<li class="is-active">
<router-link :to="{ name: RouteName.REPORTS }">{{ $t("Reports") }}</router-link>
</li>
</ul>
</nav>
<section>
<b-field>
<b-radio-button v-model="filterReports" :native-value="ReportStatusEnum.OPEN">{{
$t("Open")
}}</b-radio-button>
<b-radio-button v-model="filterReports" :native-value="ReportStatusEnum.RESOLVED">{{
$t("Resolved")
}}</b-radio-button>
<b-radio-button v-model="filterReports" :native-value="ReportStatusEnum.CLOSED">{{
$t("Closed")
}}</b-radio-button>
</b-field>
<ul v-if="reports.length > 0">
<li v-for="report in reports" :key="report.id">
<router-link :to="{ name: RouteName.REPORT, params: { reportId: report.id } }">
<report-card :report="report" />
</router-link>
</li>
</ul>
<div v-else>
<b-message v-if="filterReports === ReportStatusEnum.OPEN" type="is-info">
{{ $t("No open reports yet") }}
</b-message>
<b-message v-if="filterReports === ReportStatusEnum.RESOLVED" type="is-info">
{{ $t("No resolved reports yet") }}
</b-message>
<b-message v-if="filterReports === ReportStatusEnum.CLOSED" type="is-info">
{{ $t("No closed reports yet") }}
</b-message>
</div>
</section>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator";

View File

@ -2,19 +2,8 @@
<div class="section container">
<h1 class="title">{{ $t("Settings") }}</h1>
<div class="columns">
<SettingsMenu class="column is-one-quarter-desktop" :menu="menu" />
<SettingsMenu class="column is-one-quarter-desktop" />
<div class="column">
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li
v-for="route in routes.get($route.name)"
:class="{ 'is-active': route.to.name === $route.name }"
:key="route.to.name"
>
<router-link :to="{ name: route.to.name }">{{ route.title }}</router-link>
</li>
</ul>
</nav>
<router-view />
</div>
</div>
@ -25,7 +14,6 @@ import { Component, Vue, Watch } from "vue-property-decorator";
import { Route } from "vue-router";
import SettingsMenu from "../components/Settings/SettingsMenu.vue";
import RouteName from "../router/name";
import { ISettingMenuSection } from "../types/setting-menu.model";
import { IPerson, Person } from "../types/actor";
import { IDENTITIES } from "../graphql/actor";
import { CURRENT_USER_CLIENT } from "../graphql/user";
@ -44,148 +32,9 @@ import { ICurrentUser, ICurrentUserRole } from "../types/current-user.model";
export default class Settings extends Vue {
RouteName = RouteName;
menu: ISettingMenuSection[] = [];
identities!: IPerson[];
newIdentity!: ISettingMenuSection;
currentUser!: ICurrentUser;
mounted() {
this.newIdentity = {
title: this.$t("New profile") as string,
to: { name: RouteName.CREATE_IDENTITY } as Route,
};
this.menu = [
{
title: this.$t("Account") as string,
to: { name: RouteName.ACCOUNT_SETTINGS } as Route,
items: [
{
title: this.$t("General") as string,
to: { name: RouteName.ACCOUNT_SETTINGS_GENERAL } as Route,
},
{
title: this.$t("Preferences") as string,
to: { name: RouteName.PREFERENCES } as Route,
},
{
title: this.$t("Email notifications") as string,
to: { name: RouteName.NOTIFICATIONS } as Route,
},
],
},
{
title: this.$t("Profiles") as string,
to: { name: RouteName.IDENTITIES } as Route,
items: [this.newIdentity],
},
];
if (
[ICurrentUserRole.MODERATOR, ICurrentUserRole.ADMINISTRATOR].includes(this.currentUser.role)
) {
this.menu.push({
title: this.$t("Moderation") as string,
to: { name: RouteName.MODERATION } as Route,
items: [
{
title: this.$t("Reports") as string,
to: { name: RouteName.REPORTS } as Route,
items: [
{
title: this.$t("Report") as string,
to: { name: RouteName.REPORT } as Route,
},
],
},
{
title: this.$t("Moderation log") as string,
to: { name: RouteName.REPORT_LOGS } as Route,
},
{
title: this.$t("Users") as string,
to: { name: RouteName.USERS } as Route,
},
{
title: this.$t("Profiles") as string,
to: { name: RouteName.PROFILES } as Route,
},
],
});
}
if (this.currentUser.role === ICurrentUserRole.ADMINISTRATOR) {
this.menu.push({
title: this.$t("Admin") as string,
to: { name: RouteName.ADMIN } as Route,
items: [
{
title: this.$t("Dashboard") as string,
to: { name: RouteName.ADMIN_DASHBOARD } as Route,
},
{
title: this.$t("Instance settings") as string,
to: { name: RouteName.ADMIN_SETTINGS } as Route,
},
{
title: this.$t("Federation") as string,
to: { name: RouteName.RELAYS } as Route,
items: [
{
title: this.$t("Followings") as string,
to: { name: RouteName.RELAY_FOLLOWINGS } as Route,
},
{
title: this.$t("Followers") as string,
to: { name: RouteName.RELAY_FOLLOWERS } as Route,
},
],
},
],
});
}
}
@Watch("identities")
updateIdentities(identities: IPerson[]) {
if (!identities) return;
if (!this.menu[1].items) return;
this.menu[1].items = [];
this.menu[1].items.push(
...identities.map((identity: IPerson) => ({
to: ({
name: RouteName.UPDATE_IDENTITY,
params: { identityName: identity.preferredUsername },
} as unknown) as Route,
title: `@${identity.preferredUsername}`,
}))
);
this.menu[1].items.push(this.newIdentity);
}
get routes(): Map<string, Route[]> {
return this.getPath(this.menu);
}
getPath(object: ISettingMenuSection[]) {
function iter(menu: ISettingMenuSection[] | ISettingMenuSection, acc: ISettingMenuSection[]) {
if (Array.isArray(menu)) {
return menu.forEach((item: ISettingMenuSection) => {
iter(item, acc.concat(item));
});
}
if (menu.items && menu.items.length > 0) {
return menu.items.forEach((item: ISettingMenuSection) => {
iter(item, acc.concat(item));
});
}
result.set(menu.to.name, acc);
}
const result = new Map();
iter(object, []);
return result;
}
}
</script>

View File

@ -1,145 +1,159 @@
<template>
<section>
<div class="setting-title">
<h2>{{ $t("Email") }}</h2>
</div>
<i18n
tag="p"
class="content"
v-if="loggedUser"
path="Your current email is {email}. You use it to log in."
>
<b slot="email">{{ loggedUser.email }}</b>
</i18n>
<b-notification
type="is-danger"
has-icon
aria-close-label="Close notification"
role="alert"
:key="error"
v-for="error in changeEmailErrors"
>{{ error }}</b-notification
>
<form @submit.prevent="resetEmailAction" ref="emailForm" class="form">
<b-field :label="$t('New email')">
<b-input aria-required="true" required type="email" v-model="newEmail" />
</b-field>
<p class="help">{{ $t("You'll receive a confirmation email.") }}</p>
<b-field :label="$t('Password')">
<b-input
aria-required="true"
required
type="password"
password-reveal
minlength="6"
v-model="passwordForEmailChange"
/>
</b-field>
<button
class="button is-primary"
:disabled="!($refs.emailForm && $refs.emailForm.checkValidity())"
<div>
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<router-link :to="{ name: RouteName.ACCOUNT_SETTINGS }">{{ $t("Account") }}</router-link>
</li>
<li class="is-active">
<router-link :to="{ name: RouteName.ACCOUNT_SETTINGS_GENERAL }">{{
$t("General")
}}</router-link>
</li>
</ul>
</nav>
<section>
<div class="setting-title">
<h2>{{ $t("Email") }}</h2>
</div>
<i18n
tag="p"
class="content"
v-if="loggedUser"
path="Your current email is {email}. You use it to log in."
>
{{ $t("Change my email") }}
</button>
</form>
<div class="setting-title">
<h2>{{ $t("Password") }}</h2>
</div>
<b-notification
type="is-danger"
has-icon
aria-close-label="Close notification"
role="alert"
:key="error"
v-for="error in changePasswordErrors"
>{{ error }}</b-notification
>
<form @submit.prevent="resetPasswordAction" ref="passwordForm" class="form">
<b-field :label="$t('Old password')">
<b-input
aria-required="true"
required
type="password"
password-reveal
minlength="6"
v-model="oldPassword"
/>
</b-field>
<b-field :label="$t('New password')">
<b-input
aria-required="true"
required
type="password"
password-reveal
minlength="6"
v-model="newPassword"
/>
</b-field>
<button
class="button is-primary"
:disabled="!($refs.passwordForm && $refs.passwordForm.checkValidity())"
<b slot="email">{{ loggedUser.email }}</b>
</i18n>
<b-notification
type="is-danger"
has-icon
aria-close-label="Close notification"
role="alert"
:key="error"
v-for="error in changeEmailErrors"
>{{ error }}</b-notification
>
{{ $t("Change my password") }}
</button>
</form>
<div class="setting-title">
<h2>{{ $t("Delete account") }}</h2>
</div>
<p class="content">{{ $t("Deleting my account will delete all of my identities.") }}</p>
<b-button @click="openDeleteAccountModal" type="is-danger">
{{ $t("Delete my account") }}
</b-button>
<form @submit.prevent="resetEmailAction" ref="emailForm" class="form">
<b-field :label="$t('New email')">
<b-input aria-required="true" required type="email" v-model="newEmail" />
</b-field>
<p class="help">{{ $t("You'll receive a confirmation email.") }}</p>
<b-field :label="$t('Password')">
<b-input
aria-required="true"
required
type="password"
password-reveal
minlength="6"
v-model="passwordForEmailChange"
/>
</b-field>
<button
class="button is-primary"
:disabled="!($refs.emailForm && $refs.emailForm.checkValidity())"
>
{{ $t("Change my email") }}
</button>
</form>
<div class="setting-title">
<h2>{{ $t("Password") }}</h2>
</div>
<b-notification
type="is-danger"
has-icon
aria-close-label="Close notification"
role="alert"
:key="error"
v-for="error in changePasswordErrors"
>{{ error }}</b-notification
>
<form @submit.prevent="resetPasswordAction" ref="passwordForm" class="form">
<b-field :label="$t('Old password')">
<b-input
aria-required="true"
required
type="password"
password-reveal
minlength="6"
v-model="oldPassword"
/>
</b-field>
<b-field :label="$t('New password')">
<b-input
aria-required="true"
required
type="password"
password-reveal
minlength="6"
v-model="newPassword"
/>
</b-field>
<button
class="button is-primary"
:disabled="!($refs.passwordForm && $refs.passwordForm.checkValidity())"
>
{{ $t("Change my password") }}
</button>
</form>
<div class="setting-title">
<h2>{{ $t("Delete account") }}</h2>
</div>
<p class="content">{{ $t("Deleting my account will delete all of my identities.") }}</p>
<b-button @click="openDeleteAccountModal" type="is-danger">
{{ $t("Delete my account") }}
</b-button>
<b-modal
:active.sync="isDeleteAccountModalActive"
has-modal-card
full-screen
:can-cancel="false"
>
<section class="hero is-primary is-fullheight">
<div class="hero-body has-text-centered">
<div class="container">
<div class="columns">
<div class="column is-one-third-desktop is-offset-one-third-desktop">
<h1 class="title">{{ $t("Deleting your Mobilizon account") }}</h1>
<p class="content">
{{
$t(
"Are you really sure you want to delete your whole account? You'll lose everything. Identities, settings, events created, messages and participations will be gone forever."
)
}}
<br />
<b>{{ $t("There will be no way to recover your data.") }}</b>
</p>
<p class="content">
{{ $t("Please enter your password to confirm this action.") }}
</p>
<form @submit.prevent="deleteAccount">
<b-field>
<b-input
type="password"
v-model="passwordForAccountDeletion"
password-reveal
icon="lock"
:placeholder="$t('Password')"
/>
</b-field>
<b-button native-type="submit" type="is-danger" size="is-large">
{{ $t("Delete everything") }}
</b-button>
</form>
<div class="cancel-button">
<b-button type="is-light" @click="isDeleteAccountModalActive = false">
{{ $t("Cancel") }}
</b-button>
<b-modal
:active.sync="isDeleteAccountModalActive"
has-modal-card
full-screen
:can-cancel="false"
>
<section class="hero is-primary is-fullheight">
<div class="hero-body has-text-centered">
<div class="container">
<div class="columns">
<div class="column is-one-third-desktop is-offset-one-third-desktop">
<h1 class="title">{{ $t("Deleting your Mobilizon account") }}</h1>
<p class="content">
{{
$t(
"Are you really sure you want to delete your whole account? You'll lose everything. Identities, settings, events created, messages and participations will be gone forever."
)
}}
<br />
<b>{{ $t("There will be no way to recover your data.") }}</b>
</p>
<p class="content">
{{ $t("Please enter your password to confirm this action.") }}
</p>
<form @submit.prevent="deleteAccount">
<b-field>
<b-input
type="password"
v-model="passwordForAccountDeletion"
password-reveal
icon="lock"
:placeholder="$t('Password')"
/>
</b-field>
<b-button native-type="submit" type="is-danger" size="is-large">
{{ $t("Delete everything") }}
</b-button>
</form>
<div class="cancel-button">
<b-button type="is-light" @click="isDeleteAccountModalActive = false">
{{ $t("Cancel") }}
</b-button>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</b-modal>
</section>
</section>
</b-modal>
</section>
</div>
</template>
<script lang="ts">

View File

@ -1,5 +1,17 @@
<template>
<div v-if="loggedUser">
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<router-link :to="{ name: RouteName.ACCOUNT_SETTINGS }">{{ $t("Account") }}</router-link>
</li>
<li class="is-active">
<router-link :to="{ name: RouteName.NOTIFICATIONS }">{{
$t("Email notifications")
}}</router-link>
</li>
</ul>
</nav>
<section>
<div class="setting-title">
<h2>{{ $t("Participation notifications") }}</h2>

View File

@ -1,38 +1,50 @@
<template>
<div>
<b-field :label="$t('Language')">
<b-select
:loading="!config || !loggedUser"
v-model="$i18n.locale"
:placeholder="$t('Select a language')"
>
<option v-for="(language, lang) in languages" :value="lang" :key="lang">
{{ language }}
</option>
</b-select>
</b-field>
<b-field :label="$t('Timezone')">
<b-select
:placeholder="$t('Select a timezone')"
:loading="!config || !loggedUser"
v-model="selectedTimezone"
>
<optgroup :label="group" v-for="(groupTimezones, group) in timezones" :key="group">
<option
v-for="timezone in groupTimezones"
:value="`${group}/${timezone}`"
:key="timezone"
>
{{ sanitize(timezone) }}
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<router-link :to="{ name: RouteName.ACCOUNT_SETTINGS }">{{ $t("Account") }}</router-link>
</li>
<li class="is-active">
<router-link :to="{ name: RouteName.PREFERENCES }">{{ $t("Preferences") }}</router-link>
</li>
</ul>
</nav>
<div>
<b-field :label="$t('Language')">
<b-select
:loading="!config || !loggedUser"
v-model="$i18n.locale"
:placeholder="$t('Select a language')"
>
<option v-for="(language, lang) in languages" :value="lang" :key="lang">
{{ language }}
</option>
</optgroup>
</b-select>
</b-field>
<em>{{
$t("Timezone detected as {timezone}.", {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
})
}}</em>
</b-select>
</b-field>
<b-field :label="$t('Timezone')">
<b-select
:placeholder="$t('Select a timezone')"
:loading="!config || !loggedUser"
v-model="selectedTimezone"
>
<optgroup :label="group" v-for="(groupTimezones, group) in timezones" :key="group">
<option
v-for="timezone in groupTimezones"
:value="`${group}/${timezone}`"
:key="timezone"
>
{{ sanitize(timezone) }}
</option>
</optgroup>
</b-select>
</b-field>
<em>{{
$t("Timezone detected as {timezone}.", {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
})
}}</em>
</div>
</div>
</template>
<script lang="ts">
@ -42,6 +54,7 @@ import { USER_SETTINGS, SET_USER_SETTINGS, UPDATE_USER_LOCALE } from "../../grap
import { IConfig } from "../../types/config.model";
import { ICurrentUser } from "../../types/current-user.model";
import langs from "../../i18n/langs.json";
import RouteName from "../../router/name";
@Component({
apollo: {
@ -58,6 +71,8 @@ export default class Preferences extends Vue {
locale: string | null = null;
RouteName = RouteName;
@Watch("loggedUser")
setSavedTimezone(loggedUser: ICurrentUser) {
if (loggedUser && loggedUser.settings.timezone) {