Publish current project state
This commit is contained in:
+27
-11
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed, watch, onMounted, ref } from 'vue';
|
||||
import { computed, watch, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useMapGetter, useStore } from 'dashboard/composables/store';
|
||||
|
||||
@@ -25,9 +25,26 @@ const hoveredLabel = ref(null);
|
||||
|
||||
const allLabels = useMapGetter('labels/getLabels');
|
||||
const contactLabels = useMapGetter('contactLabels/getContactLabels');
|
||||
const hasContactLabels = useMapGetter('contactLabels/hasContactLabels');
|
||||
const contactLabelsUIFlags = useMapGetter('contactLabels/getUIFlags');
|
||||
const currentContactId = computed(
|
||||
() => props.contactId || route.params.contactId
|
||||
);
|
||||
|
||||
const labelsLoaded = computed(
|
||||
() =>
|
||||
Boolean(currentContactId.value) &&
|
||||
hasContactLabels.value(currentContactId.value)
|
||||
);
|
||||
const isBusy = computed(
|
||||
() =>
|
||||
!labelsLoaded.value ||
|
||||
contactLabelsUIFlags.value.isFetching ||
|
||||
contactLabelsUIFlags.value.isUpdating
|
||||
);
|
||||
|
||||
const savedLabels = computed(() => {
|
||||
const availableContactLabels = contactLabels.value(props.contactId);
|
||||
const availableContactLabels = contactLabels.value(currentContactId.value);
|
||||
return allLabels.value.filter(({ title }) =>
|
||||
availableContactLabels.includes(title)
|
||||
);
|
||||
@@ -55,6 +72,9 @@ const fetchLabels = async contactId => {
|
||||
};
|
||||
|
||||
const handleLabelAction = async ({ value }) => {
|
||||
if (isBusy.value) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Get current label titles
|
||||
const currentLabels = savedLabels.value.map(label => label.title);
|
||||
@@ -76,7 +96,7 @@ const handleLabelAction = async ({ value }) => {
|
||||
}
|
||||
|
||||
await store.dispatch('contactLabels/update', {
|
||||
contactId: props.contactId,
|
||||
contactId: currentContactId.value,
|
||||
labels: updatedLabels,
|
||||
});
|
||||
|
||||
@@ -91,18 +111,14 @@ const handleRemoveLabel = label => {
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.contactId,
|
||||
() => currentContactId.value,
|
||||
(newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
if (newVal && newVal !== oldVal) {
|
||||
fetchLabels(newVal);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
onMounted(() => {
|
||||
if (route.params.contactId) {
|
||||
fetchLabels(route.params.contactId);
|
||||
}
|
||||
});
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
// Reset hover state when mouse leaves the container
|
||||
|
||||
+2
-1
@@ -6,6 +6,7 @@ import { useToggle } from '@vueuse/core';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import ConfirmContactDeleteDialog from 'dashboard/components-next/Contacts/ContactsForm/ConfirmContactDeleteDialog.vue';
|
||||
import Policy from 'dashboard/components/policy.vue';
|
||||
import { ROLE_ADMINISTRATOR } from 'dashboard/constants/permissions.js';
|
||||
|
||||
defineProps({
|
||||
selectedContact: {
|
||||
@@ -25,7 +26,7 @@ const openConfirmDeleteContactDialog = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Policy :permissions="['administrator']">
|
||||
<Policy :permissions="[ROLE_ADMINISTRATOR]">
|
||||
<div class="flex flex-col items-start border-t border-n-strong px-6 py-5">
|
||||
<Button
|
||||
:label="t('CONTACTS_LAYOUT.DETAILS.DELETE_CONTACT')"
|
||||
|
||||
+101
-8
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { reactive, computed } from 'vue';
|
||||
import { reactive, computed, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useRoute } from 'vue-router';
|
||||
@@ -16,13 +16,18 @@ 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 => {
|
||||
@@ -32,26 +37,80 @@ const getWrittenBy = note => {
|
||||
: note?.user?.name || 'Bot';
|
||||
};
|
||||
|
||||
const onAdd = content => {
|
||||
if (!content) return;
|
||||
const { contactId } = route.params;
|
||||
store.dispatch('contactNotes/create', { content, contactId });
|
||||
state.message = '';
|
||||
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) return;
|
||||
if (!noteId || isDeletingNote.value) return;
|
||||
const { contactId } = route.params;
|
||||
store.dispatch('contactNotes/delete', { noteId, contactId });
|
||||
};
|
||||
|
||||
const keyboardEvents = {
|
||||
'$mod+Enter': {
|
||||
action: () => onAdd(state.message),
|
||||
action: () => (state.editingNoteId ? onUpdate() : onAdd(state.message)),
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
useKeyboardEvents(keyboardEvents);
|
||||
|
||||
watch(
|
||||
() => route.params.contactId,
|
||||
() => {
|
||||
editorGeneration += 1;
|
||||
state.message = '';
|
||||
closeEdit();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -60,6 +119,8 @@ useKeyboardEvents(keyboardEvents);
|
||||
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>
|
||||
@@ -91,11 +152,43 @@ useKeyboardEvents(keyboardEvents);
|
||||
: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>
|
||||
|
||||
+31
-10
@@ -20,13 +20,17 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
allowEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
collapsible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['delete']);
|
||||
const emit = defineEmits(['delete', 'edit']);
|
||||
const noteContentRef = useTemplateRef('noteContentRef');
|
||||
const needsCollapse = ref(false);
|
||||
const [isExpanded, toggleExpanded] = useToggle();
|
||||
@@ -37,6 +41,10 @@ const handleDelete = () => {
|
||||
emit('delete', props.note.id);
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
emit('edit', props.note);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (props.collapsible) {
|
||||
// Check if content height exceeds approximately 4 lines
|
||||
@@ -71,15 +79,28 @@ onMounted(() => {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
v-if="allowDelete"
|
||||
variant="faded"
|
||||
color="ruby"
|
||||
size="xs"
|
||||
icon="i-lucide-trash"
|
||||
class="opacity-0 group-hover/note:opacity-100"
|
||||
@click="handleDelete"
|
||||
/>
|
||||
<div
|
||||
class="flex items-center gap-1 opacity-0 group-hover/note:opacity-100"
|
||||
>
|
||||
<Button
|
||||
v-if="allowEdit"
|
||||
variant="faded"
|
||||
color="blue"
|
||||
size="xs"
|
||||
icon="i-lucide-pencil"
|
||||
:aria-label="t('CONTACTS_LAYOUT.SIDEBAR.NOTES.EDIT_NOTE')"
|
||||
@click="handleEdit"
|
||||
/>
|
||||
<Button
|
||||
v-if="allowDelete"
|
||||
variant="faded"
|
||||
color="ruby"
|
||||
size="xs"
|
||||
icon="i-lucide-trash"
|
||||
:aria-label="t('CONTACTS_LAYOUT.SIDEBAR.NOTES.DELETE_NOTE')"
|
||||
@click="handleDelete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
ref="noteContentRef"
|
||||
|
||||
+49
-2
@@ -1,9 +1,10 @@
|
||||
<script setup>
|
||||
import { computed, ref, onMounted } from 'vue';
|
||||
import { computed, ref, onMounted, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
import { ROLE_ADMINISTRATOR } from 'dashboard/constants/permissions.js';
|
||||
|
||||
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
@@ -47,6 +48,14 @@ onMounted(() => {
|
||||
Object.assign(contactData.value, getInitialContactData());
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.selectedContact,
|
||||
contact => {
|
||||
contactData.value = contact ? { ...contact } : {};
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
const createdAt = computed(() => {
|
||||
return contactData.value?.createdAt
|
||||
? dynamicTime(contactData.value.createdAt)
|
||||
@@ -63,6 +72,21 @@ const avatarSrc = computed(() => {
|
||||
return avatarUrl.value ? avatarUrl.value : contactData.value?.thumbnail;
|
||||
});
|
||||
|
||||
const shangwutongNameOperations = computed(() =>
|
||||
(
|
||||
contactData.value?.contactInboxes ||
|
||||
contactData.value?.contact_inboxes ||
|
||||
[]
|
||||
)
|
||||
.map(contactInbox => ({
|
||||
contactInbox,
|
||||
operation:
|
||||
contactInbox.shangwutongContactNameOperation ||
|
||||
contactInbox.shangwutong_contact_name_operation,
|
||||
}))
|
||||
.filter(({ operation }) => operation)
|
||||
);
|
||||
|
||||
const handleFormUpdate = updatedData => {
|
||||
Object.assign(contactData.value, updatedData);
|
||||
};
|
||||
@@ -136,6 +160,29 @@ const handleAvatarDelete = async () => {
|
||||
{{ selectedContact?.name }}
|
||||
</h3>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span
|
||||
v-for="item in shangwutongNameOperations"
|
||||
:key="
|
||||
item.operation.contactInboxId ||
|
||||
item.operation.contact_inbox_id ||
|
||||
item.contactInbox.id
|
||||
"
|
||||
class="text-xs text-n-slate-10"
|
||||
>
|
||||
{{
|
||||
item.contactInbox.inbox?.name ||
|
||||
item.contactInbox.inboxName ||
|
||||
item.contactInbox.sourceId ||
|
||||
item.contactInbox.source_id
|
||||
}}:
|
||||
{{ item.operation.status }}
|
||||
<span
|
||||
v-if="item.operation.errorMessage || item.operation.error_message"
|
||||
>
|
||||
—
|
||||
{{ item.operation.errorMessage || item.operation.error_message }}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
v-if="selectedContact?.identifier"
|
||||
class="inline-flex items-center gap-1 text-sm text-n-slate-11"
|
||||
@@ -175,7 +222,7 @@ const handleAvatarDelete = async () => {
|
||||
@click="updateContact"
|
||||
/>
|
||||
</div>
|
||||
<Policy :permissions="['administrator']">
|
||||
<Policy :permissions="[ROLE_ADMINISTRATOR]">
|
||||
<div
|
||||
class="flex flex-col items-start w-full gap-4 pt-6 border-t border-n-strong"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user