Files
2026-09-14 10:47:21 +08:00

195 lines
5.6 KiB
Vue

<script setup>
import { reactive, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStore, useMapGetter } from 'dashboard/composables/store';
import { useRoute } from 'vue-router';
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
import Editor from 'dashboard/components-next/Editor/Editor.vue';
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import ContactNoteItem from './components/ContactNoteItem.vue';
const { t } = useI18n();
const store = useStore();
const route = useRoute();
const state = reactive({
message: '',
editMessage: '',
editingNoteId: null,
});
let editorGeneration = 0;
const currentUser = useMapGetter('getCurrentUser');
const notesByContact = useMapGetter('contactNotes/getAllNotesByContactId');
const uiFlags = useMapGetter('contactNotes/getUIFlags');
const isFetchingNotes = computed(() => uiFlags.value.isFetching);
const isCreatingNote = computed(() => uiFlags.value.isCreating);
const isUpdatingNote = computed(() => uiFlags.value.isUpdating);
const isDeletingNote = computed(() => uiFlags.value.isDeleting);
const notes = computed(() => notesByContact.value(route.params.contactId));
const getWrittenBy = note => {
const isCurrentUser = note?.user?.id === currentUser.value.id;
return isCurrentUser
? t('CONTACTS_LAYOUT.SIDEBAR.NOTES.YOU')
: note?.user?.name || 'Bot';
};
const onAdd = async content => {
if (!content || isCreatingNote.value) return;
const targetContactId = route.params.contactId;
const generation = editorGeneration;
await store.dispatch('contactNotes/create', {
content,
contactId: targetContactId,
});
if (
generation === editorGeneration &&
route.params.contactId === targetContactId &&
state.message === content
) {
state.message = '';
}
};
const onEdit = note => {
if (!note?.id || isUpdatingNote.value) return;
editorGeneration += 1;
state.editingNoteId = note.id;
state.editMessage = note.content || '';
};
const closeEdit = () => {
editorGeneration += 1;
state.editingNoteId = null;
state.editMessage = '';
};
const onUpdate = async () => {
if (!state.editingNoteId || !state.editMessage || isUpdatingNote.value)
return;
const targetContactId = route.params.contactId;
const targetNoteId = state.editingNoteId;
const content = state.editMessage;
const generation = editorGeneration;
await store.dispatch('contactNotes/update', {
noteId: targetNoteId,
contactId: targetContactId,
content,
});
if (
generation === editorGeneration &&
route.params.contactId === targetContactId &&
state.editingNoteId === targetNoteId &&
state.editMessage === content
) {
closeEdit();
}
};
const onDelete = noteId => {
if (!noteId || isDeletingNote.value) return;
const { contactId } = route.params;
store.dispatch('contactNotes/delete', { noteId, contactId });
};
const keyboardEvents = {
'$mod+Enter': {
action: () => (state.editingNoteId ? onUpdate() : onAdd(state.message)),
allowOnFocusedInput: true,
},
};
useKeyboardEvents(keyboardEvents);
watch(
() => route.params.contactId,
() => {
editorGeneration += 1;
state.message = '';
closeEdit();
}
);
</script>
<template>
<div class="flex flex-col gap-6 py-6">
<Editor
v-model="state.message"
:placeholder="t('CONTACTS_LAYOUT.SIDEBAR.NOTES.PLACEHOLDER')"
focus-on-mount
:max-length="0"
:show-character-count="false"
class="[&>div]:!border-transparent [&>div]:px-4 [&>div]:py-4 px-6"
>
<template #actions>
<div class="flex items-center gap-3">
<Button
variant="link"
color="blue"
size="sm"
:label="t('CONTACTS_LAYOUT.SIDEBAR.NOTES.SAVE')"
class="hover:no-underline"
:is-loading="isCreatingNote"
:disabled="!state.message || isCreatingNote"
@click="onAdd(state.message)"
/>
</div>
</template>
</Editor>
<div
v-if="isFetchingNotes"
class="flex items-center justify-center py-10 text-n-slate-11"
>
<Spinner />
</div>
<div v-else-if="notes.length > 0">
<ContactNoteItem
v-for="note in notes"
:key="note.id"
class="mx-6 py-4"
:note="note"
:written-by="getWrittenBy(note)"
allow-delete
allow-edit
@delete="onDelete"
@edit="onEdit"
/>
</div>
<p v-else class="px-6 py-6 text-sm leading-6 text-center text-n-slate-11">
{{ t('CONTACTS_LAYOUT.SIDEBAR.NOTES.EMPTY_STATE') }}
</p>
<woot-modal
:show="Boolean(state.editingNoteId)"
:on-close="closeEdit"
:close-on-backdrop-click="false"
class="!items-start [&>div]:!top-12 [&>div]:sticky"
>
<div class="flex w-full flex-col gap-6 px-6 py-6">
<h3 class="text-lg font-semibold text-n-slate-12">
{{ t('CONTACTS_LAYOUT.SIDEBAR.NOTES.EDIT_NOTE') }}
</h3>
<Editor
v-model="state.editMessage"
focus-on-mount
:placeholder="t('CONTACTS_LAYOUT.SIDEBAR.NOTES.PLACEHOLDER')"
:max-length="0"
:show-character-count="false"
class="[&>div]:!border-transparent [&>div]:px-4 [&>div]:py-4"
/>
<div class="flex items-center justify-end gap-3">
<Button
solid
blue
:label="t('CONTACTS_LAYOUT.SIDEBAR.NOTES.SAVE')"
:is-loading="isUpdatingNote"
:disabled="!state.editMessage || isUpdatingNote"
@click="onUpdate"
/>
</div>
</div>
</woot-modal>
</div>
</template>