mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-08 00:02:15 +01:00
Add Field component system and migrate UI
This commit is contained in:
@@ -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 }) => {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,4 +78,4 @@ export async function getPasswordResetUrl(
|
|||||||
expect(resetUrlMatch).toBeTruthy();
|
expect(resetUrlMatch).toBeTruthy();
|
||||||
|
|
||||||
return resetUrlMatch![1].replace(/&/g, '&');
|
return resetUrlMatch![1].replace(/&/g, '&');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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>;
|
|
||||||
@@ -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',
|
||||||
|
|||||||
24
resources/js/Components/ui/label/Label.vue
Normal file
24
resources/js/Components/ui/label/Label.vue
Normal 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>
|
||||||
1
resources/js/Components/ui/label/index.ts
Normal file
1
resources/js/Components/ui/label/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as Label } from './Label.vue';
|
||||||
@@ -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
|
||||||
)
|
)
|
||||||
" />
|
" />
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
21
resources/js/packages/ui/src/field/Field.vue
Normal file
21
resources/js/packages/ui/src/field/Field.vue
Normal 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>
|
||||||
16
resources/js/packages/ui/src/field/FieldContent.vue
Normal file
16
resources/js/packages/ui/src/field/FieldContent.vue
Normal 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>
|
||||||
23
resources/js/packages/ui/src/field/FieldDescription.vue
Normal file
23
resources/js/packages/ui/src/field/FieldDescription.vue
Normal 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>
|
||||||
40
resources/js/packages/ui/src/field/FieldError.vue
Normal file
40
resources/js/packages/ui/src/field/FieldError.vue
Normal 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>
|
||||||
21
resources/js/packages/ui/src/field/FieldGroup.vue
Normal file
21
resources/js/packages/ui/src/field/FieldGroup.vue
Normal 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>
|
||||||
24
resources/js/packages/ui/src/field/FieldLabel.vue
Normal file
24
resources/js/packages/ui/src/field/FieldLabel.vue
Normal 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>
|
||||||
25
resources/js/packages/ui/src/field/FieldLegend.vue
Normal file
25
resources/js/packages/ui/src/field/FieldLegend.vue
Normal 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>
|
||||||
29
resources/js/packages/ui/src/field/FieldSeparator.vue
Normal file
29
resources/js/packages/ui/src/field/FieldSeparator.vue
Normal 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>
|
||||||
22
resources/js/packages/ui/src/field/FieldSet.vue
Normal file
22
resources/js/packages/ui/src/field/FieldSet.vue
Normal 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>
|
||||||
21
resources/js/packages/ui/src/field/FieldTitle.vue
Normal file
21
resources/js/packages/ui/src/field/FieldTitle.vue
Normal 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>
|
||||||
39
resources/js/packages/ui/src/field/index.ts
Normal file
39
resources/js/packages/ui/src/field/index.ts
Normal 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';
|
||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
26
resources/js/packages/ui/src/separator/Separator.vue
Normal file
26
resources/js/packages/ui/src/separator/Separator.vue
Normal 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>
|
||||||
1
resources/js/packages/ui/src/separator/index.ts
Normal file
1
resources/js/packages/ui/src/separator/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as Separator } from './Separator.vue';
|
||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user