From 846b9fb9ce0f47bc1c3b9e9a9a28f19ba8407d4c Mon Sep 17 00:00:00 2001 From: Gabriel Jablonski Date: Thu, 13 Feb 2025 08:57:29 -0300 Subject: [PATCH 001/222] chore(fazer-ai): repo setup (#1) * chore: update nvmrc * chore: bump rubocop and related gems --- .nvmrc | 2 +- Gemfile.lock | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.nvmrc b/.nvmrc index 6f7af3750..b88575e38 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.5.1 \ No newline at end of file +23.7.0 \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 9f1ee5eec..ebaa749dd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -423,6 +423,7 @@ GEM faraday-multipart json (>= 1.8) rexml + language_server-protocol (3.17.0.4) launchy (2.5.2) addressable (~> 2.8) letter_opener (1.8.1) @@ -545,9 +546,10 @@ GEM orm_adapter (0.5.0) os (1.1.4) ostruct (0.6.1) - parallel (1.23.0) - parser (3.2.2.1) + parallel (1.26.3) + parser (3.3.7.1) ast (~> 2.4.1) + racc pg (1.5.3) pg_search (2.3.6) activerecord (>= 5.2) @@ -663,14 +665,15 @@ GEM rspec-support (3.13.1) rspec_junit_formatter (0.6.0) rspec-core (>= 2, < 4, != 2.12.0) - rubocop (1.50.2) + rubocop (1.57.2) json (~> 2.3) + language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.2.0.0) + parser (>= 3.2.2.4) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.28.0, < 2.0) + rubocop-ast (>= 1.28.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) rubocop-ast (1.28.1) @@ -816,7 +819,7 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.8.2) - unicode-display_width (2.4.2) + unicode-display_width (2.6.0) uniform_notifier (1.16.0) uri (1.0.3) uri_template (0.7.0) From f6bdc40c243a8fbe0f3fcbfa29ed64f7d323827c Mon Sep 17 00:00:00 2001 From: "Cayo P. R. Oliveira" Date: Fri, 14 Feb 2025 11:43:15 -0300 Subject: [PATCH 002/222] feat: add name to webhook (#4) * feat(migration): add name column to webhook table * feat(webhooks): add name parameter to webhook params * feat(webhooks): add example webhook name constant and input field to form * fix(webhooks): add webhook name label and placeholder to multiple locales in the form * feat(webhooks): display webhook name in the UI and include it in the API response * Revert 'fix(webhooks): add webhook name label and placeholder to multiple locales in the form' This reverts commit e547778a1c038c934e22ceb25935f541cb09e2cd. * test(webhooks): add tests for creating and updating webhooks with name attribute * chore(webhooks): add name property to webhook definitions in Swagger documentation * chore(webhooks): remove unnecessary input touch event for webhook name field * chore(webhooks): apply review changes requested * chore(webhooks): revert auto lint changes in commit 18ec4cafeb72fd385b70f65f1873d7cfb65216a6 --- .../api/v1/accounts/webhooks_controller.rb | 2 +- .../dashboard/i18n/locale/en/integrations.json | 4 ++++ .../integrations/Webhooks/WebhookForm.vue | 14 ++++++++++++++ .../settings/integrations/Webhooks/WebhookRow.vue | 14 ++++++++++++-- app/models/webhook.rb | 1 + .../v1/accounts/webhooks/_webhook.json.jbuilder | 1 + db/migrate/20250213190934_add_name_to_webhooks.rb | 5 +++++ db/schema.rb | 1 + .../api/v1/accounts/webhook_controller_spec.rb | 15 +++++++++++++-- spec/factories/webhooks.rb | 1 + .../request/webhooks/create_update_payload.yml | 3 +++ swagger/definitions/resource/webhook.yml | 3 +++ swagger/swagger.json | 8 ++++++++ 13 files changed, 67 insertions(+), 5 deletions(-) create mode 100644 db/migrate/20250213190934_add_name_to_webhooks.rb diff --git a/app/controllers/api/v1/accounts/webhooks_controller.rb b/app/controllers/api/v1/accounts/webhooks_controller.rb index 7ea257ed2..9f8e94821 100644 --- a/app/controllers/api/v1/accounts/webhooks_controller.rb +++ b/app/controllers/api/v1/accounts/webhooks_controller.rb @@ -23,7 +23,7 @@ class Api::V1::Accounts::WebhooksController < Api::V1::Accounts::BaseController private def webhook_params - params.require(:webhook).permit(:inbox_id, :url, subscriptions: []) + params.require(:webhook).permit(:inbox_id, :name, :url, subscriptions: []) end def fetch_webhook diff --git a/app/javascript/dashboard/i18n/locale/en/integrations.json b/app/javascript/dashboard/i18n/locale/en/integrations.json index 5105513d5..95cd0c9f5 100644 --- a/app/javascript/dashboard/i18n/locale/en/integrations.json +++ b/app/javascript/dashboard/i18n/locale/en/integrations.json @@ -44,6 +44,10 @@ "CONTACT_UPDATED": "Contact updated" } }, + "NAME": { + "LABEL": "Webhook Name", + "PLACEHOLDER": "Enter the name of the webhook" + }, "END_POINT": { "LABEL": "Webhook URL", "PLACEHOLDER": "Example: {webhookExampleURL}", diff --git a/app/javascript/dashboard/routes/dashboard/settings/integrations/Webhooks/WebhookForm.vue b/app/javascript/dashboard/routes/dashboard/settings/integrations/Webhooks/WebhookForm.vue index f3c724bb2..6368bff19 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/integrations/Webhooks/WebhookForm.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/integrations/Webhooks/WebhookForm.vue @@ -53,6 +53,7 @@ export default { data() { return { url: this.value.url || '', + name: this.value.name || '', subscriptions: this.value.subscriptions || [], supportedWebhookEvents: SUPPORTED_WEBHOOK_EVENTS, }; @@ -66,11 +67,15 @@ export default { } ); }, + webhookNameInputPlaceholder() { + return this.$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.NAME.PLACEHOLDER'); + }, }, methods: { onSubmit() { this.$emit('submit', { url: this.url, + name: this.name, subscriptions: this.subscriptions, }); }, @@ -95,6 +100,15 @@ export default { {{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.END_POINT.ERROR') }} + diff --git a/app/javascript/dashboard/routes/dashboard/settings/integrations/Webhooks/WebhookRow.vue b/app/javascript/dashboard/routes/dashboard/settings/integrations/Webhooks/WebhookRow.vue index 1ff6244ba..1a39694b3 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/integrations/Webhooks/WebhookRow.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/integrations/Webhooks/WebhookRow.vue @@ -37,8 +37,18 @@ const subscribedEvents = computed(() => { diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/components/WhatsappBaileysLinkDeviceModal.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/WhatsappBaileysLinkDeviceModal.vue new file mode 100644 index 000000000..bf12a8982 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/WhatsappBaileysLinkDeviceModal.vue @@ -0,0 +1,165 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/ConfigurationPage.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/ConfigurationPage.vue index bc5355c3c..792a15299 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/ConfigurationPage.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/ConfigurationPage.vue @@ -5,8 +5,11 @@ import SettingsSection from '../../../../../components/SettingsSection.vue'; import ImapSettings from '../ImapSettings.vue'; import SmtpSettings from '../SmtpSettings.vue'; import { useVuelidate } from '@vuelidate/core'; -import { required } from '@vuelidate/validators'; import NextButton from 'dashboard/components-next/button/Button.vue'; +import { requiredIf } from '@vuelidate/validators'; +import { isValidURL } from '../../../../../helper/URLHelper'; +import WhatsappBaileysLinkDeviceModal from '../components/WhatsappBaileysLinkDeviceModal.vue'; +import InboxName from '../../../../../components/widgets/InboxName.vue'; export default { components: { @@ -14,6 +17,8 @@ export default { ImapSettings, SmtpSettings, NextButton, + WhatsappBaileysLinkDeviceModal, + InboxName, }, mixins: [inboxMixin], props: { @@ -29,10 +34,17 @@ export default { return { hmacMandatory: false, whatsAppInboxAPIKey: '', + whatsAppProviderUrl: '', + showBaileysLinkDeviceModal: false, }; }, - validations: { - whatsAppInboxAPIKey: { required }, + validations() { + return { + whatsAppInboxAPIKey: { + requiredIf: requiredIf(!this.isAWhatsAppBaileysChannel), + }, + whatsAppProviderUrl: { isValidURL: value => !value || isValidURL(value) }, + }; }, watch: { inbox() { @@ -83,6 +95,31 @@ export default { useAlert(this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE')); } }, + async updateWhatsAppProviderUrl() { + try { + const payload = { + id: this.inbox.id, + formData: false, + channel: { + provider_config: { + ...this.inbox.provider_config, + provider_url: this.whatsAppProviderUrl, + }, + }, + }; + + await this.$store.dispatch('inboxes/updateInbox', payload); + useAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE')); + } catch (error) { + useAlert(this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE')); + } + }, + onOpenBaileysLinkDeviceModal() { + this.showBaileysLinkDeviceModal = true; + }, + onCloseBaileysLinkDeviceModal() { + this.showBaileysLinkDeviceModal = false; + }, }, }; @@ -194,8 +231,8 @@ export default { -
-
+
+
+
+ +
+ +
+ + + {{ + $t( + 'INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_MANAGE_PROVIDER_CONNECTION_BUTTON' + ) + }} + +
+
+ +
+ + + {{ $t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_UPDATE_BUTTON') }} + +
+ + {{ $t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_PROVIDER_URL_ERROR') }} + +
+ + +
+ + + {{ $t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_UPDATE_BUTTON') }} + +
+
+
+
diff --git a/app/javascript/dashboard/components-next/combobox/ComboBox.vue b/app/javascript/dashboard/components-next/combobox/ComboBox.vue index 76296ddde..d50134e88 100644 --- a/app/javascript/dashboard/components-next/combobox/ComboBox.vue +++ b/app/javascript/dashboard/components-next/combobox/ComboBox.vue @@ -96,6 +96,7 @@ watch( :label="selectedLabel" trailing-icon :disabled="disabled" + no-animation class="justify-between w-full !px-3 !py-2.5 text-n-slate-12 font-normal group-hover/combobox:border-n-slate-6 focus:outline-n-brand" :class="{ focused: open, diff --git a/app/javascript/dashboard/components-next/copilot/CopilotEmptyState.vue b/app/javascript/dashboard/components-next/copilot/CopilotEmptyState.vue index 39a7eb35a..954dd60a5 100644 --- a/app/javascript/dashboard/components-next/copilot/CopilotEmptyState.vue +++ b/app/javascript/dashboard/components-next/copilot/CopilotEmptyState.vue @@ -78,8 +78,10 @@ const handleSuggestion = opt => {

diff --git a/app/javascript/dashboard/components-next/copilot/CopilotLauncher.vue b/app/javascript/dashboard/components-next/copilot/CopilotLauncher.vue index 9b7ac983f..a2ecd9726 100644 --- a/app/javascript/dashboard/components-next/copilot/CopilotLauncher.vue +++ b/app/javascript/dashboard/components-next/copilot/CopilotLauncher.vue @@ -2,6 +2,7 @@ import { computed } from 'vue'; import { useRoute } from 'vue-router'; import Button from 'dashboard/components-next/button/Button.vue'; +import ButtonGroup from 'dashboard/components-next/buttonGroup/ButtonGroup.vue'; import { useUISettings } from 'dashboard/composables/useUISettings'; import { useMapGetter } from 'dashboard/composables/store'; import { FEATURE_FLAGS } from 'dashboard/featureFlags'; @@ -53,14 +54,17 @@ const toggleSidebar = () => { v-if="showCopilotLauncher" class="fixed bottom-4 ltr:right-4 rtl:left-4 z-50" > -
+
+
diff --git a/app/javascript/dashboard/components-next/dropdown-menu/DropdownMenu.vue b/app/javascript/dashboard/components-next/dropdown-menu/DropdownMenu.vue index 111b9868e..6276dfec6 100644 --- a/app/javascript/dashboard/components-next/dropdown-menu/DropdownMenu.vue +++ b/app/javascript/dashboard/components-next/dropdown-menu/DropdownMenu.vue @@ -8,11 +8,15 @@ import Avatar from 'dashboard/components-next/avatar/Avatar.vue'; const props = defineProps({ menuItems: { type: Array, - required: true, + default: () => [], validator: value => { return value.every(item => item.action && item.value && item.label); }, }, + menuSections: { + type: Array, + default: () => [], + }, thumbnailSize: { type: Number, default: 20, @@ -42,19 +46,62 @@ const { t } = useI18n(); const searchInput = ref(null); const searchQuery = ref(''); -const filteredMenuItems = computed(() => { - if (!searchQuery.value) return props.menuItems; +const hasSections = computed(() => props.menuSections.length > 0); - return props.menuItems.filter(item => +const flattenedMenuItems = computed(() => { + if (!hasSections.value) { + return props.menuItems; + } + + return props.menuSections.flatMap(section => section.items || []); +}); + +const filteredMenuItems = computed(() => { + if (!searchQuery.value) return flattenedMenuItems.value; + + return flattenedMenuItems.value.filter(item => item.label.toLowerCase().includes(searchQuery.value.toLowerCase()) ); }); +const filteredMenuSections = computed(() => { + if (!hasSections.value) { + return []; + } + + if (!searchQuery.value) { + return props.menuSections; + } + + const query = searchQuery.value.toLowerCase(); + + return props.menuSections + .map(section => { + const filteredItems = (section.items || []).filter(item => + item.label.toLowerCase().includes(query) + ); + + return { + ...section, + items: filteredItems, + }; + }) + .filter(section => section.items.length > 0); +}); + const handleAction = item => { const { action, value, ...rest } = item; emit('action', { action, value, ...rest }); }; +const shouldShowEmptyState = computed(() => { + if (hasSections.value) { + return filteredMenuSections.value.length === 0; + } + + return filteredMenuItems.value.length === 0; +}); + onMounted(() => { if (searchInput.value && props.showSearch) { searchInput.value.focus(); @@ -64,54 +111,122 @@ onMounted(() => { diff --git a/app/javascript/dashboard/components-next/sidebar/SidebarChangelogCard.vue b/app/javascript/dashboard/components-next/sidebar/SidebarChangelogCard.vue new file mode 100644 index 000000000..8da24678d --- /dev/null +++ b/app/javascript/dashboard/components-next/sidebar/SidebarChangelogCard.vue @@ -0,0 +1,110 @@ + + +