mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-06-15 13:32:43 +01:00
Compare commits
8 Commits
feature/si
...
feature/im
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
857dc7f4a2 | ||
|
|
b71ac06cb6 | ||
|
|
0e8f9c58fd | ||
|
|
71ec1e9e0f | ||
|
|
a8318a458e | ||
|
|
84622c3fcc | ||
|
|
1a03637e31 | ||
|
|
cd207c6173 |
@@ -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'],
|
||||
}
|
||||
@@ -109,7 +109,7 @@ services:
|
||||
- sail
|
||||
- reverse-proxy
|
||||
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']
|
||||
working_dir: /src
|
||||
extra_hosts:
|
||||
|
||||
@@ -191,7 +191,7 @@ test('test that updating a the start of an existing time entry in the overview w
|
||||
'time_entry_range_selector'
|
||||
);
|
||||
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([
|
||||
page.waitForResponse(async (response) => {
|
||||
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
|
||||
);
|
||||
}),
|
||||
page
|
||||
.getByTestId('time_entry_range_end')
|
||||
.getByTestId('time_picker_input')
|
||||
.press('Enter'),
|
||||
page.getByTestId('time_entry_range_end').press('Enter'),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ test('test that starting and updating the time while running works', async ({
|
||||
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/);
|
||||
|
||||
36
eslint.config.mjs
Normal file
36
eslint.config.mjs
Normal 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
3424
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
41
package.json
41
package.json
@@ -4,53 +4,60 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint --ext .js,.vue,.ts --ignore-path .gitignore resources/js",
|
||||
"lint:fix": "eslint --fix --ext .js,.vue,.ts --ignore-path .gitignore resources/js",
|
||||
"lint": "eslint resources/js",
|
||||
"lint:fix": "eslint --fix resources/js",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.19.0",
|
||||
"@inertiajs/vue3": "^1.0.0",
|
||||
"@playwright/test": "^1.41.1",
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@types/node": "^20.11.5",
|
||||
"@vitejs/plugin-vue": "^4.5.0",
|
||||
"@types/node": "^22.10.10",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"axios": "^1.6.4",
|
||||
"eslint-plugin-unused-imports": "^3.1.0",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"laravel-vite-plugin": "^1.0.0",
|
||||
"openapi-zod-client": "^1.16.2",
|
||||
"postcss": "^8.4.47",
|
||||
"postcss-nesting": "^12.1.5",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-checker": "^0.7.2",
|
||||
"vue": "^3.4.0",
|
||||
"vue-tsc": "^2.0.28"
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.0.11",
|
||||
"vite-plugin-checker": "^0.8.0",
|
||||
"vue": "^3.5.0",
|
||||
"vue-tsc": "^2.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.6.0",
|
||||
"@floating-ui/vue": "^1.0.6",
|
||||
"@heroicons/vue": "^2.1.1",
|
||||
"@rushstack/eslint-patch": "^1.7.0",
|
||||
"@rushstack/eslint-patch": "^1.10.5",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tanstack/vue-query": "^5.56.2",
|
||||
"@tanstack/vue-query-devtools": "^5.58.0",
|
||||
"@vue/eslint-config-prettier": "^9.0.0",
|
||||
"@vue/eslint-config-typescript": "^13.0.0",
|
||||
"@vueuse/core": "^10.11.0",
|
||||
"@vueuse/integrations": "^11.1.0",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/eslint-config-typescript": "^14.3.0",
|
||||
"@vueuse/core": "^12.5.0",
|
||||
"@vueuse/integrations": "^12.5.0",
|
||||
"dayjs": "^1.11.11",
|
||||
"echarts": "^5.5.0",
|
||||
"focus-trap": "^7.6.0",
|
||||
"parse-duration": "^1.1.0",
|
||||
"parse-duration": "^2.0.1",
|
||||
"pinia": "^2.1.7",
|
||||
"radix-vue": "^1.9.6",
|
||||
"tailwind-merge": "^2.2.1",
|
||||
"vue-echarts": "^6.7.2"
|
||||
"vue-echarts": "^7.0.3"
|
||||
},
|
||||
"overrides": {
|
||||
"vite-plugin-checker": {
|
||||
"vue-tsc": "$vue-tsc"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ const showBlackFridayBanner = computed(() => {
|
||||
<span>Upgrade now</span>
|
||||
</div>
|
||||
</Link>
|
||||
<button @click="hideBlackFridayBanner = true" class="p-1">
|
||||
<button class="p-1" @click="hideBlackFridayBanner = true">
|
||||
<XMarkIcon
|
||||
class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
|
||||
</button>
|
||||
@@ -142,7 +142,7 @@ const showBlackFridayBanner = computed(() => {
|
||||
<span>Upgrade now</span>
|
||||
</div>
|
||||
</Link>
|
||||
<button @click="hideTrialBanner = true" class="p-1">
|
||||
<button class="p-1" @click="hideTrialBanner = true">
|
||||
<XMarkIcon
|
||||
class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
|
||||
</button>
|
||||
@@ -174,7 +174,7 @@ const showBlackFridayBanner = computed(() => {
|
||||
<span>Upgrade now</span>
|
||||
</div>
|
||||
</Link>
|
||||
<button @click="hideBlockedBanner = true" class="p-1">
|
||||
<button class="p-1" @click="hideBlockedBanner = true">
|
||||
<XMarkIcon
|
||||
class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
|
||||
</button>
|
||||
@@ -206,7 +206,7 @@ const showBlackFridayBanner = computed(() => {
|
||||
<span>Upgrade now</span>
|
||||
</div>
|
||||
</Link>
|
||||
<button @click="hideFreeUpgradeBanner = true" class="p-1">
|
||||
<button class="p-1" @click="hideFreeUpgradeBanner = true">
|
||||
<XMarkIcon
|
||||
class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
|
||||
</button>
|
||||
|
||||
@@ -45,10 +45,10 @@ useFocus(clientNameInput, { initialValue: true });
|
||||
v-model="client.name"
|
||||
type="text"
|
||||
placeholder="Client Name"
|
||||
@keydown.enter="submit"
|
||||
class="mt-1 block w-full"
|
||||
required
|
||||
autocomplete="clientName" />
|
||||
autocomplete="clientName"
|
||||
@keydown.enter="submit" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -46,10 +46,10 @@ useFocus(clientNameInput, { initialValue: true });
|
||||
v-model="clientBody.name"
|
||||
type="text"
|
||||
placeholder="Client Name"
|
||||
@keydown.enter="submit"
|
||||
class="mt-1 block w-full"
|
||||
required
|
||||
autocomplete="clientName" />
|
||||
autocomplete="clientName"
|
||||
@keydown.enter="submit" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -23,28 +23,28 @@ const props = defineProps<{
|
||||
<div class="min-w-[150px]">
|
||||
<button
|
||||
v-if="canUpdateClients()"
|
||||
@click="emit('edit')"
|
||||
:aria-label="'Edit Client ' + props.client.name"
|
||||
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
|
||||
class="w-5 text-icon-active"></PencilSquareIcon>
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
<button
|
||||
@click.prevent="emit('archive')"
|
||||
v-if="canUpdateClients()"
|
||||
: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>
|
||||
<span>{{ client.is_archived ? 'Unarchive' : 'Archive' }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="canDeleteClients()"
|
||||
@click="emit('delete')"
|
||||
:aria-label="'Delete Client ' + props.client.name"
|
||||
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>
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
|
||||
@@ -18,7 +18,7 @@ function getNameForItem(item: Client) {
|
||||
|
||||
<template>
|
||||
<MultiselectDropdown
|
||||
searchPlaceholder="Search for a Client..."
|
||||
search-placeholder="Search for a Client..."
|
||||
:items="clients"
|
||||
:get-key-from-item="getKeyFromItem"
|
||||
:get-name-for-item="getNameForItem">
|
||||
|
||||
@@ -25,18 +25,18 @@ const createClient = ref(false);
|
||||
style="grid-template-columns: 1fr 150px 200px 80px">
|
||||
<ClientTableHeading></ClientTableHeading>
|
||||
<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
|
||||
class="w-8 text-icon-default inline pb-2"></UserCircleIcon>
|
||||
<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!
|
||||
</p>
|
||||
<SecondaryButton
|
||||
v-if="canCreateClients()"
|
||||
@click="createClient = true"
|
||||
:icon="PlusIcon as Component"
|
||||
@click="createClient = true"
|
||||
>Create your First Client
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
|
||||
@@ -38,8 +38,8 @@ const showEditModal = ref(false);
|
||||
<template>
|
||||
<TableRow>
|
||||
<ClientEditModal
|
||||
:client="client"
|
||||
v-model:show="showEditModal"></ClientEditModal>
|
||||
v-model:show="showEditModal"
|
||||
:client="client"></ClientEditModal>
|
||||
<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">
|
||||
<span>
|
||||
|
||||
@@ -10,16 +10,16 @@ const emit = defineEmits<{
|
||||
<template>
|
||||
<MoreOptionsDropdown label="Actions for the invitation">
|
||||
<button
|
||||
@click="emit('resend')"
|
||||
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>
|
||||
<span>Resend Invitation</span>
|
||||
</button>
|
||||
<button
|
||||
@click="emit('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>
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
|
||||
@@ -18,10 +18,10 @@ defineEmits<{
|
||||
|
||||
<template>
|
||||
<BillableRateModal
|
||||
@submit="$emit('submit')"
|
||||
v-model:show="show"
|
||||
v-model:saving="saving"
|
||||
title="Update Member Billable Rate">
|
||||
title="Update Member Billable Rate"
|
||||
@submit="$emit('submit')">
|
||||
<p class="py-1 text-center">
|
||||
The billable rate of {{ memberName }} will be updated to
|
||||
<strong>{{
|
||||
|
||||
@@ -17,8 +17,8 @@ const model = defineModel<string>({
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
hiddenMembers: ProjectMember[];
|
||||
disabled: boolean;
|
||||
hiddenMembers?: ProjectMember[];
|
||||
disabled?: boolean;
|
||||
}>(),
|
||||
{
|
||||
hiddenMembers: () => [] as ProjectMember[],
|
||||
@@ -76,7 +76,7 @@ const currentValue = computed(() => {
|
||||
:items="filteredMembers"
|
||||
:get-key-from-item="(member) => member.id"
|
||||
:get-name-for-item="(member) => member.name">
|
||||
<template v-slot:trigger>
|
||||
<template #trigger>
|
||||
<Badge
|
||||
tag="button"
|
||||
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">
|
||||
{{ currentValue }}
|
||||
</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>
|
||||
</Badge>
|
||||
</template>
|
||||
|
||||
@@ -108,11 +108,11 @@ const roleDescription = computed(() => {
|
||||
v-model:saving="saving"
|
||||
v-model:show="showBillableRateModal"
|
||||
:member-name="member.name"
|
||||
:newBillableRate="memberBody.billable_rate"
|
||||
:new-billable-rate="memberBody.billable_rate"
|
||||
@submit="submitBillableRate"></MemberBillableRateModal>
|
||||
<MemberOwnershipTransferConfirmModal
|
||||
:member-name="member.name"
|
||||
v-model:show="showOwnershipTransferConfirmModal"
|
||||
:member-name="member.name"
|
||||
@submit="submit"></MemberOwnershipTransferConfirmModal>
|
||||
<DialogModal closeable :show="show" @close="show = false">
|
||||
<template #title>
|
||||
@@ -127,9 +127,9 @@ const roleDescription = computed(() => {
|
||||
<div>
|
||||
<InputLabel for="role" value="Role" />
|
||||
<MemberRoleSelect
|
||||
v-model="memberBody.role"
|
||||
class="mt-2"
|
||||
name="role"
|
||||
v-model="memberBody.role"></MemberRoleSelect>
|
||||
name="role"></MemberRoleSelect>
|
||||
</div>
|
||||
<div class="flex-1 text-xs flex items-center pt-6">
|
||||
<p>{{ roleDescription }}</p>
|
||||
@@ -140,28 +140,28 @@ const roleDescription = computed(() => {
|
||||
<div>
|
||||
<InputLabel for="billableType" value="Billable" />
|
||||
<MemberBillableSelect
|
||||
class="mt-2"
|
||||
name="billableType"
|
||||
v-model="
|
||||
billableRateSelect
|
||||
"></MemberBillableSelect>
|
||||
"
|
||||
class="mt-2"
|
||||
name="billableType"></MemberBillableSelect>
|
||||
</div>
|
||||
<div
|
||||
class="flex-1"
|
||||
v-if="billableRateSelect === 'custom-rate'">
|
||||
v-if="billableRateSelect === 'custom-rate'"
|
||||
class="flex-1">
|
||||
<InputLabel
|
||||
for="memberBillableRate"
|
||||
class="mb-2"
|
||||
value="Billable Rate" />
|
||||
<BillableRateInput
|
||||
v-model="
|
||||
memberBody.billable_rate
|
||||
"
|
||||
focus
|
||||
class="w-full"
|
||||
:currency="getOrganizationCurrencyString()"
|
||||
@keydown.enter="saveWithChecks()"
|
||||
name="memberBillableRate"
|
||||
v-model="
|
||||
memberBody.billable_rate
|
||||
"></BillableRateInput>
|
||||
@keydown.enter="saveWithChecks()"></BillableRateInput>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -112,11 +112,11 @@ useFocus(clientNameInput, { initialValue: true });
|
||||
v-if="isBillingActivated() && canManageBilling()"
|
||||
href="/billing">
|
||||
<PrimaryButton
|
||||
type="button"
|
||||
class="mt-6"
|
||||
v-if="
|
||||
isBillingActivated() && canUpdateOrganization()
|
||||
">
|
||||
"
|
||||
type="button"
|
||||
class="mt-6">
|
||||
<CreditCardIcon class="w-5 h-5 me-2" />
|
||||
Go to Billing
|
||||
</PrimaryButton>
|
||||
@@ -128,15 +128,15 @@ useFocus(clientNameInput, { initialValue: true });
|
||||
<InputLabel for="email" value="Email" />
|
||||
<TextInput
|
||||
id="email"
|
||||
name="email"
|
||||
ref="memberEmailInput"
|
||||
v-model="addTeamMemberForm.email"
|
||||
name="email"
|
||||
type="text"
|
||||
placeholder="Member Email"
|
||||
@keydown.enter="submit"
|
||||
class="mt-1 block w-full"
|
||||
required
|
||||
autocomplete="memberName" />
|
||||
autocomplete="memberName"
|
||||
@keydown.enter="submit" />
|
||||
<InputError :message="errors.email" class="mt-2" />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -20,19 +20,19 @@ const props = defineProps<{
|
||||
<div class="min-w-[150px]">
|
||||
<button
|
||||
v-if="canUpdateMembers()"
|
||||
@click="emit('edit')"
|
||||
: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
|
||||
class="w-5 text-icon-active"></PencilSquareIcon>
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="canDeleteMembers()"
|
||||
@click="emit('delete')"
|
||||
:aria-label="'Delete Member ' + props.member.name"
|
||||
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>
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
|
||||
@@ -18,7 +18,7 @@ function getNameForItem(item: Member) {
|
||||
|
||||
<template>
|
||||
<MultiselectDropdown
|
||||
searchPlaceholder="Search for a Member..."
|
||||
search-placeholder="Search for a Member..."
|
||||
:items="members"
|
||||
:get-key-from-item="getKeyFromItem"
|
||||
:get-name-for-item="getNameForItem">
|
||||
|
||||
@@ -36,9 +36,9 @@ const emit = defineEmits<{
|
||||
<SecondaryButton @click="show = false"> Cancel</SecondaryButton>
|
||||
<PrimaryButton
|
||||
class="ms-3"
|
||||
@click="emit('submit')"
|
||||
:class="{ 'opacity-25': saving }"
|
||||
:disabled="saving">
|
||||
:disabled="saving"
|
||||
@click="emit('submit')">
|
||||
Confirm Transfer
|
||||
</PrimaryButton>
|
||||
</template>
|
||||
|
||||
@@ -89,8 +89,8 @@ async function invitePlaceholder(id: string) {
|
||||
member.is_placeholder === true &&
|
||||
canInvitePlaceholderMembers()
|
||||
"
|
||||
@click="invitePlaceholder(member.id)"
|
||||
size="small"
|
||||
@click="invitePlaceholder(member.id)"
|
||||
>Invite</SecondaryButton
|
||||
>
|
||||
<MemberMoreOptionsDropdown
|
||||
@@ -99,8 +99,8 @@ async function invitePlaceholder(id: string) {
|
||||
@delete="removeMember"></MemberMoreOptionsDropdown>
|
||||
</div>
|
||||
<MemberEditModal
|
||||
:member="member"
|
||||
v-model:show="showEditMemberModal"></MemberEditModal>
|
||||
v-model:show="showEditMemberModal"
|
||||
:member="member"></MemberEditModal>
|
||||
</TableRow>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -34,8 +34,8 @@
|
||||
<div class="ml-4 flex flex-shrink-0">
|
||||
<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>
|
||||
<XMarkIcon class="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
@@ -17,10 +17,10 @@ defineEmits<{
|
||||
|
||||
<template>
|
||||
<BillableRateModal
|
||||
@submit="$emit('submit')"
|
||||
v-model:show="show"
|
||||
v-model:saving="saving"
|
||||
title="Update Organization Billable Rate">
|
||||
title="Update Organization Billable Rate"
|
||||
@submit="$emit('submit')">
|
||||
<p class="py-0.5 text-center">
|
||||
The organization billable rate will be updated to
|
||||
<strong>{{
|
||||
|
||||
@@ -40,7 +40,7 @@ const shownProjects = computed(() => {
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
border: boolean;
|
||||
border?: boolean;
|
||||
}>(),
|
||||
{
|
||||
border: true,
|
||||
@@ -123,17 +123,17 @@ function updateValue(project: Project) {
|
||||
<template #content>
|
||||
<ComboboxRoot
|
||||
:open="open"
|
||||
:modelValue="currentProject"
|
||||
@update:modelValue="updateValue"
|
||||
@update:searchTerm="(e) => console.log(e)"
|
||||
:searchTerm="searchValue"
|
||||
class="relative">
|
||||
:model-value="currentProject"
|
||||
:search-term="searchValue"
|
||||
class="relative"
|
||||
@update:model-value="updateValue"
|
||||
@update:search-term="(e) => console.log(e)">
|
||||
<ComboboxAnchor>
|
||||
<ComboboxInput
|
||||
@keydown.enter="addProjectIfNoneExists"
|
||||
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 project..." />
|
||||
placeholder="Search for a project..."
|
||||
@keydown.enter="addProjectIfNoneExists" />
|
||||
</ComboboxAnchor>
|
||||
<ComboboxContent>
|
||||
<ComboboxViewport
|
||||
|
||||
@@ -90,8 +90,8 @@ async function submitBillableRate() {
|
||||
<div class="text-center">
|
||||
<InputLabel for="color" value="Color" />
|
||||
<ProjectColorSelector
|
||||
class="mt-1"
|
||||
v-model="project.color"></ProjectColorSelector>
|
||||
v-model="project.color"
|
||||
class="mt-1"></ProjectColorSelector>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
@@ -102,18 +102,18 @@ async function submitBillableRate() {
|
||||
v-model="project.name"
|
||||
type="text"
|
||||
placeholder="Project Name"
|
||||
@keydown.enter="submit()"
|
||||
class="mt-1 block w-full"
|
||||
required
|
||||
autocomplete="projectName" />
|
||||
autocomplete="projectName"
|
||||
@keydown.enter="submit()" />
|
||||
</div>
|
||||
<div class="">
|
||||
<InputLabel for="client" value="Client" />
|
||||
<ClientDropdown
|
||||
:createClient
|
||||
v-model="project.client_id"
|
||||
:create-client
|
||||
:clients="clients"
|
||||
class="mt-1"
|
||||
v-model="project.client_id">
|
||||
class="mt-1">
|
||||
<template #trigger>
|
||||
<Badge
|
||||
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>
|
||||
<ProjectEditBillableSection
|
||||
@submit="submit"
|
||||
:currency="getOrganizationCurrencyString()"
|
||||
v-model:isBillable="project.is_billable"
|
||||
v-model:billableRate="
|
||||
v-model:is-billable="project.is_billable"
|
||||
v-model:billable-rate="
|
||||
project.billable_rate
|
||||
"></ProjectEditBillableSection>
|
||||
"
|
||||
:currency="getOrganizationCurrencyString()"
|
||||
@submit="submit"></ProjectEditBillableSection>
|
||||
</div>
|
||||
<div>
|
||||
<EstimatedTimeSection
|
||||
v-if="isAllowedToPerformPremiumAction()"
|
||||
@submit="submit()"
|
||||
v-model="project.estimated_time"></EstimatedTimeSection>
|
||||
v-model="project.estimated_time"
|
||||
@submit="submit()"></EstimatedTimeSection>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -163,9 +163,9 @@ async function submitBillableRate() {
|
||||
<ProjectBillableRateModal
|
||||
v-model:show="showBillableRateModal"
|
||||
:currency="getOrganizationCurrencyString()"
|
||||
@submit="submitBillableRate"
|
||||
:new-billable-rate="project.billable_rate"
|
||||
:project-name="project.name"></ProjectBillableRateModal>
|
||||
:project-name="project.name"
|
||||
@submit="submitBillableRate"></ProjectBillableRateModal>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -21,29 +21,29 @@ const props = defineProps<{
|
||||
<MoreOptionsDropdown :label="'Actions for Project ' + props.project.name">
|
||||
<div class="min-w-[150px]">
|
||||
<button
|
||||
@click.prevent="emit('edit')"
|
||||
v-if="canUpdateProjects()"
|
||||
:aria-label="'Edit Project ' + props.project.name"
|
||||
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
|
||||
class="w-5 text-icon-active"></PencilSquareIcon>
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
<button
|
||||
@click.prevent="emit('archive')"
|
||||
v-if="canUpdateProjects()"
|
||||
: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>
|
||||
<span>{{ project.is_archived ? 'Unarchive' : 'Archive' }}</span>
|
||||
</button>
|
||||
<button
|
||||
@click.prevent="emit('delete')"
|
||||
v-if="canDeleteProjects()"
|
||||
:aria-label="'Delete Project ' + props.project.name"
|
||||
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>
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
|
||||
@@ -18,7 +18,7 @@ function getNameForItem(item: Project) {
|
||||
|
||||
<template>
|
||||
<MultiselectDropdown
|
||||
searchPlaceholder="Search for a Project..."
|
||||
search-placeholder="Search for a Project..."
|
||||
:items="projects"
|
||||
:get-key-from-item="getKeyFromItem"
|
||||
:get-name-for-item="getNameForItem">
|
||||
|
||||
@@ -44,12 +44,12 @@ import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
||||
|
||||
<template>
|
||||
<ProjectCreateModal
|
||||
:createProject
|
||||
:createClient
|
||||
v-model:show="showCreateProjectModal"
|
||||
:create-project
|
||||
:create-client
|
||||
:currency="getOrganizationCurrencyString()"
|
||||
:clients="clients"
|
||||
:enableEstimatedTime="isAllowedToPerformPremiumAction"
|
||||
v-model:show="showCreateProjectModal"></ProjectCreateModal>
|
||||
:enable-estimated-time="isAllowedToPerformPremiumAction"></ProjectCreateModal>
|
||||
<div class="flow-root max-w-[100vw] overflow-x-auto">
|
||||
<div class="inline-block min-w-full align-middle">
|
||||
<div
|
||||
@@ -57,12 +57,12 @@ import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
||||
class="grid min-w-full"
|
||||
:style="gridTemplate">
|
||||
<ProjectTableHeading
|
||||
:showBillableRate="
|
||||
:show-billable-rate="
|
||||
props.showBillableRate
|
||||
"></ProjectTableHeading>
|
||||
<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
|
||||
class="w-8 text-icon-default inline pb-2"></FolderPlusIcon>
|
||||
<h3 class="text-white font-semibold">
|
||||
@@ -81,14 +81,14 @@ import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
||||
</p>
|
||||
<SecondaryButton
|
||||
v-if="canCreateProjects()"
|
||||
@click="showCreateProjectModal = true"
|
||||
:icon="PlusIcon"
|
||||
@click="showCreateProjectModal = true"
|
||||
>Create your First Project
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
<template v-for="project in projects" :key="project.id">
|
||||
<ProjectTableRow
|
||||
:showBillableRate="props.showBillableRate"
|
||||
:show-billable-rate="props.showBillableRate"
|
||||
:project="project"></ProjectTableRow>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -19,8 +19,8 @@ defineProps<{
|
||||
Progress
|
||||
</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
|
||||
</div>
|
||||
<div class="px-3 py-1.5 text-left font-semibold text-white">Status</div>
|
||||
|
||||
@@ -83,8 +83,8 @@ const showEditProjectModal = ref(false);
|
||||
</div>
|
||||
<div class="whitespace-nowrap min-w-0 px-3 py-4 text-sm text-muted">
|
||||
<div
|
||||
class="overflow-ellipsis overflow-hidden"
|
||||
v-if="project.client_id">
|
||||
v-if="project.client_id"
|
||||
class="overflow-ellipsis overflow-hidden">
|
||||
{{ client?.name }}
|
||||
</div>
|
||||
<div v-else>No client</div>
|
||||
@@ -106,8 +106,8 @@ const showEditProjectModal = ref(false);
|
||||
<span v-else> -- </span>
|
||||
</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 }}
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -18,10 +18,10 @@ defineEmits<{
|
||||
|
||||
<template>
|
||||
<BillableRateModal
|
||||
@submit="$emit('submit')"
|
||||
v-model:show="show"
|
||||
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">
|
||||
The billable rate of {{ memberName }} will be updated to
|
||||
<strong>{{
|
||||
|
||||
@@ -52,16 +52,16 @@ useFocus(projectNameInput, { initialValue: true });
|
||||
<div class="grid grid-cols-3 items-center space-x-4">
|
||||
<div class="col-span-3 sm:col-span-2">
|
||||
<MemberCombobox
|
||||
:hidden-members="props.existingMembers"
|
||||
v-model="projectMember.member_id"></MemberCombobox>
|
||||
v-model="projectMember.member_id"
|
||||
:hidden-members="props.existingMembers"></MemberCombobox>
|
||||
</div>
|
||||
<div class="col-span-3 sm:col-span-1 flex-1">
|
||||
<BillableRateInput
|
||||
name="billable_rate"
|
||||
:currency="getOrganizationCurrencyString()"
|
||||
v-model="
|
||||
projectMember.billable_rate
|
||||
"></BillableRateInput>
|
||||
"
|
||||
name="billable_rate"
|
||||
:currency="getOrganizationCurrencyString()"></BillableRateInput>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -75,8 +75,8 @@ useFocus(projectNameInput, { initialValue: true });
|
||||
|
||||
<template #content>
|
||||
<ProjectMemberBillableRateModal
|
||||
:member-name="props.name"
|
||||
v-model:show="showBillableRateModal"
|
||||
:member-name="props.name"
|
||||
:new-billable-rate="projectMemberBody.billable_rate"
|
||||
@close="showBillableRateModal = false"
|
||||
@submit="submitBillableRate"></ProjectMemberBillableRateModal>
|
||||
@@ -92,12 +92,12 @@ useFocus(projectNameInput, { initialValue: true });
|
||||
class="mb-2"
|
||||
value="Billable Rate"></InputLabel>
|
||||
<BillableRateInput
|
||||
@keydown.enter="submit"
|
||||
:currency="getOrganizationCurrencyString()"
|
||||
name="billable_rate"
|
||||
v-model="
|
||||
projectMemberBody.billable_rate
|
||||
"></BillableRateInput>
|
||||
"
|
||||
:currency="getOrganizationCurrencyString()"
|
||||
name="billable_rate"
|
||||
@keydown.enter="submit"></BillableRateInput>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -27,17 +27,17 @@ const currentMember = computed(() => {
|
||||
<MoreOptionsDropdown
|
||||
:label="'Actions for Project Member ' + currentMember?.name">
|
||||
<button
|
||||
@click.prevent="emit('edit')"
|
||||
: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>
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
<button
|
||||
@click.prevent="emit('delete')"
|
||||
:aria-label="'Delete Project Member ' + currentMember?.name"
|
||||
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>
|
||||
<span>Remove from Team</span>
|
||||
</button>
|
||||
|
||||
@@ -18,9 +18,9 @@ const createProjectMember = ref(false);
|
||||
|
||||
<template>
|
||||
<ProjectMemberCreateModal
|
||||
v-model:show="createProjectMember"
|
||||
:existing-members="projectMembers"
|
||||
:project-id="projectId"
|
||||
v-model:show="createProjectMember"></ProjectMemberCreateModal>
|
||||
:project-id="projectId"></ProjectMemberCreateModal>
|
||||
<div class="flow-root">
|
||||
<div class="inline-block min-w-full align-middle">
|
||||
<div
|
||||
@@ -29,15 +29,15 @@ const createProjectMember = ref(false);
|
||||
style="grid-template-columns: 1fr 150px 150px 80px">
|
||||
<ProjectMemberTableHeading></ProjectMemberTableHeading>
|
||||
<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
|
||||
class="w-8 text-icon-default inline pb-2"></UserGroupIcon>
|
||||
<h3 class="text-white font-semibold">No project members</h3>
|
||||
<p class="pb-5">Add the first project member!</p>
|
||||
<SecondaryButton
|
||||
@click="createProjectMember = true"
|
||||
:icon="PlusIcon"
|
||||
@click="createProjectMember = true"
|
||||
>Add a new Project Member
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
|
||||
@@ -37,8 +37,8 @@ const showEditModal = ref(false);
|
||||
<template>
|
||||
<TableRow>
|
||||
<ProjectMemberEditModal
|
||||
:name="member?.name"
|
||||
v-model:show="showEditModal"
|
||||
:name="member?.name"
|
||||
:project-member="projectMember"></ProjectMemberEditModal>
|
||||
<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">
|
||||
|
||||
@@ -82,22 +82,22 @@ async function submit() {
|
||||
<InputLabel for="name" value="Name" />
|
||||
<TextInput
|
||||
id="name"
|
||||
class="mt-1.5 w-full"
|
||||
v-model="report.name"></TextInput>
|
||||
v-model="report.name"
|
||||
class="mt-1.5 w-full"></TextInput>
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel for="description" value="Description" />
|
||||
<TextInput
|
||||
id="description"
|
||||
class="mt-1.5 w-full"
|
||||
v-model="report.description"></TextInput>
|
||||
v-model="report.description"
|
||||
class="mt-1.5 w-full"></TextInput>
|
||||
</div>
|
||||
<InputLabel value="Visibility" />
|
||||
<div class="flex items-center space-x-12">
|
||||
<div class="flex items-center space-x-3 px-2 py-3">
|
||||
<Checkbox
|
||||
v-model:checked="report.is_public"
|
||||
id="is_public"></Checkbox>
|
||||
id="is_public"
|
||||
v-model:checked="report.is_public"></Checkbox>
|
||||
<InputLabel for="is_public" value="Public" />
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -96,22 +96,22 @@ async function submit() {
|
||||
<InputLabel for="name" value="Name" />
|
||||
<TextInput
|
||||
id="name"
|
||||
class="mt-1.5 w-full"
|
||||
v-model="report.name"></TextInput>
|
||||
v-model="report.name"
|
||||
class="mt-1.5 w-full"></TextInput>
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel for="description" value="Description" />
|
||||
<TextInput
|
||||
id="description"
|
||||
class="mt-1.5 w-full"
|
||||
v-model="report.description"></TextInput>
|
||||
v-model="report.description"
|
||||
class="mt-1.5 w-full"></TextInput>
|
||||
</div>
|
||||
<InputLabel value="Visibility" />
|
||||
<div class="flex items-center space-x-12">
|
||||
<div class="flex items-center space-x-2 px-2 py-3">
|
||||
<Checkbox
|
||||
v-model:checked="report.is_public"
|
||||
id="is_public"></Checkbox>
|
||||
id="is_public"
|
||||
v-model:checked="report.is_public"></Checkbox>
|
||||
<InputLabel for="is_public" value="Public" />
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -17,19 +17,19 @@ const props = defineProps<{
|
||||
<MoreOptionsDropdown :label="'Actions for Project ' + props.report.name">
|
||||
<div class="min-w-[150px]">
|
||||
<button
|
||||
@click.prevent="emit('edit')"
|
||||
v-if="canUpdateReport()"
|
||||
: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
|
||||
class="w-5 text-icon-active"></PencilSquareIcon>
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
<button
|
||||
@click.prevent="emit('delete')"
|
||||
:aria-label="'Delete Report ' + props.report.name"
|
||||
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>
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
|
||||
@@ -27,8 +27,8 @@ function onSaveReportClick() {
|
||||
|
||||
<template>
|
||||
<ReportCreateModal
|
||||
:properties="reportProperties"
|
||||
v-model:show="showCreateReportModal"></ReportCreateModal>
|
||||
v-model:show="showCreateReportModal"
|
||||
:properties="reportProperties"></ReportCreateModal>
|
||||
<UpgradeModal v-model:show="showPremiumModal">
|
||||
<strong>Sharable Reports</strong> is only available in solidtime
|
||||
Professional.
|
||||
|
||||
@@ -27,19 +27,19 @@ const gridTemplate = computed(() => {
|
||||
:style="gridTemplate">
|
||||
<ReportTableHeading></ReportTableHeading>
|
||||
<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
|
||||
class="w-8 text-icon-default inline pb-2"></FolderPlusIcon>
|
||||
<h3 class="text-white font-semibold">
|
||||
No shared reports found
|
||||
</h3>
|
||||
<p class="pb-5" v-if="canCreateProjects()">
|
||||
<p v-if="canCreateProjects()" class="pb-5">
|
||||
Create your first project now!
|
||||
</p>
|
||||
<SecondaryButton
|
||||
@click="router.visit(route('reporting'))"
|
||||
:icon="PlusIcon"
|
||||
@click="router.visit(route('reporting'))"
|
||||
>Go to the overview to create a report
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
|
||||
@@ -87,7 +87,7 @@ async function deleteReport() {
|
||||
<span v-else>Copied!</span>
|
||||
</SecondaryButton>
|
||||
<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">
|
||||
<ArrowTopRightOnSquareIcon
|
||||
class="w-4 text-text-tertiary hover:text-text-secondary transition"></ArrowTopRightOnSquareIcon>
|
||||
|
||||
@@ -157,7 +157,7 @@ const option = ref({
|
||||
:autoresize="true"
|
||||
class="chart"
|
||||
: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">
|
||||
No time entries found
|
||||
</p>
|
||||
|
||||
@@ -22,10 +22,10 @@ function downloadCurrentExport() {
|
||||
<Modal
|
||||
closeable
|
||||
max-width="lg"
|
||||
@close="showExportModal = false"
|
||||
:show="showExportModal">
|
||||
:show="showExportModal"
|
||||
@close="showExportModal = false">
|
||||
<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>
|
||||
</button>
|
||||
<div class="text-center text-text-primary py-6">
|
||||
|
||||
@@ -21,6 +21,7 @@ const activeClass = computed(() => {
|
||||
<template>
|
||||
<Badge
|
||||
size="large"
|
||||
tag="button"
|
||||
:class="
|
||||
twMerge(
|
||||
'cursor-pointer hover:bg-card-background transition flex',
|
||||
|
||||
@@ -23,7 +23,7 @@ const title = computed(() => {
|
||||
:get-key-from-item="(item) => item.value"
|
||||
:get-name-for-item="(item) => item.label"
|
||||
:items="groupByOptions">
|
||||
<template v-slot:trigger>
|
||||
<template #trigger>
|
||||
<Badge
|
||||
size="large"
|
||||
class="cursor-pointer hover:bg-card-background transition space-x-5 flex">
|
||||
|
||||
@@ -35,9 +35,9 @@ const expanded = ref(false);
|
||||
)
|
||||
">
|
||||
<GroupedItemsCountButton
|
||||
v-if="entry.grouped_data && entry.grouped_data?.length > 0"
|
||||
:expanded="expanded"
|
||||
@click="expanded = !expanded"
|
||||
v-if="entry.grouped_data && entry.grouped_data?.length > 0">
|
||||
@click="expanded = !expanded">
|
||||
{{ entry.grouped_data?.length }}
|
||||
</GroupedItemsCountButton>
|
||||
<span>
|
||||
@@ -52,13 +52,13 @@ const expanded = ref(false);
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="expanded && entry.grouped_data"
|
||||
class="col-span-3 grid bg-quaternary"
|
||||
style="grid-template-columns: 1fr 150px 150px"
|
||||
v-if="expanded && entry.grouped_data">
|
||||
style="grid-template-columns: 1fr 150px 150px">
|
||||
<ReportingRow
|
||||
indent
|
||||
v-for="subEntry in entry.grouped_data"
|
||||
:key="subEntry.description ?? 'none'"
|
||||
indent
|
||||
:entry="subEntry"></ReportingRow>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -10,18 +10,18 @@ defineProps<{
|
||||
<template>
|
||||
<TabBar>
|
||||
<TabBarItem
|
||||
@click="router.visit(route('reporting'))"
|
||||
:active="active === 'reporting'"
|
||||
@click="router.visit(route('reporting'))"
|
||||
>Overview</TabBarItem
|
||||
>
|
||||
<TabBarItem
|
||||
@click="router.visit(route('reporting.detailed'))"
|
||||
:active="active === 'detailed'"
|
||||
@click="router.visit(route('reporting.detailed'))"
|
||||
>Detailed</TabBarItem
|
||||
>
|
||||
<TabBarItem
|
||||
@click="router.visit(route('reporting.shared'))"
|
||||
:active="active === 'shared'"
|
||||
@click="router.visit(route('reporting.shared'))"
|
||||
>Shared</TabBarItem
|
||||
>
|
||||
</TabBar>
|
||||
|
||||
@@ -14,10 +14,10 @@ const props = defineProps<{
|
||||
<template>
|
||||
<MoreOptionsDropdown :label="'Actions for Tag ' + props.tag.name">
|
||||
<button
|
||||
@click="emit('delete')"
|
||||
:aria-label="'Delete Tag ' + props.tag.name"
|
||||
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>
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
|
||||
@@ -19,8 +19,8 @@ const showCreateTagModal = ref(false);
|
||||
|
||||
<template>
|
||||
<TagCreateModal
|
||||
:createTag
|
||||
v-model:show="showCreateTagModal"></TagCreateModal>
|
||||
v-model:show="showCreateTagModal"
|
||||
:create-tag></TagCreateModal>
|
||||
<div class="flow-root">
|
||||
<div class="inline-block min-w-full align-middle">
|
||||
<div
|
||||
@@ -29,18 +29,18 @@ const showCreateTagModal = ref(false);
|
||||
style="grid-template-columns: 1fr 80px">
|
||||
<TagTableHeading></TagTableHeading>
|
||||
<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
|
||||
class="w-8 text-icon-default inline pb-2"></FolderPlusIcon>
|
||||
<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!
|
||||
</p>
|
||||
<SecondaryButton
|
||||
v-if="canCreateTags()"
|
||||
@click="showCreateTagModal = true"
|
||||
:icon="PlusIcon"
|
||||
@click="showCreateTagModal = true"
|
||||
>Create your First Tag</SecondaryButton
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -53,19 +53,19 @@ useFocus(taskNameInput, { initialValue: true });
|
||||
v-model="taskName"
|
||||
type="text"
|
||||
placeholder="Task Name"
|
||||
@keydown.enter="submit()"
|
||||
class="mt-1 block w-full"
|
||||
required
|
||||
autocomplete="taskName" />
|
||||
autocomplete="taskName"
|
||||
@keydown.enter="submit()" />
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<ProjectDropdown :modelValue="projectId"></ProjectDropdown>
|
||||
<ProjectDropdown :model-value="projectId"></ProjectDropdown>
|
||||
</div>
|
||||
</div>
|
||||
<EstimatedTimeSection
|
||||
v-if="isAllowedToPerformPremiumAction()"
|
||||
@submit="submit()"
|
||||
v-model="estimatedTime"></EstimatedTimeSection>
|
||||
v-model="estimatedTime"
|
||||
@submit="submit()"></EstimatedTimeSection>
|
||||
</template>
|
||||
<template #footer>
|
||||
<SecondaryButton @click="show = false"> Cancel </SecondaryButton>
|
||||
|
||||
@@ -50,16 +50,16 @@ useFocus(taskNameInput, { initialValue: true });
|
||||
v-model="taskBody.name"
|
||||
type="text"
|
||||
placeholder="Task Name"
|
||||
@keydown.enter="submit()"
|
||||
class="mt-1 block w-full"
|
||||
required
|
||||
autocomplete="taskName" />
|
||||
autocomplete="taskName"
|
||||
@keydown.enter="submit()" />
|
||||
</div>
|
||||
</div>
|
||||
<EstimatedTimeSection
|
||||
v-if="isAllowedToPerformPremiumAction()"
|
||||
@submit="submit()"
|
||||
v-model="taskBody.estimated_time"></EstimatedTimeSection>
|
||||
v-model="taskBody.estimated_time"
|
||||
@submit="submit()"></EstimatedTimeSection>
|
||||
</template>
|
||||
<template #footer>
|
||||
<SecondaryButton @click="show = false"> Cancel </SecondaryButton>
|
||||
|
||||
@@ -21,30 +21,30 @@ const props = defineProps<{
|
||||
<MoreOptionsDropdown :label="'Actions for Task ' + props.task.name">
|
||||
<div class="min-w-[150px]">
|
||||
<button
|
||||
@click="emit('edit')"
|
||||
v-if="canUpdateTasks()"
|
||||
:aria-label="'Edit Task ' + props.task.name"
|
||||
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
|
||||
class="w-5 text-icon-active"></PencilSquareIcon>
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
<button
|
||||
@click="emit('done')"
|
||||
v-if="canUpdateTasks()"
|
||||
: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>
|
||||
<span v-if="props.task.is_done">Mark as active</span>
|
||||
<span v-else>Mark as done</span>
|
||||
</button>
|
||||
<button
|
||||
@click="emit('delete')"
|
||||
:aria-label="'Delete Task ' + props.task.name"
|
||||
v-if="canDeleteTasks()"
|
||||
:aria-label="'Delete Task ' + props.task.name"
|
||||
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>
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
|
||||
@@ -18,7 +18,7 @@ function getNameForItem(item: Task) {
|
||||
|
||||
<template>
|
||||
<MultiselectDropdown
|
||||
searchPlaceholder="Search for a Task..."
|
||||
search-placeholder="Search for a Task..."
|
||||
:items="tasks"
|
||||
:get-key-from-item="getKeyFromItem"
|
||||
:get-name-for-item="getNameForItem">
|
||||
|
||||
@@ -19,8 +19,8 @@ const createTask = ref(false);
|
||||
|
||||
<template>
|
||||
<TaskCreateModal
|
||||
:project-id="props.projectId"
|
||||
v-model:show="createTask"></TaskCreateModal>
|
||||
v-model:show="createTask"
|
||||
:project-id="props.projectId"></TaskCreateModal>
|
||||
<div class="flow-root">
|
||||
<div class="inline-block min-w-full align-middle">
|
||||
<div
|
||||
@@ -37,18 +37,18 @@ const createTask = ref(false);
|
||||
">
|
||||
<TaskTableHeading></TaskTableHeading>
|
||||
<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
|
||||
class="w-8 text-icon-default inline pb-2"></PlusCircleIcon>
|
||||
<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!
|
||||
</p>
|
||||
<SecondaryButton
|
||||
v-if="canCreateTasks()"
|
||||
@click="createTask = true"
|
||||
:icon="PlusIcon"
|
||||
@click="createTask = true"
|
||||
>Create your First Task
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
|
||||
@@ -75,8 +75,8 @@ const showTaskEditModal = ref(false);
|
||||
@delete="deleteTask"></TaskMoreOptionsDropdown>
|
||||
</div>
|
||||
<TaskEditModal
|
||||
:task="task"
|
||||
v-model:show="showTaskEditModal"></TaskEditModal>
|
||||
v-model:show="showTaskEditModal"
|
||||
:task="task"></TaskEditModal>
|
||||
</TableRow>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ const showUpgradeModal = ref(false);
|
||||
solidtime Professional.
|
||||
</UpgradeModal>
|
||||
<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>
|
||||
<span class="text-xs text-text-secondary font-semibold"> Upgrade </span>
|
||||
</button>
|
||||
|
||||
@@ -50,12 +50,12 @@ const show = defineModel('show', { default: false });
|
||||
v-if="isBillingActivated() && canManageBilling()"
|
||||
href="/billing">
|
||||
<PrimaryButton
|
||||
type="button"
|
||||
class="mt-6"
|
||||
:icon="CreditCardIcon"
|
||||
v-if="
|
||||
isBillingActivated() && canUpdateOrganization()
|
||||
">
|
||||
"
|
||||
type="button"
|
||||
class="mt-6"
|
||||
:icon="CreditCardIcon">
|
||||
Go to Billing
|
||||
</PrimaryButton>
|
||||
</Link>
|
||||
|
||||
@@ -32,8 +32,8 @@ const isRunningInDifferentOrganization = computed(() => {
|
||||
<template>
|
||||
<div class="pt-3 pb-2.5 px-2 flex justify-between items-center relative">
|
||||
<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
|
||||
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">
|
||||
@@ -50,7 +50,7 @@ const isRunningInDifferentOrganization = computed(() => {
|
||||
</div>
|
||||
<TimeTrackerStartStop
|
||||
:active="isActive"
|
||||
@changed="setActiveState"
|
||||
size="base"></TimeTrackerStartStop>
|
||||
size="base"
|
||||
@changed="setActiveState"></TimeTrackerStartStop>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -15,8 +15,8 @@ defineProps<{
|
||||
<DashboardCard title="Last 7 Days" :icon="CalendarIcon">
|
||||
<DayOverviewCardEntry
|
||||
v-for="day in last7Days"
|
||||
:class="last7Days.length === 7 ? 'last:border-0 first:pt-3' : ''"
|
||||
:key="day.date"
|
||||
:class="last7Days.length === 7 ? 'last:border-0 first:pt-3' : ''"
|
||||
:date="day.date"
|
||||
:history="day.history"
|
||||
:duration="day.duration"></DayOverviewCardEntry>
|
||||
|
||||
@@ -20,8 +20,8 @@ const props = defineProps<{
|
||||
<DashboardCard title="Recently Tracked Tasks" :icon="CheckCircleIcon">
|
||||
<RecentlyTrackedTasksCardEntry
|
||||
v-for="lastTask in props.latestTasks"
|
||||
:class="props.latestTasks.length === 4 ? 'last:border-0' : ''"
|
||||
:key="lastTask.id"
|
||||
:class="props.latestTasks.length === 4 ? 'last:border-0' : ''"
|
||||
:project_id="lastTask.project_id"
|
||||
:task_id="lastTask.id"
|
||||
:title="lastTask.name"></RecentlyTrackedTasksCardEntry>
|
||||
|
||||
@@ -29,14 +29,14 @@ const open = useSessionStorage('nav-collapse-state-' + props.title, true);
|
||||
:icon
|
||||
:current
|
||||
:href></NavigationSidebarLink>
|
||||
<CollapsibleRoot v-model:open="open" v-else
|
||||
<CollapsibleRoot v-else v-model:open="open"
|
||||
><CollapsibleTrigger class="w-full group py-0.5">
|
||||
<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">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<component
|
||||
v-if="icon"
|
||||
:is="icon"
|
||||
v-if="icon"
|
||||
:class="[
|
||||
current
|
||||
? 'text-icon-active'
|
||||
@@ -59,8 +59,8 @@ const open = useSessionStorage('nav-collapse-state-' + props.title, true);
|
||||
<CollapsibleContent class="CollapsibleContent">
|
||||
<div class="px-3.5">
|
||||
<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
|
||||
v-for="subItem in subItems"
|
||||
:key="subItem.title"
|
||||
|
||||
@@ -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',
|
||||
]">
|
||||
<component
|
||||
v-if="icon"
|
||||
:is="icon"
|
||||
v-if="icon"
|
||||
:class="[
|
||||
current
|
||||
? 'text-icon-active'
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<div class="flex w-full flex-col items-center space-y-4 sm:items-end">
|
||||
<Notification
|
||||
v-for="notification in notifications"
|
||||
:type="notification.type"
|
||||
:key="notification.uuid"
|
||||
:type="notification.type"
|
||||
:title="notification.title"
|
||||
:message="notification.message"></Notification>
|
||||
</div>
|
||||
|
||||
@@ -110,29 +110,29 @@ const { tags } = storeToRefs(useTagsStore());
|
||||
<CardTitle title="Time Tracker" :icon="ClockIcon"></CardTitle>
|
||||
<div class="relative">
|
||||
<TimeTrackerRunningInDifferentOrganizationOverlay
|
||||
@switchOrganization="switchToTimeEntryOrganization"
|
||||
v-if="
|
||||
isRunningInDifferentOrganization
|
||||
"></TimeTrackerRunningInDifferentOrganizationOverlay>
|
||||
"
|
||||
@switch-organization="switchToTimeEntryOrganization"></TimeTrackerRunningInDifferentOrganizationOverlay>
|
||||
|
||||
<TimeTrackerControls
|
||||
:createProject
|
||||
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
|
||||
:canCreateProject="canCreateProjects()"
|
||||
:createClient
|
||||
v-model:current-time-entry="currentTimeEntry"
|
||||
v-model:live-timer="now"
|
||||
:create-project
|
||||
:enable-estimated-time="isAllowedToPerformPremiumAction()"
|
||||
:can-create-project="canCreateProjects()"
|
||||
:create-client
|
||||
:clients
|
||||
:tags
|
||||
:tasks
|
||||
:projects
|
||||
:createTag
|
||||
:isActive
|
||||
:create-tag
|
||||
:is-active
|
||||
:currency="getOrganizationCurrencyString()"
|
||||
v-model:currentTimeEntry="currentTimeEntry"
|
||||
v-model:liveTimer="now"
|
||||
@startLiveTimer="startLiveTimer"
|
||||
@stopLiveTimer="stopLiveTimer"
|
||||
@startTimer="setActiveState(true)"
|
||||
@stopTimer="setActiveState(false)"
|
||||
@updateTimeEntry="updateTimeEntry"></TimeTrackerControls>
|
||||
@start-live-timer="startLiveTimer"
|
||||
@stop-live-timer="stopLiveTimer"
|
||||
@start-timer="setActiveState(true)"
|
||||
@stop-timer="setActiveState(false)"
|
||||
@update-time-entry="updateTimeEntry"></TimeTrackerControls>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -12,7 +12,7 @@ function openDesktopGithubRepo() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="py-4 hidden lg:block" v-if="showReleaseInfo">
|
||||
<div v-if="showReleaseInfo" class="py-4 hidden lg:block">
|
||||
<div
|
||||
class="rounded-lg px-2.5 py-2 bg-card-background border border-border-secondary">
|
||||
<div class="flex items-start justify-between">
|
||||
@@ -23,8 +23,8 @@ function openDesktopGithubRepo() {
|
||||
</div>
|
||||
<button>
|
||||
<XMarkIcon
|
||||
@click="showReleaseInfo = false"
|
||||
class="w-3.5 text-text-tertiary hover:text-text-secondary"></XMarkIcon>
|
||||
class="w-3.5 text-text-tertiary hover:text-text-secondary"
|
||||
@click="showReleaseInfo = false"></XMarkIcon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -34,9 +34,9 @@ function openDesktopGithubRepo() {
|
||||
now.
|
||||
</p>
|
||||
<SecondaryButton
|
||||
@click="openDesktopGithubRepo"
|
||||
size="small"
|
||||
class="w-full text-center justify-center mt-1.5"
|
||||
@click="openDesktopGithubRepo"
|
||||
>Download now</SecondaryButton
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -85,8 +85,8 @@ const page = usePage<{
|
||||
class="border-b border-default-background-separator pb-2 flex justify-between">
|
||||
<OrganizationSwitcher class="w-full"></OrganizationSwitcher>
|
||||
<XMarkIcon
|
||||
@click="showSidebarMenu = false"
|
||||
class="w-8 lg:hidden"></XMarkIcon>
|
||||
class="w-8 lg:hidden"
|
||||
@click="showSidebarMenu = false"></XMarkIcon>
|
||||
</div>
|
||||
<div class="border-b border-default-background-separator">
|
||||
<CurrentSidebarTimer></CurrentSidebarTimer>
|
||||
@@ -162,8 +162,8 @@ const page = usePage<{
|
||||
route('clients')
|
||||
"></NavigationSidebarItem>
|
||||
<NavigationSidebarItem
|
||||
title="Members"
|
||||
v-if="canViewMembers()"
|
||||
title="Members"
|
||||
:icon="UserGroupIcon"
|
||||
:current="route().current('members')"
|
||||
:href="
|
||||
@@ -238,8 +238,8 @@ const page = usePage<{
|
||||
<div
|
||||
class="lg:hidden w-full px-3 py-1 border-b border-b-default-background-separator text-muted flex justify-between items-center">
|
||||
<Bars3Icon
|
||||
@click="showSidebarMenu = !showSidebarMenu"
|
||||
class="w-7 text-muted"></Bars3Icon>
|
||||
class="w-7 text-muted"
|
||||
@click="showSidebarMenu = !showSidebarMenu"></Bars3Icon>
|
||||
<OrganizationSwitcher></OrganizationSwitcher>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ const page = usePage<{
|
||||
</InputLabel>
|
||||
</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">
|
||||
<div class="flex items-center">
|
||||
<Checkbox
|
||||
|
||||
@@ -74,7 +74,7 @@ function refreshDashboardData() {
|
||||
<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">
|
||||
<RecentlyTrackedTasksCard
|
||||
:latestTasks="props.latestTasks"></RecentlyTrackedTasksCard>
|
||||
:latest-tasks="props.latestTasks"></RecentlyTrackedTasksCard>
|
||||
<LastSevenDaysCard
|
||||
:last7-days="props.lastSevenDays"></LastSevenDaysCard>
|
||||
<ActivityGraphCard
|
||||
@@ -84,13 +84,13 @@ function refreshDashboardData() {
|
||||
<TeamActivityCard
|
||||
v-if="canViewMembers()"
|
||||
class="flex lg:hidden xl:flex"
|
||||
:latestTeamActivity="
|
||||
:latest-team-activity="
|
||||
props.latestTeamActivity
|
||||
"></TeamActivityCard>
|
||||
</MainContainer>
|
||||
<MainContainer class="py-5">
|
||||
<ThisWeekOverview
|
||||
:weeklyProjectOverview="props.weeklyProjectOverview"
|
||||
:weekly-project-overview="props.weeklyProjectOverview"
|
||||
:total-weekly-billable-amount="props.totalWeeklyBillableAmount"
|
||||
:total-weekly-billable-time="props.totalWeeklyBillableTime"
|
||||
:total-weekly-time="props.totalWeeklyTime"
|
||||
|
||||
@@ -52,8 +52,8 @@ function isActiveTab(tab: string) {
|
||||
>Invite member</SecondaryButton
|
||||
>
|
||||
<MemberInviteModal
|
||||
:available-roles="availableRoles"
|
||||
v-model:show="inviteMember"
|
||||
:available-roles="availableRoles"
|
||||
@close="activeTab = 'invitations'"></MemberInviteModal>
|
||||
</MainContainer>
|
||||
<MemberTable v-if="activeTab === 'all'"></MemberTable>
|
||||
|
||||
@@ -204,9 +204,9 @@ const page = usePage<{
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<InputLabel for="timezone" value="Timezone" />
|
||||
<select
|
||||
name="timezone"
|
||||
id="timezone"
|
||||
v-model="form.timezone"
|
||||
name="timezone"
|
||||
required
|
||||
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>
|
||||
@@ -225,9 +225,9 @@ const page = usePage<{
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<InputLabel for="week_start" value="Start of the week" />
|
||||
<select
|
||||
name="week_start"
|
||||
id="week_start"
|
||||
v-model="form.week_start"
|
||||
name="week_start"
|
||||
required
|
||||
class="mt-1 block w-full border-input-border bg-input-background text-white focus:border-input-border-active rounded-md shadow-sm">
|
||||
<option value="" disabled>Select a week day</option>
|
||||
|
||||
@@ -129,15 +129,15 @@ const shownTasks = computed(() => {
|
||||
</nav>
|
||||
<div>
|
||||
<SecondaryButton
|
||||
v-if="canCreateProjects()"
|
||||
:icon="PencilSquareIcon"
|
||||
@click="showEditProjectModal = true"
|
||||
v-if="canCreateProjects()">
|
||||
@click="showEditProjectModal = true">
|
||||
Edit Project
|
||||
</SecondaryButton>
|
||||
<ProjectEditModal
|
||||
v-if="project"
|
||||
:originalProject="project"
|
||||
v-model:show="showEditProjectModal"></ProjectEditModal>
|
||||
v-model:show="showEditProjectModal"
|
||||
:original-project="project"></ProjectEditModal>
|
||||
</div>
|
||||
</MainContainer>
|
||||
<MainContainer>
|
||||
@@ -168,8 +168,8 @@ const shownTasks = computed(() => {
|
||||
>Create Task
|
||||
</SecondaryButton>
|
||||
<TaskCreateModal
|
||||
:project-id="projectId"
|
||||
v-model:show="createTask"></TaskCreateModal>
|
||||
v-model:show="createTask"
|
||||
:project-id="projectId"></TaskCreateModal>
|
||||
</div>
|
||||
</template>
|
||||
</CardTitle>
|
||||
@@ -188,11 +188,11 @@ const shownTasks = computed(() => {
|
||||
Add Member
|
||||
</SecondaryButton>
|
||||
<ProjectMemberCreateModal
|
||||
:project-id="projectId"
|
||||
:existing-members="projectMembers"
|
||||
v-model:show="
|
||||
createProjectMember
|
||||
"></ProjectMemberCreateModal>
|
||||
"
|
||||
:project-id="projectId"
|
||||
:existing-members="projectMembers"></ProjectMemberCreateModal>
|
||||
</template>
|
||||
</CardTitle>
|
||||
<Card>
|
||||
|
||||
@@ -94,13 +94,13 @@ const showBillableRate = computed(() => {
|
||||
>Create Project
|
||||
</SecondaryButton>
|
||||
<ProjectCreateModal
|
||||
:createProject
|
||||
:enableEstimatedTime="isAllowedToPerformPremiumAction"
|
||||
:createClient
|
||||
v-model:show="showCreateProjectModal"
|
||||
:create-project
|
||||
:enable-estimated-time="isAllowedToPerformPremiumAction"
|
||||
:create-client
|
||||
:currency="getOrganizationCurrencyString()"
|
||||
:clients="clients"
|
||||
@submit="createProject"
|
||||
v-model:show="showCreateProjectModal"></ProjectCreateModal>
|
||||
@submit="createProject"></ProjectCreateModal>
|
||||
</MainContainer>
|
||||
<ProjectTable
|
||||
:show-billable-rate="showBillableRate"
|
||||
|
||||
@@ -281,7 +281,7 @@ const tableData = computed(() => {
|
||||
class="overflow-hidden">
|
||||
<ReportingExportModal
|
||||
v-model:show="showExportModal"
|
||||
:exportUrl="exportUrl"></ReportingExportModal>
|
||||
:export-url="exportUrl"></ReportingExportModal>
|
||||
<MainContainer
|
||||
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">
|
||||
@@ -292,7 +292,7 @@ const tableData = computed(() => {
|
||||
<ReportingExportButton
|
||||
:download="downloadExport"></ReportingExportButton>
|
||||
<ReportSaveButton
|
||||
:reportProperties="reportProperties"></ReportSaveButton>
|
||||
:report-properties="reportProperties"></ReportSaveButton>
|
||||
</div>
|
||||
</MainContainer>
|
||||
<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">
|
||||
<div class="text-sm font-medium">Filters</div>
|
||||
<MemberMultiselectDropdown
|
||||
@submit="updateReporting"
|
||||
v-model="selectedMembers">
|
||||
<template v-slot:trigger>
|
||||
v-model="selectedMembers"
|
||||
@submit="updateReporting">
|
||||
<template #trigger>
|
||||
<ReportingFilterBadge
|
||||
:count="selectedMembers.length"
|
||||
:active="selectedMembers.length > 0"
|
||||
@@ -313,9 +313,9 @@ const tableData = computed(() => {
|
||||
</template>
|
||||
</MemberMultiselectDropdown>
|
||||
<ProjectMultiselectDropdown
|
||||
@submit="updateReporting"
|
||||
v-model="selectedProjects">
|
||||
<template v-slot:trigger>
|
||||
v-model="selectedProjects"
|
||||
@submit="updateReporting">
|
||||
<template #trigger>
|
||||
<ReportingFilterBadge
|
||||
:count="selectedProjects.length"
|
||||
:active="selectedProjects.length > 0"
|
||||
@@ -324,9 +324,9 @@ const tableData = computed(() => {
|
||||
</template>
|
||||
</ProjectMultiselectDropdown>
|
||||
<TaskMultiselectDropdown
|
||||
@submit="updateReporting"
|
||||
v-model="selectedTasks">
|
||||
<template v-slot:trigger>
|
||||
v-model="selectedTasks"
|
||||
@submit="updateReporting">
|
||||
<template #trigger>
|
||||
<ReportingFilterBadge
|
||||
:count="selectedTasks.length"
|
||||
:active="selectedTasks.length > 0"
|
||||
@@ -335,20 +335,20 @@ const tableData = computed(() => {
|
||||
</template>
|
||||
</TaskMultiselectDropdown>
|
||||
<ClientMultiselectDropdown
|
||||
@submit="updateReporting"
|
||||
v-model="selectedClients">
|
||||
<template v-slot:trigger>
|
||||
v-model="selectedClients"
|
||||
@submit="updateReporting">
|
||||
<template #trigger>
|
||||
<ReportingFilterBadge
|
||||
title="Clients"
|
||||
:icon="FolderIcon"></ReportingFilterBadge>
|
||||
</template>
|
||||
</ClientMultiselectDropdown>
|
||||
<TagDropdown
|
||||
@submit="updateReporting"
|
||||
:createTag
|
||||
v-model="selectedTags"
|
||||
:tags="tags">
|
||||
<template v-slot:trigger>
|
||||
:create-tag
|
||||
:tags="tags"
|
||||
@submit="updateReporting">
|
||||
<template #trigger>
|
||||
<ReportingFilterBadge
|
||||
:count="selectedTags.length"
|
||||
:active="selectedTags.length > 0"
|
||||
@@ -358,7 +358,6 @@ const tableData = computed(() => {
|
||||
</TagDropdown>
|
||||
|
||||
<SelectDropdown
|
||||
@changed="updateReporting"
|
||||
v-model="billable"
|
||||
:get-key-from-item="(item) => item.value"
|
||||
:get-name-for-item="(item) => item.label"
|
||||
@@ -375,8 +374,9 @@ const tableData = computed(() => {
|
||||
label: 'Non Billable',
|
||||
value: 'false',
|
||||
},
|
||||
]">
|
||||
<template v-slot:trigger>
|
||||
]"
|
||||
@changed="updateReporting">
|
||||
<template #trigger>
|
||||
<ReportingFilterBadge
|
||||
:active="billable !== null"
|
||||
:title="
|
||||
@@ -399,8 +399,8 @@ const tableData = computed(() => {
|
||||
<MainContainer>
|
||||
<div class="pt-10 w-full px-3 relative">
|
||||
<ReportingChart
|
||||
:groupedType="aggregatedGraphTimeEntries?.grouped_type"
|
||||
:groupedData="
|
||||
:grouped-type="aggregatedGraphTimeEntries?.grouped_type"
|
||||
:grouped-data="
|
||||
aggregatedGraphTimeEntries?.grouped_data
|
||||
"></ReportingChart>
|
||||
</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">
|
||||
<span>Group by</span>
|
||||
<ReportingGroupBySelect
|
||||
v-model="group"
|
||||
:group-by-options="groupByOptions"
|
||||
@changed="updateTableReporting"
|
||||
v-model="group"></ReportingGroupBySelect>
|
||||
@changed="updateTableReporting"></ReportingGroupBySelect>
|
||||
<span>and</span>
|
||||
<ReportingGroupBySelect
|
||||
v-model="subGroup"
|
||||
:group-by-options="
|
||||
groupByOptions.filter(
|
||||
(el) => el.value !== group
|
||||
)
|
||||
"
|
||||
@changed="updateTableReporting"
|
||||
v-model="subGroup"></ReportingGroupBySelect>
|
||||
@changed="updateTableReporting"></ReportingGroupBySelect>
|
||||
</div>
|
||||
<div
|
||||
class="grid items-center"
|
||||
@@ -473,8 +473,8 @@ const tableData = computed(() => {
|
||||
</div>
|
||||
</template>
|
||||
<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">
|
||||
No time entries found
|
||||
</p>
|
||||
|
||||
@@ -252,7 +252,7 @@ async function downloadExport(format: ExportFormat) {
|
||||
class="overflow-hidden">
|
||||
<ReportingExportModal
|
||||
v-model:show="showExportModal"
|
||||
:exportUrl="exportUrl"></ReportingExportModal>
|
||||
:export-url="exportUrl"></ReportingExportModal>
|
||||
<MainContainer
|
||||
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">
|
||||
@@ -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">
|
||||
<div class="text-sm font-medium">Filters</div>
|
||||
<MemberMultiselectDropdown
|
||||
@submit="updateFilteredTimeEntries"
|
||||
v-model="selectedMembers">
|
||||
<template v-slot:trigger>
|
||||
v-model="selectedMembers"
|
||||
@submit="updateFilteredTimeEntries">
|
||||
<template #trigger>
|
||||
<ReportingFilterBadge
|
||||
:count="selectedMembers.length"
|
||||
:active="selectedMembers.length > 0"
|
||||
@@ -281,9 +281,9 @@ async function downloadExport(format: ExportFormat) {
|
||||
</template>
|
||||
</MemberMultiselectDropdown>
|
||||
<ProjectMultiselectDropdown
|
||||
@submit="updateFilteredTimeEntries"
|
||||
v-model="selectedProjects">
|
||||
<template v-slot:trigger>
|
||||
v-model="selectedProjects"
|
||||
@submit="updateFilteredTimeEntries">
|
||||
<template #trigger>
|
||||
<ReportingFilterBadge
|
||||
:count="selectedProjects.length"
|
||||
:active="selectedProjects.length > 0"
|
||||
@@ -292,9 +292,9 @@ async function downloadExport(format: ExportFormat) {
|
||||
</template>
|
||||
</ProjectMultiselectDropdown>
|
||||
<TaskMultiselectDropdown
|
||||
@submit="updateFilteredTimeEntries"
|
||||
v-model="selectedTasks">
|
||||
<template v-slot:trigger>
|
||||
v-model="selectedTasks"
|
||||
@submit="updateFilteredTimeEntries">
|
||||
<template #trigger>
|
||||
<ReportingFilterBadge
|
||||
:count="selectedTasks.length"
|
||||
:active="selectedTasks.length > 0"
|
||||
@@ -303,20 +303,20 @@ async function downloadExport(format: ExportFormat) {
|
||||
</template>
|
||||
</TaskMultiselectDropdown>
|
||||
<ClientMultiselectDropdown
|
||||
@submit="updateFilteredTimeEntries"
|
||||
v-model="selectedClients">
|
||||
<template v-slot:trigger>
|
||||
v-model="selectedClients"
|
||||
@submit="updateFilteredTimeEntries">
|
||||
<template #trigger>
|
||||
<ReportingFilterBadge
|
||||
title="Clients"
|
||||
:icon="FolderIcon"></ReportingFilterBadge>
|
||||
</template>
|
||||
</ClientMultiselectDropdown>
|
||||
<TagDropdown
|
||||
@submit="updateFilteredTimeEntries"
|
||||
:createTag
|
||||
v-model="selectedTags"
|
||||
:tags="tags">
|
||||
<template v-slot:trigger>
|
||||
:create-tag
|
||||
:tags="tags"
|
||||
@submit="updateFilteredTimeEntries">
|
||||
<template #trigger>
|
||||
<ReportingFilterBadge
|
||||
:count="selectedTags.length"
|
||||
:active="selectedTags.length > 0"
|
||||
@@ -326,7 +326,6 @@ async function downloadExport(format: ExportFormat) {
|
||||
</TagDropdown>
|
||||
|
||||
<SelectDropdown
|
||||
@changed="updateFilteredTimeEntries"
|
||||
v-model="billable"
|
||||
:get-key-from-item="(item) => item.value"
|
||||
:get-name-for-item="(item) => item.label"
|
||||
@@ -343,8 +342,9 @@ async function downloadExport(format: ExportFormat) {
|
||||
label: 'Non Billable',
|
||||
value: 'false',
|
||||
},
|
||||
]">
|
||||
<template v-slot:trigger>
|
||||
]"
|
||||
@changed="updateFilteredTimeEntries">
|
||||
<template #trigger>
|
||||
<ReportingFilterBadge
|
||||
:active="billable !== null"
|
||||
:title="
|
||||
@@ -366,12 +366,9 @@ async function downloadExport(format: ExportFormat) {
|
||||
</div>
|
||||
<TimeEntryMassActionRow
|
||||
:selected-time-entries="selectedTimeEntries"
|
||||
:canCreateProject="canCreateProjects()"
|
||||
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
|
||||
@submit="clearSelectionAndState"
|
||||
:can-create-project="canCreateProjects()"
|
||||
:enable-estimated-time="isAllowedToPerformPremiumAction()"
|
||||
:delete-selected="deleteSelected"
|
||||
@select-all="selectedTimeEntries = [...timeEntries]"
|
||||
@unselect-all="selectedTimeEntries = []"
|
||||
:all-selected="selectedTimeEntries.length === timeEntries.length"
|
||||
:projects="projects"
|
||||
:tasks="tasks"
|
||||
@@ -387,34 +384,37 @@ async function downloadExport(format: ExportFormat) {
|
||||
"
|
||||
:create-project="createProject"
|
||||
: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 v-for="entry in timeEntries" :key="entry.id">
|
||||
<TimeEntryRow
|
||||
:selected="selectedTimeEntries.includes(entry)"
|
||||
@selected="selectedTimeEntries.push(entry)"
|
||||
:canCreateProject="canCreateProjects()"
|
||||
@unselected="
|
||||
selectedTimeEntries = selectedTimeEntries.filter(
|
||||
(item) => item.id !== entry.id
|
||||
)
|
||||
"
|
||||
:createClient
|
||||
:createProject
|
||||
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
|
||||
:can-create-project="canCreateProjects()"
|
||||
:create-client
|
||||
:create-project
|
||||
:enable-estimated-time="isAllowedToPerformPremiumAction()"
|
||||
:projects="projects"
|
||||
:tasks="tasks"
|
||||
:tags="tags"
|
||||
:clients
|
||||
:createTag
|
||||
:updateTimeEntry
|
||||
:onStartStopClick="() => startTimeEntryFromExisting(entry)"
|
||||
:deleteTimeEntry="() => deleteTimeEntries([entry])"
|
||||
:create-tag
|
||||
:update-time-entry
|
||||
:on-start-stop-click="() => startTimeEntryFromExisting(entry)"
|
||||
:delete-time-entry="() => deleteTimeEntries([entry])"
|
||||
:currency="getOrganizationCurrencyString()"
|
||||
:members="members"
|
||||
showDate
|
||||
showMember
|
||||
:time-entry="entry"></TimeEntryRow>
|
||||
show-date
|
||||
show-member
|
||||
:time-entry="entry"
|
||||
@selected="selectedTimeEntries.push(entry)"
|
||||
@unselected="
|
||||
selectedTimeEntries = selectedTimeEntries.filter(
|
||||
(item) => item.id !== entry.id
|
||||
)
|
||||
"></TimeEntryRow>
|
||||
</div>
|
||||
<div v-if="timeEntries.length === 0">
|
||||
<div class="text-center pt-12">
|
||||
@@ -431,10 +431,10 @@ async function downloadExport(format: ExportFormat) {
|
||||
</div>
|
||||
|
||||
<PaginationRoot
|
||||
v-model:page="currentPage"
|
||||
:total="totalPages"
|
||||
:items-per-page="pageLimit"
|
||||
class="flex justify-center items-center py-8"
|
||||
v-model:page="currentPage"
|
||||
:sibling-count="1"
|
||||
show-edges>
|
||||
<PaginationList
|
||||
@@ -485,13 +485,13 @@ async function downloadExport(format: ExportFormat) {
|
||||
</template>
|
||||
<style lang="postcss">
|
||||
.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 {
|
||||
@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] {
|
||||
@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>
|
||||
|
||||
@@ -101,12 +101,12 @@ watch(currentPage, () => {
|
||||
v-if="isBillingActivated() && canManageBilling()"
|
||||
href="/billing">
|
||||
<PrimaryButton
|
||||
type="button"
|
||||
class="mt-6"
|
||||
:icon="CreditCardIcon"
|
||||
v-if="
|
||||
isBillingActivated() && canUpdateOrganization()
|
||||
">
|
||||
"
|
||||
type="button"
|
||||
class="mt-6"
|
||||
:icon="CreditCardIcon">
|
||||
Go to Billing
|
||||
</PrimaryButton>
|
||||
</Link>
|
||||
@@ -120,10 +120,10 @@ watch(currentPage, () => {
|
||||
|
||||
<PaginationRoot
|
||||
v-if="reports.length > 0 || isAllowedToPerformPremiumAction()"
|
||||
v-model:page="currentPage"
|
||||
:total="totalPages"
|
||||
:items-per-page="pageLimit"
|
||||
class="flex justify-center items-center py-8"
|
||||
v-model:page="currentPage"
|
||||
:sibling-count="1"
|
||||
show-edges>
|
||||
<PaginationList
|
||||
@@ -174,13 +174,13 @@ watch(currentPage, () => {
|
||||
</template>
|
||||
<style lang="postcss">
|
||||
.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 {
|
||||
@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] {
|
||||
@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>
|
||||
|
||||
@@ -151,8 +151,8 @@ function getGroupLabel(key: string) {
|
||||
<MainContainer>
|
||||
<div class="pt-10 w-full px-3 relative">
|
||||
<ReportingChart
|
||||
:groupedType="aggregatedGraphTimeEntries?.grouped_type"
|
||||
:groupedData="
|
||||
:grouped-type="aggregatedGraphTimeEntries?.grouped_type"
|
||||
:grouped-data="
|
||||
aggregatedGraphTimeEntries?.grouped_data
|
||||
"></ReportingChart>
|
||||
</div>
|
||||
@@ -217,8 +217,8 @@ function getGroupLabel(key: string) {
|
||||
</div>
|
||||
</template>
|
||||
<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">
|
||||
No time entries found
|
||||
</p>
|
||||
|
||||
@@ -31,9 +31,9 @@ async function createTag(tag: string) {
|
||||
>Create Tag
|
||||
</SecondaryButton>
|
||||
<TagCreateModal
|
||||
:createTag="createTag"
|
||||
v-model:show="showCreateTagModal"></TagCreateModal>
|
||||
v-model:show="showCreateTagModal"
|
||||
:create-tag="createTag"></TagCreateModal>
|
||||
</MainContainer>
|
||||
<TagTable :createTag="createTag"></TagTable>
|
||||
<TagTable :create-tag="createTag"></TagTable>
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
@@ -191,9 +191,9 @@ const showResultModal = ref(false);
|
||||
<div>
|
||||
<InputLabel for="importType" value="Import Type" />
|
||||
<select
|
||||
name="importType"
|
||||
id="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">
|
||||
<option :value="null" selected disabled>
|
||||
Select an import type to get instructions...
|
||||
@@ -206,8 +206,8 @@ const showResultModal = ref(false);
|
||||
</option>
|
||||
</select>
|
||||
<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">
|
||||
Instructions:
|
||||
</div>
|
||||
@@ -233,12 +233,12 @@ const showResultModal = ref(false);
|
||||
>Upload a Toggl/Clockify Export</span
|
||||
>
|
||||
<input
|
||||
ref="importFile"
|
||||
id="file-upload"
|
||||
ref="importFile"
|
||||
name="file-upload"
|
||||
v-on:change="updateFiles"
|
||||
type="file"
|
||||
class="sr-only" />
|
||||
class="sr-only"
|
||||
@change="updateFiles" />
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-xs leading-5 text-muted">
|
||||
|
||||
@@ -62,10 +62,10 @@ function checkForConfirmationModal() {
|
||||
<template #form>
|
||||
<OrganizationBillableRateModal
|
||||
v-model:show="showConfirmationModal"
|
||||
@submit="submit"
|
||||
:new-billable-rate="
|
||||
organizationBody.billable_rate
|
||||
"></OrganizationBillableRateModal>
|
||||
"
|
||||
@submit="submit"></OrganizationBillableRateModal>
|
||||
<!-- Organization Owner Information -->
|
||||
<div class="col-span-6">
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
@@ -75,8 +75,8 @@ function checkForConfirmationModal() {
|
||||
value="Organization Billable Rate" />
|
||||
<BillableRateInput
|
||||
v-if="organization"
|
||||
:currency="getOrganizationCurrencyString()"
|
||||
v-model="organizationBody.billable_rate"
|
||||
:currency="getOrganizationCurrencyString()"
|
||||
name="organizationBillableRate"></BillableRateInput>
|
||||
</div>
|
||||
</div>
|
||||
@@ -86,10 +86,10 @@ function checkForConfirmationModal() {
|
||||
<div class="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
v-if="organization"
|
||||
id="organizationShowBillableRatesToEmployees"
|
||||
v-model:checked="
|
||||
organizationBody.employees_can_see_billable_rates
|
||||
"
|
||||
id="organizationShowBillableRatesToEmployees"></Checkbox>
|
||||
"></Checkbox>
|
||||
<InputLabel
|
||||
for="organizationShowBillableRatesToEmployees"
|
||||
value="Show Billable Rates to Employees" />
|
||||
|
||||
@@ -89,9 +89,9 @@ const updateTeamName = () => {
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<InputLabel for="currency" value="Currency" />
|
||||
<select
|
||||
name="currency"
|
||||
id="currency"
|
||||
v-model="form.currency"
|
||||
name="currency"
|
||||
: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">
|
||||
<option value="" disabled>Select a currency</option>
|
||||
|
||||
@@ -120,16 +120,16 @@ function deleteSelected() {
|
||||
|
||||
<template>
|
||||
<TimeEntryCreateModal
|
||||
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
|
||||
:createProject="createProject"
|
||||
:createClient="createClient"
|
||||
:createTag="createTag"
|
||||
:createTimeEntry="createTimeEntry"
|
||||
v-model:show="showManualTimeEntryModal"
|
||||
:enable-estimated-time="isAllowedToPerformPremiumAction()"
|
||||
:create-project="createProject"
|
||||
:create-client="createClient"
|
||||
:create-tag="createTag"
|
||||
:create-time-entry="createTimeEntry"
|
||||
:projects
|
||||
:tasks
|
||||
:tags
|
||||
:clients
|
||||
v-model:show="showManualTimeEntryModal"></TimeEntryCreateModal>
|
||||
:clients></TimeEntryCreateModal>
|
||||
<AppLayout title="Dashboard" data-testid="time_view">
|
||||
<MainContainer
|
||||
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">
|
||||
<SecondaryButton
|
||||
class="w-full text-center flex justify-center"
|
||||
@click="showManualTimeEntryModal = true"
|
||||
:icon="PlusIcon"
|
||||
@click="showManualTimeEntryModal = true"
|
||||
>Manual time entry
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
@@ -150,12 +150,9 @@ function deleteSelected() {
|
||||
</MainContainer>
|
||||
<TimeEntryMassActionRow
|
||||
:selected-time-entries="selectedTimeEntries"
|
||||
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
|
||||
:canCreateProject="canCreateProjects()"
|
||||
@submit="clearSelectionAndState"
|
||||
:enable-estimated-time="isAllowedToPerformPremiumAction()"
|
||||
:can-create-project="canCreateProjects()"
|
||||
:all-selected="selectedTimeEntries.length === timeEntries.length"
|
||||
@select-all="selectedTimeEntries = [...timeEntries]"
|
||||
@unselect-all="selectedTimeEntries = []"
|
||||
:delete-selected="deleteSelected"
|
||||
:projects="projects"
|
||||
:tasks="tasks"
|
||||
@@ -171,23 +168,26 @@ function deleteSelected() {
|
||||
"
|
||||
:create-project="createProject"
|
||||
:create-client="createClient"
|
||||
:createTag="createTag"></TimeEntryMassActionRow>
|
||||
:create-tag="createTag"
|
||||
@submit="clearSelectionAndState"
|
||||
@select-all="selectedTimeEntries = [...timeEntries]"
|
||||
@unselect-all="selectedTimeEntries = []"></TimeEntryMassActionRow>
|
||||
<TimeEntryGroupedTable
|
||||
v-model:selected="selectedTimeEntries"
|
||||
:createProject
|
||||
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
|
||||
:canCreateProject="canCreateProjects()"
|
||||
:create-project
|
||||
:enable-estimated-time="isAllowedToPerformPremiumAction()"
|
||||
:can-create-project="canCreateProjects()"
|
||||
:clients
|
||||
:createClient
|
||||
:updateTimeEntry
|
||||
:updateTimeEntries
|
||||
:deleteTimeEntries
|
||||
:createTimeEntry="startTimeEntry"
|
||||
:createTag
|
||||
:create-client
|
||||
:update-time-entry
|
||||
:update-time-entries
|
||||
:delete-time-entries
|
||||
:create-time-entry="startTimeEntry"
|
||||
:create-tag
|
||||
:projects="projects"
|
||||
:tasks="tasks"
|
||||
:currency="getOrganizationCurrencyString()"
|
||||
:timeEntries="timeEntries"
|
||||
:time-entries="timeEntries"
|
||||
:tags="tags"></TimeEntryGroupedTable>
|
||||
<div v-if="timeEntries.length === 0" class="text-center pt-12">
|
||||
<ClockIcon class="w-8 text-icon-default inline pb-2"></ClockIcon>
|
||||
|
||||
995
resources/js/packages/ui/package-lock.json
generated
995
resources/js/packages/ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -57,13 +57,13 @@
|
||||
"peerDependencies": {
|
||||
"@floating-ui/vue": "^1.1.4",
|
||||
"@heroicons/vue": "^2.1.5",
|
||||
"@vueuse/core": "^10.11.0",
|
||||
"@vueuse/core": "^12.5.0",
|
||||
"@zodios/core": "^10.9.6",
|
||||
"dayjs": "^1.11.13",
|
||||
"parse-duration": "^1.1.0",
|
||||
"parse-duration": "^2.0.1",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss": "^3.1.0",
|
||||
"vue": "^3.4.38",
|
||||
"vue-tsc": "^2.0.29"
|
||||
"vue": "^3.5.0",
|
||||
"vue-tsc": "^2.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ import { computed } from 'vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
size: 'base' | 'large' | 'xlarge';
|
||||
tag: string;
|
||||
size?: 'base' | 'large' | 'xlarge';
|
||||
tag?: string;
|
||||
class?: string;
|
||||
color: string;
|
||||
border: boolean;
|
||||
color?: string;
|
||||
border?: boolean;
|
||||
}>(),
|
||||
{
|
||||
size: 'base',
|
||||
@@ -30,6 +30,13 @@ const borderClasses = computed(() => {
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
const tagClasses = computed(() => {
|
||||
if (props.tag === 'button') {
|
||||
return 'hover:bg-tertiary';
|
||||
}
|
||||
return '';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -37,9 +44,10 @@ const borderClasses = computed(() => {
|
||||
:is="tag"
|
||||
:class="
|
||||
twMerge(
|
||||
tagClasses,
|
||||
badgeClasses[size],
|
||||
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
|
||||
)
|
||||
">
|
||||
|
||||
@@ -6,9 +6,9 @@ import { twMerge } from 'tailwind-merge';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
type: HtmlButtonType;
|
||||
type?: HtmlButtonType;
|
||||
icon?: Component;
|
||||
loading: boolean;
|
||||
loading?: boolean;
|
||||
}>(),
|
||||
{
|
||||
type: 'submit',
|
||||
@@ -21,15 +21,15 @@ const props = withDefaults(
|
||||
<button
|
||||
:type="type"
|
||||
: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
|
||||
:class="
|
||||
twMerge('flex items-center ', props.icon ? 'space-x-1.5' : '')
|
||||
">
|
||||
<LoadingSpinner v-if="loading"></LoadingSpinner>
|
||||
<component
|
||||
v-if="props.icon && !loading"
|
||||
:is="props.icon"
|
||||
v-if="props.icon && !loading"
|
||||
class="text-text-secondary w-4 -ml-0.5 mr-1"></component>
|
||||
<span>
|
||||
<slot />
|
||||
|
||||
@@ -6,10 +6,10 @@ import LoadingSpinner from '../LoadingSpinner.vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
type: HtmlButtonType;
|
||||
type?: HtmlButtonType;
|
||||
icon?: Component;
|
||||
size: 'small' | 'base';
|
||||
loading: boolean;
|
||||
size?: 'small' | 'base';
|
||||
loading?: boolean;
|
||||
class?: string;
|
||||
}>(),
|
||||
{
|
||||
@@ -31,7 +31,7 @@ const sizeClasses = {
|
||||
:disabled="loading"
|
||||
:class="
|
||||
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],
|
||||
props.class
|
||||
)
|
||||
@@ -42,8 +42,8 @@ const sizeClasses = {
|
||||
">
|
||||
<LoadingSpinner v-if="loading"></LoadingSpinner>
|
||||
<component
|
||||
v-if="props.icon && !loading"
|
||||
:is="props.icon"
|
||||
v-if="props.icon && !loading"
|
||||
class="text-text-tertiary w-4 -ml-0.5 mr-1"></component>
|
||||
<span>
|
||||
<slot />
|
||||
|
||||
@@ -12,8 +12,8 @@ defineProps<{
|
||||
<h3
|
||||
class="text-white font-bold text-sm lg:text-base flex items-center space-x-2 lg:space-x-2.5">
|
||||
<component
|
||||
v-if="icon"
|
||||
:is="icon"
|
||||
v-if="icon"
|
||||
class="w-5 lg:w-6 text-icon-default"></component>
|
||||
<span>
|
||||
{{ title }}
|
||||
|
||||
@@ -127,28 +127,28 @@ const highlightedItem = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dropdown width="120" v-model="open" :closeOnContentClick="true">
|
||||
<Dropdown v-model="open" width="120" :close-on-content-click="true">
|
||||
<template #trigger>
|
||||
<slot name="trigger"></slot>
|
||||
</template>
|
||||
<template #content>
|
||||
<input
|
||||
ref="searchInput"
|
||||
: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"
|
||||
@keydown.enter="addClientIfNoneExists"
|
||||
data-testid="client_dropdown_search"
|
||||
@keydown.up.prevent="moveHighlightUp"
|
||||
@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..." />
|
||||
@keydown.down.prevent="moveHighlightDown" />
|
||||
<div ref="dropdownViewport" class="w-60 max-h-60 overflow-y-scroll">
|
||||
<div
|
||||
v-if="
|
||||
searchValue.length > 0 && filteredClients.length === 0
|
||||
"
|
||||
@click="addClientIfNoneExists"
|
||||
class="bg-card-background-active">
|
||||
class="bg-card-background-active"
|
||||
@click="addClientIfNoneExists">
|
||||
<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">
|
||||
<PlusCircleIcon
|
||||
@@ -170,8 +170,8 @@ const highlightedItem = computed(() => {
|
||||
:data-client-id="client.id">
|
||||
<ClientDropdownItem
|
||||
:selected="isClientSelected(client.id)"
|
||||
@click="updateClient(client.id)"
|
||||
:name="client.name"></ClientDropdownItem>
|
||||
:name="client.name"
|
||||
@click="updateClient(client.id)"></ClientDropdownItem>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -14,9 +14,9 @@ const emit = defineEmits(['submit']);
|
||||
<InputLabel for="billable" value="Time Estimated" />
|
||||
</div>
|
||||
<DurationInput
|
||||
@submit="emit('submit')"
|
||||
v-model="model"
|
||||
class="max-w-[150px]"></DurationInput>
|
||||
class="max-w-[150px]"
|
||||
@submit="emit('submit')"></DurationInput>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { computed } from 'vue';
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
expanded?: boolean;
|
||||
size: string;
|
||||
size?: string;
|
||||
}>(),
|
||||
{
|
||||
expanded: false,
|
||||
@@ -17,7 +17,7 @@ const expandedStatusClasses = computed(() => {
|
||||
if (props.expanded) {
|
||||
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>
|
||||
|
||||
@@ -25,7 +25,7 @@ const expandedStatusClasses = computed(() => {
|
||||
<button
|
||||
:class="
|
||||
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,
|
||||
props.size
|
||||
)
|
||||
|
||||
@@ -77,13 +77,13 @@ const inputValue = ref(formatValue(model.value));
|
||||
:id="name"
|
||||
ref="billableRateInput"
|
||||
v-model="inputValue"
|
||||
@blur="updateRate($event.target.value)"
|
||||
@keydown.enter="updateRate($event.target.value)"
|
||||
type="text"
|
||||
:name="name"
|
||||
placeholder="Billable Rate"
|
||||
class="block w-full"
|
||||
autocomplete="teamMemberRate" />
|
||||
autocomplete="teamMemberRate"
|
||||
@blur="updateRate($event.target.value)"
|
||||
@keydown.enter="updateRate($event.target.value)" />
|
||||
<div
|
||||
class="absolute top-0 right-0 h-full flex items-center px-4 font-medium pointer-events-none">
|
||||
<span>
|
||||
|
||||
@@ -11,7 +11,7 @@ function toggleBillable() {
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
size: 'small' | 'base';
|
||||
size?: 'small' | 'base';
|
||||
}>(),
|
||||
{
|
||||
size: 'base',
|
||||
@@ -35,19 +35,19 @@ const iconSizeClasses = computed(() => {
|
||||
});
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
<button
|
||||
@click="toggleBillable"
|
||||
:class="
|
||||
twMerge(
|
||||
iconColorClasses,
|
||||
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>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
@@ -31,9 +31,9 @@ const proxyChecked = computed({
|
||||
|
||||
<template>
|
||||
<input
|
||||
:id="id"
|
||||
v-model="proxyChecked"
|
||||
type="checkbox"
|
||||
:id="id"
|
||||
: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>
|
||||
|
||||
@@ -48,10 +48,8 @@ const emit = defineEmits(['changed']);
|
||||
<template>
|
||||
<div class="flex items-center text-muted">
|
||||
<input
|
||||
id="start"
|
||||
ref="datePicker"
|
||||
@change="updateTempValue"
|
||||
@blur="updateDate"
|
||||
@keydown.enter="updateDate"
|
||||
:class="
|
||||
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',
|
||||
@@ -59,9 +57,11 @@ const emit = defineEmits(['changed']);
|
||||
)
|
||||
"
|
||||
type="date"
|
||||
id="start"
|
||||
name="trip-start"
|
||||
:value="tempDate" />
|
||||
:value="tempDate"
|
||||
@change="updateTempValue"
|
||||
@blur="updateDate"
|
||||
@keydown.enter="updateDate" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
flip,
|
||||
limitShift,
|
||||
type Placement,
|
||||
type ReferenceElement,
|
||||
shift,
|
||||
useFloating,
|
||||
} from '@floating-ui/vue';
|
||||
@@ -15,8 +14,8 @@ import { isLastLayer, layers } from '@/packages/ui/src/utils/dismissableLayer';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
align: Placement;
|
||||
closeOnContentClick: boolean;
|
||||
align?: Placement;
|
||||
closeOnContentClick?: boolean;
|
||||
}>(),
|
||||
{
|
||||
align: 'bottom-start',
|
||||
@@ -45,6 +44,11 @@ watch(open, (value) => {
|
||||
layers.value.push(id);
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
|
||||
const reference = ref<null | ReferenceElement>(null);
|
||||
const reference = ref<null | HTMLElement>(null);
|
||||
const floating = ref(null);
|
||||
const { floatingStyles } = useFloating(reference, floating, {
|
||||
placement: props.align,
|
||||
@@ -90,7 +94,7 @@ const { floatingStyles } = useFloating(reference, floating, {
|
||||
|
||||
<template>
|
||||
<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" />
|
||||
</div>
|
||||
|
||||
@@ -109,8 +113,8 @@ const { floatingStyles } = useFloating(reference, floating, {
|
||||
leave-to-class="transform opacity-0 scale-95">
|
||||
<div
|
||||
v-if="open"
|
||||
class="z-50"
|
||||
ref="floating"
|
||||
class="z-50"
|
||||
:style="floatingStyles"
|
||||
@click="onContentClick">
|
||||
<div
|
||||
|
||||
@@ -78,11 +78,11 @@ function updateTimeEntryInputValue() {
|
||||
<template>
|
||||
<TextInput
|
||||
ref="inputField"
|
||||
@blur="updateDuration"
|
||||
@keydown.enter="updateDuration"
|
||||
v-model="temporaryCustomTimerEntry"
|
||||
:class="twMerge('text-text-secondary', props.class)"
|
||||
type="text" />
|
||||
type="text"
|
||||
@blur="updateDuration"
|
||||
@keydown.enter="updateDuration" />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -54,12 +54,12 @@ function updateAndSubmit() {
|
||||
<template>
|
||||
<div class="relative">
|
||||
<TextInput
|
||||
v-model="currentTime"
|
||||
class="w-full overflow-hidden pr-14"
|
||||
placeholder="0"
|
||||
@focus="selectInput"
|
||||
@blur="updateDuration"
|
||||
@keydown.enter="updateAndSubmit"
|
||||
v-model="currentTime">
|
||||
@keydown.enter="updateAndSubmit">
|
||||
</TextInput>
|
||||
<div
|
||||
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
Reference in New Issue
Block a user