diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index 2bb9b68b..f7297ff6 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -5,5 +5,9 @@ module.exports = {
extends: ['plugin:vue/vue3-essential', '@vue/eslint-config-typescript/recommended', '@vue/eslint-config-prettier'],
rules: {
'vue/multi-word-component-names': 'off',
- }
+ "@typescript-eslint/no-unused-vars": "off",
+ "unused-imports/no-unused-imports": "error",
+ "unused-imports/no-unused-vars": "error",
+ },
+ plugins: ['unused-imports'],
}
diff --git a/e2e/tasks.spec.ts b/e2e/tasks.spec.ts
new file mode 100644
index 00000000..4a8196b4
--- /dev/null
+++ b/e2e/tasks.spec.ts
@@ -0,0 +1,109 @@
+import { expect, Page } from '@playwright/test';
+import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
+import { test } from '../playwright/fixtures';
+
+async function goToProjectsOverview(page: Page) {
+ await page.goto(PLAYWRIGHT_BASE_URL + '/projects');
+}
+
+// Create new project via modal
+test('test that creating and deleting a new tag in a new project 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.getByPlaceholder('Project Name').fill(newProjectName);
+ await Promise.all([
+ page.getByRole('button', { name: 'Create Project' }).nth(1).click(),
+ page.waitForResponse(
+ async (response) =>
+ response.url().includes('/projects') &&
+ response.request().method() === 'POST' &&
+ response.status() === 201 &&
+ (await response.json()).data.id !== null &&
+ (await response.json()).data.color !== null &&
+ (await response.json()).data.client_id === null &&
+ (await response.json()).data.name === newProjectName
+ ),
+ ]);
+
+ await expect(page.getByTestId('project_table')).toContainText(
+ newProjectName
+ );
+
+ await page.getByText(newProjectName).click();
+
+ const newTaskName = 'New Project ' + Math.floor(1 + Math.random() * 10000);
+
+ await page.getByRole('button', { name: 'Create Task' }).click();
+ await page.getByPlaceholder('Task Name').fill(newTaskName);
+
+ await Promise.all([
+ page.getByRole('button', { name: 'Create Task' }).nth(1).click(),
+ page.waitForResponse(
+ async (response) =>
+ response.url().includes('/tasks') &&
+ response.request().method() === 'POST' &&
+ response.status() === 201 &&
+ (await response.json()).data.id !== null &&
+ (await response.json()).data.project_id !== null &&
+ (await response.json()).data.name === newTaskName
+ ),
+ ]);
+
+ await expect(page.getByTestId('task_table')).toContainText(newTaskName);
+
+ const taskMoreButton = page.locator(
+ "[aria-label='Actions for Task " + newTaskName + "']"
+ );
+ taskMoreButton.click();
+ const taskDeleteButton = page.locator(
+ "[aria-label='Delete Task " + newTaskName + "']"
+ );
+
+ await Promise.all([
+ taskDeleteButton.click(),
+ page.waitForResponse(
+ async (response) =>
+ response.url().includes('/tasks') &&
+ response.request().method() === 'DELETE' &&
+ response.status() === 204
+ ),
+ ]);
+ await expect(page.getByTestId('task_table')).not.toContainText(newTaskName);
+
+ await goToProjectsOverview(page);
+
+ const moreButton = page.locator(
+ "[aria-label='Actions for Project " + newProjectName + "']"
+ );
+ moreButton.click();
+ const deleteButton = page.locator(
+ "[aria-label='Delete Project " + newProjectName + "']"
+ );
+
+ await Promise.all([
+ deleteButton.click(),
+ page.waitForResponse(
+ async (response) =>
+ response.url().includes('/projects') &&
+ response.request().method() === 'DELETE' &&
+ response.status() === 204
+ ),
+ ]);
+ await expect(page.getByTestId('project_table')).not.toContainText(
+ newProjectName
+ );
+});
+
+// Create new project with new Client
+
+// Create new project with existing Client
+
+// Delete project via More Options
+
+// Test that project task count is displayed correctly
+
+// Test that active / archive / all filter works (once implemented)
diff --git a/package-lock.json b/package-lock.json
index 4402dff0..f063c924 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -31,6 +31,7 @@
"@vue/tsconfig": "^0.5.1",
"autoprefixer": "^10.4.7",
"axios": "^1.6.4",
+ "eslint-plugin-unused-imports": "^3.1.0",
"laravel-vite-plugin": "^1.0.0",
"openapi-zod-client": "^1.16.2",
"postcss": "^8.4.14",
@@ -2859,6 +2860,27 @@
}
}
},
+ "node_modules/eslint-plugin-unused-imports": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-3.1.0.tgz",
+ "integrity": "sha512-9l1YFCzXKkw1qtAru1RWUtG2EVDZY0a0eChKXcL+EZ5jitG7qxdctu4RnvhOJHv4xfmUf7h+JJPINlVpGhZMrw==",
+ "dev": true,
+ "dependencies": {
+ "eslint-rule-composer": "^0.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/eslint-plugin": "6 - 7",
+ "eslint": "8"
+ },
+ "peerDependenciesMeta": {
+ "@typescript-eslint/eslint-plugin": {
+ "optional": true
+ }
+ }
+ },
"node_modules/eslint-plugin-vue": {
"version": "9.24.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.24.0.tgz",
@@ -2894,6 +2916,15 @@
"node": ">=4"
}
},
+ "node_modules/eslint-rule-composer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz",
+ "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
"node_modules/eslint-scope": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
diff --git a/package.json b/package.json
index 3a45efa1..95b4798e 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"@vue/tsconfig": "^0.5.1",
"autoprefixer": "^10.4.7",
"axios": "^1.6.4",
+ "eslint-plugin-unused-imports": "^3.1.0",
"laravel-vite-plugin": "^1.0.0",
"openapi-zod-client": "^1.16.2",
"postcss": "^8.4.14",
diff --git a/resources/js/Components/Common/Client/ClientTable.vue b/resources/js/Components/Common/Client/ClientTable.vue
index 374d61d4..25ba9578 100644
--- a/resources/js/Components/Common/Client/ClientTable.vue
+++ b/resources/js/Components/Common/Client/ClientTable.vue
@@ -20,7 +20,7 @@ const createClient = ref(false);
+
-
- Name
-
-
- Status
-
-
- Edit
-
+
+
+ Name
+
+
+ Status
+
+
+ Edit
+
+
diff --git a/resources/js/Components/Common/Client/ClientTableRow.vue b/resources/js/Components/Common/Client/ClientTableRow.vue
index 8b0fe700..0e3a76f6 100644
--- a/resources/js/Components/Common/Client/ClientTableRow.vue
+++ b/resources/js/Components/Common/Client/ClientTableRow.vue
@@ -6,6 +6,7 @@ import { useClientsStore } from '@/utils/useClients';
import { storeToRefs } from 'pinia';
import ClientMoreOptionsDropdown from '@/Components/Common/Client/ClientMoreOptionsDropdown.vue';
import { useProjectsStore } from '@/utils/useProjects';
+import TableRow from '@/Components/TableRow.vue';
const { projects } = storeToRefs(useProjectsStore());
@@ -25,24 +26,26 @@ const projectCount = computed(() => {
-
-
- {{ client.name }}
-
- {{ projectCount }} Projects
-
-
-
- Active
-
-
-
-
+
+
+
+ {{ client.name }}
+
+ {{ projectCount }} Projects
+
+
+
+ Active
+
+
+
+
+
diff --git a/resources/js/Components/Common/Member/MemberTable.vue b/resources/js/Components/Common/Member/MemberTable.vue
index 17944131..f316582e 100644
--- a/resources/js/Components/Common/Member/MemberTable.vue
+++ b/resources/js/Components/Common/Member/MemberTable.vue
@@ -16,7 +16,7 @@ const createClient = ref(false);
diff --git a/resources/js/Components/Common/Member/MemberTableHeading.vue b/resources/js/Components/Common/Member/MemberTableHeading.vue
index 1008fd29..55ec3ffb 100644
--- a/resources/js/Components/Common/Member/MemberTableHeading.vue
+++ b/resources/js/Components/Common/Member/MemberTableHeading.vue
@@ -1,30 +1,30 @@
-
+
-
- Name
-
-
- Email
-
-
- Role
-
-
- Billable Rate
-
-
- Status
-
-
- Edit
-
+
+
+ Name
+
+
+ Email
+
+
+ Role
+
+
+ Billable Rate
+
+
+ Status
+
+
+ Edit
+
+
diff --git a/resources/js/Components/Common/Member/MemberTableRow.vue b/resources/js/Components/Common/Member/MemberTableRow.vue
index 7913931e..b936542f 100644
--- a/resources/js/Components/Common/Member/MemberTableRow.vue
+++ b/resources/js/Components/Common/Member/MemberTableRow.vue
@@ -3,6 +3,7 @@ import type { Member } from '@/utils/api';
import { CheckCircleIcon, UserCircleIcon } from '@heroicons/vue/20/solid';
import { useClientsStore } from '@/utils/useClients';
import MemberMoreOptionsDropdown from '@/Components/Common/Member/MemberMoreOptionsDropdown.vue';
+import TableRow from '@/Components/TableRow.vue';
const props = defineProps<{
member: Member;
@@ -18,38 +19,40 @@ function capitalizeFirstLetter(string: string) {
-
-
- {{ member.name }}
-
-
-
- {{ member.email }}
-
-
- {{ capitalizeFirstLetter(member.role) }}
-
-
- {{ member.billable_rate ?? '--' }}
-
-
-
- Active
-
- Inactive
-
-
-
-
+
+
+
+ {{ member.name }}
+
+
+
+ {{ member.email }}
+
+
+ {{ capitalizeFirstLetter(member.role) }}
+
+
+ {{ member.billable_rate ?? '--' }}
+
+
+
+ Active
+
+ Inactive
+
+
+
+
+
diff --git a/resources/js/Components/Common/Project/ProjectCreateModal.vue b/resources/js/Components/Common/Project/ProjectCreateModal.vue
index 79310468..8efc15e4 100644
--- a/resources/js/Components/Common/Project/ProjectCreateModal.vue
+++ b/resources/js/Components/Common/Project/ProjectCreateModal.vue
@@ -69,6 +69,7 @@ const currentClientName = computed(() => {
v-model="project.name"
type="text"
placeholder="Project Name"
+ @keydown.enter="submit()"
class="mt-1 block w-full"
required
autocomplete="projectName" />
diff --git a/resources/js/Components/Common/Project/ProjectMoreOptionsDropdown.vue b/resources/js/Components/Common/Project/ProjectMoreOptionsDropdown.vue
index b26b4278..d4d7ff93 100644
--- a/resources/js/Components/Common/Project/ProjectMoreOptionsDropdown.vue
+++ b/resources/js/Components/Common/Project/ProjectMoreOptionsDropdown.vue
@@ -30,7 +30,7 @@ const props = defineProps<{