Added the ability to disable group similar time entries (#1054)

* Added the ability to disable group similar time entries

* Fix E2E test for Group similar time entries

* Simplify `TimeEntryGroupedTable` by replacing ternary with early return logic

* Refactor time entry grouping settings: rename storage key, move logic into a dedicated module

* Replace fixed `waitForTimeout` calls in E2E tests with element-based waits and assertions

* Run frontend linting and formatting for changes
This commit is contained in:
utlark
2026-04-17 19:44:59 +05:00
committed by GitHub
parent 353a579850
commit 77a5e979c6
7 changed files with 98 additions and 0 deletions

View File

@@ -230,6 +230,37 @@ test('test that theme can be changed to dark and light', async ({ page }) => {
await expect(page.getByText('System default:')).toBeVisible(); await expect(page.getByText('System default:')).toBeVisible();
}); });
// =============================================
// Group similar time entries
// =============================================
test('test that group similar time entries setting can be toggled', async ({ page }) => {
await goToProfilePage(page);
// Get the checkbox
const checkbox = page.getByLabel('Group similar time entries');
// Get initial value and verify it is checked (default is true)
const initialValue = await checkbox.isChecked();
await expect(checkbox).toBeChecked();
// Toggle the checkbox
await checkbox.click();
// Reload
await page.reload();
// Verify the value is toggled
const afterValue = await page.getByLabel('Group similar time entries').isChecked();
expect(afterValue).toBe(!initialValue);
// Verify localStorage persists the setting
const storedValue = await page.evaluate(() =>
localStorage.getItem('group-similar-time-entries')
);
expect(storedValue).toBe(String(!initialValue));
});
// ============================================= // =============================================
// Two Factor Authentication Tests // Two Factor Authentication Tests
// ============================================= // =============================================

View File

@@ -39,6 +39,10 @@ function getMonthFromTimestamp(timestamp: string): number {
return new Date(timestamp).getUTCMonth() + 1; return new Date(timestamp).getUTCMonth() + 1;
} }
async function goToProfilePage(page: Page) {
await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile');
}
async function goToTimeOverview(page: Page) { async function goToTimeOverview(page: Page) {
await page.goto(PLAYWRIGHT_BASE_URL + '/time'); await page.goto(PLAYWRIGHT_BASE_URL + '/time');
} }
@@ -67,6 +71,14 @@ async function createEmptyTimeEntry(page: Page) {
]); ]);
} }
async function setTimeEntriesGrouping(page: Page, enabled: boolean) {
await goToProfilePage(page);
const checkbox = page.getByLabel('Group similar time entries');
const isChecked = await checkbox.isChecked();
if (isChecked !== enabled) await checkbox.click();
await goToTimeOverview(page);
}
test('test that starting and stopping an empty time entry shows a new time entry in the overview', async ({ test('test that starting and stopping an empty time entry shows a new time entry in the overview', async ({
page, page,
}) => { }) => {
@@ -333,6 +345,30 @@ test.skip('test that load more works when the end of page is reached', async ({
await expect(page.locator('body')).toHaveText(/All time entries are loaded!/); await expect(page.locator('body')).toHaveText(/All time entries are loaded!/);
}); });
test('test that Group similar time entries option is affected', async ({ page }) => {
// Enable grouping
await setTimeEntriesGrouping(page, true);
// Create 2 similar time entries
await createEmptyTimeEntry(page);
await page.waitForSelector('[data-testid="time_entry_row"]', { timeout: 1000 });
await createEmptyTimeEntry(page);
// Verify similar time entries are grouped
await expect(page.getByTestId('grouped_items_count_button').first()).toBeVisible({
timeout: 1000,
});
// Disable grouping
await setTimeEntriesGrouping(page, false);
// Verify similar time entries are not grouped
await expect(page.locator('[data-testid="time_entry_row"]')).toHaveCount(2, { timeout: 1000 });
await expect(page.locator('[data-testid="grouped_items_count_button"]')).toHaveCount(0, {
timeout: 1000,
});
});
// TODO: Test that updating the time entry start / end times works while it is running // TODO: Test that updating the time entry start / end times works while it is running
// TODO: Test for project update // TODO: Test for project update

View File

@@ -2,8 +2,10 @@
import FormSection from '@/Components/FormSection.vue'; import FormSection from '@/Components/FormSection.vue';
import { Field, FieldLabel, FieldDescription } from '@/packages/ui/src/field'; import { Field, FieldLabel, FieldDescription } from '@/packages/ui/src/field';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/packages/ui/src'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/packages/ui/src';
import { Checkbox } from '@/packages/ui/src';
import { usePreferredColorScheme } from '@vueuse/core'; import { usePreferredColorScheme } from '@vueuse/core';
import { themeSetting } from '@/utils/theme'; import { themeSetting } from '@/utils/theme';
import { groupSimilarTimeEntriesSetting } from '@/utils/timeEntryGrouping';
const preferredColor = usePreferredColorScheme(); const preferredColor = usePreferredColorScheme();
</script> </script>
@@ -15,6 +17,7 @@ const preferredColor = usePreferredColorScheme();
<template #description> Choose how you want solidtime to look on your device </template> <template #description> Choose how you want solidtime to look on your device </template>
<template #form> <template #form>
<!-- Theme -->
<Field class="col-span-6 sm:col-span-4"> <Field class="col-span-6 sm:col-span-4">
<FieldLabel for="theme">Theme</FieldLabel> <FieldLabel for="theme">Theme</FieldLabel>
<Select id="theme" v-model="themeSetting"> <Select id="theme" v-model="themeSetting">
@@ -31,6 +34,14 @@ const preferredColor = usePreferredColorScheme();
System default: {{ preferredColor }} System default: {{ preferredColor }}
</FieldDescription> </FieldDescription>
</Field> </Field>
<!-- Group similar time entries -->
<Field class="col-span-6 sm:col-span-4" orientation="horizontal">
<Checkbox
id="group_similar_time_entries"
v-model:checked="groupSimilarTimeEntriesSetting" />
<FieldLabel for="group_similar_time_entries">Group similar time entries</FieldLabel>
</Field>
</template> </template>
</FormSection> </FormSection>
</template> </template>

View File

@@ -16,6 +16,7 @@ import { useElementVisibility } from '@vueuse/core';
import { ClockIcon } from '@heroicons/vue/20/solid'; import { ClockIcon } from '@heroicons/vue/20/solid';
import LoadingSpinner from '@/packages/ui/src/LoadingSpinner.vue'; import LoadingSpinner from '@/packages/ui/src/LoadingSpinner.vue';
import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry'; import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry';
import { groupSimilarTimeEntriesSetting } from '@/utils/timeEntryGrouping';
import { useTasksQuery } from '@/utils/useTasksQuery'; import { useTasksQuery } from '@/utils/useTasksQuery';
import { useProjectsQuery } from '@/utils/useProjectsQuery'; import { useProjectsQuery } from '@/utils/useProjectsQuery';
import TimeEntryGroupedTable from '@/packages/ui/src/TimeEntry/TimeEntryGroupedTable.vue'; import TimeEntryGroupedTable from '@/packages/ui/src/TimeEntry/TimeEntryGroupedTable.vue';
@@ -151,6 +152,7 @@ function deleteSelected() {
:tasks="tasks" :tasks="tasks"
:currency="getOrganizationCurrencyString()" :currency="getOrganizationCurrencyString()"
:time-entries="timeEntries" :time-entries="timeEntries"
:group-similar-time-entries="groupSimilarTimeEntriesSetting"
:tags="tags"></TimeEntryGroupedTable> :tags="tags"></TimeEntryGroupedTable>
<div v-if="isPending" class="flex justify-center items-center py-12"> <div v-if="isPending" class="flex justify-center items-center py-12">
<LoadingSpinner></LoadingSpinner> <LoadingSpinner></LoadingSpinner>

View File

@@ -6,10 +6,15 @@ const props = withDefaults(
defineProps<{ defineProps<{
expanded?: boolean; expanded?: boolean;
size?: string; size?: string;
/**
* Test ID used for Playwright/E2E tests.
*/
testId?: string;
}>(), }>(),
{ {
expanded: false, expanded: false,
size: 'w-7 h-7', size: 'w-7 h-7',
testId: 'grouped_items_count_button',
} }
); );
@@ -23,6 +28,7 @@ const expandedStatusClasses = computed(() => {
<template> <template>
<button <button
:data-testid="props.testId"
:class=" :class="
twMerge( twMerge(
'font-medium text-base rounded flex items-center transition justify-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:border-transparent', 'font-medium text-base rounded flex items-center transition justify-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:border-transparent',

View File

@@ -37,6 +37,7 @@ const props = defineProps<{
organizationBillableRate: number | null; organizationBillableRate: number | null;
enableEstimatedTime: boolean; enableEstimatedTime: boolean;
canCreateProject: boolean; canCreateProject: boolean;
groupSimilarTimeEntries: boolean;
}>(); }>();
const groupedTimeEntries = computed(() => { const groupedTimeEntries = computed(() => {
@@ -58,6 +59,11 @@ const groupedTimeEntries = computed(() => {
const newDailyEntries: TimeEntriesGroupedByType[] = []; const newDailyEntries: TimeEntriesGroupedByType[] = [];
for (const entry of dailyEntries) { for (const entry of dailyEntries) {
if (!props.groupSimilarTimeEntries) {
newDailyEntries.push({ ...entry, timeEntries: [entry] });
continue;
}
// check if same entry already exists // check if same entry already exists
const oldEntriesIndex = newDailyEntries.findIndex( const oldEntriesIndex = newDailyEntries.findIndex(
(e) => (e) =>

View File

@@ -0,0 +1,6 @@
import { useStorage } from '@vueuse/core';
export const groupSimilarTimeEntriesSetting = useStorage<boolean>(
'group-similar-time-entries',
true
);