Compare commits

...
Author SHA1 Message Date
Shivam MishraandGitHub 6928108b7a Merge branch 'develop' into fix/hc-editor 2025-10-13 12:30:50 +05:30
Muhsin KelothandGitHub e7e3ed3916 Merge branch 'develop' into fix/hc-editor 2025-09-22 16:38:21 +05:30
Shivam MishraandGitHub b4d589fe24 Merge branch 'develop' into fix/hc-editor 2025-08-14 17:15:05 +05:30
Muhsin KelothandGitHub 370f890f1f Merge branch 'develop' into fix/hc-editor 2025-08-14 14:33:53 +05:30
Muhsin KelothandGitHub a77ec8f5b5 Merge branch 'develop' into fix/hc-editor 2025-08-14 13:56:49 +05:30
Shivam MishraandGitHub 450736c4fe Merge branch 'develop' into fix/hc-editor 2025-02-10 20:02:49 +05:30
Shivam MishraandGitHub cb075f794d Merge branch 'develop' into fix/hc-editor 2025-01-31 13:53:53 +05:30
Shivam Mishra 4cc977b9ee fix: focus 2025-01-30 14:22:12 +05:30
Shivam Mishra 6222a8d103 feat: remove cursor manipulation
not needed anymore
2025-01-30 14:20:45 +05:30
Shivam MishraandGitHub 27d9142edf Merge branch 'develop' into fix/hc-editor 2025-01-30 14:20:31 +05:30
Shivam MishraandGitHub cb9e5806df Merge branch 'develop' into fix/hc-editor 2025-01-29 16:39:33 +05:30
Shivam Mishra af9762f04e feat: it's okay to just start with a title 2025-01-29 16:37:29 +05:30
Shivam Mishra ce56e3ed93 feat: update content one last time on blur 2025-01-29 16:37:16 +05:30
Shivam Mishra c50d00b48e feat: update title only on blur 2025-01-29 16:37:04 +05:30
Shivam Mishra 2655a8c6b4 feat: remove async saving 2025-01-29 15:59:59 +05:30
Shivam Mishra 346d7a0375 fix: model reload only once 2025-01-29 15:58:50 +05:30
Shivam Mishra e0449451f9 fix: always save 2025-01-29 15:00:25 +05:30
6 changed files with 29 additions and 70 deletions
@@ -1,5 +1,5 @@
<script setup> <script setup>
import { computed } from 'vue'; import { computed, ref, watch, onMounted } from 'vue';
import { debounce } from '@chatwoot/utils'; import { debounce } from '@chatwoot/utils';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { ARTICLE_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor'; import { ARTICLE_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor';
@@ -27,7 +27,6 @@ const props = defineProps({
const emit = defineEmits([ const emit = defineEmits([
'saveArticle', 'saveArticle',
'saveArticleAsync',
'goBack', 'goBack',
'setAuthor', 'setAuthor',
'setCategory', 'setCategory',
@@ -35,52 +34,32 @@ const emit = defineEmits([
]); ]);
const { t } = useI18n(); const { t } = useI18n();
const articleTitle = ref('');
const isNewArticle = computed(() => !props.article?.id); const saveArticle = value => {
const saveAndSync = value => {
emit('saveArticle', value); emit('saveArticle', value);
}; };
// this will only send the data to the backend const saveContent = debounce(content => saveArticle({ content }), 200, false);
// but will not update the local state preventing unnecessary re-renders const saveTitle = () => {
// since the data is already saved and we keep the editor text as the source of truth if (articleTitle.value) saveArticle({ title: articleTitle.value });
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
// Only use to save for existing articles
const saveAndSyncDebounced = debounce(saveAndSync, 2500, false);
// Debounced save for new articles
const quickSaveNewArticle = debounce(saveAndSync, 400, false);
const handleSave = value => {
if (isNewArticle.value) {
quickSaveNewArticle(value);
} else {
quickSave(value);
saveAndSyncDebounced(value);
}
}; };
const articleTitle = computed({ watch(
get: () => props.article.title, () => props.article,
set: value => { () => {
handleSave({ title: value }); articleTitle.value = props.article.title;
}, },
{ immediate: true, deep: true }
);
onMounted(() => {
articleTitle.value = props.article.title;
}); });
const articleContent = computed({ const articleContent = computed({
get: () => props.article.content, get: () => props.article.content,
set: content => { set: saveContent,
handleSave({ content });
},
}); });
const onClickGoBack = () => { const onClickGoBack = () => {
@@ -122,10 +101,11 @@ const previewArticle = () => {
custom-text-area-wrapper-class="border-0 !bg-transparent dark:!bg-transparent !py-0 !px-0" custom-text-area-wrapper-class="border-0 !bg-transparent dark:!bg-transparent !py-0 !px-0"
placeholder="Title" placeholder="Title"
autofocus autofocus
@blur="saveTitle"
/> />
<ArticleEditorControls <ArticleEditorControls
:article="article" :article="article"
@save-article="saveAndSync" @save-article="saveArticle"
@set-author="setAuthorId" @set-author="setAuthorId"
@set-category="setCategoryId" @set-category="setCategoryId"
/> />
@@ -138,6 +118,7 @@ const previewArticle = () => {
" "
:enabled-menu-options="ARTICLE_EDITOR_MENU_OPTIONS" :enabled-menu-options="ARTICLE_EDITOR_MENU_OPTIONS"
:autofocus="false" :autofocus="false"
@blur="saveContent"
/> />
</template> </template>
</HelpCenterLayout> </HelpCenterLayout>
@@ -32,7 +32,7 @@ const props = defineProps({
allowSignature: { type: Boolean, default: false }, // allowSignature is a kill switch, ensuring no signature methods are triggered except when this flag is true allowSignature: { type: Boolean, default: false }, // allowSignature is a kill switch, ensuring no signature methods are triggered except when this flag is true
}); });
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue', 'blur']);
const textareaRef = ref(null); const textareaRef = ref(null);
const isFocused = ref(false); const isFocused = ref(false);
@@ -106,6 +106,7 @@ const handleBlur = () => {
if (!props.disabled) { if (!props.disabled) {
isFocused.value = false; isFocused.value = false;
} }
emit('blur');
}; };
// Watch for changes in modelValue to adjust height // Watch for changes in modelValue to adjust height
@@ -67,10 +67,11 @@ export default {
}; };
}, },
watch: { watch: {
modelValue(newValue = '') { modelValue: {
if (newValue !== this.contentFromEditor()) { handler() {
this.reloadState(); this.reloadState();
} },
once: true,
}, },
editorId() { editorId() {
this.reloadState(); this.reloadState();
@@ -176,6 +177,7 @@ export default {
{ onImageUpload: this.openFileBrowser }, { onImageUpload: this.openFileBrowser },
this.enabledMenuOptions this.enabledMenuOptions
); );
editorView.updateState(state); editorView.updateState(state);
this.focusEditorInputField(); this.focusEditorInputField();
}, },
@@ -229,6 +231,7 @@ export default {
this.$emit('keydown'); this.$emit('keydown');
}, },
onBlur() { onBlur() {
this.$emit('update:modelValue', this.contentFromEditor());
this.$emit('blur'); this.$emit('blur');
}, },
onFocus() { onFocus() {
@@ -40,11 +40,10 @@ const articleLink = computed(() => {
); );
}); });
const saveArticle = async ({ ...values }, isAsync = false) => { const saveArticle = async ({ ...values }) => {
const actionToDispatch = isAsync ? 'articles/updateAsync' : 'articles/update';
isUpdating.value = true; isUpdating.value = true;
try { try {
await store.dispatch(actionToDispatch, { await store.dispatch('articles/update', {
portalSlug, portalSlug,
articleId: articleSlug, articleId: articleSlug,
...values, ...values,
@@ -62,10 +61,6 @@ const saveArticle = async ({ ...values }, isAsync = false) => {
} }
}; };
const saveArticleAsync = async ({ ...values }) => {
saveArticle({ ...values }, true);
};
const isCategoryArticles = computed(() => { const isCategoryArticles = computed(() => {
return ( return (
route.name === 'portals_categories_articles_index' || route.name === 'portals_categories_articles_index' ||
@@ -112,7 +107,6 @@ onMounted(fetchArticleDetails);
:is-updating="isUpdating" :is-updating="isUpdating"
:is-saved="isSaved" :is-saved="isSaved"
@save-article="saveArticle" @save-article="saveArticle"
@save-article-async="saveArticleAsync"
@preview-article="previewArticle" @preview-article="previewArticle"
@go-back="goBackToArticles" @go-back="goBackToArticles"
/> />
@@ -69,25 +69,6 @@ export const actions = {
} }
}, },
updateAsync: async ({ commit }, { portalSlug, articleId, ...articleObj }) => {
commit(types.UPDATE_ARTICLE_FLAG, {
uiFlags: { isUpdating: true },
articleId,
});
try {
await articlesAPI.updateArticle({ portalSlug, articleId, articleObj });
return articleId;
} catch (error) {
return throwErrorMessage(error);
} finally {
commit(types.UPDATE_ARTICLE_FLAG, {
uiFlags: { isUpdating: false },
articleId,
});
}
},
update: async ({ commit }, { portalSlug, articleId, ...articleObj }) => { update: async ({ commit }, { portalSlug, articleId, ...articleObj }) => {
commit(types.UPDATE_ARTICLE_FLAG, { commit(types.UPDATE_ARTICLE_FLAG, {
uiFlags: { uiFlags: {
-1
View File
@@ -58,7 +58,6 @@ class Article < ApplicationRecord
validates :account_id, presence: true validates :account_id, presence: true
validates :author_id, presence: true validates :author_id, presence: true
validates :title, presence: true validates :title, presence: true
validates :content, presence: true
# ensuring that the position is always set correctly # ensuring that the position is always set correctly
before_create :add_position_to_article before_create :add_position_to_article