Improve and activate groups
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
@@ -45,10 +45,10 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t("Registrations") }}</td>
|
||||
<td v-if="config.registrationsOpen && config.registrationsWhitelist">
|
||||
<td v-if="config.registrationsOpen && config.registrationsAllowlist">
|
||||
{{ $t("Restricted") }}
|
||||
</td>
|
||||
<td v-if="config.registrationsOpen && !config.registrationsWhitelist">
|
||||
<td v-if="config.registrationsOpen && !config.registrationsAllowlist">
|
||||
{{ $t("Open") }}
|
||||
</td>
|
||||
<td v-else>{{ $t("Closed") }}</td>
|
||||
|
||||
@@ -124,6 +124,7 @@ h1 {
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Watch } from "vue-property-decorator";
|
||||
import { mixins } from "vue-class-component";
|
||||
import { Route } from "vue-router";
|
||||
import {
|
||||
CREATE_PERSON,
|
||||
CURRENT_ACTOR_CLIENT,
|
||||
@@ -176,25 +177,25 @@ export default class EditIdentity extends mixins(identityEditionMixin) {
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
get message() {
|
||||
get message(): string | null {
|
||||
if (this.isUpdate) return null;
|
||||
return this.$t("Only alphanumeric characters and underscores are supported.");
|
||||
return this.$t("Only alphanumeric characters and underscores are supported.") as string;
|
||||
}
|
||||
|
||||
@Watch("isUpdate")
|
||||
async isUpdateChanged() {
|
||||
async isUpdateChanged(): Promise<void> {
|
||||
this.resetFields();
|
||||
}
|
||||
|
||||
@Watch("identityName", { immediate: true })
|
||||
async onIdentityParamChanged(val: string) {
|
||||
async onIdentityParamChanged(val: string): Promise<Route | undefined> {
|
||||
// Only used when we update the identity
|
||||
if (!this.isUpdate) return;
|
||||
|
||||
await this.redirectIfNoIdentitySelected(val);
|
||||
|
||||
if (!this.identityName) {
|
||||
return await this.$router.push({ name: "CreateIdentity" });
|
||||
return this.$router.push({ name: "CreateIdentity" });
|
||||
}
|
||||
|
||||
if (this.identityName && this.identity) {
|
||||
@@ -202,7 +203,7 @@ export default class EditIdentity extends mixins(identityEditionMixin) {
|
||||
}
|
||||
}
|
||||
|
||||
submit() {
|
||||
submit(): Promise<void> {
|
||||
if (this.isUpdate) return this.updateIdentity();
|
||||
|
||||
return this.createIdentity();
|
||||
@@ -211,7 +212,7 @@ export default class EditIdentity extends mixins(identityEditionMixin) {
|
||||
/**
|
||||
* Delete an identity
|
||||
*/
|
||||
async deleteIdentity() {
|
||||
async deleteIdentity(): Promise<void> {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: DELETE_PERSON,
|
||||
@@ -252,7 +253,7 @@ export default class EditIdentity extends mixins(identityEditionMixin) {
|
||||
}
|
||||
}
|
||||
|
||||
async updateIdentity() {
|
||||
async updateIdentity(): Promise<void> {
|
||||
try {
|
||||
const variables = await this.buildVariables();
|
||||
|
||||
@@ -285,7 +286,7 @@ export default class EditIdentity extends mixins(identityEditionMixin) {
|
||||
}
|
||||
}
|
||||
|
||||
async createIdentity() {
|
||||
async createIdentity(): Promise<void> {
|
||||
try {
|
||||
const variables = await this.buildVariables();
|
||||
|
||||
@@ -320,11 +321,12 @@ export default class EditIdentity extends mixins(identityEditionMixin) {
|
||||
}
|
||||
}
|
||||
|
||||
get getInstanceHost() {
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
get getInstanceHost(): string {
|
||||
return MOBILIZON_INSTANCE_HOST;
|
||||
}
|
||||
|
||||
openDeleteIdentityConfirmation() {
|
||||
openDeleteIdentityConfirmation(): void {
|
||||
this.$buefy.dialog.prompt({
|
||||
type: "is-danger",
|
||||
title: this.$t("Delete your identity") as string,
|
||||
|
||||
@@ -202,7 +202,6 @@ import { SUSPEND_PROFILE, UNSUSPEND_PROFILE } from "../../graphql/actor";
|
||||
import { IGroup, MemberRole } from "../../types/actor";
|
||||
import { usernameWithDomain, IActor } from "../../types/actor/actor.model";
|
||||
import RouteName from "../../router/name";
|
||||
import { IEvent } from "../../types/event.model";
|
||||
import ActorCard from "../../components/Account/ActorCard.vue";
|
||||
|
||||
const EVENTS_PER_PAGE = 10;
|
||||
@@ -248,22 +247,22 @@ export default class AdminGroupProfile extends Vue {
|
||||
|
||||
MemberRole = MemberRole;
|
||||
|
||||
get metadata(): Array<object> {
|
||||
get metadata(): Array<Record<string, string>> {
|
||||
if (!this.group) return [];
|
||||
const res: object[] = [
|
||||
const res: Record<string, string>[] = [
|
||||
{
|
||||
key: this.$t("Status") as string,
|
||||
value: this.group.suspended ? this.$t("Suspended") : this.$t("Active"),
|
||||
value: (this.group.suspended ? this.$t("Suspended") : this.$t("Active")) as string,
|
||||
},
|
||||
{
|
||||
key: this.$t("Domain") as string,
|
||||
value: this.group.domain ? this.group.domain : this.$t("Local"),
|
||||
value: (this.group.domain ? this.group.domain : this.$t("Local")) as string,
|
||||
},
|
||||
];
|
||||
return res;
|
||||
}
|
||||
|
||||
confirmSuspendProfile() {
|
||||
confirmSuspendProfile(): void {
|
||||
const message = (this.group.domain
|
||||
? this.$t(
|
||||
"Are you sure you want to <b>suspend</b> this group? As this group originates from instance {instance}, this will only remove local members and delete the local data, as well as rejecting all the future data.",
|
||||
@@ -284,7 +283,7 @@ export default class AdminGroupProfile extends Vue {
|
||||
});
|
||||
}
|
||||
|
||||
async suspendProfile() {
|
||||
async suspendProfile(): Promise<void> {
|
||||
this.$apollo.mutate<{ suspendProfile: { id: string } }>({
|
||||
mutation: SUSPEND_PROFILE,
|
||||
variables: {
|
||||
@@ -318,9 +317,9 @@ export default class AdminGroupProfile extends Vue {
|
||||
});
|
||||
}
|
||||
|
||||
async unsuspendProfile() {
|
||||
async unsuspendProfile(): Promise<void> {
|
||||
const profileID = this.id;
|
||||
this.$apollo.mutate<{ unsuspendProfile: { id: string } }>({
|
||||
await this.$apollo.mutate<{ unsuspendProfile: { id: string } }>({
|
||||
mutation: UNSUSPEND_PROFILE,
|
||||
variables: {
|
||||
id: this.id,
|
||||
@@ -336,7 +335,7 @@ export default class AdminGroupProfile extends Vue {
|
||||
});
|
||||
}
|
||||
|
||||
async refreshProfile() {
|
||||
async refreshProfile(): Promise<void> {
|
||||
this.$apollo.mutate<{ refreshProfile: IActor }>({
|
||||
mutation: REFRESH_PROFILE,
|
||||
variables: {
|
||||
@@ -345,7 +344,7 @@ export default class AdminGroupProfile extends Vue {
|
||||
});
|
||||
}
|
||||
|
||||
async onOrganizedEventsPageChange(page: number) {
|
||||
async onOrganizedEventsPageChange(page: number): Promise<void> {
|
||||
this.organizedEventsPage = page;
|
||||
await this.$apollo.queries.group.fetchMore({
|
||||
variables: {
|
||||
@@ -370,7 +369,7 @@ export default class AdminGroupProfile extends Vue {
|
||||
});
|
||||
}
|
||||
|
||||
async onMembersPageChange(page: number) {
|
||||
async onMembersPageChange(page: number): Promise<void> {
|
||||
this.membersPage = page;
|
||||
await this.$apollo.queries.group.fetchMore({
|
||||
variables: {
|
||||
@@ -395,7 +394,7 @@ export default class AdminGroupProfile extends Vue {
|
||||
});
|
||||
}
|
||||
|
||||
async onPostsPageChange(page: number) {
|
||||
async onPostsPageChange(page: number): Promise<void> {
|
||||
this.postsPage = page;
|
||||
await this.$apollo.queries.group.fetchMore({
|
||||
variables: {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>{{ `@${group.preferredUsername}` }}</router-link
|
||||
>{{ group.name }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
|
||||
@@ -41,21 +41,31 @@
|
||||
</b-field>
|
||||
|
||||
<subtitle>{{ $t("Organizers") }}</subtitle>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<b-field :label="$t('Organizer')">
|
||||
<identity-picker-wrapper
|
||||
v-model="event.organizerActor"
|
||||
:masked="event.options.hideOrganizerWhenGroupEvent"
|
||||
@input="resetAttributedToOnOrganizerChange"
|
||||
/>
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column" v-if="config && config.features.groups">
|
||||
<b-field :label="$t('Group')" v-if="event.organizerActor">
|
||||
<group-picker-wrapper v-model="event.attributedTo" :identity="event.organizerActor" />
|
||||
</b-field>
|
||||
</div>
|
||||
|
||||
<div v-if="config && config.features.groups">
|
||||
<b-field>
|
||||
<organizer-picker-wrapper
|
||||
v-model="event.attributedTo"
|
||||
:contacts.sync="event.contacts"
|
||||
:identity="event.organizerActor"
|
||||
/>
|
||||
</b-field>
|
||||
<p v-if="!event.attributedTo.id || attributedToEqualToOrganizerActor">
|
||||
{{ $t("The event will show as attributed to your personal profile.") }}
|
||||
</p>
|
||||
<p v-else>
|
||||
<span>{{ $t("The event will show as attributed to this group.") }}</span>
|
||||
<span
|
||||
v-if="event.contacts && event.contacts.length"
|
||||
v-html="
|
||||
$tc('<b>{contact}</b> will be displayed as contact.', event.contacts.length, {
|
||||
contact: formatList(
|
||||
event.contacts.map((contact) => displayNameAndUsername(contact))
|
||||
),
|
||||
})
|
||||
"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<!-- <div class="field" v-if="event.attributedTo.id">-->
|
||||
<!-- <label class="label">{{ $t('Hide the organizer') }}</label>-->
|
||||
@@ -332,8 +342,9 @@ import TagInput from "@/components/Event/TagInput.vue";
|
||||
import FullAddressAutoComplete from "@/components/Event/FullAddressAutoComplete.vue";
|
||||
import IdentityPickerWrapper from "@/views/Account/IdentityPickerWrapper.vue";
|
||||
import Subtitle from "@/components/Utils/Subtitle.vue";
|
||||
import GroupPickerWrapper from "@/components/Group/GroupPickerWrapper.vue";
|
||||
import { Route } from "vue-router";
|
||||
import { formatList } from "@/utils/i18n";
|
||||
import OrganizerPickerWrapper from "../../components/Event/OrganizerPickerWrapper.vue";
|
||||
import {
|
||||
CREATE_EVENT,
|
||||
EDIT_EVENT,
|
||||
@@ -354,7 +365,7 @@ import {
|
||||
LOGGED_USER_DRAFTS,
|
||||
LOGGED_USER_PARTICIPATIONS,
|
||||
} from "../../graphql/actor";
|
||||
import { Group, IPerson, Person } from "../../types/actor";
|
||||
import { IPerson, Person, displayNameAndUsername } from "../../types/actor";
|
||||
import { TAGS } from "../../graphql/tags";
|
||||
import { ITag } from "../../types/tag.model";
|
||||
import { buildFileFromIPicture, buildFileVariable, readFileAsync } from "../../utils/image";
|
||||
@@ -362,12 +373,13 @@ import RouteName from "../../router/name";
|
||||
import "intersection-observer";
|
||||
import { CONFIG } from "../../graphql/config";
|
||||
import { IConfig } from "../../types/config.model";
|
||||
import { RefetchQueryDescription } from "apollo-client/core/watchQueryOptions";
|
||||
|
||||
const DEFAULT_LIMIT_NUMBER_OF_PLACES = 10;
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
GroupPickerWrapper,
|
||||
OrganizerPickerWrapper,
|
||||
Subtitle,
|
||||
IdentityPickerWrapper,
|
||||
FullAddressAutoComplete,
|
||||
@@ -398,6 +410,7 @@ const DEFAULT_LIMIT_NUMBER_OF_PLACES = 10;
|
||||
metaInfo() {
|
||||
return {
|
||||
// if no subcomponents specify a metaInfo.title, this title will be used
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
title: (this.isUpdate ? this.$t("Event edition") : this.$t("Event creation")) as string,
|
||||
// all titles will be injected into this template
|
||||
@@ -444,8 +457,12 @@ export default class EditEvent extends Vue {
|
||||
|
||||
endsOnNull = false;
|
||||
|
||||
displayNameAndUsername = displayNameAndUsername;
|
||||
|
||||
formatList = formatList;
|
||||
|
||||
@Watch("eventId", { immediate: true })
|
||||
resetFormForCreation(eventId: string) {
|
||||
resetFormForCreation(eventId: string): void {
|
||||
if (eventId === undefined) {
|
||||
this.event = new EventModel();
|
||||
}
|
||||
@@ -467,9 +484,10 @@ export default class EditEvent extends Vue {
|
||||
this.event.organizerActor = this.event.organizerActor || this.currentActor;
|
||||
}
|
||||
|
||||
async mounted() {
|
||||
async mounted(): Promise<void> {
|
||||
this.observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const entry of entries) {
|
||||
if (entry) {
|
||||
this.showFixedNavbar = !entry.isIntersecting;
|
||||
@@ -489,16 +507,16 @@ export default class EditEvent extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
createOrUpdateDraft(e: Event) {
|
||||
createOrUpdateDraft(e: Event): void {
|
||||
e.preventDefault();
|
||||
if (this.validateForm()) {
|
||||
if (this.eventId && !this.isDuplicate) return this.updateEvent();
|
||||
if (this.eventId && !this.isDuplicate) this.updateEvent();
|
||||
|
||||
return this.createEvent();
|
||||
this.createEvent();
|
||||
}
|
||||
}
|
||||
|
||||
createOrUpdatePublish(e: Event) {
|
||||
createOrUpdatePublish(e: Event): void {
|
||||
if (this.validateForm()) {
|
||||
this.event.draft = false;
|
||||
this.createOrUpdateDraft(e);
|
||||
@@ -506,21 +524,17 @@ export default class EditEvent extends Vue {
|
||||
}
|
||||
|
||||
@Watch("currentActor")
|
||||
setCurrentActor() {
|
||||
setCurrentActor(): void {
|
||||
this.event.organizerActor = this.currentActor;
|
||||
}
|
||||
|
||||
@Watch("event")
|
||||
setInitialData() {
|
||||
setInitialData(): void {
|
||||
if (this.isUpdate && this.unmodifiedEvent === undefined && this.event && this.event.uuid) {
|
||||
this.unmodifiedEvent = JSON.parse(JSON.stringify(this.event.toEditJSON()));
|
||||
}
|
||||
}
|
||||
|
||||
resetAttributedToOnOrganizerChange() {
|
||||
this.event.attributedTo = new Group();
|
||||
}
|
||||
|
||||
// @Watch('event.attributedTo', { deep: true })
|
||||
// updateHideOrganizerWhenGroupEventOption(attributedTo) {
|
||||
// if (!attributedTo.preferredUsername) {
|
||||
@@ -537,7 +551,7 @@ export default class EditEvent extends Vue {
|
||||
return false;
|
||||
}
|
||||
|
||||
async createEvent() {
|
||||
async createEvent(): Promise<void> {
|
||||
const variables = await this.buildVariables();
|
||||
|
||||
try {
|
||||
@@ -565,7 +579,7 @@ export default class EditEvent extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
async updateEvent() {
|
||||
async updateEvent(): Promise<void> {
|
||||
const variables = await this.buildVariables();
|
||||
|
||||
try {
|
||||
@@ -652,7 +666,8 @@ export default class EditEvent extends Vue {
|
||||
/**
|
||||
* Refresh drafts or participation cache depending if the event is still draft or not
|
||||
*/
|
||||
private postRefetchQueries(updateEvent: IEvent) {
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
private postRefetchQueries(updateEvent: IEvent): RefetchQueryDescription {
|
||||
if (updateEvent.draft) {
|
||||
return [
|
||||
{
|
||||
@@ -670,6 +685,12 @@ export default class EditEvent extends Vue {
|
||||
];
|
||||
}
|
||||
|
||||
get attributedToEqualToOrganizerActor(): boolean {
|
||||
return (this.event.organizerActor &&
|
||||
this.event.attributedTo &&
|
||||
this.event.attributedTo.id === this.event.organizerActor.id) as boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build variables for Event GraphQL creation query
|
||||
*/
|
||||
@@ -680,9 +701,13 @@ export default class EditEvent extends Vue {
|
||||
organizerActorId: this.event.organizerActor.id,
|
||||
});
|
||||
}
|
||||
if (this.event.attributedTo) {
|
||||
res = Object.assign(res, { attributedToId: this.event.attributedTo.id });
|
||||
}
|
||||
const attributedToId =
|
||||
this.event.attributedTo &&
|
||||
!this.attributedToEqualToOrganizerActor &&
|
||||
this.event.attributedTo.id
|
||||
? this.event.attributedTo.id
|
||||
: null;
|
||||
res = Object.assign(res, { attributedToId });
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
@@ -736,7 +761,7 @@ export default class EditEvent extends Vue {
|
||||
}
|
||||
|
||||
@Watch("limitedPlaces")
|
||||
updatedEventCapacityOptions(limitedPlaces: boolean) {
|
||||
updatedEventCapacityOptions(limitedPlaces: boolean): void {
|
||||
if (!limitedPlaces) {
|
||||
this.event.options.maximumAttendeeCapacity = 0;
|
||||
this.event.options.remainingAttendeeCapacity = 0;
|
||||
@@ -748,7 +773,7 @@ export default class EditEvent extends Vue {
|
||||
}
|
||||
|
||||
@Watch("needsApproval")
|
||||
updateEventJoinOptions(needsApproval: boolean) {
|
||||
updateEventJoinOptions(needsApproval: boolean): void {
|
||||
if (needsApproval === true) {
|
||||
this.event.joinOptions = EventJoinOptions.RESTRICTED;
|
||||
} else {
|
||||
@@ -756,16 +781,16 @@ export default class EditEvent extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
get checkTitleLength() {
|
||||
get checkTitleLength(): Array<string | undefined> {
|
||||
return this.event.title.length > 80
|
||||
? ["is-info", this.$t("The event title will be ellipsed.")]
|
||||
? ["is-info", this.$t("The event title will be ellipsed.") as string]
|
||||
: [undefined, undefined];
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm cancel
|
||||
*/
|
||||
confirmGoElsewhere(callback: (value?: string) => any) {
|
||||
confirmGoElsewhere(callback: (value?: string) => any): void | Function {
|
||||
if (!this.isEventModified) {
|
||||
return callback();
|
||||
}
|
||||
@@ -796,11 +821,11 @@ export default class EditEvent extends Vue {
|
||||
/**
|
||||
* Confirm cancel
|
||||
*/
|
||||
confirmGoBack() {
|
||||
confirmGoBack(): void {
|
||||
this.confirmGoElsewhere(() => this.$router.go(-1));
|
||||
}
|
||||
|
||||
beforeRouteLeave(to: Route, from: Route, next: Function) {
|
||||
beforeRouteLeave(to: Route, from: Route, next: Function): void {
|
||||
if (to.name === RouteName.EVENT) return next();
|
||||
this.confirmGoElsewhere(() => next());
|
||||
}
|
||||
@@ -814,7 +839,7 @@ export default class EditEvent extends Vue {
|
||||
}
|
||||
|
||||
@Watch("beginsOn", { deep: true })
|
||||
onBeginsOnChanged(beginsOn: string) {
|
||||
onBeginsOnChanged(beginsOn: string): void {
|
||||
if (!this.event.endsOn) return;
|
||||
const dateBeginsOn = new Date(beginsOn);
|
||||
const dateEndsOn = new Date(this.event.endsOn);
|
||||
|
||||
@@ -36,16 +36,7 @@
|
||||
</popover-actor-card>
|
||||
</span>
|
||||
<span v-else-if="event.organizerActor && event.attributedTo">
|
||||
<i18n path="By {username} and {group}">
|
||||
<popover-actor-card
|
||||
:actor="event.organizerActor"
|
||||
slot="username"
|
||||
:inline="true"
|
||||
>
|
||||
{{
|
||||
$t("@{username}", { username: usernameWithDomain(event.organizerActor) })
|
||||
}}
|
||||
</popover-actor-card>
|
||||
<i18n path="By {group}">
|
||||
<popover-actor-card :actor="event.attributedTo" slot="group" :inline="true">
|
||||
<router-link
|
||||
:to="{
|
||||
@@ -338,11 +329,8 @@
|
||||
:endsOn="event.endsOn"
|
||||
/>
|
||||
</event-metadata-block>
|
||||
<event-metadata-block :title="$t('Contact')">
|
||||
<popover-actor-card
|
||||
:actor="event.organizerActor"
|
||||
v-if="!event.attributedTo || !event.options.hideOrganizerWhenGroupEvent"
|
||||
>
|
||||
<event-metadata-block :title="$tc('Contact', event.contacts.length)">
|
||||
<popover-actor-card :actor="event.organizerActor" v-if="!event.attributedTo">
|
||||
<actor-card :actor="event.organizerActor" />
|
||||
</popover-actor-card>
|
||||
<router-link
|
||||
@@ -359,6 +347,14 @@
|
||||
<actor-card :actor="event.attributedTo" />
|
||||
</popover-actor-card>
|
||||
</router-link>
|
||||
|
||||
<popover-actor-card
|
||||
:actor="contact"
|
||||
v-for="contact in event.contacts"
|
||||
:key="contact.id"
|
||||
>
|
||||
<actor-card :actor="contact" />
|
||||
</popover-actor-card>
|
||||
</event-metadata-block>
|
||||
<event-metadata-block
|
||||
v-if="event.onlineAddress && urlToHostname(event.onlineAddress)"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>{{ group.preferredUsername }}</router-link
|
||||
>{{ group.name }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Watch } from "vue-property-decorator";
|
||||
import { Component, Watch } from "vue-property-decorator";
|
||||
import { Group, IPerson, usernameWithDomain, MemberRole } from "@/types/actor";
|
||||
import { CURRENT_ACTOR_CLIENT, PERSON_MEMBERSHIPS } from "@/graphql/actor";
|
||||
import { CREATE_GROUP } from "@/graphql/group";
|
||||
@@ -84,7 +84,7 @@ export default class CreateGroup extends mixins(IdentityEditionMixin) {
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
|
||||
async createGroup() {
|
||||
async createGroup(): Promise<void> {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: CREATE_GROUP,
|
||||
@@ -125,6 +125,7 @@ export default class CreateGroup extends mixins(IdentityEditionMixin) {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
get host(): string {
|
||||
return window.location.hostname;
|
||||
}
|
||||
@@ -152,10 +153,12 @@ export default class CreateGroup extends mixins(IdentityEditionMixin) {
|
||||
|
||||
if (this.bannerFile) {
|
||||
bannerObj = {
|
||||
picture: {
|
||||
name: this.bannerFile.name,
|
||||
alt: `${this.group.preferredUsername}'s banner`,
|
||||
file: this.bannerFile,
|
||||
banner: {
|
||||
picture: {
|
||||
name: this.bannerFile.name,
|
||||
alt: `${this.group.preferredUsername}'s banner`,
|
||||
file: this.bannerFile,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -173,18 +176,7 @@ export default class CreateGroup extends mixins(IdentityEditionMixin) {
|
||||
}
|
||||
|
||||
private handleError(err: any) {
|
||||
this.errors.push(
|
||||
...err.graphQLErrors.map(({ message }: { message: string }) => this.convertMessage(message))
|
||||
);
|
||||
}
|
||||
|
||||
private convertMessage(message: string): string {
|
||||
switch (message) {
|
||||
case "A group with this name already exists":
|
||||
return this.$t("A group with this name already exists") as string;
|
||||
default:
|
||||
return message;
|
||||
}
|
||||
this.errors.push(...err.graphQLErrors.map(({ message }: { message: string }) => message));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
<header class="block-container presentation">
|
||||
<div class="block-column media">
|
||||
<div class="media-left">
|
||||
<figure class="image rounded is-128x128" v-if="group.avatar">
|
||||
<img :src="group.avatar.url" />
|
||||
<figure class="image is-128x128" v-if="group.avatar">
|
||||
<img class="is-rounded" :src="group.avatar.url" alt="" />
|
||||
</figure>
|
||||
<b-icon v-else size="is-large" icon="account-group" />
|
||||
</div>
|
||||
@@ -56,13 +56,6 @@
|
||||
class="button is-outlined"
|
||||
>{{ $t("Group settings") }}</router-link
|
||||
>
|
||||
<b-button
|
||||
type="is-danger"
|
||||
v-if="isCurrentActorAGroupMember"
|
||||
outlined
|
||||
@click="leaveGroup"
|
||||
>{{ $t("Leave group") }}</b-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -114,6 +107,7 @@
|
||||
>{{ $t("Show map") }}</span
|
||||
>
|
||||
</div>
|
||||
<img v-if="group.banner && group.banner.url" :src="group.banner.url" alt="" />
|
||||
</header>
|
||||
</div>
|
||||
<div v-if="isCurrentActorAGroupMember" class="block-container">
|
||||
@@ -186,50 +180,6 @@
|
||||
>
|
||||
</template>
|
||||
</group-section>
|
||||
<!-- Todos -->
|
||||
<group-section
|
||||
:title="$t('Ongoing tasks')"
|
||||
icon="checkbox-multiple-marked"
|
||||
:route="{
|
||||
name: RouteName.TODO_LISTS,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>
|
||||
<template v-slot:default>
|
||||
<div v-if="group.todoLists.elements.length > 0">
|
||||
<div v-for="todoList in group.todoLists.elements" :key="todoList.id">
|
||||
<router-link :to="{ name: RouteName.TODO_LIST, params: { id: todoList.id } }">
|
||||
<h2 class="is-size-3">
|
||||
{{
|
||||
$tc("{title} ({count} todos)", todoList.todos.total, {
|
||||
count: todoList.todos.total,
|
||||
title: todoList.title,
|
||||
})
|
||||
}}
|
||||
</h2>
|
||||
</router-link>
|
||||
<compact-todo
|
||||
:todo="todo"
|
||||
v-for="todo in todoList.todos.elements.slice(0, 3)"
|
||||
:key="todo.id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="content has-text-grey has-text-centered">
|
||||
<p>{{ $t("No ongoing todos") }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:create>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.TODO_LISTS,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
class="button is-primary"
|
||||
>{{ $t("+ Add a todo") }}</router-link
|
||||
>
|
||||
</template>
|
||||
</group-section>
|
||||
</div>
|
||||
<!-- Public things -->
|
||||
<div class="block-column">
|
||||
@@ -349,7 +299,7 @@
|
||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||
import EventCard from "@/components/Event/EventCard.vue";
|
||||
import { CURRENT_ACTOR_CLIENT, PERSON_MEMBERSHIPS } from "@/graphql/actor";
|
||||
import { FETCH_GROUP, LEAVE_GROUP } from "@/graphql/group";
|
||||
import { FETCH_GROUP } from "@/graphql/group";
|
||||
import {
|
||||
IActor,
|
||||
IGroup,
|
||||
@@ -369,7 +319,6 @@ import FolderItem from "@/components/Resource/FolderItem.vue";
|
||||
import { Address } from "@/types/address.model";
|
||||
import Invitations from "@/components/Group/Invitations.vue";
|
||||
import addMinutes from "date-fns/addMinutes";
|
||||
import { Route } from "vue-router";
|
||||
import GroupSection from "../../components/Group/GroupSection.vue";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@@ -451,16 +400,6 @@ export default class Group extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
async leaveGroup(): Promise<Route> {
|
||||
await this.$apollo.mutate({
|
||||
mutation: LEAVE_GROUP,
|
||||
variables: {
|
||||
groupId: this.group.id,
|
||||
},
|
||||
});
|
||||
return this.$router.push({ name: RouteName.MY_GROUPS });
|
||||
}
|
||||
|
||||
acceptInvitation(): void {
|
||||
if (this.groupMember) {
|
||||
const index = this.person.memberships.elements.findIndex(
|
||||
@@ -477,7 +416,7 @@ export default class Group extends Vue {
|
||||
|
||||
get groupTitle(): undefined | string {
|
||||
if (!this.group) return undefined;
|
||||
return this.group.preferredUsername;
|
||||
return this.group.name || this.group.preferredUsername;
|
||||
}
|
||||
|
||||
get groupSummary(): undefined | string {
|
||||
@@ -583,6 +522,8 @@ div.container {
|
||||
&.presentation {
|
||||
border: 2px solid $purple-2;
|
||||
padding: 10px 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
h1 {
|
||||
color: $purple-1;
|
||||
@@ -593,6 +534,20 @@ div.container {
|
||||
.button.is-outlined {
|
||||
border-color: $purple-2;
|
||||
}
|
||||
|
||||
& > *:not(img) {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
& > img {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.members {
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
<template>
|
||||
<section class="container section">
|
||||
<h1>{{ $t("Group List") }} ({{ groups.total }})</h1>
|
||||
<b-loading :active.sync="$apollo.loading" />
|
||||
<div class="columns">
|
||||
<GroupMemberCard
|
||||
v-for="group in groups.elements"
|
||||
:key="group.uuid"
|
||||
:group="group"
|
||||
class="column is-one-quarter-desktop is-half-mobile"
|
||||
/>
|
||||
</div>
|
||||
<router-link class="button" :to="{ name: RouteName.CREATE_GROUP }">{{
|
||||
$t("Create group")
|
||||
}}</router-link>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import { LIST_GROUPS } from "@/graphql/group";
|
||||
import { Group, IGroup } from "@/types/actor";
|
||||
import GroupMemberCard from "@/components/Group/GroupMemberCard.vue";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
groups: {
|
||||
query: {
|
||||
query: LIST_GROUPS,
|
||||
fetchPolicy: "network-only",
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
GroupMemberCard,
|
||||
},
|
||||
})
|
||||
export default class GroupList extends Vue {
|
||||
groups!: Paginate<IGroup>;
|
||||
|
||||
loading = true;
|
||||
|
||||
RouteName = RouteName;
|
||||
//
|
||||
// usernameWithDomain(actor) {
|
||||
// return actor.username + (actor.domain === null ? '' : `@${actor.domain}`);
|
||||
// }
|
||||
|
||||
// viewActor(actor) {
|
||||
// this.$router.push({
|
||||
// name: RouteName.GROUP,
|
||||
// params: { name: this.usernameWithDomain(actor) },
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// joinGroup(group) {
|
||||
// const router = this.$router;
|
||||
// // FIXME: remove eventFetch
|
||||
// // eventFetch(`/groups/${this.usernameWithDomain(group)}/join`, this.$store, { method: 'POST' })
|
||||
// // .then(response => response.json())
|
||||
// // .then(() => router.push({ name: 'Group', params: { name: this.usernameWithDomain(group) } }));
|
||||
// }
|
||||
}
|
||||
</script>
|
||||
@@ -210,14 +210,14 @@ export default class GroupMembers extends Vue {
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
|
||||
mounted() {
|
||||
mounted(): void {
|
||||
const roleQuery = this.$route.query.role as string;
|
||||
if (Object.values(MemberRole).includes(roleQuery as MemberRole)) {
|
||||
this.roles = roleQuery as MemberRole;
|
||||
}
|
||||
}
|
||||
|
||||
async inviteMember() {
|
||||
async inviteMember(): Promise<void> {
|
||||
await this.$apollo.mutate<{ inviteMember: IMember }>({
|
||||
mutation: INVITE_MEMBER,
|
||||
variables: {
|
||||
@@ -253,7 +253,7 @@ export default class GroupMembers extends Vue {
|
||||
}
|
||||
|
||||
@Watch("page")
|
||||
loadMoreMembers() {
|
||||
loadMoreMembers(): void {
|
||||
this.$apollo.queries.event.fetchMore({
|
||||
// New variables
|
||||
variables: {
|
||||
@@ -279,7 +279,7 @@ export default class GroupMembers extends Vue {
|
||||
});
|
||||
}
|
||||
|
||||
async removeMember(memberId: string) {
|
||||
async removeMember(memberId: string): Promise<void> {
|
||||
await this.$apollo.mutate<{ removeMember: IMember }>({
|
||||
mutation: REMOVE_MEMBER,
|
||||
variables: {
|
||||
@@ -310,15 +310,15 @@ export default class GroupMembers extends Vue {
|
||||
});
|
||||
}
|
||||
|
||||
promoteMember(memberId: string) {
|
||||
promoteMember(memberId: string): Promise<void> {
|
||||
return this.updateMember(memberId, MemberRole.ADMINISTRATOR);
|
||||
}
|
||||
|
||||
demoteMember(memberId: string) {
|
||||
demoteMember(memberId: string): Promise<void> {
|
||||
return this.updateMember(memberId, MemberRole.MEMBER);
|
||||
}
|
||||
|
||||
async updateMember(memberId: string, role: MemberRole) {
|
||||
async updateMember(memberId: string, role: MemberRole): Promise<void> {
|
||||
await this.$apollo.mutate<{ updateMember: IMember }>({
|
||||
mutation: UPDATE_MEMBER,
|
||||
variables: {
|
||||
|
||||
@@ -37,8 +37,23 @@
|
||||
<b-input v-model="group.name" />
|
||||
</b-field>
|
||||
<b-field :label="$t('Group short description')">
|
||||
<editor mode="basic" v-model="group.summary"
|
||||
<editor mode="basic" v-model="group.summary" :maxSize="500"
|
||||
/></b-field>
|
||||
<b-field :label="$t('Avatar')">
|
||||
<picture-upload
|
||||
:textFallback="$t('Avatar')"
|
||||
v-model="avatarFile"
|
||||
:defaultImageSrc="group.avatar ? group.avatar.url : null"
|
||||
/>
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('Banner')">
|
||||
<picture-upload
|
||||
:textFallback="$t('Banner')"
|
||||
v-model="bannerFile"
|
||||
:defaultImageSrc="group.banner ? group.banner.url : null"
|
||||
/>
|
||||
</b-field>
|
||||
<p class="label">{{ $t("Group visibility") }}</p>
|
||||
<div class="field">
|
||||
<b-radio
|
||||
@@ -106,6 +121,7 @@
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import FullAddressAutoComplete from "@/components/Event/FullAddressAutoComplete.vue";
|
||||
import { Route } from "vue-router";
|
||||
import PictureUpload from "@/components/PictureUpload.vue";
|
||||
import RouteName from "../../router/name";
|
||||
import { FETCH_GROUP, UPDATE_GROUP, DELETE_GROUP } from "../../graphql/group";
|
||||
import { IGroup, usernameWithDomain } from "../../types/actor";
|
||||
@@ -129,6 +145,7 @@ import { Group } from "../../types/actor/group.model";
|
||||
},
|
||||
components: {
|
||||
FullAddressAutoComplete,
|
||||
PictureUpload,
|
||||
editor: () => import("../../components/Editor.vue"),
|
||||
},
|
||||
})
|
||||
@@ -141,6 +158,10 @@ export default class GroupSettings extends Vue {
|
||||
|
||||
newMemberUsername = "";
|
||||
|
||||
avatarFile: File | null = null;
|
||||
|
||||
bannerFile: File | null = null;
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
|
||||
GroupVisibility = {
|
||||
@@ -151,19 +172,12 @@ export default class GroupSettings extends Vue {
|
||||
showCopiedTooltip = false;
|
||||
|
||||
async updateGroup(): Promise<void> {
|
||||
const variables = { ...this.group };
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
delete variables.__typename;
|
||||
if (variables.physicalAddress) {
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
delete variables.physicalAddress.__typename;
|
||||
}
|
||||
const variables = this.buildVariables();
|
||||
await this.$apollo.mutate<{ updateGroup: IGroup }>({
|
||||
mutation: UPDATE_GROUP,
|
||||
variables,
|
||||
});
|
||||
this.$notifier.success(this.$t("Group settings saved") as string);
|
||||
}
|
||||
|
||||
confirmDeleteGroup(): void {
|
||||
@@ -198,6 +212,52 @@ export default class GroupSettings extends Vue {
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
private buildVariables() {
|
||||
let avatarObj = {};
|
||||
let bannerObj = {};
|
||||
const variables = { ...this.group };
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
delete variables.__typename;
|
||||
if (variables.physicalAddress) {
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
delete variables.physicalAddress.__typename;
|
||||
}
|
||||
delete variables.avatar;
|
||||
delete variables.banner;
|
||||
|
||||
if (this.avatarFile) {
|
||||
avatarObj = {
|
||||
avatar: {
|
||||
picture: {
|
||||
name: this.avatarFile.name,
|
||||
alt: `${this.group.preferredUsername}'s avatar`,
|
||||
file: this.avatarFile,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (this.bannerFile) {
|
||||
bannerObj = {
|
||||
banner: {
|
||||
picture: {
|
||||
name: this.bannerFile.name,
|
||||
alt: `${this.group.preferredUsername}'s banner`,
|
||||
file: this.bannerFile,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
...variables,
|
||||
...avatarObj,
|
||||
...bannerObj,
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
get canShowCopyButton(): boolean {
|
||||
return window.isSecureContext;
|
||||
|
||||
@@ -8,7 +8,11 @@
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<router-link :to="{ name: RouteName.CREATE_GROUP }">{{ $t("Create group") }}</router-link>
|
||||
<div class="buttons">
|
||||
<router-link class="button is-primary" :to="{ name: RouteName.CREATE_GROUP }">{{
|
||||
$t("Create group")
|
||||
}}</router-link>
|
||||
</div>
|
||||
<b-loading :active.sync="$apollo.loading"></b-loading>
|
||||
<invitations
|
||||
:invitations="invitations"
|
||||
@@ -16,7 +20,13 @@
|
||||
@rejectInvitation="rejectInvitation"
|
||||
/>
|
||||
<section v-if="memberships && memberships.length > 0">
|
||||
<GroupMemberCard v-for="member in memberships" :key="member.id" :member="member" />
|
||||
<GroupMemberCard
|
||||
class="group-member-card"
|
||||
v-for="member in memberships"
|
||||
:key="member.id"
|
||||
:member="member"
|
||||
@leave="leaveGroup(member.parent)"
|
||||
/>
|
||||
</section>
|
||||
<b-message v-if="$apollo.loading === false && memberships.length === 0" type="is-danger">
|
||||
{{ $t("No groups found") }}
|
||||
@@ -27,10 +37,13 @@
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import { LOGGED_USER_MEMBERSHIPS } from "@/graphql/actor";
|
||||
import { LEAVE_GROUP } from "@/graphql/group";
|
||||
import GroupMemberCard from "@/components/Group/GroupMemberCard.vue";
|
||||
import Invitations from "@/components/Group/Invitations.vue";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
import { IMember, MemberRole, usernameWithDomain } from "@/types/actor";
|
||||
import { IGroup, IMember, MemberRole, usernameWithDomain } from "@/types/actor";
|
||||
import { Route } from "vue-router";
|
||||
import { ApolloError } from "apollo-client";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
@@ -64,14 +77,14 @@ export default class MyEvents extends Vue {
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
acceptInvitation(member: IMember) {
|
||||
acceptInvitation(member: IMember): Promise<Route> {
|
||||
return this.$router.push({
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: usernameWithDomain(member.parent) },
|
||||
});
|
||||
}
|
||||
|
||||
rejectInvitation({ id: memberId }: { id: string }) {
|
||||
rejectInvitation({ id: memberId }: { id: string }): void {
|
||||
const index = this.membershipsPages.elements.findIndex(
|
||||
(membership) => membership.role === MemberRole.INVITED && membership.id === memberId
|
||||
);
|
||||
@@ -81,14 +94,23 @@ export default class MyEvents extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
get invitations() {
|
||||
async leaveGroup(group: IGroup): Promise<void> {
|
||||
await this.$apollo.mutate({
|
||||
mutation: LEAVE_GROUP,
|
||||
variables: {
|
||||
groupId: group.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
get invitations(): IMember[] {
|
||||
if (!this.membershipsPages) return [];
|
||||
return this.membershipsPages.elements.filter(
|
||||
(member: IMember) => member.role === MemberRole.INVITED
|
||||
);
|
||||
}
|
||||
|
||||
get memberships() {
|
||||
get memberships(): IMember[] {
|
||||
if (!this.membershipsPages) return [];
|
||||
return this.membershipsPages.elements.filter(
|
||||
(member: IMember) => ![MemberRole.INVITED, MemberRole.REJECTED].includes(member.role)
|
||||
@@ -114,4 +136,8 @@ section {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
|
||||
.group-member-card {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -23,10 +23,8 @@
|
||||
</aside>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Watch } from "vue-property-decorator";
|
||||
import { Route } from "vue-router";
|
||||
import { IGroup, IPerson } from "@/types/actor";
|
||||
import { FETCH_GROUP } from "@/graphql/group";
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import { IGroup } from "@/types/actor";
|
||||
import RouteName from "../../router/name";
|
||||
import SettingMenuSection from "../../components/Settings/SettingMenuSection.vue";
|
||||
import SettingMenuItem from "../../components/Settings/SettingMenuItem.vue";
|
||||
|
||||
@@ -42,14 +42,11 @@
|
||||
import { Component, Vue, Prop } from "vue-property-decorator";
|
||||
import Editor from "@/components/Editor.vue";
|
||||
import { GraphQLError } from "graphql";
|
||||
import { FETCH_GROUP } from "@/graphql/group";
|
||||
import { CURRENT_ACTOR_CLIENT, PERSON_MEMBERSHIPS } from "../../graphql/actor";
|
||||
import { TAGS } from "../../graphql/tags";
|
||||
import { CONFIG } from "../../graphql/config";
|
||||
import { FETCH_POST, CREATE_POST } from "../../graphql/post";
|
||||
import { FETCH_POST } from "../../graphql/post";
|
||||
|
||||
import { IPost, PostVisibility } from "../../types/post.model";
|
||||
import { IGroup, IMember, usernameWithDomain } from "../../types/actor";
|
||||
import { IPost } from "../../types/post.model";
|
||||
import { IMember, usernameWithDomain } from "../../types/actor";
|
||||
import RouteName from "../../router/name";
|
||||
import Tag from "../../components/Tag.vue";
|
||||
|
||||
@@ -90,11 +87,11 @@ import Tag from "../../components/Tag.vue";
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
title: this.post ? this.post.title : "",
|
||||
// all titles will be injected into this template
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
titleTemplate: this.post ? "%s | Mobilizon" : "Mobilizon",
|
||||
};
|
||||
@@ -116,7 +113,7 @@ export default class Post extends Vue {
|
||||
return this.memberships.map(({ parent: { id } }) => id).includes(this.post.attributedTo.id);
|
||||
}
|
||||
|
||||
async handleErrors(errors: GraphQLError[]) {
|
||||
async handleErrors(errors: GraphQLError[]): Promise<void> {
|
||||
if (errors[0].message.includes("No such post")) {
|
||||
await this.$router.push({ name: RouteName.PAGE_NOT_FOUND });
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: usernameWithDomain(resource.actor) },
|
||||
}"
|
||||
>{{ resource.actor.preferredUsername }}</router-link
|
||||
>{{ resource.actor.name }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
|
||||
@@ -177,6 +177,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Ref } from "vue-property-decorator";
|
||||
import { Route } from "vue-router";
|
||||
import { CHANGE_EMAIL, CHANGE_PASSWORD, DELETE_ACCOUNT, LOGGED_USER } from "../../graphql/user";
|
||||
import RouteName from "../../router/name";
|
||||
import { IUser, IAuthProvider } from "../../types/current-user.model";
|
||||
@@ -210,7 +211,7 @@ export default class AccountSettings extends Vue {
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
async resetEmailAction() {
|
||||
async resetEmailAction(): Promise<void> {
|
||||
this.changeEmailErrors = [];
|
||||
|
||||
try {
|
||||
@@ -234,7 +235,7 @@ export default class AccountSettings extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
async resetPasswordAction() {
|
||||
async resetPasswordAction(): Promise<void> {
|
||||
this.changePasswordErrors = [];
|
||||
|
||||
try {
|
||||
@@ -252,12 +253,12 @@ export default class AccountSettings extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
protected async openDeleteAccountModal() {
|
||||
protected openDeleteAccountModal(): void {
|
||||
this.passwordForAccountDeletion = "";
|
||||
this.isDeleteAccountModalActive = true;
|
||||
}
|
||||
|
||||
async deleteAccount() {
|
||||
async deleteAccount(): Promise<Route | void> {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: DELETE_ACCOUNT,
|
||||
@@ -275,19 +276,19 @@ export default class AccountSettings extends Vue {
|
||||
|
||||
return await this.$router.push({ name: RouteName.HOME });
|
||||
} catch (err) {
|
||||
this.handleErrors("delete", err);
|
||||
return this.handleErrors("delete", err);
|
||||
}
|
||||
}
|
||||
|
||||
get canChangePassword() {
|
||||
get canChangePassword(): boolean {
|
||||
return !this.loggedUser.provider;
|
||||
}
|
||||
|
||||
get canChangeEmail() {
|
||||
get canChangeEmail(): boolean {
|
||||
return !this.loggedUser.provider;
|
||||
}
|
||||
|
||||
providerName(id: string) {
|
||||
static providerName(id: string): string {
|
||||
if (SELECTED_PROVIDERS[id]) {
|
||||
return SELECTED_PROVIDERS[id];
|
||||
}
|
||||
@@ -307,31 +308,17 @@ export default class AccountSettings extends Vue {
|
||||
if (err.graphQLErrors !== undefined) {
|
||||
err.graphQLErrors.forEach(({ message }: { message: string }) => {
|
||||
switch (type) {
|
||||
case "email":
|
||||
this.changeEmailErrors.push(this.convertMessage(message) as string);
|
||||
break;
|
||||
case "password":
|
||||
this.changePasswordErrors.push(this.convertMessage(message) as string);
|
||||
this.changePasswordErrors.push(message);
|
||||
break;
|
||||
case "email":
|
||||
default:
|
||||
this.changeEmailErrors.push(message);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private convertMessage(message: string) {
|
||||
switch (message) {
|
||||
case "The password provided is invalid":
|
||||
return this.$t("The password provided is invalid");
|
||||
case "The new email must be different":
|
||||
return this.$t("The new email must be different");
|
||||
case "The new email doesn't seem to be valid":
|
||||
return this.$t("The new email doesn't seem to be valid");
|
||||
case "The current password is invalid":
|
||||
return this.$t("The current password is invalid");
|
||||
case "The new password must be different":
|
||||
return this.$t("The new password must be different");
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: todo.todoList.actor.preferredUsername },
|
||||
}"
|
||||
>{{ todo.todoList.actor.preferredUsername }}</router-link
|
||||
>{{ todo.todoList.actor.name }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: todoList.actor.preferredUsername },
|
||||
}"
|
||||
>{{ todoList.actor.preferredUsername }}</router-link
|
||||
>{{ todoList.actor.name }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>{{ group.preferredUsername }}</router-link
|
||||
>{{ group.name }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
|
||||
@@ -30,33 +30,8 @@
|
||||
})
|
||||
}}</b-message
|
||||
>
|
||||
<b-message title="Error" type="is-danger" v-for="error in errors" :key="error">
|
||||
<span v-if="error === LoginError.USER_NOT_CONFIRMED">
|
||||
<span>
|
||||
{{
|
||||
$t(
|
||||
"The user account you're trying to login as has not been confirmed yet. Check your email inbox and eventually your spam folder."
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<i18n path="You may also ask to {resend_confirmation_email}.">
|
||||
<router-link
|
||||
slot="resend_confirmation_email"
|
||||
:to="{ name: RouteName.RESEND_CONFIRMATION }"
|
||||
>{{ $t("resend confirmation email") }}</router-link
|
||||
>
|
||||
</i18n>
|
||||
</span>
|
||||
<span v-if="error === LoginError.USER_EMAIL_PASSWORD_INVALID">{{
|
||||
$t("Impossible to login, your email or password seems incorrect.")
|
||||
}}</span>
|
||||
<!-- TODO: Shouldn't we hide this information? -->
|
||||
<span v-if="error === LoginError.USER_DOES_NOT_EXIST">{{
|
||||
$t("No user account with this email was found. Maybe you made a typo?")
|
||||
}}</span>
|
||||
<span v-if="error === LoginError.USER_DISABLED">
|
||||
{{ $t("This user has been disabled") }}
|
||||
</span>
|
||||
<b-message :title="$t('Error')" type="is-danger" v-for="error in errors" :key="error">
|
||||
{{ error }}
|
||||
</b-message>
|
||||
<form @submit="loginAction">
|
||||
<b-field :label="$t('Email')" label-for="email">
|
||||
@@ -80,9 +55,10 @@
|
||||
/>
|
||||
</b-field>
|
||||
|
||||
<p class="control has-text-centered">
|
||||
<p class="control has-text-centered" v-if="!submitted">
|
||||
<button class="button is-primary is-large">{{ $t("Login") }}</button>
|
||||
</p>
|
||||
<b-loading :is-full-page="false" v-model="submitted" />
|
||||
|
||||
<div class="control" v-if="config && config.auth.oauthProviders.length > 0">
|
||||
<auth-providers :oauthProviders="config.auth.oauthProviders" />
|
||||
@@ -121,6 +97,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { Route } from "vue-router";
|
||||
import { LOGIN } from "../../graphql/auth";
|
||||
import { validateEmailField, validateRequiredField } from "../../utils/validators";
|
||||
import { initializeCurrentActor, NoIdentitiesException, saveUserData } from "../../utils/auth";
|
||||
@@ -185,7 +162,9 @@ export default class Login extends Vue {
|
||||
|
||||
private redirect: string | null = null;
|
||||
|
||||
mounted() {
|
||||
submitted = false;
|
||||
|
||||
mounted(): void {
|
||||
this.credentials.email = this.email;
|
||||
this.credentials.password = this.password;
|
||||
|
||||
@@ -194,12 +173,16 @@ export default class Login extends Vue {
|
||||
this.redirect = query.redirect as string;
|
||||
}
|
||||
|
||||
async loginAction(e: Event) {
|
||||
async loginAction(e: Event): Promise<Route | void> {
|
||||
e.preventDefault();
|
||||
if (this.submitted) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.errors = [];
|
||||
|
||||
try {
|
||||
this.submitted = true;
|
||||
const { data } = await this.$apollo.mutate<{ login: ILogin }>({
|
||||
mutation: LOGIN,
|
||||
variables: {
|
||||
@@ -242,6 +225,7 @@ export default class Login extends Vue {
|
||||
window.localStorage.setItem("welcome-back", "yes");
|
||||
return this.$router.push({ name: RouteName.HOME });
|
||||
} catch (err) {
|
||||
this.submitted = false;
|
||||
console.error(err);
|
||||
err.graphQLErrors.forEach(({ message }: { message: string }) => {
|
||||
this.errors.push(message);
|
||||
|
||||
@@ -44,8 +44,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<b-message type="is-warning" v-if="config.registrationsWhitelist">
|
||||
{{ $t("Registrations are restricted by whitelisting.") }}
|
||||
<b-message type="is-warning" v-if="config.registrationsAllowlist">
|
||||
{{ $t("Registrations are restricted by allowlisting.") }}
|
||||
</b-message>
|
||||
<form v-on:submit.prevent="submit()">
|
||||
<b-field
|
||||
|
||||
@@ -19,14 +19,7 @@
|
||||
:key="error"
|
||||
@close="removeError(error)"
|
||||
>
|
||||
<span v-if="error == ResetError.USER_IMPOSSIBLE_TO_RESET">
|
||||
{{
|
||||
$t(
|
||||
"You can't reset your password because you use a 3rd-party auth provider to login."
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span v-else>{{ error }}</span>
|
||||
{{ error }}
|
||||
</b-message>
|
||||
<form @submit="sendResetPasswordTokenAction" v-if="!validationSent">
|
||||
<b-field :label="$t('Email address')">
|
||||
@@ -59,7 +52,6 @@ import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { validateEmailField, validateRequiredField } from "../../utils/validators";
|
||||
import { SEND_RESET_PASSWORD } from "../../graphql/auth";
|
||||
import RouteName from "../../router/name";
|
||||
import { ResetError } from "../../types/login-error-code.model";
|
||||
|
||||
@Component
|
||||
export default class SendPasswordReset extends Vue {
|
||||
@@ -75,8 +67,6 @@ export default class SendPasswordReset extends Vue {
|
||||
|
||||
errors: string[] = [];
|
||||
|
||||
ResetError = ResetError;
|
||||
|
||||
state = {
|
||||
email: {
|
||||
status: null,
|
||||
@@ -89,15 +79,15 @@ export default class SendPasswordReset extends Vue {
|
||||
email: validateEmailField,
|
||||
};
|
||||
|
||||
mounted() {
|
||||
mounted(): void {
|
||||
this.credentials.email = this.email;
|
||||
}
|
||||
|
||||
removeError(message: string) {
|
||||
removeError(message: string): void {
|
||||
this.errors.splice(this.errors.indexOf(message));
|
||||
}
|
||||
|
||||
async sendResetPasswordTokenAction(e: Event) {
|
||||
async sendResetPasswordTokenAction(e: Event): Promise<void> {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
@@ -119,7 +109,7 @@ export default class SendPasswordReset extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
resetState() {
|
||||
resetState(): void {
|
||||
this.state = {
|
||||
email: {
|
||||
status: null,
|
||||
|
||||
Reference in New Issue
Block a user