Make identity picker work with many profiles and groups

Close #784

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2021-08-11 12:43:54 +02:00
parent 12523b6ae4
commit 1e75713b1c
No known key found for this signature in database
GPG key ID: A061B9DDE0CA0773
6 changed files with 60 additions and 252 deletions

View file

@ -1,11 +1,14 @@
<template> <template>
<div class="list is-hoverable"> <div class="list is-hoverable">
<b-input
:placeholder="$t('Filter by profile or group name')"
v-model="actorFilter"
/>
<b-radio-button <b-radio-button
v-model="selectedActor" v-model="selectedActor"
:native-value="availableActor" :native-value="availableActor"
class="list-item" class="list-item"
v-for="availableActor in actualAvailableActors" v-for="availableActor in actualFilteredAvailableActors"
:class="{ 'is-active': availableActor.id === selectedActor.id }"
:key="availableActor.id" :key="availableActor.id"
> >
<div class="media"> <div class="media">
@ -61,6 +64,8 @@ export default class OrganizerPicker extends Vue {
currentActor!: IPerson; currentActor!: IPerson;
actorFilter = "";
get selectedActor(): IActor | undefined { get selectedActor(): IActor | undefined {
if (this.value?.id) { if (this.value?.id) {
return this.value; return this.value;
@ -103,6 +108,16 @@ export default class OrganizerPicker extends Vue {
...this.actualMemberships.map((member) => member.parent), ...this.actualMemberships.map((member) => member.parent),
].filter((elem) => elem); ].filter((elem) => elem);
} }
get actualFilteredAvailableActors(): IActor[] {
return this.actualAvailableActors.filter((actor) => {
return [
actor.preferredUsername.toLowerCase(),
actor.name?.toLowerCase(),
actor.domain?.toLowerCase(),
].some((match) => match?.includes(this.actorFilter.toLowerCase()));
});
}
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -52,17 +52,25 @@
</header> </header>
<section class="modal-card-body"> <section class="modal-card-body">
<div class="columns"> <div class="columns">
<div class="column"> <div class="column actor-picker">
<organizer-picker <organizer-picker
v-model="selectedActor" v-model="selectedActor"
@input="relay" @input="relay"
:restrict-moderator-level="true" :restrict-moderator-level="true"
/> />
</div> </div>
<div class="column"> <div class="column contact-picker">
<div v-if="actorMembers.length > 0"> <div v-if="isSelectedActorAGroup && actorMembers.length > 0">
<p>{{ $t("Add a contact") }}</p> <p>{{ $t("Add a contact") }}</p>
<p class="field" v-for="actor in actorMembers" :key="actor.id"> <b-input
:placeholder="$t('Filter by name')"
v-model="contactFilter"
/>
<p
class="field"
v-for="actor in filteredActorMembers"
:key="actor.id"
>
<b-checkbox v-model="actualContacts" :native-value="actor.id"> <b-checkbox v-model="actualContacts" :native-value="actor.id">
<div class="media"> <div class="media">
<div class="media-left"> <div class="media-left">
@ -88,7 +96,7 @@
</b-checkbox> </b-checkbox>
</p> </p>
</div> </div>
<div v-else class="content has-text-grey has-text-centered"> <div v-else class="content has-text-grey-dark has-text-centered">
<p>{{ $t("Your profile will be shown as contact.") }}</p> <p>{{ $t("Your profile will be shown as contact.") }}</p>
</div> </div>
</div> </div>
@ -167,6 +175,8 @@ export default class OrganizerPickerWrapper extends Vue {
isComponentModalActive = false; isComponentModalActive = false;
contactFilter = "";
usernameWithDomain = usernameWithDomain; usernameWithDomain = usernameWithDomain;
@Prop({ type: Array, required: false, default: () => [] }) @Prop({ type: Array, required: false, default: () => [] })
@ -226,19 +236,33 @@ export default class OrganizerPickerWrapper extends Vue {
} }
get actorMembers(): IActor[] { get actorMembers(): IActor[] {
if (this.selectedActor?.type === ActorType.GROUP) { if (this.isSelectedActorAGroup) {
return this.members.elements.map(({ actor }: { actor: IActor }) => actor); return this.members.elements.map(({ actor }: { actor: IActor }) => actor);
} }
return []; return [];
} }
get filteredActorMembers(): IActor[] {
return this.actorMembers.filter((actor) => {
return [
actor.preferredUsername.toLowerCase(),
actor.name?.toLowerCase(),
actor.domain?.toLowerCase(),
].some((match) => match?.includes(this.contactFilter.toLowerCase()));
});
}
get isSelectedActorAGroup(): boolean {
return this.selectedActor?.type === ActorType.GROUP;
}
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.group-picker { .modal-card-body .columns .column {
.block, &.actor-picker,
.no-group, &.contact-picker {
.inline { overflow-y: auto;
cursor: pointer; max-height: 400px;
} }
} }
</style> </style>

View file

@ -1,105 +0,0 @@
<template>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">{{ $t("Pick a group") }}</p>
</header>
<section class="modal-card-body">
<div class="list is-hoverable">
<a
class="list-item"
v-for="groupMembership in actualMemberships"
:class="{
'is-active': groupMembership.parent.id === currentGroup.id,
}"
@click="changeCurrentGroup(groupMembership.parent)"
:key="groupMembership.id"
>
<div class="media">
<img
class="media-left image is-48x48"
v-if="groupMembership.parent.avatar"
:src="groupMembership.parent.avatar.url"
alt=""
/>
<b-icon
class="media-left"
v-else
size="is-large"
icon="account-circle"
/>
<div class="media-content">
<h3>@{{ groupMembership.parent.name }}</h3>
<small>{{
`@${groupMembership.parent.preferredUsername}`
}}</small>
</div>
</div>
</a>
<a
class="list-item"
@click="changeCurrentGroup(new Group())"
v-if="currentGroup.id"
>
<h3>{{ $t("Unset group") }}</h3>
</a>
</div>
</section>
<slot name="footer" />
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { IGroup, IPerson, Group } from "@/types/actor";
import { PERSON_MEMBERSHIPS } from "@/graphql/actor";
import { Paginate } from "@/types/paginate";
import { IMember } from "@/types/actor/member.model";
import { MemberRole } from "@/types/enums";
@Component({
apollo: {
groupMemberships: {
query: PERSON_MEMBERSHIPS,
variables() {
return {
id: this.identity.id,
};
},
update: (data) => data.person.memberships,
skip() {
return !this.identity.id;
},
},
},
})
export default class GroupPicker extends Vue {
@Prop() value!: IGroup;
@Prop() identity!: IPerson;
@Prop({ required: false, default: false }) restrictModeratorLevel!: boolean;
groupMemberships: Paginate<IMember> = { elements: [], total: 0 };
currentGroup: IGroup = this.value;
Group = Group;
changeCurrentGroup(group: IGroup): void {
this.currentGroup = group;
this.$emit("input", group);
}
get actualMemberships(): IMember[] {
if (this.restrictModeratorLevel) {
return this.groupMemberships.elements.filter((membership: IMember) =>
[
MemberRole.ADMINISTRATOR,
MemberRole.MODERATOR,
MemberRole.CREATOR,
].includes(membership.role)
);
}
return this.groupMemberships.elements;
}
}
</script>

View file

@ -1,132 +0,0 @@
<template>
<div class="group-picker">
<div
class="no-group box"
v-if="!currentGroup.id && groupMemberships.total > 0"
@click="isComponentModalActive = true"
>
<p class="is-4">{{ $t("Add a group") }}</p>
<p class="is-6 is-size-6 has-text-grey">
{{ $t("The event will show the group as organizer.") }}
</p>
</div>
<div
v-if="inline && currentGroup.id"
class="inline box"
@click="isComponentModalActive = true"
>
<div class="media">
<div class="media-left">
<figure class="image is-48x48" v-if="currentGroup.avatar">
<img
class="image is-rounded"
:src="currentGroup.avatar.url"
:alt="currentGroup.avatar.alt"
/>
</figure>
<b-icon v-else size="is-large" icon="account-circle" />
</div>
<div class="media-content" v-if="currentGroup.name">
<p class="is-4">{{ currentGroup.name }}</p>
<p class="is-6 has-text-grey">
{{ `@${currentGroup.preferredUsername}` }}
</p>
</div>
<div class="media-content" v-else>
{{ `@${currentGroup.preferredUsername}` }}
</div>
<b-button type="is-text" @click="isComponentModalActive = true">
{{ $t("Change") }}
</b-button>
</div>
</div>
<span
v-else-if="currentGroup.id"
class="block"
@click="isComponentModalActive = true"
>
<img
class="image is-48x48"
v-if="currentGroup.avatar"
:src="currentGroup.avatar.url"
:alt="currentGroup.avatar.alt"
/>
<b-icon v-else size="is-large" icon="account-circle" />
</span>
<div v-if="groupMemberships.total === 0" class="box">
<p class="is-4">
{{ $t("This identity is not a member of any group.") }}
</p>
<p class="is-6 is-size-6 has-text-grey">
{{ $t("You need to create the group before you create an event.") }}
</p>
</div>
<b-modal :active.sync="isComponentModalActive" has-modal-card>
<group-picker
v-model="currentGroup"
:identity.sync="identity"
@input="relay"
:restrict-moderator-level="true"
/>
</b-modal>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import { IMember } from "@/types/actor/member.model";
import { IGroup, IPerson } from "../../types/actor";
import GroupPicker from "./GroupPicker.vue";
import { PERSON_MEMBERSHIPS } from "../../graphql/actor";
import { Paginate } from "../../types/paginate";
@Component({
components: { GroupPicker },
apollo: {
groupMemberships: {
query: PERSON_MEMBERSHIPS,
variables() {
return {
id: this.identity.id,
};
},
update: (data) => data.person.memberships,
skip() {
return !this.identity.id;
},
},
},
})
export default class GroupPickerWrapper extends Vue {
@Prop({ type: Object, required: true }) value!: IGroup;
@Prop({ default: true, type: Boolean }) inline!: boolean;
@Prop({ type: Object, required: true }) identity!: IPerson;
isComponentModalActive = false;
currentGroup: IGroup = this.value;
groupMemberships: Paginate<IMember> = { elements: [], total: 0 };
@Watch("value")
updateCurrentGroup(value: IGroup): void {
this.currentGroup = value;
}
relay(group: IGroup): void {
this.currentGroup = group;
this.$emit("input", group);
this.isComponentModalActive = false;
}
}
</script>
<style lang="scss" scoped>
.group-picker {
.block,
.no-group,
.inline {
cursor: pointer;
}
}
</style>

View file

@ -1122,5 +1122,8 @@
"Tools": "Tools", "Tools": "Tools",
"Social": "Social", "Social": "Social",
"Details": "Details", "Details": "Details",
"Booking": "Booking" "Booking": "Booking",
"Filter by profile or group name": "Filter by profile or group name",
"Filter by name": "Filter by name",
"Redirecting in progress…": "Redirecting in progress…"
} }

View file

@ -1213,5 +1213,8 @@
"Tools": "Outils", "Tools": "Outils",
"Social": "Social", "Social": "Social",
"Details": "Détails", "Details": "Détails",
"Booking": "Réservations" "Booking": "Réservations",
"Filter by profile or group name": "Filter par nom du profil ou du groupe",
"Filter by name": "Filtrer par nom",
"Redirecting in progress…": "Redirection en cours…"
} }