mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-08 00:02:15 +01:00
Add context menu actions and tests
This commit is contained in:
@@ -132,6 +132,80 @@ test('test that deleting a client via actions menu works', async ({ page, ctx })
|
|||||||
await expect(page.getByTestId('client_table')).not.toContainText(clientName);
|
await expect(page.getByTestId('client_table')).not.toContainText(clientName);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// =============================================
|
||||||
|
// Context Menu Tests
|
||||||
|
// =============================================
|
||||||
|
|
||||||
|
test('test that client context menu edit updates the client', async ({ page, ctx }) => {
|
||||||
|
const clientName = 'CtxEditClient ' + Math.floor(1 + Math.random() * 10000);
|
||||||
|
const updatedName = 'CtxUpdatedClient ' + Math.floor(1 + Math.random() * 10000);
|
||||||
|
await createClientViaApi(ctx, { name: clientName });
|
||||||
|
await goToClientsOverview(page);
|
||||||
|
|
||||||
|
const row = page.getByRole('row').filter({ hasText: clientName }).first();
|
||||||
|
await expect(row).toBeVisible();
|
||||||
|
await row.click({ button: 'right' });
|
||||||
|
await expect(page.getByRole('menu')).toBeVisible();
|
||||||
|
await page.getByRole('menuitem', { name: 'Edit' }).click();
|
||||||
|
await expect(page.getByRole('dialog')).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByPlaceholder('Client Name').fill(updatedName);
|
||||||
|
await Promise.all([
|
||||||
|
page.getByRole('button', { name: 'Update Client' }).click(),
|
||||||
|
page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url().includes('/clients') &&
|
||||||
|
response.request().method() === 'PUT' &&
|
||||||
|
response.status() === 200
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expect(page.getByTestId('client_table')).toContainText(updatedName);
|
||||||
|
await expect(page.getByTestId('client_table')).not.toContainText(clientName);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('test that client context menu archive archives the client', async ({ page, ctx }) => {
|
||||||
|
const clientName = 'CtxArchiveClient ' + Math.floor(1 + Math.random() * 10000);
|
||||||
|
await createClientViaApi(ctx, { name: clientName });
|
||||||
|
await goToClientsOverview(page);
|
||||||
|
|
||||||
|
const row = page.getByRole('row').filter({ hasText: clientName }).first();
|
||||||
|
await expect(row).toBeVisible();
|
||||||
|
await row.click({ button: 'right' });
|
||||||
|
await expect(page.getByRole('menu')).toBeVisible();
|
||||||
|
await Promise.all([
|
||||||
|
page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url().includes('/clients') &&
|
||||||
|
response.request().method() === 'PUT' &&
|
||||||
|
response.status() === 200
|
||||||
|
),
|
||||||
|
page.getByRole('menuitem', { name: 'Archive' }).click(),
|
||||||
|
]);
|
||||||
|
await expect(page.getByTestId('client_table')).not.toContainText(clientName);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('test that client context menu delete deletes the client', async ({ page, ctx }) => {
|
||||||
|
const clientName = 'CtxDeleteClient ' + Math.floor(1 + Math.random() * 10000);
|
||||||
|
await createClientViaApi(ctx, { name: clientName });
|
||||||
|
await goToClientsOverview(page);
|
||||||
|
|
||||||
|
const row = page.getByRole('row').filter({ hasText: clientName }).first();
|
||||||
|
await expect(row).toBeVisible();
|
||||||
|
await row.click({ button: 'right' });
|
||||||
|
await expect(page.getByRole('menu')).toBeVisible();
|
||||||
|
await Promise.all([
|
||||||
|
page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url().includes('/clients') &&
|
||||||
|
response.request().method() === 'DELETE' &&
|
||||||
|
response.status() === 204
|
||||||
|
),
|
||||||
|
page.getByRole('menuitem', { name: 'Delete' }).click(),
|
||||||
|
]);
|
||||||
|
await expect(page.getByTestId('client_table')).not.toContainText(clientName);
|
||||||
|
});
|
||||||
|
|
||||||
// =============================================
|
// =============================================
|
||||||
// Sorting Tests
|
// Sorting Tests
|
||||||
// =============================================
|
// =============================================
|
||||||
|
|||||||
@@ -496,6 +496,158 @@ test('test that organization owner cannot be deleted', async ({ page }) => {
|
|||||||
await expect(page.getByRole('row').filter({ hasText: 'Owner' })).toBeVisible();
|
await expect(page.getByRole('row').filter({ hasText: 'Owner' })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// =============================================
|
||||||
|
// Context Menu Tests
|
||||||
|
// =============================================
|
||||||
|
|
||||||
|
test('test that member context menu edit updates the member billable rate', async ({
|
||||||
|
page,
|
||||||
|
ctx,
|
||||||
|
}) => {
|
||||||
|
const memberName = 'CtxEditMember ' + Math.floor(1 + Math.random() * 10000);
|
||||||
|
await createPlaceholderMemberViaImportApi(ctx, memberName);
|
||||||
|
await goToMembersPage(page);
|
||||||
|
|
||||||
|
const row = page.getByRole('row').filter({ hasText: memberName }).first();
|
||||||
|
await expect(row).toBeVisible();
|
||||||
|
await row.click({ button: 'right' });
|
||||||
|
await expect(page.getByRole('menu')).toBeVisible();
|
||||||
|
await page.getByRole('menuitem', { name: 'Edit' }).click();
|
||||||
|
await expect(page.getByRole('dialog')).toBeVisible();
|
||||||
|
await expect(page.getByRole('heading', { name: 'Update Member' })).toBeVisible();
|
||||||
|
|
||||||
|
// Change billable rate from default to custom
|
||||||
|
const billableRateSelect = page.getByRole('dialog').getByRole('combobox').last();
|
||||||
|
await billableRateSelect.click();
|
||||||
|
await page.getByRole('option', { name: 'Custom Rate' }).click();
|
||||||
|
|
||||||
|
// Set a custom billable rate
|
||||||
|
await page.getByPlaceholder('Billable Rate').fill('150');
|
||||||
|
|
||||||
|
// Click Update Member — confirmation dialog should appear
|
||||||
|
await page.getByRole('button', { name: 'Update Member' }).click();
|
||||||
|
await expect(page.getByRole('heading', { name: 'Update Member Billable Rate' })).toBeVisible();
|
||||||
|
|
||||||
|
// Confirm the billable rate change
|
||||||
|
await Promise.all([
|
||||||
|
page.getByRole('button', { name: 'Yes, update existing time entries' }).click(),
|
||||||
|
page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url().includes('/members/') &&
|
||||||
|
response.request().method() === 'PUT' &&
|
||||||
|
response.status() === 200
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Verify dialog closed
|
||||||
|
await expect(page.getByRole('dialog')).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('test that member context menu merge merges the member', async ({ page, ctx }) => {
|
||||||
|
const memberName = 'CtxMergeMember ' + Math.floor(1 + Math.random() * 10000);
|
||||||
|
await createPlaceholderMemberViaImportApi(ctx, memberName);
|
||||||
|
await goToMembersPage(page);
|
||||||
|
|
||||||
|
const row = page.getByRole('row').filter({ hasText: memberName }).first();
|
||||||
|
await expect(row).toBeVisible();
|
||||||
|
await row.click({ button: 'right' });
|
||||||
|
await expect(page.getByRole('menu')).toBeVisible();
|
||||||
|
await page.getByRole('menuitem', { name: 'Merge' }).click();
|
||||||
|
await expect(page.getByRole('dialog')).toBeVisible();
|
||||||
|
await expect(page.getByRole('heading', { name: 'Merge Member' })).toBeVisible();
|
||||||
|
|
||||||
|
// Select the first available member as merge target
|
||||||
|
await page.getByRole('dialog').getByRole('button', { name: 'Select a member...' }).click();
|
||||||
|
const firstOption = page.getByRole('option').first();
|
||||||
|
await expect(firstOption).toBeVisible({ timeout: 10000 });
|
||||||
|
await firstOption.click();
|
||||||
|
|
||||||
|
// Submit merge
|
||||||
|
await Promise.all([
|
||||||
|
page.getByRole('button', { name: 'Merge Member' }).click(),
|
||||||
|
page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url().includes('/member/') &&
|
||||||
|
response.url().includes('/merge-into') &&
|
||||||
|
response.ok()
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Verify placeholder member is no longer visible
|
||||||
|
await expect(page.getByRole('dialog').filter({ hasText: 'Merge Member' })).not.toBeVisible();
|
||||||
|
await expect(page.getByRole('main').getByText(memberName)).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('test that member context menu deactivate deactivates the member', async ({
|
||||||
|
page,
|
||||||
|
browser,
|
||||||
|
}) => {
|
||||||
|
const memberId = Math.floor(Math.random() * 100000);
|
||||||
|
const memberEmail = `member+${memberId}@deactivate.test`;
|
||||||
|
const memberName = 'Deactivate Target';
|
||||||
|
|
||||||
|
// Invite and accept a new Employee member
|
||||||
|
await inviteAndAcceptMember(page, browser, memberName, memberEmail, 'Employee');
|
||||||
|
|
||||||
|
await goToMembersPage(page);
|
||||||
|
const row = page.getByRole('row').filter({ hasText: memberName }).first();
|
||||||
|
await expect(row).toBeVisible();
|
||||||
|
|
||||||
|
// Open context menu and click Deactivate
|
||||||
|
await row.click({ button: 'right' });
|
||||||
|
await expect(page.getByRole('menu')).toBeVisible();
|
||||||
|
await page.getByRole('menuitem', { name: 'Deactivate' }).click();
|
||||||
|
await expect(page.getByRole('dialog')).toBeVisible();
|
||||||
|
await expect(page.getByRole('heading', { name: 'Deactivate User' })).toBeVisible();
|
||||||
|
|
||||||
|
// Confirm deactivation
|
||||||
|
await Promise.all([
|
||||||
|
page.getByRole('button', { name: 'Deactivate' }).click(),
|
||||||
|
page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url().includes('/make-placeholder') &&
|
||||||
|
response.request().method() === 'POST' &&
|
||||||
|
response.ok()
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Verify dialog closed and member role changed to Placeholder
|
||||||
|
await expect(page.getByRole('dialog')).not.toBeVisible();
|
||||||
|
await expect(row.getByText('Placeholder', { exact: true })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('test that member context menu delete deletes the member', async ({ page, ctx }) => {
|
||||||
|
const memberName = 'CtxDeleteMember ' + Math.floor(1 + Math.random() * 10000);
|
||||||
|
await createPlaceholderMemberViaImportApi(ctx, memberName);
|
||||||
|
await goToMembersPage(page);
|
||||||
|
|
||||||
|
const row = page.getByRole('row').filter({ hasText: memberName }).first();
|
||||||
|
await expect(row).toBeVisible();
|
||||||
|
await row.click({ button: 'right' });
|
||||||
|
await expect(page.getByRole('menu')).toBeVisible();
|
||||||
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
|
await expect(page.getByRole('dialog')).toBeVisible();
|
||||||
|
await expect(page.getByRole('heading', { name: 'Delete Member' })).toBeVisible();
|
||||||
|
|
||||||
|
// Check the confirmation checkbox
|
||||||
|
await page.getByRole('checkbox').click();
|
||||||
|
|
||||||
|
// Click Delete Member button and wait for API response
|
||||||
|
await Promise.all([
|
||||||
|
page.getByRole('button', { name: 'Delete Member' }).click(),
|
||||||
|
page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url().includes('/members/') &&
|
||||||
|
response.request().method() === 'DELETE' &&
|
||||||
|
response.ok()
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Verify modal closed and member removed from table
|
||||||
|
await expect(page.getByRole('dialog')).not.toBeVisible();
|
||||||
|
await expect(page.getByRole('main').getByText(memberName)).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
// =============================================
|
// =============================================
|
||||||
// Invitations Tab Tests
|
// Invitations Tab Tests
|
||||||
// =============================================
|
// =============================================
|
||||||
|
|||||||
@@ -800,6 +800,80 @@ test('test that editing a task name on the project detail page works', async ({
|
|||||||
await expect(page.getByTestId('task_table')).not.toContainText(originalTaskName);
|
await expect(page.getByTestId('task_table')).not.toContainText(originalTaskName);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// =============================================
|
||||||
|
// Context Menu Tests
|
||||||
|
// =============================================
|
||||||
|
|
||||||
|
test('test that project context menu edit updates the project', async ({ page, ctx }) => {
|
||||||
|
const projectName = 'CtxEditProject ' + Math.floor(1 + Math.random() * 10000);
|
||||||
|
const updatedName = 'CtxUpdatedProject ' + Math.floor(1 + Math.random() * 10000);
|
||||||
|
await createProjectViaApi(ctx, { name: projectName });
|
||||||
|
await goToProjectsOverview(page);
|
||||||
|
|
||||||
|
const row = page.getByRole('row').filter({ hasText: projectName }).first();
|
||||||
|
await expect(row).toBeVisible();
|
||||||
|
await row.click({ button: 'right' });
|
||||||
|
await expect(page.getByRole('menu')).toBeVisible();
|
||||||
|
await page.getByRole('menuitem', { name: 'Edit' }).click();
|
||||||
|
await expect(page.getByRole('dialog')).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByPlaceholder('Project Name').fill(updatedName);
|
||||||
|
await Promise.all([
|
||||||
|
page.getByRole('button', { name: 'Update Project' }).click(),
|
||||||
|
page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url().includes('/projects/') &&
|
||||||
|
response.request().method() === 'PUT' &&
|
||||||
|
response.status() === 200
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expect(page.getByTestId('project_table')).toContainText(updatedName);
|
||||||
|
await expect(page.getByTestId('project_table')).not.toContainText(projectName);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('test that project context menu archive archives the project', async ({ page, ctx }) => {
|
||||||
|
const projectName = 'CtxArchiveProject ' + Math.floor(1 + Math.random() * 10000);
|
||||||
|
await createProjectViaApi(ctx, { name: projectName });
|
||||||
|
await goToProjectsOverview(page);
|
||||||
|
|
||||||
|
const row = page.getByRole('row').filter({ hasText: projectName }).first();
|
||||||
|
await expect(row).toBeVisible();
|
||||||
|
await row.click({ button: 'right' });
|
||||||
|
await expect(page.getByRole('menu')).toBeVisible();
|
||||||
|
await Promise.all([
|
||||||
|
page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url().includes('/projects') &&
|
||||||
|
response.request().method() === 'PUT' &&
|
||||||
|
response.status() === 200
|
||||||
|
),
|
||||||
|
page.getByRole('menuitem', { name: 'Archive' }).click(),
|
||||||
|
]);
|
||||||
|
await expect(page.getByTestId('project_table')).not.toContainText(projectName);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('test that project context menu delete deletes the project', async ({ page, ctx }) => {
|
||||||
|
const projectName = 'CtxDeleteProject ' + Math.floor(1 + Math.random() * 10000);
|
||||||
|
await createProjectViaApi(ctx, { name: projectName });
|
||||||
|
await goToProjectsOverview(page);
|
||||||
|
|
||||||
|
const row = page.getByRole('row').filter({ hasText: projectName }).first();
|
||||||
|
await expect(row).toBeVisible();
|
||||||
|
await row.click({ button: 'right' });
|
||||||
|
await expect(page.getByRole('menu')).toBeVisible();
|
||||||
|
await Promise.all([
|
||||||
|
page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url().includes('/projects') &&
|
||||||
|
response.request().method() === 'DELETE' &&
|
||||||
|
response.status() === 204
|
||||||
|
),
|
||||||
|
page.getByRole('menuitem', { name: 'Delete' }).click(),
|
||||||
|
]);
|
||||||
|
await expect(page.getByTestId('project_table')).not.toContainText(projectName);
|
||||||
|
});
|
||||||
|
|
||||||
// =============================================
|
// =============================================
|
||||||
// Employee Permission Tests
|
// Employee Permission Tests
|
||||||
// =============================================
|
// =============================================
|
||||||
|
|||||||
@@ -90,6 +90,59 @@ test('test that multiple tags can be created via API and displayed in the table'
|
|||||||
await expect(page.getByTestId('tag_table')).toContainText(tagName2);
|
await expect(page.getByTestId('tag_table')).toContainText(tagName2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// =============================================
|
||||||
|
// Context Menu Tests
|
||||||
|
// =============================================
|
||||||
|
|
||||||
|
test('test that tag context menu edit updates the tag', async ({ page, ctx }) => {
|
||||||
|
const tagName = 'CtxEditTag ' + Math.floor(1 + Math.random() * 10000);
|
||||||
|
const updatedName = 'CtxUpdatedTag ' + Math.floor(1 + Math.random() * 10000);
|
||||||
|
await createTagViaApi(ctx, { name: tagName });
|
||||||
|
await goToTagsOverview(page);
|
||||||
|
|
||||||
|
const row = page.getByRole('row').filter({ hasText: tagName }).first();
|
||||||
|
await expect(row).toBeVisible();
|
||||||
|
await row.click({ button: 'right' });
|
||||||
|
await expect(page.getByRole('menu')).toBeVisible();
|
||||||
|
await page.getByRole('menuitem', { name: 'Edit' }).click();
|
||||||
|
await expect(page.getByRole('dialog')).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByPlaceholder('Tag Name').fill(updatedName);
|
||||||
|
await Promise.all([
|
||||||
|
page.getByRole('button', { name: 'Update Tag' }).click(),
|
||||||
|
page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url().includes('/tags') &&
|
||||||
|
response.request().method() === 'PUT' &&
|
||||||
|
response.status() === 200
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expect(page.getByTestId('tag_table')).toContainText(updatedName);
|
||||||
|
await expect(page.getByTestId('tag_table')).not.toContainText(tagName);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('test that tag context menu delete deletes the tag', async ({ page, ctx }) => {
|
||||||
|
const tagName = 'CtxDeleteTag ' + Math.floor(1 + Math.random() * 10000);
|
||||||
|
await createTagViaApi(ctx, { name: tagName });
|
||||||
|
await goToTagsOverview(page);
|
||||||
|
|
||||||
|
const row = page.getByRole('row').filter({ hasText: tagName }).first();
|
||||||
|
await expect(row).toBeVisible();
|
||||||
|
await row.click({ button: 'right' });
|
||||||
|
await expect(page.getByRole('menu')).toBeVisible();
|
||||||
|
await Promise.all([
|
||||||
|
page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url().includes('/tags') &&
|
||||||
|
response.request().method() === 'DELETE' &&
|
||||||
|
response.status() === 204
|
||||||
|
),
|
||||||
|
page.getByRole('menuitem', { name: 'Delete' }).click(),
|
||||||
|
]);
|
||||||
|
await expect(page.getByTestId('tag_table')).not.toContainText(tagName);
|
||||||
|
});
|
||||||
|
|
||||||
// =============================================
|
// =============================================
|
||||||
// Sorting Tests
|
// Sorting Tests
|
||||||
// =============================================
|
// =============================================
|
||||||
|
|||||||
@@ -2,11 +2,24 @@
|
|||||||
import type { Client } from '@/packages/api/src';
|
import type { Client } from '@/packages/api/src';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { CheckCircleIcon, ArchiveBoxIcon } from '@heroicons/vue/24/outline';
|
import { CheckCircleIcon, ArchiveBoxIcon } from '@heroicons/vue/24/outline';
|
||||||
|
import {
|
||||||
|
PencilSquareIcon,
|
||||||
|
ArchiveBoxIcon as ArchiveBoxIconSolid,
|
||||||
|
TrashIcon,
|
||||||
|
} from '@heroicons/vue/20/solid';
|
||||||
import { useClientsStore } from '@/utils/useClients';
|
import { useClientsStore } from '@/utils/useClients';
|
||||||
import ClientMoreOptionsDropdown from '@/Components/Common/Client/ClientMoreOptionsDropdown.vue';
|
import ClientMoreOptionsDropdown from '@/Components/Common/Client/ClientMoreOptionsDropdown.vue';
|
||||||
import { useProjectsQuery } from '@/utils/useProjectsQuery';
|
import { useProjectsQuery } from '@/utils/useProjectsQuery';
|
||||||
import TableRow from '@/Components/TableRow.vue';
|
import TableRow from '@/Components/TableRow.vue';
|
||||||
import ClientEditModal from '@/Components/Common/Client/ClientEditModal.vue';
|
import ClientEditModal from '@/Components/Common/Client/ClientEditModal.vue';
|
||||||
|
import { canUpdateClients, canDeleteClients } from '@/utils/permissions';
|
||||||
|
import {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuContent,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuSeparator,
|
||||||
|
ContextMenuTrigger,
|
||||||
|
} from '@/packages/ui/src';
|
||||||
|
|
||||||
const { projects } = useProjectsQuery();
|
const { projects } = useProjectsQuery();
|
||||||
|
|
||||||
@@ -33,38 +46,63 @@ const showEditModal = ref(false);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TableRow>
|
<ContextMenu>
|
||||||
<ClientEditModal v-model:show="showEditModal" :client="client"></ClientEditModal>
|
<ContextMenuTrigger as-child>
|
||||||
<div
|
<TableRow>
|
||||||
class="whitespace-nowrap flex items-center space-x-5 py-4 pr-3 text-sm font-medium text-text-primary pl-4 sm:pl-6 lg:pl-8 3xl:pl-12">
|
<ClientEditModal v-model:show="showEditModal" :client="client"></ClientEditModal>
|
||||||
<span>
|
<div
|
||||||
{{ client.name }}
|
class="whitespace-nowrap flex items-center space-x-5 py-4 pr-3 text-sm font-medium text-text-primary pl-4 sm:pl-6 lg:pl-8 3xl:pl-12">
|
||||||
</span>
|
<span>
|
||||||
</div>
|
{{ client.name }}
|
||||||
<div
|
</span>
|
||||||
class="whitespace-nowrap flex items-center px-3 py-4 text-sm font-medium text-text-primary">
|
</div>
|
||||||
<span class="text-text-secondary"> {{ projectCount }} Projects </span>
|
<div
|
||||||
</div>
|
class="whitespace-nowrap flex items-center px-3 py-4 text-sm font-medium text-text-primary">
|
||||||
<div
|
<span class="text-text-secondary"> {{ projectCount }} Projects </span>
|
||||||
class="whitespace-nowrap px-3 py-4 text-sm text-text-secondary flex space-x-1.5 items-center font-medium">
|
</div>
|
||||||
<template v-if="client.is_archived">
|
<div
|
||||||
<ArchiveBoxIcon class="w-4 text-icon-default"></ArchiveBoxIcon>
|
class="whitespace-nowrap px-3 py-4 text-sm text-text-secondary flex space-x-1.5 items-center font-medium">
|
||||||
<span>Archived</span>
|
<template v-if="client.is_archived">
|
||||||
</template>
|
<ArchiveBoxIcon class="w-4 text-icon-default"></ArchiveBoxIcon>
|
||||||
<template v-else>
|
<span>Archived</span>
|
||||||
<CheckCircleIcon class="w-4 text-icon-default"></CheckCircleIcon>
|
</template>
|
||||||
<span>Active</span>
|
<template v-else>
|
||||||
</template>
|
<CheckCircleIcon class="w-4 text-icon-default"></CheckCircleIcon>
|
||||||
</div>
|
<span>Active</span>
|
||||||
<div
|
</template>
|
||||||
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">
|
</div>
|
||||||
<ClientMoreOptionsDropdown
|
<div
|
||||||
:client="client"
|
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">
|
||||||
@edit="showEditModal = true"
|
<ClientMoreOptionsDropdown
|
||||||
@archive="archiveClient"
|
:client="client"
|
||||||
@delete="deleteClient"></ClientMoreOptionsDropdown>
|
@edit="showEditModal = true"
|
||||||
</div>
|
@archive="archiveClient"
|
||||||
</TableRow>
|
@delete="deleteClient"></ClientMoreOptionsDropdown>
|
||||||
|
</div>
|
||||||
|
</TableRow>
|
||||||
|
</ContextMenuTrigger>
|
||||||
|
<ContextMenuContent class="min-w-[160px]">
|
||||||
|
<ContextMenuItem
|
||||||
|
v-if="canUpdateClients()"
|
||||||
|
class="space-x-3"
|
||||||
|
@select="showEditModal = true">
|
||||||
|
<PencilSquareIcon class="w-4 h-4 text-icon-default" />
|
||||||
|
<span>Edit</span>
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem v-if="canUpdateClients()" class="space-x-3" @select="archiveClient()">
|
||||||
|
<ArchiveBoxIconSolid class="w-4 h-4 text-icon-default" />
|
||||||
|
<span>{{ client.is_archived ? 'Unarchive' : 'Archive' }}</span>
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuSeparator v-if="canDeleteClients()" />
|
||||||
|
<ContextMenuItem
|
||||||
|
v-if="canDeleteClients()"
|
||||||
|
class="space-x-3 text-destructive"
|
||||||
|
@select="deleteClient()">
|
||||||
|
<TrashIcon class="w-4 h-4 text-icon-default" />
|
||||||
|
<span>Delete</span>
|
||||||
|
</ContextMenuItem>
|
||||||
|
</ContextMenuContent>
|
||||||
|
</ContextMenu>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -2,12 +2,24 @@
|
|||||||
import type { Member, Organization } from '@/packages/api/src';
|
import type { Member, Organization } from '@/packages/api/src';
|
||||||
import { api } from '@/packages/api/src';
|
import { api } from '@/packages/api/src';
|
||||||
import { CheckCircleIcon, UserCircleIcon } from '@heroicons/vue/24/outline';
|
import { CheckCircleIcon, UserCircleIcon } from '@heroicons/vue/24/outline';
|
||||||
|
import {
|
||||||
|
PencilSquareIcon,
|
||||||
|
TrashIcon,
|
||||||
|
ArrowDownOnSquareStackIcon,
|
||||||
|
UserCircleIcon as UserCircleIconSolid,
|
||||||
|
} from '@heroicons/vue/20/solid';
|
||||||
import MemberMoreOptionsDropdown from '@/Components/Common/Member/MemberMoreOptionsDropdown.vue';
|
import MemberMoreOptionsDropdown from '@/Components/Common/Member/MemberMoreOptionsDropdown.vue';
|
||||||
import TableRow from '@/Components/TableRow.vue';
|
import TableRow from '@/Components/TableRow.vue';
|
||||||
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
||||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||||
import { useNotificationsStore } from '@/utils/notification';
|
import { useNotificationsStore } from '@/utils/notification';
|
||||||
import { canInvitePlaceholderMembers } from '@/utils/permissions';
|
import {
|
||||||
|
canInvitePlaceholderMembers,
|
||||||
|
canUpdateMembers,
|
||||||
|
canDeleteMembers,
|
||||||
|
canMergeMembers,
|
||||||
|
canMakeMembersPlaceholders,
|
||||||
|
} from '@/utils/permissions';
|
||||||
import { computed, type ComputedRef, inject, ref } from 'vue';
|
import { computed, type ComputedRef, inject, ref } from 'vue';
|
||||||
import MemberEditModal from '@/Components/Common/Member/MemberEditModal.vue';
|
import MemberEditModal from '@/Components/Common/Member/MemberEditModal.vue';
|
||||||
import MemberMergeModal from '@/Components/Common/Member/MemberMergeModal.vue';
|
import MemberMergeModal from '@/Components/Common/Member/MemberMergeModal.vue';
|
||||||
@@ -15,6 +27,13 @@ import MemberMakePlaceholderModal from '@/Components/Common/Member/MemberMakePla
|
|||||||
import MemberDeleteModal from '@/Components/Common/Member/MemberDeleteModal.vue';
|
import MemberDeleteModal from '@/Components/Common/Member/MemberDeleteModal.vue';
|
||||||
import { capitalizeFirstLetter } from '../../../utils/format';
|
import { capitalizeFirstLetter } from '../../../utils/format';
|
||||||
import { formatCents } from '../../../packages/ui/src/utils/money';
|
import { formatCents } from '../../../packages/ui/src/utils/money';
|
||||||
|
import {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuContent,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuSeparator,
|
||||||
|
ContextMenuTrigger,
|
||||||
|
} from '@/packages/ui/src';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
member: Member;
|
member: Member;
|
||||||
@@ -55,73 +74,112 @@ const userHasValidMailAddress = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TableRow>
|
<ContextMenu>
|
||||||
<div
|
<ContextMenuTrigger as-child>
|
||||||
class="whitespace-nowrap flex items-center space-x-5 py-4 pr-3 text-sm font-medium text-text-primary pl-4 sm:pl-6 lg:pl-8 3xl:pl-12">
|
<TableRow>
|
||||||
<span>
|
<div
|
||||||
{{ member.name }}
|
class="whitespace-nowrap flex items-center space-x-5 py-4 pr-3 text-sm font-medium text-text-primary pl-4 sm:pl-6 lg:pl-8 3xl:pl-12">
|
||||||
</span>
|
<span>
|
||||||
</div>
|
{{ member.name }}
|
||||||
<div class="whitespace-nowrap px-3 py-4 text-sm text-text-secondary">
|
</span>
|
||||||
{{ member.email }}
|
</div>
|
||||||
</div>
|
<div class="whitespace-nowrap px-3 py-4 text-sm text-text-secondary">
|
||||||
<div class="whitespace-nowrap px-3 py-4 text-sm text-text-secondary">
|
{{ member.email }}
|
||||||
{{ capitalizeFirstLetter(member.role) }}
|
</div>
|
||||||
</div>
|
<div class="whitespace-nowrap px-3 py-4 text-sm text-text-secondary">
|
||||||
<div class="whitespace-nowrap px-3 py-4 text-sm text-text-secondary">
|
{{ capitalizeFirstLetter(member.role) }}
|
||||||
{{
|
</div>
|
||||||
member.billable_rate
|
<div class="whitespace-nowrap px-3 py-4 text-sm text-text-secondary">
|
||||||
? formatCents(
|
{{
|
||||||
member.billable_rate,
|
member.billable_rate
|
||||||
organization?.currency,
|
? formatCents(
|
||||||
organization?.currency_format,
|
member.billable_rate,
|
||||||
organization?.currency_symbol,
|
organization?.currency,
|
||||||
organization?.number_format
|
organization?.currency_format,
|
||||||
)
|
organization?.currency_symbol,
|
||||||
: '--'
|
organization?.number_format
|
||||||
}}
|
)
|
||||||
</div>
|
: '--'
|
||||||
<div
|
}}
|
||||||
class="whitespace-nowrap px-3 py-4 text-sm text-text-secondary flex space-x-1.5 items-center font-medium">
|
</div>
|
||||||
<template v-if="member.is_placeholder === false">
|
<div
|
||||||
<CheckCircleIcon class="w-4 text-icon-default"></CheckCircleIcon>
|
class="whitespace-nowrap px-3 py-4 text-sm text-text-secondary flex space-x-1.5 items-center font-medium">
|
||||||
<span>Active</span>
|
<template v-if="member.is_placeholder === false">
|
||||||
</template>
|
<CheckCircleIcon class="w-4 text-icon-default"></CheckCircleIcon>
|
||||||
<template v-else>
|
<span>Active</span>
|
||||||
<UserCircleIcon class="w-4 text-icon-default"></UserCircleIcon>
|
</template>
|
||||||
<span>Inactive</span>
|
<template v-else>
|
||||||
</template>
|
<UserCircleIcon class="w-4 text-icon-default"></UserCircleIcon>
|
||||||
</div>
|
<span>Inactive</span>
|
||||||
<div
|
</template>
|
||||||
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">
|
</div>
|
||||||
<SecondaryButton
|
<div
|
||||||
v-if="
|
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">
|
||||||
member.is_placeholder === true &&
|
<SecondaryButton
|
||||||
canInvitePlaceholderMembers() &&
|
v-if="
|
||||||
userHasValidMailAddress
|
member.is_placeholder === true &&
|
||||||
"
|
canInvitePlaceholderMembers() &&
|
||||||
size="small"
|
userHasValidMailAddress
|
||||||
@click="invitePlaceholder(member.id)"
|
"
|
||||||
>Invite
|
size="small"
|
||||||
</SecondaryButton>
|
@click="invitePlaceholder(member.id)"
|
||||||
<MemberMoreOptionsDropdown
|
>Invite
|
||||||
:member="member"
|
</SecondaryButton>
|
||||||
@edit="showEditMemberModal = true"
|
<MemberMoreOptionsDropdown
|
||||||
@delete="removeMember"
|
:member="member"
|
||||||
@merge="showMergeMemberModal = true"
|
@edit="showEditMemberModal = true"
|
||||||
@make-placeholder="
|
@delete="removeMember"
|
||||||
showMakeMemberPlaceholderModal = true
|
@merge="showMergeMemberModal = true"
|
||||||
"></MemberMoreOptionsDropdown>
|
@make-placeholder="
|
||||||
</div>
|
showMakeMemberPlaceholderModal = true
|
||||||
<MemberEditModal v-model:show="showEditMemberModal" :member="member"></MemberEditModal>
|
"></MemberMoreOptionsDropdown>
|
||||||
<MemberMergeModal v-model:show="showMergeMemberModal" :member="member"></MemberMergeModal>
|
</div>
|
||||||
<MemberMakePlaceholderModal
|
<MemberEditModal
|
||||||
v-model:show="showMakeMemberPlaceholderModal"
|
v-model:show="showEditMemberModal"
|
||||||
:member="member"></MemberMakePlaceholderModal>
|
:member="member"></MemberEditModal>
|
||||||
<MemberDeleteModal
|
<MemberMergeModal
|
||||||
v-model:show="showDeleteMemberModal"
|
v-model:show="showMergeMemberModal"
|
||||||
:member="member"></MemberDeleteModal>
|
:member="member"></MemberMergeModal>
|
||||||
</TableRow>
|
<MemberMakePlaceholderModal
|
||||||
|
v-model:show="showMakeMemberPlaceholderModal"
|
||||||
|
:member="member"></MemberMakePlaceholderModal>
|
||||||
|
<MemberDeleteModal
|
||||||
|
v-model:show="showDeleteMemberModal"
|
||||||
|
:member="member"></MemberDeleteModal>
|
||||||
|
</TableRow>
|
||||||
|
</ContextMenuTrigger>
|
||||||
|
<ContextMenuContent class="min-w-[160px]">
|
||||||
|
<ContextMenuItem
|
||||||
|
v-if="canUpdateMembers()"
|
||||||
|
class="space-x-3"
|
||||||
|
@select="showEditMemberModal = true">
|
||||||
|
<PencilSquareIcon class="w-4 h-4 text-icon-default" />
|
||||||
|
<span>Edit</span>
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem
|
||||||
|
v-if="member.role === 'placeholder' && canMergeMembers()"
|
||||||
|
class="space-x-3"
|
||||||
|
@select="showMergeMemberModal = true">
|
||||||
|
<ArrowDownOnSquareStackIcon class="w-4 h-4 text-icon-default" />
|
||||||
|
<span>Merge</span>
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem
|
||||||
|
v-if="member.role !== 'placeholder' && canMakeMembersPlaceholders()"
|
||||||
|
class="space-x-3"
|
||||||
|
@select="showMakeMemberPlaceholderModal = true">
|
||||||
|
<UserCircleIconSolid class="w-4 h-4 text-icon-default" />
|
||||||
|
<span>Deactivate</span>
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuSeparator v-if="canDeleteMembers()" />
|
||||||
|
<ContextMenuItem
|
||||||
|
v-if="canDeleteMembers()"
|
||||||
|
class="space-x-3 text-destructive"
|
||||||
|
@select="showDeleteMemberModal = true">
|
||||||
|
<TrashIcon class="w-4 h-4 text-icon-default" />
|
||||||
|
<span>Delete</span>
|
||||||
|
</ContextMenuItem>
|
||||||
|
</ContextMenuContent>
|
||||||
|
</ContextMenu>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ import ProjectMoreOptionsDropdown from '@/Components/Common/Project/ProjectMoreO
|
|||||||
import type { Project } from '@/packages/api/src';
|
import type { Project } from '@/packages/api/src';
|
||||||
import { computed, ref, inject, type ComputedRef } from 'vue';
|
import { computed, ref, inject, type ComputedRef } from 'vue';
|
||||||
import { CheckCircleIcon, ArchiveBoxIcon } from '@heroicons/vue/24/outline';
|
import { CheckCircleIcon, ArchiveBoxIcon } from '@heroicons/vue/24/outline';
|
||||||
|
import {
|
||||||
|
PencilSquareIcon,
|
||||||
|
ArchiveBoxIcon as ArchiveBoxIconSolid,
|
||||||
|
TrashIcon,
|
||||||
|
} from '@heroicons/vue/20/solid';
|
||||||
import { useClientsQuery } from '@/utils/useClientsQuery';
|
import { useClientsQuery } from '@/utils/useClientsQuery';
|
||||||
import { useTasksQuery } from '@/utils/useTasksQuery';
|
import { useTasksQuery } from '@/utils/useTasksQuery';
|
||||||
import { useProjectsStore } from '@/utils/useProjects';
|
import { useProjectsStore } from '@/utils/useProjects';
|
||||||
@@ -14,7 +19,15 @@ import EstimatedTimeProgress from '@/packages/ui/src/EstimatedTimeProgress.vue';
|
|||||||
import UpgradeBadge from '@/Components/Common/UpgradeBadge.vue';
|
import UpgradeBadge from '@/Components/Common/UpgradeBadge.vue';
|
||||||
import { formatHumanReadableDuration } from '../../../packages/ui/src/utils/time';
|
import { formatHumanReadableDuration } from '../../../packages/ui/src/utils/time';
|
||||||
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
||||||
|
import { canUpdateProjects, canDeleteProjects } from '@/utils/permissions';
|
||||||
import type { Organization } from '@/packages/api/src';
|
import type { Organization } from '@/packages/api/src';
|
||||||
|
import {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuContent,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuSeparator,
|
||||||
|
ContextMenuTrigger,
|
||||||
|
} from '@/packages/ui/src';
|
||||||
|
|
||||||
const { clients } = useClientsQuery();
|
const { clients } = useClientsQuery();
|
||||||
const { tasks } = useTasksQuery();
|
const { tasks } = useTasksQuery();
|
||||||
@@ -69,71 +82,99 @@ const showEditProjectModal = ref(false);
|
|||||||
<ProjectEditModal
|
<ProjectEditModal
|
||||||
v-model:show="showEditProjectModal"
|
v-model:show="showEditProjectModal"
|
||||||
:original-project="project"></ProjectEditModal>
|
:original-project="project"></ProjectEditModal>
|
||||||
<TableRow :href="route('projects.show', { project: project.id })">
|
<ContextMenu>
|
||||||
<div
|
<ContextMenuTrigger as-child>
|
||||||
class="whitespace-nowrap min-w-0 flex items-center space-x-5 3xl:pl-12 py-4 pr-3 text-sm font-medium text-text-primary pl-4 sm:pl-6 lg:pl-8 3xl:pl-12">
|
<TableRow :href="route('projects.show', { project: project.id })">
|
||||||
<div
|
<div
|
||||||
:style="{
|
class="whitespace-nowrap min-w-0 flex items-center space-x-5 3xl:pl-12 py-4 pr-3 text-sm font-medium text-text-primary pl-4 sm:pl-6 lg:pl-8 3xl:pl-12">
|
||||||
backgroundColor: project.color,
|
<div
|
||||||
boxShadow: `var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) ${project.color}30`,
|
:style="{
|
||||||
}"
|
backgroundColor: project.color,
|
||||||
class="w-3 h-3 rounded-full"></div>
|
boxShadow: `var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) ${project.color}30`,
|
||||||
<span class="overflow-ellipsis overflow-hidden">
|
}"
|
||||||
{{ project.name }}
|
class="w-3 h-3 rounded-full"></div>
|
||||||
</span>
|
<span class="overflow-ellipsis overflow-hidden">
|
||||||
<span class="text-text-secondary"> {{ projectTasksCount }} Tasks </span>
|
{{ project.name }}
|
||||||
</div>
|
</span>
|
||||||
<div class="whitespace-nowrap min-w-0 px-3 py-4 text-sm text-text-secondary">
|
<span class="text-text-secondary"> {{ projectTasksCount }} Tasks </span>
|
||||||
<div v-if="project.client_id" class="overflow-ellipsis overflow-hidden">
|
</div>
|
||||||
{{ client?.name }}
|
<div class="whitespace-nowrap min-w-0 px-3 py-4 text-sm text-text-secondary">
|
||||||
</div>
|
<div v-if="project.client_id" class="overflow-ellipsis overflow-hidden">
|
||||||
<div v-else>No client</div>
|
{{ client?.name }}
|
||||||
</div>
|
</div>
|
||||||
<div class="whitespace-nowrap px-3 py-4 text-sm text-text-secondary">
|
<div v-else>No client</div>
|
||||||
<div v-if="project.spent_time">
|
</div>
|
||||||
{{
|
<div class="whitespace-nowrap px-3 py-4 text-sm text-text-secondary">
|
||||||
formatHumanReadableDuration(
|
<div v-if="project.spent_time">
|
||||||
project.spent_time,
|
{{
|
||||||
organization?.interval_format,
|
formatHumanReadableDuration(
|
||||||
organization?.number_format
|
project.spent_time,
|
||||||
)
|
organization?.interval_format,
|
||||||
}}
|
organization?.number_format
|
||||||
</div>
|
)
|
||||||
<div v-else>--</div>
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="whitespace-nowrap px-3 flex items-center text-sm text-text-secondary">
|
<div v-else>--</div>
|
||||||
<UpgradeBadge v-if="!isAllowedToPerformPremiumAction()"></UpgradeBadge>
|
</div>
|
||||||
<EstimatedTimeProgress
|
<div class="whitespace-nowrap px-3 flex items-center text-sm text-text-secondary">
|
||||||
v-else-if="project.estimated_time"
|
<UpgradeBadge v-if="!isAllowedToPerformPremiumAction()"></UpgradeBadge>
|
||||||
:estimated="project.estimated_time"
|
<EstimatedTimeProgress
|
||||||
:current="project.spent_time"></EstimatedTimeProgress>
|
v-else-if="project.estimated_time"
|
||||||
<span v-else> -- </span>
|
:estimated="project.estimated_time"
|
||||||
</div>
|
:current="project.spent_time"></EstimatedTimeProgress>
|
||||||
<div
|
<span v-else> -- </span>
|
||||||
v-if="showBillableRate"
|
</div>
|
||||||
class="whitespace-nowrap px-3 py-4 text-sm text-text-secondary">
|
<div
|
||||||
{{ billableRateInfo }}
|
v-if="showBillableRate"
|
||||||
</div>
|
class="whitespace-nowrap px-3 py-4 text-sm text-text-secondary">
|
||||||
<div
|
{{ billableRateInfo }}
|
||||||
class="whitespace-nowrap px-3 py-4 text-sm text-text-secondary flex space-x-1.5 items-center font-medium">
|
</div>
|
||||||
<template v-if="project.is_archived">
|
<div
|
||||||
<ArchiveBoxIcon class="w-4 text-icon-default"></ArchiveBoxIcon>
|
class="whitespace-nowrap px-3 py-4 text-sm text-text-secondary flex space-x-1.5 items-center font-medium">
|
||||||
<span>Archived</span>
|
<template v-if="project.is_archived">
|
||||||
</template>
|
<ArchiveBoxIcon class="w-4 text-icon-default"></ArchiveBoxIcon>
|
||||||
<template v-else>
|
<span>Archived</span>
|
||||||
<CheckCircleIcon class="w-4 text-icon-default"></CheckCircleIcon>
|
</template>
|
||||||
<span>Active</span>
|
<template v-else>
|
||||||
</template>
|
<CheckCircleIcon class="w-4 text-icon-default"></CheckCircleIcon>
|
||||||
</div>
|
<span>Active</span>
|
||||||
<div
|
</template>
|
||||||
class="relative whitespace-nowrap flex items-center pl-3 text-right text-sm font-medium pr-4 sm:pr-6 lg:pr-8 3xl:pr-12">
|
</div>
|
||||||
<ProjectMoreOptionsDropdown
|
<div
|
||||||
:project="project"
|
class="relative whitespace-nowrap flex items-center pl-3 text-right text-sm font-medium pr-4 sm:pr-6 lg:pr-8 3xl:pr-12">
|
||||||
@edit="showEditProjectModal = true"
|
<ProjectMoreOptionsDropdown
|
||||||
@archive="archiveProject"
|
:project="project"
|
||||||
@delete="deleteProject"></ProjectMoreOptionsDropdown>
|
@edit="showEditProjectModal = true"
|
||||||
</div>
|
@archive="archiveProject"
|
||||||
</TableRow>
|
@delete="deleteProject"></ProjectMoreOptionsDropdown>
|
||||||
|
</div>
|
||||||
|
</TableRow>
|
||||||
|
</ContextMenuTrigger>
|
||||||
|
<ContextMenuContent class="min-w-[160px]">
|
||||||
|
<ContextMenuItem
|
||||||
|
v-if="canUpdateProjects()"
|
||||||
|
class="space-x-3"
|
||||||
|
@select="showEditProjectModal = true">
|
||||||
|
<PencilSquareIcon class="w-4 h-4 text-icon-default" />
|
||||||
|
<span>Edit</span>
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem
|
||||||
|
v-if="canUpdateProjects()"
|
||||||
|
class="space-x-3"
|
||||||
|
@select="archiveProject()">
|
||||||
|
<ArchiveBoxIconSolid class="w-4 h-4 text-icon-default" />
|
||||||
|
<span>{{ project.is_archived ? 'Unarchive' : 'Archive' }}</span>
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuSeparator v-if="canDeleteProjects()" />
|
||||||
|
<ContextMenuItem
|
||||||
|
v-if="canDeleteProjects()"
|
||||||
|
class="space-x-3 text-destructive"
|
||||||
|
@select="deleteProject()">
|
||||||
|
<TrashIcon class="w-4 h-4 text-icon-default" />
|
||||||
|
<span>Delete</span>
|
||||||
|
</ContextMenuItem>
|
||||||
|
</ContextMenuContent>
|
||||||
|
</ContextMenu>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -6,6 +6,14 @@ import TagEditModal from '@/Components/Common/Tag/TagEditModal.vue';
|
|||||||
import TableRow from '@/Components/TableRow.vue';
|
import TableRow from '@/Components/TableRow.vue';
|
||||||
import { canDeleteTags, canUpdateTags } from '@/utils/permissions';
|
import { canDeleteTags, canUpdateTags } from '@/utils/permissions';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
import { PencilSquareIcon, TrashIcon } from '@heroicons/vue/20/solid';
|
||||||
|
import {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuContent,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuSeparator,
|
||||||
|
ContextMenuTrigger,
|
||||||
|
} from '@/packages/ui/src';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
tag: Tag;
|
tag: Tag;
|
||||||
@@ -19,23 +27,44 @@ function deleteTag() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TableRow>
|
<ContextMenu>
|
||||||
<div
|
<ContextMenuTrigger as-child>
|
||||||
class="whitespace-nowrap flex items-center space-x-5 3xl:pl-12 py-4 pr-3 text-sm font-medium text-text-primary pl-4 sm:pl-6 lg:pl-8 3xl:pl-12">
|
<TableRow>
|
||||||
<span>
|
<div
|
||||||
{{ tag.name }}
|
class="whitespace-nowrap flex items-center space-x-5 3xl:pl-12 py-4 pr-3 text-sm font-medium text-text-primary pl-4 sm:pl-6 lg:pl-8 3xl:pl-12">
|
||||||
</span>
|
<span>
|
||||||
</div>
|
{{ tag.name }}
|
||||||
<div
|
</span>
|
||||||
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">
|
</div>
|
||||||
<TagMoreOptionsDropdown
|
<div
|
||||||
v-if="canDeleteTags() || canUpdateTags()"
|
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">
|
||||||
:tag="tag"
|
<TagMoreOptionsDropdown
|
||||||
@edit="showTagEditModal = true"
|
v-if="canDeleteTags() || canUpdateTags()"
|
||||||
@delete="deleteTag"></TagMoreOptionsDropdown>
|
:tag="tag"
|
||||||
</div>
|
@edit="showTagEditModal = true"
|
||||||
<TagEditModal v-model:show="showTagEditModal" :tag="tag"></TagEditModal>
|
@delete="deleteTag"></TagMoreOptionsDropdown>
|
||||||
</TableRow>
|
</div>
|
||||||
|
<TagEditModal v-model:show="showTagEditModal" :tag="tag"></TagEditModal>
|
||||||
|
</TableRow>
|
||||||
|
</ContextMenuTrigger>
|
||||||
|
<ContextMenuContent class="min-w-[160px]">
|
||||||
|
<ContextMenuItem
|
||||||
|
v-if="canUpdateTags()"
|
||||||
|
class="space-x-3"
|
||||||
|
@select="showTagEditModal = true">
|
||||||
|
<PencilSquareIcon class="w-4 h-4 text-icon-default" />
|
||||||
|
<span>Edit</span>
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuSeparator v-if="canDeleteTags()" />
|
||||||
|
<ContextMenuItem
|
||||||
|
v-if="canDeleteTags()"
|
||||||
|
class="space-x-3 text-destructive"
|
||||||
|
@select="deleteTag()">
|
||||||
|
<TrashIcon class="w-4 h-4 text-icon-default" />
|
||||||
|
<span>Delete</span>
|
||||||
|
</ContextMenuItem>
|
||||||
|
</ContextMenuContent>
|
||||||
|
</ContextMenu>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
Reference in New Issue
Block a user