Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions e2e/profile.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,37 @@ test('test that theme can be changed to dark and light', async ({ page }) => {
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
// =============================================
Expand Down
36 changes: 36 additions & 0 deletions e2e/time.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ function getMonthFromTimestamp(timestamp: string): number {
return new Date(timestamp).getUTCMonth() + 1;
}

async function goToProfilePage(page: Page) {
await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile');
}

async function goToTimeOverview(page: Page) {
await page.goto(PLAYWRIGHT_BASE_URL + '/time');
}
Expand Down Expand Up @@ -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 ({
page,
}) => {
Expand Down Expand Up @@ -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!/);
});

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 for project update
Expand Down
11 changes: 11 additions & 0 deletions resources/js/Pages/Profile/Partials/ThemeForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
import FormSection from '@/Components/FormSection.vue';
import { Field, FieldLabel, FieldDescription } from '@/packages/ui/src/field';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/packages/ui/src';
import { Checkbox } from '@/packages/ui/src';
import { usePreferredColorScheme } from '@vueuse/core';
import { themeSetting } from '@/utils/theme';
import { groupSimilarTimeEntriesSetting } from '@/utils/timeEntryGrouping';

const preferredColor = usePreferredColorScheme();
</script>
Expand All @@ -15,6 +17,7 @@ const preferredColor = usePreferredColorScheme();
<template #description> Choose how you want solidtime to look on your device </template>

<template #form>
<!-- Theme -->
<Field class="col-span-6 sm:col-span-4">
<FieldLabel for="theme">Theme</FieldLabel>
<Select id="theme" v-model="themeSetting">
Expand All @@ -31,6 +34,14 @@ const preferredColor = usePreferredColorScheme();
System default: {{ preferredColor }}
</FieldDescription>
</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>
</FormSection>
</template>
2 changes: 2 additions & 0 deletions resources/js/Pages/Time.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { useElementVisibility } from '@vueuse/core';
import { ClockIcon } from '@heroicons/vue/20/solid';
import LoadingSpinner from '@/packages/ui/src/LoadingSpinner.vue';
import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry';
import { groupSimilarTimeEntriesSetting } from '@/utils/timeEntryGrouping';
import { useTasksQuery } from '@/utils/useTasksQuery';
import { useProjectsQuery } from '@/utils/useProjectsQuery';
import TimeEntryGroupedTable from '@/packages/ui/src/TimeEntry/TimeEntryGroupedTable.vue';
Expand Down Expand Up @@ -151,6 +152,7 @@ function deleteSelected() {
:tasks="tasks"
:currency="getOrganizationCurrencyString()"
:time-entries="timeEntries"
:group-similar-time-entries="groupSimilarTimeEntriesSetting"
:tags="tags"></TimeEntryGroupedTable>
<div v-if="isPending" class="flex justify-center items-center py-12">
<LoadingSpinner></LoadingSpinner>
Expand Down
6 changes: 6 additions & 0 deletions resources/js/packages/ui/src/GroupedItemsCountButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@ const props = withDefaults(
defineProps<{
expanded?: boolean;
size?: string;
/**
* Test ID used for Playwright/E2E tests.
*/
testId?: string;
}>(),
{
expanded: false,
size: 'w-7 h-7',
testId: 'grouped_items_count_button',
}
);

Expand All @@ -23,6 +28,7 @@ const expandedStatusClasses = computed(() => {

<template>
<button
:data-testid="props.testId"
:class="
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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const props = defineProps<{
organizationBillableRate: number | null;
enableEstimatedTime: boolean;
canCreateProject: boolean;
groupSimilarTimeEntries: boolean;
}>();

const groupedTimeEntries = computed(() => {
Expand All @@ -58,6 +59,11 @@ const groupedTimeEntries = computed(() => {
const newDailyEntries: TimeEntriesGroupedByType[] = [];

for (const entry of dailyEntries) {
if (!props.groupSimilarTimeEntries) {
newDailyEntries.push({ ...entry, timeEntries: [entry] });
continue;
}

// check if same entry already exists
const oldEntriesIndex = newDailyEntries.findIndex(
(e) =>
Expand Down
6 changes: 6 additions & 0 deletions resources/js/utils/timeEntryGrouping.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { useStorage } from '@vueuse/core';

export const groupSimilarTimeEntriesSetting = useStorage<boolean>(
'group-similar-time-entries',
true
);