OrganizerPicker improvements
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
parent
d8cf49e315
commit
4f9e0911e7
56
js/src/components/Event/OrganizerPicker.story.vue
Normal file
56
js/src/components/Event/OrganizerPicker.story.vue
Normal 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>
|
|
@ -1,13 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="list is-hoverable">
|
<div class="max-w-md mx-auto">
|
||||||
<o-input
|
<o-input
|
||||||
dir="auto"
|
dir="auto"
|
||||||
:placeholder="$t('Filter by profile or group name')"
|
:placeholder="t('Filter by profile or group name')"
|
||||||
v-model="actorFilter"
|
v-model="actorFilterProxy"
|
||||||
|
class=""
|
||||||
/>
|
/>
|
||||||
<transition-group
|
<transition-group
|
||||||
tag="ul"
|
tag="ul"
|
||||||
class="grid grid-cols-1 gap-y-3 m-5 max-w-md mx-auto"
|
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-active-class="duration-300 ease-out"
|
||||||
enter-from-class="transform opacity-0"
|
enter-from-class="transform opacity-0"
|
||||||
enter-to-class="opacity-100"
|
enter-to-class="opacity-100"
|
||||||
|
@ -52,54 +54,37 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { IActor } from "@/types/actor";
|
import { IActor, IPerson } from "@/types/actor";
|
||||||
import { LOGGED_USER_MEMBERSHIPS } from "@/graphql/actor";
|
|
||||||
import { IMember } from "@/types/actor/member.model";
|
import { IMember } from "@/types/actor/member.model";
|
||||||
import { MemberRole } from "@/types/enums";
|
import { MemberRole } from "@/types/enums";
|
||||||
import { computed, ref } from "vue";
|
import { computed } from "vue";
|
||||||
import {
|
import { useI18n } from "vue-i18n";
|
||||||
useCurrentActorClient,
|
|
||||||
useCurrentUserIdentities,
|
|
||||||
} from "@/composition/apollo/actor";
|
|
||||||
import { IUser } from "@/types/current-user.model";
|
|
||||||
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
||||||
import { useQuery } from "@vue/apollo-composable";
|
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{ modelValue: IActor; restrictModeratorLevel?: boolean }>(),
|
defineProps<{
|
||||||
|
currentActor: IPerson;
|
||||||
|
modelValue: IActor;
|
||||||
|
restrictModeratorLevel?: boolean;
|
||||||
|
identities: IActor[];
|
||||||
|
actorFilter: string;
|
||||||
|
groupMemberships: IMember[];
|
||||||
|
}>(),
|
||||||
{ restrictModeratorLevel: false }
|
{ restrictModeratorLevel: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
const emit = defineEmits(["update:modelValue"]);
|
const emit = defineEmits(["update:modelValue", "update:actorFilter"]);
|
||||||
|
|
||||||
const { currentActor } = useCurrentActorClient();
|
const { t } = useI18n({ useScope: "global" });
|
||||||
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 selectedActor = computed({
|
const selectedActor = computed({
|
||||||
get(): IActor | undefined {
|
get(): IActor | undefined {
|
||||||
if (props.modelValue?.id) {
|
if (props.modelValue?.id) {
|
||||||
return props.modelValue;
|
return props.modelValue;
|
||||||
}
|
}
|
||||||
if (currentActor.value) {
|
if (props.currentActor) {
|
||||||
return (identities.value ?? []).find(
|
return props.identities.find(
|
||||||
(identity) => identity.id === currentActor.value?.id
|
(identity) => identity.id === props.currentActor?.id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -112,7 +97,7 @@ const selectedActor = computed({
|
||||||
|
|
||||||
const actualMemberships = computed((): IMember[] => {
|
const actualMemberships = computed((): IMember[] => {
|
||||||
if (props.restrictModeratorLevel) {
|
if (props.restrictModeratorLevel) {
|
||||||
return groupMemberships.value.elements.filter((membership: IMember) =>
|
return props.groupMemberships.filter((membership: IMember) =>
|
||||||
[
|
[
|
||||||
MemberRole.ADMINISTRATOR,
|
MemberRole.ADMINISTRATOR,
|
||||||
MemberRole.MODERATOR,
|
MemberRole.MODERATOR,
|
||||||
|
@ -120,14 +105,14 @@ const actualMemberships = computed((): IMember[] => {
|
||||||
].includes(membership.role)
|
].includes(membership.role)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return groupMemberships.value.elements;
|
return props.groupMemberships;
|
||||||
});
|
});
|
||||||
|
|
||||||
const actualAvailableActors = computed((): (IActor | undefined)[] => {
|
const actualAvailableActors = computed((): (IActor | undefined)[] => {
|
||||||
return [
|
return [
|
||||||
currentActor.value,
|
props.currentActor,
|
||||||
...(identities.value ?? []).filter(
|
...props.identities.filter(
|
||||||
(identity: IActor) => identity.id !== currentActor.value?.id
|
(identity: IActor) => identity.id !== props.currentActor?.id
|
||||||
),
|
),
|
||||||
...actualMemberships.value.map((member) => member.parent),
|
...actualMemberships.value.map((member) => member.parent),
|
||||||
].filter((elem) => elem);
|
].filter((elem) => elem);
|
||||||
|
@ -137,12 +122,21 @@ const actualFilteredAvailableActors = computed((): (IActor | undefined)[] => {
|
||||||
return (actualAvailableActors.value ?? []).filter((actor) => {
|
return (actualAvailableActors.value ?? []).filter((actor) => {
|
||||||
if (actor === undefined) return false;
|
if (actor === undefined) return false;
|
||||||
return [
|
return [
|
||||||
actor.preferredUsername.toLowerCase(),
|
actor.preferredUsername?.toLowerCase(),
|
||||||
actor.name?.toLowerCase(),
|
actor.name?.toLowerCase(),
|
||||||
actor.domain?.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>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@use "@/styles/_mixins" as *;
|
@use "@/styles/_mixins" as *;
|
||||||
|
|
87
js/src/components/Event/OrganizerPickerWrapper.story.vue
Normal file
87
js/src/components/Event/OrganizerPickerWrapper.story.vue
Normal 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>
|
|
@ -63,15 +63,20 @@
|
||||||
<h2 class="">{{ $t("Pick a profile or a group") }}</h2>
|
<h2 class="">{{ $t("Pick a profile or a group") }}</h2>
|
||||||
</header>
|
</header>
|
||||||
<section class="">
|
<section class="">
|
||||||
<div class="flex gap-2">
|
<div class="flex flex-wrap gap-2 items-center">
|
||||||
<div class="actor-picker">
|
<div class="max-h-[400px] overflow-y-auto flex-1">
|
||||||
<organizer-picker
|
<organizer-picker
|
||||||
|
v-if="currentActor"
|
||||||
|
:current-actor="currentActor"
|
||||||
|
:identities="identities ?? []"
|
||||||
v-model="selectedActor"
|
v-model="selectedActor"
|
||||||
@input="relay"
|
@update:model-value="relay"
|
||||||
:restrict-moderator-level="true"
|
:restrict-moderator-level="true"
|
||||||
|
:group-memberships="groupMemberships"
|
||||||
|
v-model:actorFilter="actorFilter"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="contact-picker">
|
<div class="max-h-[400px] overflow-y-auto">
|
||||||
<div v-if="isSelectedActorAGroup">
|
<div v-if="isSelectedActorAGroup">
|
||||||
<p>{{ $t("Add a contact") }}</p>
|
<p>{{ $t("Add a contact") }}</p>
|
||||||
<o-input
|
<o-input
|
||||||
|
@ -132,7 +137,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<footer class="">
|
<footer class="my-2">
|
||||||
<o-button variant="primary" @click="pickActor">
|
<o-button variant="primary" @click="pickActor">
|
||||||
{{ $t("Pick") }}
|
{{ $t("Pick") }}
|
||||||
</o-button>
|
</o-button>
|
||||||
|
@ -145,7 +150,10 @@
|
||||||
import { IActor, IGroup, usernameWithDomain } from "../../types/actor";
|
import { IActor, IGroup, usernameWithDomain } from "../../types/actor";
|
||||||
import OrganizerPicker from "./OrganizerPicker.vue";
|
import OrganizerPicker from "./OrganizerPicker.vue";
|
||||||
import EmptyContent from "../Utils/EmptyContent.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 { GROUP_MEMBERS } from "@/graphql/member";
|
||||||
import { ActorType, MemberRole } from "@/types/enums";
|
import { ActorType, MemberRole } from "@/types/enums";
|
||||||
import { useQuery } from "@vue/apollo-composable";
|
import { useQuery } from "@vue/apollo-composable";
|
||||||
|
@ -157,6 +165,7 @@ import {
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
||||||
import debounce from "lodash/debounce";
|
import debounce from "lodash/debounce";
|
||||||
|
import { IUser } from "@/types/current-user.model";
|
||||||
|
|
||||||
const MEMBER_ROLES = [
|
const MEMBER_ROLES = [
|
||||||
MemberRole.CREATOR,
|
MemberRole.CREATOR,
|
||||||
|
@ -212,8 +221,8 @@ const selectedActor = computed({
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
},
|
},
|
||||||
set(selectedActor: IActor | undefined) {
|
set(newSelectedActor: IActor | undefined) {
|
||||||
emit("update:modelValue", selectedActor);
|
emit("update:modelValue", newSelectedActor);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -292,13 +301,17 @@ const filteredActorMembers = computed((): IActor[] => {
|
||||||
const isSelectedActorAGroup = computed((): boolean => {
|
const isSelectedActorAGroup = computed((): boolean => {
|
||||||
return selectedActor.value?.type === ActorType.GROUP;
|
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>
|
</script>
|
||||||
<style lang="scss" scoped>
|
|
||||||
.modal-card-body .columns .column {
|
|
||||||
&.actor-picker,
|
|
||||||
&.contact-picker {
|
|
||||||
overflow-y: auto;
|
|
||||||
max-height: 400px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
Loading…
Reference in a new issue