Add Field component system and migrate UI

This commit is contained in:
Gregor Vostrak
2026-02-10 12:22:53 +01:00
parent 125f6f062f
commit 15e61e9789
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.getByLabel('Password').fill('wrongpassword123');
await page.getByRole('button', { name: 'Log in' }).click(); await page.getByRole('button', { name: 'Log in' }).click();
await expect( await expect(page.getByText('These credentials do not match our records.')).toBeVisible();
page.getByText('These credentials do not match our records.')
).toBeVisible();
}); });
test('shows error when registering with existing email', async ({ page }) => { 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); 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 }) => { test('test that user can delete an API key', async ({ page }) => {
await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile'); await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile');
await createNewApiToken(page); 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') page.getByRole('button', { name: 'Projects' }).first().getByText('1')
).toBeVisible(); ).toBeVisible();
// Deselect project // Deselect project (no network request expected — TanStack Query serves cached unfiltered data)
await page.getByRole('button', { name: 'Projects' }).first().click(); await page.getByRole('button', { name: 'Projects' }).first().click();
await Promise.all([ await page.getByRole('option').filter({ hasText: project1Name }).click();
page.getByRole('option').filter({ hasText: project1Name }).click(),
waitForReportingUpdate(page),
]);
await page.keyboard.press('Escape'); await page.keyboard.press('Escape');
// Verify badge count is gone (no count displayed when 0) // 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') page.getByRole('button', { name: 'Clients' }).first().getByText('1')
).toBeVisible(); ).toBeVisible();
// Deselect client // Deselect client (no network request expected — TanStack Query serves cached unfiltered data)
await page.getByRole('button', { name: 'Clients' }).first().click(); await page.getByRole('button', { name: 'Clients' }).first().click();
await Promise.all([ await page.getByRole('option').filter({ hasText: client1Name }).click();
page.getByRole('option').filter({ hasText: client1Name }).click(),
waitForReportingUpdate(page),
]);
await page.keyboard.press('Escape'); await page.keyboard.press('Escape');
await expect( 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') page.getByRole('button', { name: 'Members' }).first().getByText('1')
).toBeVisible(); ).toBeVisible();
// Deselect member // Deselect member (no network request expected — TanStack Query serves cached unfiltered data)
await page.getByRole('button', { name: 'Members' }).first().click(); await page.getByRole('button', { name: 'Members' }).first().click();
await Promise.all([ await page.getByRole('option').filter({ hasText: 'John Doe' }).click();
page.getByRole('option').filter({ hasText: 'John Doe' }).click(),
waitForReportingUpdate(page),
]);
await page.keyboard.press('Escape'); await page.keyboard.press('Escape');
// Verify badge count is gone // 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(); 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 page.getByRole('button', { name: 'Tags' }).click();
await Promise.all([ await page.getByRole('option').filter({ hasText: tag1Name }).click();
page.getByRole('option').filter({ hasText: tag1Name }).click(),
waitForReportingUpdate(page),
]);
await page.keyboard.press('Escape'); await page.keyboard.press('Escape');
await expect(page.getByRole('button', { name: 'Tags' }).getByText(/^\d+$/)).not.toBeVisible(); await expect(page.getByRole('button', { name: 'Tags' }).getByText(/^\d+$/)).not.toBeVisible();

View File

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

View File

@@ -78,4 +78,4 @@ export async function getPasswordResetUrl(
expect(resetUrlMatch).toBeTruthy(); expect(resetUrlMatch).toBeTruthy();
return resetUrlMatch![1].replace(/&/g, '&'); 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 PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import { useFocus } from '@vueuse/core'; import { useFocus } from '@vueuse/core';
import { useClientsStore } from '@/utils/useClients'; 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 { createClient } = useClientsStore();
const show = defineModel('show', { default: false }); const show = defineModel('show', { default: false });
@@ -37,19 +37,19 @@ useFocus(clientNameInput, { initialValue: true });
<template #content> <template #content>
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<div class="col-span-6 sm:col-span-4 flex-1"> <Field class="col-span-6 sm:col-span-4 flex-1">
<InputLabel for="clientName" value="Client Name" /> <FieldLabel for="clientName">Client Name</FieldLabel>
<TextInput <TextInput
id="clientName" id="clientName"
ref="clientNameInput" ref="clientNameInput"
v-model="client.name" v-model="client.name"
type="text" type="text"
placeholder="Client Name" placeholder="Client Name"
class="mt-1 block w-full" class="block w-full"
required required
autocomplete="clientName" autocomplete="clientName"
@keydown.enter="submit" /> @keydown.enter="submit" />
</div> </Field>
</div> </div>
</template> </template>
<template #footer> <template #footer>

View File

@@ -14,7 +14,7 @@ import {
ComboboxViewport, ComboboxViewport,
} from 'radix-vue'; } from 'radix-vue';
import Dropdown from '@/packages/ui/src/Input/Dropdown.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(); const { members } = useMembersQuery();
@@ -77,7 +77,6 @@ function selectMember(member: Member) {
:disabled="disabled" :disabled="disabled"
type="button" type="button"
variant="input" variant="input"
size="input"
class="w-full justify-between text-start font-normal"> class="w-full justify-between text-start font-normal">
<div class="flex items-center gap-3 truncate"> <div class="flex items-center gap-3 truncate">
<UserIcon class="w-4 text-text-secondary shrink-0" /> <UserIcon class="w-4 text-text-secondary shrink-0" />
@@ -92,7 +91,7 @@ function selectMember(member: Member) {
<template #content> <template #content>
<ComboboxRoot <ComboboxRoot
v-model:search-term="searchValue" v-model:search-term="searchValue"
:open="open" v-model:open="open"
class="relative" class="relative"
:filter-function="(val: string[]) => val"> :filter-function="(val: string[]) => val">
<ComboboxAnchor> <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 Checkbox from '@/packages/ui/src/Input/Checkbox.vue';
import { useNotificationsStore } from '@/utils/notification'; import { useNotificationsStore } from '@/utils/notification';
import { getCurrentOrganizationId } from '@/utils/useUser'; import { getCurrentOrganizationId } from '@/utils/useUser';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue'; import { Field, FieldLabel, FieldError } from '@/packages/ui/src/field';
import InputError from '@/packages/ui/src/Input/InputError.vue';
const props = defineProps<{ const props = defineProps<{
show: boolean; show: boolean;
@@ -113,25 +112,21 @@ const close = () => {
}, },
}"> }">
<template #default="{ field }"> <template #default="{ field }">
<div class="flex flex-col"> <Field orientation="horizontal">
<div class="flex items-center space-x-3 text-sm"> <Checkbox
<Checkbox :id="field.name"
:id="field.name" :name="field.name"
:name="field.name" :checked="field.state.value"
:checked="field.state.value" @update:checked="field.handleChange"
@update:checked="field.handleChange" @blur="field.handleBlur" />
@blur="field.handleBlur" /> <FieldLabel :for="field.name" class="font-medium text-text-primary">
<InputLabel I understand that this will permanently delete all data related
:for="field.name" to this member
class="font-medium text-text-primary"> </FieldLabel>
I understand that this will permanently delete all data <FieldError v-if="field.state.meta.errors[0]" class="pl-7 pt-2">
related to this member {{ field.state.meta.errors[0] }}
</InputLabel> </FieldError>
</div> </Field>
<InputError
class="pl-7 pt-2"
:message="field.state.meta.errors[0]" />
</div>
</template> </template>
</form.Field> </form.Field>
</div> </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 PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import { type MemberBillableKey, useMembersStore } from '@/utils/useMembers'; import { type MemberBillableKey, useMembersStore } from '@/utils/useMembers';
import BillableRateInput from '@/packages/ui/src/Input/BillableRateInput.vue'; 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 MemberBillableRateModal from '@/Components/Common/Member/MemberBillableRateModal.vue';
import MemberBillableSelect from '@/Components/Common/Member/MemberBillableSelect.vue'; import MemberBillableSelect from '@/Components/Common/Member/MemberBillableSelect.vue';
import { onMounted, watch } from 'vue'; import { onMounted, watch } from 'vue';
@@ -121,28 +121,24 @@ const roleDescription = computed(() => {
<template #content> <template #content>
<div class="pb-5 pt-2 divide-y divide-border-secondary"> <div class="pb-5 pt-2 divide-y divide-border-secondary">
<div class="pb-5 flex space-x-6"> <div class="pb-5 flex space-x-6">
<div> <Field>
<InputLabel for="role" value="Role" /> <FieldLabel for="role">Role</FieldLabel>
<MemberRoleSelect <MemberRoleSelect v-model="memberBody.role" name="role"></MemberRoleSelect>
v-model="memberBody.role" </Field>
class="mt-2"
name="role"></MemberRoleSelect>
</div>
<div class="flex-1 text-xs flex items-center pt-6"> <div class="flex-1 text-xs flex items-center pt-6">
<p>{{ roleDescription }}</p> <p>{{ roleDescription }}</p>
</div> </div>
</div> </div>
<div class="flex items-center space-x-4 pt-5"> <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 class="col-span-6 sm:col-span-4 flex-1 flex space-x-5">
<div> <Field>
<InputLabel for="billableType" value="Billable" /> <FieldLabel for="billableType">Billable</FieldLabel>
<MemberBillableSelect <MemberBillableSelect
v-model="billableRateSelect" v-model="billableRateSelect"
class="mt-2"
name="billableType"></MemberBillableSelect> name="billableType"></MemberBillableSelect>
</div> </Field>
<div v-if="billableRateSelect === 'custom-rate'" class="flex-1"> <Field v-if="billableRateSelect === 'custom-rate'" class="flex-1">
<InputLabel for="memberBillableRate" value="Billable Rate" /> <FieldLabel for="memberBillableRate">Billable Rate</FieldLabel>
<BillableRateInput <BillableRateInput
v-model="memberBody.billable_rate" v-model="memberBody.billable_rate"
focus focus
@@ -150,7 +146,7 @@ const roleDescription = computed(() => {
:currency="getOrganizationCurrencyString()" :currency="getOrganizationCurrencyString()"
name="memberBillableRate" name="memberBillableRate"
@keydown.enter="saveWithChecks()"></BillableRateInput> @keydown.enter="saveWithChecks()"></BillableRateInput>
</div> </Field>
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ import { useProjectMembersStore } from '@/utils/useProjectMembers';
import BillableRateInput from '@/packages/ui/src/Input/BillableRateInput.vue'; import BillableRateInput from '@/packages/ui/src/Input/BillableRateInput.vue';
import { UserIcon } from '@heroicons/vue/24/solid'; import { UserIcon } from '@heroicons/vue/24/solid';
import ProjectMemberBillableRateModal from '@/Components/Common/ProjectMember/ProjectMemberBillableRateModal.vue'; 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'; import { getOrganizationCurrencyString } from '@/utils/money';
const { updateProjectMember } = useProjectMembersStore(); const { updateProjectMember } = useProjectMembersStore();
@@ -82,14 +82,14 @@ useFocus(projectNameInput, { initialValue: true });
<UserIcon class="w-4 text-text-secondary"></UserIcon> <UserIcon class="w-4 text-text-secondary"></UserIcon>
<span>{{ props.name }}</span> <span>{{ props.name }}</span>
</div> </div>
<div class="col-span-3 sm:col-span-1 flex-1"> <Field class="col-span-3 sm:col-span-1 flex-1">
<InputLabel for="billable_rate" class="mb-2" value="Billable Rate"></InputLabel> <FieldLabel for="billable_rate">Billable Rate</FieldLabel>
<BillableRateInput <BillableRateInput
v-model="projectMemberBody.billable_rate" v-model="projectMemberBody.billable_rate"
:currency="getOrganizationCurrencyString()" :currency="getOrganizationCurrencyString()"
name="billable_rate" name="billable_rate"
@keydown.enter="submit"></BillableRateInput> @keydown.enter="submit"></BillableRateInput>
</div> </Field>
</div> </div>
</template> </template>
<template #footer> <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 DialogModal from '@/packages/ui/src/DialogModal.vue';
import { ref } from 'vue'; import { ref } from 'vue';
import PrimaryButton from '../../../packages/ui/src/Buttons/PrimaryButton.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 type { CreateReportBody, CreateReportBodyProperties } from '@/packages/api/src';
import { useMutation } from '@tanstack/vue-query'; import { useMutation } from '@tanstack/vue-query';
import { getCurrentOrganizationId } from '@/utils/useUser'; import { getCurrentOrganizationId } from '@/utils/useUser';
@@ -82,31 +82,33 @@ async function submit() {
<template #content> <template #content>
<div class="items-center space-y-4 w-full"> <div class="items-center space-y-4 w-full">
<div class="w-full"> <Field class="w-full">
<InputLabel for="name" value="Name" /> <FieldLabel for="name">Name</FieldLabel>
<TextInput id="name" v-model="report.name" class="mt-1.5 w-full"></TextInput> <TextInput id="name" v-model="report.name" class="w-full"></TextInput>
</div> </Field>
<div> <Field>
<InputLabel for="description" value="Description" /> <FieldLabel for="description">Description</FieldLabel>
<TextInput <TextInput
id="description" id="description"
v-model="report.description" v-model="report.description"
class="mt-1.5 w-full"></TextInput> class="w-full"></TextInput>
</div> </Field>
<InputLabel value="Visibility" /> <Field>
<div class="flex items-center space-x-12"> <FieldLabel>Visibility</FieldLabel>
<div class="flex items-center space-x-3 px-2 py-3"> <div class="flex items-center space-x-12">
<Checkbox id="is_public" v-model:checked="report.is_public"></Checkbox> <Field orientation="horizontal" class="px-2 py-3">
<InputLabel for="is_public" value="Public" /> <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>
<div v-if="report.is_public" class="flex items-center space-x-4"> </Field>
<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>
</div> </div>
</template> </template>
<template #footer> <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 DialogModal from '@/packages/ui/src/DialogModal.vue';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import PrimaryButton from '../../../packages/ui/src/Buttons/PrimaryButton.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 type { UpdateReportBody } from '@/packages/api/src';
import { useMutation, useQueryClient } from '@tanstack/vue-query'; import { useMutation, useQueryClient } from '@tanstack/vue-query';
import { getCurrentOrganizationId } from '@/utils/useUser'; import { getCurrentOrganizationId } from '@/utils/useUser';
@@ -105,28 +105,30 @@ async function submit() {
<template #content> <template #content>
<div class="items-center space-y-4 w-full"> <div class="items-center space-y-4 w-full">
<div class="w-full"> <Field class="w-full">
<InputLabel for="name" value="Name" /> <FieldLabel for="name">Name</FieldLabel>
<TextInput id="name" v-model="report.name" class="mt-1.5 w-full"></TextInput> <TextInput id="name" v-model="report.name" class="w-full"></TextInput>
</div> </Field>
<div> <Field>
<InputLabel for="description" value="Description" /> <FieldLabel for="description">Description</FieldLabel>
<TextInput <TextInput
id="description" id="description"
v-model="report.description" v-model="report.description"
class="mt-1.5 w-full"></TextInput> class="w-full"></TextInput>
</div> </Field>
<InputLabel value="Visibility" /> <Field>
<div class="flex items-center space-x-12"> <FieldLabel>Visibility</FieldLabel>
<div class="flex items-center space-x-2 px-2 py-3"> <div class="flex items-center space-x-12">
<Checkbox id="is_public" v-model:checked="report.is_public"></Checkbox> <Field orientation="horizontal" class="px-2 py-3">
<InputLabel for="is_public" value="Public" /> <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>
<div v-if="report.is_public" class="flex items-center space-x-4"> </Field>
<InputLabel for="public_until" value="Expires at" />
<DatePicker v-model="localPublicUntil"></DatePicker>
</div>
</div>
</div> </div>
</template> </template>
<template #footer> <template #footer>

View File

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

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, nextTick } from 'vue'; import { ref, reactive, nextTick } from 'vue';
import DialogModal from '@/packages/ui/src/DialogModal.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 PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue'; import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
import TextInput from '@/packages/ui/src/Input/TextInput.vue'; import TextInput from '@/packages/ui/src/Input/TextInput.vue';
@@ -87,18 +87,18 @@ const closeModal = () => {
<template #content> <template #content>
{{ content }} {{ content }}
<div class="mt-4"> <Field class="mt-4">
<TextInput <TextInput
ref="passwordInput" ref="passwordInput"
v-model="form.password" v-model="form.password"
type="password" type="password"
class="mt-1 block w-3/4" class="block w-3/4"
placeholder="Password" placeholder="Password"
autocomplete="current-password" autocomplete="current-password"
@keyup.enter="confirmPassword" /> @keyup.enter="confirmPassword" />
<InputError :message="form.error" class="mt-2" /> <FieldError v-if="form.error">{{ form.error }}</FieldError>
</div> </Field>
</template> </template>
<template #footer> <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> <PopoverTrigger as-child>
<Button <Button
variant="input" variant="input"
size="input"
:class="[ :class="[
'w-full justify-start text-left font-normal', 'w-full justify-start text-left font-normal',
!model && 'text-muted-foreground', !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" data-slot="input"
:class=" :class="
cn( 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 props.class
) )
" /> " />

View File

@@ -7,7 +7,7 @@ import { computed, type HTMLAttributes } from 'vue';
const props = withDefaults( const props = withDefaults(
defineProps< defineProps<
SelectTriggerProps & { SelectTriggerProps & {
size?: 'small'; size?: 'default' | 'sm' | 'lg';
class?: HTMLAttributes['class']; class?: HTMLAttributes['class'];
showChevron?: boolean; showChevron?: boolean;
variant?: 'default' | 'outline'; variant?: 'default' | 'outline';
@@ -18,11 +18,19 @@ const props = withDefaults(
showChevron: true, showChevron: true,
variant: 'default', variant: 'default',
active: false, active: false,
size: 'default',
} }
); );
const delegatedProps = computed(() => { const delegatedProps = computed(() => {
const { class: _, showChevron: __, variant: ___, active: ____, ...delegated } = props; const {
class: _,
showChevron: __,
variant: ___,
active: ____,
size: _____,
...delegated
} = props;
return delegated; return delegated;
}); });
@@ -30,7 +38,14 @@ const delegatedProps = computed(() => {
const forwardedProps = useForwardProps(delegatedProps); const forwardedProps = useForwardProps(delegatedProps);
const sizeClasses = computed(() => { 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(() => { const variantClasses = computed(() => {
@@ -49,7 +64,7 @@ const variantClasses = computed(() => {
v-bind="forwardedProps" v-bind="forwardedProps"
:class=" :class="
cn( 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, sizeClasses,
variantClasses, variantClasses,
props.class props.class

View File

@@ -8,8 +8,7 @@ import ConfirmationModal from '@/Components/ConfirmationModal.vue';
import DangerButton from '@/packages/ui/src/Buttons/DangerButton.vue'; import DangerButton from '@/packages/ui/src/Buttons/DangerButton.vue';
import DialogModal from '@/packages/ui/src/DialogModal.vue'; import DialogModal from '@/packages/ui/src/DialogModal.vue';
import FormSection from '@/Components/FormSection.vue'; import FormSection from '@/Components/FormSection.vue';
import InputError from '@/packages/ui/src/Input/InputError.vue'; import { Field, FieldLabel, FieldError } from '@/packages/ui/src/field';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue'; import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue'; import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
import SectionBorder from '@/Components/SectionBorder.vue'; import SectionBorder from '@/Components/SectionBorder.vue';
@@ -96,20 +95,22 @@ const deleteApiToken = () => {
<template #form> <template #form>
<!-- Token Name --> <!-- Token Name -->
<div class="col-span-6 sm:col-span-4"> <Field class="col-span-6 sm:col-span-4">
<InputLabel for="name" value="Name" /> <FieldLabel for="name">Name</FieldLabel>
<TextInput <TextInput
id="name" id="name"
v-model="createApiTokenForm.name" v-model="createApiTokenForm.name"
type="text" type="text"
class="mt-1 block w-full" class="block w-full"
autofocus /> autofocus />
<InputError :message="createApiTokenForm.errors.name" class="mt-2" /> <FieldError v-if="createApiTokenForm.errors.name">{{
</div> createApiTokenForm.errors.name
}}</FieldError>
</Field>
<!-- Token Permissions --> <!-- Token Permissions -->
<div v-if="availablePermissions.length > 0" class="col-span-6"> <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 class="mt-2 grid grid-cols-1 md:grid-cols-2 gap-4">
<div v-for="permission in availablePermissions" :key="permission"> <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 { Head, useForm } from '@inertiajs/vue3';
import AuthenticationCard from '@/Components/AuthenticationCard.vue'; import AuthenticationCard from '@/Components/AuthenticationCard.vue';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.vue'; import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.vue';
import InputError from '@/packages/ui/src/Input/InputError.vue'; import { Field, FieldLabel, FieldError } from '@/packages/ui/src/field';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue'; import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import TextInput from '@/packages/ui/src/Input/TextInput.vue'; import TextInput from '@/packages/ui/src/Input/TextInput.vue';
@@ -39,19 +38,19 @@ const submit = () => {
</div> </div>
<form @submit.prevent="submit"> <form @submit.prevent="submit">
<div> <Field>
<InputLabel for="password" value="Password" /> <FieldLabel for="password">Password</FieldLabel>
<TextInput <TextInput
id="password" id="password"
ref="passwordInput" ref="passwordInput"
v-model="form.password" v-model="form.password"
type="password" type="password"
class="mt-1 block w-full" class="block w-full"
required required
autocomplete="current-password" autocomplete="current-password"
autofocus /> autofocus />
<InputError class="mt-2" :message="form.errors.password" /> <FieldError v-if="form.errors.password">{{ form.errors.password }}</FieldError>
</div> </Field>
<div class="flex justify-end mt-4"> <div class="flex justify-end mt-4">
<PrimaryButton <PrimaryButton

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import { computed, onMounted, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
import { useNotificationsStore } from '@/utils/notification'; import { useNotificationsStore } from '@/utils/notification';
import { api } from '@/packages/api/src'; 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 { DocumentIcon } from '@heroicons/vue/24/solid';
import { ArrowDownOnSquareIcon, InformationCircleIcon } from '@heroicons/vue/24/outline'; import { ArrowDownOnSquareIcon, InformationCircleIcon } from '@heroicons/vue/24/outline';
@@ -163,13 +163,13 @@ const showResultModal = ref(false);
<Card> <Card>
<div class="px-4 py-5 sm:px-5"> <div class="px-4 py-5 sm:px-5">
<div> <Field>
<InputLabel for="importType" value="Import Type" /> <FieldLabel for="importType">Import Type</FieldLabel>
<select <select
id="importType" id="importType"
v-model="importType" v-model="importType"
name="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> <option :value="null" selected disabled>
Select an import type to get instructions... Select an import type to get instructions...
</option> </option>
@@ -184,7 +184,7 @@ const showResultModal = ref(false);
<div class="font-semibold text-text-secondary py-1">Instructions:</div> <div class="font-semibold text-text-secondary py-1">Instructions:</div>
<div class="max-w-2xl" v-html="currentImporterDescription"></div> <div class="max-w-2xl" v-html="currentImporterDescription"></div>
</div> </div>
</div> </Field>
<div <div
class="mt-2 flex justify-center rounded-lg border border-dashed border-border-primary px-6 py-10"> 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 FormSection from '@/Components/FormSection.vue';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue'; import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import { onMounted, ref } from '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 type { UpdateOrganizationBody } from '@/packages/api/src';
import BillableRateInput from '@/packages/ui/src/Input/BillableRateInput.vue'; import BillableRateInput from '@/packages/ui/src/Input/BillableRateInput.vue';
import { useOrganizationStore } from '@/utils/useOrganization'; import { useOrganizationStore } from '@/utils/useOrganization';
@@ -61,35 +61,27 @@ function checkForConfirmationModal() {
v-model:show="showConfirmationModal" v-model:show="showConfirmationModal"
:new-billable-rate="organizationBody.billable_rate" :new-billable-rate="organizationBody.billable_rate"
@submit="submit"></OrganizationBillableRateModal> @submit="submit"></OrganizationBillableRateModal>
<!-- Organization Owner Information --> <Field class="col-span-6 sm:col-span-4">
<div class="col-span-6"> <FieldLabel for="organizationBillableRate">Organization Billable Rate</FieldLabel>
<div class="col-span-6 sm:col-span-4"> <BillableRateInput
<InputLabel v-if="organization"
for="organizationBillableRate" v-model="organizationBody.billable_rate"
class="mb-2" :currency="getOrganizationCurrencyString()"
value="Organization Billable Rate" /> name="organizationBillableRate"></BillableRateInput>
<BillableRateInput </Field>
v-if="organization"
v-model="organizationBody.billable_rate"
:currency="getOrganizationCurrencyString()"
name="organizationBillableRate"></BillableRateInput>
</div>
</div>
<div class="col-span-6"> <div class="col-span-6 sm:col-span-4">
<div class="col-span-6 sm:col-span-4"> <Field orientation="horizontal">
<div class="flex items-center space-x-2"> <Checkbox
<Checkbox v-if="organization"
v-if="organization" id="organizationShowBillableRatesToEmployees"
id="organizationShowBillableRatesToEmployees" v-model:checked="
v-model:checked=" organizationBody.employees_can_see_billable_rates
organizationBody.employees_can_see_billable_rates "></Checkbox>
"></Checkbox> <FieldLabel for="organizationShowBillableRatesToEmployees"
<InputLabel >Show Billable Rates to Employees</FieldLabel
for="organizationShowBillableRatesToEmployees" >
value="Show Billable Rates to Employees" /> </Field>
</div>
</div>
</div> </div>
</template> </template>
<template #actions> <template #actions>

View File

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

View File

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

View File

@@ -7,8 +7,8 @@ import ConfirmationModal from '@/Components/ConfirmationModal.vue';
import DangerButton from '@/packages/ui/src/Buttons/DangerButton.vue'; import DangerButton from '@/packages/ui/src/Buttons/DangerButton.vue';
import DialogModal from '@/packages/ui/src/DialogModal.vue'; import DialogModal from '@/packages/ui/src/DialogModal.vue';
import FormSection from '@/Components/FormSection.vue'; import FormSection from '@/Components/FormSection.vue';
import InputError from '@/packages/ui/src/Input/InputError.vue'; import { Field, FieldLabel, FieldError } from '@/packages/ui/src/field';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue'; import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue'; import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
import SectionBorder from '@/Components/SectionBorder.vue'; import SectionBorder from '@/Components/SectionBorder.vue';
@@ -139,20 +139,24 @@ const displayableRole = (role: string) => {
</div> </div>
<!-- Member Email --> <!-- Member Email -->
<div class="col-span-6 sm:col-span-4"> <Field class="col-span-6 sm:col-span-4">
<InputLabel for="email" value="Email" /> <FieldLabel for="email">Email</FieldLabel>
<TextInput <TextInput
id="email" id="email"
v-model="addTeamMemberForm.email" v-model="addTeamMemberForm.email"
type="email" type="email"
class="mt-1 block w-full" /> class="block w-full" />
<InputError :message="addTeamMemberForm.errors.email" class="mt-2" /> <FieldError v-if="addTeamMemberForm.errors.email">{{
</div> addTeamMemberForm.errors.email
}}</FieldError>
</Field>
<!-- Role --> <!-- Role -->
<div v-if="availableRoles.length > 0" class="col-span-6 lg:col-span-4"> <div v-if="availableRoles.length > 0" class="col-span-6 lg:col-span-4">
<InputLabel for="roles" value="Role" /> <FieldLabel for="roles">Role</FieldLabel>
<InputError :message="addTeamMemberForm.errors.role" class="mt-2" /> <FieldError v-if="addTeamMemberForm.errors.role">{{
addTeamMemberForm.errors.role
}}</FieldError>
<div <div
class="relative z-0 mt-1 border border-card-border rounded-lg cursor-pointer"> 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 { Link, useForm } from '@inertiajs/vue3';
import ActionMessage from '@/Components/ActionMessage.vue'; import ActionMessage from '@/Components/ActionMessage.vue';
import FormSection from '@/Components/FormSection.vue'; import FormSection from '@/Components/FormSection.vue';
import InputError from '@/packages/ui/src/Input/InputError.vue'; import { Field, FieldLabel, FieldError } from '@/packages/ui/src/field';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue'; import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import TextInput from '@/packages/ui/src/Input/TextInput.vue'; import TextInput from '@/packages/ui/src/Input/TextInput.vue';
import type { Organization } from '@/types/models'; import type { Organization } from '@/types/models';
@@ -40,7 +39,7 @@ const updateTeamName = () => {
<!-- Organization Owner Information --> <!-- Organization Owner Information -->
<div class="col-span-6 flex items-center justify-between"> <div class="col-span-6 flex items-center justify-between">
<div class=""> <div class="">
<InputLabel value="Organization Owner" /> <FieldLabel>Organization Owner</FieldLabel>
<div class="flex items-center mt-2"> <div class="flex items-center mt-2">
<img <img
@@ -68,28 +67,28 @@ const updateTeamName = () => {
</div> </div>
<!-- Organization Name --> <!-- Organization Name -->
<div class="col-span-6 sm:col-span-4"> <Field class="col-span-6 sm:col-span-4">
<InputLabel for="name" value="Organization Name" /> <FieldLabel for="name">Organization Name</FieldLabel>
<TextInput <TextInput
id="name" id="name"
v-model="form.name" v-model="form.name"
type="text" type="text"
class="mt-1 block w-full" class="block w-full"
:disabled="!permissions.canUpdateTeam" /> :disabled="!permissions.canUpdateTeam" />
<InputError :message="form.errors.name" class="mt-2" /> <FieldError v-if="form.errors.name">{{ form.errors.name }}</FieldError>
</div> </Field>
<!-- Currency --> <!-- Currency -->
<div class="col-span-6 sm:col-span-4"> <Field class="col-span-6 sm:col-span-4">
<InputLabel for="currency" value="Currency" /> <FieldLabel for="currency">Currency</FieldLabel>
<select <select
id="currency" id="currency"
v-model="form.currency" v-model="form.currency"
name="currency" name="currency"
:disabled="!permissions.canUpdateTeam" :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 value="" disabled>Select a currency</option>
<option <option
v-for="(currencyTranslated, currencyKey) in $page.props.currencies" v-for="(currencyTranslated, currencyKey) in $page.props.currencies"
@@ -98,8 +97,8 @@ const updateTeamName = () => {
{{ currencyKey }} - {{ currencyTranslated }} {{ currencyKey }} - {{ currencyTranslated }}
</option> </option>
</select> </select>
<InputError :message="form.errors.currency" class="mt-2" /> <FieldError v-if="form.errors.currency">{{ form.errors.currency }}</FieldError>
</div> </Field>
</template> </template>
<template v-if="permissions.canUpdateTeam" #actions> <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', 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: { size: {
default: 'h-9 px-4 py-2', default: 'h-9 px-3 text-sm',
xs: 'h-7 rounded px-2', xs: 'h-7 rounded px-2 text-xs',
sm: 'h-8 rounded-md px-3 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', icon: 'h-9 w-9',
input: 'h-[42px] px-3 py-2 text-base',
}, },
}, },
defaultVariants: { defaultVariants: {

View File

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

View File

@@ -75,12 +75,12 @@ const emit = defineEmits(['update:modelValue', 'changed', 'submit']);
<template #content> <template #content>
<ComboboxRoot <ComboboxRoot
v-model:search-term="searchValue" v-model:search-term="searchValue"
:open="open" v-model:open="open"
class="p-2" class="p-2"
:filter-function="(val: string[]) => val"> :filter-function="(val: string[]) => val">
<ComboboxAnchor> <ComboboxAnchor>
<ComboboxInput <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" /> :placeholder="searchPlaceholder" />
</ComboboxAnchor> </ComboboxAnchor>
<ComboboxContent <ComboboxContent

View File

@@ -25,7 +25,7 @@ const model = defineModel();
v-model="model" v-model="model"
:class=" :class="
twMerge( 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 props.class
) )
" "

View File

@@ -3,7 +3,6 @@ import { ref, watch } from 'vue';
import { getLocalizedDayJs } from '@/packages/ui/src/utils/time'; import { getLocalizedDayJs } from '@/packages/ui/src/utils/time';
import { useFocus } from '@vueuse/core'; import { useFocus } from '@vueuse/core';
import { TextInput } from '@/packages/ui/src'; import { TextInput } from '@/packages/ui/src';
import { twMerge } from 'tailwind-merge';
// This has to be a localized timestamp, not UTC // This has to be a localized timestamp, not UTC
const model = defineModel<string | null>({ const model = defineModel<string | null>({
@@ -12,11 +11,9 @@ const model = defineModel<string | null>({
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
size?: 'base' | 'large';
focus?: boolean; focus?: boolean;
}>(), }>(),
{ {
size: 'base',
focus: false, focus: false,
} }
); );
@@ -101,7 +98,7 @@ const inputValue = ref(model.value ? getLocalizedDayJs(model.value).format('HH:m
v-bind="$attrs" v-bind="$attrs"
ref="timeInput" ref="timeInput"
v-model="inputValue" 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" data-testid="time_picker_input"
type="text" type="text"
@blur="updateTime" @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 ProjectColorSelector from '@/packages/ui/src/Project/ProjectColorSelector.vue';
import { UserCircleIcon } from '@heroicons/vue/20/solid'; import { UserCircleIcon } from '@heroicons/vue/20/solid';
import EstimatedTimeSection from '@/packages/ui/src/EstimatedTimeSection.vue'; 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 ProjectEditBillableSection from '@/packages/ui/src/Project/ProjectEditBillableSection.vue';
import type { Client } from '@/packages/api/src'; import type { Client } from '@/packages/api/src';
@@ -76,14 +76,12 @@ const currentClientName = computed(() => {
<template #content> <template #content>
<div class="sm:flex items-center space-y-2 sm:space-y-0 sm:space-x-4"> <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="flex-1 flex items-center">
<div class="text-center pr-5"> <Field class="text-center pr-5">
<InputLabel for="color" value="Color" /> <FieldLabel for="color">Color</FieldLabel>
<ProjectColorSelector <ProjectColorSelector v-model="project.color"></ProjectColorSelector>
v-model="project.color" </Field>
class="mt-2.5"></ProjectColorSelector> <Field class="w-full">
</div> <FieldLabel for="projectName">Project name</FieldLabel>
<div class="w-full">
<InputLabel for="projectName" value="Project name" />
<TextInput <TextInput
id="projectName" id="projectName"
ref="projectNameInput" ref="projectNameInput"
@@ -91,19 +89,18 @@ const currentClientName = computed(() => {
name="projectName" name="projectName"
type="text" type="text"
placeholder="The next big thing" placeholder="The next big thing"
class="mt-2 block w-full" class="block w-full"
required required
autocomplete="projectName" autocomplete="projectName"
@keydown.enter="submit()" /> @keydown.enter="submit()" />
</div> </Field>
</div> </div>
<div> <Field>
<InputLabel for="client" value="Client" /> <FieldLabel for="client">Client</FieldLabel>
<ClientDropdown <ClientDropdown
v-model="project.client_id" v-model="project.client_id"
:create-client="createClient" :create-client="createClient"
:clients="activeClients" :clients="activeClients">
class="mt-2">
<template #trigger> <template #trigger>
<Badge <Badge
tag="button" tag="button"
@@ -118,7 +115,7 @@ const currentClientName = computed(() => {
</Badge> </Badge>
</template> </template>
</ClientDropdown> </ClientDropdown>
</div> </Field>
</div> </div>
<div> <div>
<div> <div>

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,8 @@ import type {
} from '@/packages/api/src'; } from '@/packages/api/src';
import { ref } from 'vue'; import { ref } from 'vue';
import { twMerge } from 'tailwind-merge'; 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<{ const props = defineProps<{
selectedTimeEntries: TimeEntry[]; selectedTimeEntries: TimeEntry[];
@@ -71,14 +72,14 @@ const showMassUpdateModal = ref(false);
:checked="allSelected" :checked="allSelected"
@update:checked="allSelected ? emit('unselectAll') : emit('selectAll')"> @update:checked="allSelected ? emit('unselectAll') : emit('selectAll')">
</Checkbox> </Checkbox>
<InputLabel <FieldLabel
v-if="selectedTimeEntries.length > 0" v-if="selectedTimeEntries.length > 0"
for="selectAll" for="selectAll"
class="select-none text-text-secondary"> class="select-none text-text-secondary">
{{ selectedTimeEntries.length }} selected {{ selectedTimeEntries.length }} selected
</InputLabel> </FieldLabel>
<InputLabel v-else for="selectAll" class="text-text-secondary select-none" <FieldLabel v-else for="selectAll" class="text-text-secondary select-none"
>Select All</InputLabel >Select All</FieldLabel
> >
<button <button
v-if="selectedTimeEntries.length" 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 { computed, nextTick, ref, watch } from 'vue';
import PrimaryButton from '../Buttons/PrimaryButton.vue'; import PrimaryButton from '../Buttons/PrimaryButton.vue';
import TimeTrackerProjectTaskDropdown from '@/packages/ui/src/TimeTracker/TimeTrackerProjectTaskDropdown.vue'; import TimeTrackerProjectTaskDropdown from '@/packages/ui/src/TimeTracker/TimeTrackerProjectTaskDropdown.vue';
import InputLabel from '../Input/InputLabel.vue'; import { Field, FieldLabel } from '../field';
import { import {
type CreateClientBody, type CreateClientBody,
type CreateProjectBody, type CreateProjectBody,
@@ -23,7 +23,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/Components/ui/select'; } 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 TagDropdown from '@/packages/ui/src/Tag/TagDropdown.vue';
import type { Tag, Task } from '@/packages/api/src'; import type { Tag, Task } from '@/packages/api/src';
@@ -146,18 +146,18 @@ watch(removeAllTags, () => {
<template #content> <template #content>
<div class="space-y-4"> <div class="space-y-4">
<div class="space-y-2"> <Field>
<InputLabel for="description" value="Description" /> <FieldLabel for="description">Description</FieldLabel>
<TextInput <TextInput
id="description" id="description"
ref="descriptionInput" ref="descriptionInput"
v-model="description" v-model="description"
type="text" type="text"
class="mt-1 block w-full" class="block w-full"
@keydown.enter="submit" /> @keydown.enter="submit" />
</div> </Field>
<div class="space-y-2"> <Field>
<InputLabel for="project" value="Project" /> <FieldLabel for="project">Project</FieldLabel>
<TimeTrackerProjectTaskDropdown <TimeTrackerProjectTaskDropdown
v-model:project="projectId" v-model:project="projectId"
v-model:task="taskId" v-model:task="taskId"
@@ -169,15 +169,14 @@ watch(removeAllTags, () => {
:create-client :create-client
:currency="currency" :currency="currency"
:can-create-project :can-create-project
class="mt-1"
empty-placeholder="Select project..." empty-placeholder="Select project..."
allow-reset allow-reset
:enable-estimated-time :enable-estimated-time
:projects="projects" :projects="projects"
:tasks="tasks"></TimeTrackerProjectTaskDropdown> :tasks="tasks"></TimeTrackerProjectTaskDropdown>
</div> </Field>
<div class="space-y-2"> <Field>
<InputLabel for="project" value="Tag" /> <FieldLabel>Tag</FieldLabel>
<div class="flex space-x-5"> <div class="flex space-x-5">
<TagDropdown <TagDropdown
v-model="selectedTags" v-model="selectedTags"
@@ -194,33 +193,31 @@ watch(removeAllTags, () => {
</Button> </Button>
</template> </template>
</TagDropdown> </TagDropdown>
<div class="flex items-center space-x-2"> <Field orientation="horizontal">
<Checkbox id="no_tags" v-model:checked="removeAllTags"></Checkbox> <Checkbox id="no_tags" v-model:checked="removeAllTags"></Checkbox>
<InputLabel for="no_tags" value="Remove all tags" /> <FieldLabel for="no_tags">Remove all tags</FieldLabel>
</div> </Field>
</div> </div>
</div> </Field>
<div class="space-y-2"> <Field>
<InputLabel for="project" value="Billable" /> <FieldLabel>Billable</FieldLabel>
<div class="flex"> <Select v-model="timeEntryBillable">
<Select v-model="timeEntryBillable"> <SelectTrigger>
<SelectTrigger> <SelectValue>
<SelectValue> <span v-if="billable === undefined">Set billable status</span>
<span v-if="billable === undefined">Set billable status</span> <span v-else-if="billable === true">Billable</span>
<span v-else-if="billable === true">Billable</span> <span v-else>Non Billable</span>
<span v-else>Non Billable</span> </SelectValue>
</SelectValue> </SelectTrigger>
</SelectTrigger> <SelectContent>
<SelectContent> <SelectItem value="do-not-update">
<SelectItem value="do-not-update"> Keep current billable status
Keep current billable status </SelectItem>
</SelectItem> <SelectItem value="billable">Billable</SelectItem>
<SelectItem value="billable">Billable</SelectItem> <SelectItem value="non-billable">Non Billable</SelectItem>
<SelectItem value="non-billable">Non Billable</SelectItem> </SelectContent>
</SelectContent> </Select>
</Select> </Field>
</div>
</div>
</div> </div>
</template> </template>
<template #footer> <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 { Popover, PopoverContent, PopoverTrigger, PopoverAnchor } from './popover/index';
import { RangeCalendar } from './range-calendar/index'; import { RangeCalendar } from './range-calendar/index';
import { CommandPalette } from './CommandPalette/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 { ActivityPeriod } from './FullCalendar/idleStatusPlugin';
export type { export type {
CommandPaletteCommand, CommandPaletteCommand,
@@ -93,4 +108,16 @@ export {
PopoverAnchor, PopoverAnchor,
RangeCalendar, RangeCalendar,
CommandPalette, 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), queries: unref(filterParams),
}), }),
enabled: computed(() => !!getCurrentOrganizationId()), enabled: computed(() => !!getCurrentOrganizationId()),
placeholderData: (previousData) => previousData,
staleTime: 1000 * 30, staleTime: 1000 * 30,
}); });