Skip to content

Commit 50b315e

Browse files
authored
Merge pull request #10 from Dobrunia/main
tags
2 parents 79bf70c + 3bd87ea commit 50b315e

10 files changed

Lines changed: 695 additions & 214 deletions

File tree

TODO.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
# Фичи
66

7-
- Отображать превью ссылок (если тип ссылка)
7+
- Markdown в заметках
88
- давай реализуем большую фичу - Архив карточек/воркспейсов карточка отправляется в архив из модального окна редактирования воркспейс из списка на главной добавь иконку к карточке воркспейса. Для Архива есть отдельная вкладка (vue) там 2 радиокнопки архивные карточки и воркспейсы (архивные карточки удаляются вместе с воркспейсом, это на всякий прописал чтобы не было такого что некуда восстановить карточку)
99
- Экспорт/импорт JSON (всё) данные
1010

@@ -14,5 +14,9 @@
1414

1515
# На далекое будущее
1616

17+
- AI-саммари — автоматическое резюме по карточке
18+
- AI-саммари конкретно по ссылке пишет в конспект (карточка типа ссылка) кнопка получить краткую выжимку
1719
- совместный доступ к воркспейсам
1820
- из статистики переход к воркспейсам
21+
- Вид воркспейсов (пространство как в миро)
22+
- Отображать превью ссылок (если тип ссылка)

client/src/App.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ watch(
3535
<template v-else>
3636
<div class="min-h-screen bg-bg">
3737
<AppHeader />
38-
<main class="pt-16">
38+
<main :class="authStore.user ? 'pt-25' : 'pt-14'">
3939
<RouterView />
4040
</main>
4141
<Toaster position="bottom-right" />
Lines changed: 25 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,48 @@
11
<script setup lang="ts">
2-
import { ref, computed } from 'vue';
3-
import { useWorkspaceStore } from '@/stores/workspace';
4-
import { useCardsStore } from '@/stores/cards';
2+
import { ref, computed, toRef } from 'vue';
53
import type { CardGql } from '@/graphql/types';
64
import { parseCard } from '@/lib/card';
75
import { useInlineEdit } from '@/composables/useInlineEdit';
6+
import { useCardActions } from '@/composables/useCardActions';
87
import CardNote from './CardNote.vue';
98
import CardLink from './CardLink.vue';
109
import CardChecklist from './CardChecklist.vue';
1110
import CreateCardDialog from '@/components/card/CreateCardDialog.vue';
12-
import { toast } from 'vue-sonner';
13-
import { getGraphQLErrorMessage } from '@/lib/graphql-error';
1411
1512
const props = withDefaults(
1613
defineProps<{
1714
card: CardGql;
1815
columnId?: string;
1916
/** Беклог: карточка без колонки, только просмотр/редакт/удаление */
2017
isBacklog?: boolean;
18+
/** Отключить drag-курсор (для Tags view и т.п.) */
19+
static?: boolean;
2120
}>(),
22-
{ isBacklog: false }
21+
{ isBacklog: false, static: false }
2322
);
2423
25-
const workspaceStore = useWorkspaceStore();
26-
const cardsStore = useCardsStore();
24+
const emit = defineEmits<{
25+
(e: 'updated', card: CardGql): void;
26+
(e: 'deleted', cardId: string): void;
27+
}>();
28+
2729
const showEditDialog = ref(false);
2830
const titleContainerRef = ref<HTMLElement | null>(null);
2931
32+
// Используем composable для всех операций с карточкой
33+
const cardActions = useCardActions(toRef(props, 'card'), {
34+
onUpdated: (card) => emit('updated', card),
35+
onDeleted: (cardId) => emit('deleted', cardId),
36+
});
37+
3038
const { isEditing: isEditingTitle, editTitle, inputRef: titleInputRef, startEdit: startEditTitle, saveEdit: saveEditTitle, cancelEdit: cancelEditTitle } = useInlineEdit(
3139
titleContainerRef,
3240
() => props.card.title ?? '',
3341
async (newTitle) => {
34-
const payload = props.card.payload;
35-
if (isHubCard.value) await cardsStore.updateCard(props.card.id, { title: newTitle || undefined, payload });
36-
else await workspaceStore.updateCard(props.card.id, { title: newTitle || undefined, payload });
42+
await cardActions.updateCard({ title: newTitle || undefined, payload: props.card.payload });
3743
}
3844
);
3945
40-
const isHubCard = computed(() => props.isBacklog && props.card.workspaceId == null);
41-
4246
const parsed = computed(() => {
4347
try {
4448
return parseCard(props.card);
@@ -66,79 +70,9 @@ const linkUrl = computed(() => {
6670
return (parsed.value.payload as { url?: string }).url ?? null;
6771
});
6872
69-
async function toggleDone() {
70-
try {
71-
const payload = { done: !props.card.done };
72-
if (isHubCard.value) await cardsStore.updateCard(props.card.id, payload);
73-
else await workspaceStore.updateCard(props.card.id, payload);
74-
} catch (e) {
75-
toast.error(getGraphQLErrorMessage(e));
76-
}
77-
}
78-
79-
async function toggleChecklistItem(index: number) {
80-
if (props.card.type !== 'CHECKLIST') return;
81-
try {
82-
const raw = typeof props.card.payload === 'string' ? JSON.parse(props.card.payload || '{}') : props.card.payload;
83-
const items = Array.isArray((raw as { items?: unknown[] }).items) ? (raw as { items: { id: string; text: string; done: boolean; order: number }[] }).items : [];
84-
const next = items.map((it, i) => (i === index ? { ...it, done: !it.done } : it));
85-
const payload = { payload: JSON.stringify({ items: next }) };
86-
if (isHubCard.value) await cardsStore.updateCard(props.card.id, payload);
87-
else await workspaceStore.updateCard(props.card.id, payload);
88-
} catch (e) {
89-
toast.error(getGraphQLErrorMessage(e));
90-
}
91-
}
92-
93-
async function doDelete() {
94-
try {
95-
if (isHubCard.value) {
96-
await cardsStore.deleteCard(props.card.id);
97-
toast('Карточка удалена', {
98-
action: {
99-
label: 'Отменить',
100-
onClick: () => {
101-
cardsStore.undoDeleteCard().catch((e) => {
102-
toast.error(getGraphQLErrorMessage(e));
103-
});
104-
},
105-
},
106-
duration: 5000,
107-
});
108-
} else {
109-
await workspaceStore.deleteCard(props.card.id);
110-
toast('Карточка удалена', {
111-
action: {
112-
label: 'Отменить',
113-
onClick: () => {
114-
workspaceStore.undoDeleteCard().catch((e) => {
115-
toast.error(getGraphQLErrorMessage(e));
116-
});
117-
},
118-
},
119-
duration: 5000,
120-
});
121-
}
122-
} catch (e) {
123-
toast.error(getGraphQLErrorMessage(e));
124-
}
125-
}
126-
12773
function handleDeleteFromDialog() {
128-
if (!confirm('Удалить карточку?')) return;
129-
doDelete();
13074
showEditDialog.value = false;
131-
}
132-
133-
async function updateSummary(newSummary: string) {
134-
try {
135-
const raw = typeof props.card.payload === 'string' ? JSON.parse(props.card.payload || '{}') : props.card.payload;
136-
const payload = JSON.stringify({ ...raw, summary: newSummary.trim() || undefined });
137-
if (isHubCard.value) await cardsStore.updateCard(props.card.id, { payload });
138-
else await workspaceStore.updateCard(props.card.id, { payload });
139-
} catch (e) {
140-
toast.error(getGraphQLErrorMessage(e));
141-
}
75+
cardActions.deleteCard();
14276
}
14377
</script>
14478

@@ -157,13 +91,13 @@ async function updateSummary(newSummary: string) {
15791
class="card"
15892
:class="[
15993
{ 'opacity-60': card.done },
160-
columnId || (isBacklog && card.workspaceId != null) ? 'cursor-grab active:cursor-grabbing' : 'cursor-default',
94+
!static && (columnId || (isBacklog && card.workspaceId != null)) ? 'cursor-grab active:cursor-grabbing' : 'cursor-default',
16195
]"
16296
>
16397
<div class="flex items-center gap-2 p-3 pb-0">
16498
<button
16599
type="button"
166-
@click="toggleDone"
100+
@click="cardActions.toggleDone()"
167101
class="w-4 h-4 rounded flex-center border transition-colors shrink-0"
168102
:class="card.done ? 'bg-success border-success text-on-primary' : 'border-border hover:border-success'"
169103
>
@@ -203,24 +137,24 @@ async function updateSummary(newSummary: string) {
203137
:done="card.done"
204138
:payload="parsed.payload"
205139
:editable-summary="true"
206-
@update-summary="updateSummary"
140+
@update-summary="cardActions.updateSummary"
207141
/>
208142
<CardLink
209143
v-else-if="parsed.type === 'link'"
210144
:title="card.title"
211145
:done="card.done"
212146
:payload="parsed.payload"
213147
:editable-summary="true"
214-
@update-summary="updateSummary"
148+
@update-summary="cardActions.updateSummary"
215149
/>
216150
<CardChecklist
217151
v-else-if="parsed.type === 'checklist'"
218152
:title="card.title"
219153
:done="card.done"
220154
:payload="parsed.payload"
221155
:editable-summary="true"
222-
@toggle-item="toggleChecklistItem"
223-
@update-summary="updateSummary"
156+
@toggle-item="cardActions.toggleChecklistItem"
157+
@update-summary="cardActions.updateSummary"
224158
/>
225159
</template>
226160

@@ -248,6 +182,7 @@ async function updateSummary(newSummary: string) {
248182
:is-backlog="isBacklog"
249183
@close="showEditDialog = false"
250184
@delete="handleDeleteFromDialog"
185+
@updated="(c) => emit('updated', c)"
251186
/>
252187
</div>
253188
</template>

client/src/components/card/CreateCardDialog.vue

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const props = withDefaults(
3333
const emit = defineEmits<{
3434
close: [];
3535
delete: [];
36+
updated: [card: CardGql];
3637
}>();
3738
3839
const openProxy = computed({
@@ -171,7 +172,14 @@ function addChecklistItem() {
171172
done: false,
172173
order: maxOrder + 100,
173174
});
174-
nextTick(() => initChecklistSortable());
175+
nextTick(() => {
176+
initChecklistSortable();
177+
// Focus the new input
178+
const inputs = checklistListRef.value?.querySelectorAll('input');
179+
if (inputs?.length) {
180+
(inputs[inputs.length - 1] as HTMLInputElement).focus();
181+
}
182+
});
175183
}
176184
177185
function removeChecklistItem(index: number) {
@@ -226,9 +234,11 @@ async function handleSubmit() {
226234
const tags = buildTags();
227235
228236
if (props.card) {
229-
const updatePayload = { title: title.value.trim() || undefined, payload, tags };
237+
const newTitle = title.value.trim() || null;
238+
const updatePayload = { title: newTitle ?? undefined, payload, tags };
230239
if (isHubEdit.value) await cardsStore.updateCard(props.card.id, updatePayload);
231240
else await workspaceStore.updateCard(props.card.id, updatePayload);
241+
emit('updated', { ...props.card, title: newTitle, payload, tags });
232242
toast.success('Сохранено!');
233243
} else if (isGlobalAdd.value && addDestination.value === 'hub') {
234244
await cardsStore.createCard({

0 commit comments

Comments
 (0)