add Progress component and Reorganize UI exports

This commit is contained in:
Gregor Vostrak
2026-03-11 13:41:18 +01:00
parent 189682cfaf
commit c94ca804f8
5 changed files with 304 additions and 157 deletions
+117
View File
@@ -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 };
}
@@ -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"
+160 -156
View File
@@ -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,
};
@@ -0,0 +1,25 @@
<script setup lang="ts">
import { cn } from '../utils/cn';
import { ProgressIndicator, ProgressRoot, type ProgressRootProps } from 'reka-ui';
import { computed, type HTMLAttributes } from 'vue';
const props = withDefaults(defineProps<ProgressRootProps & { class?: HTMLAttributes['class'] }>(), {
modelValue: 0,
});
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<ProgressRoot
v-bind="delegatedProps"
:class="cn('relative h-2 w-full overflow-hidden rounded-full bg-primary/20', props.class)">
<ProgressIndicator
class="h-full w-full flex-1 bg-quaternary transition-all"
:style="`transform: translateX(-${100 - (props.modelValue ?? 0)}%)`" />
</ProgressRoot>
</template>
@@ -0,0 +1 @@
export { default as Progress } from './Progress.vue';