From c94ca804f84fef8e3ad660bd28598368b067dfe5 Mon Sep 17 00:00:00 2001 From: Gregor Vostrak Date: Wed, 11 Mar 2026 13:41:18 +0100 Subject: [PATCH] add Progress component and Reorganize UI exports --- e2e/utils/api.ts | 117 +++++++ .../TimeTracker/TimeTrackerRangeSelector.vue | 2 +- resources/js/packages/ui/src/index.ts | 316 +++++++++--------- .../js/packages/ui/src/progress/Progress.vue | 25 ++ .../js/packages/ui/src/progress/index.ts | 1 + 5 files changed, 304 insertions(+), 157 deletions(-) create mode 100644 resources/js/packages/ui/src/progress/Progress.vue create mode 100644 resources/js/packages/ui/src/progress/index.ts diff --git a/e2e/utils/api.ts b/e2e/utils/api.ts index 8566e4b2..4f21240c 100644 --- a/e2e/utils/api.ts +++ b/e2e/utils/api.ts @@ -603,7 +603,124 @@ export async function getInvitationsViaApi(ctx: TestContext) { const response = await ctx.request.get( `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}/invitations` ); + expect(response.status()).toBe(200); const body = await response.json(); return body.data as Array<{ id: string; email: string; role: string }>; } + +// ────────────────────────────────────────────────── +// Timestamp-based time entry helpers +// ────────────────────────────────────────────────── + +export async function createTimeEntryWithTimestampsViaApi( + ctx: TestContext, + data: { + description?: string; + start: string; + end: string; + projectId?: string | null; + taskId?: string | null; + tags?: string[]; + billable?: boolean; + } +) { + const response = await ctx.request.post( + `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}/time-entries`, + { + data: { + member_id: ctx.memberId, + start: data.start, + end: data.end, + description: data.description ?? '', + project_id: data.projectId ?? null, + task_id: data.taskId ?? null, + tags: data.tags ?? [], + billable: data.billable ?? false, + }, + } + ); + expect(response.status()).toBe(201); + const body = await response.json(); + return body.data as { id: string; start: string; end: string; description: string }; +} + +// ────────────────────────────────────────────────── +// User profile helpers +// ────────────────────────────────────────────────── + +export async function updateUserProfileViaWeb( + page: Page, + settings: { timezone?: string; week_start?: string } +) { + // Read user info from Inertia's data-page attribute on the root element + const userInfo = await page.evaluate(() => { + // Try Inertia's data-page attribute (stores initial page props as JSON) + const appEl = document.getElementById('app'); + if (appEl) { + const dataPage = appEl.getAttribute('data-page'); + if (dataPage) { + try { + const parsed = JSON.parse(dataPage); + const user = parsed?.props?.auth?.user; + if (user) { + return { + name: user.name, + email: user.email, + timezone: user.timezone, + week_start: user.week_start, + }; + } + } catch { + // JSON parse failed + } + } + } + return null; + }); + if (!userInfo) throw new Error('Could not read user info from Inertia data-page attribute'); + + const cookies = await page.context().cookies(); + const xsrfCookie = cookies.find((c) => c.name === 'XSRF-TOKEN'); + const xsrfToken = xsrfCookie ? decodeURIComponent(xsrfCookie.value) : ''; + + const response = await page.request.put(`${PLAYWRIGHT_BASE_URL}/user/profile-information`, { + headers: { + 'X-XSRF-TOKEN': xsrfToken, + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + data: { + name: userInfo.name, + email: userInfo.email, + timezone: settings.timezone ?? userInfo.timezone, + week_start: settings.week_start ?? userInfo.week_start, + }, + }); + expect(response.status()).toBe(200); +} + +// ────────────────────────────────────────────────── +// Running time entry with specific start +// ────────────────────────────────────────────────── + +export async function createRunningTimeEntryWithStartViaApi( + ctx: TestContext, + description: string, + start: string +) { + const response = await ctx.request.post( + `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}/time-entries`, + { + data: { + member_id: ctx.memberId, + start, + description, + billable: false, + }, + } + ); + expect(response.status()).toBe(201); + const body = await response.json(); + return body.data as { id: string; start: string; end: null; description: string }; +} diff --git a/resources/js/packages/ui/src/TimeTracker/TimeTrackerRangeSelector.vue b/resources/js/packages/ui/src/TimeTracker/TimeTrackerRangeSelector.vue index 9c202408..a706f81e 100644 --- a/resources/js/packages/ui/src/TimeTracker/TimeTrackerRangeSelector.vue +++ b/resources/js/packages/ui/src/TimeTracker/TimeTrackerRangeSelector.vue @@ -154,7 +154,7 @@ function closeAndFocusInput() { v-model="currentTime" placeholder="00:00:00" data-testid="time_entry_time" - 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" + 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 bg-card-background border-none placeholder-text-tertiary focus:ring-0 transition" type="text" @focusin="openModalOnTab" @click="openModalOnClick" diff --git a/resources/js/packages/ui/src/index.ts b/resources/js/packages/ui/src/index.ts index 94455145..9dcc9fa9 100644 --- a/resources/js/packages/ui/src/index.ts +++ b/resources/js/packages/ui/src/index.ts @@ -5,46 +5,58 @@ declare global { } } -import * as money from './utils/money'; import * as color from './utils/color'; +import * as money from './utils/money'; import * as random from './utils/random'; import * as time from './utils/time'; -export { cn } from './utils/cn'; export { buttonVariants, type ButtonVariants } from './Buttons/index'; +export type { + CommandPaletteCommand, + CommandPaletteGroup, + EntitySearchResult, +} from './CommandPalette/index'; +export type { FieldVariants } from './field/index'; +export type { CalendarSettings } from './FullCalendar/calendarSettings'; +export type { ActivityPeriod } from './FullCalendar/activityTypes'; +export { cn } from './utils/cn'; +import Badge from './Badge.vue'; +import Button from './Buttons/Button.vue'; import PrimaryButton from './Buttons/PrimaryButton.vue'; import SecondaryButton from './Buttons/SecondaryButton.vue'; -import Button from './Buttons/Button.vue'; -import TimeTrackerStartStop from './TimeTrackerStartStop.vue'; -import ProjectBadge from './Project/ProjectBadge.vue'; +import CardTitle from './CardTitle.vue'; +import Checkbox from './Input/Checkbox.vue'; +import InputLabel from './Input/InputLabel.vue'; +import TextInput from './Input/TextInput.vue'; import LoadingSpinner from './LoadingSpinner.vue'; import Modal from './Modal.vue'; -import TextInput from './Input/TextInput.vue'; -import InputLabel from './Input/InputLabel.vue'; -import TimeTrackerRunningInDifferentOrganizationOverlay from './TimeTracker/TimeTrackerRunningInDifferentOrganizationOverlay.vue'; -import TimeTrackerControls from './TimeTracker/TimeTrackerControls.vue'; -import TimeTrackerMoreOptionsDropdown from './TimeTracker/TimeTrackerMoreOptionsDropdown.vue'; -import CardTitle from './CardTitle.vue'; -import Badge from './Badge.vue'; -import Checkbox from './Input/Checkbox.vue'; -import TimeEntryGroupedTable from './TimeEntry/TimeEntryGroupedTable.vue'; -import TimeEntryMassActionRow from './TimeEntry/TimeEntryMassActionRow.vue'; +import ProjectBadge from './Project/ProjectBadge.vue'; import TimeEntryCreateModal from './TimeEntry/TimeEntryCreateModal.vue'; import TimeEntryEditModal from './TimeEntry/TimeEntryEditModal.vue'; +import TimeEntryGroupedTable from './TimeEntry/TimeEntryGroupedTable.vue'; +import TimeEntryMassActionRow from './TimeEntry/TimeEntryMassActionRow.vue'; +import TimeTrackerControls from './TimeTracker/TimeTrackerControls.vue'; +import TimeTrackerMoreOptionsDropdown from './TimeTracker/TimeTrackerMoreOptionsDropdown.vue'; +import TimeTrackerRunningInDifferentOrganizationOverlay from './TimeTracker/TimeTrackerRunningInDifferentOrganizationOverlay.vue'; +import TimeTrackerStartStop from './TimeTrackerStartStop.vue'; -import FullCalendarEventContent from './FullCalendar/FullCalendarEventContent.vue'; -import FullCalendarDayHeader from './FullCalendar/FullCalendarDayHeader.vue'; -import TimeEntryCalendar from './FullCalendar/TimeEntryCalendar.vue'; -import CalendarSettingsPopover from './FullCalendar/CalendarSettingsPopover.vue'; -import DateRangePicker from './Input/DateRangePicker.vue'; -import TimezoneMismatchModal from './TimezoneMismatchModal.vue'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './tooltip/index'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from './accordion/index'; -import { Popover, PopoverContent, PopoverTrigger, PopoverAnchor } from './popover/index'; -import { RangeCalendar } from './range-calendar/index'; +import { + Calendar, + CalendarCell, + CalendarCellTrigger, + CalendarGrid, + CalendarGridBody, + CalendarGridHead, + CalendarGridRow, + CalendarHeadCell, + CalendarHeader, + CalendarHeading, + CalendarNextButton, + CalendarPrevButton, +} from './calendar/index'; import { CommandPalette } from './CommandPalette/index'; -import { Separator } from './separator/index'; import { ContextMenu, ContextMenuCheckboxItem, @@ -61,6 +73,17 @@ import { ContextMenuSubTrigger, ContextMenuTrigger, } from './context-menu/index'; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogScrollContent, + DialogTitle, + DialogTrigger, +} from './dialog/index'; import { DropdownMenu, DropdownMenuCheckboxItem, @@ -78,52 +101,6 @@ import { DropdownMenuSubTrigger, DropdownMenuTrigger, } from './dropdown-menu/index'; -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectItemText, - SelectLabel, - SelectScrollDownButton, - SelectScrollUpButton, - SelectSeparator, - SelectTrigger, - SelectValue, -} from './select/index'; -import { - NumberField, - NumberFieldContent, - NumberFieldDecrement, - NumberFieldIncrement, - NumberFieldInput, -} from './number-field/index'; -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogScrollContent, - DialogTitle, - DialogTrigger, -} from './dialog/index'; -import { - Calendar, - CalendarCell, - CalendarCellTrigger, - CalendarGrid, - CalendarGridBody, - CalendarGridHead, - CalendarGridRow, - CalendarHeadCell, - CalendarHeader, - CalendarHeading, - CalendarNextButton, - CalendarPrevButton, -} from './calendar/index'; -import { Label } from './label/index'; import { Field, FieldContent, @@ -137,71 +114,65 @@ import { FieldTitle, fieldVariants, } from './field/index'; -export type { FieldVariants } from './field/index'; -export type { ActivityPeriod } from './FullCalendar/idleStatusPlugin'; -export type { CalendarSettings } from './FullCalendar/calendarSettings'; -export type { - CommandPaletteCommand, - CommandPaletteGroup, - EntitySearchResult, -} from './CommandPalette/index'; +import CalendarSettingsPopover from './FullCalendar/CalendarSettingsPopover.vue'; +import CalendarToolbar from './FullCalendar/CalendarToolbar.vue'; +import FullCalendarDayHeader from './FullCalendar/FullCalendarDayHeader.vue'; +import FullCalendarEventContent from './FullCalendar/FullCalendarEventContent.vue'; +import TimeEntryCalendar from './FullCalendar/TimeEntryCalendar.vue'; +import DateRangePicker from './Input/DateRangePicker.vue'; +import { Label } from './label/index'; +import { + NumberField, + NumberFieldContent, + NumberFieldDecrement, + NumberFieldIncrement, + NumberFieldInput, +} from './number-field/index'; +import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from './popover/index'; +import { Progress } from './progress/index'; +import { RangeCalendar } from './range-calendar/index'; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectItemText, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +} from './select/index'; +import { Separator } from './separator/index'; +import TimezoneMismatchModal from './TimezoneMismatchModal.vue'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './tooltip/index'; export { - money, - color, - random, - time, - Button, - PrimaryButton, - SecondaryButton, - TimeTrackerStartStop, - ProjectBadge, - LoadingSpinner, - Modal, - TextInput, - InputLabel, - TimeTrackerRunningInDifferentOrganizationOverlay, - TimeTrackerControls, - TimeTrackerMoreOptionsDropdown, - CardTitle, - Badge, - Checkbox, - TimeEntryGroupedTable, - TimeEntryMassActionRow, - TimeEntryCreateModal, - TimeEntryEditModal, - FullCalendarEventContent, - FullCalendarDayHeader, - TimeEntryCalendar, - CalendarSettingsPopover, - DateRangePicker, - TimezoneMismatchModal, - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, Accordion, AccordionContent, AccordionItem, AccordionTrigger, - Popover, - PopoverContent, - PopoverTrigger, - PopoverAnchor, - RangeCalendar, + Badge, + Button, + Calendar, + CalendarCell, + CalendarCellTrigger, + CalendarGrid, + CalendarGridBody, + CalendarGridHead, + CalendarGridRow, + CalendarHeadCell, + CalendarHeader, + CalendarHeading, + CalendarNextButton, + CalendarPrevButton, + CalendarSettingsPopover, + CalendarToolbar, + CardTitle, + Checkbox, + color, CommandPalette, - Separator, - Field, - FieldContent, - FieldDescription, - FieldError, - FieldGroup, - FieldLabel, - FieldLegend, - FieldSeparator, - FieldSet, - FieldTitle, - fieldVariants, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, @@ -216,6 +187,16 @@ export { ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, + DateRangePicker, + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogScrollContent, + DialogTitle, + DialogTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, @@ -231,6 +212,39 @@ export { DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, + Field, + FieldContent, + FieldDescription, + FieldError, + FieldGroup, + FieldLabel, + FieldLegend, + FieldSeparator, + FieldSet, + FieldTitle, + fieldVariants, + FullCalendarDayHeader, + FullCalendarEventContent, + InputLabel, + Label, + LoadingSpinner, + Modal, + money, + NumberField, + NumberFieldContent, + NumberFieldDecrement, + NumberFieldIncrement, + NumberFieldInput, + Popover, + PopoverAnchor, + PopoverContent, + PopoverTrigger, + PrimaryButton, + Progress, + ProjectBadge, + random, + RangeCalendar, + SecondaryButton, Select, SelectContent, SelectGroup, @@ -242,31 +256,21 @@ export { SelectSeparator, SelectTrigger, SelectValue, - NumberField, - NumberFieldContent, - NumberFieldDecrement, - NumberFieldIncrement, - NumberFieldInput, - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogScrollContent, - DialogTitle, - DialogTrigger, - Calendar, - CalendarCell, - CalendarCellTrigger, - CalendarGrid, - CalendarGridBody, - CalendarGridHead, - CalendarGridRow, - CalendarHeadCell, - CalendarHeader, - CalendarHeading, - CalendarNextButton, - CalendarPrevButton, - Label, + Separator, + TextInput, + time, + TimeEntryCalendar, + TimeEntryCreateModal, + TimeEntryEditModal, + TimeEntryGroupedTable, + TimeEntryMassActionRow, + TimeTrackerControls, + TimeTrackerMoreOptionsDropdown, + TimeTrackerRunningInDifferentOrganizationOverlay, + TimeTrackerStartStop, + TimezoneMismatchModal, + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, }; diff --git a/resources/js/packages/ui/src/progress/Progress.vue b/resources/js/packages/ui/src/progress/Progress.vue new file mode 100644 index 00000000..9fed4ca8 --- /dev/null +++ b/resources/js/packages/ui/src/progress/Progress.vue @@ -0,0 +1,25 @@ + + + diff --git a/resources/js/packages/ui/src/progress/index.ts b/resources/js/packages/ui/src/progress/index.ts new file mode 100644 index 00000000..63f2d58f --- /dev/null +++ b/resources/js/packages/ui/src/progress/index.ts @@ -0,0 +1 @@ +export { default as Progress } from './Progress.vue';