Compare commits

...

8 Commits

138 changed files with 3510 additions and 3305 deletions

View File

@@ -1,13 +0,0 @@
/* eslint-env node */
require("@rushstack/eslint-patch/modern-module-resolution")
module.exports = {
extends: ['plugin:vue/vue3-essential', '@vue/eslint-config-typescript/recommended', '@vue/eslint-config-prettier'],
rules: {
'vue/multi-word-component-names': 'off',
"@typescript-eslint/no-unused-vars": "off",
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": "error",
},
plugins: ['unused-imports'],
}

View File

@@ -109,7 +109,7 @@ services:
- sail - sail
- reverse-proxy - reverse-proxy
playwright: playwright:
image: mcr.microsoft.com/playwright:v1.46.1-jammy image: mcr.microsoft.com/playwright:v1.50.0-jammy
command: ['npx', 'playwright', 'test', '--ui-port=8080', '--ui-host=0.0.0.0'] command: ['npx', 'playwright', 'test', '--ui-port=8080', '--ui-host=0.0.0.0']
working_dir: /src working_dir: /src
extra_hosts: extra_hosts:

View File

@@ -191,7 +191,7 @@ test('test that updating a the start of an existing time entry in the overview w
'time_entry_range_selector' 'time_entry_range_selector'
); );
await timeEntryRangeElement.click(); await timeEntryRangeElement.click();
await page.getByTestId('time_picker_input').first().fill('1'); await page.getByTestId('time_entry_range_start').first().fill('1');
await Promise.all([ await Promise.all([
page.waitForResponse(async (response) => { page.waitForResponse(async (response) => {
return ( return (
@@ -204,10 +204,7 @@ test('test that updating a the start of an existing time entry in the overview w
(await response.json()).data.end !== null (await response.json()).data.end !== null
); );
}), }),
page page.getByTestId('time_entry_range_end').press('Enter'),
.getByTestId('time_entry_range_end')
.getByTestId('time_picker_input')
.press('Enter'),
]); ]);
}); });

View File

@@ -152,7 +152,7 @@ test('test that starting and updating the time while running works', async ({
JSON.stringify([]) JSON.stringify([])
); );
}), }),
page.getByTestId('time_entry_time').press('Tab'), page.getByTestId('time_entry_time').press('Enter'),
]); ]);
await expect(page.getByTestId('time_entry_time')).toHaveValue(/00:20/); await expect(page.getByTestId('time_entry_time')).toHaveValue(/00:20/);

36
eslint.config.mjs Normal file
View File

@@ -0,0 +1,36 @@
import eslint from '@eslint/js';
import eslintConfigPrettier from 'eslint-config-prettier';
import eslintPluginVue from 'eslint-plugin-vue';
import globals from 'globals';
import typescriptEslint from 'typescript-eslint';
import unusedImports from "eslint-plugin-unused-imports";
export default typescriptEslint.config(
{ ignores: ['*.d.ts', '**/coverage', '**/dist'] },
{
extends: [
eslint.configs.recommended,
...typescriptEslint.configs.recommended,
...eslintPluginVue.configs['flat/recommended'],
],
files: ['**/*.{ts,vue,js}'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: globals.browser,
parserOptions: {
parser: typescriptEslint.parser,
},
},
plugins: {
"unused-imports": unusedImports,
},
rules: {
"vue/multi-word-component-names": "off",
"@typescript-eslint/no-unused-vars": "off",
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": "error",
},
},
eslintConfigPrettier
);

3424
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,53 +4,60 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"lint": "eslint --ext .js,.vue,.ts --ignore-path .gitignore resources/js", "lint": "eslint resources/js",
"lint:fix": "eslint --fix --ext .js,.vue,.ts --ignore-path .gitignore resources/js", "lint:fix": "eslint --fix resources/js",
"type-check": "vue-tsc --noEmit", "type-check": "vue-tsc --noEmit",
"test:e2e": "rm -rf test-results/.auth && npx playwright test", "test:e2e": "rm -rf test-results/.auth && npx playwright test",
"zod:generate": "npx openapi-zod-client http://localhost:80/docs/api.json --output resources/js/packages/api/src/openapi.json.client.ts --base-url /api" "zod:generate": "npx openapi-zod-client http://localhost:80/docs/api.json --output resources/js/packages/api/src/openapi.json.client.ts --base-url /api"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.19.0",
"@inertiajs/vue3": "^1.0.0", "@inertiajs/vue3": "^1.0.0",
"@playwright/test": "^1.41.1", "@playwright/test": "^1.41.1",
"@tailwindcss/forms": "^0.5.9", "@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15", "@tailwindcss/typography": "^0.5.15",
"@types/node": "^20.11.5", "@types/node": "^22.10.10",
"@vitejs/plugin-vue": "^4.5.0", "@vitejs/plugin-vue": "^5.2.1",
"@vue/tsconfig": "^0.5.1", "@vue/tsconfig": "^0.5.1",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"axios": "^1.6.4", "axios": "^1.6.4",
"eslint-plugin-unused-imports": "^3.1.0", "eslint-plugin-unused-imports": "^4.1.4",
"laravel-vite-plugin": "^1.0.0", "laravel-vite-plugin": "^1.0.0",
"openapi-zod-client": "^1.16.2", "openapi-zod-client": "^1.16.2",
"postcss": "^8.4.47", "postcss": "^8.4.47",
"postcss-nesting": "^12.1.5", "postcss-nesting": "^12.1.5",
"tailwindcss": "^3.4.13", "tailwindcss": "^3.4.13",
"typescript": "^5.3.3", "typescript": "^5.7.3",
"vite": "^5.0.0", "vite": "^6.0.11",
"vite-plugin-checker": "^0.7.2", "vite-plugin-checker": "^0.8.0",
"vue": "^3.4.0", "vue": "^3.5.0",
"vue-tsc": "^2.0.28" "vue-tsc": "^2.2.0"
}, },
"dependencies": { "dependencies": {
"@floating-ui/core": "^1.6.0", "@floating-ui/core": "^1.6.0",
"@floating-ui/vue": "^1.0.6", "@floating-ui/vue": "^1.0.6",
"@heroicons/vue": "^2.1.1", "@heroicons/vue": "^2.1.1",
"@rushstack/eslint-patch": "^1.7.0", "@rushstack/eslint-patch": "^1.10.5",
"@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/container-queries": "^0.1.1",
"@tanstack/vue-query": "^5.56.2", "@tanstack/vue-query": "^5.56.2",
"@tanstack/vue-query-devtools": "^5.58.0", "@tanstack/vue-query-devtools": "^5.58.0",
"@vue/eslint-config-prettier": "^9.0.0", "@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^13.0.0", "@vue/eslint-config-typescript": "^14.3.0",
"@vueuse/core": "^10.11.0", "@vueuse/core": "^12.5.0",
"@vueuse/integrations": "^11.1.0", "@vueuse/integrations": "^12.5.0",
"dayjs": "^1.11.11", "dayjs": "^1.11.11",
"echarts": "^5.5.0", "echarts": "^5.5.0",
"focus-trap": "^7.6.0", "focus-trap": "^7.6.0",
"parse-duration": "^1.1.0", "parse-duration": "^2.0.1",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"radix-vue": "^1.9.6", "radix-vue": "^1.9.6",
"tailwind-merge": "^2.2.1", "tailwind-merge": "^2.2.1",
"vue-echarts": "^6.7.2" "vue-echarts": "^7.0.3"
},
"overrides": {
"vite-plugin-checker": {
"vue-tsc": "$vue-tsc"
}
} }
} }

View File

@@ -112,7 +112,7 @@ const showBlackFridayBanner = computed(() => {
<span>Upgrade now</span> <span>Upgrade now</span>
</div> </div>
</Link> </Link>
<button @click="hideBlackFridayBanner = true" class="p-1"> <button class="p-1" @click="hideBlackFridayBanner = true">
<XMarkIcon <XMarkIcon
class="w-4 opacity-50 hover:opacity-100"></XMarkIcon> class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
</button> </button>
@@ -142,7 +142,7 @@ const showBlackFridayBanner = computed(() => {
<span>Upgrade now</span> <span>Upgrade now</span>
</div> </div>
</Link> </Link>
<button @click="hideTrialBanner = true" class="p-1"> <button class="p-1" @click="hideTrialBanner = true">
<XMarkIcon <XMarkIcon
class="w-4 opacity-50 hover:opacity-100"></XMarkIcon> class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
</button> </button>
@@ -174,7 +174,7 @@ const showBlackFridayBanner = computed(() => {
<span>Upgrade now</span> <span>Upgrade now</span>
</div> </div>
</Link> </Link>
<button @click="hideBlockedBanner = true" class="p-1"> <button class="p-1" @click="hideBlockedBanner = true">
<XMarkIcon <XMarkIcon
class="w-4 opacity-50 hover:opacity-100"></XMarkIcon> class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
</button> </button>
@@ -206,7 +206,7 @@ const showBlackFridayBanner = computed(() => {
<span>Upgrade now</span> <span>Upgrade now</span>
</div> </div>
</Link> </Link>
<button @click="hideFreeUpgradeBanner = true" class="p-1"> <button class="p-1" @click="hideFreeUpgradeBanner = true">
<XMarkIcon <XMarkIcon
class="w-4 opacity-50 hover:opacity-100"></XMarkIcon> class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
</button> </button>

View File

@@ -45,10 +45,10 @@ useFocus(clientNameInput, { initialValue: true });
v-model="client.name" v-model="client.name"
type="text" type="text"
placeholder="Client Name" placeholder="Client Name"
@keydown.enter="submit"
class="mt-1 block w-full" class="mt-1 block w-full"
required required
autocomplete="clientName" /> autocomplete="clientName"
@keydown.enter="submit" />
</div> </div>
</div> </div>
</template> </template>

View File

@@ -46,10 +46,10 @@ useFocus(clientNameInput, { initialValue: true });
v-model="clientBody.name" v-model="clientBody.name"
type="text" type="text"
placeholder="Client Name" placeholder="Client Name"
@keydown.enter="submit"
class="mt-1 block w-full" class="mt-1 block w-full"
required required
autocomplete="clientName" /> autocomplete="clientName"
@keydown.enter="submit" />
</div> </div>
</div> </div>
</template> </template>

View File

@@ -23,28 +23,28 @@ const props = defineProps<{
<div class="min-w-[150px]"> <div class="min-w-[150px]">
<button <button
v-if="canUpdateClients()" v-if="canUpdateClients()"
@click="emit('edit')"
:aria-label="'Edit Client ' + props.client.name" :aria-label="'Edit Client ' + props.client.name"
data-testid="client_edit" data-testid="client_edit"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"> class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click="emit('edit')">
<PencilSquareIcon <PencilSquareIcon
class="w-5 text-icon-active"></PencilSquareIcon> class="w-5 text-icon-active"></PencilSquareIcon>
<span>Edit</span> <span>Edit</span>
</button> </button>
<button <button
@click.prevent="emit('archive')"
v-if="canUpdateClients()" v-if="canUpdateClients()"
:aria-label="'Archive Client ' + props.client.name" :aria-label="'Archive Client ' + props.client.name"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"> class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click.prevent="emit('archive')">
<ArchiveBoxIcon class="w-5 text-icon-active"></ArchiveBoxIcon> <ArchiveBoxIcon class="w-5 text-icon-active"></ArchiveBoxIcon>
<span>{{ client.is_archived ? 'Unarchive' : 'Archive' }}</span> <span>{{ client.is_archived ? 'Unarchive' : 'Archive' }}</span>
</button> </button>
<button <button
v-if="canDeleteClients()" v-if="canDeleteClients()"
@click="emit('delete')"
:aria-label="'Delete Client ' + props.client.name" :aria-label="'Delete Client ' + props.client.name"
data-testid="client_delete" data-testid="client_delete"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"> class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click="emit('delete')">
<TrashIcon class="w-5 text-icon-active"></TrashIcon> <TrashIcon class="w-5 text-icon-active"></TrashIcon>
<span>Delete</span> <span>Delete</span>
</button> </button>

View File

@@ -18,7 +18,7 @@ function getNameForItem(item: Client) {
<template> <template>
<MultiselectDropdown <MultiselectDropdown
searchPlaceholder="Search for a Client..." search-placeholder="Search for a Client..."
:items="clients" :items="clients"
:get-key-from-item="getKeyFromItem" :get-key-from-item="getKeyFromItem"
:get-name-for-item="getNameForItem"> :get-name-for-item="getNameForItem">

View File

@@ -25,18 +25,18 @@ const createClient = ref(false);
style="grid-template-columns: 1fr 150px 200px 80px"> style="grid-template-columns: 1fr 150px 200px 80px">
<ClientTableHeading></ClientTableHeading> <ClientTableHeading></ClientTableHeading>
<div <div
class="col-span-2 py-24 text-center" v-if="clients.length === 0"
v-if="clients.length === 0"> class="col-span-2 py-24 text-center">
<UserCircleIcon <UserCircleIcon
class="w-8 text-icon-default inline pb-2"></UserCircleIcon> class="w-8 text-icon-default inline pb-2"></UserCircleIcon>
<h3 class="text-white font-semibold">No clients found</h3> <h3 class="text-white font-semibold">No clients found</h3>
<p class="pb-5" v-if="canCreateClients()"> <p v-if="canCreateClients()" class="pb-5">
Create your first client now! Create your first client now!
</p> </p>
<SecondaryButton <SecondaryButton
v-if="canCreateClients()" v-if="canCreateClients()"
@click="createClient = true"
:icon="PlusIcon as Component" :icon="PlusIcon as Component"
@click="createClient = true"
>Create your First Client >Create your First Client
</SecondaryButton> </SecondaryButton>
</div> </div>

View File

@@ -38,8 +38,8 @@ const showEditModal = ref(false);
<template> <template>
<TableRow> <TableRow>
<ClientEditModal <ClientEditModal
:client="client" v-model:show="showEditModal"
v-model:show="showEditModal"></ClientEditModal> :client="client"></ClientEditModal>
<div <div
class="whitespace-nowrap flex items-center space-x-5 3xl:pl-12 py-4 pr-3 text-sm font-medium text-white pl-4 sm:pl-6 lg:pl-8 3xl:pl-12"> class="whitespace-nowrap flex items-center space-x-5 3xl:pl-12 py-4 pr-3 text-sm font-medium text-white pl-4 sm:pl-6 lg:pl-8 3xl:pl-12">
<span> <span>

View File

@@ -10,16 +10,16 @@ const emit = defineEmits<{
<template> <template>
<MoreOptionsDropdown label="Actions for the invitation"> <MoreOptionsDropdown label="Actions for the invitation">
<button <button
@click="emit('resend')"
data-testid="invitation_delete" data-testid="invitation_delete"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"> class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click="emit('resend')">
<ArrowPathIcon class="w-5 text-icon-active"></ArrowPathIcon> <ArrowPathIcon class="w-5 text-icon-active"></ArrowPathIcon>
<span>Resend Invitation</span> <span>Resend Invitation</span>
</button> </button>
<button <button
@click="emit('delete')"
data-testid="invitation_delete" data-testid="invitation_delete"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"> class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click="emit('delete')">
<TrashIcon class="w-5 text-icon-active"></TrashIcon> <TrashIcon class="w-5 text-icon-active"></TrashIcon>
<span>Delete</span> <span>Delete</span>
</button> </button>

View File

@@ -18,10 +18,10 @@ defineEmits<{
<template> <template>
<BillableRateModal <BillableRateModal
@submit="$emit('submit')"
v-model:show="show" v-model:show="show"
v-model:saving="saving" v-model:saving="saving"
title="Update Member Billable Rate"> title="Update Member Billable Rate"
@submit="$emit('submit')">
<p class="py-1 text-center"> <p class="py-1 text-center">
The billable rate of {{ memberName }} will be updated to The billable rate of {{ memberName }} will be updated to
<strong>{{ <strong>{{

View File

@@ -17,8 +17,8 @@ const model = defineModel<string>({
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
hiddenMembers: ProjectMember[]; hiddenMembers?: ProjectMember[];
disabled: boolean; disabled?: boolean;
}>(), }>(),
{ {
hiddenMembers: () => [] as ProjectMember[], hiddenMembers: () => [] as ProjectMember[],
@@ -76,7 +76,7 @@ const currentValue = computed(() => {
:items="filteredMembers" :items="filteredMembers"
:get-key-from-item="(member) => member.id" :get-key-from-item="(member) => member.id"
:get-name-for-item="(member) => member.name"> :get-name-for-item="(member) => member.name">
<template v-slot:trigger> <template #trigger>
<Badge <Badge
tag="button" tag="button"
class="flex w-full text-base text-left space-x-3 px-3 text-text-secondary font-normal cursor py-1.5"> class="flex w-full text-base text-left space-x-3 px-3 text-text-secondary font-normal cursor py-1.5">
@@ -84,7 +84,7 @@ const currentValue = computed(() => {
<div v-if="currentValue" class="flex-1 truncate"> <div v-if="currentValue" class="flex-1 truncate">
{{ currentValue }} {{ currentValue }}
</div> </div>
<div class="flex-1" v-else>Select a member...</div> <div v-else class="flex-1">Select a member...</div>
<ChevronDownIcon class="w-4 text-muted"></ChevronDownIcon> <ChevronDownIcon class="w-4 text-muted"></ChevronDownIcon>
</Badge> </Badge>
</template> </template>

View File

@@ -108,11 +108,11 @@ const roleDescription = computed(() => {
v-model:saving="saving" v-model:saving="saving"
v-model:show="showBillableRateModal" v-model:show="showBillableRateModal"
:member-name="member.name" :member-name="member.name"
:newBillableRate="memberBody.billable_rate" :new-billable-rate="memberBody.billable_rate"
@submit="submitBillableRate"></MemberBillableRateModal> @submit="submitBillableRate"></MemberBillableRateModal>
<MemberOwnershipTransferConfirmModal <MemberOwnershipTransferConfirmModal
:member-name="member.name"
v-model:show="showOwnershipTransferConfirmModal" v-model:show="showOwnershipTransferConfirmModal"
:member-name="member.name"
@submit="submit"></MemberOwnershipTransferConfirmModal> @submit="submit"></MemberOwnershipTransferConfirmModal>
<DialogModal closeable :show="show" @close="show = false"> <DialogModal closeable :show="show" @close="show = false">
<template #title> <template #title>
@@ -127,9 +127,9 @@ const roleDescription = computed(() => {
<div> <div>
<InputLabel for="role" value="Role" /> <InputLabel for="role" value="Role" />
<MemberRoleSelect <MemberRoleSelect
v-model="memberBody.role"
class="mt-2" class="mt-2"
name="role" name="role"></MemberRoleSelect>
v-model="memberBody.role"></MemberRoleSelect>
</div> </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>
@@ -140,28 +140,28 @@ const roleDescription = computed(() => {
<div> <div>
<InputLabel for="billableType" value="Billable" /> <InputLabel for="billableType" value="Billable" />
<MemberBillableSelect <MemberBillableSelect
class="mt-2"
name="billableType"
v-model=" v-model="
billableRateSelect billableRateSelect
"></MemberBillableSelect> "
class="mt-2"
name="billableType"></MemberBillableSelect>
</div> </div>
<div <div
class="flex-1" v-if="billableRateSelect === 'custom-rate'"
v-if="billableRateSelect === 'custom-rate'"> class="flex-1">
<InputLabel <InputLabel
for="memberBillableRate" for="memberBillableRate"
class="mb-2" class="mb-2"
value="Billable Rate" /> value="Billable Rate" />
<BillableRateInput <BillableRateInput
v-model="
memberBody.billable_rate
"
focus focus
class="w-full" class="w-full"
:currency="getOrganizationCurrencyString()" :currency="getOrganizationCurrencyString()"
@keydown.enter="saveWithChecks()"
name="memberBillableRate" name="memberBillableRate"
v-model=" @keydown.enter="saveWithChecks()"></BillableRateInput>
memberBody.billable_rate
"></BillableRateInput>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -112,11 +112,11 @@ useFocus(clientNameInput, { initialValue: true });
v-if="isBillingActivated() && canManageBilling()" v-if="isBillingActivated() && canManageBilling()"
href="/billing"> href="/billing">
<PrimaryButton <PrimaryButton
type="button"
class="mt-6"
v-if=" v-if="
isBillingActivated() && canUpdateOrganization() isBillingActivated() && canUpdateOrganization()
"> "
type="button"
class="mt-6">
<CreditCardIcon class="w-5 h-5 me-2" /> <CreditCardIcon class="w-5 h-5 me-2" />
Go to Billing Go to Billing
</PrimaryButton> </PrimaryButton>
@@ -128,15 +128,15 @@ useFocus(clientNameInput, { initialValue: true });
<InputLabel for="email" value="Email" /> <InputLabel for="email" value="Email" />
<TextInput <TextInput
id="email" id="email"
name="email"
ref="memberEmailInput" ref="memberEmailInput"
v-model="addTeamMemberForm.email" v-model="addTeamMemberForm.email"
name="email"
type="text" type="text"
placeholder="Member Email" placeholder="Member Email"
@keydown.enter="submit"
class="mt-1 block w-full" class="mt-1 block w-full"
required required
autocomplete="memberName" /> autocomplete="memberName"
@keydown.enter="submit" />
<InputError :message="errors.email" class="mt-2" /> <InputError :message="errors.email" class="mt-2" />
</div> </div>

View File

@@ -20,19 +20,19 @@ const props = defineProps<{
<div class="min-w-[150px]"> <div class="min-w-[150px]">
<button <button
v-if="canUpdateMembers()" v-if="canUpdateMembers()"
@click="emit('edit')"
:aria-label="'Edit Member ' + props.member.name" :aria-label="'Edit Member ' + props.member.name"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"> class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click="emit('edit')">
<PencilSquareIcon <PencilSquareIcon
class="w-5 text-icon-active"></PencilSquareIcon> class="w-5 text-icon-active"></PencilSquareIcon>
<span>Edit</span> <span>Edit</span>
</button> </button>
<button <button
v-if="canDeleteMembers()" v-if="canDeleteMembers()"
@click="emit('delete')"
:aria-label="'Delete Member ' + props.member.name" :aria-label="'Delete Member ' + props.member.name"
data-testid="member_delete" data-testid="member_delete"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"> class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click="emit('delete')">
<TrashIcon class="w-5 text-icon-active"></TrashIcon> <TrashIcon class="w-5 text-icon-active"></TrashIcon>
<span>Delete</span> <span>Delete</span>
</button> </button>

View File

@@ -18,7 +18,7 @@ function getNameForItem(item: Member) {
<template> <template>
<MultiselectDropdown <MultiselectDropdown
searchPlaceholder="Search for a Member..." search-placeholder="Search for a Member..."
:items="members" :items="members"
:get-key-from-item="getKeyFromItem" :get-key-from-item="getKeyFromItem"
:get-name-for-item="getNameForItem"> :get-name-for-item="getNameForItem">

View File

@@ -36,9 +36,9 @@ const emit = defineEmits<{
<SecondaryButton @click="show = false"> Cancel</SecondaryButton> <SecondaryButton @click="show = false"> Cancel</SecondaryButton>
<PrimaryButton <PrimaryButton
class="ms-3" class="ms-3"
@click="emit('submit')"
:class="{ 'opacity-25': saving }" :class="{ 'opacity-25': saving }"
:disabled="saving"> :disabled="saving"
@click="emit('submit')">
Confirm Transfer Confirm Transfer
</PrimaryButton> </PrimaryButton>
</template> </template>

View File

@@ -89,8 +89,8 @@ async function invitePlaceholder(id: string) {
member.is_placeholder === true && member.is_placeholder === true &&
canInvitePlaceholderMembers() canInvitePlaceholderMembers()
" "
@click="invitePlaceholder(member.id)"
size="small" size="small"
@click="invitePlaceholder(member.id)"
>Invite</SecondaryButton >Invite</SecondaryButton
> >
<MemberMoreOptionsDropdown <MemberMoreOptionsDropdown
@@ -99,8 +99,8 @@ async function invitePlaceholder(id: string) {
@delete="removeMember"></MemberMoreOptionsDropdown> @delete="removeMember"></MemberMoreOptionsDropdown>
</div> </div>
<MemberEditModal <MemberEditModal
:member="member" v-model:show="showEditMemberModal"
v-model:show="showEditMemberModal"></MemberEditModal> :member="member"></MemberEditModal>
</TableRow> </TableRow>
</template> </template>

View File

@@ -34,8 +34,8 @@
<div class="ml-4 flex flex-shrink-0"> <div class="ml-4 flex flex-shrink-0">
<button <button
type="button" type="button"
@click="show = false" class="inline-flex rounded-md bg-card-background text-muted hover:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
class="inline-flex rounded-md bg-card-background text-muted hover:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"> @click="show = false">
<span class="sr-only">Close</span> <span class="sr-only">Close</span>
<XMarkIcon class="h-5 w-5" aria-hidden="true" /> <XMarkIcon class="h-5 w-5" aria-hidden="true" />
</button> </button>

View File

@@ -17,10 +17,10 @@ defineEmits<{
<template> <template>
<BillableRateModal <BillableRateModal
@submit="$emit('submit')"
v-model:show="show" v-model:show="show"
v-model:saving="saving" v-model:saving="saving"
title="Update Organization Billable Rate"> title="Update Organization Billable Rate"
@submit="$emit('submit')">
<p class="py-0.5 text-center"> <p class="py-0.5 text-center">
The organization billable rate will be updated to The organization billable rate will be updated to
<strong>{{ <strong>{{

View File

@@ -40,7 +40,7 @@ const shownProjects = computed(() => {
withDefaults( withDefaults(
defineProps<{ defineProps<{
border: boolean; border?: boolean;
}>(), }>(),
{ {
border: true, border: true,
@@ -123,17 +123,17 @@ function updateValue(project: Project) {
<template #content> <template #content>
<ComboboxRoot <ComboboxRoot
:open="open" :open="open"
:modelValue="currentProject" :model-value="currentProject"
@update:modelValue="updateValue" :search-term="searchValue"
@update:searchTerm="(e) => console.log(e)" class="relative"
:searchTerm="searchValue" @update:model-value="updateValue"
class="relative"> @update:search-term="(e) => console.log(e)">
<ComboboxAnchor> <ComboboxAnchor>
<ComboboxInput <ComboboxInput
@keydown.enter="addProjectIfNoneExists"
ref="searchInput" ref="searchInput"
class="bg-card-background border-0 placeholder-muted text-sm text-white py-2.5 focus:ring-0 border-b border-card-background-separator focus:border-card-background-separator w-full" class="bg-card-background border-0 placeholder-muted text-sm text-white py-2.5 focus:ring-0 border-b border-card-background-separator focus:border-card-background-separator w-full"
placeholder="Search for a project..." /> placeholder="Search for a project..."
@keydown.enter="addProjectIfNoneExists" />
</ComboboxAnchor> </ComboboxAnchor>
<ComboboxContent> <ComboboxContent>
<ComboboxViewport <ComboboxViewport

View File

@@ -90,8 +90,8 @@ async function submitBillableRate() {
<div class="text-center"> <div class="text-center">
<InputLabel for="color" value="Color" /> <InputLabel for="color" value="Color" />
<ProjectColorSelector <ProjectColorSelector
class="mt-1" v-model="project.color"
v-model="project.color"></ProjectColorSelector> class="mt-1"></ProjectColorSelector>
</div> </div>
</div> </div>
<div class="w-full"> <div class="w-full">
@@ -102,18 +102,18 @@ async function submitBillableRate() {
v-model="project.name" v-model="project.name"
type="text" type="text"
placeholder="Project Name" placeholder="Project Name"
@keydown.enter="submit()"
class="mt-1 block w-full" class="mt-1 block w-full"
required required
autocomplete="projectName" /> autocomplete="projectName"
@keydown.enter="submit()" />
</div> </div>
<div class=""> <div class="">
<InputLabel for="client" value="Client" /> <InputLabel for="client" value="Client" />
<ClientDropdown <ClientDropdown
:createClient v-model="project.client_id"
:create-client
:clients="clients" :clients="clients"
class="mt-1" class="mt-1">
v-model="project.client_id">
<template #trigger> <template #trigger>
<Badge <Badge
class="bg-input-background cursor-pointer hover:bg-tertiary" class="bg-input-background cursor-pointer hover:bg-tertiary"
@@ -133,18 +133,18 @@ async function submitBillableRate() {
<div class="lg:grid grid-cols-2 gap-12"> <div class="lg:grid grid-cols-2 gap-12">
<div> <div>
<ProjectEditBillableSection <ProjectEditBillableSection
@submit="submit" v-model:is-billable="project.is_billable"
:currency="getOrganizationCurrencyString()" v-model:billable-rate="
v-model:isBillable="project.is_billable"
v-model:billableRate="
project.billable_rate project.billable_rate
"></ProjectEditBillableSection> "
:currency="getOrganizationCurrencyString()"
@submit="submit"></ProjectEditBillableSection>
</div> </div>
<div> <div>
<EstimatedTimeSection <EstimatedTimeSection
v-if="isAllowedToPerformPremiumAction()" v-if="isAllowedToPerformPremiumAction()"
@submit="submit()" v-model="project.estimated_time"
v-model="project.estimated_time"></EstimatedTimeSection> @submit="submit()"></EstimatedTimeSection>
</div> </div>
</div> </div>
</template> </template>
@@ -163,9 +163,9 @@ async function submitBillableRate() {
<ProjectBillableRateModal <ProjectBillableRateModal
v-model:show="showBillableRateModal" v-model:show="showBillableRateModal"
:currency="getOrganizationCurrencyString()" :currency="getOrganizationCurrencyString()"
@submit="submitBillableRate"
:new-billable-rate="project.billable_rate" :new-billable-rate="project.billable_rate"
:project-name="project.name"></ProjectBillableRateModal> :project-name="project.name"
@submit="submitBillableRate"></ProjectBillableRateModal>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@@ -21,29 +21,29 @@ const props = defineProps<{
<MoreOptionsDropdown :label="'Actions for Project ' + props.project.name"> <MoreOptionsDropdown :label="'Actions for Project ' + props.project.name">
<div class="min-w-[150px]"> <div class="min-w-[150px]">
<button <button
@click.prevent="emit('edit')"
v-if="canUpdateProjects()" v-if="canUpdateProjects()"
:aria-label="'Edit Project ' + props.project.name" :aria-label="'Edit Project ' + props.project.name"
data-testid="project_edit" data-testid="project_edit"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"> class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click.prevent="emit('edit')">
<PencilSquareIcon <PencilSquareIcon
class="w-5 text-icon-active"></PencilSquareIcon> class="w-5 text-icon-active"></PencilSquareIcon>
<span>Edit</span> <span>Edit</span>
</button> </button>
<button <button
@click.prevent="emit('archive')"
v-if="canUpdateProjects()" v-if="canUpdateProjects()"
:aria-label="'Archive Project ' + props.project.name" :aria-label="'Archive Project ' + props.project.name"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"> class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click.prevent="emit('archive')">
<ArchiveBoxIcon class="w-5 text-icon-active"></ArchiveBoxIcon> <ArchiveBoxIcon class="w-5 text-icon-active"></ArchiveBoxIcon>
<span>{{ project.is_archived ? 'Unarchive' : 'Archive' }}</span> <span>{{ project.is_archived ? 'Unarchive' : 'Archive' }}</span>
</button> </button>
<button <button
@click.prevent="emit('delete')" v-if="canDeleteProjects()"
:aria-label="'Delete Project ' + props.project.name" :aria-label="'Delete Project ' + props.project.name"
data-testid="project_delete" data-testid="project_delete"
v-if="canDeleteProjects()" class="border-b border-card-background-separator flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
class="border-b border-card-background-separator flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"> @click.prevent="emit('delete')">
<TrashIcon class="w-5 text-icon-active"></TrashIcon> <TrashIcon class="w-5 text-icon-active"></TrashIcon>
<span>Delete</span> <span>Delete</span>
</button> </button>

View File

@@ -18,7 +18,7 @@ function getNameForItem(item: Project) {
<template> <template>
<MultiselectDropdown <MultiselectDropdown
searchPlaceholder="Search for a Project..." search-placeholder="Search for a Project..."
:items="projects" :items="projects"
:get-key-from-item="getKeyFromItem" :get-key-from-item="getKeyFromItem"
:get-name-for-item="getNameForItem"> :get-name-for-item="getNameForItem">

View File

@@ -44,12 +44,12 @@ import { isAllowedToPerformPremiumAction } from '@/utils/billing';
<template> <template>
<ProjectCreateModal <ProjectCreateModal
:createProject v-model:show="showCreateProjectModal"
:createClient :create-project
:create-client
:currency="getOrganizationCurrencyString()" :currency="getOrganizationCurrencyString()"
:clients="clients" :clients="clients"
:enableEstimatedTime="isAllowedToPerformPremiumAction" :enable-estimated-time="isAllowedToPerformPremiumAction"></ProjectCreateModal>
v-model:show="showCreateProjectModal"></ProjectCreateModal>
<div class="flow-root max-w-[100vw] overflow-x-auto"> <div class="flow-root max-w-[100vw] overflow-x-auto">
<div class="inline-block min-w-full align-middle"> <div class="inline-block min-w-full align-middle">
<div <div
@@ -57,12 +57,12 @@ import { isAllowedToPerformPremiumAction } from '@/utils/billing';
class="grid min-w-full" class="grid min-w-full"
:style="gridTemplate"> :style="gridTemplate">
<ProjectTableHeading <ProjectTableHeading
:showBillableRate=" :show-billable-rate="
props.showBillableRate props.showBillableRate
"></ProjectTableHeading> "></ProjectTableHeading>
<div <div
class="col-span-5 py-24 text-center" v-if="projects.length === 0"
v-if="projects.length === 0"> class="col-span-5 py-24 text-center">
<FolderPlusIcon <FolderPlusIcon
class="w-8 text-icon-default inline pb-2"></FolderPlusIcon> class="w-8 text-icon-default inline pb-2"></FolderPlusIcon>
<h3 class="text-white font-semibold"> <h3 class="text-white font-semibold">
@@ -81,14 +81,14 @@ import { isAllowedToPerformPremiumAction } from '@/utils/billing';
</p> </p>
<SecondaryButton <SecondaryButton
v-if="canCreateProjects()" v-if="canCreateProjects()"
@click="showCreateProjectModal = true"
:icon="PlusIcon" :icon="PlusIcon"
@click="showCreateProjectModal = true"
>Create your First Project >Create your First Project
</SecondaryButton> </SecondaryButton>
</div> </div>
<template v-for="project in projects" :key="project.id"> <template v-for="project in projects" :key="project.id">
<ProjectTableRow <ProjectTableRow
:showBillableRate="props.showBillableRate" :show-billable-rate="props.showBillableRate"
:project="project"></ProjectTableRow> :project="project"></ProjectTableRow>
</template> </template>
</div> </div>

View File

@@ -19,8 +19,8 @@ defineProps<{
Progress Progress
</div> </div>
<div <div
class="px-3 py-1.5 text-left font-semibold text-white" v-if="showBillableRate"
v-if="showBillableRate"> class="px-3 py-1.5 text-left font-semibold text-white">
Billable Rate Billable Rate
</div> </div>
<div class="px-3 py-1.5 text-left font-semibold text-white">Status</div> <div class="px-3 py-1.5 text-left font-semibold text-white">Status</div>

View File

@@ -83,8 +83,8 @@ const showEditProjectModal = ref(false);
</div> </div>
<div class="whitespace-nowrap min-w-0 px-3 py-4 text-sm text-muted"> <div class="whitespace-nowrap min-w-0 px-3 py-4 text-sm text-muted">
<div <div
class="overflow-ellipsis overflow-hidden" v-if="project.client_id"
v-if="project.client_id"> class="overflow-ellipsis overflow-hidden">
{{ client?.name }} {{ client?.name }}
</div> </div>
<div v-else>No client</div> <div v-else>No client</div>
@@ -106,8 +106,8 @@ const showEditProjectModal = ref(false);
<span v-else> -- </span> <span v-else> -- </span>
</div> </div>
<div <div
class="whitespace-nowrap px-3 py-4 text-sm text-muted" v-if="showBillableRate"
v-if="showBillableRate"> class="whitespace-nowrap px-3 py-4 text-sm text-muted">
{{ billableRateInfo }} {{ billableRateInfo }}
</div> </div>
<div <div

View File

@@ -18,10 +18,10 @@ defineEmits<{
<template> <template>
<BillableRateModal <BillableRateModal
@submit="$emit('submit')"
v-model:show="show" v-model:show="show"
v-model:saving="saving" v-model:saving="saving"
title="Update Project Member Billable Rate"> title="Update Project Member Billable Rate"
@submit="$emit('submit')">
<p class="py-1 text-center"> <p class="py-1 text-center">
The billable rate of {{ memberName }} will be updated to The billable rate of {{ memberName }} will be updated to
<strong>{{ <strong>{{

View File

@@ -52,16 +52,16 @@ useFocus(projectNameInput, { initialValue: true });
<div class="grid grid-cols-3 items-center space-x-4"> <div class="grid grid-cols-3 items-center space-x-4">
<div class="col-span-3 sm:col-span-2"> <div class="col-span-3 sm:col-span-2">
<MemberCombobox <MemberCombobox
:hidden-members="props.existingMembers" v-model="projectMember.member_id"
v-model="projectMember.member_id"></MemberCombobox> :hidden-members="props.existingMembers"></MemberCombobox>
</div> </div>
<div class="col-span-3 sm:col-span-1 flex-1"> <div class="col-span-3 sm:col-span-1 flex-1">
<BillableRateInput <BillableRateInput
name="billable_rate"
:currency="getOrganizationCurrencyString()"
v-model=" v-model="
projectMember.billable_rate projectMember.billable_rate
"></BillableRateInput> "
name="billable_rate"
:currency="getOrganizationCurrencyString()"></BillableRateInput>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -75,8 +75,8 @@ useFocus(projectNameInput, { initialValue: true });
<template #content> <template #content>
<ProjectMemberBillableRateModal <ProjectMemberBillableRateModal
:member-name="props.name"
v-model:show="showBillableRateModal" v-model:show="showBillableRateModal"
:member-name="props.name"
:new-billable-rate="projectMemberBody.billable_rate" :new-billable-rate="projectMemberBody.billable_rate"
@close="showBillableRateModal = false" @close="showBillableRateModal = false"
@submit="submitBillableRate"></ProjectMemberBillableRateModal> @submit="submitBillableRate"></ProjectMemberBillableRateModal>
@@ -92,12 +92,12 @@ useFocus(projectNameInput, { initialValue: true });
class="mb-2" class="mb-2"
value="Billable Rate"></InputLabel> value="Billable Rate"></InputLabel>
<BillableRateInput <BillableRateInput
@keydown.enter="submit"
:currency="getOrganizationCurrencyString()"
name="billable_rate"
v-model=" v-model="
projectMemberBody.billable_rate projectMemberBody.billable_rate
"></BillableRateInput> "
:currency="getOrganizationCurrencyString()"
name="billable_rate"
@keydown.enter="submit"></BillableRateInput>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -27,17 +27,17 @@ const currentMember = computed(() => {
<MoreOptionsDropdown <MoreOptionsDropdown
:label="'Actions for Project Member ' + currentMember?.name"> :label="'Actions for Project Member ' + currentMember?.name">
<button <button
@click.prevent="emit('edit')"
:aria-label="'Edit Project Member ' + currentMember?.name" :aria-label="'Edit Project Member ' + currentMember?.name"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"> class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click.prevent="emit('edit')">
<PencilSquareIcon class="w-5 text-icon-active"></PencilSquareIcon> <PencilSquareIcon class="w-5 text-icon-active"></PencilSquareIcon>
<span>Edit</span> <span>Edit</span>
</button> </button>
<button <button
@click.prevent="emit('delete')"
:aria-label="'Delete Project Member ' + currentMember?.name" :aria-label="'Delete Project Member ' + currentMember?.name"
data-testid="project_delete" data-testid="project_delete"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"> class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click.prevent="emit('delete')">
<TrashIcon class="w-5 text-icon-active"></TrashIcon> <TrashIcon class="w-5 text-icon-active"></TrashIcon>
<span>Remove from Team</span> <span>Remove from Team</span>
</button> </button>

View File

@@ -18,9 +18,9 @@ const createProjectMember = ref(false);
<template> <template>
<ProjectMemberCreateModal <ProjectMemberCreateModal
v-model:show="createProjectMember"
:existing-members="projectMembers" :existing-members="projectMembers"
:project-id="projectId" :project-id="projectId"></ProjectMemberCreateModal>
v-model:show="createProjectMember"></ProjectMemberCreateModal>
<div class="flow-root"> <div class="flow-root">
<div class="inline-block min-w-full align-middle"> <div class="inline-block min-w-full align-middle">
<div <div
@@ -29,15 +29,15 @@ const createProjectMember = ref(false);
style="grid-template-columns: 1fr 150px 150px 80px"> style="grid-template-columns: 1fr 150px 150px 80px">
<ProjectMemberTableHeading></ProjectMemberTableHeading> <ProjectMemberTableHeading></ProjectMemberTableHeading>
<div <div
class="col-span-5 py-24 text-center" v-if="projectMembers.length === 0"
v-if="projectMembers.length === 0"> class="col-span-5 py-24 text-center">
<UserGroupIcon <UserGroupIcon
class="w-8 text-icon-default inline pb-2"></UserGroupIcon> class="w-8 text-icon-default inline pb-2"></UserGroupIcon>
<h3 class="text-white font-semibold">No project members</h3> <h3 class="text-white font-semibold">No project members</h3>
<p class="pb-5">Add the first project member!</p> <p class="pb-5">Add the first project member!</p>
<SecondaryButton <SecondaryButton
@click="createProjectMember = true"
:icon="PlusIcon" :icon="PlusIcon"
@click="createProjectMember = true"
>Add a new Project Member >Add a new Project Member
</SecondaryButton> </SecondaryButton>
</div> </div>

View File

@@ -37,8 +37,8 @@ const showEditModal = ref(false);
<template> <template>
<TableRow> <TableRow>
<ProjectMemberEditModal <ProjectMemberEditModal
:name="member?.name"
v-model:show="showEditModal" v-model:show="showEditModal"
:name="member?.name"
:project-member="projectMember"></ProjectMemberEditModal> :project-member="projectMember"></ProjectMemberEditModal>
<div <div
class="whitespace-nowrap flex items-center space-x-5 3xl:pl-12 py-4 pr-3 text-sm font-medium text-white pl-4 sm:pl-6 lg:pl-8 3xl:pl-12"> class="whitespace-nowrap flex items-center space-x-5 3xl:pl-12 py-4 pr-3 text-sm font-medium text-white pl-4 sm:pl-6 lg:pl-8 3xl:pl-12">

View File

@@ -82,22 +82,22 @@ async function submit() {
<InputLabel for="name" value="Name" /> <InputLabel for="name" value="Name" />
<TextInput <TextInput
id="name" id="name"
class="mt-1.5 w-full" v-model="report.name"
v-model="report.name"></TextInput> class="mt-1.5 w-full"></TextInput>
</div> </div>
<div> <div>
<InputLabel for="description" value="Description" /> <InputLabel for="description" value="Description" />
<TextInput <TextInput
id="description" id="description"
class="mt-1.5 w-full" v-model="report.description"
v-model="report.description"></TextInput> class="mt-1.5 w-full"></TextInput>
</div> </div>
<InputLabel value="Visibility" /> <InputLabel value="Visibility" />
<div class="flex items-center space-x-12"> <div class="flex items-center space-x-12">
<div class="flex items-center space-x-3 px-2 py-3"> <div class="flex items-center space-x-3 px-2 py-3">
<Checkbox <Checkbox
v-model:checked="report.is_public" id="is_public"
id="is_public"></Checkbox> v-model:checked="report.is_public"></Checkbox>
<InputLabel for="is_public" value="Public" /> <InputLabel for="is_public" value="Public" />
</div> </div>
<div <div

View File

@@ -96,22 +96,22 @@ async function submit() {
<InputLabel for="name" value="Name" /> <InputLabel for="name" value="Name" />
<TextInput <TextInput
id="name" id="name"
class="mt-1.5 w-full" v-model="report.name"
v-model="report.name"></TextInput> class="mt-1.5 w-full"></TextInput>
</div> </div>
<div> <div>
<InputLabel for="description" value="Description" /> <InputLabel for="description" value="Description" />
<TextInput <TextInput
id="description" id="description"
class="mt-1.5 w-full" v-model="report.description"
v-model="report.description"></TextInput> class="mt-1.5 w-full"></TextInput>
</div> </div>
<InputLabel value="Visibility" /> <InputLabel value="Visibility" />
<div class="flex items-center space-x-12"> <div class="flex items-center space-x-12">
<div class="flex items-center space-x-2 px-2 py-3"> <div class="flex items-center space-x-2 px-2 py-3">
<Checkbox <Checkbox
v-model:checked="report.is_public" id="is_public"
id="is_public"></Checkbox> v-model:checked="report.is_public"></Checkbox>
<InputLabel for="is_public" value="Public" /> <InputLabel for="is_public" value="Public" />
</div> </div>
<div <div

View File

@@ -17,19 +17,19 @@ const props = defineProps<{
<MoreOptionsDropdown :label="'Actions for Project ' + props.report.name"> <MoreOptionsDropdown :label="'Actions for Project ' + props.report.name">
<div class="min-w-[150px]"> <div class="min-w-[150px]">
<button <button
@click.prevent="emit('edit')"
v-if="canUpdateReport()" v-if="canUpdateReport()"
:aria-label="'Edit Report ' + props.report.name" :aria-label="'Edit Report ' + props.report.name"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"> class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click.prevent="emit('edit')">
<PencilSquareIcon <PencilSquareIcon
class="w-5 text-icon-active"></PencilSquareIcon> class="w-5 text-icon-active"></PencilSquareIcon>
<span>Edit</span> <span>Edit</span>
</button> </button>
<button <button
@click.prevent="emit('delete')"
:aria-label="'Delete Report ' + props.report.name"
v-if="canDeleteReport()" v-if="canDeleteReport()"
class="border-b border-card-background-separator flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"> :aria-label="'Delete Report ' + props.report.name"
class="border-b border-card-background-separator flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click.prevent="emit('delete')">
<TrashIcon class="w-5 text-icon-active"></TrashIcon> <TrashIcon class="w-5 text-icon-active"></TrashIcon>
<span>Delete</span> <span>Delete</span>
</button> </button>

View File

@@ -27,8 +27,8 @@ function onSaveReportClick() {
<template> <template>
<ReportCreateModal <ReportCreateModal
:properties="reportProperties" v-model:show="showCreateReportModal"
v-model:show="showCreateReportModal"></ReportCreateModal> :properties="reportProperties"></ReportCreateModal>
<UpgradeModal v-model:show="showPremiumModal"> <UpgradeModal v-model:show="showPremiumModal">
<strong>Sharable Reports</strong> is only available in solidtime <strong>Sharable Reports</strong> is only available in solidtime
Professional. Professional.

View File

@@ -27,19 +27,19 @@ const gridTemplate = computed(() => {
:style="gridTemplate"> :style="gridTemplate">
<ReportTableHeading></ReportTableHeading> <ReportTableHeading></ReportTableHeading>
<div <div
class="col-span-5 py-24 text-center" v-if="reports.length === 0"
v-if="reports.length === 0"> class="col-span-5 py-24 text-center">
<FolderPlusIcon <FolderPlusIcon
class="w-8 text-icon-default inline pb-2"></FolderPlusIcon> class="w-8 text-icon-default inline pb-2"></FolderPlusIcon>
<h3 class="text-white font-semibold"> <h3 class="text-white font-semibold">
No shared reports found No shared reports found
</h3> </h3>
<p class="pb-5" v-if="canCreateProjects()"> <p v-if="canCreateProjects()" class="pb-5">
Create your first project now! Create your first project now!
</p> </p>
<SecondaryButton <SecondaryButton
@click="router.visit(route('reporting'))"
:icon="PlusIcon" :icon="PlusIcon"
@click="router.visit(route('reporting'))"
>Go to the overview to create a report >Go to the overview to create a report
</SecondaryButton> </SecondaryButton>
</div> </div>

View File

@@ -87,7 +87,7 @@ async function deleteReport() {
<span v-else>Copied!</span> <span v-else>Copied!</span>
</SecondaryButton> </SecondaryButton>
<button <button
class="outline-0 focus-visible:ring-2 w-6 h-6 flex items-center justify-center rounded focus-visible:ring-white/80" class="outline-0 focus-visible:ring-2 w-6 h-6 flex items-center justify-center rounded focus-visible:ring-ring"
@click="openSharableLink"> @click="openSharableLink">
<ArrowTopRightOnSquareIcon <ArrowTopRightOnSquareIcon
class="w-4 text-text-tertiary hover:text-text-secondary transition"></ArrowTopRightOnSquareIcon> class="w-4 text-text-tertiary hover:text-text-secondary transition"></ArrowTopRightOnSquareIcon>

View File

@@ -157,7 +157,7 @@ const option = ref({
:autoresize="true" :autoresize="true"
class="chart" class="chart"
:option="option" /> :option="option" />
<div class="chart flex flex-col items-center justify-center" v-else> <div v-else class="chart flex flex-col items-center justify-center">
<p class="text-lg text-white font-semibold"> <p class="text-lg text-white font-semibold">
No time entries found No time entries found
</p> </p>

View File

@@ -22,10 +22,10 @@ function downloadCurrentExport() {
<Modal <Modal
closeable closeable
max-width="lg" max-width="lg"
@close="showExportModal = false" :show="showExportModal"
:show="showExportModal"> @close="showExportModal = false">
<button <button
class="text-text-tertiary w-6 mx-auto absolute focus-visible:outline-none focus-visible:ring-2 rounded-full focus-visible:ring-white/80 transition focus-visible:text-text-primary hover:text-text-primary top-2 right-2"> class="text-text-tertiary w-6 mx-auto absolute focus-visible:outline-none focus-visible:ring-2 rounded-full focus-visible:ring-ring transition focus-visible:text-text-primary hover:text-text-primary top-2 right-2">
<XMarkIcon @click="showExportModal = false"></XMarkIcon> <XMarkIcon @click="showExportModal = false"></XMarkIcon>
</button> </button>
<div class="text-center text-text-primary py-6"> <div class="text-center text-text-primary py-6">

View File

@@ -21,6 +21,7 @@ const activeClass = computed(() => {
<template> <template>
<Badge <Badge
size="large" size="large"
tag="button"
:class=" :class="
twMerge( twMerge(
'cursor-pointer hover:bg-card-background transition flex', 'cursor-pointer hover:bg-card-background transition flex',

View File

@@ -23,7 +23,7 @@ const title = computed(() => {
:get-key-from-item="(item) => item.value" :get-key-from-item="(item) => item.value"
:get-name-for-item="(item) => item.label" :get-name-for-item="(item) => item.label"
:items="groupByOptions"> :items="groupByOptions">
<template v-slot:trigger> <template #trigger>
<Badge <Badge
size="large" size="large"
class="cursor-pointer hover:bg-card-background transition space-x-5 flex"> class="cursor-pointer hover:bg-card-background transition space-x-5 flex">

View File

@@ -35,9 +35,9 @@ const expanded = ref(false);
) )
"> ">
<GroupedItemsCountButton <GroupedItemsCountButton
v-if="entry.grouped_data && entry.grouped_data?.length > 0"
:expanded="expanded" :expanded="expanded"
@click="expanded = !expanded" @click="expanded = !expanded">
v-if="entry.grouped_data && entry.grouped_data?.length > 0">
{{ entry.grouped_data?.length }} {{ entry.grouped_data?.length }}
</GroupedItemsCountButton> </GroupedItemsCountButton>
<span> <span>
@@ -52,13 +52,13 @@ const expanded = ref(false);
</div> </div>
</div> </div>
<div <div
v-if="expanded && entry.grouped_data"
class="col-span-3 grid bg-quaternary" class="col-span-3 grid bg-quaternary"
style="grid-template-columns: 1fr 150px 150px" style="grid-template-columns: 1fr 150px 150px">
v-if="expanded && entry.grouped_data">
<ReportingRow <ReportingRow
indent
v-for="subEntry in entry.grouped_data" v-for="subEntry in entry.grouped_data"
:key="subEntry.description ?? 'none'" :key="subEntry.description ?? 'none'"
indent
:entry="subEntry"></ReportingRow> :entry="subEntry"></ReportingRow>
</div> </div>
</template> </template>

View File

@@ -10,18 +10,18 @@ defineProps<{
<template> <template>
<TabBar> <TabBar>
<TabBarItem <TabBarItem
@click="router.visit(route('reporting'))"
:active="active === 'reporting'" :active="active === 'reporting'"
@click="router.visit(route('reporting'))"
>Overview</TabBarItem >Overview</TabBarItem
> >
<TabBarItem <TabBarItem
@click="router.visit(route('reporting.detailed'))"
:active="active === 'detailed'" :active="active === 'detailed'"
@click="router.visit(route('reporting.detailed'))"
>Detailed</TabBarItem >Detailed</TabBarItem
> >
<TabBarItem <TabBarItem
@click="router.visit(route('reporting.shared'))"
:active="active === 'shared'" :active="active === 'shared'"
@click="router.visit(route('reporting.shared'))"
>Shared</TabBarItem >Shared</TabBarItem
> >
</TabBar> </TabBar>

View File

@@ -14,10 +14,10 @@ const props = defineProps<{
<template> <template>
<MoreOptionsDropdown :label="'Actions for Tag ' + props.tag.name"> <MoreOptionsDropdown :label="'Actions for Tag ' + props.tag.name">
<button <button
@click="emit('delete')"
:aria-label="'Delete Tag ' + props.tag.name" :aria-label="'Delete Tag ' + props.tag.name"
data-testid="tag_delete" data-testid="tag_delete"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"> class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click="emit('delete')">
<TrashIcon class="w-5 text-icon-active"></TrashIcon> <TrashIcon class="w-5 text-icon-active"></TrashIcon>
<span>Delete</span> <span>Delete</span>
</button> </button>

View File

@@ -19,8 +19,8 @@ const showCreateTagModal = ref(false);
<template> <template>
<TagCreateModal <TagCreateModal
:createTag v-model:show="showCreateTagModal"
v-model:show="showCreateTagModal"></TagCreateModal> :create-tag></TagCreateModal>
<div class="flow-root"> <div class="flow-root">
<div class="inline-block min-w-full align-middle"> <div class="inline-block min-w-full align-middle">
<div <div
@@ -29,18 +29,18 @@ const showCreateTagModal = ref(false);
style="grid-template-columns: 1fr 80px"> style="grid-template-columns: 1fr 80px">
<TagTableHeading></TagTableHeading> <TagTableHeading></TagTableHeading>
<div <div
class="col-span-5 py-24 text-center" v-if="tags.length === 0"
v-if="tags.length === 0"> class="col-span-5 py-24 text-center">
<FolderPlusIcon <FolderPlusIcon
class="w-8 text-icon-default inline pb-2"></FolderPlusIcon> class="w-8 text-icon-default inline pb-2"></FolderPlusIcon>
<h3 class="text-white font-semibold">No tags found</h3> <h3 class="text-white font-semibold">No tags found</h3>
<p class="pb-5" v-if="canCreateTags()"> <p v-if="canCreateTags()" class="pb-5">
Create your first tag now! Create your first tag now!
</p> </p>
<SecondaryButton <SecondaryButton
v-if="canCreateTags()" v-if="canCreateTags()"
@click="showCreateTagModal = true"
:icon="PlusIcon" :icon="PlusIcon"
@click="showCreateTagModal = true"
>Create your First Tag</SecondaryButton >Create your First Tag</SecondaryButton
> >
</div> </div>

View File

@@ -53,19 +53,19 @@ useFocus(taskNameInput, { initialValue: true });
v-model="taskName" v-model="taskName"
type="text" type="text"
placeholder="Task Name" placeholder="Task Name"
@keydown.enter="submit()"
class="mt-1 block w-full" class="mt-1 block w-full"
required required
autocomplete="taskName" /> autocomplete="taskName"
@keydown.enter="submit()" />
</div> </div>
<div class="col-span-6 sm:col-span-4"> <div class="col-span-6 sm:col-span-4">
<ProjectDropdown :modelValue="projectId"></ProjectDropdown> <ProjectDropdown :model-value="projectId"></ProjectDropdown>
</div> </div>
</div> </div>
<EstimatedTimeSection <EstimatedTimeSection
v-if="isAllowedToPerformPremiumAction()" v-if="isAllowedToPerformPremiumAction()"
@submit="submit()" v-model="estimatedTime"
v-model="estimatedTime"></EstimatedTimeSection> @submit="submit()"></EstimatedTimeSection>
</template> </template>
<template #footer> <template #footer>
<SecondaryButton @click="show = false"> Cancel </SecondaryButton> <SecondaryButton @click="show = false"> Cancel </SecondaryButton>

View File

@@ -50,16 +50,16 @@ useFocus(taskNameInput, { initialValue: true });
v-model="taskBody.name" v-model="taskBody.name"
type="text" type="text"
placeholder="Task Name" placeholder="Task Name"
@keydown.enter="submit()"
class="mt-1 block w-full" class="mt-1 block w-full"
required required
autocomplete="taskName" /> autocomplete="taskName"
@keydown.enter="submit()" />
</div> </div>
</div> </div>
<EstimatedTimeSection <EstimatedTimeSection
v-if="isAllowedToPerformPremiumAction()" v-if="isAllowedToPerformPremiumAction()"
@submit="submit()" v-model="taskBody.estimated_time"
v-model="taskBody.estimated_time"></EstimatedTimeSection> @submit="submit()"></EstimatedTimeSection>
</template> </template>
<template #footer> <template #footer>
<SecondaryButton @click="show = false"> Cancel </SecondaryButton> <SecondaryButton @click="show = false"> Cancel </SecondaryButton>

View File

@@ -21,30 +21,30 @@ const props = defineProps<{
<MoreOptionsDropdown :label="'Actions for Task ' + props.task.name"> <MoreOptionsDropdown :label="'Actions for Task ' + props.task.name">
<div class="min-w-[150px]"> <div class="min-w-[150px]">
<button <button
@click="emit('edit')"
v-if="canUpdateTasks()" v-if="canUpdateTasks()"
:aria-label="'Edit Task ' + props.task.name" :aria-label="'Edit Task ' + props.task.name"
data-testid="task_edit" data-testid="task_edit"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"> class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click="emit('edit')">
<PencilSquareIcon <PencilSquareIcon
class="w-5 text-icon-active"></PencilSquareIcon> class="w-5 text-icon-active"></PencilSquareIcon>
<span>Edit</span> <span>Edit</span>
</button> </button>
<button <button
@click="emit('done')"
v-if="canUpdateTasks()" v-if="canUpdateTasks()"
:aria-label="'Mark Task ' + props.task.name + ' as done'" :aria-label="'Mark Task ' + props.task.name + ' as done'"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"> class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click="emit('done')">
<CheckCircleIcon class="w-5 text-icon-active"></CheckCircleIcon> <CheckCircleIcon class="w-5 text-icon-active"></CheckCircleIcon>
<span v-if="props.task.is_done">Mark as active</span> <span v-if="props.task.is_done">Mark as active</span>
<span v-else>Mark as done</span> <span v-else>Mark as done</span>
</button> </button>
<button <button
@click="emit('delete')"
:aria-label="'Delete Task ' + props.task.name"
v-if="canDeleteTasks()" v-if="canDeleteTasks()"
:aria-label="'Delete Task ' + props.task.name"
data-testid="task_delete" data-testid="task_delete"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"> class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click="emit('delete')">
<TrashIcon class="w-5 text-icon-active"></TrashIcon> <TrashIcon class="w-5 text-icon-active"></TrashIcon>
<span>Delete</span> <span>Delete</span>
</button> </button>

View File

@@ -18,7 +18,7 @@ function getNameForItem(item: Task) {
<template> <template>
<MultiselectDropdown <MultiselectDropdown
searchPlaceholder="Search for a Task..." search-placeholder="Search for a Task..."
:items="tasks" :items="tasks"
:get-key-from-item="getKeyFromItem" :get-key-from-item="getKeyFromItem"
:get-name-for-item="getNameForItem"> :get-name-for-item="getNameForItem">

View File

@@ -19,8 +19,8 @@ const createTask = ref(false);
<template> <template>
<TaskCreateModal <TaskCreateModal
:project-id="props.projectId" v-model:show="createTask"
v-model:show="createTask"></TaskCreateModal> :project-id="props.projectId"></TaskCreateModal>
<div class="flow-root"> <div class="flow-root">
<div class="inline-block min-w-full align-middle"> <div class="inline-block min-w-full align-middle">
<div <div
@@ -37,18 +37,18 @@ const createTask = ref(false);
"> ">
<TaskTableHeading></TaskTableHeading> <TaskTableHeading></TaskTableHeading>
<div <div
class="col-span-5 py-24 text-center" v-if="tasks.length === 0"
v-if="tasks.length === 0"> class="col-span-5 py-24 text-center">
<PlusCircleIcon <PlusCircleIcon
class="w-8 text-icon-default inline pb-2"></PlusCircleIcon> class="w-8 text-icon-default inline pb-2"></PlusCircleIcon>
<h3 class="text-white font-semibold">No tasks found</h3> <h3 class="text-white font-semibold">No tasks found</h3>
<p class="pb-5" v-if="canCreateTasks()"> <p v-if="canCreateTasks()" class="pb-5">
Create your first task now! Create your first task now!
</p> </p>
<SecondaryButton <SecondaryButton
v-if="canCreateTasks()" v-if="canCreateTasks()"
@click="createTask = true"
:icon="PlusIcon" :icon="PlusIcon"
@click="createTask = true"
>Create your First Task >Create your First Task
</SecondaryButton> </SecondaryButton>
</div> </div>

View File

@@ -75,8 +75,8 @@ const showTaskEditModal = ref(false);
@delete="deleteTask"></TaskMoreOptionsDropdown> @delete="deleteTask"></TaskMoreOptionsDropdown>
</div> </div>
<TaskEditModal <TaskEditModal
:task="task" v-model:show="showTaskEditModal"
v-model:show="showTaskEditModal"></TaskEditModal> :task="task"></TaskEditModal>
</TableRow> </TableRow>
</template> </template>

View File

@@ -11,8 +11,8 @@ const showUpgradeModal = ref(false);
solidtime Professional. solidtime Professional.
</UpgradeModal> </UpgradeModal>
<button <button
@click.prevent="showUpgradeModal = true" class="inline-flex bg-secondary hover:bg-tertiary px-2 py-1 rounded border border-border-secondary hover:border-border-tertiary items-center space-x-1"
class="inline-flex bg-secondary hover:bg-tertiary px-2 py-1 rounded border border-border-secondary hover:border-border-tertiary items-center space-x-1"> @click.prevent="showUpgradeModal = true">
<LockClosedIcon class="w-3 text-text-tertiary"></LockClosedIcon> <LockClosedIcon class="w-3 text-text-tertiary"></LockClosedIcon>
<span class="text-xs text-text-secondary font-semibold"> Upgrade </span> <span class="text-xs text-text-secondary font-semibold"> Upgrade </span>
</button> </button>

View File

@@ -50,12 +50,12 @@ const show = defineModel('show', { default: false });
v-if="isBillingActivated() && canManageBilling()" v-if="isBillingActivated() && canManageBilling()"
href="/billing"> href="/billing">
<PrimaryButton <PrimaryButton
type="button"
class="mt-6"
:icon="CreditCardIcon"
v-if=" v-if="
isBillingActivated() && canUpdateOrganization() isBillingActivated() && canUpdateOrganization()
"> "
type="button"
class="mt-6"
:icon="CreditCardIcon">
Go to Billing Go to Billing
</PrimaryButton> </PrimaryButton>
</Link> </Link>

View File

@@ -32,8 +32,8 @@ const isRunningInDifferentOrganization = computed(() => {
<template> <template>
<div class="pt-3 pb-2.5 px-2 flex justify-between items-center relative"> <div class="pt-3 pb-2.5 px-2 flex justify-between items-center relative">
<div <div
class="absolute w-full h-full backdrop-blur-sm z-10 flex items-center justify-center" v-if="isRunningInDifferentOrganization"
v-if="isRunningInDifferentOrganization"> class="absolute w-full h-full backdrop-blur-sm z-10 flex items-center justify-center">
<div <div
class="w-full h-[calc(100%+10px)] absolute bg-default-background opacity-75 backdrop-blur-sm"></div> class="w-full h-[calc(100%+10px)] absolute bg-default-background opacity-75 backdrop-blur-sm"></div>
<div class="flex space-x-3 items-center w-full z-20 justify-center"> <div class="flex space-x-3 items-center w-full z-20 justify-center">
@@ -50,7 +50,7 @@ const isRunningInDifferentOrganization = computed(() => {
</div> </div>
<TimeTrackerStartStop <TimeTrackerStartStop
:active="isActive" :active="isActive"
@changed="setActiveState" size="base"
size="base"></TimeTrackerStartStop> @changed="setActiveState"></TimeTrackerStartStop>
</div> </div>
</template> </template>

View File

@@ -15,8 +15,8 @@ defineProps<{
<DashboardCard title="Last 7 Days" :icon="CalendarIcon"> <DashboardCard title="Last 7 Days" :icon="CalendarIcon">
<DayOverviewCardEntry <DayOverviewCardEntry
v-for="day in last7Days" v-for="day in last7Days"
:class="last7Days.length === 7 ? 'last:border-0 first:pt-3' : ''"
:key="day.date" :key="day.date"
:class="last7Days.length === 7 ? 'last:border-0 first:pt-3' : ''"
:date="day.date" :date="day.date"
:history="day.history" :history="day.history"
:duration="day.duration"></DayOverviewCardEntry> :duration="day.duration"></DayOverviewCardEntry>

View File

@@ -20,8 +20,8 @@ const props = defineProps<{
<DashboardCard title="Recently Tracked Tasks" :icon="CheckCircleIcon"> <DashboardCard title="Recently Tracked Tasks" :icon="CheckCircleIcon">
<RecentlyTrackedTasksCardEntry <RecentlyTrackedTasksCardEntry
v-for="lastTask in props.latestTasks" v-for="lastTask in props.latestTasks"
:class="props.latestTasks.length === 4 ? 'last:border-0' : ''"
:key="lastTask.id" :key="lastTask.id"
:class="props.latestTasks.length === 4 ? 'last:border-0' : ''"
:project_id="lastTask.project_id" :project_id="lastTask.project_id"
:task_id="lastTask.id" :task_id="lastTask.id"
:title="lastTask.name"></RecentlyTrackedTasksCardEntry> :title="lastTask.name"></RecentlyTrackedTasksCardEntry>

View File

@@ -29,14 +29,14 @@ const open = useSessionStorage('nav-collapse-state-' + props.title, true);
:icon :icon
:current :current
:href></NavigationSidebarLink> :href></NavigationSidebarLink>
<CollapsibleRoot v-model:open="open" v-else <CollapsibleRoot v-else v-model:open="open"
><CollapsibleTrigger class="w-full group py-0.5"> ><CollapsibleTrigger class="w-full group py-0.5">
<div <div
class="text-muted group-hover:text-white group-hover:bg-menu-active group flex gap-x-2 rounded-md transition leading-6 py-1 px-2 font-medium text-sm items-center justify-between"> class="text-muted group-hover:text-white group-hover:bg-menu-active group flex gap-x-2 rounded-md transition leading-6 py-1 px-2 font-medium text-sm items-center justify-between">
<div class="flex items-center gap-x-2"> <div class="flex items-center gap-x-2">
<component <component
v-if="icon"
:is="icon" :is="icon"
v-if="icon"
:class="[ :class="[
current current
? 'text-icon-active' ? 'text-icon-active'
@@ -59,8 +59,8 @@ const open = useSessionStorage('nav-collapse-state-' + props.title, true);
<CollapsibleContent class="CollapsibleContent"> <CollapsibleContent class="CollapsibleContent">
<div class="px-3.5"> <div class="px-3.5">
<ul <ul
class="flex min-w-0 flex-col border-l border-border-secondary px-3 w-full my-0.5" v-if="subItems"
v-if="subItems"> class="flex min-w-0 flex-col border-l border-border-secondary px-3 w-full my-0.5">
<li <li
v-for="subItem in subItems" v-for="subItem in subItems"
:key="subItem.title" :key="subItem.title"

View File

@@ -19,8 +19,8 @@ defineProps<{
'group flex gap-x-2 rounded-md transition leading-6 py-1 px-2 font-medium text-sm items-center', 'group flex gap-x-2 rounded-md transition leading-6 py-1 px-2 font-medium text-sm items-center',
]"> ]">
<component <component
v-if="icon"
:is="icon" :is="icon"
v-if="icon"
:class="[ :class="[
current current
? 'text-icon-active' ? 'text-icon-active'

View File

@@ -5,8 +5,8 @@
<div class="flex w-full flex-col items-center space-y-4 sm:items-end"> <div class="flex w-full flex-col items-center space-y-4 sm:items-end">
<Notification <Notification
v-for="notification in notifications" v-for="notification in notifications"
:type="notification.type"
:key="notification.uuid" :key="notification.uuid"
:type="notification.type"
:title="notification.title" :title="notification.title"
:message="notification.message"></Notification> :message="notification.message"></Notification>
</div> </div>

View File

@@ -110,29 +110,29 @@ const { tags } = storeToRefs(useTagsStore());
<CardTitle title="Time Tracker" :icon="ClockIcon"></CardTitle> <CardTitle title="Time Tracker" :icon="ClockIcon"></CardTitle>
<div class="relative"> <div class="relative">
<TimeTrackerRunningInDifferentOrganizationOverlay <TimeTrackerRunningInDifferentOrganizationOverlay
@switchOrganization="switchToTimeEntryOrganization"
v-if=" v-if="
isRunningInDifferentOrganization isRunningInDifferentOrganization
"></TimeTrackerRunningInDifferentOrganizationOverlay> "
@switch-organization="switchToTimeEntryOrganization"></TimeTrackerRunningInDifferentOrganizationOverlay>
<TimeTrackerControls <TimeTrackerControls
:createProject v-model:current-time-entry="currentTimeEntry"
:enableEstimatedTime="isAllowedToPerformPremiumAction()" v-model:live-timer="now"
:canCreateProject="canCreateProjects()" :create-project
:createClient :enable-estimated-time="isAllowedToPerformPremiumAction()"
:can-create-project="canCreateProjects()"
:create-client
:clients :clients
:tags :tags
:tasks :tasks
:projects :projects
:createTag :create-tag
:isActive :is-active
:currency="getOrganizationCurrencyString()" :currency="getOrganizationCurrencyString()"
v-model:currentTimeEntry="currentTimeEntry" @start-live-timer="startLiveTimer"
v-model:liveTimer="now" @stop-live-timer="stopLiveTimer"
@startLiveTimer="startLiveTimer" @start-timer="setActiveState(true)"
@stopLiveTimer="stopLiveTimer" @stop-timer="setActiveState(false)"
@startTimer="setActiveState(true)" @update-time-entry="updateTimeEntry"></TimeTrackerControls>
@stopTimer="setActiveState(false)"
@updateTimeEntry="updateTimeEntry"></TimeTrackerControls>
</div> </div>
</template> </template>

View File

@@ -12,7 +12,7 @@ function openDesktopGithubRepo() {
</script> </script>
<template> <template>
<div class="py-4 hidden lg:block" v-if="showReleaseInfo"> <div v-if="showReleaseInfo" class="py-4 hidden lg:block">
<div <div
class="rounded-lg px-2.5 py-2 bg-card-background border border-border-secondary"> class="rounded-lg px-2.5 py-2 bg-card-background border border-border-secondary">
<div class="flex items-start justify-between"> <div class="flex items-start justify-between">
@@ -23,8 +23,8 @@ function openDesktopGithubRepo() {
</div> </div>
<button> <button>
<XMarkIcon <XMarkIcon
@click="showReleaseInfo = false" class="w-3.5 text-text-tertiary hover:text-text-secondary"
class="w-3.5 text-text-tertiary hover:text-text-secondary"></XMarkIcon> @click="showReleaseInfo = false"></XMarkIcon>
</button> </button>
</div> </div>
@@ -34,9 +34,9 @@ function openDesktopGithubRepo() {
now. now.
</p> </p>
<SecondaryButton <SecondaryButton
@click="openDesktopGithubRepo"
size="small" size="small"
class="w-full text-center justify-center mt-1.5" class="w-full text-center justify-center mt-1.5"
@click="openDesktopGithubRepo"
>Download now</SecondaryButton >Download now</SecondaryButton
> >
</div> </div>

View File

@@ -85,8 +85,8 @@ const page = usePage<{
class="border-b border-default-background-separator pb-2 flex justify-between"> class="border-b border-default-background-separator pb-2 flex justify-between">
<OrganizationSwitcher class="w-full"></OrganizationSwitcher> <OrganizationSwitcher class="w-full"></OrganizationSwitcher>
<XMarkIcon <XMarkIcon
@click="showSidebarMenu = false" class="w-8 lg:hidden"
class="w-8 lg:hidden"></XMarkIcon> @click="showSidebarMenu = false"></XMarkIcon>
</div> </div>
<div class="border-b border-default-background-separator"> <div class="border-b border-default-background-separator">
<CurrentSidebarTimer></CurrentSidebarTimer> <CurrentSidebarTimer></CurrentSidebarTimer>
@@ -162,8 +162,8 @@ const page = usePage<{
route('clients') route('clients')
"></NavigationSidebarItem> "></NavigationSidebarItem>
<NavigationSidebarItem <NavigationSidebarItem
title="Members"
v-if="canViewMembers()" v-if="canViewMembers()"
title="Members"
:icon="UserGroupIcon" :icon="UserGroupIcon"
:current="route().current('members')" :current="route().current('members')"
:href=" :href="
@@ -238,8 +238,8 @@ const page = usePage<{
<div <div
class="lg:hidden w-full px-3 py-1 border-b border-b-default-background-separator text-muted flex justify-between items-center"> class="lg:hidden w-full px-3 py-1 border-b border-b-default-background-separator text-muted flex justify-between items-center">
<Bars3Icon <Bars3Icon
@click="showSidebarMenu = !showSidebarMenu" class="w-7 text-muted"
class="w-7 text-muted"></Bars3Icon> @click="showSidebarMenu = !showSidebarMenu"></Bars3Icon>
<OrganizationSwitcher></OrganizationSwitcher> <OrganizationSwitcher></OrganizationSwitcher>
</div> </div>

View File

@@ -151,7 +151,7 @@ const page = usePage<{
</InputLabel> </InputLabel>
</div> </div>
<div class="mt-4" v-if="page.props.newsletter_consent"> <div v-if="page.props.newsletter_consent" class="mt-4">
<InputLabel for="newsletter_consent"> <InputLabel for="newsletter_consent">
<div class="flex items-center"> <div class="flex items-center">
<Checkbox <Checkbox

View File

@@ -74,7 +74,7 @@ function refreshDashboardData() {
<MainContainer <MainContainer
class="grid gap-5 sm:gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 pt-3 sm:pt-5 pb-4 sm:pb-6 border-b border-default-background-separator items-stretch"> class="grid gap-5 sm:gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 pt-3 sm:pt-5 pb-4 sm:pb-6 border-b border-default-background-separator items-stretch">
<RecentlyTrackedTasksCard <RecentlyTrackedTasksCard
:latestTasks="props.latestTasks"></RecentlyTrackedTasksCard> :latest-tasks="props.latestTasks"></RecentlyTrackedTasksCard>
<LastSevenDaysCard <LastSevenDaysCard
:last7-days="props.lastSevenDays"></LastSevenDaysCard> :last7-days="props.lastSevenDays"></LastSevenDaysCard>
<ActivityGraphCard <ActivityGraphCard
@@ -84,13 +84,13 @@ function refreshDashboardData() {
<TeamActivityCard <TeamActivityCard
v-if="canViewMembers()" v-if="canViewMembers()"
class="flex lg:hidden xl:flex" class="flex lg:hidden xl:flex"
:latestTeamActivity=" :latest-team-activity="
props.latestTeamActivity props.latestTeamActivity
"></TeamActivityCard> "></TeamActivityCard>
</MainContainer> </MainContainer>
<MainContainer class="py-5"> <MainContainer class="py-5">
<ThisWeekOverview <ThisWeekOverview
:weeklyProjectOverview="props.weeklyProjectOverview" :weekly-project-overview="props.weeklyProjectOverview"
:total-weekly-billable-amount="props.totalWeeklyBillableAmount" :total-weekly-billable-amount="props.totalWeeklyBillableAmount"
:total-weekly-billable-time="props.totalWeeklyBillableTime" :total-weekly-billable-time="props.totalWeeklyBillableTime"
:total-weekly-time="props.totalWeeklyTime" :total-weekly-time="props.totalWeeklyTime"

View File

@@ -52,8 +52,8 @@ function isActiveTab(tab: string) {
>Invite member</SecondaryButton >Invite member</SecondaryButton
> >
<MemberInviteModal <MemberInviteModal
:available-roles="availableRoles"
v-model:show="inviteMember" v-model:show="inviteMember"
:available-roles="availableRoles"
@close="activeTab = 'invitations'"></MemberInviteModal> @close="activeTab = 'invitations'"></MemberInviteModal>
</MainContainer> </MainContainer>
<MemberTable v-if="activeTab === 'all'"></MemberTable> <MemberTable v-if="activeTab === 'all'"></MemberTable>

View File

@@ -204,9 +204,9 @@ const page = usePage<{
<div class="col-span-6 sm:col-span-4"> <div class="col-span-6 sm:col-span-4">
<InputLabel for="timezone" value="Timezone" /> <InputLabel for="timezone" value="Timezone" />
<select <select
name="timezone"
id="timezone" id="timezone"
v-model="form.timezone" v-model="form.timezone"
name="timezone"
required required
class="mt-1 block w-full border-input-border bg-input-background text-white focus:border-input-border-active rounded-md shadow-sm"> class="mt-1 block w-full border-input-border bg-input-background text-white focus:border-input-border-active rounded-md shadow-sm">
<option value="" disabled>Select a Timezone</option> <option value="" disabled>Select a Timezone</option>
@@ -225,9 +225,9 @@ const page = usePage<{
<div class="col-span-6 sm:col-span-4"> <div class="col-span-6 sm:col-span-4">
<InputLabel for="week_start" value="Start of the week" /> <InputLabel for="week_start" value="Start of the week" />
<select <select
name="week_start"
id="week_start" id="week_start"
v-model="form.week_start" v-model="form.week_start"
name="week_start"
required required
class="mt-1 block w-full border-input-border bg-input-background text-white focus:border-input-border-active rounded-md shadow-sm"> class="mt-1 block w-full border-input-border bg-input-background text-white 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>

View File

@@ -129,15 +129,15 @@ const shownTasks = computed(() => {
</nav> </nav>
<div> <div>
<SecondaryButton <SecondaryButton
v-if="canCreateProjects()"
:icon="PencilSquareIcon" :icon="PencilSquareIcon"
@click="showEditProjectModal = true" @click="showEditProjectModal = true">
v-if="canCreateProjects()">
Edit Project Edit Project
</SecondaryButton> </SecondaryButton>
<ProjectEditModal <ProjectEditModal
v-if="project" v-if="project"
:originalProject="project" v-model:show="showEditProjectModal"
v-model:show="showEditProjectModal"></ProjectEditModal> :original-project="project"></ProjectEditModal>
</div> </div>
</MainContainer> </MainContainer>
<MainContainer> <MainContainer>
@@ -168,8 +168,8 @@ const shownTasks = computed(() => {
>Create Task >Create Task
</SecondaryButton> </SecondaryButton>
<TaskCreateModal <TaskCreateModal
:project-id="projectId" v-model:show="createTask"
v-model:show="createTask"></TaskCreateModal> :project-id="projectId"></TaskCreateModal>
</div> </div>
</template> </template>
</CardTitle> </CardTitle>
@@ -188,11 +188,11 @@ const shownTasks = computed(() => {
Add Member Add Member
</SecondaryButton> </SecondaryButton>
<ProjectMemberCreateModal <ProjectMemberCreateModal
:project-id="projectId"
:existing-members="projectMembers"
v-model:show=" v-model:show="
createProjectMember createProjectMember
"></ProjectMemberCreateModal> "
:project-id="projectId"
:existing-members="projectMembers"></ProjectMemberCreateModal>
</template> </template>
</CardTitle> </CardTitle>
<Card> <Card>

View File

@@ -94,13 +94,13 @@ const showBillableRate = computed(() => {
>Create Project >Create Project
</SecondaryButton> </SecondaryButton>
<ProjectCreateModal <ProjectCreateModal
:createProject v-model:show="showCreateProjectModal"
:enableEstimatedTime="isAllowedToPerformPremiumAction" :create-project
:createClient :enable-estimated-time="isAllowedToPerformPremiumAction"
:create-client
:currency="getOrganizationCurrencyString()" :currency="getOrganizationCurrencyString()"
:clients="clients" :clients="clients"
@submit="createProject" @submit="createProject"></ProjectCreateModal>
v-model:show="showCreateProjectModal"></ProjectCreateModal>
</MainContainer> </MainContainer>
<ProjectTable <ProjectTable
:show-billable-rate="showBillableRate" :show-billable-rate="showBillableRate"

View File

@@ -281,7 +281,7 @@ const tableData = computed(() => {
class="overflow-hidden"> class="overflow-hidden">
<ReportingExportModal <ReportingExportModal
v-model:show="showExportModal" v-model:show="showExportModal"
:exportUrl="exportUrl"></ReportingExportModal> :export-url="exportUrl"></ReportingExportModal>
<MainContainer <MainContainer
class="py-3 sm:py-5 border-b border-default-background-separator flex justify-between items-center"> class="py-3 sm:py-5 border-b border-default-background-separator flex justify-between items-center">
<div class="flex items-center space-x-3 sm:space-x-6"> <div class="flex items-center space-x-3 sm:space-x-6">
@@ -292,7 +292,7 @@ const tableData = computed(() => {
<ReportingExportButton <ReportingExportButton
:download="downloadExport"></ReportingExportButton> :download="downloadExport"></ReportingExportButton>
<ReportSaveButton <ReportSaveButton
:reportProperties="reportProperties"></ReportSaveButton> :report-properties="reportProperties"></ReportSaveButton>
</div> </div>
</MainContainer> </MainContainer>
<div class="py-2.5 w-full border-b border-default-background-separator"> <div class="py-2.5 w-full border-b border-default-background-separator">
@@ -302,9 +302,9 @@ const tableData = computed(() => {
class="flex flex-wrap items-center space-y-2 sm:space-y-0 space-x-4"> class="flex flex-wrap items-center space-y-2 sm:space-y-0 space-x-4">
<div class="text-sm font-medium">Filters</div> <div class="text-sm font-medium">Filters</div>
<MemberMultiselectDropdown <MemberMultiselectDropdown
@submit="updateReporting" v-model="selectedMembers"
v-model="selectedMembers"> @submit="updateReporting">
<template v-slot:trigger> <template #trigger>
<ReportingFilterBadge <ReportingFilterBadge
:count="selectedMembers.length" :count="selectedMembers.length"
:active="selectedMembers.length > 0" :active="selectedMembers.length > 0"
@@ -313,9 +313,9 @@ const tableData = computed(() => {
</template> </template>
</MemberMultiselectDropdown> </MemberMultiselectDropdown>
<ProjectMultiselectDropdown <ProjectMultiselectDropdown
@submit="updateReporting" v-model="selectedProjects"
v-model="selectedProjects"> @submit="updateReporting">
<template v-slot:trigger> <template #trigger>
<ReportingFilterBadge <ReportingFilterBadge
:count="selectedProjects.length" :count="selectedProjects.length"
:active="selectedProjects.length > 0" :active="selectedProjects.length > 0"
@@ -324,9 +324,9 @@ const tableData = computed(() => {
</template> </template>
</ProjectMultiselectDropdown> </ProjectMultiselectDropdown>
<TaskMultiselectDropdown <TaskMultiselectDropdown
@submit="updateReporting" v-model="selectedTasks"
v-model="selectedTasks"> @submit="updateReporting">
<template v-slot:trigger> <template #trigger>
<ReportingFilterBadge <ReportingFilterBadge
:count="selectedTasks.length" :count="selectedTasks.length"
:active="selectedTasks.length > 0" :active="selectedTasks.length > 0"
@@ -335,20 +335,20 @@ const tableData = computed(() => {
</template> </template>
</TaskMultiselectDropdown> </TaskMultiselectDropdown>
<ClientMultiselectDropdown <ClientMultiselectDropdown
@submit="updateReporting" v-model="selectedClients"
v-model="selectedClients"> @submit="updateReporting">
<template v-slot:trigger> <template #trigger>
<ReportingFilterBadge <ReportingFilterBadge
title="Clients" title="Clients"
:icon="FolderIcon"></ReportingFilterBadge> :icon="FolderIcon"></ReportingFilterBadge>
</template> </template>
</ClientMultiselectDropdown> </ClientMultiselectDropdown>
<TagDropdown <TagDropdown
@submit="updateReporting"
:createTag
v-model="selectedTags" v-model="selectedTags"
:tags="tags"> :create-tag
<template v-slot:trigger> :tags="tags"
@submit="updateReporting">
<template #trigger>
<ReportingFilterBadge <ReportingFilterBadge
:count="selectedTags.length" :count="selectedTags.length"
:active="selectedTags.length > 0" :active="selectedTags.length > 0"
@@ -358,7 +358,6 @@ const tableData = computed(() => {
</TagDropdown> </TagDropdown>
<SelectDropdown <SelectDropdown
@changed="updateReporting"
v-model="billable" v-model="billable"
:get-key-from-item="(item) => item.value" :get-key-from-item="(item) => item.value"
:get-name-for-item="(item) => item.label" :get-name-for-item="(item) => item.label"
@@ -375,8 +374,9 @@ const tableData = computed(() => {
label: 'Non Billable', label: 'Non Billable',
value: 'false', value: 'false',
}, },
]"> ]"
<template v-slot:trigger> @changed="updateReporting">
<template #trigger>
<ReportingFilterBadge <ReportingFilterBadge
:active="billable !== null" :active="billable !== null"
:title=" :title="
@@ -399,8 +399,8 @@ const tableData = computed(() => {
<MainContainer> <MainContainer>
<div class="pt-10 w-full px-3 relative"> <div class="pt-10 w-full px-3 relative">
<ReportingChart <ReportingChart
:groupedType="aggregatedGraphTimeEntries?.grouped_type" :grouped-type="aggregatedGraphTimeEntries?.grouped_type"
:groupedData=" :grouped-data="
aggregatedGraphTimeEntries?.grouped_data aggregatedGraphTimeEntries?.grouped_data
"></ReportingChart> "></ReportingChart>
</div> </div>
@@ -413,18 +413,18 @@ const tableData = computed(() => {
class="text-sm flex text-white items-center space-x-3 font-medium px-6 border-b border-card-background-separator pb-3"> class="text-sm flex text-white items-center space-x-3 font-medium px-6 border-b border-card-background-separator pb-3">
<span>Group by</span> <span>Group by</span>
<ReportingGroupBySelect <ReportingGroupBySelect
v-model="group"
:group-by-options="groupByOptions" :group-by-options="groupByOptions"
@changed="updateTableReporting" @changed="updateTableReporting"></ReportingGroupBySelect>
v-model="group"></ReportingGroupBySelect>
<span>and</span> <span>and</span>
<ReportingGroupBySelect <ReportingGroupBySelect
v-model="subGroup"
:group-by-options=" :group-by-options="
groupByOptions.filter( groupByOptions.filter(
(el) => el.value !== group (el) => el.value !== group
) )
" "
@changed="updateTableReporting" @changed="updateTableReporting"></ReportingGroupBySelect>
v-model="subGroup"></ReportingGroupBySelect>
</div> </div>
<div <div
class="grid items-center" class="grid items-center"
@@ -473,8 +473,8 @@ const tableData = computed(() => {
</div> </div>
</template> </template>
<div <div
class="chart flex flex-col items-center justify-center py-12 col-span-3" v-else
v-else> class="chart flex flex-col items-center justify-center py-12 col-span-3">
<p class="text-lg text-white font-semibold"> <p class="text-lg text-white font-semibold">
No time entries found No time entries found
</p> </p>

View File

@@ -252,7 +252,7 @@ async function downloadExport(format: ExportFormat) {
class="overflow-hidden"> class="overflow-hidden">
<ReportingExportModal <ReportingExportModal
v-model:show="showExportModal" v-model:show="showExportModal"
:exportUrl="exportUrl"></ReportingExportModal> :export-url="exportUrl"></ReportingExportModal>
<MainContainer <MainContainer
class="py-3 sm:py-5 border-b border-default-background-separator flex justify-between items-center"> class="py-3 sm:py-5 border-b border-default-background-separator flex justify-between items-center">
<div class="flex items-center space-x-3 sm:space-x-6"> <div class="flex items-center space-x-3 sm:space-x-6">
@@ -270,9 +270,9 @@ async function downloadExport(format: ExportFormat) {
class="flex flex-wrap items-center space-y-2 sm:space-y-0 space-x-4"> class="flex flex-wrap items-center space-y-2 sm:space-y-0 space-x-4">
<div class="text-sm font-medium">Filters</div> <div class="text-sm font-medium">Filters</div>
<MemberMultiselectDropdown <MemberMultiselectDropdown
@submit="updateFilteredTimeEntries" v-model="selectedMembers"
v-model="selectedMembers"> @submit="updateFilteredTimeEntries">
<template v-slot:trigger> <template #trigger>
<ReportingFilterBadge <ReportingFilterBadge
:count="selectedMembers.length" :count="selectedMembers.length"
:active="selectedMembers.length > 0" :active="selectedMembers.length > 0"
@@ -281,9 +281,9 @@ async function downloadExport(format: ExportFormat) {
</template> </template>
</MemberMultiselectDropdown> </MemberMultiselectDropdown>
<ProjectMultiselectDropdown <ProjectMultiselectDropdown
@submit="updateFilteredTimeEntries" v-model="selectedProjects"
v-model="selectedProjects"> @submit="updateFilteredTimeEntries">
<template v-slot:trigger> <template #trigger>
<ReportingFilterBadge <ReportingFilterBadge
:count="selectedProjects.length" :count="selectedProjects.length"
:active="selectedProjects.length > 0" :active="selectedProjects.length > 0"
@@ -292,9 +292,9 @@ async function downloadExport(format: ExportFormat) {
</template> </template>
</ProjectMultiselectDropdown> </ProjectMultiselectDropdown>
<TaskMultiselectDropdown <TaskMultiselectDropdown
@submit="updateFilteredTimeEntries" v-model="selectedTasks"
v-model="selectedTasks"> @submit="updateFilteredTimeEntries">
<template v-slot:trigger> <template #trigger>
<ReportingFilterBadge <ReportingFilterBadge
:count="selectedTasks.length" :count="selectedTasks.length"
:active="selectedTasks.length > 0" :active="selectedTasks.length > 0"
@@ -303,20 +303,20 @@ async function downloadExport(format: ExportFormat) {
</template> </template>
</TaskMultiselectDropdown> </TaskMultiselectDropdown>
<ClientMultiselectDropdown <ClientMultiselectDropdown
@submit="updateFilteredTimeEntries" v-model="selectedClients"
v-model="selectedClients"> @submit="updateFilteredTimeEntries">
<template v-slot:trigger> <template #trigger>
<ReportingFilterBadge <ReportingFilterBadge
title="Clients" title="Clients"
:icon="FolderIcon"></ReportingFilterBadge> :icon="FolderIcon"></ReportingFilterBadge>
</template> </template>
</ClientMultiselectDropdown> </ClientMultiselectDropdown>
<TagDropdown <TagDropdown
@submit="updateFilteredTimeEntries"
:createTag
v-model="selectedTags" v-model="selectedTags"
:tags="tags"> :create-tag
<template v-slot:trigger> :tags="tags"
@submit="updateFilteredTimeEntries">
<template #trigger>
<ReportingFilterBadge <ReportingFilterBadge
:count="selectedTags.length" :count="selectedTags.length"
:active="selectedTags.length > 0" :active="selectedTags.length > 0"
@@ -326,7 +326,6 @@ async function downloadExport(format: ExportFormat) {
</TagDropdown> </TagDropdown>
<SelectDropdown <SelectDropdown
@changed="updateFilteredTimeEntries"
v-model="billable" v-model="billable"
:get-key-from-item="(item) => item.value" :get-key-from-item="(item) => item.value"
:get-name-for-item="(item) => item.label" :get-name-for-item="(item) => item.label"
@@ -343,8 +342,9 @@ async function downloadExport(format: ExportFormat) {
label: 'Non Billable', label: 'Non Billable',
value: 'false', value: 'false',
}, },
]"> ]"
<template v-slot:trigger> @changed="updateFilteredTimeEntries">
<template #trigger>
<ReportingFilterBadge <ReportingFilterBadge
:active="billable !== null" :active="billable !== null"
:title=" :title="
@@ -366,12 +366,9 @@ async function downloadExport(format: ExportFormat) {
</div> </div>
<TimeEntryMassActionRow <TimeEntryMassActionRow
:selected-time-entries="selectedTimeEntries" :selected-time-entries="selectedTimeEntries"
:canCreateProject="canCreateProjects()" :can-create-project="canCreateProjects()"
:enableEstimatedTime="isAllowedToPerformPremiumAction()" :enable-estimated-time="isAllowedToPerformPremiumAction()"
@submit="clearSelectionAndState"
:delete-selected="deleteSelected" :delete-selected="deleteSelected"
@select-all="selectedTimeEntries = [...timeEntries]"
@unselect-all="selectedTimeEntries = []"
:all-selected="selectedTimeEntries.length === timeEntries.length" :all-selected="selectedTimeEntries.length === timeEntries.length"
:projects="projects" :projects="projects"
:tasks="tasks" :tasks="tasks"
@@ -387,34 +384,37 @@ async function downloadExport(format: ExportFormat) {
" "
:create-project="createProject" :create-project="createProject"
:create-client="createClient" :create-client="createClient"
:createTag="createTag"></TimeEntryMassActionRow> :create-tag="createTag"
@submit="clearSelectionAndState"
@select-all="selectedTimeEntries = [...timeEntries]"
@unselect-all="selectedTimeEntries = []"></TimeEntryMassActionRow>
<div class="w-full relative"> <div class="w-full relative">
<div v-for="entry in timeEntries" :key="entry.id"> <div v-for="entry in timeEntries" :key="entry.id">
<TimeEntryRow <TimeEntryRow
:selected="selectedTimeEntries.includes(entry)" :selected="selectedTimeEntries.includes(entry)"
@selected="selectedTimeEntries.push(entry)" :can-create-project="canCreateProjects()"
:canCreateProject="canCreateProjects()" :create-client
@unselected=" :create-project
selectedTimeEntries = selectedTimeEntries.filter( :enable-estimated-time="isAllowedToPerformPremiumAction()"
(item) => item.id !== entry.id
)
"
:createClient
:createProject
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
:projects="projects" :projects="projects"
:tasks="tasks" :tasks="tasks"
:tags="tags" :tags="tags"
:clients :clients
:createTag :create-tag
:updateTimeEntry :update-time-entry
:onStartStopClick="() => startTimeEntryFromExisting(entry)" :on-start-stop-click="() => startTimeEntryFromExisting(entry)"
:deleteTimeEntry="() => deleteTimeEntries([entry])" :delete-time-entry="() => deleteTimeEntries([entry])"
:currency="getOrganizationCurrencyString()" :currency="getOrganizationCurrencyString()"
:members="members" :members="members"
showDate show-date
showMember show-member
:time-entry="entry"></TimeEntryRow> :time-entry="entry"
@selected="selectedTimeEntries.push(entry)"
@unselected="
selectedTimeEntries = selectedTimeEntries.filter(
(item) => item.id !== entry.id
)
"></TimeEntryRow>
</div> </div>
<div v-if="timeEntries.length === 0"> <div v-if="timeEntries.length === 0">
<div class="text-center pt-12"> <div class="text-center pt-12">
@@ -431,10 +431,10 @@ async function downloadExport(format: ExportFormat) {
</div> </div>
<PaginationRoot <PaginationRoot
v-model:page="currentPage"
:total="totalPages" :total="totalPages"
:items-per-page="pageLimit" :items-per-page="pageLimit"
class="flex justify-center items-center py-8" class="flex justify-center items-center py-8"
v-model:page="currentPage"
:sibling-count="1" :sibling-count="1"
show-edges> show-edges>
<PaginationList <PaginationList
@@ -485,13 +485,13 @@ async function downloadExport(format: ExportFormat) {
</template> </template>
<style lang="postcss"> <style lang="postcss">
.navigation-item { .navigation-item {
@apply bg-quaternary h-8 w-8 flex items-center justify-center rounded border border-border-primary text-text-tertiary hover:text-text-primary transition cursor-pointer hover:border-border-secondary hover:bg-secondary focus-visible:text-text-primary focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-white/80; @apply bg-quaternary h-8 w-8 flex items-center justify-center rounded border border-border-primary text-text-tertiary hover:text-text-primary transition cursor-pointer hover:border-border-secondary hover:bg-secondary focus-visible:text-text-primary focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-ring;
} }
.pagination-item { .pagination-item {
@apply bg-secondary h-8 w-8 flex items-center justify-center rounded border border-border-tertiary text-text-secondary hover:text-text-primary transition cursor-pointer hover:border-border-secondary hover:bg-secondary focus-visible:text-text-primary focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-white/80; @apply bg-secondary h-8 w-8 flex items-center justify-center rounded border border-border-tertiary text-text-secondary hover:text-text-primary transition cursor-pointer hover:border-border-secondary hover:bg-secondary focus-visible:text-text-primary focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-ring;
} }
.pagination-item[data-selected] { .pagination-item[data-selected] {
@apply text-white bg-accent-300/10 border border-accent-300/20 rounded-md font-medium hover:bg-accent-300/20 active:bg-accent-300/20 outline-0 focus-visible:ring-2 focus:ring-white/80 transition ease-in-out duration-150; @apply text-white bg-accent-300/10 border border-accent-300/20 rounded-md font-medium hover:bg-accent-300/20 active:bg-accent-300/20 outline-0 focus-visible:ring-2 focus:ring-ring transition ease-in-out duration-150;
} }
</style> </style>

View File

@@ -101,12 +101,12 @@ watch(currentPage, () => {
v-if="isBillingActivated() && canManageBilling()" v-if="isBillingActivated() && canManageBilling()"
href="/billing"> href="/billing">
<PrimaryButton <PrimaryButton
type="button"
class="mt-6"
:icon="CreditCardIcon"
v-if=" v-if="
isBillingActivated() && canUpdateOrganization() isBillingActivated() && canUpdateOrganization()
"> "
type="button"
class="mt-6"
:icon="CreditCardIcon">
Go to Billing Go to Billing
</PrimaryButton> </PrimaryButton>
</Link> </Link>
@@ -120,10 +120,10 @@ watch(currentPage, () => {
<PaginationRoot <PaginationRoot
v-if="reports.length > 0 || isAllowedToPerformPremiumAction()" v-if="reports.length > 0 || isAllowedToPerformPremiumAction()"
v-model:page="currentPage"
:total="totalPages" :total="totalPages"
:items-per-page="pageLimit" :items-per-page="pageLimit"
class="flex justify-center items-center py-8" class="flex justify-center items-center py-8"
v-model:page="currentPage"
:sibling-count="1" :sibling-count="1"
show-edges> show-edges>
<PaginationList <PaginationList
@@ -174,13 +174,13 @@ watch(currentPage, () => {
</template> </template>
<style lang="postcss"> <style lang="postcss">
.navigation-item { .navigation-item {
@apply bg-quaternary h-8 w-8 flex items-center justify-center rounded border border-border-primary text-text-tertiary hover:text-text-primary transition cursor-pointer hover:border-border-secondary hover:bg-secondary focus-visible:text-text-primary focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-white/80; @apply bg-quaternary h-8 w-8 flex items-center justify-center rounded border border-border-primary text-text-tertiary hover:text-text-primary transition cursor-pointer hover:border-border-secondary hover:bg-secondary focus-visible:text-text-primary focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-ring;
} }
.pagination-item { .pagination-item {
@apply bg-secondary h-8 w-8 flex items-center justify-center rounded border border-border-tertiary text-text-secondary hover:text-text-primary transition cursor-pointer hover:border-border-secondary hover:bg-secondary focus-visible:text-text-primary focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-white/80; @apply bg-secondary h-8 w-8 flex items-center justify-center rounded border border-border-tertiary text-text-secondary hover:text-text-primary transition cursor-pointer hover:border-border-secondary hover:bg-secondary focus-visible:text-text-primary focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-ring;
} }
.pagination-item[data-selected] { .pagination-item[data-selected] {
@apply text-white bg-accent-300/10 border border-accent-300/20 rounded-md font-medium hover:bg-accent-300/20 active:bg-accent-300/20 outline-0 focus-visible:ring-2 focus:ring-white/80 transition ease-in-out duration-150; @apply text-white bg-accent-300/10 border border-accent-300/20 rounded-md font-medium hover:bg-accent-300/20 active:bg-accent-300/20 outline-0 focus-visible:ring-2 focus:ring-ring transition ease-in-out duration-150;
} }
</style> </style>

View File

@@ -151,8 +151,8 @@ function getGroupLabel(key: string) {
<MainContainer> <MainContainer>
<div class="pt-10 w-full px-3 relative"> <div class="pt-10 w-full px-3 relative">
<ReportingChart <ReportingChart
:groupedType="aggregatedGraphTimeEntries?.grouped_type" :grouped-type="aggregatedGraphTimeEntries?.grouped_type"
:groupedData=" :grouped-data="
aggregatedGraphTimeEntries?.grouped_data aggregatedGraphTimeEntries?.grouped_data
"></ReportingChart> "></ReportingChart>
</div> </div>
@@ -217,8 +217,8 @@ function getGroupLabel(key: string) {
</div> </div>
</template> </template>
<div <div
class="chart flex flex-col items-center justify-center py-12 col-span-3" v-else
v-else> class="chart flex flex-col items-center justify-center py-12 col-span-3">
<p class="text-lg text-white font-semibold"> <p class="text-lg text-white font-semibold">
No time entries found No time entries found
</p> </p>

View File

@@ -31,9 +31,9 @@ async function createTag(tag: string) {
>Create Tag >Create Tag
</SecondaryButton> </SecondaryButton>
<TagCreateModal <TagCreateModal
:createTag="createTag" v-model:show="showCreateTagModal"
v-model:show="showCreateTagModal"></TagCreateModal> :create-tag="createTag"></TagCreateModal>
</MainContainer> </MainContainer>
<TagTable :createTag="createTag"></TagTable> <TagTable :create-tag="createTag"></TagTable>
</AppLayout> </AppLayout>
</template> </template>

View File

@@ -191,9 +191,9 @@ const showResultModal = ref(false);
<div> <div>
<InputLabel for="importType" value="Import Type" /> <InputLabel for="importType" value="Import Type" />
<select <select
name="importType"
id="importType" id="importType"
v-model="importType" v-model="importType"
name="importType"
class="mt-1 block w-full border-input-border bg-input-background text-white focus:border-input-border-active rounded-md shadow-sm"> class="mt-1 block w-full border-input-border bg-input-background text-white 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...
@@ -206,8 +206,8 @@ const showResultModal = ref(false);
</option> </option>
</select> </select>
<div <div
class="py-3 text-white" v-if="currentImporterDescription"
v-if="currentImporterDescription"> class="py-3 text-white">
<div class="font-semibold text-muted py-1"> <div class="font-semibold text-muted py-1">
Instructions: Instructions:
</div> </div>
@@ -233,12 +233,12 @@ const showResultModal = ref(false);
>Upload a Toggl/Clockify Export</span >Upload a Toggl/Clockify Export</span
> >
<input <input
ref="importFile"
id="file-upload" id="file-upload"
ref="importFile"
name="file-upload" name="file-upload"
v-on:change="updateFiles"
type="file" type="file"
class="sr-only" /> class="sr-only"
@change="updateFiles" />
</label> </label>
</div> </div>
<p class="text-xs leading-5 text-muted"> <p class="text-xs leading-5 text-muted">

View File

@@ -62,10 +62,10 @@ function checkForConfirmationModal() {
<template #form> <template #form>
<OrganizationBillableRateModal <OrganizationBillableRateModal
v-model:show="showConfirmationModal" v-model:show="showConfirmationModal"
@submit="submit"
:new-billable-rate=" :new-billable-rate="
organizationBody.billable_rate organizationBody.billable_rate
"></OrganizationBillableRateModal> "
@submit="submit"></OrganizationBillableRateModal>
<!-- Organization Owner Information --> <!-- Organization Owner Information -->
<div class="col-span-6"> <div class="col-span-6">
<div class="col-span-6 sm:col-span-4"> <div class="col-span-6 sm:col-span-4">
@@ -75,8 +75,8 @@ function checkForConfirmationModal() {
value="Organization Billable Rate" /> value="Organization Billable Rate" />
<BillableRateInput <BillableRateInput
v-if="organization" v-if="organization"
:currency="getOrganizationCurrencyString()"
v-model="organizationBody.billable_rate" v-model="organizationBody.billable_rate"
:currency="getOrganizationCurrencyString()"
name="organizationBillableRate"></BillableRateInput> name="organizationBillableRate"></BillableRateInput>
</div> </div>
</div> </div>
@@ -86,10 +86,10 @@ function checkForConfirmationModal() {
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<Checkbox <Checkbox
v-if="organization" v-if="organization"
id="organizationShowBillableRatesToEmployees"
v-model:checked=" v-model:checked="
organizationBody.employees_can_see_billable_rates organizationBody.employees_can_see_billable_rates
" "></Checkbox>
id="organizationShowBillableRatesToEmployees"></Checkbox>
<InputLabel <InputLabel
for="organizationShowBillableRatesToEmployees" for="organizationShowBillableRatesToEmployees"
value="Show Billable Rates to Employees" /> value="Show Billable Rates to Employees" />

View File

@@ -89,9 +89,9 @@ const updateTeamName = () => {
<div class="col-span-6 sm:col-span-4"> <div class="col-span-6 sm:col-span-4">
<InputLabel for="currency" value="Currency" /> <InputLabel for="currency" value="Currency" />
<select <select
name="currency"
id="currency" id="currency"
v-model="form.currency" v-model="form.currency"
name="currency"
:disabled="!permissions.canUpdateTeam" :disabled="!permissions.canUpdateTeam"
class="mt-1 block w-full border-input-border bg-input-background text-white focus:border-input-border-active rounded-md shadow-sm"> class="mt-1 block w-full border-input-border bg-input-background text-white focus:border-input-border-active rounded-md shadow-sm">
<option value="" disabled>Select a currency</option> <option value="" disabled>Select a currency</option>

View File

@@ -120,16 +120,16 @@ function deleteSelected() {
<template> <template>
<TimeEntryCreateModal <TimeEntryCreateModal
:enableEstimatedTime="isAllowedToPerformPremiumAction()" v-model:show="showManualTimeEntryModal"
:createProject="createProject" :enable-estimated-time="isAllowedToPerformPremiumAction()"
:createClient="createClient" :create-project="createProject"
:createTag="createTag" :create-client="createClient"
:createTimeEntry="createTimeEntry" :create-tag="createTag"
:create-time-entry="createTimeEntry"
:projects :projects
:tasks :tasks
:tags :tags
:clients :clients></TimeEntryCreateModal>
v-model:show="showManualTimeEntryModal"></TimeEntryCreateModal>
<AppLayout title="Dashboard" data-testid="time_view"> <AppLayout title="Dashboard" data-testid="time_view">
<MainContainer <MainContainer
class="pt-5 lg:pt-8 pb-4 lg:pb-6 border-b border-default-background-separator"> class="pt-5 lg:pt-8 pb-4 lg:pb-6 border-b border-default-background-separator">
@@ -141,8 +141,8 @@ function deleteSelected() {
<div class="pb-2 pt-2 lg:pt-0 lg:pl-4 flex justify-center"> <div class="pb-2 pt-2 lg:pt-0 lg:pl-4 flex justify-center">
<SecondaryButton <SecondaryButton
class="w-full text-center flex justify-center" class="w-full text-center flex justify-center"
@click="showManualTimeEntryModal = true"
:icon="PlusIcon" :icon="PlusIcon"
@click="showManualTimeEntryModal = true"
>Manual time entry >Manual time entry
</SecondaryButton> </SecondaryButton>
</div> </div>
@@ -150,12 +150,9 @@ function deleteSelected() {
</MainContainer> </MainContainer>
<TimeEntryMassActionRow <TimeEntryMassActionRow
:selected-time-entries="selectedTimeEntries" :selected-time-entries="selectedTimeEntries"
:enableEstimatedTime="isAllowedToPerformPremiumAction()" :enable-estimated-time="isAllowedToPerformPremiumAction()"
:canCreateProject="canCreateProjects()" :can-create-project="canCreateProjects()"
@submit="clearSelectionAndState"
:all-selected="selectedTimeEntries.length === timeEntries.length" :all-selected="selectedTimeEntries.length === timeEntries.length"
@select-all="selectedTimeEntries = [...timeEntries]"
@unselect-all="selectedTimeEntries = []"
:delete-selected="deleteSelected" :delete-selected="deleteSelected"
:projects="projects" :projects="projects"
:tasks="tasks" :tasks="tasks"
@@ -171,23 +168,26 @@ function deleteSelected() {
" "
:create-project="createProject" :create-project="createProject"
:create-client="createClient" :create-client="createClient"
:createTag="createTag"></TimeEntryMassActionRow> :create-tag="createTag"
@submit="clearSelectionAndState"
@select-all="selectedTimeEntries = [...timeEntries]"
@unselect-all="selectedTimeEntries = []"></TimeEntryMassActionRow>
<TimeEntryGroupedTable <TimeEntryGroupedTable
v-model:selected="selectedTimeEntries" v-model:selected="selectedTimeEntries"
:createProject :create-project
:enableEstimatedTime="isAllowedToPerformPremiumAction()" :enable-estimated-time="isAllowedToPerformPremiumAction()"
:canCreateProject="canCreateProjects()" :can-create-project="canCreateProjects()"
:clients :clients
:createClient :create-client
:updateTimeEntry :update-time-entry
:updateTimeEntries :update-time-entries
:deleteTimeEntries :delete-time-entries
:createTimeEntry="startTimeEntry" :create-time-entry="startTimeEntry"
:createTag :create-tag
:projects="projects" :projects="projects"
:tasks="tasks" :tasks="tasks"
:currency="getOrganizationCurrencyString()" :currency="getOrganizationCurrencyString()"
:timeEntries="timeEntries" :time-entries="timeEntries"
:tags="tags"></TimeEntryGroupedTable> :tags="tags"></TimeEntryGroupedTable>
<div v-if="timeEntries.length === 0" class="text-center pt-12"> <div v-if="timeEntries.length === 0" class="text-center pt-12">
<ClockIcon class="w-8 text-icon-default inline pb-2"></ClockIcon> <ClockIcon class="w-8 text-icon-default inline pb-2"></ClockIcon>

File diff suppressed because it is too large Load Diff

View File

@@ -57,13 +57,13 @@
"peerDependencies": { "peerDependencies": {
"@floating-ui/vue": "^1.1.4", "@floating-ui/vue": "^1.1.4",
"@heroicons/vue": "^2.1.5", "@heroicons/vue": "^2.1.5",
"@vueuse/core": "^10.11.0", "@vueuse/core": "^12.5.0",
"@zodios/core": "^10.9.6", "@zodios/core": "^10.9.6",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"parse-duration": "^1.1.0", "parse-duration": "^2.0.1",
"tailwind-merge": "^2.5.2", "tailwind-merge": "^2.5.2",
"tailwindcss": "^3.1.0", "tailwindcss": "^3.1.0",
"vue": "^3.4.38", "vue": "^3.5.0",
"vue-tsc": "^2.0.29" "vue-tsc": "^2.2.0"
} }
} }

View File

@@ -4,11 +4,11 @@ import { computed } from 'vue';
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
size: 'base' | 'large' | 'xlarge'; size?: 'base' | 'large' | 'xlarge';
tag: string; tag?: string;
class?: string; class?: string;
color: string; color?: string;
border: boolean; border?: boolean;
}>(), }>(),
{ {
size: 'base', size: 'base',
@@ -30,6 +30,13 @@ const borderClasses = computed(() => {
} }
return ''; return '';
}); });
const tagClasses = computed(() => {
if (props.tag === 'button') {
return 'hover:bg-tertiary';
}
return '';
});
</script> </script>
<template> <template>
@@ -37,9 +44,10 @@ const borderClasses = computed(() => {
:is="tag" :is="tag"
:class=" :class="
twMerge( twMerge(
tagClasses,
badgeClasses[size], badgeClasses[size],
borderClasses, borderClasses,
'rounded inline-flex items-center font-semibold text-white disabled:text-text-quaternary outline-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80', 'rounded transition inline-flex items-center font-semibold text-white disabled:text-text-quaternary outline-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
props.class props.class
) )
"> ">

View File

@@ -6,9 +6,9 @@ import { twMerge } from 'tailwind-merge';
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
type: HtmlButtonType; type?: HtmlButtonType;
icon?: Component; icon?: Component;
loading: boolean; loading?: boolean;
}>(), }>(),
{ {
type: 'submit', type: 'submit',
@@ -21,15 +21,15 @@ const props = withDefaults(
<button <button
:type="type" :type="type"
:disabled="loading" :disabled="loading"
class="inline-flex items-center px-2 sm:px-3 py-1 sm:py-2 bg-accent-300/10 border border-accent-300/20 rounded-md font-medium text-xs sm:text-sm text-white hover:bg-accent-300/20 active:bg-accent-300/20 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150"> class="inline-flex items-center px-2 sm:px-3 py-1 sm:py-2 bg-accent-300/10 border border-accent-300/20 rounded-md font-medium text-xs sm:text-sm text-white hover:bg-accent-300/20 active:bg-accent-300/20 focus:outline-none focus-visible:ring-2 focus-visible:border-transparent focus-visible:ring-ring transition ease-in-out duration-150">
<span <span
:class=" :class="
twMerge('flex items-center ', props.icon ? 'space-x-1.5' : '') twMerge('flex items-center ', props.icon ? 'space-x-1.5' : '')
"> ">
<LoadingSpinner v-if="loading"></LoadingSpinner> <LoadingSpinner v-if="loading"></LoadingSpinner>
<component <component
v-if="props.icon && !loading"
:is="props.icon" :is="props.icon"
v-if="props.icon && !loading"
class="text-text-secondary w-4 -ml-0.5 mr-1"></component> class="text-text-secondary w-4 -ml-0.5 mr-1"></component>
<span> <span>
<slot /> <slot />

View File

@@ -6,10 +6,10 @@ import LoadingSpinner from '../LoadingSpinner.vue';
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
type: HtmlButtonType; type?: HtmlButtonType;
icon?: Component; icon?: Component;
size: 'small' | 'base'; size?: 'small' | 'base';
loading: boolean; loading?: boolean;
class?: string; class?: string;
}>(), }>(),
{ {
@@ -31,7 +31,7 @@ const sizeClasses = {
:disabled="loading" :disabled="loading"
:class=" :class="
twMerge( twMerge(
'bg-button-secondary-background border border-button-secondary-border hover:bg-button-secondary-background-hover shadow-sm transition text-white rounded-lg font-semibold inline-flex items-center space-x-1.5 focus-visible:border-input-border-active focus:outline-none focus:ring-0 disabled:opacity-25 ease-in-out', 'bg-button-secondary-background border border-button-secondary-border hover:bg-button-secondary-background-hover shadow-sm transition text-white rounded-lg font-semibold inline-flex items-center space-x-1.5 focus-visible:outline-none focus-visible:border-transparent focus-visible:ring-2 focus-visible:ring-ring focus:border-transparent disabled:opacity-25 ease-in-out',
sizeClasses[props.size], sizeClasses[props.size],
props.class props.class
) )
@@ -42,8 +42,8 @@ const sizeClasses = {
"> ">
<LoadingSpinner v-if="loading"></LoadingSpinner> <LoadingSpinner v-if="loading"></LoadingSpinner>
<component <component
v-if="props.icon && !loading"
:is="props.icon" :is="props.icon"
v-if="props.icon && !loading"
class="text-text-tertiary w-4 -ml-0.5 mr-1"></component> class="text-text-tertiary w-4 -ml-0.5 mr-1"></component>
<span> <span>
<slot /> <slot />

View File

@@ -12,8 +12,8 @@ defineProps<{
<h3 <h3
class="text-white font-bold text-sm lg:text-base flex items-center space-x-2 lg:space-x-2.5"> class="text-white font-bold text-sm lg:text-base flex items-center space-x-2 lg:space-x-2.5">
<component <component
v-if="icon"
:is="icon" :is="icon"
v-if="icon"
class="w-5 lg:w-6 text-icon-default"></component> class="w-5 lg:w-6 text-icon-default"></component>
<span> <span>
{{ title }} {{ title }}

View File

@@ -127,28 +127,28 @@ const highlightedItem = computed(() => {
</script> </script>
<template> <template>
<Dropdown width="120" v-model="open" :closeOnContentClick="true"> <Dropdown v-model="open" width="120" :close-on-content-click="true">
<template #trigger> <template #trigger>
<slot name="trigger"></slot> <slot name="trigger"></slot>
</template> </template>
<template #content> <template #content>
<input <input
ref="searchInput"
:value="searchValue" :value="searchValue"
data-testid="client_dropdown_search"
class="bg-card-background border-0 placeholder-muted text-sm text-white py-2.5 focus:ring-0 border-b border-card-background-separator focus:border-card-background-separator w-full"
placeholder="Search for a client..."
@input="updateSearchValue" @input="updateSearchValue"
@keydown.enter="addClientIfNoneExists" @keydown.enter="addClientIfNoneExists"
data-testid="client_dropdown_search"
@keydown.up.prevent="moveHighlightUp" @keydown.up.prevent="moveHighlightUp"
@keydown.down.prevent="moveHighlightDown" @keydown.down.prevent="moveHighlightDown" />
ref="searchInput"
class="bg-card-background border-0 placeholder-muted text-sm text-white py-2.5 focus:ring-0 border-b border-card-background-separator focus:border-card-background-separator w-full"
placeholder="Search for a client..." />
<div ref="dropdownViewport" class="w-60 max-h-60 overflow-y-scroll"> <div ref="dropdownViewport" class="w-60 max-h-60 overflow-y-scroll">
<div <div
v-if=" v-if="
searchValue.length > 0 && filteredClients.length === 0 searchValue.length > 0 && filteredClients.length === 0
" "
@click="addClientIfNoneExists" class="bg-card-background-active"
class="bg-card-background-active"> @click="addClientIfNoneExists">
<div <div
class="flex space-x-3 items-center px-4 py-3 text-xs text-white font-medium border-t rounded-b-lg border-card-background-separator"> class="flex space-x-3 items-center px-4 py-3 text-xs text-white font-medium border-t rounded-b-lg border-card-background-separator">
<PlusCircleIcon <PlusCircleIcon
@@ -170,8 +170,8 @@ const highlightedItem = computed(() => {
:data-client-id="client.id"> :data-client-id="client.id">
<ClientDropdownItem <ClientDropdownItem
:selected="isClientSelected(client.id)" :selected="isClientSelected(client.id)"
@click="updateClient(client.id)" :name="client.name"
:name="client.name"></ClientDropdownItem> @click="updateClient(client.id)"></ClientDropdownItem>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -14,9 +14,9 @@ const emit = defineEmits(['submit']);
<InputLabel for="billable" value="Time Estimated" /> <InputLabel for="billable" value="Time Estimated" />
</div> </div>
<DurationInput <DurationInput
@submit="emit('submit')"
v-model="model" v-model="model"
class="max-w-[150px]"></DurationInput> class="max-w-[150px]"
@submit="emit('submit')"></DurationInput>
</div> </div>
</template> </template>

View File

@@ -5,7 +5,7 @@ import { computed } from 'vue';
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
expanded?: boolean; expanded?: boolean;
size: string; size?: string;
}>(), }>(),
{ {
expanded: false, expanded: false,
@@ -17,7 +17,7 @@ const expandedStatusClasses = computed(() => {
if (props.expanded) { if (props.expanded) {
return 'border-card-border border bg-card-background-active text-white'; return 'border-card-border border bg-card-background-active text-white';
} }
return 'border-card-border border bg-card-background text-muted'; return 'border-card-border border bg-card-background hover:bg-card-background-active hover:text-text-primary transition text-muted';
}); });
</script> </script>
@@ -25,7 +25,7 @@ const expandedStatusClasses = computed(() => {
<button <button
:class=" :class="
twMerge( twMerge(
'font-medium rounded flex items-center transition justify-center', 'font-medium rounded flex items-center transition justify-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:border-transparent',
expandedStatusClasses, expandedStatusClasses,
props.size props.size
) )

View File

@@ -77,13 +77,13 @@ const inputValue = ref(formatValue(model.value));
:id="name" :id="name"
ref="billableRateInput" ref="billableRateInput"
v-model="inputValue" v-model="inputValue"
@blur="updateRate($event.target.value)"
@keydown.enter="updateRate($event.target.value)"
type="text" type="text"
:name="name" :name="name"
placeholder="Billable Rate" placeholder="Billable Rate"
class="block w-full" class="block w-full"
autocomplete="teamMemberRate" /> autocomplete="teamMemberRate"
@blur="updateRate($event.target.value)"
@keydown.enter="updateRate($event.target.value)" />
<div <div
class="absolute top-0 right-0 h-full flex items-center px-4 font-medium pointer-events-none"> class="absolute top-0 right-0 h-full flex items-center px-4 font-medium pointer-events-none">
<span> <span>

View File

@@ -11,7 +11,7 @@ function toggleBillable() {
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
size: 'small' | 'base'; size?: 'small' | 'base';
}>(), }>(),
{ {
size: 'base', size: 'base',
@@ -35,19 +35,19 @@ const iconSizeClasses = computed(() => {
}); });
const iconSizeWrapperClasses = const iconSizeWrapperClasses =
props.size === 'small' ? 'w-6 sm:w-8 h-6 sm:h-8' : 'w-11 h-11'; props.size === 'small' ? 'w-6 sm:w-8 h-6 sm:h-8' : 'w-10 h-10';
</script> </script>
<template> <template>
<button <button
@click="toggleBillable"
:class=" :class="
twMerge( twMerge(
iconColorClasses, iconColorClasses,
iconSizeWrapperClasses, iconSizeWrapperClasses,
'flex-shrink-0 ring-0 focus:outline-none focus:ring-0 transition focus:bg-card-background-separator hover:bg-card-background-separator rounded-full flex items-center justify-center' 'flex-shrink-0 ring-0 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring transition focus:bg-card-background-separator hover:bg-card-background-separator rounded-full flex items-center justify-center'
) )
"> "
@click="toggleBillable">
<BillableIcon :class="iconSizeClasses"></BillableIcon> <BillableIcon :class="iconSizeClasses"></BillableIcon>
</button> </button>
</template> </template>

View File

@@ -31,9 +31,9 @@ const proxyChecked = computed({
<template> <template>
<input <input
:id="id"
v-model="proxyChecked" v-model="proxyChecked"
type="checkbox" type="checkbox"
:id="id"
:value="value" :value="value"
class="h-4 w-4 rounded bg-card-background border-input-border text-accent-500/80 focus:ring-accent-500/80" /> class="h-4 w-4 rounded bg-card-background border-input-border text-accent-500/80 focus:outline-none focus:ring-ring/50 focus-visible:outline-none focus-visible:ring-ring/50" />
</template> </template>

View File

@@ -48,10 +48,8 @@ const emit = defineEmits(['changed']);
<template> <template>
<div class="flex items-center text-muted"> <div class="flex items-center text-muted">
<input <input
id="start"
ref="datePicker" ref="datePicker"
@change="updateTempValue"
@blur="updateDate"
@keydown.enter="updateDate"
:class=" :class="
twMerge( twMerge(
'bg-input-background border text-white border-input-border focus-visible:outline-0 focus-visible:border-input-border-active focus-visible:ring-0 rounded-md', 'bg-input-background border text-white border-input-border focus-visible:outline-0 focus-visible:border-input-border-active focus-visible:ring-0 rounded-md',
@@ -59,9 +57,11 @@ const emit = defineEmits(['changed']);
) )
" "
type="date" type="date"
id="start"
name="trip-start" name="trip-start"
:value="tempDate" /> :value="tempDate"
@change="updateTempValue"
@blur="updateDate"
@keydown.enter="updateDate" />
</div> </div>
</template> </template>

View File

@@ -4,7 +4,6 @@ import {
flip, flip,
limitShift, limitShift,
type Placement, type Placement,
type ReferenceElement,
shift, shift,
useFloating, useFloating,
} from '@floating-ui/vue'; } from '@floating-ui/vue';
@@ -15,8 +14,8 @@ import { isLastLayer, layers } from '@/packages/ui/src/utils/dismissableLayer';
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
align: Placement; align?: Placement;
closeOnContentClick: boolean; closeOnContentClick?: boolean;
}>(), }>(),
{ {
align: 'bottom-start', align: 'bottom-start',
@@ -45,6 +44,11 @@ watch(open, (value) => {
layers.value.push(id); layers.value.push(id);
} else { } else {
layers.value = layers.value.filter((layer) => layer !== id); layers.value = layers.value.filter((layer) => layer !== id);
reference.value
?.querySelector<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
?.focus();
} }
}); });
@@ -69,7 +73,7 @@ function onBackgroundClick() {
open.value = false; open.value = false;
} }
const reference = ref<null | ReferenceElement>(null); const reference = ref<null | HTMLElement>(null);
const floating = ref(null); const floating = ref(null);
const { floatingStyles } = useFloating(reference, floating, { const { floatingStyles } = useFloating(reference, floating, {
placement: props.align, placement: props.align,
@@ -90,7 +94,7 @@ const { floatingStyles } = useFloating(reference, floating, {
<template> <template>
<div class="min-w-0 isolate"> <div class="min-w-0 isolate">
<div @click.prevent="toggleOpen" ref="reference" class="min-w-0"> <div ref="reference" class="min-w-0" @click.prevent="toggleOpen">
<slot name="trigger" /> <slot name="trigger" />
</div> </div>
@@ -109,8 +113,8 @@ const { floatingStyles } = useFloating(reference, floating, {
leave-to-class="transform opacity-0 scale-95"> leave-to-class="transform opacity-0 scale-95">
<div <div
v-if="open" v-if="open"
class="z-50"
ref="floating" ref="floating"
class="z-50"
:style="floatingStyles" :style="floatingStyles"
@click="onContentClick"> @click="onContentClick">
<div <div

View File

@@ -78,11 +78,11 @@ function updateTimeEntryInputValue() {
<template> <template>
<TextInput <TextInput
ref="inputField" ref="inputField"
@blur="updateDuration"
@keydown.enter="updateDuration"
v-model="temporaryCustomTimerEntry" v-model="temporaryCustomTimerEntry"
:class="twMerge('text-text-secondary', props.class)" :class="twMerge('text-text-secondary', props.class)"
type="text" /> type="text"
@blur="updateDuration"
@keydown.enter="updateDuration" />
</template> </template>
<style scoped></style> <style scoped></style>

View File

@@ -54,12 +54,12 @@ function updateAndSubmit() {
<template> <template>
<div class="relative"> <div class="relative">
<TextInput <TextInput
v-model="currentTime"
class="w-full overflow-hidden pr-14" class="w-full overflow-hidden pr-14"
placeholder="0" placeholder="0"
@focus="selectInput" @focus="selectInput"
@blur="updateDuration" @blur="updateDuration"
@keydown.enter="updateAndSubmit" @keydown.enter="updateAndSubmit">
v-model="currentTime">
</TextInput> </TextInput>
<div <div
class="absolute top-0 right-0 h-full flex items-center px-4 font-medium"> class="absolute top-0 right-0 h-full flex items-center px-4 font-medium">

Some files were not shown because too many files have changed in this diff Show More