mirror of
https://github.com/gregor-tokarev/cubicdone.git
synced 2026-09-14 11:06:21 +05:00
Translate projects page
This commit is contained in:
parent
1a5c36b2d3
commit
148368dce1
@ -3,59 +3,69 @@ import { useProjectStatusModalStore } from "@store/select-modal.ts";
|
||||
import { useProjectStatusStore } from "@store/project-status.ts";
|
||||
import { computed, ref } from "vue";
|
||||
import SelectModal from "./SelectModal.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const projectStatusModalStore = useProjectStatusModalStore();
|
||||
const projectStatusStore = useProjectStatusStore();
|
||||
|
||||
const query = ref("");
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
'not started': "not started",
|
||||
"in progress": "in progress",
|
||||
'finished': "finished"
|
||||
},
|
||||
ru: {
|
||||
'not started': "Не начат",
|
||||
"in progress": "В прогрессе",
|
||||
'finished': "Завершен"
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const projectOptions = computed(() => {
|
||||
const searchIndex = projectStatusStore.getIndex;
|
||||
const searchResults = searchIndex.search(query.value);
|
||||
const searchIndex = projectStatusStore.getIndex;
|
||||
const searchResults = searchIndex.search(query.value);
|
||||
|
||||
const result = query.value
|
||||
? searchResults.map((r) => r.item)
|
||||
: projectStatusStore.projectStatuses;
|
||||
const result = query.value
|
||||
? searchResults.map((r) => r.item)
|
||||
: projectStatusStore.projectStatuses;
|
||||
|
||||
return result.map((project) => ({
|
||||
id: project.id,
|
||||
text: project.title,
|
||||
icon: project.icon,
|
||||
}));
|
||||
return result.map((project) => ({
|
||||
id: project.id,
|
||||
text: t(project.title),
|
||||
icon: project.icon,
|
||||
}));
|
||||
});
|
||||
|
||||
function onSubmit(id: string) {
|
||||
projectStatusModalStore.resolveFn && projectStatusModalStore.resolveFn(id);
|
||||
projectStatusModalStore.resolveFn && projectStatusModalStore.resolveFn(id);
|
||||
}
|
||||
|
||||
const open = computed({
|
||||
get() {
|
||||
return projectStatusModalStore.open;
|
||||
},
|
||||
set(value: boolean) {
|
||||
if (!value) {
|
||||
projectStatusModalStore.close();
|
||||
query.value = "";
|
||||
}
|
||||
},
|
||||
get() {
|
||||
return projectStatusModalStore.open;
|
||||
},
|
||||
set(value: boolean) {
|
||||
if (!value) {
|
||||
projectStatusModalStore.close();
|
||||
query.value = "";
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const checkedIndex = computed(() => {
|
||||
return projectOptions.value.findIndex(
|
||||
(p) => p.id === projectStatusModalStore.modalOptions?.statusId,
|
||||
);
|
||||
return projectOptions.value.findIndex(
|
||||
(p) => p.id === projectStatusModalStore.modalOptions?.statusId,
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectModal
|
||||
v-if="projectStatusModalStore.modalOptions"
|
||||
:hint-text="projectStatusModalStore.modalOptions.hintText"
|
||||
:checked-index="checkedIndex"
|
||||
v-model:query="query"
|
||||
:options="projectOptions"
|
||||
v-model:open="open"
|
||||
@submit="onSubmit"
|
||||
/>
|
||||
<SelectModal v-if="projectStatusModalStore.modalOptions" :hint-text="projectStatusModalStore.modalOptions.hintText"
|
||||
:checked-index="checkedIndex" v-model:query="query" :options="projectOptions" v-model:open="open"
|
||||
@submit="onSubmit" />
|
||||
</template>
|
||||
@store/select-modal
|
||||
|
||||
@ -6,21 +6,39 @@ import { onClickOutside, useDebounceFn } from "@vueuse/core";
|
||||
import Checkbox from "@components/UI/Checkbox.vue";
|
||||
import { useProjectStatusStore } from "@store/project-status.ts";
|
||||
import Icon from "@components/Icon.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const projectStore = useProjectStore();
|
||||
const projectStatusStore = useProjectStatusStore();
|
||||
|
||||
const attrs = useAttrs();
|
||||
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
'not started': "not started",
|
||||
"in progress": "in progress",
|
||||
'finished': "finished"
|
||||
},
|
||||
ru: {
|
||||
'not started': "Не начат",
|
||||
"in progress": "В прогрессе",
|
||||
'finished': "Завершен"
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const props = defineProps<{
|
||||
project: Project;
|
||||
selected: boolean;
|
||||
project: Project;
|
||||
selected: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update", value: Partial<Project>): void;
|
||||
(e: "update:selected", value: boolean): void;
|
||||
(e: "setStatus", value: void): void;
|
||||
(e: "update", value: Partial<Project>): void;
|
||||
(e: "update:selected", value: boolean): void;
|
||||
(e: "setStatus", value: void): void;
|
||||
}>();
|
||||
|
||||
const showColorPicker = ref(false);
|
||||
@ -28,103 +46,78 @@ const colorPicker = ref<HTMLElement | null>(null);
|
||||
const color = ref<HTMLElement | null>(null);
|
||||
|
||||
onClickOutside(colorPicker, () => {
|
||||
showColorPicker.value = false;
|
||||
showColorPicker.value = false;
|
||||
});
|
||||
|
||||
const colorBound = computed(() => {
|
||||
if (!color.value) return;
|
||||
if (!color.value) return;
|
||||
|
||||
return color.value.getBoundingClientRect();
|
||||
return color.value.getBoundingClientRect();
|
||||
});
|
||||
|
||||
const projectStatus = computed<ProjectStatus | undefined>(() => {
|
||||
if (!props.project.statusId) return;
|
||||
if (!props.project.statusId) return;
|
||||
|
||||
return projectStatusStore.projectStatuses.find(
|
||||
(ps) => ps.id === props.project.statusId,
|
||||
);
|
||||
return projectStatusStore.projectStatuses.find(
|
||||
(ps) => ps.id === props.project.statusId,
|
||||
);
|
||||
});
|
||||
|
||||
function editDraft(event: Event) {
|
||||
const target = event.target as HTMLElement;
|
||||
const value = target.textContent;
|
||||
const target = event.target as HTMLElement;
|
||||
const value = target.textContent;
|
||||
|
||||
value && emit("update", { title: value });
|
||||
value && emit("update", { title: value });
|
||||
}
|
||||
|
||||
const onEditDraft = useDebounceFn(editDraft, 1000);
|
||||
|
||||
function onPickColor(color: string) {
|
||||
emit("update", { color });
|
||||
emit("update", { color });
|
||||
|
||||
showColorPicker.value = false;
|
||||
showColorPicker.value = false;
|
||||
}
|
||||
|
||||
function onEnter(event: Event) {
|
||||
const target = event.currentTarget as HTMLElement;
|
||||
target.blur();
|
||||
const target = event.currentTarget as HTMLElement;
|
||||
target.blur();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex cursor-pointer items-center border-b border-gray-400 py-5 transition-colors hover:bg-gray-50"
|
||||
:class="{ '!border-black bg-gray-50': selected }"
|
||||
v-bind="attrs"
|
||||
>
|
||||
<Checkbox
|
||||
class="mr-2"
|
||||
:model-value="selected"
|
||||
@update:model-value="emit('update:selected', $event)"
|
||||
></Checkbox>
|
||||
<div class="flex w-[56%] items-center space-x-2">
|
||||
<div
|
||||
ref="color"
|
||||
@click="showColorPicker = !showColorPicker"
|
||||
class="hover:shadow-colorPick h-5 w-5 cursor-pointer rounded-full"
|
||||
:class="{ [`bg-${project.color}-400`]: true }"
|
||||
></div>
|
||||
<span>
|
||||
#<span
|
||||
class="outline-0"
|
||||
contenteditable="true"
|
||||
@input="onEditDraft"
|
||||
@keydown.enter.prevent="onEnter"
|
||||
>
|
||||
{{ project.title }}
|
||||
</span>
|
||||
</span>
|
||||
<div class="flex cursor-pointer items-center border-b border-gray-400 py-5 transition-colors hover:bg-gray-50"
|
||||
:class="{ '!border-black bg-gray-50': selected }" v-bind="attrs">
|
||||
<Checkbox class="mr-2" :model-value="selected" @update:model-value="emit('update:selected', $event)"></Checkbox>
|
||||
<div class="flex w-[56%] items-center space-x-2">
|
||||
<div ref="color" @click="showColorPicker = !showColorPicker"
|
||||
class="hover:shadow-colorPick h-5 w-5 cursor-pointer rounded-full"
|
||||
:class="{ [`bg-${project.color}-400`]: true }"></div>
|
||||
<span>
|
||||
#<span class="outline-0" contenteditable="true" @input="onEditDraft" @keydown.enter.prevent="onEnter">
|
||||
{{ project.title }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex cursor-pointer items-center space-x-1.5 rounded px-2 py-1 text-xs capitalize text-gray-800 transition-colors hover:bg-gray-100"
|
||||
@click="emit('setStatus')">
|
||||
<template v-if="projectStatus">
|
||||
<Icon :name="projectStatus?.icon" :size="14"></Icon>
|
||||
<span>{{ t(projectStatus?.title) }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span>Set status</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex cursor-pointer items-center space-x-1.5 rounded px-2 py-1 text-xs capitalize text-gray-800 transition-colors hover:bg-gray-100"
|
||||
@click="emit('setStatus')"
|
||||
>
|
||||
<template v-if="projectStatus">
|
||||
<Icon :name="projectStatus?.icon" :size="14"></Icon>
|
||||
<span>{{ projectStatus?.title }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span>Set status</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<teleport to="body">
|
||||
<div
|
||||
v-if="showColorPicker && colorBound"
|
||||
ref="colorPicker"
|
||||
class="fixed flex w-[156px] -translate-x-1/2 flex-wrap gap-2.5 rounded-lg bg-white p-2 shadow"
|
||||
:style="{
|
||||
top: colorBound.top + colorBound.height + 4 + 'px',
|
||||
left: colorBound.left + 'px',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-for="c in projectStore.colors"
|
||||
:key="c"
|
||||
class="hover:shadow-colorPick h-5 w-5 cursor-pointer rounded-full border"
|
||||
:class="{ [`bg-${c}-400`]: true, 'border-black': c === project.color }"
|
||||
@click="onPickColor(c)"
|
||||
></div>
|
||||
</div>
|
||||
</teleport>
|
||||
<teleport to="body">
|
||||
<div v-if="showColorPicker && colorBound" ref="colorPicker"
|
||||
class="fixed flex w-[156px] -translate-x-1/2 flex-wrap gap-2.5 rounded-lg bg-white p-2 shadow" :style="{
|
||||
top: colorBound.top + colorBound.height + 4 + 'px',
|
||||
left: colorBound.left + 'px',
|
||||
}">
|
||||
<div v-for="c in projectStore.colors" :key="c"
|
||||
class="hover:shadow-colorPick h-5 w-5 cursor-pointer rounded-full border"
|
||||
:class="{ [`bg-${c}-400`]: true, 'border-black': c === project.color }" @click="onPickColor(c)"></div>
|
||||
</div>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
@ -111,27 +111,18 @@ app
|
||||
})
|
||||
.use(router);
|
||||
|
||||
|
||||
|
||||
|
||||
inject() // injecting analytics
|
||||
|
||||
function customRule(choice: number, choicesLength: number) {
|
||||
if (choice === 0) {
|
||||
return 0
|
||||
}
|
||||
function customRule(choice: number, _choicesLength: number) {
|
||||
const calcValue = Math.abs(choice) % 100;
|
||||
const num = calcValue % 10;
|
||||
|
||||
const teen = choice > 10 && choice < 20
|
||||
const endsWithOne = choice % 10 === 1
|
||||
if (!teen && endsWithOne) {
|
||||
return 1
|
||||
}
|
||||
if (!teen && choice % 10 >= 2 && choice % 10 <= 4) {
|
||||
return 2
|
||||
}
|
||||
|
||||
return choicesLength < 4 ? 2 : 3
|
||||
if (calcValue > 10 && calcValue < 20) return 3;
|
||||
if (num > 1 && num < 5) return 2;
|
||||
if (num === 1) return 1;
|
||||
return 3;
|
||||
}
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: "ru",
|
||||
fallbackLocale: "en",
|
||||
|
||||
@ -11,136 +11,168 @@ import hotkeys from "hotkeys-js";
|
||||
import { useDeleteModalStore } from "@store/delete-modal";
|
||||
import { nanoid } from "nanoid";
|
||||
import { onMounted, onUnmounted, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const projectStore = useProjectStore();
|
||||
const projectStatusModalStore = useProjectStatusModalStore();
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
title: "Projects",
|
||||
create: "New project",
|
||||
updateStatus: "Update {length} projects?",
|
||||
column: {
|
||||
title: "Title",
|
||||
status: "Status"
|
||||
},
|
||||
deleteModal: {
|
||||
multipleDelete: `Are you sure you want to delete {length} projects?`,
|
||||
warning: "Delete would be permanent, you can’t restore issues later.",
|
||||
spesificDelete: `Are you sure you want to delete “{title}”?`
|
||||
},
|
||||
},
|
||||
|
||||
ru: {
|
||||
title: "Проекты",
|
||||
create: "Новый проект",
|
||||
updateStatus: "Обновить {length} проект? | Обновить {length} проекта? | Обновить {length} проектов?",
|
||||
column: {
|
||||
title: "Название",
|
||||
status: "Статус"
|
||||
},
|
||||
deleteModal: {
|
||||
multipleDelete: `Вы уверенны, что хотите удалить {length} проект? | Вы уверенны, что хотите удалить {length} проекта? | Вы уверенны, что хотите удалить {length} проектов?`,
|
||||
warning: "Удаление будет пермонентным, вы не сможете восстановить проекты",
|
||||
spesificDelete: `Вы уверены, что хотети удалить “{title}”?`
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const rowsContainer = ref<HTMLElement | null>(null);
|
||||
|
||||
onMounted(async () => {
|
||||
await projectStore.loadProjects();
|
||||
await projectStore.loadProjects();
|
||||
|
||||
hotkeys("C", onCreateProject);
|
||||
hotkeys("C", onCreateProject);
|
||||
|
||||
hotkeys("x", () => {
|
||||
if (hoveredProjectId.value) {
|
||||
updateSelection(hoveredProjectId.value);
|
||||
}
|
||||
});
|
||||
hotkeys("x", () => {
|
||||
if (hoveredProjectId.value) {
|
||||
updateSelection(hoveredProjectId.value);
|
||||
}
|
||||
});
|
||||
|
||||
hotkeys("cmd+backspace", () => {
|
||||
removeProject();
|
||||
});
|
||||
hotkeys("cmd+backspace", () => {
|
||||
removeProject();
|
||||
});
|
||||
|
||||
hotkeys("S", () => {
|
||||
onSetStatus();
|
||||
});
|
||||
hotkeys("S", () => {
|
||||
onSetStatus();
|
||||
});
|
||||
|
||||
hotkeys("esc", () => {
|
||||
selectedProjectsIds.value = [];
|
||||
});
|
||||
hotkeys("esc", () => {
|
||||
selectedProjectsIds.value = [];
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
hotkeys.unbind("C", "all", onCreateProject);
|
||||
hotkeys.unbind("C", "all", onCreateProject);
|
||||
});
|
||||
|
||||
function onUpdateProject(id: string, project: Partial<Project>) {
|
||||
if (project?.title === "") return;
|
||||
if (project?.title === "") return;
|
||||
|
||||
projectStore.edit(id, project);
|
||||
projectStore.edit(id, project);
|
||||
}
|
||||
|
||||
function onCreateProject() {
|
||||
projectStore.create("");
|
||||
projectStore.create("");
|
||||
|
||||
setTimeout(() => {
|
||||
if (!rowsContainer.value) return;
|
||||
setTimeout(() => {
|
||||
if (!rowsContainer.value) return;
|
||||
|
||||
const addedElement = Array.from(rowsContainer.value.children).find((el) => {
|
||||
const editable = el.querySelector("[contenteditable]")!;
|
||||
return editable.textContent === "";
|
||||
const addedElement = Array.from(rowsContainer.value.children).find((el) => {
|
||||
const editable = el.querySelector("[contenteditable]")!;
|
||||
return editable.textContent === "";
|
||||
});
|
||||
|
||||
addedElement?.scrollIntoView();
|
||||
addedElement &&
|
||||
focusOnEditableElement(
|
||||
addedElement.querySelector("[contenteditable]") as HTMLElement,
|
||||
);
|
||||
});
|
||||
|
||||
addedElement?.scrollIntoView();
|
||||
addedElement &&
|
||||
focusOnEditableElement(
|
||||
addedElement.querySelector("[contenteditable]") as HTMLElement,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const selectedProjectsIds = ref<string[]>([]);
|
||||
|
||||
function updateSelection(projectId: string) {
|
||||
if (selectedProjectsIds.value.includes(projectId)) {
|
||||
const idx = selectedProjectsIds.value.findIndex((id) => id === projectId);
|
||||
selectedProjectsIds.value.splice(idx, 1);
|
||||
return;
|
||||
}
|
||||
if (selectedProjectsIds.value.includes(projectId)) {
|
||||
const idx = selectedProjectsIds.value.findIndex((id) => id === projectId);
|
||||
selectedProjectsIds.value.splice(idx, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
selectedProjectsIds.value.push(projectId);
|
||||
selectedProjectsIds.value.push(projectId);
|
||||
}
|
||||
|
||||
async function onSetStatus() {
|
||||
if (selectedProjectsIds.value.length <= 0) {
|
||||
if (!hoveredProjectId.value) return;
|
||||
if (selectedProjectsIds.value.length <= 0) {
|
||||
if (!hoveredProjectId.value) return;
|
||||
|
||||
const project = projectStore.getOne(hoveredProjectId.value);
|
||||
if (!project) return;
|
||||
const project = projectStore.getOne(hoveredProjectId.value);
|
||||
if (!project) return;
|
||||
|
||||
selectedProjectsIds.value = [];
|
||||
selectedProjectsIds.value = [];
|
||||
|
||||
const statusId = await projectStatusModalStore.use({
|
||||
hintText: project.title,
|
||||
statusId: project.statusId,
|
||||
});
|
||||
|
||||
projectStore.edit(project.id, { statusId });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const statusId = await projectStatusModalStore.use({
|
||||
hintText: project.title,
|
||||
statusId: project.statusId,
|
||||
hintText: t("updateStatus", { length: selectedProjectsIds.value.length }, selectedProjectsIds.value.length),
|
||||
statusId: null,
|
||||
});
|
||||
|
||||
projectStore.edit(project.id, { statusId });
|
||||
selectedProjectsIds.value.forEach((id) => {
|
||||
projectStore.edit(id, { statusId });
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const statusId = await projectStatusModalStore.use({
|
||||
hintText: `Update ${selectedProjectsIds.value.length} projects`,
|
||||
statusId: null,
|
||||
});
|
||||
|
||||
selectedProjectsIds.value.forEach((id) => {
|
||||
projectStore.edit(id, { statusId });
|
||||
});
|
||||
|
||||
selectedProjectsIds.value = [];
|
||||
selectedProjectsIds.value = [];
|
||||
}
|
||||
|
||||
const deleteModalStore = useDeleteModalStore();
|
||||
async function removeProject() {
|
||||
if (selectedProjectsIds.value.length) {
|
||||
if (selectedProjectsIds.value.length) {
|
||||
const res = await deleteModalStore.use({
|
||||
titleText: t("deleteModal.multipleDelete", { length: selectedProjectsIds.value.length }, selectedProjectsIds.value.length),
|
||||
descriptionText: t("deleteModal.warning"),
|
||||
});
|
||||
if (!res) return;
|
||||
|
||||
projectStore.remove(selectedProjectsIds.value);
|
||||
selectedProjectsIds.value = [];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const draft = hoveredProjectId.value
|
||||
? projectStore.getOne(hoveredProjectId.value)
|
||||
: null;
|
||||
if (!draft) return;
|
||||
|
||||
const res = await deleteModalStore.use({
|
||||
titleText: `Are you sure you want to delete ${selectedProjectsIds.value.length} drafts?`,
|
||||
descriptionText:
|
||||
"Delete would be permanent, you can’t restore projects later.",
|
||||
titleText: t("deleteModal.spesificDelete", { title: draft.title }),
|
||||
descriptionText: t("deleteModal.warning"),
|
||||
});
|
||||
if (!res) return;
|
||||
|
||||
projectStore.remove(selectedProjectsIds.value);
|
||||
selectedProjectsIds.value = [];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const draft = hoveredProjectId.value
|
||||
? projectStore.getOne(hoveredProjectId.value)
|
||||
: null;
|
||||
if (!draft) return;
|
||||
|
||||
const res = await deleteModalStore.use({
|
||||
titleText: `Are you sure you want to delete “${draft.title}”?`,
|
||||
descriptionText:
|
||||
"Delete would be permanent, you can’t restore project later.",
|
||||
});
|
||||
|
||||
res && projectStore.remove(draft.id);
|
||||
res && projectStore.remove(draft.id);
|
||||
}
|
||||
|
||||
const contextMenuOpen = ref(false);
|
||||
@ -151,88 +183,63 @@ const clickY = ref(0);
|
||||
const clickX = ref(0);
|
||||
|
||||
function onOpenContextMenu(evt: MouseEvent) {
|
||||
clickX.value = evt.clientX;
|
||||
clickY.value = evt.clientY;
|
||||
clickX.value = evt.clientX;
|
||||
clickY.value = evt.clientY;
|
||||
|
||||
contextMenuOpen.value = true;
|
||||
setScrolling(false);
|
||||
contextMenuOpen.value = true;
|
||||
setScrolling(false);
|
||||
}
|
||||
|
||||
function onListLeave() {
|
||||
if (!contextMenuOpen.value) {
|
||||
hoveredProjectId.value = null;
|
||||
}
|
||||
if (!contextMenuOpen.value) {
|
||||
hoveredProjectId.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function onSelectOption(action: string) {
|
||||
if (action === "stat") {
|
||||
onSetStatus();
|
||||
} else if (action === "del") {
|
||||
removeProject();
|
||||
}
|
||||
if (action === "stat") {
|
||||
onSetStatus();
|
||||
} else if (action === "del") {
|
||||
removeProject();
|
||||
}
|
||||
|
||||
contextMenuOpen.value = false;
|
||||
contextMenuOpen.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pt-8">
|
||||
<div class="mb-6 flex items-center space-x-5">
|
||||
<h1 class="text-xl">Projects</h1>
|
||||
<div
|
||||
v-hint="'C'"
|
||||
@click="onCreateProject()"
|
||||
class="cursor-pointer rounded-lg px-1.5 py-[3px] text-gray-600 transition-colors hover:bg-gray-400"
|
||||
>
|
||||
New project +
|
||||
</div>
|
||||
<div class="pt-8">
|
||||
<div class="mb-6 flex items-center space-x-5">
|
||||
<h1 class="text-xl">{{ t("title") }}</h1>
|
||||
<div v-hint="'C'" @click="onCreateProject()"
|
||||
class="cursor-pointer rounded-lg px-1.5 py-[3px] text-gray-600 transition-colors hover:bg-gray-400">
|
||||
{{ t("create") }} +
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="projectStore.projects.length" class="flex items-center space-x-0.5">
|
||||
<div class="flex w-[59%] items-center pl-[2.4%] text-xs text-gray-600">
|
||||
<span> {{ t("column.title") }} </span>
|
||||
</div>
|
||||
<div class="flex items-center text-xs text-gray-600">
|
||||
<span> {{ t("column.status") }} </span>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="rowsContainer" class="" @contextmenu.prevent="onOpenContextMenu" @mouseleave="onListLeave">
|
||||
<ProjectRow v-for="row in projectStore.projects" :project="row" :key="row.id"
|
||||
:selected="selectedProjectsIds.includes(row.id)" @mouseenter="hoveredProjectId = row.id"
|
||||
@update:selected="updateSelection(row.id)" @update="onUpdateProject(row.id, $event)"
|
||||
@setStatus="onSetStatus()" @contextmenu="onOpenContextMenu"></ProjectRow>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="projectStore.projects.length"
|
||||
class="flex items-center space-x-0.5"
|
||||
>
|
||||
<div class="flex w-[59%] items-center pl-[2.4%] text-xs text-gray-600">
|
||||
<span> Title </span>
|
||||
</div>
|
||||
<div class="flex items-center text-xs text-gray-600">
|
||||
<span> Status </span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="rowsContainer"
|
||||
class=""
|
||||
@contextmenu.prevent="onOpenContextMenu"
|
||||
@mouseleave="onListLeave"
|
||||
>
|
||||
<ProjectRow
|
||||
v-for="row in projectStore.projects"
|
||||
:project="row"
|
||||
:key="row.id"
|
||||
:selected="selectedProjectsIds.includes(row.id)"
|
||||
@mouseenter="hoveredProjectId = row.id"
|
||||
@update:selected="updateSelection(row.id)"
|
||||
@update="onUpdateProject(row.id, $event)"
|
||||
@setStatus="onSetStatus()"
|
||||
@contextmenu="onOpenContextMenu"
|
||||
></ProjectRow>
|
||||
</div>
|
||||
</div>
|
||||
<teleport to="body">
|
||||
<ContextMenu
|
||||
ref="contextMenuEl"
|
||||
v-model:show="contextMenuOpen"
|
||||
:options="[
|
||||
{ id: nanoid(3), label: 'Delete', kbd: '⌘ ⌫', value: 'del' },
|
||||
{ id: nanoid(3), label: 'Set status', kbd: 'S', value: 'stat' },
|
||||
]"
|
||||
class="absolute"
|
||||
:style="{
|
||||
top: `${clickY}px`,
|
||||
left: `${clickX}px`,
|
||||
}"
|
||||
@option="onSelectOption"
|
||||
></ContextMenu>
|
||||
</teleport>
|
||||
<ProjectStatusModal></ProjectStatusModal>
|
||||
<teleport to="body">
|
||||
<ContextMenu ref="contextMenuEl" v-model:show="contextMenuOpen" :options="[
|
||||
{ id: nanoid(3), label: 'Delete', kbd: '⌘ ⌫', value: 'del' },
|
||||
{ id: nanoid(3), label: 'Set status', kbd: 'S', value: 'stat' },
|
||||
]" class="absolute" :style="{
|
||||
top: `${clickY}px`,
|
||||
left: `${clickX}px`,
|
||||
}" @option="onSelectOption"></ContextMenu>
|
||||
</teleport>
|
||||
<ProjectStatusModal></ProjectStatusModal>
|
||||
</template>
|
||||
@store/select-modal
|
||||
|
||||
Loading…
Reference in New Issue
Block a user