From 79daf56c311827b138a9f07e67a6269c52104486 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Fri, 22 Nov 2024 03:17:57 +0530 Subject: [PATCH 01/15] feat: Add contact note item component (#10479) --- .../components/ContactNoteItem.vue | 63 +++++++++++++++++ .../story/ContactNoteItem.story.vue | 39 +++++++++++ .../components/story/fixtures.js | 69 +++++++++++++++++++ .../dashboard/i18n/locale/en/contact.json | 9 +++ app/javascript/histoire.setup.ts | 3 + 5 files changed, 183 insertions(+) create mode 100644 app/javascript/dashboard/components-next/Contacts/ContactsSidebar/components/ContactNoteItem.vue create mode 100644 app/javascript/dashboard/components-next/Contacts/ContactsSidebar/components/story/ContactNoteItem.story.vue create mode 100644 app/javascript/dashboard/components-next/Contacts/ContactsSidebar/components/story/fixtures.js diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/components/ContactNoteItem.vue b/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/components/ContactNoteItem.vue new file mode 100644 index 000000000..81f7a147f --- /dev/null +++ b/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/components/ContactNoteItem.vue @@ -0,0 +1,63 @@ + + + diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/components/story/ContactNoteItem.story.vue b/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/components/story/ContactNoteItem.story.vue new file mode 100644 index 000000000..0dbf8bf01 --- /dev/null +++ b/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/components/story/ContactNoteItem.story.vue @@ -0,0 +1,39 @@ + + + diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/components/story/fixtures.js b/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/components/story/fixtures.js new file mode 100644 index 000000000..440ad95df --- /dev/null +++ b/app/javascript/dashboard/components-next/Contacts/ContactsSidebar/components/story/fixtures.js @@ -0,0 +1,69 @@ +export default [ + { + id: 12, + content: + 'This tutorial will show you how to use Chatwoot and, hence, ensure you practice effective customer communication. We will explain in detail the following:\n\n* Step-by-step setup of your account, with illustrative screenshots.\n\n* An in-depth explanation of all the core features of Chatwoot.\n\n* Get your account up and running by the end of this tutorial.\n\n* Basic concepts of customer communication.', + accountId: null, + contactId: null, + user: { + id: 30, + account_id: 2, + availability_status: 'offline', + auto_offline: true, + confirmed: true, + email: 'bruce@paperlayer.test', + available_name: 'Bruce', + name: 'Bruce', + role: 'administrator', + thumbnail: + 'https://sivin-tunnel.chatwoot.dev/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBJZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--515dbb35e9ba3c36d14f4c4b77220a675513c1fb/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RTNKbGMybDZaVjkwYjE5bWFXeHNXd2RwQWZvdyIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--df796c2af3c0153e55236c2f3cf3a199ac2cb6f7/2.jpg', + custom_role_id: null, + }, + createdAt: 1730786556, + updatedAt: 1730786556, + }, + { + id: 10, + content: + 'We discussed a couple of things:\n\n* Product offering and how it can be useful to talk with people.\n\n* They’ll reach out to us after an internal review.', + accountId: null, + contactId: null, + user: { + id: 1, + account_id: 2, + availability_status: 'online', + auto_offline: false, + confirmed: true, + email: 'hillary@chatwoot.com', + available_name: 'Hillary', + name: 'Hillary', + role: 'administrator', + thumbnail: '', + custom_role_id: null, + }, + createdAt: 1730782566, + updatedAt: 1730782566, + }, + { + id: 9, + content: + 'We discussed a couple of things:\n\n* Product offering and how it can be useful to talk with people.\n\n* They’ll reach out to us after an internal review.', + accountId: null, + contactId: null, + user: { + id: 1, + account_id: 2, + availability_status: 'online', + auto_offline: false, + confirmed: true, + email: 'john@chatwoot.com', + available_name: 'John', + name: 'John', + role: 'administrator', + thumbnail: '', + custom_role_id: null, + }, + createdAt: 1730782564, + updatedAt: 1730782564, + }, +]; diff --git a/app/javascript/dashboard/i18n/locale/en/contact.json b/app/javascript/dashboard/i18n/locale/en/contact.json index e0483d224..5ac4fea21 100644 --- a/app/javascript/dashboard/i18n/locale/en/contact.json +++ b/app/javascript/dashboard/i18n/locale/en/contact.json @@ -440,6 +440,15 @@ } } } + }, + "SIDEBAR": { + "NOTES": { + "PLACEHOLDER": "Add a note", + "WROTE": "wrote", + "YOU": "You", + "SAVE": "Save note", + "EMPTY_STATE": "There are no notes associated to this contact. You can add a note by typing in the box above." + } } } } diff --git a/app/javascript/histoire.setup.ts b/app/javascript/histoire.setup.ts index 13a81608a..6b7358d59 100644 --- a/app/javascript/histoire.setup.ts +++ b/app/javascript/histoire.setup.ts @@ -4,6 +4,8 @@ import i18nMessages from 'dashboard/i18n'; import { createI18n } from 'vue-i18n'; import { vResizeObserver } from '@vueuse/components'; import store from 'dashboard/store'; +import VueDOMPurifyHTML from 'vue-dompurify-html'; +import { domPurifyConfig } from 'shared/helpers/HTMLSanitizer.js'; const i18n = createI18n({ legacy: false, // https://github.com/intlify/vue-i18n/issues/1902 @@ -15,4 +17,5 @@ export const setupVue3 = defineSetupVue3(({ app }) => { app.use(store); app.use(i18n); app.directive('resize', vResizeObserver); + app.use(VueDOMPurifyHTML, domPurifyConfig); }); From 497bc055a2bf24cf3e19762cbeac99fdb2b6db52 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 22 Nov 2024 03:18:25 +0530 Subject: [PATCH 02/15] feat: Attributify button component (#10473) This PR allows attributify for `variant`, `size` and `color` props. This allows using shorthands, instant of writing full props. We also added a small computed method to ensure these does not show up in the DOM and pollute it --------- Co-authored-by: Pranav --- .../components-next/button/Button.vue | 109 ++++++++++++------ .../components-next/button/constants.js | 15 +++ 2 files changed, 90 insertions(+), 34 deletions(-) create mode 100644 app/javascript/dashboard/components-next/button/constants.js diff --git a/app/javascript/dashboard/components-next/button/Button.vue b/app/javascript/dashboard/components-next/button/Button.vue index 02dcfad53..b174ca375 100644 --- a/app/javascript/dashboard/components-next/button/Button.vue +++ b/app/javascript/dashboard/components-next/button/Button.vue @@ -1,46 +1,85 @@ + + diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsForm/story/ContactMergeForm.story.vue b/app/javascript/dashboard/components-next/Contacts/ContactsForm/story/ContactMergeForm.story.vue new file mode 100644 index 000000000..dbf22dadd --- /dev/null +++ b/app/javascript/dashboard/components-next/Contacts/ContactsForm/story/ContactMergeForm.story.vue @@ -0,0 +1,73 @@ + + + diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsForm/story/ContactsForm.story.vue b/app/javascript/dashboard/components-next/Contacts/ContactsForm/story/ContactsForm.story.vue index 4be69380c..306df1df0 100644 --- a/app/javascript/dashboard/components-next/Contacts/ContactsForm/story/ContactsForm.story.vue +++ b/app/javascript/dashboard/components-next/Contacts/ContactsForm/story/ContactsForm.story.vue @@ -1,6 +1,6 @@ + + diff --git a/app/javascript/dashboard/components-next/Label/LabelItem.vue b/app/javascript/dashboard/components-next/Label/LabelItem.vue new file mode 100644 index 000000000..b96b9b368 --- /dev/null +++ b/app/javascript/dashboard/components-next/Label/LabelItem.vue @@ -0,0 +1,22 @@ + + + diff --git a/app/javascript/dashboard/components-next/Label/story/AddLabel.story.vue b/app/javascript/dashboard/components-next/Label/story/AddLabel.story.vue new file mode 100644 index 000000000..ea6466feb --- /dev/null +++ b/app/javascript/dashboard/components-next/Label/story/AddLabel.story.vue @@ -0,0 +1,27 @@ + + + diff --git a/app/javascript/dashboard/components-next/Label/story/Label.story.vue b/app/javascript/dashboard/components-next/Label/story/Label.story.vue new file mode 100644 index 000000000..5319d56ae --- /dev/null +++ b/app/javascript/dashboard/components-next/Label/story/Label.story.vue @@ -0,0 +1,21 @@ + + + diff --git a/app/javascript/dashboard/components-next/Label/story/fixtures.js b/app/javascript/dashboard/components-next/Label/story/fixtures.js new file mode 100644 index 000000000..980ba8c0d --- /dev/null +++ b/app/javascript/dashboard/components-next/Label/story/fixtures.js @@ -0,0 +1,62 @@ +export const label = { + id: 1, + title: 'delivery', + color: '#A2FDD5', +}; + +export const labelMenuItems = [ + { + label: 'delivery', + value: 3, + thumbnail: { + color: '#A2FDD5', + }, + isSelected: true, + action: 'addLabel', + }, + { + label: 'lead', + value: 6, + thumbnail: { + color: '#F161C8', + }, + isSelected: false, + action: 'addLabel', + }, + { + label: 'ops-handover', + value: 4, + thumbnail: { + color: '#A53326', + }, + isSelected: false, + action: 'addLabel', + }, + { + label: 'billing', + value: 1, + thumbnail: { + color: '#28AD21', + }, + isSelected: false, + action: 'addLabel', + }, + { + label: 'premium-customer', + value: 5, + thumbnail: { + color: '#6FD4EF', + }, + isSelected: false, + action: 'addLabel', + }, + { + label: 'software', + value: 2, + thumbnail: { + color: '#8F6EF2', + }, + isSelected: false, + action: 'addLabel', + }, +]; diff --git a/app/javascript/dashboard/i18n/locale/en/components.json b/app/javascript/dashboard/i18n/locale/en/components.json index 7a0533cef..f5207fde9 100644 --- a/app/javascript/dashboard/i18n/locale/en/components.json +++ b/app/javascript/dashboard/i18n/locale/en/components.json @@ -30,5 +30,8 @@ }, "BREADCRUMB": { "ARIA_LABEL": "Breadcrumb" + }, + "LABEL": { + "TAG_BUTTON": "tag" } } From 0f659224a7418c7474a01b9280ac0915080fadda Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 22 Nov 2024 09:08:08 +0530 Subject: [PATCH 05/15] feat: async update of article [CW-3721] (#10435) ### The problem Writing in the text editor can be very frustrating, the reason is that the editor had a debounced save method which would push the article to the backend and update the current state. This however is a bad idea, since the can take anywhere between 100-300ms depending on network conditions. While this would be in progress, the article is still being edited by the user. So at the end of the network request, the state returned from the backend and the current state in the editor is diverged. But since the update happens anyway, the editor would prepend older context. ``` Time --> User Action: [Edit 1] ---> [Edit 2] ---> [Edit 3] Backend Save: Save Req (Edit 1) ----> Response (Edit 1) Resulting Editor State: [Edit 3] + [Edit 1] (Outdated state prepended) ``` ### The solution The solution is to unbind the article from the backend state, ensuring that the article editor is the source of truth and ignoring the responses. This pull request does this by adding an asynchronous save functionality. The changes include adding a new `saveArticleAsync` event and ensuring that the local state is not updated unnecessarily during asynchronous saves. ``` Time --> User Action: [Edit 1] ---> [Edit 2] ---> [Edit 3] Backend Save: Save Req (Edit 1) ----> Response (ignored) Resulting Editor State: [Edit 3] (Consistent and up-to-date) ``` Added the following two debounced methods These complementary debounce methods prevent unnecessary re-renders while ensuring backend is in sync. `saveArticleAsync` preserves the editor as the source of truth, while `saveArticle` manages periodic state updates from the backend with a delay large enough to safely assume that the user has stopped typing Method | Delay | Behavior -- | -- | -- `saveArticleAsync` | 400ms | Sends data to backend and ignores the response `saveArticle` | 2.5s | Sends data and updates local state with the backend response ### How to test 1. Remove the following line https://github.com/chatwoot/chatwoot/blob/dc042f6ddcb4e108795ff83bb57f8d92c6a2b90a/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditor.vue#L64 1. Update the latency here to 400 (P.S. the diff shows the latency to be 600, but that was added as a stop-gap solution) https://github.com/chatwoot/chatwoot/blob/dc042f6ddcb4e108795ff83bb57f8d92c6a2b90a/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditor.vue#L51 1. Set the browser network latency to Slow 3G or 3G 1. Start writing on the editor, try fixing typos with backspace or moving around with the cursor --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Pranav --- .../Pages/ArticleEditorPage/ArticleEditor.vue | 27 ++++++++++++++++--- .../pages/PortalsArticlesEditPage.vue | 14 ++++++---- .../modules/helpCenterArticles/actions.js | 19 +++++++++++++ 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditor.vue b/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditor.vue index f15135737..f5b925fbe 100644 --- a/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditor.vue +++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditor.vue @@ -27,6 +27,7 @@ const props = defineProps({ const emit = defineEmits([ 'saveArticle', + 'saveArticleAsync', 'goBack', 'setAuthor', 'setCategory', @@ -35,19 +36,37 @@ const emit = defineEmits([ const { t } = useI18n(); -const saveArticle = debounce(value => emit('saveArticle', value), 600, false); +const saveAndSync = value => { + emit('saveArticle', value); +}; + +// this will only send the data to the backend +// but will not update the local state preventing unnecessary re-renders +// since the data is already saved and we keep the editor text as the source of truth +const quickSave = debounce( + value => emit('saveArticleAsync', value), + 400, + false +); + +// 2.5 seconds is enough to know that the user has stopped typing and is taking a pause +// so we can save the data to the backend and retrieve the updated data +// this will update the local state with response data +const saveAndSyncDebounced = debounce(saveAndSync, 2500, false); const articleTitle = computed({ get: () => props.article.title, set: value => { - saveArticle({ title: value }); + quickSave({ title: value }); + saveAndSyncDebounced({ title: value }); }, }); const articleContent = computed({ get: () => props.article.content, set: content => { - saveArticle({ content }); + quickSave({ content }); + saveAndSyncDebounced({ content }); }, }); @@ -93,7 +112,7 @@ const previewArticle = () => { /> diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/PortalsArticlesEditPage.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/PortalsArticlesEditPage.vue index 736b34096..3bb868a58 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/PortalsArticlesEditPage.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/PortalsArticlesEditPage.vue @@ -34,10 +34,11 @@ const portalLink = computed(() => { ); }); -const saveArticle = async ({ ...values }) => { +const saveArticle = async ({ ...values }, isAsync = false) => { + const actionToDispatch = isAsync ? 'articles/updateAsync' : 'articles/update'; isUpdating.value = true; try { - await store.dispatch('articles/update', { + await store.dispatch(actionToDispatch, { portalSlug, articleId: articleSlug, ...values, @@ -55,6 +56,10 @@ const saveArticle = async ({ ...values }) => { } }; +const saveArticleAsync = async ({ ...values }) => { + saveArticle({ ...values }, true); +}; + const isCategoryArticles = computed(() => { return ( route.name === 'portals_categories_articles_index' || @@ -92,9 +97,7 @@ const previewArticle = () => { }); }; -onMounted(() => { - fetchArticleDetails(); -}); +onMounted(fetchArticleDetails);