fix "No project" duplicating rows, unify no project senitel to null

This commit is contained in:
Gregor Vostrak
2026-05-12 17:02:26 +02:00
parent f926e8fde3
commit d292396dd4
4 changed files with 89 additions and 3 deletions

View File

@@ -123,6 +123,7 @@ const emit = defineEmits<{
:create-project="createProject"
:create-client="createClient"
:organization-billable-rate="organization?.billable_rate ?? null"
:no-project-value="null"
@changed="(p, t) => emit('add-row', p, t)">
<template #trigger>
<Button variant="ghost" size="sm" class="text-text-secondary">

View File

@@ -81,6 +81,7 @@ function hasRunningEntry(dayIndex: number): boolean {
:create-project="createProject"
:create-client="createClient"
:organization-billable-rate="organization?.billable_rate ?? null"
:no-project-value="null"
variant="ghost"
size="sm"
class="w-full" />

View File

@@ -0,0 +1,80 @@
/* eslint-disable vue/one-component-per-file */
import { mount } from '@vue/test-utils';
import { describe, expect, it, vi } from 'vitest';
import { defineComponent, h, nextTick, onMounted } from 'vue';
import TimeTrackerProjectTaskDropdown from './TimeTrackerProjectTaskDropdown.vue';
import type { Client, Project, Task } from '@/packages/api/src';
const DropdownStub = defineComponent({
props: {
modelValue: {
type: Boolean,
default: false,
},
},
emits: ['update:modelValue'],
setup(_, { emit, slots }) {
onMounted(() => emit('update:modelValue', true));
return () => h('div', [slots.trigger?.(), slots.content?.()]);
},
});
const FocusTrapStub = defineComponent({
setup(_, { slots }) {
return () => h('div', slots.default?.());
},
});
function mountDropdown(props: Record<string, unknown> = {}) {
return mount(TimeTrackerProjectTaskDropdown, {
props: {
project: null,
task: null,
projects: [] as Project[],
tasks: [] as Task[],
clients: [] as Client[],
createProject: vi.fn(),
createClient: vi.fn(),
currency: 'EUR',
enableEstimatedTime: false,
organizationBillableRate: null,
canCreateProject: false,
...props,
},
global: {
stubs: {
Dropdown: DropdownStub,
UseFocusTrap: FocusTrapStub,
},
},
});
}
async function openDropdown() {
const wrapper = mountDropdown();
await nextTick();
await nextTick();
return wrapper;
}
describe('TimeTrackerProjectTaskDropdown', () => {
it('keeps the existing empty-string no-project value by default', async () => {
const wrapper = await openDropdown();
await wrapper.find('[data-project-id=""]').trigger('click');
expect(wrapper.emitted('update:project')?.at(-1)).toEqual(['']);
expect(wrapper.emitted('changed')?.at(-1)).toEqual(['', null]);
});
it('can emit null for no-project consumers that use null as the domain value', async () => {
const wrapper = mountDropdown({ project: 'p-1', noProjectValue: null });
await nextTick();
await nextTick();
await wrapper.find('[data-project-id=""]').trigger('click');
expect(wrapper.emitted('update:project')?.at(-1)).toEqual([null]);
expect(wrapper.emitted('changed')?.at(-1)).toEqual([null, null]);
});
});

View File

@@ -16,6 +16,8 @@ import ProjectCreateModal from '@/packages/ui/src/Project/ProjectCreateModal.vue
import { twMerge } from 'tailwind-merge';
import { Button } from '@/packages/ui/src/Buttons';
const NO_PROJECT_ID = '';
const task = defineModel<string | null>('task', {
default: null,
});
@@ -57,6 +59,7 @@ const props = withDefaults(
currency: string;
emptyPlaceholder?: string;
allowReset?: boolean;
noProjectValue?: string | null;
enableEstimatedTime: boolean;
organizationBillableRate: number | null;
canCreateProject: boolean;
@@ -68,6 +71,7 @@ const props = withDefaults(
{
emptyPlaceholder: 'No Project',
allowReset: false,
noProjectValue: NO_PROJECT_ID,
variant: 'ghost',
align: 'center',
size: 'sm',
@@ -164,10 +168,10 @@ function updateFilteredResults() {
is_archived: false,
projects: [
{
id: '',
id: NO_PROJECT_ID,
name: 'No Project',
color: 'var(--theme-color-icon-default)',
value: '',
value: NO_PROJECT_ID,
client_id: null,
billable_rate: null,
is_archived: false,
@@ -490,7 +494,7 @@ function selectTask(taskId: string) {
}
function selectProject(projectId: string) {
project.value = projectId;
project.value = projectId === NO_PROJECT_ID ? props.noProjectValue : projectId;
task.value = null;
open.value = false;
searchValue.value = '';