feat: Allow creating contact notes (#12494)

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
Shivam Mishra
2025-09-23 18:53:00 +05:30
committed by GitHub
parent 2e108653ae
commit d762829519
2 changed files with 142 additions and 25 deletions

View File

@@ -555,10 +555,12 @@
"WROTE": "wrote", "WROTE": "wrote",
"YOU": "You", "YOU": "You",
"SAVE": "Save note", "SAVE": "Save note",
"ADD_NOTE": "Add contact note",
"EXPAND": "Expand", "EXPAND": "Expand",
"COLLAPSE": "Collapse", "COLLAPSE": "Collapse",
"NO_NOTES": "No notes, you can add notes from the contact details page.", "NO_NOTES": "No notes, you can add notes from the contact details page.",
"EMPTY_STATE": "There are no notes associated to this contact. You can add a note by typing in the box above." "EMPTY_STATE": "There are no notes associated to this contact. You can add a note by typing in the box above.",
"CONVERSATION_EMPTY_STATE": "There are no notes yet. Use the Add note button to create one."
} }
}, },
"EMPTY_STATE": { "EMPTY_STATE": {

View File

@@ -1,21 +1,34 @@
<script setup> <script setup>
import { watch, computed } from 'vue'; import { watch, computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
import { useStore, useMapGetter } from 'dashboard/composables/store'; import { useStore, useMapGetter } from 'dashboard/composables/store';
import ContactNoteItem from 'next/Contacts/ContactsSidebar/components/ContactNoteItem.vue';
import Spinner from 'next/spinner/Spinner.vue';
const { contactId } = defineProps({ import Editor from 'dashboard/components-next/Editor/Editor.vue';
contactId: { type: String, required: true }, import NextButton from 'dashboard/components-next/button/Button.vue';
import ContactNoteItem from 'next/Contacts/ContactsSidebar/components/ContactNoteItem.vue';
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
const props = defineProps({
contactId: { type: [String, Number], required: true },
}); });
const { t } = useI18n(); const { t } = useI18n();
const store = useStore(); const store = useStore();
const currentUser = useMapGetter('getCurrentUser'); const currentUser = useMapGetter('getCurrentUser');
const uiFlags = useMapGetter('contactNotes/getUIFlags'); const uiFlags = useMapGetter('contactNotes/getUIFlags');
const notesByContact = useMapGetter('contactNotes/getAllNotesByContactId');
const isFetchingNotes = computed(() => uiFlags.value.isFetching); const isFetchingNotes = computed(() => uiFlags.value.isFetching);
const notGetterFn = useMapGetter('contactNotes/getAllNotesByContactId'); const isCreatingNote = computed(() => uiFlags.value.isCreating);
const notes = computed(() => notGetterFn.value(contactId)); const contactId = computed(() => props.contactId);
const noteContent = ref('');
const shouldShowCreateModal = ref(false);
const notes = computed(() => {
if (!contactId.value) {
return [];
}
return notesByContact.value(contactId.value) || [];
});
const getWrittenBy = ({ user } = {}) => { const getWrittenBy = ({ user } = {}) => {
const currentUserId = currentUser.value?.id; const currentUserId = currentUser.value?.id;
@@ -24,28 +37,130 @@ const getWrittenBy = ({ user } = {}) => {
: user?.name || t('CONVERSATION.BOT'); : user?.name || t('CONVERSATION.BOT');
}; };
const openCreateModal = () => {
if (!contactId.value) {
return;
}
noteContent.value = '';
shouldShowCreateModal.value = true;
};
const closeCreateModal = () => {
shouldShowCreateModal.value = false;
noteContent.value = '';
};
const onAdd = async () => {
if (!contactId.value || !noteContent.value || isCreatingNote.value) {
return;
}
await store.dispatch('contactNotes/create', {
content: noteContent.value,
contactId: contactId.value,
});
noteContent.value = '';
closeCreateModal();
};
const onDelete = noteId => {
if (!contactId.value || !noteId) {
return;
}
store.dispatch('contactNotes/delete', {
noteId,
contactId: contactId.value,
});
};
const keyboardEvents = {
'$mod+Enter': {
action: onAdd,
allowOnFocusedInput: true,
},
};
useKeyboardEvents(keyboardEvents);
watch( watch(
() => contactId, contactId,
() => store.dispatch('contactNotes/get', { contactId }), newContactId => {
closeCreateModal();
if (newContactId) {
store.dispatch('contactNotes/get', { contactId: newContactId });
}
},
{ immediate: true } { immediate: true }
); );
</script> </script>
<template> <template>
<div v-if="isFetchingNotes" class="p-8 grid place-content-center"> <div>
<div class="px-4 pt-3 pb-2">
<NextButton
ghost
xs
icon="i-lucide-plus"
:label="$t('CONTACTS_LAYOUT.SIDEBAR.NOTES.ADD_NOTE')"
:disabled="!contactId || isFetchingNotes"
@click="openCreateModal"
/>
</div>
<div
v-if="isFetchingNotes"
class="flex items-center justify-center py-8 text-n-slate-11"
>
<Spinner /> <Spinner />
</div> </div>
<div v-else-if="!notes.length" class="p-8 grid place-content-center"> <div
<p class="text-center">{{ t('CONTACTS_LAYOUT.SIDEBAR.NOTES.NO_NOTES') }}</p> v-else-if="notes.length"
</div> class="flex flex-col max-h-[300px] overflow-y-auto"
<div v-else class="max-h-[300px] overflow-scroll"> >
<ContactNoteItem <ContactNoteItem
v-for="note in notes" v-for="note in notes"
:key="note.id" :key="note.id"
class="p-4 last-of-type:border-b-0" class="py-4 last-of-type:border-b-0 px-4"
:note="note" :note="note"
collapsible
:written-by="getWrittenBy(note)" :written-by="getWrittenBy(note)"
allow-delete
collapsible
@delete="onDelete"
/> />
</div> </div>
<p v-else class="px-6 py-6 text-sm leading-6 text-center text-n-slate-11">
{{ t('CONTACTS_LAYOUT.SIDEBAR.NOTES.CONVERSATION_EMPTY_STATE') }}
</p>
<woot-modal
v-model:show="shouldShowCreateModal"
:on-close="closeCreateModal"
: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.ADD_NOTE') }}
</h3>
<Editor
v-model="noteContent"
focus-on-mount
:placeholder="t('CONTACTS_LAYOUT.SIDEBAR.NOTES.PLACEHOLDER')"
class="[&>div]:!border-transparent [&>div]:px-4 [&>div]:py-4"
/>
<div class="flex items-center justify-end gap-3">
<NextButton
solid
blue
:label="t('CONTACTS_LAYOUT.SIDEBAR.NOTES.SAVE')"
:is-loading="isCreatingNote"
:disabled="!noteContent || isCreatingNote"
@click="onAdd"
/>
</div>
</div>
</woot-modal>
</div>
</template> </template>