OrganizerPicker improvements

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2022-08-12 16:46:04 +02:00
parent d8cf49e315
commit 4f9e0911e7
No known key found for this signature in database
GPG key ID: A061B9DDE0CA0773
4 changed files with 210 additions and 60 deletions

View file

@ -0,0 +1,56 @@
<template>
<Story :setup-app="setupApp">
<Variant>
<OrganizerPicker
v-model="actor"
:identities="identities"
v-model:actor-filter="actorFilter"
:groupMemberships="[]"
:current-actor="currentActor"
@update:actor-filter="hstEvent('Actor Filter updated', $event)"
@update:model-value="hstEvent('Selected actor updated', $event)"
/>
</Variant>
</Story>
</template>
<script lang="ts" setup>
import OrganizerPicker from "./OrganizerPicker.vue";
import { createMemoryHistory, createRouter } from "vue-router";
import { reactive, ref } from "vue";
import { ActorType } from "@/types/enums";
import { hstEvent } from "histoire/client";
const currentActor = reactive({
id: "59",
preferredUsername: "me",
name: "Someone",
type: ActorType.PERSON,
});
const actor = reactive({
id: "5",
preferredUsername: "hello",
name: "Sigmund",
type: ActorType.PERSON,
});
const group = reactive({
id: "89",
preferredUsername: "congregation",
name: "College",
type: ActorType.GROUP,
});
const identities = [actor, group];
const actorFilter = ref("");
function setupApp({ app }) {
app.use(
createRouter({
history: createMemoryHistory(),
routes: [{ path: "/", name: "home", component: { render: () => null } }],
})
);
}
</script>

View file

@ -1,13 +1,15 @@
<template>
<div class="list is-hoverable">
<div class="max-w-md mx-auto">
<o-input
dir="auto"
:placeholder="$t('Filter by profile or group name')"
v-model="actorFilter"
:placeholder="t('Filter by profile or group name')"
v-model="actorFilterProxy"
class=""
/>
<transition-group
tag="ul"
class="grid grid-cols-1 gap-y-3 m-5 max-w-md mx-auto"
:class="{ hidden: actualFilteredAvailableActors.length === 0 }"
enter-active-class="duration-300 ease-out"
enter-from-class="transform opacity-0"
enter-to-class="opacity-100"
@ -52,54 +54,37 @@
</div>
</template>
<script lang="ts" setup>
import { IActor } from "@/types/actor";
import { LOGGED_USER_MEMBERSHIPS } from "@/graphql/actor";
import { IActor, IPerson } from "@/types/actor";
import { IMember } from "@/types/actor/member.model";
import { MemberRole } from "@/types/enums";
import { computed, ref } from "vue";
import {
useCurrentActorClient,
useCurrentUserIdentities,
} from "@/composition/apollo/actor";
import { IUser } from "@/types/current-user.model";
import { computed } from "vue";
import { useI18n } from "vue-i18n";
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
import { useQuery } from "@vue/apollo-composable";
const props = withDefaults(
defineProps<{ modelValue: IActor; restrictModeratorLevel?: boolean }>(),
defineProps<{
currentActor: IPerson;
modelValue: IActor;
restrictModeratorLevel?: boolean;
identities: IActor[];
actorFilter: string;
groupMemberships: IMember[];
}>(),
{ restrictModeratorLevel: false }
);
const emit = defineEmits(["update:modelValue"]);
const emit = defineEmits(["update:modelValue", "update:actorFilter"]);
const { currentActor } = useCurrentActorClient();
const { identities } = useCurrentUserIdentities();
const actorFilter = ref("");
const { result: groupMembershipsResult } = useQuery<{
loggedUser: Pick<IUser, "memberships">;
}>(LOGGED_USER_MEMBERSHIPS, () => ({
page: 1,
limit: 10,
membershipName: actorFilter.value,
}));
const groupMemberships = computed(
() =>
groupMembershipsResult.value?.loggedUser.memberships ?? {
elements: [],
total: 0,
}
);
const { t } = useI18n({ useScope: "global" });
const selectedActor = computed({
get(): IActor | undefined {
if (props.modelValue?.id) {
return props.modelValue;
}
if (currentActor.value) {
return (identities.value ?? []).find(
(identity) => identity.id === currentActor.value?.id
if (props.currentActor) {
return props.identities.find(
(identity) => identity.id === props.currentActor?.id
);
}
return undefined;
@ -112,7 +97,7 @@ const selectedActor = computed({
const actualMemberships = computed((): IMember[] => {
if (props.restrictModeratorLevel) {
return groupMemberships.value.elements.filter((membership: IMember) =>
return props.groupMemberships.filter((membership: IMember) =>
[
MemberRole.ADMINISTRATOR,
MemberRole.MODERATOR,
@ -120,14 +105,14 @@ const actualMemberships = computed((): IMember[] => {
].includes(membership.role)
);
}
return groupMemberships.value.elements;
return props.groupMemberships;
});
const actualAvailableActors = computed((): (IActor | undefined)[] => {
return [
currentActor.value,
...(identities.value ?? []).filter(
(identity: IActor) => identity.id !== currentActor.value?.id
props.currentActor,
...props.identities.filter(
(identity: IActor) => identity.id !== props.currentActor?.id
),
...actualMemberships.value.map((member) => member.parent),
].filter((elem) => elem);
@ -137,12 +122,21 @@ const actualFilteredAvailableActors = computed((): (IActor | undefined)[] => {
return (actualAvailableActors.value ?? []).filter((actor) => {
if (actor === undefined) return false;
return [
actor.preferredUsername.toLowerCase(),
actor.preferredUsername?.toLowerCase(),
actor.name?.toLowerCase(),
actor.domain?.toLowerCase(),
].some((match) => match?.includes(actorFilter.value.toLowerCase()));
].some((match) => match?.includes(actorFilterProxy.value.toLowerCase()));
});
});
const actorFilterProxy = computed({
get() {
return props.actorFilter;
},
set(newActorFilter: string) {
emit("update:actorFilter", newActorFilter);
},
});
</script>
<style lang="scss" scoped>
@use "@/styles/_mixins" as *;

View file

@ -0,0 +1,87 @@
<template>
<Story :setup-app="setupApp">
<Variant>
<OrganizerPickerWrapper
v-model="actor"
@update:model-value="hstEvent('Value', $event)"
@update:contacts="hstEvent('Contacts', $event)"
/>
</Variant>
</Story>
</template>
<script lang="ts" setup>
import OrganizerPickerWrapper from "./OrganizerPickerWrapper.vue";
import { DefaultApolloClient } from "@vue/apollo-composable";
import { createMockClient } from "mock-apollo-client";
import { cache } from "@/apollo/memory";
import { ICurrentUserRole } from "@/types/enums";
import { PERSON_GROUP_MEMBERSHIPS } from "@/graphql/actor";
import { createMemoryHistory, createRouter } from "vue-router";
import { IDENTITIES } from "@/graphql/actor";
import { reactive } from "vue";
import { hstEvent } from "histoire/client";
const actor = reactive({
id: "5",
preferredUsername: "hello",
name: "Sigmund",
});
function setupApp({ app }) {
const defaultResolvers = {
Query: {
currentUser: (): Record<string, any> => ({
email: "user@mail.com",
id: "2",
role: ICurrentUserRole.USER,
isLoggedIn: true,
__typename: "CurrentUser",
}),
currentActor: (): Record<string, any> => ({
id: "67",
preferredUsername: "someone",
name: "Personne",
avatar: null,
__typename: "CurrentActor",
}),
},
};
const mockClient = createMockClient({
cache,
resolvers: defaultResolvers,
});
mockClient.setRequestHandler(
PERSON_GROUP_MEMBERSHIPS,
() =>
new Promise((resolve) =>
resolve({
data: {
person: { id: "5", memberships: { total: 0, elements: [] } },
},
})
)
);
mockClient.setRequestHandler(
IDENTITIES,
() =>
new Promise((resolve) =>
resolve({
data: {
identities: [{ id: "9", preferredUsername: "sam", name: "Samuel" }],
},
})
)
);
app.provide(DefaultApolloClient, mockClient);
app.use(
createRouter({
history: createMemoryHistory(),
routes: [{ path: "/", name: "home", component: { render: () => null } }],
})
);
}
</script>

View file

@ -63,15 +63,20 @@
<h2 class="">{{ $t("Pick a profile or a group") }}</h2>
</header>
<section class="">
<div class="flex gap-2">
<div class="actor-picker">
<div class="flex flex-wrap gap-2 items-center">
<div class="max-h-[400px] overflow-y-auto flex-1">
<organizer-picker
v-if="currentActor"
:current-actor="currentActor"
:identities="identities ?? []"
v-model="selectedActor"
@input="relay"
@update:model-value="relay"
:restrict-moderator-level="true"
:group-memberships="groupMemberships"
v-model:actorFilter="actorFilter"
/>
</div>
<div class="contact-picker">
<div class="max-h-[400px] overflow-y-auto">
<div v-if="isSelectedActorAGroup">
<p>{{ $t("Add a contact") }}</p>
<o-input
@ -132,7 +137,7 @@
</div>
</div>
</section>
<footer class="">
<footer class="my-2">
<o-button variant="primary" @click="pickActor">
{{ $t("Pick") }}
</o-button>
@ -145,7 +150,10 @@
import { IActor, IGroup, usernameWithDomain } from "../../types/actor";
import OrganizerPicker from "./OrganizerPicker.vue";
import EmptyContent from "../Utils/EmptyContent.vue";
import { PERSON_GROUP_MEMBERSHIPS } from "../../graphql/actor";
import {
LOGGED_USER_MEMBERSHIPS,
PERSON_GROUP_MEMBERSHIPS,
} from "../../graphql/actor";
import { GROUP_MEMBERS } from "@/graphql/member";
import { ActorType, MemberRole } from "@/types/enums";
import { useQuery } from "@vue/apollo-composable";
@ -157,6 +165,7 @@ import {
import { useRoute } from "vue-router";
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
import debounce from "lodash/debounce";
import { IUser } from "@/types/current-user.model";
const MEMBER_ROLES = [
MemberRole.CREATOR,
@ -212,8 +221,8 @@ const selectedActor = computed({
}
return undefined;
},
set(selectedActor: IActor | undefined) {
emit("update:modelValue", selectedActor);
set(newSelectedActor: IActor | undefined) {
emit("update:modelValue", newSelectedActor);
},
});
@ -292,13 +301,17 @@ const filteredActorMembers = computed((): IActor[] => {
const isSelectedActorAGroup = computed((): boolean => {
return selectedActor.value?.type === ActorType.GROUP;
});
const actorFilter = ref("");
const { result: groupMembershipsResult } = useQuery<{
loggedUser: Pick<IUser, "memberships">;
}>(LOGGED_USER_MEMBERSHIPS, () => ({
page: 1,
limit: 10,
membershipName: actorFilter.value,
}));
const groupMemberships = computed(
() => groupMembershipsResult.value?.loggedUser.memberships.elements ?? []
);
</script>
<style lang="scss" scoped>
.modal-card-body .columns .column {
&.actor-picker,
&.contact-picker {
overflow-y: auto;
max-height: 400px;
}
}
</style>