Compare commits

...

11 Commits

Author SHA1 Message Date
Gregor Vostrak
857dc7f4a2 start time entry on click in recently tracked time entries dropdown 2025-02-06 18:26:55 +01:00
Gregor Vostrak
b71ac06cb6 add recently tracked timeentries dropdown to timetracker 2025-02-04 15:12:59 +01:00
Gregor Vostrak
0e8f9c58fd update dependencies, update eslint config, update optional ts props types 2025-01-29 17:58:03 +01:00
Gregor Vostrak
71ec1e9e0f fix TimeTrackerRangeSelector detection so it does not open the Dropdown again after pressing Escape 2025-01-29 15:38:22 +01:00
Gregor Vostrak
a8318a458e fix time update test to respect new taborder logic 2025-01-29 14:41:20 +01:00
Gregor Vostrak
84622c3fcc fix enter submits in the time range dropdown 2025-01-29 13:55:07 +01:00
Gregor Vostrak
1a03637e31 fix focus state for dropdowns, fix taborder for timerange select in timetracker and timeentryrows 2025-01-29 13:16:54 +01:00
Gregor Vostrak
cd207c6173 improve focus state styling 2025-01-27 14:12:56 +01:00
Constantin Graf
49e045809b Enhanced description for Clockify imports 2024-12-20 19:57:50 -05:00
Constantin Graf
e90fa8307f Fixed timezones in unit tests 2024-12-20 19:57:50 -05:00
dependabot[bot]
895540d0a9 Bump codecov/codecov-action from 4.5.0 to 5.1.2
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.5.0 to 5.1.2.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4.5.0...v5.1.2)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-20 19:29:52 -05:00
144 changed files with 3533 additions and 3324 deletions

View File

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

View File

@@ -63,7 +63,7 @@ jobs:
run: php artisan test --stop-on-failure --coverage-text --coverage-clover=coverage.xml run: php artisan test --stop-on-failure --coverage-text --coverage-clover=coverage.xml
- name: "Upload coverage reports to Codecov" - name: "Upload coverage reports to Codecov"
uses: codecov/codecov-action@v4.5.0 uses: codecov/codecov-action@v5.1.2
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
slug: solidtime-io/solidtime slug: solidtime-io/solidtime

View File

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

View File

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

View File

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

36
eslint.config.mjs Normal file
View File

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

View File

@@ -5,18 +5,20 @@ declare(strict_types=1);
return [ return [
'clockify_time_entries' => [ 'clockify_time_entries' => [
'name' => 'Clockify Time Entries', 'name' => 'Clockify Time Entries',
'description' => '1. First make sure that you set the Date format to "MM/DD/YYYY" and the Time format to "12-hour" in the user settings.<br> '. 'description' => '1. First make sure that you set the Date format to "MM/DD/YYYY" and the Time format to "12-hour" in the user settings.<br>'.
'2. Go to REPORTS -> TIME -> Detailed in the navigation on the left. <br>'. '2. In the same preferences page change the language of Clockfiy to English.<br>'.
'3. Now select the date range that you want to export in the right top. '. '3. Go to REPORTS -> TIME -> Detailed in the navigation on the left. <br>'.
'4. Now select the date range that you want to export in the right top. '.
'It is currently not possible to select more than one year. You can export each year separately and import them one after another .'. 'It is currently not possible to select more than one year. You can export each year separately and import them one after another .'.
'<br> 4. Now click Export -> Save as CSV. The Export dropdown is in the header of the export table left of the printer symbol. '. '<br> 4. Now click Export -> Save as CSV. The Export dropdown is in the header of the export table left of the printer symbol. '.
'<br><br>Before you import make sure that the Timezone settings in Clockify are the same as in solidtime.', '<br><br>Before you import make sure that the Timezone settings in Clockify are the same as in solidtime.',
], ],
'clockify_projects' => [ 'clockify_projects' => [
'name' => 'Clockify Projects', 'name' => 'Clockify Projects',
'description' => '1. Go to PROJECTS in the navigation on the left.<br> '. 'description' => '1. Make sure to set the language of Clockify to English in "Preferences -> General".<br>'.
'2. Now click on the three dots on the right of the project that you want to export and select Export.<br> '. '2. Go to PROJECTS in the navigation on the left.<br> '.
'3. Now click Export -> Save as CSV. The Export dropdown is in the header of the export table in the top right corner.', '3. Now click on the three dots on the right of the project that you want to export and select Export.<br> '.
'4. Now click Export -> Save as CSV. The Export dropdown is in the header of the export table in the top right corner.',
], ],
'toggl_data_importer' => [ 'toggl_data_importer' => [
'name' => 'Toggl Data Importer', 'name' => 'Toggl Data Importer',

3424
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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