mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-16 20:22:15 +01:00
add command palette
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, computed } from 'vue';
|
||||
import { router, usePage } from '@inertiajs/vue3';
|
||||
import { CommandPalette } from '@/packages/ui/src/CommandPalette';
|
||||
import { useCommandPalette } from '@/utils/useCommandPalette';
|
||||
import { useProjectsStore } from '@/utils/useProjects';
|
||||
import { useClientsStore } from '@/utils/useClients';
|
||||
import { useTagsStore } from '@/utils/useTags';
|
||||
import { useTimeEntriesMutations } from '@/utils/useTimeEntriesMutations';
|
||||
import { getOrganizationCurrencyString } from '@/utils/money';
|
||||
import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
||||
import { canCreateProjects } from '@/utils/permissions';
|
||||
import type {
|
||||
CreateClientBody,
|
||||
CreateProjectBody,
|
||||
CreateTimeEntryBody,
|
||||
Project,
|
||||
Client,
|
||||
Tag,
|
||||
} from '@/packages/api/src';
|
||||
import type { User } from '@/types/models';
|
||||
import type { Role } from '@/types/jetstream';
|
||||
|
||||
// Import modals
|
||||
import ProjectCreateModal from '@/packages/ui/src/Project/ProjectCreateModal.vue';
|
||||
import ClientCreateModal from '@/Components/Common/Client/ClientCreateModal.vue';
|
||||
import TaskCreateModal from '@/Components/Common/Task/TaskCreateModal.vue';
|
||||
import TagCreateModal from '@/packages/ui/src/Tag/TagCreateModal.vue';
|
||||
import MemberInviteModal from '@/Components/Common/Member/MemberInviteModal.vue';
|
||||
import TimeEntryCreateModal from '@/packages/ui/src/TimeEntry/TimeEntryCreateModal.vue';
|
||||
|
||||
// Import dropdowns for active timer selectors
|
||||
import TimeTrackerProjectTaskDropdown from '@/packages/ui/src/TimeTracker/TimeTrackerProjectTaskDropdown.vue';
|
||||
import TagDropdown from '@/packages/ui/src/Tag/TagDropdown.vue';
|
||||
|
||||
// Dialog components for selectors
|
||||
import DialogModal from '@/packages/ui/src/DialogModal.vue';
|
||||
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
||||
|
||||
const {
|
||||
isOpen,
|
||||
searchTerm,
|
||||
groups,
|
||||
entityResults,
|
||||
togglePalette,
|
||||
showCreateProjectModal,
|
||||
showCreateClientModal,
|
||||
showCreateTaskModal,
|
||||
showCreateTagModal,
|
||||
showInviteMemberModal,
|
||||
showCreateTimeEntryModal,
|
||||
showProjectSelector,
|
||||
showTaskSelector,
|
||||
showTagsSelector,
|
||||
currentTimeEntry,
|
||||
updateTimer,
|
||||
projects,
|
||||
clients,
|
||||
tasks,
|
||||
tags,
|
||||
} = useCommandPalette();
|
||||
|
||||
// Stores for creating entities
|
||||
const projectsStore = useProjectsStore();
|
||||
const clientsStore = useClientsStore();
|
||||
const tagsStore = useTagsStore();
|
||||
|
||||
// Time entry mutations
|
||||
const { createTimeEntry: createTimeEntryMutation } = useTimeEntriesMutations();
|
||||
|
||||
// Get available roles from page props (for member invite modal)
|
||||
const page = usePage<{
|
||||
availableRoles?: Role[];
|
||||
auth: {
|
||||
user: User;
|
||||
};
|
||||
}>();
|
||||
|
||||
const availableRoles = computed(() => page.props.availableRoles ?? []);
|
||||
|
||||
// Active clients for dropdowns
|
||||
const activeClients = computed(() => clients.value.filter((c) => !c.is_archived));
|
||||
|
||||
// Keyboard shortcut handler
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
||||
e.preventDefault();
|
||||
togglePalette();
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
});
|
||||
|
||||
// Project creation
|
||||
async function createProject(project: CreateProjectBody): Promise<Project | undefined> {
|
||||
const openedFromCommandPalette = showCreateProjectModal.value;
|
||||
const newProject = await projectsStore.createProject(project);
|
||||
showCreateProjectModal.value = false;
|
||||
if (newProject && openedFromCommandPalette) {
|
||||
router.visit(route('projects.show', { project: newProject.id }));
|
||||
}
|
||||
return newProject;
|
||||
}
|
||||
|
||||
async function createClient(client: CreateClientBody): Promise<Client | undefined> {
|
||||
const openedFromCommandPalette = showCreateClientModal.value;
|
||||
const newClient = await clientsStore.createClient(client);
|
||||
if (newClient && openedFromCommandPalette) {
|
||||
showCreateClientModal.value = false;
|
||||
router.visit(route('clients'));
|
||||
}
|
||||
return newClient;
|
||||
}
|
||||
|
||||
async function createTag(name: string): Promise<Tag | undefined> {
|
||||
const openedFromCommandPalette = showCreateTagModal.value;
|
||||
const newTag = await tagsStore.createTag(name);
|
||||
if (newTag && openedFromCommandPalette) {
|
||||
showCreateTagModal.value = false;
|
||||
router.visit(route('tags'));
|
||||
}
|
||||
return newTag;
|
||||
}
|
||||
|
||||
async function createTimeEntry(timeEntry: Omit<CreateTimeEntryBody, 'member_id'>) {
|
||||
await createTimeEntryMutation(timeEntry);
|
||||
showCreateTimeEntryModal.value = false;
|
||||
}
|
||||
|
||||
async function handleProjectTaskSelect() {
|
||||
showProjectSelector.value = false;
|
||||
showTaskSelector.value = false;
|
||||
await updateTimer();
|
||||
}
|
||||
|
||||
async function handleTagsSelect() {
|
||||
showTagsSelector.value = false;
|
||||
await updateTimer();
|
||||
}
|
||||
|
||||
const firstProjectId = computed(() => projects.value[0]?.id ?? '');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Command Palette Dialog -->
|
||||
<CommandPalette
|
||||
v-model:open="isOpen"
|
||||
v-model:search-term="searchTerm"
|
||||
:groups="groups"
|
||||
:entity-results="entityResults" />
|
||||
|
||||
<!-- Project Create Modal -->
|
||||
<ProjectCreateModal
|
||||
v-model:show="showCreateProjectModal"
|
||||
:create-project="createProject"
|
||||
:create-client="createClient"
|
||||
:clients="activeClients"
|
||||
:currency="getOrganizationCurrencyString()"
|
||||
:enable-estimated-time="isAllowedToPerformPremiumAction()" />
|
||||
|
||||
<!-- Client Create Modal -->
|
||||
<ClientCreateModal v-model:show="showCreateClientModal" />
|
||||
|
||||
<!-- Task Create Modal -->
|
||||
<TaskCreateModal
|
||||
v-if="firstProjectId"
|
||||
v-model:show="showCreateTaskModal"
|
||||
:project-id="firstProjectId" />
|
||||
|
||||
<!-- Tag Create Modal -->
|
||||
<TagCreateModal v-model:show="showCreateTagModal" :create-tag="createTag" />
|
||||
|
||||
<!-- Member Invite Modal -->
|
||||
<MemberInviteModal v-model:show="showInviteMemberModal" :available-roles="availableRoles" />
|
||||
|
||||
<!-- Time Entry Create Modal -->
|
||||
<TimeEntryCreateModal
|
||||
v-model:show="showCreateTimeEntryModal"
|
||||
:create-time-entry="createTimeEntry"
|
||||
:create-project="createProject"
|
||||
:create-client="createClient"
|
||||
:create-tag="createTag"
|
||||
:projects="projects"
|
||||
:tasks="tasks"
|
||||
:tags="tags"
|
||||
:clients="activeClients"
|
||||
:currency="getOrganizationCurrencyString()"
|
||||
:enable-estimated-time="isAllowedToPerformPremiumAction()"
|
||||
:can-create-project="canCreateProjects()" />
|
||||
|
||||
<!-- Project Selector Dialog for Active Timer -->
|
||||
<DialogModal :show="showProjectSelector" closeable @close="showProjectSelector = false">
|
||||
<template #title>Set Project</template>
|
||||
<template #content>
|
||||
<TimeTrackerProjectTaskDropdown
|
||||
v-model:project="currentTimeEntry.project_id"
|
||||
v-model:task="currentTimeEntry.task_id"
|
||||
:projects="projects"
|
||||
:tasks="tasks"
|
||||
:clients="activeClients"
|
||||
:create-project="createProject"
|
||||
:create-client="createClient"
|
||||
:can-create-project="canCreateProjects()"
|
||||
:currency="getOrganizationCurrencyString()"
|
||||
:enable-estimated-time="isAllowedToPerformPremiumAction()"
|
||||
size="xlarge"
|
||||
class="w-full" />
|
||||
</template>
|
||||
<template #footer>
|
||||
<SecondaryButton @click="showProjectSelector = false"> Cancel </SecondaryButton>
|
||||
<SecondaryButton class="ms-3" @click="handleProjectTaskSelect"> Save </SecondaryButton>
|
||||
</template>
|
||||
</DialogModal>
|
||||
|
||||
<!-- Task Selector Dialog for Active Timer -->
|
||||
<DialogModal :show="showTaskSelector" closeable @close="showTaskSelector = false">
|
||||
<template #title>Set Task</template>
|
||||
<template #content>
|
||||
<TimeTrackerProjectTaskDropdown
|
||||
v-model:project="currentTimeEntry.project_id"
|
||||
v-model:task="currentTimeEntry.task_id"
|
||||
:projects="projects"
|
||||
:tasks="tasks"
|
||||
:clients="activeClients"
|
||||
:create-project="createProject"
|
||||
:create-client="createClient"
|
||||
:can-create-project="canCreateProjects()"
|
||||
:currency="getOrganizationCurrencyString()"
|
||||
:enable-estimated-time="isAllowedToPerformPremiumAction()"
|
||||
size="xlarge"
|
||||
class="w-full" />
|
||||
</template>
|
||||
<template #footer>
|
||||
<SecondaryButton @click="showTaskSelector = false"> Cancel </SecondaryButton>
|
||||
<SecondaryButton class="ms-3" @click="handleProjectTaskSelect"> Save </SecondaryButton>
|
||||
</template>
|
||||
</DialogModal>
|
||||
|
||||
<!-- Tags Selector Dialog for Active Timer -->
|
||||
<DialogModal :show="showTagsSelector" closeable @close="showTagsSelector = false">
|
||||
<template #title>Set Tags</template>
|
||||
<template #content>
|
||||
<TagDropdown v-model="currentTimeEntry.tags" :tags="tags" :create-tag="createTag">
|
||||
<template #trigger>
|
||||
<div
|
||||
class="w-full p-3 border border-card-border rounded-lg cursor-pointer hover:bg-tertiary transition">
|
||||
<span
|
||||
v-if="currentTimeEntry.tags.length === 0"
|
||||
class="text-muted-foreground">
|
||||
Click to select tags...
|
||||
</span>
|
||||
<span v-else> {{ currentTimeEntry.tags.length }} tag(s) selected </span>
|
||||
</div>
|
||||
</template>
|
||||
</TagDropdown>
|
||||
</template>
|
||||
<template #footer>
|
||||
<SecondaryButton @click="showTagsSelector = false"> Cancel </SecondaryButton>
|
||||
<SecondaryButton class="ms-3" @click="handleTagsSelect"> Save </SecondaryButton>
|
||||
</template>
|
||||
</DialogModal>
|
||||
</template>
|
||||
1
resources/js/Components/CommandPalette/index.ts
Normal file
1
resources/js/Components/CommandPalette/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as CommandPaletteProvider } from './CommandPaletteProvider.vue';
|
||||
@@ -38,7 +38,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'bg-default-background grid w-full max-w-lg border border-border-tertiary shadow-lg duration-200 sm:rounded-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
||||
'pointer-events-auto bg-default-background grid w-full max-w-lg border border-border-tertiary shadow-lg duration-200 sm:rounded-lg outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
||||
props.class
|
||||
)
|
||||
">
|
||||
|
||||
@@ -32,7 +32,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
'z-50 min-w-32 overflow-hidden rounded-md border border-border-secondary bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
props.class
|
||||
)
|
||||
">
|
||||
|
||||
Reference in New Issue
Block a user