Add Field component system and migrate UI

This commit is contained in:
Gregor Vostrak
2026-02-10 12:22:53 +01:00
parent 1ecb332458
commit fd012e7c69
71 changed files with 1023 additions and 741 deletions

View File

@@ -192,9 +192,7 @@ test('shows error for invalid login credentials', async ({ page }) => {
await page.getByLabel('Password').fill('wrongpassword123');
await page.getByRole('button', { name: 'Log in' }).click();
await expect(
page.getByText('These credentials do not match our records.')
).toBeVisible();
await expect(page.getByText('These credentials do not match our records.')).toBeVisible();
});
test('shows error when registering with existing email', async ({ page }) => {

View File

@@ -44,6 +44,28 @@ test('test that user can create an API key', async ({ page }) => {
await createNewApiToken(page);
});
test('test that creating an API key with empty name shows validation error', async ({ page }) => {
await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile');
// Wait for the API Key Name input to be visible before interacting
const nameInput = page.getByLabel('API Key Name');
await expect(nameInput).toBeVisible();
// Ensure the API Key Name input is empty
await nameInput.fill('');
// Click the create button and wait for the 422 response
const [response] = await Promise.all([
page.waitForResponse('**/users/me/api-tokens'),
page.getByRole('button', { name: 'Create API Key' }).click(),
]);
expect(response.status()).toBe(422);
// Verify that an error notification is shown with validation message about the name field
await expect(page.getByText('name field is required')).toBeVisible({ timeout: 5000 });
});
test('test that user can delete an API key', async ({ page }) => {
await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile');
await createNewApiToken(page);

View File

@@ -158,12 +158,9 @@ test('test that deselecting a project removes the filter', async ({ page, ctx })
page.getByRole('button', { name: 'Projects' }).first().getByText('1')
).toBeVisible();
// Deselect project
// Deselect project (no network request expected — TanStack Query serves cached unfiltered data)
await page.getByRole('button', { name: 'Projects' }).first().click();
await Promise.all([
page.getByRole('option').filter({ hasText: project1Name }).click(),
waitForReportingUpdate(page),
]);
await page.getByRole('option').filter({ hasText: project1Name }).click();
await page.keyboard.press('Escape');
// Verify badge count is gone (no count displayed when 0)
@@ -281,12 +278,9 @@ test('test that deselecting a client removes the filter', async ({ page, ctx })
page.getByRole('button', { name: 'Clients' }).first().getByText('1')
).toBeVisible();
// Deselect client
// Deselect client (no network request expected — TanStack Query serves cached unfiltered data)
await page.getByRole('button', { name: 'Clients' }).first().click();
await Promise.all([
page.getByRole('option').filter({ hasText: client1Name }).click(),
waitForReportingUpdate(page),
]);
await page.getByRole('option').filter({ hasText: client1Name }).click();
await page.keyboard.press('Escape');
await expect(
@@ -445,12 +439,9 @@ test('test that deselecting a member removes the filter', async ({ page, ctx })
page.getByRole('button', { name: 'Members' }).first().getByText('1')
).toBeVisible();
// Deselect member
// Deselect member (no network request expected — TanStack Query serves cached unfiltered data)
await page.getByRole('button', { name: 'Members' }).first().click();
await Promise.all([
page.getByRole('option').filter({ hasText: 'John Doe' }).click(),
waitForReportingUpdate(page),
]);
await page.getByRole('option').filter({ hasText: 'John Doe' }).click();
await page.keyboard.press('Escape');
// Verify badge count is gone
@@ -544,12 +535,9 @@ test('test that deselecting a tag removes the filter', async ({ page, ctx }) =>
await expect(page.getByRole('button', { name: 'Tags' }).getByText('1')).toBeVisible();
// Deselect tag
// Deselect tag (no network request expected — TanStack Query serves cached unfiltered data)
await page.getByRole('button', { name: 'Tags' }).click();
await Promise.all([
page.getByRole('option').filter({ hasText: tag1Name }).click(),
waitForReportingUpdate(page),
]);
await page.getByRole('option').filter({ hasText: tag1Name }).click();
await page.keyboard.press('Escape');
await expect(page.getByRole('button', { name: 'Tags' }).getByText(/^\d+$/)).not.toBeVisible();

View File

@@ -417,10 +417,9 @@ export async function updateOrganizationCurrencyViaWeb(
currency: string,
name: string = 'Test Organization'
) {
const response = await ctx.request.put(
`${PLAYWRIGHT_BASE_URL}/teams/${ctx.orgId}`,
{ data: { name, currency } }
);
const response = await ctx.request.put(`${PLAYWRIGHT_BASE_URL}/teams/${ctx.orgId}`, {
data: { name, currency },
});
expect(response.status()).toBe(200);
}

View File

@@ -78,4 +78,4 @@ export async function getPasswordResetUrl(
expect(resetUrlMatch).toBeTruthy();
return resetUrlMatch![1].replace(/&/g, '&');
}
}

View File

@@ -7,7 +7,7 @@ import type { CreateClientBody } from '@/packages/api/src';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import { useFocus } from '@vueuse/core';
import { useClientsStore } from '@/utils/useClients';
import InputLabel from '../../../packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel } from '@/packages/ui/src/field';
const { createClient } = useClientsStore();
const show = defineModel('show', { default: false });
@@ -37,19 +37,19 @@ useFocus(clientNameInput, { initialValue: true });
<template #content>
<div class="flex items-center space-x-4">
<div class="col-span-6 sm:col-span-4 flex-1">
<InputLabel for="clientName" value="Client Name" />
<Field class="col-span-6 sm:col-span-4 flex-1">
<FieldLabel for="clientName">Client Name</FieldLabel>
<TextInput
id="clientName"
ref="clientNameInput"
v-model="client.name"
type="text"
placeholder="Client Name"
class="mt-1 block w-full"
class="block w-full"
required
autocomplete="clientName"
@keydown.enter="submit" />
</div>
</Field>
</div>
</template>
<template #footer>

View File

@@ -14,7 +14,7 @@ import {
ComboboxViewport,
} from 'radix-vue';
import Dropdown from '@/packages/ui/src/Input/Dropdown.vue';
import { Button } from '@/Components/ui/button';
import { Button } from '@/packages/ui/src/Buttons';
const { members } = useMembersQuery();
@@ -77,7 +77,6 @@ function selectMember(member: Member) {
:disabled="disabled"
type="button"
variant="input"
size="input"
class="w-full justify-between text-start font-normal">
<div class="flex items-center gap-3 truncate">
<UserIcon class="w-4 text-text-secondary shrink-0" />
@@ -92,7 +91,7 @@ function selectMember(member: Member) {
<template #content>
<ComboboxRoot
v-model:search-term="searchValue"
:open="open"
v-model:open="open"
class="relative"
:filter-function="(val: string[]) => val">
<ComboboxAnchor>

View File

@@ -9,8 +9,7 @@ import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
import Checkbox from '@/packages/ui/src/Input/Checkbox.vue';
import { useNotificationsStore } from '@/utils/notification';
import { getCurrentOrganizationId } from '@/utils/useUser';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import InputError from '@/packages/ui/src/Input/InputError.vue';
import { Field, FieldLabel, FieldError } from '@/packages/ui/src/field';
const props = defineProps<{
show: boolean;
@@ -113,25 +112,21 @@ const close = () => {
},
}">
<template #default="{ field }">
<div class="flex flex-col">
<div class="flex items-center space-x-3 text-sm">
<Checkbox
:id="field.name"
:name="field.name"
:checked="field.state.value"
@update:checked="field.handleChange"
@blur="field.handleBlur" />
<InputLabel
:for="field.name"
class="font-medium text-text-primary">
I understand that this will permanently delete all data
related to this member
</InputLabel>
</div>
<InputError
class="pl-7 pt-2"
:message="field.state.meta.errors[0]" />
</div>
<Field orientation="horizontal">
<Checkbox
:id="field.name"
:name="field.name"
:checked="field.state.value"
@update:checked="field.handleChange"
@blur="field.handleBlur" />
<FieldLabel :for="field.name" class="font-medium text-text-primary">
I understand that this will permanently delete all data related
to this member
</FieldLabel>
<FieldError v-if="field.state.meta.errors[0]" class="pl-7 pt-2">
{{ field.state.meta.errors[0] }}
</FieldError>
</Field>
</template>
</form.Field>
</div>

View File

@@ -6,7 +6,7 @@ import type { Member, UpdateMemberBody } from '@/packages/api/src';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import { type MemberBillableKey, useMembersStore } from '@/utils/useMembers';
import BillableRateInput from '@/packages/ui/src/Input/BillableRateInput.vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel } from '@/packages/ui/src/field';
import MemberBillableRateModal from '@/Components/Common/Member/MemberBillableRateModal.vue';
import MemberBillableSelect from '@/Components/Common/Member/MemberBillableSelect.vue';
import { onMounted, watch } from 'vue';
@@ -121,28 +121,24 @@ const roleDescription = computed(() => {
<template #content>
<div class="pb-5 pt-2 divide-y divide-border-secondary">
<div class="pb-5 flex space-x-6">
<div>
<InputLabel for="role" value="Role" />
<MemberRoleSelect
v-model="memberBody.role"
class="mt-2"
name="role"></MemberRoleSelect>
</div>
<Field>
<FieldLabel for="role">Role</FieldLabel>
<MemberRoleSelect v-model="memberBody.role" name="role"></MemberRoleSelect>
</Field>
<div class="flex-1 text-xs flex items-center pt-6">
<p>{{ roleDescription }}</p>
</div>
</div>
<div class="flex items-center space-x-4 pt-5">
<div class="col-span-6 sm:col-span-4 flex-1 flex space-x-5">
<div>
<InputLabel for="billableType" value="Billable" />
<Field>
<FieldLabel for="billableType">Billable</FieldLabel>
<MemberBillableSelect
v-model="billableRateSelect"
class="mt-2"
name="billableType"></MemberBillableSelect>
</div>
<div v-if="billableRateSelect === 'custom-rate'" class="flex-1">
<InputLabel for="memberBillableRate" value="Billable Rate" />
</Field>
<Field v-if="billableRateSelect === 'custom-rate'" class="flex-1">
<FieldLabel for="memberBillableRate">Billable Rate</FieldLabel>
<BillableRateInput
v-model="memberBody.billable_rate"
focus
@@ -150,7 +146,7 @@ const roleDescription = computed(() => {
:currency="getOrganizationCurrencyString()"
name="memberBillableRate"
@keydown.enter="saveWithChecks()"></BillableRateInput>
</div>
</Field>
</div>
</div>
</div>

View File

@@ -5,8 +5,7 @@ import DialogModal from '@/packages/ui/src/DialogModal.vue';
import { ref } from 'vue';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import { useFocus } from '@vueuse/core';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import InputError from '@/packages/ui/src/Input/InputError.vue';
import { Field, FieldLabel, FieldError } from '@/packages/ui/src/field';
import type { Role } from '@/types/jetstream';
import { Link, useForm } from '@inertiajs/vue3';
import { getCurrentOrganizationId } from '@/utils/useUser';
@@ -111,8 +110,8 @@ useFocus(clientNameInput, { initialValue: true });
</div>
</div>
<div v-else class="space-y-4">
<div class="col-span-6 sm:col-span-4 flex-1">
<InputLabel for="email" value="Email" />
<Field class="col-span-6 sm:col-span-4 flex-1">
<FieldLabel for="email">Email</FieldLabel>
<TextInput
id="email"
ref="memberEmailInput"
@@ -120,16 +119,16 @@ useFocus(clientNameInput, { initialValue: true });
name="email"
type="text"
placeholder="Member Email"
class="mt-1 block w-full"
class="block w-full"
required
autocomplete="memberName"
@keydown.enter="submit" />
<InputError :message="errors.email" class="mt-2" />
</div>
<FieldError v-if="errors.email">{{ errors.email }}</FieldError>
</Field>
<div v-if="availableRoles.length > 0">
<InputLabel for="roles" value="Role" />
<InputError :message="errors.role" class="mt-2" />
<Field v-if="availableRoles.length > 0">
<FieldLabel for="roles">Role</FieldLabel>
<FieldError v-if="errors.role">{{ errors.role }}</FieldError>
<div
class="relative z-0 mt-1 border border-card-border rounded-lg bg-card-background cursor-pointer">
@@ -182,7 +181,7 @@ useFocus(clientNameInput, { initialValue: true });
</div>
</button>
</div>
</div>
</Field>
</div>
</template>
<template #footer>

View File

@@ -123,7 +123,7 @@ function updateValue(project: Project) {
<UseFocusTrap v-if="open" :options="{ immediate: true, allowOutsideClick: true }">
<ComboboxRoot
v-model:search-term="searchValue"
:open="open"
v-model:open="open"
:model-value="currentProject"
class="relative"
@update:model-value="updateValue">

View File

@@ -14,7 +14,7 @@ import { useClientsQuery } from '@/utils/useClientsQuery';
import ProjectColorSelector from '@/packages/ui/src/Project/ProjectColorSelector.vue';
import { UserCircleIcon } from '@heroicons/vue/20/solid';
import EstimatedTimeSection from '@/packages/ui/src/EstimatedTimeSection.vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel } from '@/packages/ui/src/field';
import ProjectBillableRateModal from '@/packages/ui/src/Project/ProjectBillableRateModal.vue';
import { getOrganizationCurrencyString } from '@/utils/money';
import ProjectEditBillableSection from '@/packages/ui/src/Project/ProjectEditBillableSection.vue';
@@ -82,34 +82,28 @@ async function submitBillableRate() {
<template #content>
<div class="sm:flex items-center space-y-2 sm:space-y-0 sm:space-x-5">
<div class="flex-1 flex items-center">
<Field class="flex-1 flex items-center">
<div class="text-center">
<InputLabel for="color" value="Color" />
<ProjectColorSelector
v-model="project.color"
class="mt-1"></ProjectColorSelector>
<FieldLabel for="color">Color</FieldLabel>
<ProjectColorSelector v-model="project.color"></ProjectColorSelector>
</div>
</div>
<div class="w-full">
<InputLabel for="projectName" value="Project name" />
</Field>
<Field class="w-full">
<FieldLabel for="projectName">Project name</FieldLabel>
<TextInput
id="projectName"
ref="projectNameInput"
v-model="project.name"
type="text"
placeholder="Project Name"
class="mt-1 block w-full"
class="block w-full"
required
autocomplete="projectName"
@keydown.enter="submit()" />
</div>
<div class="">
<InputLabel for="client" value="Client" />
<ClientDropdown
v-model="project.client_id"
:create-client
:clients="clients"
class="mt-1">
</Field>
<Field>
<FieldLabel for="client">Client</FieldLabel>
<ClientDropdown v-model="project.client_id" :create-client :clients="clients">
<template #trigger>
<Badge
class="bg-input-background cursor-pointer hover:bg-tertiary"
@@ -123,7 +117,7 @@ async function submitBillableRate() {
</Badge>
</template>
</ClientDropdown>
</div>
</Field>
</div>
<div>
<div>

View File

@@ -9,7 +9,7 @@ import { useProjectMembersStore } from '@/utils/useProjectMembers';
import BillableRateInput from '@/packages/ui/src/Input/BillableRateInput.vue';
import { UserIcon } from '@heroicons/vue/24/solid';
import ProjectMemberBillableRateModal from '@/Components/Common/ProjectMember/ProjectMemberBillableRateModal.vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel } from '@/packages/ui/src/field';
import { getOrganizationCurrencyString } from '@/utils/money';
const { updateProjectMember } = useProjectMembersStore();
@@ -82,14 +82,14 @@ useFocus(projectNameInput, { initialValue: true });
<UserIcon class="w-4 text-text-secondary"></UserIcon>
<span>{{ props.name }}</span>
</div>
<div class="col-span-3 sm:col-span-1 flex-1">
<InputLabel for="billable_rate" class="mb-2" value="Billable Rate"></InputLabel>
<Field class="col-span-3 sm:col-span-1 flex-1">
<FieldLabel for="billable_rate">Billable Rate</FieldLabel>
<BillableRateInput
v-model="projectMemberBody.billable_rate"
:currency="getOrganizationCurrencyString()"
name="billable_rate"
@keydown.enter="submit"></BillableRateInput>
</div>
</Field>
</div>
</template>
<template #footer>

View File

@@ -4,7 +4,7 @@ import SecondaryButton from '../../../packages/ui/src/Buttons/SecondaryButton.vu
import DialogModal from '@/packages/ui/src/DialogModal.vue';
import { ref } from 'vue';
import PrimaryButton from '../../../packages/ui/src/Buttons/PrimaryButton.vue';
import InputLabel from '../../../packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel } from '@/packages/ui/src/field';
import type { CreateReportBody, CreateReportBodyProperties } from '@/packages/api/src';
import { useMutation } from '@tanstack/vue-query';
import { getCurrentOrganizationId } from '@/utils/useUser';
@@ -82,31 +82,33 @@ async function submit() {
<template #content>
<div class="items-center space-y-4 w-full">
<div class="w-full">
<InputLabel for="name" value="Name" />
<TextInput id="name" v-model="report.name" class="mt-1.5 w-full"></TextInput>
</div>
<div>
<InputLabel for="description" value="Description" />
<Field class="w-full">
<FieldLabel for="name">Name</FieldLabel>
<TextInput id="name" v-model="report.name" class="w-full"></TextInput>
</Field>
<Field>
<FieldLabel for="description">Description</FieldLabel>
<TextInput
id="description"
v-model="report.description"
class="mt-1.5 w-full"></TextInput>
</div>
<InputLabel value="Visibility" />
<div class="flex items-center space-x-12">
<div class="flex items-center space-x-3 px-2 py-3">
<Checkbox id="is_public" v-model:checked="report.is_public"></Checkbox>
<InputLabel for="is_public" value="Public" />
class="w-full"></TextInput>
</Field>
<Field>
<FieldLabel>Visibility</FieldLabel>
<div class="flex items-center space-x-12">
<Field orientation="horizontal" class="px-2 py-3">
<Checkbox id="is_public" v-model:checked="report.is_public"></Checkbox>
<FieldLabel for="is_public">Public</FieldLabel>
</Field>
<Field v-if="report.is_public" class="flex-row items-center space-x-4">
<div>
<FieldLabel for="public_until">Expires at</FieldLabel>
<div class="text-text-tertiary font-medium">(optional)</div>
</div>
<DatePicker v-model="report.public_until"></DatePicker>
</Field>
</div>
<div v-if="report.is_public" class="flex items-center space-x-4">
<div>
<InputLabel for="public_until" value="Expires at" />
<div class="text-text-tertiary font-medium">(optional)</div>
</div>
<DatePicker v-model="report.public_until"></DatePicker>
</div>
</div>
</Field>
</div>
</template>
<template #footer>

View File

@@ -4,7 +4,7 @@ import SecondaryButton from '../../../packages/ui/src/Buttons/SecondaryButton.vu
import DialogModal from '@/packages/ui/src/DialogModal.vue';
import { computed, ref, watch } from 'vue';
import PrimaryButton from '../../../packages/ui/src/Buttons/PrimaryButton.vue';
import InputLabel from '../../../packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel } from '@/packages/ui/src/field';
import type { UpdateReportBody } from '@/packages/api/src';
import { useMutation, useQueryClient } from '@tanstack/vue-query';
import { getCurrentOrganizationId } from '@/utils/useUser';
@@ -105,28 +105,30 @@ async function submit() {
<template #content>
<div class="items-center space-y-4 w-full">
<div class="w-full">
<InputLabel for="name" value="Name" />
<TextInput id="name" v-model="report.name" class="mt-1.5 w-full"></TextInput>
</div>
<div>
<InputLabel for="description" value="Description" />
<Field class="w-full">
<FieldLabel for="name">Name</FieldLabel>
<TextInput id="name" v-model="report.name" class="w-full"></TextInput>
</Field>
<Field>
<FieldLabel for="description">Description</FieldLabel>
<TextInput
id="description"
v-model="report.description"
class="mt-1.5 w-full"></TextInput>
</div>
<InputLabel value="Visibility" />
<div class="flex items-center space-x-12">
<div class="flex items-center space-x-2 px-2 py-3">
<Checkbox id="is_public" v-model:checked="report.is_public"></Checkbox>
<InputLabel for="is_public" value="Public" />
class="w-full"></TextInput>
</Field>
<Field>
<FieldLabel>Visibility</FieldLabel>
<div class="flex items-center space-x-12">
<Field orientation="horizontal" class="px-2 py-3">
<Checkbox id="is_public" v-model:checked="report.is_public"></Checkbox>
<FieldLabel for="is_public">Public</FieldLabel>
</Field>
<Field v-if="report.is_public" orientation="horizontal">
<FieldLabel for="public_until">Expires at</FieldLabel>
<DatePicker v-model="localPublicUntil"></DatePicker>
</Field>
</div>
<div v-if="report.is_public" class="flex items-center space-x-4">
<InputLabel for="public_until" value="Expires at" />
<DatePicker v-model="localPublicUntil"></DatePicker>
</div>
</div>
</Field>
</div>
</template>
<template #footer>

View File

@@ -9,7 +9,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/Components/ui/select';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel } from '@/packages/ui/src/field';
import {
NumberField,
NumberFieldInput,
@@ -162,7 +162,7 @@ const iconClass = computed(() => {
>Rounding is a premium feature. Upgrade to unlock this feature.</span
>
<Link href="/billing">
<Button size="sm" variant="input" class="items-center space-x-1">
<Button size="sm" variant="outline" class="items-center space-x-1">
<CreditCardIcon class="w-3.5 h-3.5 text-text-tertiary mr-1" />
Go to Billing
</Button>
@@ -170,14 +170,14 @@ const iconClass = computed(() => {
</div>
<div v-else class="space-y-4">
<div>
<div class="flex items-center justify-between">
<InputLabel for="enable-rounding" value="Enable Rounding" />
<Field orientation="horizontal" class="justify-between">
<FieldLabel for="enable-rounding">Enable Rounding</FieldLabel>
<Switch
id="enable-rounding"
:model-value="enabled"
class="data-[state=checked]:bg-accent-500"
@update:model-value="updateEnabled" />
</div>
</Field>
<div
class="mb-3 pb-2 pt-1 text-xs text-muted-foreground border-b border-border-secondary text-text-tertiary">
Rounding is applied to each individual time entry, not to the accumulated
@@ -185,15 +185,15 @@ const iconClass = computed(() => {
</div>
</div>
<div>
<InputLabel for="rounding-type" value="Rounding Type" class="mb-2" />
<Field>
<FieldLabel for="rounding-type">Rounding Type</FieldLabel>
<Select
:model-value="type"
:disabled="!enabled"
@update:model-value="(value) => updateType(value as TimeEntryRoundingType)">
<SelectTrigger
id="rounding-type"
size="small"
size="sm"
class="w-full"
:disabled="!enabled">
<SelectValue placeholder="Select rounding type" />
@@ -204,16 +204,16 @@ const iconClass = computed(() => {
<SelectItem value="nearest">Round Nearest</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<InputLabel for="minutes-interval" value="Minutes Interval" class="mb-2" />
</Field>
<Field>
<FieldLabel for="minutes-interval">Minutes Interval</FieldLabel>
<Select
:model-value="selectedInterval"
:disabled="!enabled"
@update:model-value="(value) => handleIntervalChange(value as string)">
<SelectTrigger
id="minutes-interval"
size="small"
size="sm"
class="w-full"
:disabled="!enabled">
<SelectValue placeholder="Select interval" />
@@ -232,7 +232,6 @@ const iconClass = computed(() => {
<NumberField
id="custom-minutes"
:model-value="customMinutes"
size="small"
:min="1"
:max="1440"
:disabled="!enabled"
@@ -247,7 +246,7 @@ const iconClass = computed(() => {
</NumberFieldContent>
</NumberField>
</div>
</div>
</Field>
</div>
</PopoverContent>
</Popover>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, reactive, nextTick } from 'vue';
import DialogModal from '@/packages/ui/src/DialogModal.vue';
import InputError from '@/packages/ui/src/Input/InputError.vue';
import { Field, FieldError } from '@/packages/ui/src/field';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
import TextInput from '@/packages/ui/src/Input/TextInput.vue';
@@ -87,18 +87,18 @@ const closeModal = () => {
<template #content>
{{ content }}
<div class="mt-4">
<Field class="mt-4">
<TextInput
ref="passwordInput"
v-model="form.password"
type="password"
class="mt-1 block w-3/4"
class="block w-3/4"
placeholder="Password"
autocomplete="current-password"
@keyup.enter="confirmPassword" />
<InputError :message="form.error" class="mt-2" />
</div>
<FieldError v-if="form.error">{{ form.error }}</FieldError>
</Field>
</template>
<template #footer>

View File

@@ -1,25 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@/lib/utils';
import { Primitive, type PrimitiveProps } from 'reka-ui';
import { type ButtonVariants, buttonVariants } from '.';
interface Props extends PrimitiveProps {
variant?: ButtonVariants['variant'];
size?: ButtonVariants['size'];
class?: HTMLAttributes['class'];
}
const props = withDefaults(defineProps<Props>(), {
as: 'button',
});
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)">
<slot />
</Primitive>
</template>

View File

@@ -1,36 +0,0 @@
import { cva, type VariantProps } from 'class-variance-authority';
export { default as Button } from './Button.vue';
export const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline:
'border shadow-xs hover:text-text-primary bg-card-background dark:bg-transparent border-input dark:border-input hover:bg-white/5',
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
input: 'border-input-border border bg-input-background text-text-primary focus-visible:ring-2 focus-visible:ring-ring focus-visible:border-transparent shadow-sm',
},
size: {
default: 'h-9 px-4 py-2',
xs: 'h-7 rounded px-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
input: 'h-[42px] px-3 py-2 text-base',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
export type ButtonVariants = VariantProps<typeof buttonVariants>;

View File

@@ -42,7 +42,6 @@ const organization = inject<ComputedRef<Organization>>('organization');
<PopoverTrigger as-child>
<Button
variant="input"
size="input"
:class="[
'w-full justify-start text-left font-normal',
!model && 'text-muted-foreground',

View File

@@ -0,0 +1,24 @@
<script setup lang="ts">
import type { LabelProps } from 'reka-ui';
import type { HTMLAttributes } from 'vue';
import { reactiveOmit } from '@vueuse/core';
import { Label } from 'reka-ui';
import { cn } from '@/lib/utils';
const props = defineProps<LabelProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = reactiveOmit(props, 'class');
</script>
<template>
<Label
v-bind="delegatedProps"
:class="
cn(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
props.class
)
">
<slot />
</Label>
</template>

View File

@@ -0,0 +1 @@
export { default as Label } from './Label.vue';

View File

@@ -13,7 +13,7 @@ const props = defineProps<{
data-slot="input"
:class="
cn(
'flex h-[42px] w-full rounded-md border border-input-border bg-input-background px-3 py-2.5 text-sm text-center shadow-sm transition-colors placeholder:text-muted-foreground focus:border-input-border focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
'flex h-9 w-full rounded-md border border-input-border bg-input-background px-3 py-1 text-sm text-center shadow-sm transition-colors placeholder:text-muted-foreground focus:border-input-border focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
props.class
)
" />

View File

@@ -7,7 +7,7 @@ import { computed, type HTMLAttributes } from 'vue';
const props = withDefaults(
defineProps<
SelectTriggerProps & {
size?: 'small';
size?: 'default' | 'sm' | 'lg';
class?: HTMLAttributes['class'];
showChevron?: boolean;
variant?: 'default' | 'outline';
@@ -18,11 +18,19 @@ const props = withDefaults(
showChevron: true,
variant: 'default',
active: false,
size: 'default',
}
);
const delegatedProps = computed(() => {
const { class: _, showChevron: __, variant: ___, active: ____, ...delegated } = props;
const {
class: _,
showChevron: __,
variant: ___,
active: ____,
size: _____,
...delegated
} = props;
return delegated;
});
@@ -30,7 +38,14 @@ const delegatedProps = computed(() => {
const forwardedProps = useForwardProps(delegatedProps);
const sizeClasses = computed(() => {
return props.size === 'small' ? 'h-8 text-xs' : 'h-[42px] w-full text-sm';
switch (props.size) {
case 'sm':
return 'h-8 px-3 text-xs';
case 'lg':
return 'h-10 px-4 text-sm';
default:
return 'h-9 px-3 text-sm';
}
});
const variantClasses = computed(() => {
@@ -49,7 +64,7 @@ const variantClasses = computed(() => {
v-bind="forwardedProps"
:class="
cn(
'flex items-center justify-between gap-3 whitespace-nowrap rounded-md px-3 py-2.5 data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:truncate text-start font-medium transition-colors',
'flex items-center justify-between gap-3 whitespace-nowrap rounded-md data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:truncate text-start font-medium transition-colors',
sizeClasses,
variantClasses,
props.class

View File

@@ -8,8 +8,7 @@ import ConfirmationModal from '@/Components/ConfirmationModal.vue';
import DangerButton from '@/packages/ui/src/Buttons/DangerButton.vue';
import DialogModal from '@/packages/ui/src/DialogModal.vue';
import FormSection from '@/Components/FormSection.vue';
import InputError from '@/packages/ui/src/Input/InputError.vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel, FieldError } from '@/packages/ui/src/field';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
import SectionBorder from '@/Components/SectionBorder.vue';
@@ -96,20 +95,22 @@ const deleteApiToken = () => {
<template #form>
<!-- Token Name -->
<div class="col-span-6 sm:col-span-4">
<InputLabel for="name" value="Name" />
<Field class="col-span-6 sm:col-span-4">
<FieldLabel for="name">Name</FieldLabel>
<TextInput
id="name"
v-model="createApiTokenForm.name"
type="text"
class="mt-1 block w-full"
class="block w-full"
autofocus />
<InputError :message="createApiTokenForm.errors.name" class="mt-2" />
</div>
<FieldError v-if="createApiTokenForm.errors.name">{{
createApiTokenForm.errors.name
}}</FieldError>
</Field>
<!-- Token Permissions -->
<div v-if="availablePermissions.length > 0" class="col-span-6">
<InputLabel for="permissions" value="Permissions" />
<FieldLabel for="permissions">Permissions</FieldLabel>
<div class="mt-2 grid grid-cols-1 md:grid-cols-2 gap-4">
<div v-for="permission in availablePermissions" :key="permission">

View File

@@ -3,8 +3,7 @@ import { ref } from 'vue';
import { Head, useForm } from '@inertiajs/vue3';
import AuthenticationCard from '@/Components/AuthenticationCard.vue';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.vue';
import InputError from '@/packages/ui/src/Input/InputError.vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel, FieldError } from '@/packages/ui/src/field';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import TextInput from '@/packages/ui/src/Input/TextInput.vue';
@@ -39,19 +38,19 @@ const submit = () => {
</div>
<form @submit.prevent="submit">
<div>
<InputLabel for="password" value="Password" />
<Field>
<FieldLabel for="password">Password</FieldLabel>
<TextInput
id="password"
ref="passwordInput"
v-model="form.password"
type="password"
class="mt-1 block w-full"
class="block w-full"
required
autocomplete="current-password"
autofocus />
<InputError class="mt-2" :message="form.errors.password" />
</div>
<FieldError v-if="form.errors.password">{{ form.errors.password }}</FieldError>
</Field>
<div class="flex justify-end mt-4">
<PrimaryButton

View File

@@ -2,8 +2,7 @@
import { Head, useForm } from '@inertiajs/vue3';
import AuthenticationCard from '@/Components/AuthenticationCard.vue';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.vue';
import InputError from '@/packages/ui/src/Input/InputError.vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel, FieldError } from '@/packages/ui/src/field';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import TextInput from '@/packages/ui/src/Input/TextInput.vue';
@@ -38,18 +37,18 @@ const submit = () => {
</div>
<form @submit.prevent="submit">
<div>
<InputLabel for="email" value="Email" />
<Field>
<FieldLabel for="email">Email</FieldLabel>
<TextInput
id="email"
v-model="form.email"
type="email"
class="mt-1 block w-full"
class="block w-full"
required
autofocus
autocomplete="username" />
<InputError class="mt-2" :message="form.errors.email" />
</div>
<FieldError v-if="form.errors.email">{{ form.errors.email }}</FieldError>
</Field>
<div class="flex items-center justify-end mt-4">
<PrimaryButton

View File

@@ -2,8 +2,7 @@
import { Head, Link, useForm, usePage } from '@inertiajs/vue3';
import AuthenticationCard from '@/Components/AuthenticationCard.vue';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.vue';
import InputError from '@/packages/ui/src/Input/InputError.vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel, FieldError } from '@/packages/ui/src/field';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import TextInput from '@/packages/ui/src/Input/TextInput.vue';
@@ -60,30 +59,30 @@ const page = usePage<{
</div>
<form @submit.prevent="submit">
<div>
<InputLabel for="email" value="Email" />
<Field>
<FieldLabel for="email">Email</FieldLabel>
<TextInput
id="email"
v-model="form.email"
type="email"
class="mt-1 block w-full"
class="block w-full"
required
autofocus
autocomplete="username" />
<InputError class="mt-2" :message="form.errors.email" />
</div>
<FieldError v-if="form.errors.email">{{ form.errors.email }}</FieldError>
</Field>
<div class="mt-4">
<InputLabel for="password" value="Password" />
<Field class="mt-4">
<FieldLabel for="password">Password</FieldLabel>
<TextInput
id="password"
v-model="form.password"
type="password"
class="mt-1 block w-full"
class="block w-full"
required
autocomplete="current-password" />
<InputError class="mt-2" :message="form.errors.password" />
</div>
<FieldError v-if="form.errors.password">{{ form.errors.password }}</FieldError>
</Field>
<div class="flex items-center justify-end mt-4">
<Link

View File

@@ -3,8 +3,7 @@ import { Head, Link, useForm, usePage } from '@inertiajs/vue3';
import AuthenticationCard from '@/Components/AuthenticationCard.vue';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.vue';
import Checkbox from '@/packages/ui/src/Input/Checkbox.vue';
import InputError from '@/packages/ui/src/Input/InputError.vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel, FieldError } from '@/packages/ui/src/field';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import TextInput from '@/packages/ui/src/Input/TextInput.vue';
@@ -63,54 +62,56 @@ const page = usePage<{
</div>
<form @submit.prevent="submit">
<div>
<InputLabel for="name" value="Name" />
<Field>
<FieldLabel for="name">Name</FieldLabel>
<TextInput
id="name"
v-model="form.name"
type="text"
class="mt-1 block w-full"
class="block w-full"
required
autofocus
autocomplete="name" />
<InputError class="mt-2" :message="form.errors.name" />
</div>
<FieldError v-if="form.errors.name">{{ form.errors.name }}</FieldError>
</Field>
<div class="mt-4">
<InputLabel for="email" value="Email" />
<Field class="mt-4">
<FieldLabel for="email">Email</FieldLabel>
<TextInput
id="email"
v-model="form.email"
type="email"
class="mt-1 block w-full"
class="block w-full"
required
autocomplete="username" />
<InputError class="mt-2" :message="form.errors.email" />
</div>
<FieldError v-if="form.errors.email">{{ form.errors.email }}</FieldError>
</Field>
<div class="mt-4">
<InputLabel for="password" value="Password" />
<Field class="mt-4">
<FieldLabel for="password">Password</FieldLabel>
<TextInput
id="password"
v-model="form.password"
type="password"
class="mt-1 block w-full"
class="block w-full"
required
autocomplete="new-password" />
<InputError class="mt-2" :message="form.errors.password" />
</div>
<FieldError v-if="form.errors.password">{{ form.errors.password }}</FieldError>
</Field>
<div class="mt-4">
<InputLabel for="password_confirmation" value="Confirm Password" />
<Field class="mt-4">
<FieldLabel for="password_confirmation">Confirm Password</FieldLabel>
<TextInput
id="password_confirmation"
v-model="form.password_confirmation"
type="password"
class="mt-1 block w-full"
class="block w-full"
required
autocomplete="new-password" />
<InputError class="mt-2" :message="form.errors.password_confirmation" />
</div>
<FieldError v-if="form.errors.password_confirmation">{{
form.errors.password_confirmation
}}</FieldError>
</Field>
<div
v-if="
@@ -119,45 +120,41 @@ const page = usePage<{
page.props.privacy_policy_url !== null
"
class="mt-4">
<InputLabel for="terms">
<div class="flex items-center">
<Checkbox id="terms" v-model:checked="form.terms" name="terms" />
<div class="ms-2">
I agree to the
<a
target="_blank"
:href="page.props.terms_url"
class="underline text-sm text-text-secondary hover:text-white rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>Terms of Service</a
>
and
<a
target="_blank"
:href="page.props.privacy_policy_url"
class="underline text-sm text-text-secondary hover:text-white rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>Privacy Policy</a
>
</div>
</div>
<InputError class="mt-2" :message="form.errors.terms" />
</InputLabel>
<Field orientation="horizontal">
<Checkbox id="terms" v-model:checked="form.terms" name="terms" />
<FieldLabel for="terms">
I agree to the
<a
target="_blank"
:href="page.props.terms_url"
class="underline text-sm text-text-secondary hover:text-white rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>Terms of Service</a
>
and
<a
target="_blank"
:href="page.props.privacy_policy_url"
class="underline text-sm text-text-secondary hover:text-white rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>Privacy Policy</a
>
</FieldLabel>
<FieldError v-if="form.errors.terms">{{ form.errors.terms }}</FieldError>
</Field>
</div>
<div v-if="page.props.newsletter_consent" class="mt-4">
<InputLabel for="newsletter_consent">
<div class="flex items-center">
<Checkbox
id="newsletter_consent"
v-model:checked="form.newsletter_consent"
name="newsletter_consent" />
<div class="ms-2">
I agree to receive emails about product related updates
</div>
</div>
<InputError class="mt-2" :message="form.errors.newsletter_consent" />
</InputLabel>
<Field orientation="horizontal">
<Checkbox
id="newsletter_consent"
v-model:checked="form.newsletter_consent"
name="newsletter_consent" />
<FieldLabel for="newsletter_consent">
I agree to receive emails about product related updates
</FieldLabel>
<FieldError v-if="form.errors.newsletter_consent">{{
form.errors.newsletter_consent
}}</FieldError>
</Field>
</div>
<div class="flex items-center justify-end mt-4">

View File

@@ -2,8 +2,7 @@
import { Head, useForm } from '@inertiajs/vue3';
import AuthenticationCard from '@/Components/AuthenticationCard.vue';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.vue';
import InputError from '@/packages/ui/src/Input/InputError.vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel, FieldError } from '@/packages/ui/src/field';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import TextInput from '@/packages/ui/src/Input/TextInput.vue';
@@ -35,42 +34,44 @@ const submit = () => {
</template>
<form @submit.prevent="submit">
<div>
<InputLabel for="email" value="Email" />
<Field>
<FieldLabel for="email">Email</FieldLabel>
<TextInput
id="email"
v-model="form.email"
type="email"
class="mt-1 block w-full"
class="block w-full"
required
autofocus
autocomplete="username" />
<InputError class="mt-2" :message="form.errors.email" />
</div>
<FieldError v-if="form.errors.email">{{ form.errors.email }}</FieldError>
</Field>
<div class="mt-4">
<InputLabel for="password" value="Password" />
<Field class="mt-4">
<FieldLabel for="password">Password</FieldLabel>
<TextInput
id="password"
v-model="form.password"
type="password"
class="mt-1 block w-full"
class="block w-full"
required
autocomplete="new-password" />
<InputError class="mt-2" :message="form.errors.password" />
</div>
<FieldError v-if="form.errors.password">{{ form.errors.password }}</FieldError>
</Field>
<div class="mt-4">
<InputLabel for="password_confirmation" value="Confirm Password" />
<Field class="mt-4">
<FieldLabel for="password_confirmation">Confirm Password</FieldLabel>
<TextInput
id="password_confirmation"
v-model="form.password_confirmation"
type="password"
class="mt-1 block w-full"
class="block w-full"
required
autocomplete="new-password" />
<InputError class="mt-2" :message="form.errors.password_confirmation" />
</div>
<FieldError v-if="form.errors.password_confirmation">{{
form.errors.password_confirmation
}}</FieldError>
</Field>
<div class="flex items-center justify-end mt-4">
<PrimaryButton

View File

@@ -3,8 +3,7 @@ import { nextTick, ref } from 'vue';
import { Head, useForm } from '@inertiajs/vue3';
import AuthenticationCard from '@/Components/AuthenticationCard.vue';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.vue';
import InputError from '@/packages/ui/src/Input/InputError.vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel, FieldError } from '@/packages/ui/src/field';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import TextInput from '@/packages/ui/src/Input/TextInput.vue';
@@ -58,31 +57,33 @@ const submit = () => {
</div>
<form @submit.prevent="submit">
<div v-if="!recovery">
<InputLabel for="code" value="Code" />
<Field v-if="!recovery">
<FieldLabel for="code">Code</FieldLabel>
<TextInput
id="code"
ref="codeInput"
v-model="form.code"
type="text"
inputmode="numeric"
class="mt-1 block w-full"
class="block w-full"
autofocus
autocomplete="one-time-code" />
<InputError class="mt-2" :message="form.errors.code" />
</div>
<FieldError v-if="form.errors.code">{{ form.errors.code }}</FieldError>
</Field>
<div v-else>
<InputLabel for="recovery_code" value="Recovery Code" />
<Field v-else>
<FieldLabel for="recovery_code">Recovery Code</FieldLabel>
<TextInput
id="recovery_code"
ref="recoveryCodeInput"
v-model="form.recovery_code"
type="text"
class="mt-1 block w-full"
class="block w-full"
autocomplete="one-time-code" />
<InputError class="mt-2" :message="form.errors.recovery_code" />
</div>
<FieldError v-if="form.errors.recovery_code">{{
form.errors.recovery_code
}}</FieldError>
</Field>
<div class="flex items-center justify-end mt-4">
<button

View File

@@ -2,14 +2,13 @@
import FormSection from '@/Components/FormSection.vue';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import { computed, ref, inject, type ComputedRef } from 'vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel, FieldDescription, FieldError } from '@/packages/ui/src/field';
import { api, type ApiToken, type CreateApiTokenBody } from '@/packages/api/src';
import SectionBorder from '@/Components/SectionBorder.vue';
import DangerButton from '@/packages/ui/src/Buttons/DangerButton.vue';
import TextInput from '../../../packages/ui/src/Input/TextInput.vue';
import SecondaryButton from '../../../packages/ui/src/Buttons/SecondaryButton.vue';
import DialogModal from '@/packages/ui/src/DialogModal.vue';
import InputError from '@/packages/ui/src/Input/InputError.vue';
import ActionMessage from '@/Components/ActionMessage.vue';
import ConfirmationModal from '@/Components/ConfirmationModal.vue';
import ActionSection from '@/Components/ActionSection.vue';
@@ -147,20 +146,23 @@ const revokeApiTokenMutation = useMutation({
<template #form>
<!-- Token Name -->
<div class="col-span-6 sm:col-span-4">
<InputLabel for="api_key_name" value="API Key Name" />
<Field class="col-span-6 sm:col-span-4">
<FieldLabel for="api_key_name">API Key Name</FieldLabel>
<TextInput
id="api_key_name"
v-model="createApiTokenForm.name"
type="text"
class="mt-1 block w-full" />
<InputError :message="createApiTokenForm.errors.name" class="mt-2" />
<div
class="text-text-tertiary text-sm pt-3 flex space-x-1.5 font-medium items-center">
<ClockIcon class="w-4"></ClockIcon>
<span> API Tokens are valid for 1 year </span>
</div>
</div>
class="block w-full" />
<FieldError v-if="createApiTokenForm.errors.name">{{
createApiTokenForm.errors.name
}}</FieldError>
<FieldDescription>
<span class="flex space-x-1.5 items-center text-text-tertiary font-medium">
<ClockIcon class="w-4"></ClockIcon>
<span> API Tokens are valid for 1 year </span>
</span>
</FieldDescription>
</Field>
</template>
<template #actions>

View File

@@ -4,7 +4,7 @@ import { useForm } from '@inertiajs/vue3';
import ActionSection from '@/Components/ActionSection.vue';
import DangerButton from '@/packages/ui/src/Buttons/DangerButton.vue';
import DialogModal from '@/packages/ui/src/DialogModal.vue';
import InputError from '@/packages/ui/src/Input/InputError.vue';
import { Field, FieldError } from '@/packages/ui/src/field';
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
import TextInput from '@/packages/ui/src/Input/TextInput.vue';
@@ -63,18 +63,20 @@ const closeModal = () => {
of its resources and data will be permanently deleted. Please enter your
password to confirm you would like to permanently delete your account.
<div class="mt-4">
<Field class="mt-4">
<TextInput
ref="passwordInput"
v-model="form.password"
type="password"
class="mt-1 block w-3/4"
class="block w-3/4"
placeholder="Password"
autocomplete="current-password"
@keyup.enter="deleteUser" />
<InputError :message="form.errors.password" class="mt-2" />
</div>
<FieldError v-if="form.errors.password">{{
form.errors.password
}}</FieldError>
</Field>
</template>
<template #footer>

View File

@@ -4,7 +4,7 @@ import { useForm } from '@inertiajs/vue3';
import ActionMessage from '@/Components/ActionMessage.vue';
import ActionSection from '@/Components/ActionSection.vue';
import DialogModal from '@/packages/ui/src/DialogModal.vue';
import InputError from '@/packages/ui/src/Input/InputError.vue';
import { Field, FieldError } from '@/packages/ui/src/field';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
import TextInput from '@/packages/ui/src/Input/TextInput.vue';
@@ -131,18 +131,20 @@ const closeModal = () => {
Please enter your password to confirm you would like to log out of your other
browser sessions across all of your devices.
<div class="mt-4">
<Field class="mt-4">
<TextInput
ref="passwordInput"
v-model="form.password"
type="password"
class="mt-1 block w-3/4"
class="block w-3/4"
placeholder="Password"
autocomplete="current-password"
@keyup.enter="logoutOtherBrowserSessions" />
<InputError :message="form.errors.password" class="mt-2" />
</div>
<FieldError v-if="form.errors.password">{{
form.errors.password
}}</FieldError>
</Field>
</template>
<template #footer>

View File

@@ -1,6 +1,13 @@
<script setup lang="ts">
import InputLabel from '../../../packages/ui/src/Input/InputLabel.vue';
import FormSection from '@/Components/FormSection.vue';
import { Field, FieldLabel, FieldDescription } from '@/packages/ui/src/field';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/Components/ui/select';
import { usePreferredColorScheme } from '@vueuse/core';
import { themeSetting } from '@/utils/theme';
@@ -14,26 +21,22 @@ const preferredColor = usePreferredColorScheme();
<template #description> Choose how you want solidtime to look on your device </template>
<template #form>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="theme" value="Theme" />
<select
id="theme"
v-model="themeSetting"
name="theme"
required
class="mt-1 block w-full border-input-border bg-input-background text-text-primary focus:border-input-border-active rounded-md shadow-sm">
<option value="system">System</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<p
v-if="themeSetting === 'system'"
class="w-full text-sm text-text-tertiary font-medium mt-2">
<Field class="col-span-6 sm:col-span-4">
<FieldLabel for="theme">Theme</FieldLabel>
<Select id="theme" v-model="themeSetting">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="system">System</SelectItem>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
</SelectContent>
</Select>
<FieldDescription v-if="themeSetting === 'system'">
System default: {{ preferredColor }}
</p>
</div>
</FieldDescription>
</Field>
</template>
</FormSection>
</template>
<style scoped></style>

View File

@@ -4,8 +4,7 @@ import { router, useForm, usePage } from '@inertiajs/vue3';
import ActionSection from '@/Components/ActionSection.vue';
import ConfirmsPassword from '@/Components/ConfirmsPassword.vue';
import DangerButton from '@/packages/ui/src/Buttons/DangerButton.vue';
import InputError from '@/packages/ui/src/Input/InputError.vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel, FieldError } from '@/packages/ui/src/field';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
import TextInput from '@/packages/ui/src/Input/TextInput.vue';
@@ -162,22 +161,24 @@ const disableTwoFactorAuthentication = () => {
<p class="font-semibold">Setup Key: <span v-html="setupKey"></span></p>
</div>
<div v-if="confirming" class="mt-4">
<InputLabel for="code" value="Code" />
<Field v-if="confirming" class="mt-4 w-1/2">
<FieldLabel for="code">Code</FieldLabel>
<TextInput
id="code"
v-model="confirmationForm.code"
type="text"
name="code"
class="block mt-1 w-1/2"
class="block w-full"
inputmode="numeric"
autofocus
autocomplete="one-time-code"
@keyup.enter="confirmTwoFactorAuthentication" />
<InputError :message="confirmationForm.errors.code" class="mt-2" />
</div>
<FieldError v-if="confirmationForm.errors.code">{{
confirmationForm.errors.code
}}</FieldError>
</Field>
</div>
<div v-if="recoveryCodes.length > 0 && !confirming">

View File

@@ -3,8 +3,7 @@ import { ref } from 'vue';
import { useForm } from '@inertiajs/vue3';
import ActionMessage from '@/Components/ActionMessage.vue';
import FormSection from '@/Components/FormSection.vue';
import InputError from '@/packages/ui/src/Input/InputError.vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel, FieldError } from '@/packages/ui/src/field';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import TextInput from '@/packages/ui/src/Input/TextInput.vue';
@@ -46,40 +45,44 @@ const updatePassword = () => {
</template>
<template #form>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="current_password" value="Current Password" />
<Field class="col-span-6 sm:col-span-4">
<FieldLabel for="current_password">Current Password</FieldLabel>
<TextInput
id="current_password"
ref="currentPasswordInput"
v-model="form.current_password"
type="password"
class="mt-1 block w-full"
class="block w-full"
autocomplete="current-password" />
<InputError :message="form.errors.current_password" class="mt-2" />
</div>
<FieldError v-if="form.errors.current_password">{{
form.errors.current_password
}}</FieldError>
</Field>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="password" value="New Password" />
<Field class="col-span-6 sm:col-span-4">
<FieldLabel for="password">New Password</FieldLabel>
<TextInput
id="password"
ref="passwordInput"
v-model="form.password"
type="password"
class="mt-1 block w-full"
class="block w-full"
autocomplete="new-password" />
<InputError :message="form.errors.password" class="mt-2" />
</div>
<FieldError v-if="form.errors.password">{{ form.errors.password }}</FieldError>
</Field>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="password_confirmation" value="Confirm Password" />
<Field class="col-span-6 sm:col-span-4">
<FieldLabel for="password_confirmation">Confirm Password</FieldLabel>
<TextInput
id="password_confirmation"
v-model="form.password_confirmation"
type="password"
class="mt-1 block w-full"
class="block w-full"
autocomplete="new-password" />
<InputError :message="form.errors.password_confirmation" class="mt-2" />
</div>
<FieldError v-if="form.errors.password_confirmation">{{
form.errors.password_confirmation
}}</FieldError>
</Field>
</template>
<template #actions>

View File

@@ -3,8 +3,7 @@ import { ref } from 'vue';
import { Link, router, useForm, usePage } from '@inertiajs/vue3';
import ActionMessage from '@/Components/ActionMessage.vue';
import FormSection from '@/Components/FormSection.vue';
import InputError from '@/packages/ui/src/Input/InputError.vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel, FieldError } from '@/packages/ui/src/field';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
import TextInput from '@/packages/ui/src/Input/TextInput.vue';
@@ -105,7 +104,7 @@ const page = usePage<{
class="hidden"
@change="updatePhotoPreview" />
<InputLabel for="photo" value="Photo" />
<FieldLabel for="photo">Photo</FieldLabel>
<!-- Current Profile Photo -->
<div v-show="!photoPreview" class="mt-2">
@@ -134,33 +133,33 @@ const page = usePage<{
Remove Photo
</SecondaryButton>
<InputError :message="form.errors.photo" class="mt-2" />
<FieldError v-if="form.errors.photo">{{ form.errors.photo }}</FieldError>
</div>
<!-- Name -->
<div class="col-span-6 sm:col-span-4">
<InputLabel for="name" value="Name" />
<Field class="col-span-6 sm:col-span-4">
<FieldLabel for="name">Name</FieldLabel>
<TextInput
id="name"
v-model="form.name"
type="text"
class="mt-1 block w-full"
class="block w-full"
required
autocomplete="name" />
<InputError :message="form.errors.name" class="mt-2" />
</div>
<FieldError v-if="form.errors.name">{{ form.errors.name }}</FieldError>
</Field>
<!-- Email -->
<div class="col-span-6 sm:col-span-4">
<InputLabel for="email" value="Email" />
<Field class="col-span-6 sm:col-span-4">
<FieldLabel for="email">Email</FieldLabel>
<TextInput
id="email"
v-model="form.email"
type="email"
class="mt-1 block w-full"
class="block w-full"
required
autocomplete="username" />
<InputError :message="form.errors.email" class="mt-2" />
<FieldError v-if="form.errors.email">{{ form.errors.email }}</FieldError>
<div
v-if="
@@ -185,17 +184,17 @@ const page = usePage<{
A new verification link has been sent to your email address.
</div>
</div>
</div>
</Field>
<!-- Timezone -->
<div class="col-span-6 sm:col-span-4">
<InputLabel for="timezone" value="Timezone" />
<Field class="col-span-6 sm:col-span-4">
<FieldLabel for="timezone">Timezone</FieldLabel>
<select
id="timezone"
v-model="form.timezone"
name="timezone"
required
class="mt-1 block w-full border-input-border bg-input-background text-text-primary focus:border-input-border-active rounded-md shadow-sm">
class="block w-full border-input-border bg-input-background text-text-primary focus:border-input-border-active rounded-md shadow-sm">
<option value="" disabled>Select a Timezone</option>
<option
v-for="(timezoneTranslated, timezoneKey) in $page.props.timezones"
@@ -204,18 +203,18 @@ const page = usePage<{
{{ timezoneTranslated }}
</option>
</select>
<InputError :message="form.errors.timezone" class="mt-2" />
</div>
<FieldError v-if="form.errors.timezone">{{ form.errors.timezone }}</FieldError>
</Field>
<!-- Week start -->
<div class="col-span-6 sm:col-span-4">
<InputLabel for="week_start" value="Start of the week" />
<Field class="col-span-6 sm:col-span-4">
<FieldLabel for="week_start">Start of the week</FieldLabel>
<select
id="week_start"
v-model="form.week_start"
name="week_start"
required
class="mt-1 block w-full border-input-border bg-input-background text-text-primary focus:border-input-border-active rounded-md shadow-sm">
class="block w-full border-input-border bg-input-background text-text-primary focus:border-input-border-active rounded-md shadow-sm">
<option value="" disabled>Select a week day</option>
<option
v-for="(weekdayTranslated, weekdayKey) in $page.props.weekdays"
@@ -224,8 +223,8 @@ const page = usePage<{
{{ weekdayTranslated }}
</option>
</select>
<InputError :message="form.errors.week_start" class="mt-2" />
</div>
<FieldError v-if="form.errors.week_start">{{ form.errors.week_start }}</FieldError>
</Field>
</template>
<template #actions>

View File

@@ -1,8 +1,7 @@
<script setup lang="ts">
import { useForm, usePage } from '@inertiajs/vue3';
import FormSection from '@/Components/FormSection.vue';
import InputError from '@/packages/ui/src/Input/InputError.vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel, FieldError } from '@/packages/ui/src/field';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import TextInput from '@/packages/ui/src/Input/TextInput.vue';
import type { User } from '@/types/models';
@@ -38,7 +37,7 @@ const page = usePage<{
<template #form>
<div class="col-span-6">
<InputLabel value="Organization Owner" />
<FieldLabel>Organization Owner</FieldLabel>
<div class="flex items-center mt-2">
<img
@@ -57,16 +56,16 @@ const page = usePage<{
</div>
</div>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="name" value="Organization Name" />
<Field class="col-span-6 sm:col-span-4">
<FieldLabel for="name">Organization Name</FieldLabel>
<TextInput
id="name"
v-model="form.name"
type="text"
class="block w-full mt-1"
class="block w-full"
autofocus />
<InputError :message="form.errors.name" class="mt-2" />
</div>
<FieldError v-if="form.errors.name">{{ form.errors.name }}</FieldError>
</Field>
</template>
<template #actions>

View File

@@ -3,7 +3,7 @@ import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import { computed, onMounted, ref } from 'vue';
import { useNotificationsStore } from '@/utils/notification';
import { api } from '@/packages/api/src';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel } from '@/packages/ui/src/field';
import { DocumentIcon } from '@heroicons/vue/24/solid';
import { ArrowDownOnSquareIcon, InformationCircleIcon } from '@heroicons/vue/24/outline';
@@ -163,13 +163,13 @@ const showResultModal = ref(false);
<Card>
<div class="px-4 py-5 sm:px-5">
<div>
<InputLabel for="importType" value="Import Type" />
<Field>
<FieldLabel for="importType">Import Type</FieldLabel>
<select
id="importType"
v-model="importType"
name="importType"
class="mt-1 block w-full border-input-border bg-input-background text-text-primary focus:border-input-border-active rounded-md shadow-sm">
class="block w-full border-input-border bg-input-background text-text-primary focus:border-input-border-active rounded-md shadow-sm">
<option :value="null" selected disabled>
Select an import type to get instructions...
</option>
@@ -184,7 +184,7 @@ const showResultModal = ref(false);
<div class="font-semibold text-text-secondary py-1">Instructions:</div>
<div class="max-w-2xl" v-html="currentImporterDescription"></div>
</div>
</div>
</Field>
<div
class="mt-2 flex justify-center rounded-lg border border-dashed border-border-primary px-6 py-10">

View File

@@ -2,7 +2,7 @@
import FormSection from '@/Components/FormSection.vue';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import { onMounted, ref } from 'vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel } from '@/packages/ui/src/field';
import type { UpdateOrganizationBody } from '@/packages/api/src';
import BillableRateInput from '@/packages/ui/src/Input/BillableRateInput.vue';
import { useOrganizationStore } from '@/utils/useOrganization';
@@ -61,35 +61,27 @@ function checkForConfirmationModal() {
v-model:show="showConfirmationModal"
:new-billable-rate="organizationBody.billable_rate"
@submit="submit"></OrganizationBillableRateModal>
<!-- Organization Owner Information -->
<div class="col-span-6">
<div class="col-span-6 sm:col-span-4">
<InputLabel
for="organizationBillableRate"
class="mb-2"
value="Organization Billable Rate" />
<BillableRateInput
v-if="organization"
v-model="organizationBody.billable_rate"
:currency="getOrganizationCurrencyString()"
name="organizationBillableRate"></BillableRateInput>
</div>
</div>
<Field class="col-span-6 sm:col-span-4">
<FieldLabel for="organizationBillableRate">Organization Billable Rate</FieldLabel>
<BillableRateInput
v-if="organization"
v-model="organizationBody.billable_rate"
:currency="getOrganizationCurrencyString()"
name="organizationBillableRate"></BillableRateInput>
</Field>
<div class="col-span-6">
<div class="col-span-6 sm:col-span-4">
<div class="flex items-center space-x-2">
<Checkbox
v-if="organization"
id="organizationShowBillableRatesToEmployees"
v-model:checked="
organizationBody.employees_can_see_billable_rates
"></Checkbox>
<InputLabel
for="organizationShowBillableRatesToEmployees"
value="Show Billable Rates to Employees" />
</div>
</div>
<div class="col-span-6 sm:col-span-4">
<Field orientation="horizontal">
<Checkbox
v-if="organization"
id="organizationShowBillableRatesToEmployees"
v-model:checked="
organizationBody.employees_can_see_billable_rates
"></Checkbox>
<FieldLabel for="organizationShowBillableRatesToEmployees"
>Show Billable Rates to Employees</FieldLabel
>
</Field>
</div>
</template>
<template #actions>

View File

@@ -2,7 +2,7 @@
import FormSection from '@/Components/FormSection.vue';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import { onMounted, ref } from 'vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel } from '@/packages/ui/src/field';
import type { UpdateOrganizationBody } from '@/packages/api/src';
import { useOrganizationStore } from '@/utils/useOrganization';
import { storeToRefs } from 'pinia';
@@ -73,99 +73,89 @@ async function submit() {
<template #form>
<!-- Number Format -->
<div class="col-span-6">
<div class="col-span-6 sm:col-span-4">
<InputLabel for="numberFormat" class="mb-2" value="Number Format" />
<Select v-model="form.number_format">
<SelectTrigger id="numberFormat">
<SelectValue placeholder="Select number format" />
</SelectTrigger>
<SelectContent>
<SelectItem value="point-comma">1.111,11</SelectItem>
<SelectItem value="comma-point">1,111.11</SelectItem>
<SelectItem value="space-comma">1 111,11</SelectItem>
<SelectItem value="space-point">1 111.11</SelectItem>
<SelectItem value="apostrophe-point">1'111.11</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<Field class="col-span-6 sm:col-span-4">
<FieldLabel for="numberFormat">Number Format</FieldLabel>
<Select v-model="form.number_format">
<SelectTrigger id="numberFormat">
<SelectValue placeholder="Select number format" />
</SelectTrigger>
<SelectContent>
<SelectItem value="point-comma">1.111,11</SelectItem>
<SelectItem value="comma-point">1,111.11</SelectItem>
<SelectItem value="space-comma">1 111,11</SelectItem>
<SelectItem value="space-point">1 111.11</SelectItem>
<SelectItem value="apostrophe-point">1'111.11</SelectItem>
</SelectContent>
</Select>
</Field>
<!-- Currency Format -->
<div class="col-span-6">
<div class="col-span-6 sm:col-span-4">
<InputLabel for="currencyFormat" class="mb-2" value="Currency Format" />
<Select v-model="form.currency_format">
<SelectTrigger id="currencyFormat">
<SelectValue placeholder="Select currency format" />
</SelectTrigger>
<SelectContent>
<SelectItem value="iso-code-before-with-space">EUR 111</SelectItem>
<SelectItem value="iso-code-after-with-space">111 EUR</SelectItem>
<SelectItem value="symbol-before">111</SelectItem>
<SelectItem value="symbol-after">111</SelectItem>
<SelectItem value="symbol-before-with-space">111</SelectItem>
<SelectItem value="symbol-after-with-space">111 €</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<Field class="col-span-6 sm:col-span-4">
<FieldLabel for="currencyFormat">Currency Format</FieldLabel>
<Select v-model="form.currency_format">
<SelectTrigger id="currencyFormat">
<SelectValue placeholder="Select currency format" />
</SelectTrigger>
<SelectContent>
<SelectItem value="iso-code-before-with-space">EUR 111</SelectItem>
<SelectItem value="iso-code-after-with-space">111 EUR</SelectItem>
<SelectItem value="symbol-before">111</SelectItem>
<SelectItem value="symbol-after">111</SelectItem>
<SelectItem value="symbol-before-with-space">€ 111</SelectItem>
<SelectItem value="symbol-after-with-space">111</SelectItem>
</SelectContent>
</Select>
</Field>
<!-- Date Format -->
<div class="col-span-6">
<div class="col-span-6 sm:col-span-4">
<InputLabel for="dateFormat" class="mb-2" value="Date Format" />
<Select v-model="form.date_format">
<SelectTrigger id="dateFormat">
<SelectValue placeholder="Select date format" />
</SelectTrigger>
<SelectContent>
<SelectItem value="point-separated-d-m-yyyy">D.M.YYYY</SelectItem>
<SelectItem value="slash-separated-mm-dd-yyyy">MM/DD/YYYY</SelectItem>
<SelectItem value="slash-separated-dd-mm-yyyy">DD/MM/YYYY</SelectItem>
<SelectItem value="hyphen-separated-dd-mm-yyyy">DD-MM-YYYY</SelectItem>
<SelectItem value="hyphen-separated-mm-dd-yyyy">MM-DD-YYYY</SelectItem>
<SelectItem value="hyphen-separated-yyyy-mm-dd">YYYY-MM-DD</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<Field class="col-span-6 sm:col-span-4">
<FieldLabel for="dateFormat">Date Format</FieldLabel>
<Select v-model="form.date_format">
<SelectTrigger id="dateFormat">
<SelectValue placeholder="Select date format" />
</SelectTrigger>
<SelectContent>
<SelectItem value="point-separated-d-m-yyyy">D.M.YYYY</SelectItem>
<SelectItem value="slash-separated-mm-dd-yyyy">MM/DD/YYYY</SelectItem>
<SelectItem value="slash-separated-dd-mm-yyyy">DD/MM/YYYY</SelectItem>
<SelectItem value="hyphen-separated-dd-mm-yyyy">DD-MM-YYYY</SelectItem>
<SelectItem value="hyphen-separated-mm-dd-yyyy">MM-DD-YYYY</SelectItem>
<SelectItem value="hyphen-separated-yyyy-mm-dd">YYYY-MM-DD</SelectItem>
</SelectContent>
</Select>
</Field>
<!-- Time Format -->
<div class="col-span-6">
<div class="col-span-6 sm:col-span-4">
<InputLabel for="timeFormat" class="mb-2" value="Time Format" />
<Select v-model="form.time_format">
<SelectTrigger id="timeFormat">
<SelectValue placeholder="Select time format" />
</SelectTrigger>
<SelectContent>
<SelectItem value="12-hours">12-hour clock</SelectItem>
<SelectItem value="24-hours">24-hour clock</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<Field class="col-span-6 sm:col-span-4">
<FieldLabel for="timeFormat">Time Format</FieldLabel>
<Select v-model="form.time_format">
<SelectTrigger id="timeFormat">
<SelectValue placeholder="Select time format" />
</SelectTrigger>
<SelectContent>
<SelectItem value="12-hours">12-hour clock</SelectItem>
<SelectItem value="24-hours">24-hour clock</SelectItem>
</SelectContent>
</Select>
</Field>
<!-- Interval Format -->
<div class="col-span-6">
<div class="col-span-6 sm:col-span-4">
<InputLabel for="intervalFormat" class="mb-2" value="Time Duration Format" />
<Select v-model="form.interval_format">
<SelectTrigger id="intervalFormat">
<SelectValue placeholder="Select interval format" />
</SelectTrigger>
<SelectContent>
<SelectItem value="decimal">Decimal</SelectItem>
<SelectItem value="hours-minutes">12h 3m</SelectItem>
<SelectItem value="hours-minutes-colon-separated">12:03</SelectItem>
<SelectItem value="hours-minutes-seconds-colon-separated"
>12:03:45</SelectItem
>
</SelectContent>
</Select>
</div>
</div>
<Field class="col-span-6 sm:col-span-4">
<FieldLabel for="intervalFormat">Time Duration Format</FieldLabel>
<Select v-model="form.interval_format">
<SelectTrigger id="intervalFormat">
<SelectValue placeholder="Select interval format" />
</SelectTrigger>
<SelectContent>
<SelectItem value="decimal">Decimal</SelectItem>
<SelectItem value="hours-minutes">12h 3m</SelectItem>
<SelectItem value="hours-minutes-colon-separated">12:03</SelectItem>
<SelectItem value="hours-minutes-seconds-colon-separated"
>12:03:45</SelectItem
>
</SelectContent>
</Select>
</Field>
</template>
<template #actions>

View File

@@ -2,7 +2,7 @@
import FormSection from '@/Components/FormSection.vue';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import { onMounted, ref } from 'vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel } from '@/packages/ui/src/field';
import { Checkbox } from '@/packages/ui/src';
import type { UpdateOrganizationBody } from '@/packages/api/src';
import { useOrganizationStore } from '@/utils/useOrganization';
@@ -52,25 +52,23 @@ async function submit() {
</template>
<template #form>
<div class="col-span-6">
<div class="col-span-6 sm:col-span-4 space-y-4">
<div class="flex items-center space-x-2">
<Checkbox
id="preventOverlappingTimeEntries"
v-model:checked="form.prevent_overlapping_time_entries" />
<InputLabel
for="preventOverlappingTimeEntries"
value="Prevent overlapping time entries (new entries only)" />
</div>
<div class="flex items-center space-x-2">
<Checkbox
id="employeesCanManageTasks"
v-model:checked="form.employees_can_manage_tasks" />
<InputLabel
for="employeesCanManageTasks"
value="Allow Employees to manage tasks" />
</div>
</div>
<div class="col-span-6 sm:col-span-4 space-y-4">
<Field orientation="horizontal">
<Checkbox
id="preventOverlappingTimeEntries"
v-model:checked="form.prevent_overlapping_time_entries" />
<FieldLabel for="preventOverlappingTimeEntries"
>Prevent overlapping time entries (new entries only)</FieldLabel
>
</Field>
<Field orientation="horizontal">
<Checkbox
id="employeesCanManageTasks"
v-model:checked="form.employees_can_manage_tasks" />
<FieldLabel for="employeesCanManageTasks"
>Allow Employees to manage tasks</FieldLabel
>
</Field>
</div>
</template>

View File

@@ -7,8 +7,8 @@ import ConfirmationModal from '@/Components/ConfirmationModal.vue';
import DangerButton from '@/packages/ui/src/Buttons/DangerButton.vue';
import DialogModal from '@/packages/ui/src/DialogModal.vue';
import FormSection from '@/Components/FormSection.vue';
import InputError from '@/packages/ui/src/Input/InputError.vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel, FieldError } from '@/packages/ui/src/field';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
import SectionBorder from '@/Components/SectionBorder.vue';
@@ -139,20 +139,24 @@ const displayableRole = (role: string) => {
</div>
<!-- Member Email -->
<div class="col-span-6 sm:col-span-4">
<InputLabel for="email" value="Email" />
<Field class="col-span-6 sm:col-span-4">
<FieldLabel for="email">Email</FieldLabel>
<TextInput
id="email"
v-model="addTeamMemberForm.email"
type="email"
class="mt-1 block w-full" />
<InputError :message="addTeamMemberForm.errors.email" class="mt-2" />
</div>
class="block w-full" />
<FieldError v-if="addTeamMemberForm.errors.email">{{
addTeamMemberForm.errors.email
}}</FieldError>
</Field>
<!-- Role -->
<div v-if="availableRoles.length > 0" class="col-span-6 lg:col-span-4">
<InputLabel for="roles" value="Role" />
<InputError :message="addTeamMemberForm.errors.role" class="mt-2" />
<FieldLabel for="roles">Role</FieldLabel>
<FieldError v-if="addTeamMemberForm.errors.role">{{
addTeamMemberForm.errors.role
}}</FieldError>
<div
class="relative z-0 mt-1 border border-card-border rounded-lg cursor-pointer">

View File

@@ -2,8 +2,7 @@
import { Link, useForm } from '@inertiajs/vue3';
import ActionMessage from '@/Components/ActionMessage.vue';
import FormSection from '@/Components/FormSection.vue';
import InputError from '@/packages/ui/src/Input/InputError.vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel, FieldError } from '@/packages/ui/src/field';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import TextInput from '@/packages/ui/src/Input/TextInput.vue';
import type { Organization } from '@/types/models';
@@ -40,7 +39,7 @@ const updateTeamName = () => {
<!-- Organization Owner Information -->
<div class="col-span-6 flex items-center justify-between">
<div class="">
<InputLabel value="Organization Owner" />
<FieldLabel>Organization Owner</FieldLabel>
<div class="flex items-center mt-2">
<img
@@ -68,28 +67,28 @@ const updateTeamName = () => {
</div>
<!-- Organization Name -->
<div class="col-span-6 sm:col-span-4">
<InputLabel for="name" value="Organization Name" />
<Field class="col-span-6 sm:col-span-4">
<FieldLabel for="name">Organization Name</FieldLabel>
<TextInput
id="name"
v-model="form.name"
type="text"
class="mt-1 block w-full"
class="block w-full"
:disabled="!permissions.canUpdateTeam" />
<InputError :message="form.errors.name" class="mt-2" />
</div>
<FieldError v-if="form.errors.name">{{ form.errors.name }}</FieldError>
</Field>
<!-- Currency -->
<div class="col-span-6 sm:col-span-4">
<InputLabel for="currency" value="Currency" />
<Field class="col-span-6 sm:col-span-4">
<FieldLabel for="currency">Currency</FieldLabel>
<select
id="currency"
v-model="form.currency"
name="currency"
:disabled="!permissions.canUpdateTeam"
class="mt-1 block w-full border-input-border bg-input-background text-text-primary focus:border-input-border-active rounded-md shadow-sm">
class="block w-full border-input-border bg-input-background text-text-primary focus:border-input-border-active rounded-md shadow-sm">
<option value="" disabled>Select a currency</option>
<option
v-for="(currencyTranslated, currencyKey) in $page.props.currencies"
@@ -98,8 +97,8 @@ const updateTeamName = () => {
{{ currencyKey }} - {{ currencyTranslated }}
</option>
</select>
<InputError :message="form.errors.currency" class="mt-2" />
</div>
<FieldError v-if="form.errors.currency">{{ form.errors.currency }}</FieldError>
</Field>
</template>
<template v-if="permissions.canUpdateTeam" #actions>

View File

@@ -18,12 +18,11 @@ export const buttonVariants = cva(
input: 'border-input-border border bg-input-background text-text-primary focus-visible:ring-2 focus-visible:ring-ring focus-visible:border-transparent shadow-sm',
},
size: {
default: 'h-9 px-4 py-2',
xs: 'h-7 rounded px-2',
default: 'h-9 px-3 text-sm',
xs: 'h-7 rounded px-2 text-xs',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
lg: 'h-10 rounded-md px-4',
icon: 'h-9 w-9',
input: 'h-[42px] px-3 py-2 text-base',
},
},
defaultVariants: {

View File

@@ -1,17 +1,17 @@
<script setup lang="ts">
import EstimatedTimeInput from '@/packages/ui/src/Input/EstimatedTimeInput.vue';
import { ClockIcon } from '@heroicons/vue/20/solid';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel } from './field';
const model = defineModel<number | null>();
const emit = defineEmits(['submit']);
</script>
<template>
<div class="pt-6">
<div class="flex items-center space-x-1 mb-2">
<Field class="pt-6">
<div class="flex items-center space-x-1">
<ClockIcon class="text-text-quaternary w-4"></ClockIcon>
<InputLabel for="billable" value="Time Estimated" />
<FieldLabel for="billable">Time Estimated</FieldLabel>
</div>
<EstimatedTimeInput v-model="model" @submit="emit('submit')"></EstimatedTimeInput>
<div class="flex items-center text-text-secondary text-xs pt-2 pl-1">
@@ -21,7 +21,7 @@ const emit = defineEmits(['submit']);
<span class="font-semibold">2h 30m</span>
</span>
</div>
</div>
</Field>
</template>
<style scoped></style>

View File

@@ -75,12 +75,12 @@ const emit = defineEmits(['update:modelValue', 'changed', 'submit']);
<template #content>
<ComboboxRoot
v-model:search-term="searchValue"
:open="open"
v-model:open="open"
class="p-2"
:filter-function="(val: string[]) => val">
<ComboboxAnchor>
<ComboboxInput
class="w-full rounded-md border border-input-border bg-input-background px-3 py-1.5 text-sm text-text-primary placeholder:text-text-tertiary focus:outline-none"
class="w-full h-8 rounded-md border border-input-border bg-input-background px-3 text-sm text-text-primary placeholder:text-text-tertiary focus:outline-none"
:placeholder="searchPlaceholder" />
</ComboboxAnchor>
<ComboboxContent

View File

@@ -25,7 +25,7 @@ const model = defineModel();
v-model="model"
:class="
twMerge(
'border-input-border border bg-input-background text-text-primary focus-visible:ring-2 focus-visible:ring-ring focus-visible:border-transparent rounded-md shadow-sm',
'h-9 px-3 py-1 text-sm border-input-border border bg-input-background text-text-primary focus-visible:ring-2 focus-visible:ring-ring focus-visible:border-transparent rounded-md shadow-sm',
props.class
)
"

View File

@@ -3,7 +3,6 @@ import { ref, watch } from 'vue';
import { getLocalizedDayJs } from '@/packages/ui/src/utils/time';
import { useFocus } from '@vueuse/core';
import { TextInput } from '@/packages/ui/src';
import { twMerge } from 'tailwind-merge';
// This has to be a localized timestamp, not UTC
const model = defineModel<string | null>({
@@ -12,11 +11,9 @@ const model = defineModel<string | null>({
const props = withDefaults(
defineProps<{
size?: 'base' | 'large';
focus?: boolean;
}>(),
{
size: 'base',
focus: false,
}
);
@@ -101,7 +98,7 @@ const inputValue = ref(model.value ? getLocalizedDayJs(model.value).format('HH:m
v-bind="$attrs"
ref="timeInput"
v-model="inputValue"
:class="twMerge('text-center w-24 px-3 py-2', size === 'large' && 'w-28')"
class="text-center w-full"
data-testid="time_picker_input"
type="text"
@blur="updateTime"

View File

@@ -12,7 +12,7 @@ import Badge from '@/packages/ui/src/Badge.vue';
import ProjectColorSelector from '@/packages/ui/src/Project/ProjectColorSelector.vue';
import { UserCircleIcon } from '@heroicons/vue/20/solid';
import EstimatedTimeSection from '@/packages/ui/src/EstimatedTimeSection.vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel } from '../field';
import ProjectEditBillableSection from '@/packages/ui/src/Project/ProjectEditBillableSection.vue';
import type { Client } from '@/packages/api/src';
@@ -76,14 +76,12 @@ const currentClientName = computed(() => {
<template #content>
<div class="sm:flex items-center space-y-2 sm:space-y-0 sm:space-x-4">
<div class="flex-1 flex items-center">
<div class="text-center pr-5">
<InputLabel for="color" value="Color" />
<ProjectColorSelector
v-model="project.color"
class="mt-2.5"></ProjectColorSelector>
</div>
<div class="w-full">
<InputLabel for="projectName" value="Project name" />
<Field class="text-center pr-5">
<FieldLabel for="color">Color</FieldLabel>
<ProjectColorSelector v-model="project.color"></ProjectColorSelector>
</Field>
<Field class="w-full">
<FieldLabel for="projectName">Project name</FieldLabel>
<TextInput
id="projectName"
ref="projectNameInput"
@@ -91,19 +89,18 @@ const currentClientName = computed(() => {
name="projectName"
type="text"
placeholder="The next big thing"
class="mt-2 block w-full"
class="block w-full"
required
autocomplete="projectName"
@keydown.enter="submit()" />
</div>
</Field>
</div>
<div>
<InputLabel for="client" value="Client" />
<Field>
<FieldLabel for="client">Client</FieldLabel>
<ClientDropdown
v-model="project.client_id"
:create-client="createClient"
:clients="activeClients"
class="mt-2">
:clients="activeClients">
<template #trigger>
<Badge
tag="button"
@@ -118,7 +115,7 @@ const currentClientName = computed(() => {
</Badge>
</template>
</ClientDropdown>
</div>
</Field>
</div>
<div>
<div>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel } from '../field';
import BillableRateInput from '@/packages/ui/src/Input/BillableRateInput.vue';
import ProjectBillableSelect from '@/packages/ui/src/Project/ProjectBillableSelect.vue';
import { computed, onMounted, ref, watch } from 'vue';
@@ -55,23 +55,21 @@ const emit = defineEmits(['submit']);
<template>
<div class="sm:flex items-center space-y-2 sm:space-y-0 sm:space-x-4 pt-6">
<div>
<div class="flex items-center space-x-1 mb-2">
<Field>
<div class="flex items-center space-x-1">
<BillableIcon class="text-text-quaternary h-4 ml-1 mr-0.5"></BillableIcon>
<InputLabel for="billable" value="Billable Default" />
<FieldLabel for="billable">Billable Default</FieldLabel>
</div>
<ProjectBillableSelect
v-model="billableRateSelect"
class="mt-2"></ProjectBillableSelect>
</div>
<div v-if="billableRateSelect === 'custom-rate'">
<InputLabel for="billableRate" value="Billable Rate" class="mb-2" />
<ProjectBillableSelect v-model="billableRateSelect"></ProjectBillableSelect>
</Field>
<Field v-if="billableRateSelect === 'custom-rate'">
<FieldLabel for="billableRate">Billable Rate</FieldLabel>
<BillableRateInput
v-model="billableRate"
:currency="currency"
name="billableRate"
@keydown.enter="emit('submit')" />
</div>
</Field>
</div>
<div class="flex items-center text-text-secondary text-xs pt-2 pl-1">
<span>

View File

@@ -5,7 +5,7 @@ import DialogModal from '@/packages/ui/src/DialogModal.vue';
import { computed, nextTick, ref, watch } from 'vue';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import TimeTrackerProjectTaskDropdown from '@/packages/ui/src/TimeTracker/TimeTrackerProjectTaskDropdown.vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel } from '../field';
import { TagIcon } from '@heroicons/vue/20/solid';
import { getDayJsInstance, getLocalizedDayJs } from '@/packages/ui/src/utils/time';
import type {
@@ -24,7 +24,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/Components/ui/select';
import { Button } from '@/Components/ui/button';
import { Button } from '@/packages/ui/src/Buttons';
import DatePicker from '@/packages/ui/src/Input/DatePicker.vue';
import DurationHumanInput from '@/packages/ui/src/Input/DurationHumanInput.vue';
@@ -165,6 +165,7 @@ const billableProxy = computed({
v-model:project="timeEntry.project_id"
v-model:task="timeEntry.task_id"
variant="input"
size="default"
:clients
:create-project
:create-client
@@ -181,7 +182,7 @@ const billableProxy = computed({
:tags="tags"
:show-no-tag-option="false">
<template #trigger>
<Button variant="input" size="sm">
<Button variant="input">
<TagIcon class="h-4 text-icon-default" />
<span>{{
timeEntry.tags.length === 0
@@ -192,7 +193,7 @@ const billableProxy = computed({
</template>
</TagDropdown>
<Select v-model="billableProxy">
<SelectTrigger size="small" :show-chevron="false">
<SelectTrigger :show-chevron="false">
<SelectValue class="flex items-center gap-2">
<BillableIcon class="h-4 text-icon-default" />
<span>{{ timeEntry.billable ? 'Billable' : 'Non-Billable' }}</span>
@@ -206,9 +207,9 @@ const billableProxy = computed({
</div>
</div>
<div class="flex flex-col sm:flex-row gap-4 pt-4">
<div class="flex-1">
<InputLabel>Duration</InputLabel>
<div class="space-y-2 mt-1 flex flex-col">
<Field class="flex-1">
<FieldLabel>Duration</FieldLabel>
<div class="space-y-2 flex flex-col">
<DurationHumanInput
v-model:start="localStart"
v-model:end="localEnd"
@@ -222,31 +223,27 @@ const billableProxy = computed({
</span>
</div>
</div>
</div>
</Field>
<div class="grid grid-cols-2 sm:flex gap-4">
<div>
<InputLabel>Start</InputLabel>
<div class="flex flex-col gap-2 mt-1 sm:w-28">
<Field>
<FieldLabel>Start</FieldLabel>
<div class="flex flex-col gap-2 sm:w-28">
<TimePickerSimple
v-model="localStart"
class="w-full"
size="large"></TimePickerSimple>
class="w-full"></TimePickerSimple>
<DatePicker
v-model="localStart"
class="w-full"
tabindex="1"></DatePicker>
</div>
</div>
<div>
<InputLabel>End</InputLabel>
<div class="flex flex-col gap-2 mt-1 sm:w-28">
<TimePickerSimple
v-model="localEnd"
class="w-full"
size="large"></TimePickerSimple>
</Field>
<Field>
<FieldLabel>End</FieldLabel>
<div class="flex flex-col gap-2 sm:w-28">
<TimePickerSimple v-model="localEnd" class="w-full"></TimePickerSimple>
<DatePicker v-model="localEnd" class="w-full" tabindex="1"></DatePicker>
</div>
</div>
</Field>
</div>
</div>
</template>

View File

@@ -5,7 +5,7 @@ import DialogModal from '@/packages/ui/src/DialogModal.vue';
import { computed, nextTick, ref, watch } from 'vue';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import TimeTrackerProjectTaskDropdown from '@/packages/ui/src/TimeTracker/TimeTrackerProjectTaskDropdown.vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel } from '../field';
import { TagIcon } from '@heroicons/vue/20/solid';
import { getLocalizedDayJs } from '@/packages/ui/src/utils/time';
import type {
@@ -24,7 +24,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/Components/ui/select';
import { Button } from '@/Components/ui/button';
import { Button } from '@/packages/ui/src/Buttons';
import DatePicker from '@/packages/ui/src/Input/DatePicker.vue';
import DurationHumanInput from '@/packages/ui/src/Input/DurationHumanInput.vue';
@@ -199,7 +199,7 @@ const billableProxy = computed({
</template>
</TagDropdown>
<Select v-model="billableProxy">
<SelectTrigger size="small" :show-chevron="false">
<SelectTrigger size="sm" :show-chevron="false">
<SelectValue class="flex items-center gap-2">
<BillableIcon class="h-4 text-icon-default" />
<span>{{
@@ -215,9 +215,9 @@ const billableProxy = computed({
</div>
</div>
<div class="flex flex-col sm:flex-row gap-4 pt-4">
<div class="flex-1">
<InputLabel>Duration</InputLabel>
<div class="space-y-2 mt-1 flex flex-col">
<Field class="flex-1">
<FieldLabel>Duration</FieldLabel>
<div class="space-y-2 flex flex-col">
<DurationHumanInput
v-model:start="localStart"
v-model:end="localEnd"
@@ -231,34 +231,32 @@ const billableProxy = computed({
</span>
</div>
</div>
</div>
</Field>
<div class="grid grid-cols-2 sm:flex gap-4">
<div>
<InputLabel>Start</InputLabel>
<div class="flex flex-col gap-2 mt-1 sm:w-28">
<Field>
<FieldLabel>Start</FieldLabel>
<div class="flex flex-col gap-2 sm:w-28">
<TimePickerSimple
v-model="localStart"
class="w-full"
size="large"></TimePickerSimple>
class="w-full"></TimePickerSimple>
<DatePicker
v-model="localStart"
class="w-full"
tabindex="1"></DatePicker>
</div>
</div>
<div>
<InputLabel>End</InputLabel>
<div class="flex flex-col gap-2 mt-1 sm:w-28">
</Field>
<Field>
<FieldLabel>End</FieldLabel>
<div class="flex flex-col gap-2 sm:w-28">
<TimePickerSimple
v-model="localEnd"
class="w-full"
size="large"></TimePickerSimple>
class="w-full"></TimePickerSimple>
<DatePicker
v-model="localEnd"
class="w-full"
tabindex="1"></DatePicker>
</div>
</div>
</Field>
</div>
</div>
</div>

View File

@@ -14,7 +14,8 @@ import type {
} from '@/packages/api/src';
import { ref } from 'vue';
import { twMerge } from 'tailwind-merge';
import { Checkbox, InputLabel } from '@/packages/ui/src';
import { Checkbox } from '@/packages/ui/src';
import { FieldLabel } from '../field';
const props = defineProps<{
selectedTimeEntries: TimeEntry[];
@@ -71,14 +72,14 @@ const showMassUpdateModal = ref(false);
:checked="allSelected"
@update:checked="allSelected ? emit('unselectAll') : emit('selectAll')">
</Checkbox>
<InputLabel
<FieldLabel
v-if="selectedTimeEntries.length > 0"
for="selectAll"
class="select-none text-text-secondary">
{{ selectedTimeEntries.length }} selected
</InputLabel>
<InputLabel v-else for="selectAll" class="text-text-secondary select-none"
>Select All</InputLabel
</FieldLabel>
<FieldLabel v-else for="selectAll" class="text-text-secondary select-none"
>Select All</FieldLabel
>
<button
v-if="selectedTimeEntries.length"

View File

@@ -5,7 +5,7 @@ import DialogModal from '@/packages/ui/src/DialogModal.vue';
import { computed, nextTick, ref, watch } from 'vue';
import PrimaryButton from '../Buttons/PrimaryButton.vue';
import TimeTrackerProjectTaskDropdown from '@/packages/ui/src/TimeTracker/TimeTrackerProjectTaskDropdown.vue';
import InputLabel from '../Input/InputLabel.vue';
import { Field, FieldLabel } from '../field';
import {
type CreateClientBody,
type CreateProjectBody,
@@ -23,7 +23,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/Components/ui/select';
import { Button } from '@/Components/ui/button';
import { Button } from '@/packages/ui/src/Buttons';
import TagDropdown from '@/packages/ui/src/Tag/TagDropdown.vue';
import type { Tag, Task } from '@/packages/api/src';
@@ -146,18 +146,18 @@ watch(removeAllTags, () => {
<template #content>
<div class="space-y-4">
<div class="space-y-2">
<InputLabel for="description" value="Description" />
<Field>
<FieldLabel for="description">Description</FieldLabel>
<TextInput
id="description"
ref="descriptionInput"
v-model="description"
type="text"
class="mt-1 block w-full"
class="block w-full"
@keydown.enter="submit" />
</div>
<div class="space-y-2">
<InputLabel for="project" value="Project" />
</Field>
<Field>
<FieldLabel for="project">Project</FieldLabel>
<TimeTrackerProjectTaskDropdown
v-model:project="projectId"
v-model:task="taskId"
@@ -169,15 +169,14 @@ watch(removeAllTags, () => {
:create-client
:currency="currency"
:can-create-project
class="mt-1"
empty-placeholder="Select project..."
allow-reset
:enable-estimated-time
:projects="projects"
:tasks="tasks"></TimeTrackerProjectTaskDropdown>
</div>
<div class="space-y-2">
<InputLabel for="project" value="Tag" />
</Field>
<Field>
<FieldLabel>Tag</FieldLabel>
<div class="flex space-x-5">
<TagDropdown
v-model="selectedTags"
@@ -194,33 +193,31 @@ watch(removeAllTags, () => {
</Button>
</template>
</TagDropdown>
<div class="flex items-center space-x-2">
<Field orientation="horizontal">
<Checkbox id="no_tags" v-model:checked="removeAllTags"></Checkbox>
<InputLabel for="no_tags" value="Remove all tags" />
</div>
<FieldLabel for="no_tags">Remove all tags</FieldLabel>
</Field>
</div>
</div>
<div class="space-y-2">
<InputLabel for="project" value="Billable" />
<div class="flex">
<Select v-model="timeEntryBillable">
<SelectTrigger>
<SelectValue>
<span v-if="billable === undefined">Set billable status</span>
<span v-else-if="billable === true">Billable</span>
<span v-else>Non Billable</span>
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="do-not-update">
Keep current billable status
</SelectItem>
<SelectItem value="billable">Billable</SelectItem>
<SelectItem value="non-billable">Non Billable</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</Field>
<Field>
<FieldLabel>Billable</FieldLabel>
<Select v-model="timeEntryBillable">
<SelectTrigger>
<SelectValue>
<span v-if="billable === undefined">Set billable status</span>
<span v-else-if="billable === true">Billable</span>
<span v-else>Non Billable</span>
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="do-not-update">
Keep current billable status
</SelectItem>
<SelectItem value="billable">Billable</SelectItem>
<SelectItem value="non-billable">Non Billable</SelectItem>
</SelectContent>
</Select>
</Field>
</div>
</template>
<template #footer>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import type { FieldVariants } from '.';
import { cn } from '@/lib/utils';
import { fieldVariants } from '.';
const props = defineProps<{
class?: HTMLAttributes['class'];
orientation?: FieldVariants['orientation'];
}>();
</script>
<template>
<div
role="group"
data-slot="field"
:data-orientation="orientation"
:class="cn(fieldVariants({ orientation }), props.class)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@/lib/utils';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<div
data-slot="field-content"
:class="cn('group/field-content flex flex-1 flex-col gap-1.5 leading-snug', props.class)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,23 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@/lib/utils';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<p
data-slot="field-description"
:class="
cn(
'text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance',
'last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5',
'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
props.class
)
">
<slot />
</p>
</template>

View File

@@ -0,0 +1,40 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { computed } from 'vue';
import { cn } from '@/lib/utils';
const props = defineProps<{
class?: HTMLAttributes['class'];
errors?: Array<{ message?: string } | undefined>;
}>();
const content = computed(() => {
if (!props.errors || props.errors.length === 0) return null;
if (props.errors.length === 1 && props.errors[0]?.message) {
return props.errors[0].message;
}
return props.errors.some((e) => e?.message) ? props.errors : null;
});
</script>
<template>
<div
v-if="$slots.default || content"
role="alert"
data-slot="field-error"
:class="cn('text-destructive text-sm font-normal', props.class)">
<slot v-if="$slots.default" />
<template v-else-if="typeof content === 'string'">
{{ content }}
</template>
<ul v-else-if="Array.isArray(content)" class="ml-4 flex list-disc flex-col gap-1">
<li v-for="(error, index) in content" :key="index">
{{ error?.message }}
</li>
</ul>
</div>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@/lib/utils';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<div
data-slot="field-group"
:class="
cn(
'group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4',
props.class
)
">
<slot />
</div>
</template>

View File

@@ -0,0 +1,24 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@/lib/utils';
import { Label } from '@/Components/ui/label';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<Label
data-slot="field-label"
:class="
cn(
'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50',
'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&_>[data-slot=field]]:p-3',
'has-[[data-state=checked]]:bg-primary/5 has-[[data-state=checked]]:border-primary dark:has-[[data-state=checked]]:bg-primary/10',
props.class
)
">
<slot />
</Label>
</template>

View File

@@ -0,0 +1,25 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@/lib/utils';
const props = defineProps<{
class?: HTMLAttributes['class'];
variant?: 'legend' | 'label';
}>();
</script>
<template>
<legend
data-slot="field-legend"
:data-variant="variant"
:class="
cn(
'mb-3 font-medium',
'data-[variant=legend]:text-base',
'data-[variant=label]:text-sm',
props.class
)
">
<slot />
</legend>
</template>

View File

@@ -0,0 +1,29 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@/lib/utils';
import { Separator } from '../separator';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<div
data-slot="field-separator"
:data-content="!!$slots.default"
:class="
cn(
'relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2',
props.class
)
">
<Separator class="absolute inset-0 top-1/2" />
<span
v-if="$slots.default"
class="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
data-slot="field-separator-content">
<slot />
</span>
</div>
</template>

View File

@@ -0,0 +1,22 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@/lib/utils';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<fieldset
data-slot="field-set"
:class="
cn(
'flex flex-col gap-6',
'has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',
props.class
)
">
<slot />
</fieldset>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@/lib/utils';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<div
data-slot="field-label"
:class="
cn(
'flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50',
props.class
)
">
<slot />
</div>
</template>

View File

@@ -0,0 +1,39 @@
import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
export const fieldVariants = cva(
'group/field flex w-full gap-3 data-[invalid=true]:text-destructive',
{
variants: {
orientation: {
vertical: ['flex-col [&>*]:w-full [&>.sr-only]:w-auto'],
horizontal: [
'flex-row items-center',
'[&>[data-slot=field-label]]:flex-auto',
'has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
],
responsive: [
'flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto',
'@md/field-group:[&>[data-slot=field-label]]:flex-auto',
'@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
],
},
},
defaultVariants: {
orientation: 'vertical',
},
}
);
export type FieldVariants = VariantProps<typeof fieldVariants>;
export { default as Field } from './Field.vue';
export { default as FieldContent } from './FieldContent.vue';
export { default as FieldDescription } from './FieldDescription.vue';
export { default as FieldError } from './FieldError.vue';
export { default as FieldGroup } from './FieldGroup.vue';
export { default as FieldLabel } from './FieldLabel.vue';
export { default as FieldLegend } from './FieldLegend.vue';
export { default as FieldSeparator } from './FieldSeparator.vue';
export { default as FieldSet } from './FieldSet.vue';
export { default as FieldTitle } from './FieldTitle.vue';

View File

@@ -43,6 +43,21 @@ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from './
import { Popover, PopoverContent, PopoverTrigger, PopoverAnchor } from './popover/index';
import { RangeCalendar } from './range-calendar/index';
import { CommandPalette } from './CommandPalette/index';
import { Separator } from './separator/index';
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
FieldTitle,
fieldVariants,
} from './field/index';
export type { FieldVariants } from './field/index';
export type { ActivityPeriod } from './FullCalendar/idleStatusPlugin';
export type {
CommandPaletteCommand,
@@ -93,4 +108,16 @@ export {
PopoverAnchor,
RangeCalendar,
CommandPalette,
Separator,
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
FieldTitle,
fieldVariants,
};

View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
import type { SeparatorProps } from 'reka-ui';
import type { HTMLAttributes } from 'vue';
import { reactiveOmit } from '@vueuse/core';
import { Separator } from 'reka-ui';
import { cn } from '@/lib/utils';
const props = withDefaults(defineProps<SeparatorProps & { class?: HTMLAttributes['class'] }>(), {
orientation: 'horizontal',
decorative: true,
});
const delegatedProps = reactiveOmit(props, 'class');
</script>
<template>
<Separator
v-bind="delegatedProps"
:class="
cn(
'shrink-0 bg-border',
props.orientation === 'horizontal' ? 'h-px w-full' : 'w-px h-full',
props.class
)
" />
</template>

View File

@@ -0,0 +1 @@
export { default as Separator } from './Separator.vue';

View File

@@ -26,6 +26,7 @@ export function useAggregatedTimeEntriesQuery(
queries: unref(filterParams),
}),
enabled: computed(() => !!getCurrentOrganizationId()),
placeholderData: (previousData) => previousData,
staleTime: 1000 * 30,
});