diff --git a/js/src/components/Event/OrganizerPicker.vue b/js/src/components/Event/OrganizerPicker.vue index c9d8baf45..2a6368fd9 100644 --- a/js/src/components/Event/OrganizerPicker.vue +++ b/js/src/components/Event/OrganizerPicker.vue @@ -1,11 +1,14 @@ <template> <div class="list is-hoverable"> + <b-input + :placeholder="$t('Filter by profile or group name')" + v-model="actorFilter" + /> <b-radio-button v-model="selectedActor" :native-value="availableActor" class="list-item" - v-for="availableActor in actualAvailableActors" - :class="{ 'is-active': availableActor.id === selectedActor.id }" + v-for="availableActor in actualFilteredAvailableActors" :key="availableActor.id" > <div class="media"> @@ -61,6 +64,8 @@ export default class OrganizerPicker extends Vue { currentActor!: IPerson; + actorFilter = ""; + get selectedActor(): IActor | undefined { if (this.value?.id) { return this.value; @@ -103,6 +108,16 @@ export default class OrganizerPicker extends Vue { ...this.actualMemberships.map((member) => member.parent), ].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> <style lang="scss" scoped> diff --git a/js/src/components/Event/OrganizerPickerWrapper.vue b/js/src/components/Event/OrganizerPickerWrapper.vue index d142c50a2..ce763cc2e 100644 --- a/js/src/components/Event/OrganizerPickerWrapper.vue +++ b/js/src/components/Event/OrganizerPickerWrapper.vue @@ -52,17 +52,25 @@ </header> <section class="modal-card-body"> <div class="columns"> - <div class="column"> + <div class="column actor-picker"> <organizer-picker v-model="selectedActor" @input="relay" :restrict-moderator-level="true" /> </div> - <div class="column"> - <div v-if="actorMembers.length > 0"> + <div class="column contact-picker"> + <div v-if="isSelectedActorAGroup && actorMembers.length > 0"> <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"> <div class="media"> <div class="media-left"> @@ -88,7 +96,7 @@ </b-checkbox> </p> </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> </div> </div> @@ -167,6 +175,8 @@ export default class OrganizerPickerWrapper extends Vue { isComponentModalActive = false; + contactFilter = ""; + usernameWithDomain = usernameWithDomain; @Prop({ type: Array, required: false, default: () => [] }) @@ -226,19 +236,33 @@ export default class OrganizerPickerWrapper extends Vue { } get actorMembers(): IActor[] { - if (this.selectedActor?.type === ActorType.GROUP) { + if (this.isSelectedActorAGroup) { return this.members.elements.map(({ actor }: { actor: IActor }) => actor); } 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> <style lang="scss" scoped> -.group-picker { - .block, - .no-group, - .inline { - cursor: pointer; +.modal-card-body .columns .column { + &.actor-picker, + &.contact-picker { + overflow-y: auto; + max-height: 400px; } } </style> diff --git a/js/src/components/Group/GroupPicker.vue b/js/src/components/Group/GroupPicker.vue deleted file mode 100644 index 583b49671..000000000 --- a/js/src/components/Group/GroupPicker.vue +++ /dev/null @@ -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> diff --git a/js/src/components/Group/GroupPickerWrapper.vue b/js/src/components/Group/GroupPickerWrapper.vue deleted file mode 100644 index 9be9b28f9..000000000 --- a/js/src/components/Group/GroupPickerWrapper.vue +++ /dev/null @@ -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> diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json index 3d054a646..9f4586aab 100644 --- a/js/src/i18n/en_US.json +++ b/js/src/i18n/en_US.json @@ -1122,5 +1122,8 @@ "Tools": "Tools", "Social": "Social", "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…" } diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json index 58630776b..0615a1480 100644 --- a/js/src/i18n/fr_FR.json +++ b/js/src/i18n/fr_FR.json @@ -1213,5 +1213,8 @@ "Tools": "Outils", "Social": "Social", "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…" }