From 2309424cb19d5b1f9d95f11f29109ddeb2537914 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Thu, 21 Nov 2024 10:48:25 +0530 Subject: [PATCH 01/33] feat: Add Contact card and form component (#10466) Co-authored-by: Pranav --- .../Contacts/ContactsCard/ContactsCard.vue | 102 ++++++ .../ContactsCard/story/ContactsCard.story.vue | 67 ++++ .../Contacts/ContactsCard/story/fixtures.js | 149 +++++++++ .../Contacts/ContactsForm/ContactsForm.vue | 297 ++++++++++++++++++ .../ContactsForm/story/ContactsForm.story.vue | 80 +++++ .../Contacts/ContactsForm/story/fixtures.js | 20 ++ .../dashboard/i18n/locale/en/contact.json | 56 ++++ 7 files changed, 771 insertions(+) create mode 100644 app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactsCard.vue create mode 100644 app/javascript/dashboard/components-next/Contacts/ContactsCard/story/ContactsCard.story.vue create mode 100644 app/javascript/dashboard/components-next/Contacts/ContactsCard/story/fixtures.js create mode 100644 app/javascript/dashboard/components-next/Contacts/ContactsForm/ContactsForm.vue create mode 100644 app/javascript/dashboard/components-next/Contacts/ContactsForm/story/ContactsForm.story.vue create mode 100644 app/javascript/dashboard/components-next/Contacts/ContactsForm/story/fixtures.js diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactsCard.vue b/app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactsCard.vue new file mode 100644 index 000000000..4ba3546ce --- /dev/null +++ b/app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactsCard.vue @@ -0,0 +1,102 @@ + + + diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsCard/story/ContactsCard.story.vue b/app/javascript/dashboard/components-next/Contacts/ContactsCard/story/ContactsCard.story.vue new file mode 100644 index 000000000..3f8d5a411 --- /dev/null +++ b/app/javascript/dashboard/components-next/Contacts/ContactsCard/story/ContactsCard.story.vue @@ -0,0 +1,67 @@ + + + diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsCard/story/fixtures.js b/app/javascript/dashboard/components-next/Contacts/ContactsCard/story/fixtures.js new file mode 100644 index 000000000..67bdba9b4 --- /dev/null +++ b/app/javascript/dashboard/components-next/Contacts/ContactsCard/story/fixtures.js @@ -0,0 +1,149 @@ +export default [ + { + additionalAttributes: { + socialProfiles: {}, + }, + availabilityStatus: null, + email: 'johndoe@chatwoot.com', + id: 370, + name: 'John Doe', + phoneNumber: '+918634322418', + identifier: null, + thumbnail: 'https://api.dicebear.com/9.x/thumbs/svg?seed=Felix', + customAttributes: {}, + lastActivityAt: 1731608270, + createdAt: 1731586271, + }, + { + additionalAttributes: { + city: 'kerala', + country: 'India', + description: 'Curious about the web. ', + companyName: 'Chatwoot', + countryCode: '', + socialProfiles: { + github: 'abozler', + twitter: 'ozler', + facebook: 'abozler', + linkedin: 'abozler', + instagram: 'ozler', + }, + }, + availabilityStatus: null, + email: 'ozler@chatwoot.com', + id: 29, + name: 'Abraham Ozlers', + phoneNumber: '+246232222222', + identifier: null, + thumbnail: 'https://api.dicebear.com/9.x/thumbs/svg?seed=Upload', + customAttributes: { + dateContact: '2024-02-01T00:00:00.000Z', + linkContact: 'https://staging.chatwoot.com/app/accounts/3/contacts-new', + listContact: 'Not spam', + numberContact: '12', + }, + lastActivityAt: 1712127410, + createdAt: 1712127389, + }, + { + additionalAttributes: { + city: 'Kerala', + country: 'India', + description: + "I'm Candice developer focusing on building things for the web 🌍. Currently, I’m working as a Product Developer here at @chatwootapp ⚡️🔥", + companyName: 'Chatwoot', + countryCode: 'IN', + socialProfiles: { + github: 'cmathersonj', + twitter: 'cmather', + facebook: 'cmathersonj', + linkedin: 'cmathersonj', + instagram: 'cmathersonjs', + }, + }, + availabilityStatus: null, + email: 'cmathersonj@va.test', + id: 22, + name: 'Candice Matherson', + phoneNumber: '+917474774742', + identifier: null, + thumbnail: 'https://api.dicebear.com/9.x/thumbs/svg?seed=Emery', + customAttributes: { + dateContact: '2024-11-12T03:23:06.963Z', + linkContact: 'https://sd.sd', + textContact: 'hey', + numberContact: '12', + checkboxContact: true, + }, + lastActivityAt: 1712123233, + createdAt: 1712123233, + }, + { + additionalAttributes: { + city: '', + country: '', + description: '', + companyName: '', + countryCode: '', + socialProfiles: { + github: '', + twitter: '', + facebook: '', + linkedin: '', + instagram: '', + }, + }, + availabilityStatus: null, + email: 'ofolkardi@taobao.test', + id: 21, + name: 'Ophelia Folkard', + phoneNumber: '', + identifier: null, + thumbnail: + 'https://sivin-tunnel.chatwoot.dev/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBPZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--08dcac8eb72ef12b2cad92d58dddd04cd8a5f513/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RTNKbGMybDZaVjkwYjE5bWFXeHNXd2RwQWZvdyIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--df796c2af3c0153e55236c2f3cf3a199ac2cb6f7/32.jpg', + customAttributes: {}, + lastActivityAt: 1712123233, + createdAt: 1712123233, + }, + { + additionalAttributes: { + socialProfiles: {}, + }, + availabilityStatus: null, + email: 'wcasteloth@exblog.jp', + id: 20, + name: 'Willy Castelot', + phoneNumber: '+919384', + identifier: null, + thumbnail: 'https://api.dicebear.com/9.x/thumbs/svg?seed=Jade', + customAttributes: {}, + lastActivityAt: 1712123233, + createdAt: 1712123233, + }, + { + additionalAttributes: { + city: '', + country: '', + description: '', + companyName: '', + countryCode: '', + socialProfiles: { + github: '', + twitter: '', + facebook: '', + linkedin: '', + instagram: '', + }, + }, + availabilityStatus: null, + email: 'ederingtong@printfriendly.test', + id: 19, + name: 'Elisabeth Derington', + phoneNumber: '', + identifier: null, + thumbnail: 'https://api.dicebear.com/9.x/avataaars/svg?seed=Jade', + customAttributes: {}, + lastActivityAt: 1712123232, + createdAt: 1712123232, + }, +]; diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsForm/ContactsForm.vue b/app/javascript/dashboard/components-next/Contacts/ContactsForm/ContactsForm.vue new file mode 100644 index 000000000..866811ae2 --- /dev/null +++ b/app/javascript/dashboard/components-next/Contacts/ContactsForm/ContactsForm.vue @@ -0,0 +1,297 @@ + + + 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 new file mode 100644 index 000000000..4be69380c --- /dev/null +++ b/app/javascript/dashboard/components-next/Contacts/ContactsForm/story/ContactsForm.story.vue @@ -0,0 +1,80 @@ + + + diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsForm/story/fixtures.js b/app/javascript/dashboard/components-next/Contacts/ContactsForm/story/fixtures.js new file mode 100644 index 000000000..30eb6ee22 --- /dev/null +++ b/app/javascript/dashboard/components-next/Contacts/ContactsForm/story/fixtures.js @@ -0,0 +1,20 @@ +export default { + id: 370, + name: 'John Doe', + email: 'johndoe@chatwoot.com', + phoneNumber: '+918634322418', + additionalAttributes: { + city: 'Kerala', + country: 'India', + description: 'Curious about the web.', + companyName: 'Chatwoot', + countryCode: 'IN', + socialProfiles: { + github: 'johndoe', + twitter: 'johndoe', + facebook: 'johndoe', + linkedin: 'johndoe', + instagram: 'johndoe', + }, + }, +}; diff --git a/app/javascript/dashboard/i18n/locale/en/contact.json b/app/javascript/dashboard/i18n/locale/en/contact.json index 54a06bb53..e0483d224 100644 --- a/app/javascript/dashboard/i18n/locale/en/contact.json +++ b/app/javascript/dashboard/i18n/locale/en/contact.json @@ -385,5 +385,61 @@ "DROPDOWN_ITEM": { "ID": "(ID: {identifier})" } + }, + + "CONTACTS_LAYOUT": { + "CARD": { + "OF": "of", + "VIEW_DETAILS": "View details", + "EDIT_DETAILS_FORM": { + "TITLE": "Edit contact details", + "FORM": { + "FIRST_NAME": { + "PLACEHOLDER": "Enter the first name" + }, + "LAST_NAME": { + "PLACEHOLDER": "Enter the last name" + }, + "EMAIL_ADDRESS": { + "PLACEHOLDER": "Enter the email address" + }, + "PHONE_NUMBER": { + "PLACEHOLDER": "Enter the phone number" + }, + "CITY": { + "PLACEHOLDER": "Enter the city name" + }, + "COUNTRY": { + "PLACEHOLDER": "Select country" + }, + "BIO": { + "PLACEHOLDER": "Enter the bio" + }, + "COMPANY_NAME": { + "PLACEHOLDER": "Enter the company name" + } + } + }, + "SOCIAL_MEDIA": { + "TITLE": "Edit social links", + "FORM": { + "FACEBOOK": { + "PLACEHOLDER": "Add Facebook" + }, + "GITHUB": { + "PLACEHOLDER": "Add Github" + }, + "INSTAGRAM": { + "PLACEHOLDER": "Add Instagram" + }, + "LINKEDIN": { + "PLACEHOLDER": "Add LinkedIn" + }, + "TWITTER": { + "PLACEHOLDER": "Add Twitter" + } + } + } + } } } From 7b6195f28b3ca453825c6c2064de69c09d7c3715 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Thu, 21 Nov 2024 16:25:13 +0530 Subject: [PATCH 02/33] fix: Component `` reactivity issue. (#10476) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Pull Request Template ## Description This PR will fix reactivity issue with `` component. **Cause of issue** The `` component used an internal ref, `internalActiveIndex` to track the `active` tab. However, it didn’t sync with the `index` prop when updated by the parent, causing mismatched tab selections. **Solution** The component now directly uses `props.index` to ensure it always reflects the latest value from the parent. The unnecessary `internalActiveIndex` ref has been removed. Changes to the active tab emit a `change` event to update the parent. ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? **Loom video** **Before** https://www.loom.com/share/76eb32f1e7f7422f84055a102bf80951?sid=bc28c6ff-9640-4d3b-956c-99c1ec164971 **After** https://www.loom.com/share/6bd8125ede5d43dc8fe115c3f1fb159b?sid=c376617a-94fb-4f71-8664-e0bd9e7af0b4 ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --- app/javascript/dashboard/components/ui/Tabs/Tabs.vue | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/app/javascript/dashboard/components/ui/Tabs/Tabs.vue b/app/javascript/dashboard/components/ui/Tabs/Tabs.vue index 682957204..3a7344fb4 100644 --- a/app/javascript/dashboard/components/ui/Tabs/Tabs.vue +++ b/app/javascript/dashboard/components/ui/Tabs/Tabs.vue @@ -1,5 +1,4 @@ + + 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 04/33] 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 07/33] 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);