From f6be0d80efbf373e166f7f5b4211016c659d7a78 Mon Sep 17 00:00:00 2001 From: Aakash Bakhle <48802744+aakashb95@users.noreply.github.com> Date: Mon, 11 May 2026 20:13:29 +0530 Subject: [PATCH] feat: UI changes for document auto sync [AI-153] (#14258) # Pull Request Template ## Description FE code for document sync Adds: - UI to show counts (stats) of available web pages, stale and synced documents and last synced at - Bulk action and manual ways to sync web documents - index to stats related columns ## Type of change Please delete options that are not relevant. - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. https://linear.app/chatwoot/issue/AI-153/fe-document-auto-sync Documents dashboard: CleanShot 2026-05-11 at 17 57 09@2x Filters: CleanShot 2026-05-11 at 17 58 13@2x Needs update: CleanShot 2026-05-11 at 17 57 53@2x pdfs: CleanShot 2026-05-11 at 17 58 30@2x bulk actions: CleanShot 2026-05-11 at 17 58 57@2x single url sync: CleanShot 2026-05-11 at 17 59 19@2x ## 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 - [x] 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 - [x] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: iamsivin Co-authored-by: Muhsin Keloth Co-authored-by: Sony Mathew Co-authored-by: Vishnu Narayanan --- .../dashboard/api/captain/document.js | 11 +- .../captain/assistant/DocumentBulkActions.vue | 113 +++++++ .../captain/assistant/DocumentCard.vue | 107 ++++++- .../captain/assistant/DocumentFilter.vue | 76 +++++ .../captain/assistant/DocumentFiltersBar.vue | 165 +++++++++++ .../captain/assistant/DocumentSyncStatus.vue | 153 ++++++++++ .../document/CreateDocumentDialog.vue | 3 +- app/javascript/dashboard/featureFlags.js | 1 + .../i18n/locale/en/integrations.json | 50 ++++ .../dashboard/captain/documents/Index.vue | 276 ++++++++++++++---- .../dashboard/store/captain/bulkActions.js | 13 + .../dashboard/store/captain/document.js | 40 +++ .../shared/helpers/documentHelper.js | 44 ++- .../helpers/specs/documentHelper.spec.js | 48 ++- ...d_sync_stats_index_to_captain_documents.rb | 12 + db/schema.rb | 3 +- .../captain/bulk_actions_controller.rb | 7 +- .../accounts/captain/documents_controller.rb | 67 ++++- .../captain/documents/perform_sync_job.rb | 26 +- .../captain/documents/schedule_syncs_job.rb | 11 +- enterprise/app/models/captain/document.rb | 11 + .../app/policies/captain/assistant_policy.rb | 4 + .../captain/documents/sync_service.rb | 12 +- .../captain/documents/index.json.jbuilder | 1 + .../v1/models/captain/_document.json.jbuilder | 2 + 25 files changed, 1155 insertions(+), 101 deletions(-) create mode 100644 app/javascript/dashboard/components-next/captain/assistant/DocumentBulkActions.vue create mode 100644 app/javascript/dashboard/components-next/captain/assistant/DocumentFilter.vue create mode 100644 app/javascript/dashboard/components-next/captain/assistant/DocumentFiltersBar.vue create mode 100644 app/javascript/dashboard/components-next/captain/assistant/DocumentSyncStatus.vue create mode 100644 db/migrate/20260429043000_add_sync_stats_index_to_captain_documents.rb diff --git a/app/javascript/dashboard/api/captain/document.js b/app/javascript/dashboard/api/captain/document.js index dc22b0c32..e23a8c460 100644 --- a/app/javascript/dashboard/api/captain/document.js +++ b/app/javascript/dashboard/api/captain/document.js @@ -6,15 +6,22 @@ class CaptainDocument extends ApiClient { super('captain/documents', { accountScoped: true }); } - get({ page = 1, searchKey, assistantId } = {}) { + get({ page = 1, searchKey, assistantId, filter, source, sort } = {}) { return axios.get(this.url, { params: { page, - searchKey, + search_key: searchKey, assistant_id: assistantId, + filter, + source, + sort, }, }); } + + sync(id) { + return axios.post(`${this.url}/${id}/sync`); + } } export default new CaptainDocument(); diff --git a/app/javascript/dashboard/components-next/captain/assistant/DocumentBulkActions.vue b/app/javascript/dashboard/components-next/captain/assistant/DocumentBulkActions.vue new file mode 100644 index 000000000..378860b3e --- /dev/null +++ b/app/javascript/dashboard/components-next/captain/assistant/DocumentBulkActions.vue @@ -0,0 +1,113 @@ + + + diff --git a/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue b/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue index 30ebfc448..c8a011b9a 100644 --- a/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue +++ b/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue @@ -5,14 +5,17 @@ import { useI18n } from 'vue-i18n'; import { dynamicTime } from 'shared/helpers/timeHelper'; import { usePolicy } from 'dashboard/composables/usePolicy'; import { - isPdfDocument, + isSafeHttpLink, formatDocumentLink, + getDocumentDisplayPath, } from 'shared/helpers/documentHelper'; +import Icon from 'dashboard/components-next/icon/Icon.vue'; import CardLayout from 'dashboard/components-next/CardLayout.vue'; import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue'; import Button from 'dashboard/components-next/button/Button.vue'; import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue'; +import DocumentSyncStatus from 'dashboard/components-next/captain/assistant/DocumentSyncStatus.vue'; const props = defineProps({ id: { @@ -31,10 +34,38 @@ const props = defineProps({ type: String, required: true, }, + pdfDocument: { + type: Boolean, + default: false, + }, createdAt: { type: Number, required: true, }, + status: { + type: String, + default: null, + }, + syncStatus: { + type: String, + default: null, + }, + lastSyncedAt: { + type: Number, + default: null, + }, + lastSyncErrorCode: { + type: String, + default: null, + }, + syncInProgress: { + type: Boolean, + default: false, + }, + syncStaleAfterHours: { + type: Number, + default: null, + }, isSelected: { type: Boolean, default: false, @@ -64,6 +95,20 @@ const modelValue = computed({ set: () => emit('select', props.id), }); +const isPdf = computed(() => props.pdfDocument); +const hasSafeLink = computed(() => isSafeHttpLink(props.externalLink)); +const canManage = computed(() => checkPermissions(['administrator'])); +const isAvailable = computed(() => props.status === 'available'); +const canSync = computed( + () => canManage.value && !isPdf.value && isAvailable.value +); +const isSyncing = computed(() => props.syncStatus === 'syncing'); +const isFailed = computed(() => props.syncStatus === 'failed'); +const isRetryableSync = computed( + () => isFailed.value || (isSyncing.value && !props.syncInProgress) +); +const showSyncStatus = computed(() => !isPdf.value); + const menuItems = computed(() => { const allOptions = [ { @@ -74,7 +119,19 @@ const menuItems = computed(() => { }, ]; - if (checkPermissions(['administrator'])) { + if (canSync.value) { + allOptions.push({ + label: isRetryableSync.value + ? t('CAPTAIN.DOCUMENTS.OPTIONS.RETRY_SYNC') + : t('CAPTAIN.DOCUMENTS.OPTIONS.SYNC_NOW'), + value: 'sync', + action: 'sync', + icon: 'i-lucide-refresh-cw', + disabled: props.syncInProgress, + }); + } + + if (canManage.value) { allOptions.push({ label: t('CAPTAIN.DOCUMENTS.OPTIONS.DELETE_DOCUMENT'), value: 'delete', @@ -86,17 +143,25 @@ const menuItems = computed(() => { return allOptions; }); -const createdAt = computed(() => dynamicTime(props.createdAt)); +const createdAtLabel = computed(() => dynamicTime(props.createdAt)); -const displayLink = computed(() => formatDocumentLink(props.externalLink)); +const displayLink = computed(() => + isPdf.value + ? formatDocumentLink(props.externalLink) + : getDocumentDisplayPath(props.externalLink) +); const linkIcon = computed(() => - isPdfDocument(props.externalLink) ? 'i-ph-file-pdf' : 'i-ph-link-simple' + isPdf.value ? 'i-ph-file-pdf' : 'i-ph-link-simple' ); const handleAction = ({ action, value }) => { toggleDropdown(false); emit('action', { action, value, id: props.id }); }; + +const handleRetry = () => { + emit('action', { action: 'sync', id: props.id }); +};