mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-18 21:22:15 +01:00
add support for archiving projects and marking tasks as done
This commit is contained in:
@@ -54,6 +54,37 @@ test('test that creating and deleting a new project via the modal works', async
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('test that archiving and unarchiving projects works', async ({ page }) => {
|
||||||
|
const newProjectName =
|
||||||
|
'New Project ' + Math.floor(1 + Math.random() * 10000);
|
||||||
|
await goToProjectsOverview(page);
|
||||||
|
await page.getByRole('button', { name: 'Create Project' }).click();
|
||||||
|
await page.getByLabel('Project Name').fill(newProjectName);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Create Project' }).nth(1).click();
|
||||||
|
await expect(page.getByText(newProjectName)).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('row').first().getByRole('button').click();
|
||||||
|
await Promise.all([
|
||||||
|
page.getByRole('button').getByText('Archive').first().click(),
|
||||||
|
expect(page.getByText(newProjectName)).not.toBeVisible(),
|
||||||
|
]);
|
||||||
|
await Promise.all([
|
||||||
|
page.getByRole('tab', { name: 'Archived' }).click(),
|
||||||
|
expect(page.getByText(newProjectName)).toBeVisible(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await page.getByRole('row').first().getByRole('button').click();
|
||||||
|
await Promise.all([
|
||||||
|
page.getByRole('button').getByText('Unarchive').first().click(),
|
||||||
|
expect(page.getByText(newProjectName)).not.toBeVisible(),
|
||||||
|
]);
|
||||||
|
await Promise.all([
|
||||||
|
page.getByRole('tab', { name: 'Active' }).click(),
|
||||||
|
expect(page.getByText(newProjectName)).toBeVisible(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
// Create new project with new Client
|
// Create new project with new Client
|
||||||
|
|
||||||
// Create new project with existing Client
|
// Create new project with existing Client
|
||||||
|
|||||||
@@ -98,6 +98,47 @@ test('test that creating and deleting a new tag in a new project works', async (
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('test that archiving and unarchiving tasks works', async ({ page }) => {
|
||||||
|
const newProjectName =
|
||||||
|
'New Project ' + Math.floor(1 + Math.random() * 10000);
|
||||||
|
const newTaskName = 'New Project ' + Math.floor(1 + Math.random() * 10000);
|
||||||
|
|
||||||
|
await goToProjectsOverview(page);
|
||||||
|
await page.getByRole('button', { name: 'Create Project' }).click();
|
||||||
|
await page.getByLabel('Project Name').fill(newProjectName);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Create Project' }).nth(1).click();
|
||||||
|
await expect(page.getByText(newProjectName)).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByText(newProjectName).click();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Create Task' }).click();
|
||||||
|
await page.getByPlaceholder('Task Name').fill(newTaskName);
|
||||||
|
await page.getByRole('button', { name: 'Create Task' }).nth(1).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole('table')).toContainText(newTaskName);
|
||||||
|
|
||||||
|
await page.getByRole('row').first().getByRole('button').click();
|
||||||
|
await Promise.all([
|
||||||
|
page.getByRole('button').getByText('Mark as done').first().click(),
|
||||||
|
expect(page.getByText(newTaskName)).not.toBeVisible(),
|
||||||
|
]);
|
||||||
|
await Promise.all([
|
||||||
|
page.getByRole('tab', { name: 'Done' }).click(),
|
||||||
|
expect(page.getByText(newTaskName)).toBeVisible(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await page.getByRole('row').first().getByRole('button').click();
|
||||||
|
await Promise.all([
|
||||||
|
page.getByRole('button').getByText('Mark as active').first().click(),
|
||||||
|
expect(page.getByText(newTaskName)).not.toBeVisible(),
|
||||||
|
]);
|
||||||
|
await Promise.all([
|
||||||
|
page.getByRole('tab', { name: 'Active' }).click(),
|
||||||
|
expect(page.getByText(newTaskName)).toBeVisible(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
// Create new project with new Client
|
// Create new project with new Client
|
||||||
|
|
||||||
// Create new project with existing Client
|
// Create new project with existing Client
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ defineProps<{
|
|||||||
{{ title }}
|
{{ title }}
|
||||||
</span>
|
</span>
|
||||||
</h3>
|
</h3>
|
||||||
<div>
|
<div class="flex-1 flex justify-end items-center">
|
||||||
<slot name="actions"></slot>
|
<slot name="actions"></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Dropdown from '@/Components/Dropdown.vue';
|
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 type { Project } from '@/utils/api';
|
||||||
import { canDeleteProjects, canUpdateProjects } from '@/utils/permissions';
|
import { canDeleteProjects, canUpdateProjects } from '@/utils/permissions';
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
delete: [];
|
delete: [];
|
||||||
edit: [];
|
edit: [];
|
||||||
|
archive: [];
|
||||||
}>();
|
}>();
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
project: Project;
|
project: Project;
|
||||||
@@ -15,20 +20,23 @@ const props = defineProps<{
|
|||||||
<template>
|
<template>
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<template #trigger>
|
<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"
|
data-testid="project_actions"
|
||||||
:aria-label="'Actions for Project ' + props.project.name"
|
: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"
|
<svg
|
||||||
viewBox="0 0 24 24"
|
class="h-10 w-10 p-2 rounded-full"
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
viewBox="0 0 24 24"
|
||||||
<path
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
fill="none"
|
<path
|
||||||
stroke="currentColor"
|
fill="none"
|
||||||
stroke-linecap="round"
|
stroke="currentColor"
|
||||||
stroke-linejoin="round"
|
stroke-linecap="round"
|
||||||
stroke-width="1.5"
|
stroke-linejoin="round"
|
||||||
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" />
|
stroke-width="1.5"
|
||||||
</svg>
|
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>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="min-w-[150px]">
|
<div class="min-w-[150px]">
|
||||||
@@ -42,6 +50,17 @@ const props = defineProps<{
|
|||||||
class="w-5 text-icon-active"></PencilSquareIcon>
|
class="w-5 text-icon-active"></PencilSquareIcon>
|
||||||
<span>Edit</span>
|
<span>Edit</span>
|
||||||
</button>
|
</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
|
<button
|
||||||
@click.prevent="emit('delete')"
|
@click.prevent="emit('delete')"
|
||||||
:aria-label="'Delete Project ' + props.project.name"
|
:aria-label="'Delete Project ' + props.project.name"
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useProjectsStore } from '@/utils/useProjects';
|
|
||||||
import SecondaryButton from '@/Components/SecondaryButton.vue';
|
import SecondaryButton from '@/Components/SecondaryButton.vue';
|
||||||
import { FolderPlusIcon } from '@heroicons/vue/24/solid';
|
import { FolderPlusIcon } from '@heroicons/vue/24/solid';
|
||||||
import { PlusIcon } from '@heroicons/vue/16/solid';
|
import { PlusIcon } from '@heroicons/vue/16/solid';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import ProjectCreateModal from '@/Components/Common/Project/ProjectCreateModal.vue';
|
import ProjectCreateModal from '@/Components/Common/Project/ProjectCreateModal.vue';
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import ProjectTableHeading from '@/Components/Common/Project/ProjectTableHeading.vue';
|
import ProjectTableHeading from '@/Components/Common/Project/ProjectTableHeading.vue';
|
||||||
import ProjectTableRow from '@/Components/Common/Project/ProjectTableRow.vue';
|
import ProjectTableRow from '@/Components/Common/Project/ProjectTableRow.vue';
|
||||||
import { canCreateProjects } from '@/utils/permissions';
|
import { canCreateProjects } from '@/utils/permissions';
|
||||||
|
import type { Project } from '@/utils/api';
|
||||||
|
|
||||||
const { projects } = storeToRefs(useProjectsStore());
|
defineProps<{
|
||||||
|
projects: Project[];
|
||||||
|
}>();
|
||||||
|
|
||||||
const createProject = ref(false);
|
const createProject = ref(false);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -33,6 +33,13 @@ function deleteProject() {
|
|||||||
useProjectsStore().deleteProject(props.project.id);
|
useProjectsStore().deleteProject(props.project.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function archiveProject() {
|
||||||
|
useProjectsStore().updateProject(props.project.id, {
|
||||||
|
...props.project,
|
||||||
|
is_archived: !props.project.is_archived,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const billableRateInfo = computed(() => {
|
const billableRateInfo = computed(() => {
|
||||||
if (props.project.is_billable) {
|
if (props.project.is_billable) {
|
||||||
if (props.project.billable_rate) {
|
if (props.project.billable_rate) {
|
||||||
@@ -84,6 +91,7 @@ const showEditProjectModal = ref(false);
|
|||||||
<ProjectMoreOptionsDropdown
|
<ProjectMoreOptionsDropdown
|
||||||
:project="project"
|
:project="project"
|
||||||
@edit="showEditProjectModal = true"
|
@edit="showEditProjectModal = true"
|
||||||
|
@archive="archiveProject"
|
||||||
@delete="deleteProject"></ProjectMoreOptionsDropdown>
|
@delete="deleteProject"></ProjectMoreOptionsDropdown>
|
||||||
</div>
|
</div>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const activeClass = computed(() => {
|
const activeClass = computed(() => {
|
||||||
if (props.active) {
|
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 '';
|
return '';
|
||||||
});
|
});
|
||||||
@@ -16,9 +16,10 @@ const activeClass = computed(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
|
role="tab"
|
||||||
:class="
|
:class="
|
||||||
twMerge(
|
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
|
activeClass
|
||||||
)
|
)
|
||||||
">
|
">
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Dropdown from '@/Components/Dropdown.vue';
|
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 type { Task } from '@/utils/api';
|
||||||
import { canDeleteTasks, canUpdateTasks } from '@/utils/permissions';
|
import { canDeleteTasks, canUpdateTasks } from '@/utils/permissions';
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
delete: [];
|
delete: [];
|
||||||
edit: [];
|
edit: [];
|
||||||
|
done: [];
|
||||||
}>();
|
}>();
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
task: Task;
|
task: Task;
|
||||||
@@ -15,20 +20,23 @@ const props = defineProps<{
|
|||||||
<template>
|
<template>
|
||||||
<Dropdown align="bottom-end">
|
<Dropdown align="bottom-end">
|
||||||
<template #trigger>
|
<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"
|
data-testid="task_actions"
|
||||||
:aria-label="'Actions for Task ' + props.task.name"
|
: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"
|
<svg
|
||||||
viewBox="0 0 24 24"
|
class="h-10 w-10 p-2 rounded-full"
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
viewBox="0 0 24 24"
|
||||||
<path
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
fill="none"
|
<path
|
||||||
stroke="currentColor"
|
fill="none"
|
||||||
stroke-linecap="round"
|
stroke="currentColor"
|
||||||
stroke-linejoin="round"
|
stroke-linecap="round"
|
||||||
stroke-width="1.5"
|
stroke-linejoin="round"
|
||||||
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" />
|
stroke-width="1.5"
|
||||||
</svg>
|
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>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="min-w-[150px]">
|
<div class="min-w-[150px]">
|
||||||
@@ -42,6 +50,16 @@ const props = defineProps<{
|
|||||||
class="w-5 text-icon-active"></PencilSquareIcon>
|
class="w-5 text-icon-active"></PencilSquareIcon>
|
||||||
<span>Edit</span>
|
<span>Edit</span>
|
||||||
</button>
|
</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
|
<button
|
||||||
@click="emit('delete')"
|
@click="emit('delete')"
|
||||||
:aria-label="'Delete Task ' + props.task.name"
|
:aria-label="'Delete Task ' + props.task.name"
|
||||||
|
|||||||
@@ -2,24 +2,18 @@
|
|||||||
import SecondaryButton from '@/Components/SecondaryButton.vue';
|
import SecondaryButton from '@/Components/SecondaryButton.vue';
|
||||||
import { PlusCircleIcon } from '@heroicons/vue/24/solid';
|
import { PlusCircleIcon } from '@heroicons/vue/24/solid';
|
||||||
import { PlusIcon } from '@heroicons/vue/16/solid';
|
import { PlusIcon } from '@heroicons/vue/16/solid';
|
||||||
import { computed, ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import { useTasksStore } from '@/utils/useTasks';
|
|
||||||
import TaskTableRow from '@/Components/Common/Task/TaskTableRow.vue';
|
import TaskTableRow from '@/Components/Common/Task/TaskTableRow.vue';
|
||||||
import TaskTableHeading from '@/Components/Common/Task/TaskTableHeading.vue';
|
import TaskTableHeading from '@/Components/Common/Task/TaskTableHeading.vue';
|
||||||
import TaskCreateModal from '@/Components/Common/Task/TaskCreateModal.vue';
|
import TaskCreateModal from '@/Components/Common/Task/TaskCreateModal.vue';
|
||||||
import { canCreateTasks } from '@/utils/permissions';
|
import { canCreateTasks } from '@/utils/permissions';
|
||||||
|
import type { Task } from '@/utils/api';
|
||||||
const { tasks } = storeToRefs(useTasksStore());
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
tasks: Task[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const projectTasks = computed(() => {
|
|
||||||
return tasks.value.filter((task) => task.project_id === props.projectId);
|
|
||||||
});
|
|
||||||
|
|
||||||
const createTask = ref(false);
|
const createTask = ref(false);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -31,12 +25,13 @@ const createTask = ref(false);
|
|||||||
<div class="inline-block min-w-full align-middle">
|
<div class="inline-block min-w-full align-middle">
|
||||||
<div
|
<div
|
||||||
data-testid="task_table"
|
data-testid="task_table"
|
||||||
|
role="table"
|
||||||
class="grid min-w-full"
|
class="grid min-w-full"
|
||||||
style="grid-template-columns: 1fr 150px 80px">
|
style="grid-template-columns: 1fr 150px 80px">
|
||||||
<TaskTableHeading></TaskTableHeading>
|
<TaskTableHeading></TaskTableHeading>
|
||||||
<div
|
<div
|
||||||
class="col-span-5 py-24 text-center"
|
class="col-span-5 py-24 text-center"
|
||||||
v-if="projectTasks.length === 0">
|
v-if="tasks.length === 0">
|
||||||
<PlusCircleIcon
|
<PlusCircleIcon
|
||||||
class="w-8 text-icon-default inline pb-2"></PlusCircleIcon>
|
class="w-8 text-icon-default inline pb-2"></PlusCircleIcon>
|
||||||
<h3 class="text-white font-semibold">No tasks found</h3>
|
<h3 class="text-white font-semibold">No tasks found</h3>
|
||||||
@@ -50,7 +45,7 @@ const createTask = ref(false);
|
|||||||
>Create your First Task
|
>Create your First Task
|
||||||
</SecondaryButton>
|
</SecondaryButton>
|
||||||
</div>
|
</div>
|
||||||
<template v-for="task in projectTasks" :key="task.id">
|
<template v-for="task in tasks" :key="task.id">
|
||||||
<TaskTableRow :task="task"></TaskTableRow>
|
<TaskTableRow :task="task"></TaskTableRow>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,6 +15,14 @@ const props = defineProps<{
|
|||||||
function deleteTask() {
|
function deleteTask() {
|
||||||
useTasksStore().deleteTask(props.task.id);
|
useTasksStore().deleteTask(props.task.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function markTaskAsDone() {
|
||||||
|
useTasksStore().updateTask(props.task.id, {
|
||||||
|
...props.task,
|
||||||
|
is_done: !props.task.is_done,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const showTaskEditModal = ref(false);
|
const showTaskEditModal = ref(false);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -28,14 +36,20 @@ const showTaskEditModal = ref(false);
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="whitespace-nowrap px-3 py-4 text-sm text-muted flex space-x-1 items-center font-medium">
|
class="whitespace-nowrap px-3 py-4 text-sm text-muted flex space-x-1 items-center font-medium">
|
||||||
<CheckCircleIcon class="w-5"></CheckCircleIcon>
|
<template v-if="task.is_done">
|
||||||
<span>Active</span>
|
<CheckCircleIcon class="w-5"></CheckCircleIcon>
|
||||||
|
<span>Done</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span>Active</span>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<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">
|
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
|
<TaskMoreOptionsDropdown
|
||||||
v-if="canDeleteTasks()"
|
v-if="canDeleteTasks()"
|
||||||
:task="task"
|
:task="task"
|
||||||
|
@done="markTaskAsDone"
|
||||||
@edit="showTaskEditModal = true"
|
@edit="showTaskEditModal = true"
|
||||||
@delete="deleteTask"></TaskMoreOptionsDropdown>
|
@delete="deleteTask"></TaskMoreOptionsDropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -58,24 +58,33 @@ withDefaults(
|
|||||||
|
|
||||||
const filteredProjects = computed(() => {
|
const filteredProjects = computed(() => {
|
||||||
return projects.value.reduce(
|
return projects.value.reduce(
|
||||||
(filtered: ProjectWithTasks[], project) => {
|
(filtered: ProjectWithTasks[], filterProject) => {
|
||||||
const projectNameIncludesSearchTerm = project.name
|
const projectNameIncludesSearchTerm = filterProject.name
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(searchValue.value?.toLowerCase()?.trim() || '');
|
.includes(searchValue.value?.toLowerCase()?.trim() || '');
|
||||||
|
|
||||||
// check if one of the project tasks
|
// check if one of the project tasks
|
||||||
const projectTasks = tasks.value.filter((task) => {
|
const projectTasks = tasks.value.filter((task) => {
|
||||||
return task.project_id === project.id;
|
return task.project_id === filterProject.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredTasks = projectTasks.filter((task) => {
|
const filteredTasks = projectTasks.filter((filterTask) => {
|
||||||
return task.name
|
return (
|
||||||
.toLowerCase()
|
filterTask.name
|
||||||
.includes(searchValue.value?.toLowerCase()?.trim() || '');
|
.toLowerCase()
|
||||||
|
.includes(
|
||||||
|
searchValue.value?.toLowerCase()?.trim() || ''
|
||||||
|
) &&
|
||||||
|
(!filterTask.is_done || filterTask.id === task.value)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (projectNameIncludesSearchTerm || filteredTasks.length > 0) {
|
if (
|
||||||
filtered.push({ project: project, tasks: filteredTasks });
|
(projectNameIncludesSearchTerm || filteredTasks.length > 0) &&
|
||||||
|
(!filterProject.is_archived ||
|
||||||
|
project.value === filterProject.id)
|
||||||
|
) {
|
||||||
|
filtered.push({ project: filterProject, tasks: filteredTasks });
|
||||||
}
|
}
|
||||||
|
|
||||||
return filtered;
|
return filtered;
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ import ProjectMemberTable from '@/Components/Common/ProjectMember/ProjectMemberT
|
|||||||
import ProjectMemberCreateModal from '@/Components/Common/ProjectMember/ProjectMemberCreateModal.vue';
|
import ProjectMemberCreateModal from '@/Components/Common/ProjectMember/ProjectMemberCreateModal.vue';
|
||||||
import { useProjectMembersStore } from '@/utils/useProjectMembers';
|
import { useProjectMembersStore } from '@/utils/useProjectMembers';
|
||||||
import { canCreateTasks, canViewProjectMembers } from '@/utils/permissions';
|
import { canCreateTasks, canViewProjectMembers } from '@/utils/permissions';
|
||||||
|
import TabBarItem from '@/Components/Common/TabBar/TabBarItem.vue';
|
||||||
|
import TabBar from '@/Components/Common/TabBar/TabBar.vue';
|
||||||
|
import { useTasksStore } from '@/utils/useTasks';
|
||||||
|
|
||||||
const { projects } = storeToRefs(useProjectsStore());
|
const { projects } = storeToRefs(useProjectsStore());
|
||||||
|
|
||||||
@@ -41,6 +44,23 @@ onMounted(() => {
|
|||||||
useProjectMembersStore().fetchProjectMembers(projectId);
|
useProjectMembersStore().fetchProjectMembers(projectId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const activeTab = ref<'active' | 'done'>('active');
|
||||||
|
|
||||||
|
function isActiveTab(tab: string) {
|
||||||
|
return activeTab.value === tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { tasks } = storeToRefs(useTasksStore());
|
||||||
|
|
||||||
|
const shownTasks = computed(() => {
|
||||||
|
return tasks.value.filter((task) => {
|
||||||
|
if (activeTab.value === 'active') {
|
||||||
|
return task.project_id === projectId && !task.is_done;
|
||||||
|
}
|
||||||
|
return task.project_id === projectId && task.is_done;
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -85,19 +105,38 @@ onMounted(() => {
|
|||||||
<div>
|
<div>
|
||||||
<CardTitle title="Tasks" :icon="CheckCircleIcon">
|
<CardTitle title="Tasks" :icon="CheckCircleIcon">
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<SecondaryButton
|
<div
|
||||||
v-if="canCreateTasks()"
|
class="w-full items-center flex justify-between">
|
||||||
:icon="PlusIcon"
|
<div class="pl-6">
|
||||||
@click="createTask = true"
|
<TabBar>
|
||||||
>Create Task
|
<TabBarItem
|
||||||
</SecondaryButton>
|
:active="isActiveTab('active')"
|
||||||
<TaskCreateModal
|
@click="activeTab = 'active'"
|
||||||
:project-id="projectId"
|
>Active
|
||||||
v-model:show="createTask"></TaskCreateModal>
|
</TabBarItem>
|
||||||
|
<TabBarItem
|
||||||
|
:active="isActiveTab('done')"
|
||||||
|
@click="activeTab = 'done'"
|
||||||
|
>Done
|
||||||
|
</TabBarItem>
|
||||||
|
</TabBar>
|
||||||
|
</div>
|
||||||
|
<SecondaryButton
|
||||||
|
v-if="canCreateTasks()"
|
||||||
|
:icon="PlusIcon"
|
||||||
|
@click="createTask = true"
|
||||||
|
>Create Task
|
||||||
|
</SecondaryButton>
|
||||||
|
<TaskCreateModal
|
||||||
|
:project-id="projectId"
|
||||||
|
v-model:show="createTask"></TaskCreateModal>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<Card>
|
<Card>
|
||||||
<TaskTable :project-id="projectId"></TaskTable>
|
<TaskTable
|
||||||
|
:tasks="shownTasks"
|
||||||
|
:project-id="projectId"></TaskTable>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="canViewProjectMembers()">
|
<div v-if="canViewProjectMembers()">
|
||||||
|
|||||||
@@ -4,17 +4,37 @@ import AppLayout from '@/Layouts/AppLayout.vue';
|
|||||||
import { FolderIcon, PlusIcon } from '@heroicons/vue/16/solid';
|
import { FolderIcon, PlusIcon } from '@heroicons/vue/16/solid';
|
||||||
import SecondaryButton from '@/Components/SecondaryButton.vue';
|
import SecondaryButton from '@/Components/SecondaryButton.vue';
|
||||||
import ProjectTable from '@/Components/Common/Project/ProjectTable.vue';
|
import ProjectTable from '@/Components/Common/Project/ProjectTable.vue';
|
||||||
import { onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { useProjectsStore } from '@/utils/useProjects';
|
import { useProjectsStore } from '@/utils/useProjects';
|
||||||
import ProjectCreateModal from '@/Components/Common/Project/ProjectCreateModal.vue';
|
import ProjectCreateModal from '@/Components/Common/Project/ProjectCreateModal.vue';
|
||||||
import PageTitle from '@/Components/Common/PageTitle.vue';
|
import PageTitle from '@/Components/Common/PageTitle.vue';
|
||||||
import { canCreateProjects } from '@/utils/permissions';
|
import { canCreateProjects } from '@/utils/permissions';
|
||||||
|
import TabBarItem from '@/Components/Common/TabBar/TabBarItem.vue';
|
||||||
|
import TabBar from '@/Components/Common/TabBar/TabBar.vue';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
useProjectsStore().fetchProjects();
|
useProjectsStore().fetchProjects();
|
||||||
});
|
});
|
||||||
|
|
||||||
const createProject = ref(false);
|
const createProject = ref(false);
|
||||||
|
|
||||||
|
const activeTab = ref<'active' | 'archived'>('active');
|
||||||
|
|
||||||
|
function isActiveTab(tab: string) {
|
||||||
|
return activeTab.value === tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { projects } = storeToRefs(useProjectsStore());
|
||||||
|
|
||||||
|
const shownProjects = computed(() => {
|
||||||
|
return projects.value.filter((project) => {
|
||||||
|
if (activeTab.value === 'active') {
|
||||||
|
return !project.is_archived;
|
||||||
|
}
|
||||||
|
return project.is_archived;
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -22,17 +42,29 @@ const createProject = ref(false);
|
|||||||
<MainContainer
|
<MainContainer
|
||||||
class="py-3 sm:py-5 border-b border-default-background-separator flex justify-between items-center">
|
class="py-3 sm:py-5 border-b border-default-background-separator flex justify-between items-center">
|
||||||
<div class="flex items-center space-x-3 sm:space-x-6">
|
<div class="flex items-center space-x-3 sm:space-x-6">
|
||||||
<PageTitle :icon="FolderIcon" title="Projects"> </PageTitle>
|
<PageTitle :icon="FolderIcon" title="Projects"></PageTitle>
|
||||||
|
<TabBar>
|
||||||
|
<TabBarItem
|
||||||
|
:active="isActiveTab('active')"
|
||||||
|
@click="activeTab = 'active'"
|
||||||
|
>Active</TabBarItem
|
||||||
|
>
|
||||||
|
<TabBarItem
|
||||||
|
:active="isActiveTab('archived')"
|
||||||
|
@click="activeTab = 'archived'">
|
||||||
|
Archived
|
||||||
|
</TabBarItem>
|
||||||
|
</TabBar>
|
||||||
</div>
|
</div>
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
v-if="canCreateProjects()"
|
v-if="canCreateProjects()"
|
||||||
:icon="PlusIcon"
|
:icon="PlusIcon"
|
||||||
@click="createProject = true"
|
@click="createProject = true"
|
||||||
>Create Project</SecondaryButton
|
>Create Project
|
||||||
>
|
</SecondaryButton>
|
||||||
<ProjectCreateModal
|
<ProjectCreateModal
|
||||||
v-model:show="createProject"></ProjectCreateModal>
|
v-model:show="createProject"></ProjectCreateModal>
|
||||||
</MainContainer>
|
</MainContainer>
|
||||||
<ProjectTable></ProjectTable>
|
<ProjectTable :projects="shownProjects"></ProjectTable>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ export const useProjectsStore = defineStore('projects', () => {
|
|||||||
params: {
|
params: {
|
||||||
organization: organization,
|
organization: organization,
|
||||||
},
|
},
|
||||||
|
queries: {
|
||||||
|
archived: 'all',
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
undefined,
|
undefined,
|
||||||
'Failed to fetch projects'
|
'Failed to fetch projects'
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ export const useTasksStore = defineStore('tasks', () => {
|
|||||||
params: {
|
params: {
|
||||||
organization: organizationId,
|
organization: organizationId,
|
||||||
},
|
},
|
||||||
|
queries: {
|
||||||
|
done: 'all',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
if (tasksResponse?.data) {
|
if (tasksResponse?.data) {
|
||||||
|
|||||||
Reference in New Issue
Block a user