mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-08 00:02:15 +01:00
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:
@@ -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
|
||||||
// =============================================
|
// =============================================
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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) =>
|
||||||
|
|||||||
6
resources/js/utils/timeEntryGrouping.ts
Normal file
6
resources/js/utils/timeEntryGrouping.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { useStorage } from '@vueuse/core';
|
||||||
|
|
||||||
|
export const groupSimilarTimeEntriesSetting = useStorage<boolean>(
|
||||||
|
'group-similar-time-entries',
|
||||||
|
true
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user