refactor to common MoreOptionsDropdown component for shared ui

This commit is contained in:
Gregor Vostrak
2024-07-11 13:20:23 +02:00
parent cbdbcef9eb
commit 10d8540e6c
10 changed files with 204 additions and 324 deletions

View File

@@ -307,9 +307,11 @@ test('test that deleting a time entry from the overview works', async ({
const timeEntryCount = await timeEntryRows.count(); const timeEntryCount = await timeEntryRows.count();
const newTimeEntry = timeEntryRows.first(); const newTimeEntry = timeEntryRows.first();
const actionsDropdown = newTimeEntry.getByTestId('time_entry_actions'); const actionsDropdown = newTimeEntry
.getByRole('button', { name: 'Actions for the time entry' })
.first();
await actionsDropdown.click(); await actionsDropdown.click();
const deleteButton = page.getByTestId('time_entry_delete'); const deleteButton = page.getByText('Delete');
await deleteButton.click(); await deleteButton.click();
await expect(timeEntryRows).toHaveCount(timeEntryCount - 1); await expect(timeEntryRows).toHaveCount(timeEntryCount - 1);
}); });

View File

@@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import Dropdown from '@/Components/Dropdown.vue';
import { PencilSquareIcon, TrashIcon } from '@heroicons/vue/20/solid'; import { PencilSquareIcon, TrashIcon } from '@heroicons/vue/20/solid';
import type { Client } from '@/utils/api'; import type { Client } from '@/utils/api';
import { canDeleteClients, canUpdateClients } from '@/utils/permissions'; import { canDeleteClients, canUpdateClients } from '@/utils/permissions';
import MoreOptionsDropdown from '@/Components/MoreOptionsDropdown.vue';
const emit = defineEmits<{ const emit = defineEmits<{
delete: []; delete: [];
@@ -14,24 +14,7 @@ const props = defineProps<{
</script> </script>
<template> <template>
<Dropdown> <MoreOptionsDropdown :label="'Actions for Client ' + props.client.name">
<template #trigger>
<svg
data-testid="client_actions"
:aria-label="'Actions for Client ' + props.client.name"
class="h-10 w-10 p-2 rounded-full hover:bg-card-background opacity-20 group-hover:opacity-100 transition"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M12 5.92A.96.96 0 1 0 12 4a.96.96 0 0 0 0 1.92m0 7.04a.96.96 0 1 0 0-1.92a.96.96 0 0 0 0 1.92M12 20a.96.96 0 1 0 0-1.92a.96.96 0 0 0 0 1.92" />
</svg>
</template>
<template #content>
<div class="min-w-[150px]"> <div class="min-w-[150px]">
<button <button
v-if="canUpdateClients()" v-if="canUpdateClients()"
@@ -53,8 +36,7 @@ const props = defineProps<{
<span>Delete</span> <span>Delete</span>
</button> </button>
</div> </div>
</template> </MoreOptionsDropdown>
</Dropdown>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import Dropdown from '@/Components/Dropdown.vue';
import { TrashIcon, ArrowPathIcon } from '@heroicons/vue/20/solid'; import { TrashIcon, ArrowPathIcon } from '@heroicons/vue/20/solid';
import MoreOptionsDropdown from '@/Components/MoreOptionsDropdown.vue';
const emit = defineEmits<{ const emit = defineEmits<{
delete: []; delete: [];
resend: []; resend: [];
@@ -8,23 +8,7 @@ const emit = defineEmits<{
</script> </script>
<template> <template>
<Dropdown align="bottom-end"> <MoreOptionsDropdown label="Actions for the invitation">
<template #trigger>
<svg
data-testid="invitation_actions"
class="h-10 w-10 p-2 rounded-full hover:bg-card-background opacity-20 group-hover:opacity-100 transition"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M12 5.92A.96.96 0 1 0 12 4a.96.96 0 0 0 0 1.92m0 7.04a.96.96 0 1 0 0-1.92a.96.96 0 0 0 0 1.92M12 20a.96.96 0 1 0 0-1.92a.96.96 0 0 0 0 1.92" />
</svg>
</template>
<template #content>
<button <button
@click="emit('resend')" @click="emit('resend')"
data-testid="invitation_delete" data-testid="invitation_delete"
@@ -39,8 +23,7 @@ const emit = defineEmits<{
<TrashIcon class="w-5 text-icon-active"></TrashIcon> <TrashIcon class="w-5 text-icon-active"></TrashIcon>
<span>Delete</span> <span>Delete</span>
</button> </button>
</template> </MoreOptionsDropdown>
</Dropdown>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import Dropdown from '@/Components/Dropdown.vue';
import { TrashIcon, PencilSquareIcon } from '@heroicons/vue/20/solid'; import { TrashIcon, PencilSquareIcon } from '@heroicons/vue/20/solid';
import type { Member } from '@/utils/api'; import type { Member } from '@/utils/api';
import { canDeleteMembers, canUpdateMembers } from '@/utils/permissions'; import { canDeleteMembers, canUpdateMembers } from '@/utils/permissions';
import MoreOptionsDropdown from '@/Components/MoreOptionsDropdown.vue';
const emit = defineEmits<{ const emit = defineEmits<{
delete: []; delete: [];
@@ -14,28 +14,9 @@ const props = defineProps<{
</script> </script>
<template> <template>
<Dropdown <MoreOptionsDropdown
v-if="canUpdateMembers() || canDeleteMembers()" v-if="canUpdateMembers() || canDeleteMembers()"
align="bottom-end"> :label="'Actions for Member ' + props.member.name">
<template #trigger>
<button
class="focus-visible:outline-none focus-visible:bg-card-background rounded-full focus-visible:ring-1 focus-visible:ring-input-border-active focus-visible:opacity-100 hover:bg-card-background group-hover:opacity-100 opacity-20 transition-opacity"
:aria-label="'Actions for Member ' + props.member.name">
<svg
class="h-10 w-10 p-2 rounded-full"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M12 5.92A.96.96 0 1 0 12 4a.96.96 0 0 0 0 1.92m0 7.04a.96.96 0 1 0 0-1.92a.96.96 0 0 0 0 1.92M12 20a.96.96 0 1 0 0-1.92a.96.96 0 0 0 0 1.92" />
</svg>
</button>
</template>
<template #content>
<div class="min-w-[150px]"> <div class="min-w-[150px]">
<button <button
v-if="canUpdateMembers()" v-if="canUpdateMembers()"
@@ -56,8 +37,7 @@ const props = defineProps<{
<span>Delete</span> <span>Delete</span>
</button> </button>
</div> </div>
</template> </MoreOptionsDropdown>
</Dropdown>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import Dropdown from '@/Components/Dropdown.vue';
import { import {
TrashIcon, TrashIcon,
PencilSquareIcon, PencilSquareIcon,
@@ -7,6 +6,7 @@ import {
} from '@heroicons/vue/20/solid'; } from '@heroicons/vue/20/solid';
import type { Project } from '@/utils/api'; import type { Project } from '@/utils/api';
import { canDeleteProjects, canUpdateProjects } from '@/utils/permissions'; import { canDeleteProjects, canUpdateProjects } from '@/utils/permissions';
import MoreOptionsDropdown from '@/Components/MoreOptionsDropdown.vue';
const emit = defineEmits<{ const emit = defineEmits<{
delete: []; delete: [];
edit: []; edit: [];
@@ -18,27 +18,7 @@ const props = defineProps<{
</script> </script>
<template> <template>
<Dropdown> <MoreOptionsDropdown :label="'Actions for Project ' + props.project.name">
<template #trigger>
<button
class="focus-visible:outline-none focus-visible:bg-card-background rounded-full focus-visible:ring-1 focus-visible:ring-input-border-active focus-visible:opacity-100 hover:bg-card-background group-hover:opacity-100 opacity-20 transition-opacity"
data-testid="project_actions"
:aria-label="'Actions for Project ' + props.project.name">
<svg
class="h-10 w-10 p-2 rounded-full"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M12 5.92A.96.96 0 1 0 12 4a.96.96 0 0 0 0 1.92m0 7.04a.96.96 0 1 0 0-1.92a.96.96 0 0 0 0 1.92M12 20a.96.96 0 1 0 0-1.92a.96.96 0 0 0 0 1.92" />
</svg>
</button>
</template>
<template #content>
<div class="min-w-[150px]"> <div class="min-w-[150px]">
<button <button
@click.prevent="emit('edit')" @click.prevent="emit('edit')"
@@ -55,11 +35,8 @@ const props = defineProps<{
v-if="canUpdateProjects()" v-if="canUpdateProjects()"
:aria-label="'Archive Project ' + props.project.name" :aria-label="'Archive Project ' + props.project.name"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"> class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
<ArchiveBoxIcon <ArchiveBoxIcon class="w-5 text-icon-active"></ArchiveBoxIcon>
class="w-5 text-icon-active"></ArchiveBoxIcon> <span>{{ project.is_archived ? 'Unarchive' : 'Archive' }}</span>
<span>{{
project.is_archived ? 'Unarchive' : 'Archive'
}}</span>
</button> </button>
<button <button
@click.prevent="emit('delete')" @click.prevent="emit('delete')"
@@ -71,8 +48,7 @@ const props = defineProps<{
<span>Delete</span> <span>Delete</span>
</button> </button>
</div> </div>
</template> </MoreOptionsDropdown>
</Dropdown>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import Dropdown from '@/Components/Dropdown.vue';
import { TrashIcon, PencilSquareIcon } from '@heroicons/vue/20/solid'; import { TrashIcon, PencilSquareIcon } from '@heroicons/vue/20/solid';
import type { ProjectMember } from '@/utils/api'; import type { ProjectMember } from '@/utils/api';
import { useMembersStore } from '@/utils/useMembers'; import { useMembersStore } from '@/utils/useMembers';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { computed } from 'vue'; import { computed } from 'vue';
import MoreOptionsDropdown from '@/Components/MoreOptionsDropdown.vue';
const emit = defineEmits<{ const emit = defineEmits<{
delete: []; delete: [];
@@ -24,34 +24,13 @@ const currentMember = computed(() => {
</script> </script>
<template> <template>
<Dropdown> <MoreOptionsDropdown
<template #trigger> :label="'Actions for Project Member ' + currentMember?.name">
<button
class="focus-visible:outline-none focus-visible:bg-card-background rounded-full focus-visible:ring-1 focus-visible:ring-input-border-active focus-visible:opacity-100 hover:bg-card-background group-hover:opacity-100 opacity-20 transition-opacity"
:aria-label="
'Actions for Project Member ' + currentMember?.name
">
<svg
class="h-10 w-10 p-2 rounded-full"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M12 5.92A.96.96 0 1 0 12 4a.96.96 0 0 0 0 1.92m0 7.04a.96.96 0 1 0 0-1.92a.96.96 0 0 0 0 1.92M12 20a.96.96 0 1 0 0-1.92a.96.96 0 0 0 0 1.92" />
</svg>
</button>
</template>
<template #content>
<button <button
@click.prevent="emit('edit')" @click.prevent="emit('edit')"
:aria-label="'Edit Project Member ' + currentMember?.name" :aria-label="'Edit Project Member ' + currentMember?.name"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"> class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
<PencilSquareIcon <PencilSquareIcon class="w-5 text-icon-active"></PencilSquareIcon>
class="w-5 text-icon-active"></PencilSquareIcon>
<span>Edit</span> <span>Edit</span>
</button> </button>
<button <button
@@ -62,8 +41,7 @@ const currentMember = computed(() => {
<TrashIcon class="w-5 text-icon-active"></TrashIcon> <TrashIcon class="w-5 text-icon-active"></TrashIcon>
<span>Remove from Team</span> <span>Remove from Team</span>
</button> </button>
</template> </MoreOptionsDropdown>
</Dropdown>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import Dropdown from '@/Components/Dropdown.vue';
import { TrashIcon } from '@heroicons/vue/20/solid'; import { TrashIcon } from '@heroicons/vue/20/solid';
import type { Tag } from '@/utils/api'; import type { Tag } from '@/utils/api';
import MoreOptionsDropdown from '@/Components/MoreOptionsDropdown.vue';
const emit = defineEmits<{ const emit = defineEmits<{
delete: []; delete: [];
@@ -12,24 +12,7 @@ const props = defineProps<{
</script> </script>
<template> <template>
<Dropdown> <MoreOptionsDropdown :label="'Actions for Tag ' + props.tag.name">
<template #trigger>
<svg
data-testid="tag_actions"
:aria-label="'Actions for Tag ' + props.tag.name"
class="h-10 w-10 p-2 rounded-full hover:bg-card-background opacity-20 group-hover:opacity-100 transition"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M12 5.92A.96.96 0 1 0 12 4a.96.96 0 0 0 0 1.92m0 7.04a.96.96 0 1 0 0-1.92a.96.96 0 0 0 0 1.92M12 20a.96.96 0 1 0 0-1.92a.96.96 0 0 0 0 1.92" />
</svg>
</template>
<template #content>
<button <button
@click="emit('delete')" @click="emit('delete')"
:aria-label="'Delete Tag ' + props.tag.name" :aria-label="'Delete Tag ' + props.tag.name"
@@ -38,8 +21,7 @@ const props = defineProps<{
<TrashIcon class="w-5 text-icon-active"></TrashIcon> <TrashIcon class="w-5 text-icon-active"></TrashIcon>
<span>Delete</span> <span>Delete</span>
</button> </button>
</template> </MoreOptionsDropdown>
</Dropdown>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import Dropdown from '@/Components/Dropdown.vue';
import { import {
TrashIcon, TrashIcon,
PencilSquareIcon, PencilSquareIcon,
@@ -7,6 +6,7 @@ import {
} from '@heroicons/vue/20/solid'; } from '@heroicons/vue/20/solid';
import type { Task } from '@/utils/api'; import type { Task } from '@/utils/api';
import { canDeleteTasks, canUpdateTasks } from '@/utils/permissions'; import { canDeleteTasks, canUpdateTasks } from '@/utils/permissions';
import MoreOptionsDropdown from '@/Components/MoreOptionsDropdown.vue';
const emit = defineEmits<{ const emit = defineEmits<{
delete: []; delete: [];
edit: []; edit: [];
@@ -18,27 +18,7 @@ const props = defineProps<{
</script> </script>
<template> <template>
<Dropdown align="bottom-end"> <MoreOptionsDropdown :label="'Actions for Task ' + props.task.name">
<template #trigger>
<button
class="focus-visible:outline-none focus-visible:bg-card-background rounded-full focus-visible:ring-1 focus-visible:ring-input-border-active focus-visible:opacity-100 hover:bg-card-background group-hover:opacity-100 opacity-20 transition-opacity"
data-testid="task_actions"
:aria-label="'Actions for Task ' + props.task.name">
<svg
class="h-10 w-10 p-2 rounded-full"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M12 5.92A.96.96 0 1 0 12 4a.96.96 0 0 0 0 1.92m0 7.04a.96.96 0 1 0 0-1.92a.96.96 0 0 0 0 1.92M12 20a.96.96 0 1 0 0-1.92a.96.96 0 0 0 0 1.92" />
</svg>
</button>
</template>
<template #content>
<div class="min-w-[150px]"> <div class="min-w-[150px]">
<button <button
@click="emit('edit')" @click="emit('edit')"
@@ -55,8 +35,7 @@ const props = defineProps<{
v-if="canUpdateTasks()" v-if="canUpdateTasks()"
:aria-label="'Mark Task ' + props.task.name + ' as done'" :aria-label="'Mark Task ' + props.task.name + ' as done'"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"> class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
<CheckCircleIcon <CheckCircleIcon class="w-5 text-icon-active"></CheckCircleIcon>
class="w-5 text-icon-active"></CheckCircleIcon>
<span v-if="props.task.is_done">Mark as active</span> <span v-if="props.task.is_done">Mark as active</span>
<span v-else>Mark as done</span> <span v-else>Mark as done</span>
</button> </button>
@@ -70,8 +49,7 @@ const props = defineProps<{
<span>Delete</span> <span>Delete</span>
</button> </button>
</div> </div>
</template> </MoreOptionsDropdown>
</Dropdown>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@@ -1,29 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import Dropdown from '@/Components/Dropdown.vue';
import { TrashIcon } from '@heroicons/vue/20/solid'; import { TrashIcon } from '@heroicons/vue/20/solid';
import MoreOptionsDropdown from '@/Components/MoreOptionsDropdown.vue';
const emit = defineEmits<{ const emit = defineEmits<{
delete: []; delete: [];
}>(); }>();
</script> </script>
<template> <template>
<Dropdown> <MoreOptionsDropdown label="Actions for the time entry">
<template #trigger>
<svg
data-testid="time_entry_actions"
class="h-10 w-10 p-2 rounded-full hover:bg-card-background opacity-20 group-hover:opacity-100 transition"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M12 5.92A.96.96 0 1 0 12 4a.96.96 0 0 0 0 1.92m0 7.04a.96.96 0 1 0 0-1.92a.96.96 0 0 0 0 1.92M12 20a.96.96 0 1 0 0-1.92a.96.96 0 0 0 0 1.92" />
</svg>
</template>
<template #content>
<button <button
@click="emit('delete')" @click="emit('delete')"
data-testid="time_entry_delete" data-testid="time_entry_delete"
@@ -31,8 +16,7 @@ const emit = defineEmits<{
<TrashIcon class="w-5 text-icon-active"></TrashIcon> <TrashIcon class="w-5 text-icon-active"></TrashIcon>
<span>Delete</span> <span>Delete</span>
</button> </button>
</template> </MoreOptionsDropdown>
</Dropdown>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@@ -0,0 +1,35 @@
<script setup lang="ts">
import Dropdown from '@/Components/Dropdown.vue';
defineProps<{
label: string;
}>();
</script>
<template>
<Dropdown align="bottom-end">
<template #trigger>
<button
class="focus-visible:outline-none focus-visible:bg-card-background rounded-full focus-visible:ring-1 focus-visible:ring-input-border-active focus-visible:opacity-100 hover:bg-card-background group-hover:opacity-100 opacity-20 transition-opacity"
:aria-label="label">
<svg
class="h-10 w-10 p-2 rounded-full"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M12 5.92A.96.96 0 1 0 12 4a.96.96 0 0 0 0 1.92m0 7.04a.96.96 0 1 0 0-1.92a.96.96 0 0 0 0 1.92M12 20a.96.96 0 1 0 0-1.92a.96.96 0 0 0 0 1.92" />
</svg>
</button>
</template>
<template #content>
<slot></slot>
</template>
</Dropdown>
</template>
<style scoped></style>