add notice when time tracker is running in a different organization

This commit is contained in:
Gregor Vostrak
2024-04-26 02:10:45 +02:00
parent 3c158cc015
commit bb5ccf3606
9 changed files with 237 additions and 81 deletions

View File

@@ -256,7 +256,7 @@ test('test that updating a the duration in the overview works on blur', async ({
await expect(
newTimeEntry.getByTestId('time_entry_duration_input')
).toHaveValue('00h 20min');
).toHaveValue('0h 20min');
});
// Test that start stop button stops running timer

View File

@@ -128,6 +128,7 @@ const TimeEntryResource = z
description: z.union([z.string(), z.null()]),
task_id: z.union([z.string(), z.null()]),
project_id: z.union([z.string(), z.null()]),
organization_id: z.string(),
user_id: z.string(),
tags: z.array(z.string()),
billable: z.boolean(),
@@ -391,6 +392,17 @@ const endpoints = makeApi([
],
response: z.null(),
errors: [
{
status: 400,
description: `API exception`,
schema: z
.object({
error: z.boolean(),
key: z.string(),
message: z.string(),
})
.passthrough(),
},
{
status: 403,
description: `Authorization error`,
@@ -623,6 +635,78 @@ const endpoints = makeApi([
},
],
},
{
method: 'delete',
path: '/v1/organizations/:organization/invitations/:invitation',
alias: 'removeInvitation',
requestFormat: 'json',
parameters: [
{
name: 'body',
type: 'Body',
schema: z.object({}).partial().passthrough(),
},
{
name: 'organization',
type: 'Path',
schema: z.string().uuid(),
},
{
name: 'invitation',
type: 'Path',
schema: z.string().uuid(),
},
],
response: z.null(),
errors: [
{
status: 403,
description: `Authorization error`,
schema: z.object({ message: z.string() }).passthrough(),
},
{
status: 404,
description: `Not found`,
schema: z.object({ message: z.string() }).passthrough(),
},
],
},
{
method: 'post',
path: '/v1/organizations/:organization/invitations/:invitation/resend',
alias: 'resendInvitationEmail',
requestFormat: 'json',
parameters: [
{
name: 'body',
type: 'Body',
schema: z.object({}).partial().passthrough(),
},
{
name: 'organization',
type: 'Path',
schema: z.string().uuid(),
},
{
name: 'invitation',
type: 'Path',
schema: z.string().uuid(),
},
],
response: z.null(),
errors: [
{
status: 403,
description: `Authorization error`,
schema: z.object({ message: z.string() }).passthrough(),
},
{
status: 404,
description: `Not found`,
schema: z.object({ message: z.string() }).passthrough(),
},
],
},
{
method: 'get',
path: '/v1/organizations/:organization/members',
@@ -761,6 +845,17 @@ const endpoints = makeApi([
],
response: z.null(),
errors: [
{
status: 400,
description: `API exception`,
schema: z
.object({
error: z.boolean(),
key: z.string(),
message: z.string(),
})
.passthrough(),
},
{
status: 403,
description: `Authorization error`,
@@ -1102,6 +1197,17 @@ const endpoints = makeApi([
],
response: z.null(),
errors: [
{
status: 400,
description: `API exception`,
schema: z
.object({
error: z.boolean(),
key: z.string(),
message: z.string(),
})
.passthrough(),
},
{
status: 403,
description: `Authorization error`,
@@ -1371,6 +1477,17 @@ const endpoints = makeApi([
],
response: z.null(),
errors: [
{
status: 400,
description: `API exception`,
schema: z
.object({
error: z.boolean(),
key: z.string(),
message: z.string(),
})
.passthrough(),
},
{
status: 403,
description: `Authorization error`,
@@ -1567,6 +1684,17 @@ const endpoints = makeApi([
],
response: z.null(),
errors: [
{
status: 400,
description: `API exception`,
schema: z
.object({
error: z.boolean(),
key: z.string(),
message: z.string(),
})
.passthrough(),
},
{
status: 403,
description: `Authorization error`,

View File

@@ -79,7 +79,6 @@ function updateSearchValue(event: Event) {
const emit = defineEmits(['update:modelValue', 'changed']);
function updateMember(newValue: string | null) {
console.log(newValue);
if (newValue) {
model.value = newValue;
nextTick(() => {

View File

@@ -136,7 +136,6 @@ function moveHighlightDown() {
filteredTags.value[currentHightlightedIndex + 1].id;
}
}
console.log('move down');
}
const highlightedItemId = ref<string | null>(null);

View File

@@ -2,11 +2,11 @@
import { ChevronDownIcon } from '@heroicons/vue/20/solid';
import Dropdown from '@/Components/Dropdown.vue';
import DropdownLink from '@/Components/DropdownLink.vue';
import { router, usePage } from '@inertiajs/vue3';
import { usePage } from '@inertiajs/vue3';
import type { Organization, User } from '@/types/models';
import { isBillingActivated } from '@/utils/billing';
import { initializeStores } from '@/utils/init';
import { canUpdateOrganization } from '@/utils/permissions';
import { switchOrganization } from '@/utils/useOrganization';
const page = usePage<{
jetstream: {
@@ -22,19 +22,8 @@ const page = usePage<{
};
}>();
const switchToTeam = (team: Organization) => {
router.put(
route('current-team.update'),
{
team_id: team.id,
},
{
preserveState: false,
onSuccess: () => {
initializeStores();
},
}
);
const switchToTeam = (organization: Organization) => {
switchOrganization(organization.id);
};
</script>

View File

@@ -15,6 +15,9 @@ import { storeToRefs } from 'pinia';
import parse from 'parse-duration';
import TimeTrackerTagDropdown from '@/Components/Common/TimeTracker/TimeTrackerTagDropdown.vue';
import TimeTrackerProjectTaskDropdown from '@/Components/Common/TimeTracker/TimeTrackerProjectTaskDropdown.vue';
import { getCurrentOrganizationId } from '@/utils/useUser';
import { switchOrganization } from '@/utils/useOrganization';
import SecondaryButton from '@/Components/SecondaryButton.vue';
const page = usePage<{
auth: {
@@ -148,62 +151,93 @@ function onTimeEntryEnterPress() {
const activeElement = document.activeElement as HTMLElement;
activeElement?.blur();
}
const isRunningInDifferentOrganization = computed(() => {
return (
currentTimeEntry.value.organization_id &&
getCurrentOrganizationId() &&
currentTimeEntry.value.organization_id !== getCurrentOrganizationId()
);
});
function switchToTimeEntryOrganization() {
if (currentTimeEntry.value.organization_id) {
switchOrganization(currentTimeEntry.value.organization_id);
}
}
</script>
<template>
<CardTitle title="Time Tracker" :icon="ClockIcon"></CardTitle>
<div class="flex items-center relative" data-testid="dashboard_timer">
<div class="relative">
<div
class="flex flex-col sm:flex-row w-full rounded-lg bg-card-background border-card-border border transition">
<div class="flex-1 flex items-center pr-6">
<input
placeholder="What are you working on?"
data-testid="time_entry_description"
v-model="currentTimeEntry.description"
@keydown.enter="startTimerIfNotActive"
@blur="updateTimeEntry"
class="w-full rounded-l-lg py-2.5 px-3 border-b border-b-card-background-separator sm:px-4 text-sm sm:text-lg text-white focus:bg-card-background-active font-medium bg-transparent border-none placeholder-muted focus:ring-0 transition"
type="text" />
</div>
<div class="flex items-center justify-between pl-2">
<div class="flex items-center w-[130px] sm:w-auto">
<TimeTrackerProjectTaskDropdown
@changed="updateTimeEntry"
v-model:project="currentTimeEntry.project_id"
v-model:task="
currentTimeEntry.task_id
"></TimeTrackerProjectTaskDropdown>
</div>
<div class="flex items-center space-x-2 px-4">
<TimeTrackerTagDropdown
@changed="updateTimeEntry"
v-model="
currentTimeEntry.tags
"></TimeTrackerTagDropdown>
<BillableToggleButton
@changed="updateTimeEntry"
v-model="
currentTimeEntry.billable
"></BillableToggleButton>
</div>
<div class="border-l border-card-border">
<input
placeholder="00:00:00"
@focus="pauseLiveTimerUpdate"
data-testid="time_entry_time"
@blur="updateTimerAndStartLiveTimerUpdate"
@keydown.enter="onTimeEntryEnterPress"
v-model="currentTime"
class="w-[110px] sm:w-[130px] h-full text-white py-2.5 rounded-r-lg text-center px-4 text-sm sm:text-lg font-bold bg-card-background border-none placeholder-muted focus:ring-0 transition focus:bg-card-background-active"
type="text" />
</div>
class="absolute w-full h-full backdrop-blur-sm z-10 flex items-center justify-center"
v-if="isRunningInDifferentOrganization">
<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">
<span class="text-sm text-white">
The Timer is running in a different organization.
</span>
<SecondaryButton @click="switchToTimeEntryOrganization"
>Switch to organization</SecondaryButton
>
</div>
</div>
<div class="pl-6 pr-3 absolute sm:relative top-[6px] sm:top-0 right-0">
<TimeTrackerStartStop
:active="isActive"
@changed="onToggleButtonPress"
size="large"></TimeTrackerStartStop>
<div class="flex items-center relative" data-testid="dashboard_timer">
<div
class="flex flex-col sm:flex-row w-full rounded-lg bg-card-background border-card-border border transition">
<div class="flex-1 flex items-center pr-6">
<input
placeholder="What are you working on?"
data-testid="time_entry_description"
v-model="currentTimeEntry.description"
@keydown.enter="startTimerIfNotActive"
@blur="updateTimeEntry"
class="w-full rounded-l-lg py-2.5 px-3 border-b border-b-card-background-separator sm:px-4 text-sm sm:text-lg text-white focus:bg-card-background-active font-medium bg-transparent border-none placeholder-muted focus:ring-0 transition"
type="text" />
</div>
<div class="flex items-center justify-between pl-2">
<div class="flex items-center w-[130px] sm:w-auto">
<TimeTrackerProjectTaskDropdown
@changed="updateTimeEntry"
v-model:project="currentTimeEntry.project_id"
v-model:task="
currentTimeEntry.task_id
"></TimeTrackerProjectTaskDropdown>
</div>
<div class="flex items-center space-x-2 px-4">
<TimeTrackerTagDropdown
@changed="updateTimeEntry"
v-model="
currentTimeEntry.tags
"></TimeTrackerTagDropdown>
<BillableToggleButton
@changed="updateTimeEntry"
v-model="
currentTimeEntry.billable
"></BillableToggleButton>
</div>
<div class="border-l border-card-border">
<input
placeholder="00:00:00"
@focus="pauseLiveTimerUpdate"
data-testid="time_entry_time"
@blur="updateTimerAndStartLiveTimerUpdate"
@keydown.enter="onTimeEntryEnterPress"
v-model="currentTime"
class="w-[110px] sm:w-[130px] h-full text-white py-2.5 rounded-r-lg text-center px-4 text-sm sm:text-lg font-bold bg-card-background border-none placeholder-muted focus:ring-0 transition focus:bg-card-background-active"
type="text" />
</div>
</div>
</div>
<div
class="pl-6 pr-3 absolute sm:relative top-[6px] sm:top-0 right-0">
<TimeTrackerStartStop
:active="isActive"
@changed="onToggleButtonPress"
size="large"></TimeTrackerStartStop>
</div>
</div>
</div>
</template>

View File

@@ -7,7 +7,6 @@ const page = usePage<{
}>();
function currentUserHasPermission(permission: string) {
console.log(page.props.auth.permissions);
if (Array.isArray(page.props.auth.permissions)) {
return page.props.auth.permissions.includes(permission);
}

View File

@@ -22,6 +22,7 @@ const emptyTimeEntry = {
project_id: null,
tags: [],
billable: false,
organization_id: '',
} as TimeEntry;
export const useCurrentTimeEntryStore = defineStore('currentTimeEntry', () => {
@@ -56,20 +57,10 @@ export const useCurrentTimeEntryStore = defineStore('currentTimeEntry', () => {
async function fetchCurrentTimeEntry() {
const organizationId = getCurrentOrganizationId();
if (organizationId) {
const timeEntriesResponse = await handleApiRequestNotifications(
api.getTimeEntries({
queries: {
active: 'true',
user_id: getCurrentUserId(),
},
params: {
organization: organizationId,
},
})
);
const timeEntriesResponse = await api.getMyActiveTimeEntry({});
if (timeEntriesResponse?.data) {
if (timeEntriesResponse.data.length === 1) {
currentTimeEntry.value = timeEntriesResponse.data[0];
if (timeEntriesResponse.data) {
currentTimeEntry.value = timeEntriesResponse.data;
} else {
currentTimeEntry.value = { ...emptyTimeEntry };
}

View File

@@ -0,0 +1,17 @@
import { router } from '@inertiajs/vue3';
import { initializeStores } from '@/utils/init';
export function switchOrganization(organizationId: string) {
router.put(
route('current-team.update'),
{
team_id: organizationId,
},
{
preserveState: false,
onSuccess: () => {
initializeStores();
},
}
);
}