mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-16 20:22:15 +01:00
Replace fullcalendar calendar header with custom toolbar
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
|
import type { Page } from '@playwright/test';
|
||||||
|
import { expect } 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 { expect } from '@playwright/test';
|
|
||||||
import type { Page } from '@playwright/test';
|
|
||||||
|
|
||||||
async function goToCalendar(page: Page) {
|
async function goToCalendar(page: Page) {
|
||||||
await page.goto(PLAYWRIGHT_BASE_URL + '/calendar');
|
await page.goto(PLAYWRIGHT_BASE_URL + '/calendar');
|
||||||
@@ -17,6 +17,10 @@ async function clearCalendarSettings(page: Page) {
|
|||||||
await page.evaluate(() => localStorage.removeItem('solidtime:calendar-settings'));
|
await page.evaluate(() => localStorage.removeItem('solidtime:calendar-settings'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCalendarTitle(page: Page) {
|
||||||
|
return page.getByTestId('calendar-title');
|
||||||
|
}
|
||||||
|
|
||||||
test.describe('Calendar Settings', () => {
|
test.describe('Calendar Settings', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await clearCalendarSettings(page);
|
await clearCalendarSettings(page);
|
||||||
@@ -39,7 +43,9 @@ test.describe('Calendar Settings', () => {
|
|||||||
// Change snap interval to 30 min
|
// Change snap interval to 30 min
|
||||||
await page.getByLabel('Snap Interval').click();
|
await page.getByLabel('Snap Interval').click();
|
||||||
await page.getByRole('option', { name: '30 min' }).click();
|
await page.getByRole('option', { name: '30 min' }).click();
|
||||||
await page.locator('.fc-toolbar-title').click();
|
|
||||||
|
// Close the popover by pressing Escape
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
|
||||||
// Verify localStorage was updated
|
// Verify localStorage was updated
|
||||||
const stored = await page.evaluate(() =>
|
const stored = await page.evaluate(() =>
|
||||||
@@ -54,7 +60,7 @@ test.describe('Calendar Settings', () => {
|
|||||||
await expect(page.getByLabel('Snap Interval')).toContainText('30 min');
|
await expect(page.getByLabel('Snap Interval')).toContainText('30 min');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('start time change is applied to calendar and rejects values >= end time', async ({
|
test('start time change is applied to calendar and rejects invalid values', async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
await goToCalendar(page);
|
await goToCalendar(page);
|
||||||
@@ -71,24 +77,23 @@ test.describe('Calendar Settings', () => {
|
|||||||
// Change start time to 8 AM (valid)
|
// Change start time to 8 AM (valid)
|
||||||
await page.getByLabel('Start Time').click();
|
await page.getByLabel('Start Time').click();
|
||||||
await page.getByRole('option', { name: '8:00 AM' }).click();
|
await page.getByRole('option', { name: '8:00 AM' }).click();
|
||||||
await page.locator('.fc-toolbar-title').click();
|
|
||||||
|
|
||||||
// Calendar should no longer show hours before 8 AM
|
// Try to set start time to 6 PM (invalid: equals end time) — should be rejected
|
||||||
await expect(page.locator('.fc-timegrid-slot[data-time="07:00:00"]')).toHaveCount(0);
|
|
||||||
await expect(page.locator('.fc-timegrid-slot[data-time="08:00:00"]')).not.toHaveCount(0);
|
|
||||||
|
|
||||||
// Try to set start time to 6 PM (invalid: equals end time)
|
|
||||||
await openSettingsPopover(page);
|
|
||||||
await page.getByLabel('Start Time').click();
|
await page.getByLabel('Start Time').click();
|
||||||
await page.getByRole('option', { name: '6:00 PM' }).click();
|
await page.getByRole('option', { name: '6:00 PM' }).click();
|
||||||
|
|
||||||
// Should be rejected — start time stays at 8 AM
|
// Should be rejected — start time stays at 8 AM
|
||||||
await expect(page.getByLabel('Start Time')).toContainText('8:00 AM');
|
await expect(page.getByLabel('Start Time')).toContainText('8:00 AM');
|
||||||
|
|
||||||
|
// Close the popover
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
|
||||||
|
// Calendar should no longer show hours before 8 AM
|
||||||
|
await expect(page.locator('.fc-timegrid-slot[data-time="07:00:00"]')).toHaveCount(0);
|
||||||
|
await expect(page.locator('.fc-timegrid-slot[data-time="08:00:00"]')).not.toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('end time change is applied to calendar and rejects values <= start time', async ({
|
test('end time change is applied to calendar and rejects invalid values', async ({ page }) => {
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
await goToCalendar(page);
|
await goToCalendar(page);
|
||||||
|
|
||||||
// Verify 19:00 slot exists with default end (24:00)
|
// Verify 19:00 slot exists with default end (24:00)
|
||||||
@@ -103,19 +108,20 @@ test.describe('Calendar Settings', () => {
|
|||||||
// Change end time to 6 PM (valid)
|
// Change end time to 6 PM (valid)
|
||||||
await page.getByLabel('End Time').click();
|
await page.getByLabel('End Time').click();
|
||||||
await page.getByRole('option', { name: '6:00 PM' }).click();
|
await page.getByRole('option', { name: '6:00 PM' }).click();
|
||||||
await page.locator('.fc-toolbar-title').click();
|
|
||||||
|
|
||||||
// Calendar should no longer show hours at or after 6 PM
|
// Try to set end time to 8 AM (invalid: equals start time) — should be rejected
|
||||||
await expect(page.locator('.fc-timegrid-slot[data-time="18:00:00"]')).toHaveCount(0);
|
|
||||||
await expect(page.locator('.fc-timegrid-slot[data-time="17:00:00"]')).not.toHaveCount(0);
|
|
||||||
|
|
||||||
// Try to set end time to 8 AM (invalid: equals start time)
|
|
||||||
await openSettingsPopover(page);
|
|
||||||
await page.getByLabel('End Time').click();
|
await page.getByLabel('End Time').click();
|
||||||
await page.getByRole('option', { name: '8:00 AM' }).click();
|
await page.getByRole('option', { name: '8:00 AM' }).click();
|
||||||
|
|
||||||
// Should be rejected — end time stays at 6 PM
|
// Should be rejected — end time stays at 6 PM
|
||||||
await expect(page.getByLabel('End Time')).toContainText('6:00 PM');
|
await expect(page.getByLabel('End Time')).toContainText('6:00 PM');
|
||||||
|
|
||||||
|
// Close the popover
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
|
||||||
|
// Calendar should no longer show hours at or after 6 PM
|
||||||
|
await expect(page.locator('.fc-timegrid-slot[data-time="18:00:00"]')).toHaveCount(0);
|
||||||
|
await expect(page.locator('.fc-timegrid-slot[data-time="17:00:00"]')).not.toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('grid scale affects number of calendar slots', async ({ page }) => {
|
test('grid scale affects number of calendar slots', async ({ page }) => {
|
||||||
@@ -128,19 +134,31 @@ test.describe('Calendar Settings', () => {
|
|||||||
await openSettingsPopover(page);
|
await openSettingsPopover(page);
|
||||||
await page.getByLabel('Grid Scale').click();
|
await page.getByLabel('Grid Scale').click();
|
||||||
await page.getByRole('option', { name: '30 min' }).click();
|
await page.getByRole('option', { name: '30 min' }).click();
|
||||||
await page.locator('.fc-toolbar-title').click();
|
await page.keyboard.press('Escape');
|
||||||
|
|
||||||
|
// Wait for FullCalendar to re-render with new slot count
|
||||||
|
await expect(async () => {
|
||||||
|
const count = await page.locator('.fc-timegrid-slot').count();
|
||||||
|
expect(count).toBeLessThan(defaultSlotCount);
|
||||||
|
}).toPass({ timeout: 5000 });
|
||||||
|
|
||||||
const largerSlotCount = await page.locator('.fc-timegrid-slot').count();
|
const largerSlotCount = await page.locator('.fc-timegrid-slot').count();
|
||||||
expect(largerSlotCount).toBeLessThan(defaultSlotCount);
|
|
||||||
|
|
||||||
// Change to 5 min scale (should have many more slots)
|
// Navigate away and back to get a clean calendar mount
|
||||||
|
await page.goto(PLAYWRIGHT_BASE_URL + '/time');
|
||||||
|
await goToCalendar(page);
|
||||||
|
|
||||||
|
// Change to 5 min scale (many more slots)
|
||||||
await openSettingsPopover(page);
|
await openSettingsPopover(page);
|
||||||
await page.getByLabel('Grid Scale').click();
|
await page.getByLabel('Grid Scale').click();
|
||||||
await page.getByRole('option', { name: '5 min', exact: true }).click();
|
await page.getByRole('option', { name: '5 min', exact: true }).click();
|
||||||
await page.locator('.fc-toolbar-title').click();
|
await page.keyboard.press('Escape');
|
||||||
|
|
||||||
const smallerSlotCount = await page.locator('.fc-timegrid-slot').count();
|
// Wait for FullCalendar to re-render with new slot count
|
||||||
expect(smallerSlotCount).toBeGreaterThan(defaultSlotCount);
|
await expect(async () => {
|
||||||
|
const count = await page.locator('.fc-timegrid-slot').count();
|
||||||
|
expect(count).toBeGreaterThan(largerSlotCount);
|
||||||
|
}).toPass({ timeout: 5000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('all settings persist across navigation', async ({ page }) => {
|
test('all settings persist across navigation', async ({ page }) => {
|
||||||
@@ -156,7 +174,9 @@ test.describe('Calendar Settings', () => {
|
|||||||
await page.getByRole('option', { name: '10:00 PM' }).click();
|
await page.getByRole('option', { name: '10:00 PM' }).click();
|
||||||
await page.getByLabel('Grid Scale').click();
|
await page.getByLabel('Grid Scale').click();
|
||||||
await page.getByRole('option', { name: '30 min' }).click();
|
await page.getByRole('option', { name: '30 min' }).click();
|
||||||
await page.locator('.fc-toolbar-title').click();
|
|
||||||
|
// Close the popover
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
|
||||||
// Navigate away and back
|
// Navigate away and back
|
||||||
await page.goto(PLAYWRIGHT_BASE_URL + '/time');
|
await page.goto(PLAYWRIGHT_BASE_URL + '/time');
|
||||||
@@ -170,3 +190,66 @@ test.describe('Calendar Settings', () => {
|
|||||||
await expect(page.getByLabel('Grid Scale')).toContainText('30 min');
|
await expect(page.getByLabel('Grid Scale')).toContainText('30 min');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe('Calendar Toolbar', () => {
|
||||||
|
test('prev and next buttons navigate the calendar', async ({ page }) => {
|
||||||
|
await goToCalendar(page);
|
||||||
|
|
||||||
|
const initialTitle = await getCalendarTitle(page).textContent();
|
||||||
|
|
||||||
|
// Click next
|
||||||
|
await page.getByRole('button', { name: 'Next', exact: true }).click();
|
||||||
|
await expect(page.locator('.fc')).toBeVisible();
|
||||||
|
|
||||||
|
const nextTitle = await getCalendarTitle(page).textContent();
|
||||||
|
expect(nextTitle).not.toBe(initialTitle);
|
||||||
|
|
||||||
|
// Click prev — should go back to original
|
||||||
|
await page.getByRole('button', { name: 'Previous', exact: true }).click();
|
||||||
|
await expect(page.locator('.fc')).toBeVisible();
|
||||||
|
|
||||||
|
const backTitle = await getCalendarTitle(page).textContent();
|
||||||
|
expect(backTitle).toBe(initialTitle);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('today button returns to current week', async ({ page }) => {
|
||||||
|
await goToCalendar(page);
|
||||||
|
|
||||||
|
const initialTitle = await getCalendarTitle(page).textContent();
|
||||||
|
|
||||||
|
// Navigate away
|
||||||
|
await page.getByRole('button', { name: 'Next', exact: true }).click();
|
||||||
|
await page.getByRole('button', { name: 'Next', exact: true }).click();
|
||||||
|
|
||||||
|
const awayTitle = await getCalendarTitle(page).textContent();
|
||||||
|
expect(awayTitle).not.toBe(initialTitle);
|
||||||
|
|
||||||
|
// Click today
|
||||||
|
await page.getByRole('button', { name: 'today', exact: true }).click();
|
||||||
|
await expect(page.locator('.fc')).toBeVisible();
|
||||||
|
|
||||||
|
const todayTitle = await getCalendarTitle(page).textContent();
|
||||||
|
expect(todayTitle).toBe(initialTitle);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('view switcher toggles between week and day views', async ({ page }) => {
|
||||||
|
await goToCalendar(page);
|
||||||
|
|
||||||
|
// Default should be week view — verify multiple day columns exist
|
||||||
|
await expect(page.locator('.fc-col-header-cell')).not.toHaveCount(1);
|
||||||
|
|
||||||
|
// Switch to day view
|
||||||
|
await page.getByRole('tab', { name: 'day', exact: true }).click();
|
||||||
|
await expect(page.locator('.fc')).toBeVisible();
|
||||||
|
|
||||||
|
// Day view should show exactly 1 day column
|
||||||
|
await expect(page.locator('.fc-col-header-cell')).toHaveCount(1);
|
||||||
|
|
||||||
|
// Switch back to week view
|
||||||
|
await page.getByRole('tab', { name: 'week', exact: true }).click();
|
||||||
|
await expect(page.locator('.fc')).toBeVisible();
|
||||||
|
|
||||||
|
// Week view should show multiple day columns again
|
||||||
|
await expect(page.locator('.fc-col-header-cell')).not.toHaveCount(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -222,16 +222,16 @@ test('test that calendar navigation buttons work', async ({ page }) => {
|
|||||||
await expect(page.locator('.fc')).toBeVisible();
|
await expect(page.locator('.fc')).toBeVisible();
|
||||||
|
|
||||||
// Click the "next" button to navigate forward
|
// Click the "next" button to navigate forward
|
||||||
await page.locator('button.fc-next-button').click();
|
await page.getByRole('button', { name: 'Next' }).click();
|
||||||
await expect(page.locator('.fc')).toBeVisible();
|
await expect(page.locator('.fc')).toBeVisible();
|
||||||
|
|
||||||
// Click the "prev" button to navigate back
|
// Click the "prev" button to navigate back
|
||||||
await page.locator('button.fc-prev-button').click();
|
await page.getByRole('button', { name: 'Previous' }).click();
|
||||||
await expect(page.locator('.fc')).toBeVisible();
|
await expect(page.locator('.fc')).toBeVisible();
|
||||||
|
|
||||||
// Navigate forward first so "today" button becomes enabled, then click it
|
// Navigate forward first, then click today
|
||||||
await page.locator('button.fc-next-button').click();
|
await page.getByRole('button', { name: 'Next' }).click();
|
||||||
await page.locator('button.fc-today-button').click();
|
await page.getByRole('button', { name: 'today' }).click();
|
||||||
await expect(page.locator('.fc')).toBeVisible();
|
await expect(page.locator('.fc')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const isRunningInDifferentOrganization = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-text-secondary font-medium text-xs">Current Timer</div>
|
<div class="text-text-secondary font-medium text-xs">Current Timer</div>
|
||||||
<div class="text-text-primary font-medium text-lg">
|
<div class="text-text-primary font-medium text-base">
|
||||||
{{ currentTime }}
|
{{ currentTime }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ const { tags } = useTagsQuery();
|
|||||||
:tags
|
:tags
|
||||||
:clients></TimeEntryCreateModal>
|
:clients></TimeEntryCreateModal>
|
||||||
<CardTitle title="Time Tracker" :icon="ClockIcon"></CardTitle>
|
<CardTitle title="Time Tracker" :icon="ClockIcon"></CardTitle>
|
||||||
<div class="relative pt-1">
|
<div class="relative pt-1.5">
|
||||||
<TimeTrackerRunningInDifferentOrganizationOverlay
|
<TimeTrackerRunningInDifferentOrganizationOverlay
|
||||||
v-if="isRunningInDifferentOrganization"
|
v-if="isRunningInDifferentOrganization"
|
||||||
@switch-organization="
|
@switch-organization="
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Button } from '..';
|
||||||
|
import { ChevronLeft, ChevronRight } from 'lucide-vue-next';
|
||||||
|
import { Tabs, TabsList } from '@/Components/ui/tabs';
|
||||||
|
import TabBarItem from '@/Components/Common/TabBar/TabBarItem.vue';
|
||||||
|
import CalendarSettingsPopover from './CalendarSettingsPopover.vue';
|
||||||
|
import type { CalendarSettings } from './calendarSettings';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
viewTitle: string;
|
||||||
|
activeView: string;
|
||||||
|
settings: CalendarSettings;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
prev: [];
|
||||||
|
next: [];
|
||||||
|
today: [];
|
||||||
|
'change-view': [view: string];
|
||||||
|
'update:settings': [value: CalendarSettings];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-between bg-background px-2 py-1.5">
|
||||||
|
<!-- Left: Navigation -->
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
class="h-8 w-8 p-0"
|
||||||
|
aria-label="Previous"
|
||||||
|
@click="emit('prev')">
|
||||||
|
<ChevronLeft class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
class="h-8 w-8 p-0"
|
||||||
|
aria-label="Next"
|
||||||
|
@click="emit('next')">
|
||||||
|
<ChevronRight class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" @click="emit('today')"> today </Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Center: Title -->
|
||||||
|
<span data-testid="calendar-title" class="text-base font-semibold text-foreground">{{
|
||||||
|
viewTitle
|
||||||
|
}}</span>
|
||||||
|
|
||||||
|
<!-- Right: View switcher + Settings -->
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<Tabs
|
||||||
|
:model-value="activeView"
|
||||||
|
@update:model-value="(v) => emit('change-view', String(v))">
|
||||||
|
<TabsList class="flex items-center space-x-0.5 sm:space-x-1">
|
||||||
|
<TabBarItem value="timeGridWeek">week</TabBarItem>
|
||||||
|
<TabBarItem value="timeGridDay">day</TabBarItem>
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
|
<CalendarSettingsPopover
|
||||||
|
:settings="settings"
|
||||||
|
@update:settings="(v) => emit('update:settings', v)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -29,7 +29,7 @@ import { getUserTimezone, getWeekStart } from '../utils/settings';
|
|||||||
import { LoadingSpinner, TimeEntryCreateModal, TimeEntryEditModal } from '..';
|
import { LoadingSpinner, TimeEntryCreateModal, TimeEntryEditModal } from '..';
|
||||||
import FullCalendarEventContent from './FullCalendarEventContent.vue';
|
import FullCalendarEventContent from './FullCalendarEventContent.vue';
|
||||||
import FullCalendarDayHeader from './FullCalendarDayHeader.vue';
|
import FullCalendarDayHeader from './FullCalendarDayHeader.vue';
|
||||||
import CalendarSettingsPopover from './CalendarSettingsPopover.vue';
|
import CalendarToolbar from './CalendarToolbar.vue';
|
||||||
import type { CalendarSettings } from './calendarSettings';
|
import type { CalendarSettings } from './calendarSettings';
|
||||||
import { useVisualSnap } from './useVisualSnap';
|
import { useVisualSnap } from './useVisualSnap';
|
||||||
import {
|
import {
|
||||||
@@ -123,6 +123,27 @@ function onSettingsUpdate(newSettings: CalendarSettings) {
|
|||||||
calendarSettings.value = newSettings;
|
calendarSettings.value = newSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Toolbar state — updated via datesSet callback
|
||||||
|
const viewTitle = ref('');
|
||||||
|
const activeView = ref('timeGridWeek');
|
||||||
|
|
||||||
|
function handlePrev() {
|
||||||
|
calendarRef.value?.getApi().prev();
|
||||||
|
scrollToCurrentTime();
|
||||||
|
}
|
||||||
|
function handleNext() {
|
||||||
|
calendarRef.value?.getApi().next();
|
||||||
|
scrollToCurrentTime();
|
||||||
|
}
|
||||||
|
function handleToday() {
|
||||||
|
calendarRef.value?.getApi().today();
|
||||||
|
scrollToCurrentTime();
|
||||||
|
}
|
||||||
|
function handleChangeView(view: string) {
|
||||||
|
calendarRef.value?.getApi().changeView(view);
|
||||||
|
scrollToCurrentTime();
|
||||||
|
}
|
||||||
|
|
||||||
// Reactive "now" for running time entry - updates every minute
|
// Reactive "now" for running time entry - updates every minute
|
||||||
const currentTime = ref(getDayJsInstance()());
|
const currentTime = ref(getDayJsInstance()());
|
||||||
let currentTimeInterval: ReturnType<typeof setInterval> | null = null;
|
let currentTimeInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
@@ -240,6 +261,8 @@ const dailyTotals = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function emitDatesChange(arg: DatesSetArg) {
|
function emitDatesChange(arg: DatesSetArg) {
|
||||||
|
viewTitle.value = arg.view.title;
|
||||||
|
activeView.value = arg.view.type;
|
||||||
emit('dates-change', { start: arg.start, end: arg.end });
|
emit('dates-change', { start: arg.start, end: arg.end });
|
||||||
// Render activity boxes after calendar view has been rendered
|
// Render activity boxes after calendar view has been rendered
|
||||||
renderActivityBoxes();
|
renderActivityBoxes();
|
||||||
@@ -506,11 +529,7 @@ const calendarOptions = computed(() => {
|
|||||||
return {
|
return {
|
||||||
plugins: [dayGridPlugin, timeGridPlugin, interactionPlugin, activityStatusPlugin],
|
plugins: [dayGridPlugin, timeGridPlugin, interactionPlugin, activityStatusPlugin],
|
||||||
initialView: 'timeGridWeek',
|
initialView: 'timeGridWeek',
|
||||||
headerToolbar: {
|
headerToolbar: false as const,
|
||||||
left: 'prev,next today',
|
|
||||||
center: 'title',
|
|
||||||
right: 'timeGridWeek,timeGridDay',
|
|
||||||
},
|
|
||||||
height: 'parent',
|
height: 'parent',
|
||||||
slotMinTime: formatDuration(s.startHour * 3600),
|
slotMinTime: formatDuration(s.startHour * 3600),
|
||||||
slotMaxTime: formatDuration(s.endHour * 3600),
|
slotMaxTime: formatDuration(s.endHour * 3600),
|
||||||
@@ -617,7 +636,7 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full relative h-full flex-1">
|
<div class="w-full relative h-full flex-1 flex flex-col">
|
||||||
<div v-if="loading" class="flex items-center justify-center h-full">
|
<div v-if="loading" class="flex items-center justify-center h-full">
|
||||||
<div class="flex flex-col items-center space-y-4">
|
<div class="flex flex-col items-center space-y-4">
|
||||||
<LoadingSpinner class="h-8 w-8" />
|
<LoadingSpinner class="h-8 w-8" />
|
||||||
@@ -656,13 +675,20 @@ onUnmounted(() => {
|
|||||||
:clients="clients"
|
:clients="clients"
|
||||||
:currency="currency"
|
:currency="currency"
|
||||||
:can-create-project="canCreateProject" />
|
:can-create-project="canCreateProject" />
|
||||||
<div class="calendar-settings-trigger">
|
<CalendarToolbar
|
||||||
<CalendarSettingsPopover
|
:view-title="viewTitle"
|
||||||
:settings="calendarSettings"
|
:active-view="activeView"
|
||||||
@update:settings="onSettingsUpdate" />
|
:settings="calendarSettings"
|
||||||
</div>
|
@prev="handlePrev"
|
||||||
|
@next="handleNext"
|
||||||
|
@today="handleToday"
|
||||||
|
@change-view="handleChangeView"
|
||||||
|
@update:settings="onSettingsUpdate" />
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
<ContextMenuTrigger as="div" class="h-full" @contextmenu="handleCalendarContextMenu">
|
<ContextMenuTrigger
|
||||||
|
as="div"
|
||||||
|
class="flex-1 min-h-0"
|
||||||
|
@contextmenu="handleCalendarContextMenu">
|
||||||
<FullCalendar ref="calendarRef" class="fullcalendar" :options="calendarOptions">
|
<FullCalendar ref="calendarRef" class="fullcalendar" :options="calendarOptions">
|
||||||
<template #eventContent="arg">
|
<template #eventContent="arg">
|
||||||
<FullCalendarEventContent
|
<FullCalendarEventContent
|
||||||
@@ -743,13 +769,6 @@ onUnmounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.calendar-settings-trigger {
|
|
||||||
position: absolute;
|
|
||||||
top: 0.5rem;
|
|
||||||
right: 0.5rem;
|
|
||||||
z-index: 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullcalendar {
|
.fullcalendar {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
--fc-border-color: var(--border);
|
--fc-border-color: var(--border);
|
||||||
@@ -764,49 +783,12 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
.fullcalendar :deep(.fc-timegrid-slot) {
|
.fullcalendar :deep(.fc-timegrid-slot) {
|
||||||
height: 25px;
|
height: 25px;
|
||||||
transition: height 0.2s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.fullcalendar :deep(.fc-timegrid-slot-label) {
|
.fullcalendar :deep(.fc-timegrid-slot-label) {
|
||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fullcalendar :deep(.fc-toolbar) {
|
|
||||||
background-color: var(--background);
|
|
||||||
padding: 0.5rem;
|
|
||||||
padding-right: 2.75rem;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullcalendar :deep(.fc-toolbar-title) {
|
|
||||||
color: var(--foreground);
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullcalendar :deep(.fc-button) {
|
|
||||||
background-color: var(--secondary);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
color: var(--foreground);
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 14px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullcalendar :deep(.fc-button:hover) {
|
|
||||||
background-color: var(--muted);
|
|
||||||
border-color: var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullcalendar :deep(.fc-button:focus) {
|
|
||||||
box-shadow: 0 0 0 2px var(--ring);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullcalendar :deep(.fc-button-active) {
|
|
||||||
background-color: var(--primary);
|
|
||||||
border-color: var(--primary);
|
|
||||||
color: var(--primary-foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullcalendar :deep(.fc-col-header) {
|
.fullcalendar :deep(.fc-col-header) {
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ useSelectEvents(
|
|||||||
v-model="tempDescription"
|
v-model="tempDescription"
|
||||||
placeholder="What are you working on?"
|
placeholder="What are you working on?"
|
||||||
data-testid="time_entry_description"
|
data-testid="time_entry_description"
|
||||||
class="w-full rounded-l-lg py-4 sm:py-2.5 px-3.5 border-b border-b-card-background-separator @2xl:px-4 text-lg text-text-primary bg-transparent border-none placeholder-text-secondary font-medium focus:ring-0 transition"
|
class="w-full rounded-l-lg py-4 sm:py-2.5 px-3.5 border-b border-b-card-background-separator @2xl:px-4 text-base text-text-primary bg-transparent border-none placeholder-text-secondary font-medium focus:ring-0 transition"
|
||||||
type="text"
|
type="text"
|
||||||
@keydown.enter="startTimerIfNotActive"
|
@keydown.enter="startTimerIfNotActive"
|
||||||
@keydown.esc="showDropdown = false"
|
@keydown.esc="showDropdown = false"
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ function closeAndFocusInput() {
|
|||||||
v-model="currentTime"
|
v-model="currentTime"
|
||||||
placeholder="00:00:00"
|
placeholder="00:00:00"
|
||||||
data-testid="time_entry_time"
|
data-testid="time_entry_time"
|
||||||
class="w-[110px] lg:w-[130px] h-full text-text-primary py-2.5 rounded-lg border-border-secondary border text-center px-4 text-base lg:text-lg font-semibold bg-card-background border-none placeholder-text-tertiary focus:ring-0 transition"
|
class="w-[110px] lg:w-[120px] h-full text-text-primary py-2.5 rounded-lg border-border-secondary border text-center px-4 text-base font-semibold tabular-nums bg-card-background border-none placeholder-text-tertiary focus:ring-0 transition"
|
||||||
type="text"
|
type="text"
|
||||||
@focusin="openModalOnTab"
|
@focusin="openModalOnTab"
|
||||||
@click="openModalOnClick"
|
@click="openModalOnClick"
|
||||||
|
|||||||
@@ -19,17 +19,11 @@ export const solidtimeTheme = {
|
|||||||
'2xs': ['0.625rem', { lineHeight: '0.75rem' }],
|
'2xs': ['0.625rem', { lineHeight: '0.75rem' }],
|
||||||
xs: ['0.75rem', { lineHeight: '1rem' }],
|
xs: ['0.75rem', { lineHeight: '1rem' }],
|
||||||
sm: ['0.8125rem', { lineHeight: '1.125rem' }],
|
sm: ['0.8125rem', { lineHeight: '1.125rem' }],
|
||||||
base: ['0.875rem', { lineHeight: '1.25rem' }],
|
base: ['0.9375rem', { lineHeight: '1.375rem' }],
|
||||||
lg: ['1rem', { lineHeight: '1.5rem' }],
|
lg: ['1.125rem', { lineHeight: '1.5rem' }],
|
||||||
xl: ['1.125rem', { lineHeight: '1.75rem' }],
|
xl: ['1.25rem', { lineHeight: '1.75rem' }],
|
||||||
'2xl': ['1.25rem', { lineHeight: '1.75rem' }],
|
'2xl': ['1.5rem', { lineHeight: '2rem' }],
|
||||||
'3xl': ['1.5rem', { lineHeight: '2rem' }],
|
'3xl': ['2.25rem', { lineHeight: '2.5rem' }],
|
||||||
'4xl': ['1.75rem', { lineHeight: '2.25rem' }],
|
|
||||||
'5xl': ['2rem', { lineHeight: '1' }],
|
|
||||||
'6xl': ['2.25rem', { lineHeight: '1' }],
|
|
||||||
'7xl': ['2.5rem', { lineHeight: '1' }],
|
|
||||||
'8xl': ['3rem', { lineHeight: '1' }],
|
|
||||||
'9xl': ['3.5rem', { lineHeight: '1' }],
|
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
ring: 'var(--ring)',
|
ring: 'var(--ring)',
|
||||||
|
|||||||
Reference in New Issue
Block a user