add support for archiving projects and marking tasks as done

This commit is contained in:
Gregor Vostrak
2024-07-02 17:01:12 +02:00
parent 0f32e42002
commit 6593a8c24f
15 changed files with 285 additions and 71 deletions

View File

@@ -19,7 +19,7 @@ defineProps<{
{{ title }}
</span>
</h3>
<div>
<div class="flex-1 flex justify-end items-center">
<slot name="actions"></slot>
</div>
</div>

View File

@@ -1,11 +1,16 @@
<script setup lang="ts">
import Dropdown from '@/Components/Dropdown.vue';
import { TrashIcon, PencilSquareIcon } from '@heroicons/vue/20/solid';
import {
TrashIcon,
PencilSquareIcon,
ArchiveBoxIcon,
} from '@heroicons/vue/20/solid';
import type { Project } from '@/utils/api';
import { canDeleteProjects, canUpdateProjects } from '@/utils/permissions';
const emit = defineEmits<{
delete: [];
edit: [];
archive: [];
}>();
const props = defineProps<{
project: Project;
@@ -15,20 +20,23 @@ const props = defineProps<{
<template>
<Dropdown>
<template #trigger>
<svg
<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"
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>
: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]">
@@ -42,6 +50,17 @@ const props = defineProps<{
class="w-5 text-icon-active"></PencilSquareIcon>
<span>Edit</span>
</button>
<button
@click.prevent="emit('archive')"
v-if="canUpdateProjects()"
: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">
<ArchiveBoxIcon
class="w-5 text-icon-active"></ArchiveBoxIcon>
<span>{{
project.is_archived ? 'Unarchive' : 'Archive'
}}</span>
</button>
<button
@click.prevent="emit('delete')"
:aria-label="'Delete Project ' + props.project.name"

View File

@@ -1,16 +1,17 @@
<script setup lang="ts">
import { useProjectsStore } from '@/utils/useProjects';
import SecondaryButton from '@/Components/SecondaryButton.vue';
import { FolderPlusIcon } from '@heroicons/vue/24/solid';
import { PlusIcon } from '@heroicons/vue/16/solid';
import { ref } from 'vue';
import ProjectCreateModal from '@/Components/Common/Project/ProjectCreateModal.vue';
import { storeToRefs } from 'pinia';
import ProjectTableHeading from '@/Components/Common/Project/ProjectTableHeading.vue';
import ProjectTableRow from '@/Components/Common/Project/ProjectTableRow.vue';
import { canCreateProjects } from '@/utils/permissions';
import type { Project } from '@/utils/api';
const { projects } = storeToRefs(useProjectsStore());
defineProps<{
projects: Project[];
}>();
const createProject = ref(false);
</script>

View File

@@ -33,6 +33,13 @@ function deleteProject() {
useProjectsStore().deleteProject(props.project.id);
}
function archiveProject() {
useProjectsStore().updateProject(props.project.id, {
...props.project,
is_archived: !props.project.is_archived,
});
}
const billableRateInfo = computed(() => {
if (props.project.is_billable) {
if (props.project.billable_rate) {
@@ -84,6 +91,7 @@ const showEditProjectModal = ref(false);
<ProjectMoreOptionsDropdown
:project="project"
@edit="showEditProjectModal = true"
@archive="archiveProject"
@delete="deleteProject"></ProjectMoreOptionsDropdown>
</div>
</TableRow>

View File

@@ -8,7 +8,7 @@ const props = defineProps<{
const activeClass = computed(() => {
if (props.active) {
return 'bg-tab-background hover:bg-tab-background-active border border-tab-border text-white font-semibold';
return 'bg-tab-background border border-tab-border text-white font-semibold';
}
return '';
});
@@ -16,9 +16,10 @@ const activeClass = computed(() => {
<template>
<button
role="tab"
:class="
twMerge(
'rounded-md transition px-2 sm:px-3 py-1 sm:py-1.5 text-xs sm:text-sm font-medium hover:text-white focus-visible:outline-none',
'rounded-md px-2 sm:px-3 py-1 sm:py-1.5 text-xs sm:text-sm font-medium hover:text-white focus-visible:outline-none',
activeClass
)
">

View File

@@ -1,11 +1,16 @@
<script setup lang="ts">
import Dropdown from '@/Components/Dropdown.vue';
import { TrashIcon, PencilSquareIcon } from '@heroicons/vue/20/solid';
import {
TrashIcon,
PencilSquareIcon,
CheckCircleIcon,
} from '@heroicons/vue/20/solid';
import type { Task } from '@/utils/api';
import { canDeleteTasks, canUpdateTasks } from '@/utils/permissions';
const emit = defineEmits<{
delete: [];
edit: [];
done: [];
}>();
const props = defineProps<{
task: Task;
@@ -15,20 +20,23 @@ const props = defineProps<{
<template>
<Dropdown align="bottom-end">
<template #trigger>
<svg
<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"
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>
: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]">
@@ -42,6 +50,16 @@ const props = defineProps<{
class="w-5 text-icon-active"></PencilSquareIcon>
<span>Edit</span>
</button>
<button
@click="emit('done')"
v-if="canUpdateTasks()"
: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">
<CheckCircleIcon
class="w-5 text-icon-active"></CheckCircleIcon>
<span v-if="props.task.is_done">Mark as active</span>
<span v-else>Mark as done</span>
</button>
<button
@click="emit('delete')"
:aria-label="'Delete Task ' + props.task.name"

View File

@@ -2,24 +2,18 @@
import SecondaryButton from '@/Components/SecondaryButton.vue';
import { PlusCircleIcon } from '@heroicons/vue/24/solid';
import { PlusIcon } from '@heroicons/vue/16/solid';
import { computed, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useTasksStore } from '@/utils/useTasks';
import { ref } from 'vue';
import TaskTableRow from '@/Components/Common/Task/TaskTableRow.vue';
import TaskTableHeading from '@/Components/Common/Task/TaskTableHeading.vue';
import TaskCreateModal from '@/Components/Common/Task/TaskCreateModal.vue';
import { canCreateTasks } from '@/utils/permissions';
const { tasks } = storeToRefs(useTasksStore());
import type { Task } from '@/utils/api';
const props = defineProps<{
projectId: string;
tasks: Task[];
}>();
const projectTasks = computed(() => {
return tasks.value.filter((task) => task.project_id === props.projectId);
});
const createTask = ref(false);
</script>
@@ -31,12 +25,13 @@ const createTask = ref(false);
<div class="inline-block min-w-full align-middle">
<div
data-testid="task_table"
role="table"
class="grid min-w-full"
style="grid-template-columns: 1fr 150px 80px">
<TaskTableHeading></TaskTableHeading>
<div
class="col-span-5 py-24 text-center"
v-if="projectTasks.length === 0">
v-if="tasks.length === 0">
<PlusCircleIcon
class="w-8 text-icon-default inline pb-2"></PlusCircleIcon>
<h3 class="text-white font-semibold">No tasks found</h3>
@@ -50,7 +45,7 @@ const createTask = ref(false);
>Create your First Task
</SecondaryButton>
</div>
<template v-for="task in projectTasks" :key="task.id">
<template v-for="task in tasks" :key="task.id">
<TaskTableRow :task="task"></TaskTableRow>
</template>
</div>

View File

@@ -15,6 +15,14 @@ const props = defineProps<{
function deleteTask() {
useTasksStore().deleteTask(props.task.id);
}
function markTaskAsDone() {
useTasksStore().updateTask(props.task.id, {
...props.task,
is_done: !props.task.is_done,
});
}
const showTaskEditModal = ref(false);
</script>
@@ -28,14 +36,20 @@ const showTaskEditModal = ref(false);
</div>
<div
class="whitespace-nowrap px-3 py-4 text-sm text-muted flex space-x-1 items-center font-medium">
<CheckCircleIcon class="w-5"></CheckCircleIcon>
<span>Active</span>
<template v-if="task.is_done">
<CheckCircleIcon class="w-5"></CheckCircleIcon>
<span>Done</span>
</template>
<template v-else>
<span>Active</span>
</template>
</div>
<div
class="relative whitespace-nowrap flex items-center pl-3 text-right text-sm font-medium sm:pr-0 pr-4 sm:pr-6 lg:pr-8 3xl:pr-12">
<TaskMoreOptionsDropdown
v-if="canDeleteTasks()"
:task="task"
@done="markTaskAsDone"
@edit="showTaskEditModal = true"
@delete="deleteTask"></TaskMoreOptionsDropdown>
</div>

View File

@@ -58,24 +58,33 @@ withDefaults(
const filteredProjects = computed(() => {
return projects.value.reduce(
(filtered: ProjectWithTasks[], project) => {
const projectNameIncludesSearchTerm = project.name
(filtered: ProjectWithTasks[], filterProject) => {
const projectNameIncludesSearchTerm = filterProject.name
.toLowerCase()
.includes(searchValue.value?.toLowerCase()?.trim() || '');
// check if one of the project tasks
const projectTasks = tasks.value.filter((task) => {
return task.project_id === project.id;
return task.project_id === filterProject.id;
});
const filteredTasks = projectTasks.filter((task) => {
return task.name
.toLowerCase()
.includes(searchValue.value?.toLowerCase()?.trim() || '');
const filteredTasks = projectTasks.filter((filterTask) => {
return (
filterTask.name
.toLowerCase()
.includes(
searchValue.value?.toLowerCase()?.trim() || ''
) &&
(!filterTask.is_done || filterTask.id === task.value)
);
});
if (projectNameIncludesSearchTerm || filteredTasks.length > 0) {
filtered.push({ project: project, tasks: filteredTasks });
if (
(projectNameIncludesSearchTerm || filteredTasks.length > 0) &&
(!filterProject.is_archived ||
project.value === filterProject.id)
) {
filtered.push({ project: filterProject, tasks: filteredTasks });
}
return filtered;