add billable rate updates for time entries in the past to projects and project members, fixes ST-304

This commit is contained in:
Gregor Vostrak
2024-07-04 18:30:47 +02:00
parent 7fd5d25781
commit e3b4cfd881
13 changed files with 480 additions and 32 deletions

126
e2e/project-members.spec.ts Normal file
View File

@@ -0,0 +1,126 @@
import { expect, Page } from '@playwright/test';
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
import { test } from '../playwright/fixtures';
import { formatCents } from '../resources/js/utils/money';
async function goToProjectsOverview(page: Page) {
await page.goto(PLAYWRIGHT_BASE_URL + '/projects');
}
test('test that updating project member billable rate works', async ({
page,
}) => {
const newProjectName =
'New Project ' + Math.floor(1 + Math.random() * 10000);
const newBillableRate = Math.round(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: 'Add Member' }).click();
await expect(page.getByText('Add Project Member').first()).toBeVisible();
await page.keyboard.press('Enter');
await page.getByRole('button', { name: 'Add Project Member' }).click();
await page
.getByTestId('project_member_table')
.getByRole('row')
.first()
.getByRole('button')
.click();
await page.getByRole('button', { name: 'Edit' }).first().click();
await page.getByLabel('Billable Rate').fill(newBillableRate.toString());
await page.getByRole('button', { name: 'Update Project Member' }).click();
await Promise.all([
page
.getByRole('button', { name: 'No, only for new time entries' })
.click(),
page.waitForRequest(
async (request) =>
request.url().includes('/project-members/') &&
request.method() === 'PUT' &&
request.postDataJSON().billable_rate ===
newBillableRate * 100 &&
request.postDataJSON().billable_rate_update_time_entries ===
false
),
page.waitForResponse(
async (response) =>
response.url().includes('/project-members/') &&
response.request().method() === 'PUT' &&
response.status() === 200 &&
(await response.json()).data.billable_rate ===
newBillableRate * 100
),
]);
await expect(
page
.getByRole('row')
.first()
.getByText(formatCents(newBillableRate * 100))
).toBeVisible();
});
test('test that updating project member billable rate works for existing time entries', async ({
page,
}) => {
const newProjectName =
'New Project ' + Math.floor(1 + Math.random() * 10000);
const newBillableRate = Math.round(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: 'Add Member' }).click();
await expect(page.getByText('Add Project Member').first()).toBeVisible();
await page.keyboard.press('Enter');
await page.getByRole('button', { name: 'Add Project Member' }).click();
await page
.getByTestId('project_member_table')
.getByRole('row')
.first()
.getByRole('button')
.click();
await page.getByRole('button', { name: 'Edit' }).first().click();
await page.getByLabel('Billable Rate').fill(newBillableRate.toString());
await page.getByRole('button', { name: 'Update Project Member' }).click();
await Promise.all([
page.getByRole('button', { name: 'Yes, update existing time' }).click(),
page.waitForRequest(
async (request) =>
request.url().includes('/project-members/') &&
request.method() === 'PUT' &&
request.postDataJSON().billable_rate ===
newBillableRate * 100 &&
request.postDataJSON().billable_rate_update_time_entries ===
true
),
page.waitForResponse(
async (response) =>
response.url().includes('/project-members/') &&
response.request().method() === 'PUT' &&
response.status() === 200 &&
(await response.json()).data.billable_rate ===
newBillableRate * 100
),
]);
await expect(
page
.getByRole('row')
.first()
.getByText(formatCents(newBillableRate * 100))
).toBeVisible();
});

View File

@@ -1,6 +1,7 @@
import { expect, Page } from '@playwright/test'; import { expect, Page } from '@playwright/test';
import { PLAYWRIGHT_BASE_URL } from '../playwright/config'; import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
import { test } from '../playwright/fixtures'; import { test } from '../playwright/fixtures';
import { formatCents } from '../resources/js/utils/money';
async function goToProjectsOverview(page: Page) { async function goToProjectsOverview(page: Page) {
await page.goto(PLAYWRIGHT_BASE_URL + '/projects'); await page.goto(PLAYWRIGHT_BASE_URL + '/projects');
@@ -85,6 +86,108 @@ test('test that archiving and unarchiving projects works', async ({ page }) => {
]); ]);
}); });
test('test that updating billable rate works', async ({ page }) => {
const newProjectName =
'New Project ' + Math.floor(1 + Math.random() * 10000);
const newBillableRate = Math.round(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 page.getByRole('button').getByText('Edit').first().click(),
await page.getByText('Non-Billable').click();
await page.getByText('Custom Rate').click();
await page
.getByPlaceholder('Billable Rate')
.fill(newBillableRate.toString());
await page.getByRole('button', { name: 'Update Project' }).click();
await Promise.all([
page
.getByRole('button', { name: 'No, only for new time entries' })
.click(),
page.waitForRequest(
async (request) =>
request.url().includes('/projects/') &&
request.method() === 'PUT' &&
request.postDataJSON().billable_rate ===
newBillableRate * 100 &&
request.postDataJSON().billable_rate_update_time_entries ===
false
),
page.waitForResponse(
async (response) =>
response.url().includes('/projects/') &&
response.request().method() === 'PUT' &&
response.status() === 200 &&
(await response.json()).data.billable_rate ===
newBillableRate * 100
),
]);
await expect(
page
.getByRole('row')
.first()
.getByText(formatCents(newBillableRate * 100))
).toBeVisible();
});
test('test that updating billable rate works with existing time entries', async ({
page,
}) => {
const newProjectName =
'New Project ' + Math.floor(1 + Math.random() * 10000);
const newBillableRate = Math.round(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 page.getByRole('button').getByText('Edit').first().click(),
await page.getByText('Non-Billable').click();
await page.getByText('Custom Rate').click();
await page
.getByPlaceholder('Billable Rate')
.fill(newBillableRate.toString());
await page.getByRole('button', { name: 'Update Project' }).click();
await Promise.all([
page
.getByRole('button', { name: 'Yes, update existing time entries' })
.click(),
page.waitForRequest(
async (request) =>
request.url().includes('/projects/') &&
request.method() === 'PUT' &&
request.postDataJSON().billable_rate ===
newBillableRate * 100 &&
request.postDataJSON().billable_rate_update_time_entries ===
true
),
page.waitForResponse(
async (response) =>
response.url().includes('/projects/') &&
response.request().method() === 'PUT' &&
response.status() === 200 &&
(await response.json()).data.billable_rate ===
newBillableRate * 100
),
]);
await expect(
page
.getByRole('row')
.first()
.getByText(formatCents(newBillableRate * 100))
).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

View File

@@ -56,6 +56,7 @@ function updateRate(value: string) {
} }
inputValue.value = formatValue(model.value); inputValue.value = formatValue(model.value);
} }
function formatValue(modelValue: number | null) { function formatValue(modelValue: number | null) {
const formattedValue = formatCents(modelValue ?? 0); const formattedValue = formatCents(modelValue ?? 0);
return formattedValue.replace(getOrganizationCurrencySymbol(), '').trim(); return formattedValue.replace(getOrganizationCurrencySymbol(), '').trim();
@@ -81,14 +82,12 @@ const inputValue = ref(formatValue(model.value));
placeholder="Billable Rate" placeholder="Billable Rate"
class="mt-2 block w-full" class="mt-2 block w-full"
autocomplete="teamMemberRate" /> autocomplete="teamMemberRate" />
<span> <div
<div class="absolute top-0 right-0 h-full flex items-center px-4 font-medium pointer-events-none">
class="absolute top-0 right-0 h-full flex items-center px-4 font-medium"> <span>
<span> {{ getOrganizationCurrencyString() }}
{{ getOrganizationCurrencyString() }} </span>
</span> </div>
</div>
</span>
</div> </div>
</template> </template>

View File

@@ -3,6 +3,7 @@ import SecondaryButton from '@/Components/SecondaryButton.vue';
import DialogModal from '@/Components/DialogModal.vue'; import DialogModal from '@/Components/DialogModal.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue'; import PrimaryButton from '@/Components/PrimaryButton.vue';
import { formatCents } from '../../../utils/money'; import { formatCents } from '../../../utils/money';
import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
const show = defineModel('show', { default: false }); const show = defineModel('show', { default: false });
const saving = defineModel('saving', { default: false }); const saving = defineModel('saving', { default: false });
@@ -36,7 +37,8 @@ const emit = defineEmits<{
>. >.
</p> </p>
<p class="py-0.5 text-center font-semibold"> <p class="py-0.5 text-center font-semibold">
Do you want to update all existing time entries as well? Do you want to update all existing time entries, where
the organization billable rate applies as well?
</p> </p>
<div class="space-x-3 pt-5 pb-2 flex justify-center"> <div class="space-x-3 pt-5 pb-2 flex justify-center">
<PrimaryButton <PrimaryButton
@@ -52,6 +54,17 @@ const emit = defineEmits<{
No, only for new time entries No, only for new time entries
</PrimaryButton> </PrimaryButton>
</div> </div>
<p class="text-center pt-3 pb-1">
Learn more about the
<a
target="_blank"
href="https://docs.solidtime.io/user-guide/billable-rates"
class="text-blue-400 hover:text-blue-500 transition"
>billable rate logic
<ArrowTopRightOnSquareIcon
class="w-4 -mt-0.5 inline-block"></ArrowTopRightOnSquareIcon
></a>
</p>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -0,0 +1,80 @@
<script setup lang="ts">
import SecondaryButton from '@/Components/SecondaryButton.vue';
import DialogModal from '@/Components/DialogModal.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import { formatCents } from '../../../utils/money';
import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
const show = defineModel('show', { default: false });
const saving = defineModel('saving', { default: false });
defineProps<{
newBillableRate?: number | null;
projectName: string;
}>();
const emit = defineEmits<{
submit: [billable_rate_update_time_entries: boolean];
}>();
</script>
<template>
<DialogModal closeable :show="show" @close="show = false">
<template #title>
<div class="flex justify-center">
<span> Update Project Billable Rate </span>
</div>
</template>
<template #content>
<div class="flex items-center space-x-4">
<div class="col-span-6 sm:col-span-4 flex-1">
<p class="py-1 text-center">
The billable rate of {{ projectName }} will be updated
to
<strong>{{
newBillableRate
? formatCents(newBillableRate)
: ' the default rate of the organization'
}}</strong
>.
</p>
<p class="py-1 text-center font-semibold max-w-md mx-auto">
Do you want to update all existing time entries, where
the project billable rate applies as well?
</p>
<div class="space-x-3 pt-5 pb-2 flex justify-center">
<PrimaryButton
:class="{ 'opacity-25': saving }"
:disabled="saving"
@click="emit('submit', true)">
Yes, update existing time entries
</PrimaryButton>
<PrimaryButton
:class="{ 'opacity-25': saving }"
:disabled="saving"
@click="emit('submit', false)">
No, only for new time entries
</PrimaryButton>
</div>
<p class="text-center pt-3 pb-1">
Learn more about the
<a
target="_blank"
href="https://docs.solidtime.io/user-guide/billable-rates"
class="text-blue-400 hover:text-blue-500 transition"
>billable rate logic
<ArrowTopRightOnSquareIcon
class="w-4 -mt-0.5 inline-block"></ArrowTopRightOnSquareIcon
></a>
</p>
</div>
</div>
</template>
<template #footer>
<SecondaryButton @click="show = false"> Cancel </SecondaryButton>
</template>
</DialogModal>
</template>
<style scoped></style>

View File

@@ -15,7 +15,7 @@ const model = defineModel<string>({ default: '' });
:style="{ :style="{
backgroundColor: model, backgroundColor: model,
}" }"
class="w-5 h-5 rounded-full cursor-pointer"></div> class="w-6 h-6 rounded-full cursor-pointer"></div>
</button> </button>
</template> </template>
<template #content> <template #content>

View File

@@ -15,12 +15,13 @@ import ProjectColorSelector from '@/Components/Common/Project/ProjectColorSelect
import ProjectEditBillableSection from '@/Components/Common/Project/ProjectEditBillableSection.vue'; import ProjectEditBillableSection from '@/Components/Common/Project/ProjectEditBillableSection.vue';
import { UserCircleIcon } from '@heroicons/vue/20/solid'; import { UserCircleIcon } from '@heroicons/vue/20/solid';
import InputLabel from '@/Components/InputLabel.vue'; import InputLabel from '@/Components/InputLabel.vue';
import ProjectBillableRateModal from '@/Components/Common/Project/ProjectBillableRateModal.vue';
const { updateProject } = useProjectsStore(); const { updateProject } = useProjectsStore();
const { clients } = storeToRefs(useClientsStore()); const { clients } = storeToRefs(useClientsStore());
const show = defineModel('show', { default: false }); const show = defineModel('show', { default: false });
const saving = ref(false); const saving = ref(false);
const showBillableRateModal = ref(false);
const props = defineProps<{ const props = defineProps<{
originalProject: Project; originalProject: Project;
}>(); }>();
@@ -34,6 +35,10 @@ const project = ref<CreateProjectBody>({
}); });
async function submit() { async function submit() {
if (props.originalProject.billable_rate !== project.value.billable_rate) {
showBillableRateModal.value = true;
return;
}
await updateProject(props.originalProject.id, project.value); await updateProject(props.originalProject.id, project.value);
show.value = false; show.value = false;
} }
@@ -50,6 +55,14 @@ const currentClientName = computed(() => {
} }
return 'No Client'; return 'No Client';
}); });
async function submitBillableRate(billableRateUpdateTimeEntries: boolean) {
project.value.billable_rate_update_time_entries =
billableRateUpdateTimeEntries;
await updateProject(props.originalProject.id, project.value);
show.value = false;
showBillableRateModal.value = false;
}
</script> </script>
<template> <template>
@@ -62,11 +75,12 @@ const currentClientName = computed(() => {
<template #content> <template #content>
<div <div
class="sm:flex items-center space-y-2 sm:space-y-0 sm:space-x-4"> class="sm:flex items-center space-y-2 sm:space-y-0 sm:space-x-5">
<div class="flex-1 flex items-center"> <div class="flex-1 flex items-center">
<div class="text-center pr-5"> <div class="text-center">
<InputLabel for="color" value="Color" /> <InputLabel for="color" value="Color" />
<ProjectColorSelector <ProjectColorSelector
class="mt-1"
v-model="project.color"></ProjectColorSelector> v-model="project.color"></ProjectColorSelector>
</div> </div>
</div> </div>
@@ -85,7 +99,7 @@ const currentClientName = computed(() => {
</div> </div>
<div class=""> <div class="">
<InputLabel for="client" value="Client" /> <InputLabel for="client" value="Client" />
<ClientDropdown class="mt-2" v-model="project.client_id"> <ClientDropdown class="mt-1" v-model="project.client_id">
<template #trigger> <template #trigger>
<Badge <Badge
class="bg-input-background cursor-pointer hover:bg-tertiary" class="bg-input-background cursor-pointer hover:bg-tertiary"
@@ -93,7 +107,7 @@ const currentClientName = computed(() => {
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<UserCircleIcon <UserCircleIcon
class="w-5 text-icon-default"></UserCircleIcon> class="w-5 text-icon-default"></UserCircleIcon>
<span> <span class="whitespace-nowrap">
{{ currentClientName }} {{ currentClientName }}
</span> </span>
</div> </div>
@@ -121,6 +135,11 @@ const currentClientName = computed(() => {
</PrimaryButton> </PrimaryButton>
</template> </template>
</DialogModal> </DialogModal>
<ProjectBillableRateModal
v-model:show="showBillableRateModal"
@submit="submitBillableRate"
:new-billable-rate="project.billable_rate"
:project-name="project.name"></ProjectBillableRateModal>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@@ -0,0 +1,79 @@
<script setup lang="ts">
import SecondaryButton from '@/Components/SecondaryButton.vue';
import DialogModal from '@/Components/DialogModal.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import { formatCents } from '../../../utils/money';
import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
const show = defineModel('show', { default: false });
const saving = defineModel('saving', { default: false });
defineProps<{
newBillableRate?: number | null;
memberName: string;
}>();
const emit = defineEmits<{
submit: [billable_rate_update_time_entries: boolean];
}>();
</script>
<template>
<DialogModal closeable :show="show" @close="show = false">
<template #title>
<div class="flex justify-center">
<span> Update Project Member Billable Rate </span>
</div>
</template>
<template #content>
<div class="flex items-center space-x-4">
<div class="col-span-6 sm:col-span-4 flex-1">
<p class="py-1 text-center">
The billable rate of {{ memberName }} will be updated to
<strong>{{
newBillableRate
? formatCents(newBillableRate)
: ' the default rate of the organization'
}}</strong
>.
</p>
<p class="py-1 text-center font-semibold max-w-md mx-auto">
Do you want to update all existing time entries, where
the project member billable rate applies as well?
</p>
<div class="space-x-3 pt-5 pb-2 flex justify-center">
<PrimaryButton
:class="{ 'opacity-25': saving }"
:disabled="saving"
@click="emit('submit', true)">
Yes, update existing time entries
</PrimaryButton>
<PrimaryButton
:class="{ 'opacity-25': saving }"
:disabled="saving"
@click="emit('submit', false)">
No, only for new time entries
</PrimaryButton>
</div>
<p class="text-center pt-3 pb-1">
Learn more about the
<a
target="_blank"
href="https://docs.solidtime.io/user-guide/billable-rates"
class="text-blue-400 hover:text-blue-500 transition"
>billable rate logic
<ArrowTopRightOnSquareIcon
class="w-4 -mt-0.5 inline-block"></ArrowTopRightOnSquareIcon
></a>
</p>
</div>
</div>
</template>
<template #footer>
<SecondaryButton @click="show = false"> Cancel </SecondaryButton>
</template>
</DialogModal>
</template>
<style scoped></style>

View File

@@ -8,6 +8,8 @@ import { useFocus } from '@vueuse/core';
import { useProjectMembersStore } from '@/utils/useProjectMembers'; import { useProjectMembersStore } from '@/utils/useProjectMembers';
import BillableRateInput from '@/Components/Common/BillableRateInput.vue'; import BillableRateInput from '@/Components/Common/BillableRateInput.vue';
import { UserIcon } from '@heroicons/vue/24/solid'; import { UserIcon } from '@heroicons/vue/24/solid';
import ProjectMemberBillableRateModal from '@/Components/Common/ProjectMember/ProjectMemberBillableRateModal.vue';
import InputLabel from '@/Components/InputLabel.vue';
const { updateProjectMember } = useProjectMembersStore(); const { updateProjectMember } = useProjectMembersStore();
const show = defineModel('show', { default: false }); const show = defineModel('show', { default: false });
@@ -21,8 +23,15 @@ const props = defineProps<{
const projectMemberBody = ref<UpdateProjectMemberBody>({ const projectMemberBody = ref<UpdateProjectMemberBody>({
billable_rate: props.projectMember.billable_rate, billable_rate: props.projectMember.billable_rate,
}); });
const showBillableRateModal = ref(false);
async function submit() { async function submit() {
if (
props.projectMember.billable_rate !==
projectMemberBody.value.billable_rate
) {
showBillableRateModal.value = true;
return;
}
await updateProjectMember(props.projectMember.id, projectMemberBody.value); await updateProjectMember(props.projectMember.id, projectMemberBody.value);
show.value = false; show.value = false;
projectMemberBody.value = { projectMemberBody.value = {
@@ -30,6 +39,14 @@ async function submit() {
}; };
} }
async function submitBillableRate(billableRateUpdateTimeEntries: boolean) {
projectMemberBody.value.billable_rate_update_time_entries =
billableRateUpdateTimeEntries;
await updateProjectMember(props.projectMember.id, projectMemberBody.value);
show.value = false;
showBillableRateModal.value = false;
}
watch( watch(
() => show.value, () => show.value,
(value) => { (value) => {
@@ -55,6 +72,11 @@ useFocus(projectNameInput, { initialValue: true });
</template> </template>
<template #content> <template #content>
<ProjectMemberBillableRateModal
member-name="props.name"
v-model:show="showBillableRateModal"
@close="showBillableRateModal = false"
@submit="submitBillableRate"></ProjectMemberBillableRateModal>
<div class="grid grid-cols-3 items-center space-x-4"> <div class="grid grid-cols-3 items-center space-x-4">
<div <div
class="col-span-3 sm:col-span-2 space-x-2 flex items-center"> class="col-span-3 sm:col-span-2 space-x-2 flex items-center">
@@ -62,6 +84,9 @@ useFocus(projectNameInput, { initialValue: true });
<span>{{ props.name }}</span> <span>{{ props.name }}</span>
</div> </div>
<div class="col-span-3 sm:col-span-1 flex-1"> <div class="col-span-3 sm:col-span-1 flex-1">
<InputLabel
for="billable_rate"
value="Billable Rate"></InputLabel>
<BillableRateInput <BillableRateInput
@keydown.enter="submit" @keydown.enter="submit"
name="billable_rate" name="billable_rate"

View File

@@ -5,6 +5,7 @@ import type { ProjectMember } from '@/utils/api';
import { useMembersStore } from '@/utils/useMembers'; import { useMembersStore } from '@/utils/useMembers';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { computed } from 'vue'; import { computed } from 'vue';
const emit = defineEmits<{ const emit = defineEmits<{
delete: []; delete: [];
edit: []; edit: [];
@@ -25,22 +26,24 @@ const currentMember = computed(() => {
<template> <template>
<Dropdown> <Dropdown>
<template #trigger> <template #trigger>
<svg <button
data-testid="project_actions" class="focus-visible:outline-none focus-visible:bg-card-background rounded-full focus-visible:ring-1 focus-visible:ring-input-border-active focus-visible:opacity-100 hover:bg-card-background group-hover:opacity-100 opacity-20 transition-opacity"
:aria-label=" :aria-label="
'Actions for Project Member ' + currentMember?.name 'Actions for Project Member ' + currentMember?.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>
<button <button

View File

@@ -24,7 +24,7 @@ const createProjectMember = ref(false);
<div class="flow-root"> <div class="flow-root">
<div class="inline-block min-w-full align-middle"> <div class="inline-block min-w-full align-middle">
<div <div
data-testid="project_table" data-testid="project_member_table"
class="grid min-w-full" class="grid min-w-full"
style="grid-template-columns: 1fr 150px 150px 80px"> style="grid-template-columns: 1fr 150px 150px 80px">
<ProjectMemberTableHeading></ProjectMemberTableHeading> <ProjectMemberTableHeading></ProjectMemberTableHeading>

View File

@@ -30,7 +30,7 @@ const close = () => {
:closeable="closeable" :closeable="closeable"
@close="close"> @close="close">
<div class="px-6 py-4"> <div class="px-6 py-4">
<div class="text-lg font-medium text-white"> <div class="text-lg font-medium text-white" role="heading">
<slot name="title" /> <slot name="title" />
</div> </div>

View File

@@ -91,6 +91,7 @@ const maxWidthClass = computed(() => {
leave-to-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"> leave-to-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
<div <div
v-show="show" v-show="show"
role="dialog"
class="mb-6 bg-default-background border border-card-border rounded-lg shadow-xl transform transition-all sm:w-full sm:mx-auto" class="mb-6 bg-default-background border border-card-border rounded-lg shadow-xl transform transition-all sm:w-full sm:mx-auto"
:class="maxWidthClass"> :class="maxWidthClass">
<slot v-if="show" /> <slot v-if="show" />