diff --git a/.env.example b/.env.example index 6e2b7fe56..b7ba0920d 100644 --- a/.env.example +++ b/.env.example @@ -155,10 +155,6 @@ TWITTER_ENVIRONMENT= SLACK_CLIENT_ID= SLACK_CLIENT_SECRET= -#Linear Integration -LINEAR_CLIENT_ID= -LINEAR_CLIENT_SECRET= - # Google OAuth GOOGLE_OAUTH_CLIENT_ID= GOOGLE_OAUTH_CLIENT_SECRET= diff --git a/.github/workflows/nightly_installer.yml b/.github/workflows/nightly_installer.yml index d11fe6401..a01ba1093 100644 --- a/.github/workflows/nightly_installer.yml +++ b/.github/workflows/nightly_installer.yml @@ -16,7 +16,7 @@ on: jobs: nightly: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - name: get installer diff --git a/.gitignore b/.gitignore index 5eb883db0..77c4a4740 100644 --- a/.gitignore +++ b/.gitignore @@ -73,6 +73,7 @@ test/cypress/videos/* #ignore files under .vscode directory .vscode +.cursor # yalc for local testing .yalc diff --git a/Gemfile.lock b/Gemfile.lock index e9a573816..857319fc4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -561,7 +561,7 @@ GEM activesupport (>= 3.0.0) raabro (1.4.0) racc (1.8.1) - rack (2.2.11) + rack (2.2.12) rack-attack (6.7.0) rack (>= 1.0, < 4) rack-contrib (2.5.0) @@ -799,7 +799,7 @@ GEM unf_ext (0.0.8.2) unicode-display_width (2.4.2) uniform_notifier (1.16.0) - uri (0.13.0) + uri (1.0.3) uri_template (0.7.0) valid_email2 (5.2.6) activemodel (>= 3.2) diff --git a/app/assets/stylesheets/administrate/components/_buttons.scss b/app/assets/stylesheets/administrate/components/_buttons.scss index 7b2f62045..a0c3699ba 100644 --- a/app/assets/stylesheets/administrate/components/_buttons.scss +++ b/app/assets/stylesheets/administrate/components/_buttons.scss @@ -1,8 +1,8 @@ -button, -input[type="button"], -input[type="reset"], -input[type="submit"], -.button { +button:not(.reset-base), +input[type='button']:not(.reset-base), +input[type='reset']:not(.reset-base), +input[type='submit']:not(.reset-base), +.button:not(.reset-base) { appearance: none; background-color: $color-woot; border: 0; diff --git a/app/assets/stylesheets/administrate/custom_styles.scss b/app/assets/stylesheets/administrate/custom_styles.scss index 5e6d803d8..00f1a058c 100644 --- a/app/assets/stylesheets/administrate/custom_styles.scss +++ b/app/assets/stylesheets/administrate/custom_styles.scss @@ -10,7 +10,6 @@ .icon-container { margin-right: 2px; - } .value-container { diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb index 2cd5281ff..138c2bd68 100644 --- a/app/controllers/api/v1/accounts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/conversations_controller.rb @@ -6,6 +6,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro before_action :conversation, except: [:index, :meta, :search, :create, :filter] before_action :inbox, :contact, :contact_inbox, only: [:create] + ATTACHMENT_RESULTS_PER_PAGE = 100 + def index result = conversation_finder.perform @conversations = result[:conversations] @@ -24,7 +26,12 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro end def attachments + @attachments_count = @conversation.attachments.count @attachments = @conversation.attachments + .includes(:message) + .order(created_at: :desc) + .page(attachment_params[:page]) + .per(ATTACHMENT_RESULTS_PER_PAGE) end def show; end @@ -124,6 +131,10 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro params.permit(:priority) end + def attachment_params + params.permit(:page) + end + def update_last_seen_on_conversation(last_seen_at, update_assignee) # rubocop:disable Rails/SkipsModelValidations @conversation.update_column(:agent_last_seen_at, last_seen_at) diff --git a/app/controllers/concerns/switch_locale.rb b/app/controllers/concerns/switch_locale.rb index 3013ff3cc..a8ea8ae05 100644 --- a/app/controllers/concerns/switch_locale.rb +++ b/app/controllers/concerns/switch_locale.rb @@ -5,10 +5,11 @@ module SwitchLocale def switch_locale(&) # priority is for locale set in query string (mostly for widget/from js sdk) - locale ||= locale_from_params + locale ||= params[:locale] + locale ||= locale_from_custom_domain # if locale is not set in account, let's use DEFAULT_LOCALE env variable - locale ||= locale_from_env_variable + locale ||= ENV.fetch('DEFAULT_LOCALE', nil) set_locale(locale, &) end @@ -32,26 +33,30 @@ module SwitchLocale end def set_locale(locale, &) - # if locale is empty, use default_locale - locale ||= I18n.default_locale + safe_locale = validate_and_get_locale(locale) # Ensure locale won't bleed into other requests # https://guides.rubyonrails.org/i18n.html#managing-the-locale-across-requests - I18n.with_locale(locale, &) + I18n.with_locale(safe_locale, &) end - def locale_from_params - I18n.available_locales.map(&:to_s).include?(params[:locale]) ? params[:locale] : nil + def validate_and_get_locale(locale) + return I18n.default_locale.to_s if locale.blank? + + available_locales = I18n.available_locales.map(&:to_s) + locale_without_variant = locale.split('_')[0] + + if available_locales.include?(locale) + locale + elsif available_locales.include?(locale_without_variant) + locale_without_variant + else + I18n.default_locale.to_s + end end def locale_from_account(account) return unless account - I18n.available_locales.map(&:to_s).include?(account.locale) ? account.locale : nil - end - - def locale_from_env_variable - return unless ENV.fetch('DEFAULT_LOCALE', nil) - - I18n.available_locales.map(&:to_s).include?(ENV.fetch('DEFAULT_LOCALE')) ? ENV.fetch('DEFAULT_LOCALE') : nil + account.locale end end diff --git a/app/controllers/linear/callbacks_controller.rb b/app/controllers/linear/callbacks_controller.rb index c0688cefc..2eea49333 100644 --- a/app/controllers/linear/callbacks_controller.rb +++ b/app/controllers/linear/callbacks_controller.rb @@ -16,9 +16,12 @@ class Linear::CallbacksController < ApplicationController private def oauth_client + app_id = GlobalConfigService.load('LINEAR_CLIENT_ID', nil) + app_secret = GlobalConfigService.load('LINEAR_CLIENT_SECRET', nil) + OAuth2::Client.new( - ENV.fetch('LINEAR_CLIENT_ID', nil), - ENV.fetch('LINEAR_CLIENT_SECRET', nil), + app_id, + app_secret, { site: 'https://api.linear.app', token_url: '/oauth/token', diff --git a/app/controllers/public/api/v1/portals/base_controller.rb b/app/controllers/public/api/v1/portals/base_controller.rb index f6c10f7c4..66b052b1e 100644 --- a/app/controllers/public/api/v1/portals/base_controller.rb +++ b/app/controllers/public/api/v1/portals/base_controller.rb @@ -1,4 +1,6 @@ class Public::Api::V1::Portals::BaseController < PublicController + include SwitchLocale + before_action :show_plain_layout before_action :set_color_scheme before_action :set_global_config @@ -27,14 +29,7 @@ class Public::Api::V1::Portals::BaseController < PublicController end def switch_locale_with_portal(&) - locale_without_variant = params[:locale].split('_')[0] - is_locale_available = I18n.available_locales.map(&:to_s).include?(params[:locale]) - is_locale_variant_available = I18n.available_locales.map(&:to_s).include?(locale_without_variant) - if is_locale_available - @locale = params[:locale] - elsif is_locale_variant_available - @locale = locale_without_variant - end + @locale = validate_and_get_locale(params[:locale]) I18n.with_locale(@locale, &) end @@ -44,12 +39,12 @@ class Public::Api::V1::Portals::BaseController < PublicController Rails.logger.info "Article: not found for slug: #{params[:article_slug]}" render_404 && return if article.blank? - @locale = if article.category.present? - article.category.locale - else - article.portal.default_locale - end - + article_locale = if article.category.present? + article.category.locale + else + article.portal.default_locale + end + @locale = validate_and_get_locale(article_locale) I18n.with_locale(@locale, &) end diff --git a/app/controllers/super_admin/app_configs_controller.rb b/app/controllers/super_admin/app_configs_controller.rb index b8f3bd9a9..43157fa0e 100644 --- a/app/controllers/super_admin/app_configs_controller.rb +++ b/app/controllers/super_admin/app_configs_controller.rb @@ -39,6 +39,8 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController %w[AZURE_APP_ID AZURE_APP_SECRET] when 'email' ['MAILER_INBOUND_EMAIL_DOMAIN'] + when 'linear' + %w[LINEAR_CLIENT_ID LINEAR_CLIENT_SECRET] else %w[ENABLE_ACCOUNT_SIGNUP FIREBASE_PROJECT_ID FIREBASE_CREDENTIALS] end diff --git a/app/dashboards/account_dashboard.rb b/app/dashboards/account_dashboard.rb index b566551f4..f7b04a167 100644 --- a/app/dashboards/account_dashboard.rb +++ b/app/dashboards/account_dashboard.rb @@ -78,7 +78,11 @@ class AccountDashboard < Administrate::BaseDashboard # COLLECTION_FILTERS = { # open: ->(resources) { resources.where(open: true) } # }.freeze - COLLECTION_FILTERS = {}.freeze + COLLECTION_FILTERS = { + active: ->(resources) { resources.where(status: :active) }, + suspended: ->(resources) { resources.where(status: :suspended) }, + recent: ->(resources) { resources.where('created_at > ?', 30.days.ago) } + }.freeze # Overwrite this method to customize how accounts are displayed # across all pages of the admin dashboard. diff --git a/app/dashboards/user_dashboard.rb b/app/dashboards/user_dashboard.rb index 6b2129eed..8abdefd1a 100644 --- a/app/dashboards/user_dashboard.rb +++ b/app/dashboards/user_dashboard.rb @@ -94,7 +94,12 @@ class UserDashboard < Administrate::BaseDashboard # COLLECTION_FILTERS = { # open: ->(resources) { resources.where(open: true) } # }.freeze - COLLECTION_FILTERS = {}.freeze + COLLECTION_FILTERS = { + super_admin: ->(resources) { resources.where(type: 'SuperAdmin') }, + confirmed: ->(resources) { resources.where.not(confirmed_at: nil) }, + unconfirmed: ->(resources) { resources.where(confirmed_at: nil) }, + recent: ->(resources) { resources.where('created_at > ?', 30.days.ago) } + }.freeze # Overwrite this method to customize how users are displayed # across all pages of the admin dashboard. diff --git a/app/helpers/filter_helper.rb b/app/helpers/filters/filter_helper.rb similarity index 90% rename from app/helpers/filter_helper.rb rename to app/helpers/filters/filter_helper.rb index 9b5cac684..fe03dae28 100644 --- a/app/helpers/filter_helper.rb +++ b/app/helpers/filters/filter_helper.rb @@ -1,4 +1,4 @@ -module FilterHelper +module Filters::FilterHelper def build_condition_query(model_filters, query_hash, current_index) current_filter = model_filters[query_hash['attribute_key']] @@ -89,4 +89,18 @@ module FilterHelper operator = condition['query_operator'].upcase raise CustomExceptions::CustomFilter::InvalidQueryOperator.new({}) unless %w[AND OR].include?(operator) end + + def conversation_status_values(values) + return Conversation.statuses.values if values.include?('all') + + values.map { |x| Conversation.statuses[x.to_sym] } + end + + def conversation_priority_values(values) + values.map { |x| Conversation.priorities[x.to_sym] } + end + + def message_type_values(values) + values.map { |x| Message.message_types[x.to_sym] } + end end diff --git a/app/helpers/linear/integration_helper.rb b/app/helpers/linear/integration_helper.rb index 19f16832d..67df836ce 100644 --- a/app/helpers/linear/integration_helper.rb +++ b/app/helpers/linear/integration_helper.rb @@ -32,7 +32,7 @@ module Linear::IntegrationHelper private def client_secret - @client_secret ||= ENV.fetch('LINEAR_CLIENT_SECRET', nil) + @client_secret ||= GlobalConfigService.load('LINEAR_CLIENT_SECRET', nil) end def decode_token(token, secret) diff --git a/app/javascript/dashboard/api/captain/bulkActions.js b/app/javascript/dashboard/api/captain/bulkActions.js new file mode 100644 index 000000000..fd69a1108 --- /dev/null +++ b/app/javascript/dashboard/api/captain/bulkActions.js @@ -0,0 +1,9 @@ +import ApiClient from '../ApiClient'; + +class CaptainBulkActionsAPI extends ApiClient { + constructor() { + super('captain/bulk_actions', { accountScoped: true }); + } +} + +export default new CaptainBulkActionsAPI(); diff --git a/app/javascript/dashboard/api/inbox/conversation.js b/app/javascript/dashboard/api/inbox/conversation.js index 8b9eacf3f..39546096f 100644 --- a/app/javascript/dashboard/api/inbox/conversation.js +++ b/app/javascript/dashboard/api/inbox/conversation.js @@ -137,6 +137,10 @@ class ConversationApi extends ApiClient { requestCopilot(conversationId, body) { return axios.post(`${this.url}/${conversationId}/copilot`, body); } + + getInboxAssistant(conversationId) { + return axios.get(`${this.url}/${conversationId}/inbox_assistant`); + } } export default new ConversationApi(); diff --git a/app/javascript/dashboard/components-next/CardLayout.vue b/app/javascript/dashboard/components-next/CardLayout.vue index 0fd3f5986..462402167 100644 --- a/app/javascript/dashboard/components-next/CardLayout.vue +++ b/app/javascript/dashboard/components-next/CardLayout.vue @@ -4,6 +4,10 @@ defineProps({ type: String, default: 'col', }, + selectable: { + type: Boolean, + default: false, + }, }); const emit = defineEmits(['click']); @@ -18,10 +22,11 @@ const handleClick = () => { class="flex flex-col w-full shadow outline-1 outline outline-n-container group/cardLayout rounded-2xl bg-n-solid-2" >
diff --git a/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue b/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue index 312f2ad34..f00354105 100644 --- a/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue +++ b/app/javascript/dashboard/components-next/captain/assistant/ResponseCard.vue @@ -7,6 +7,7 @@ import { dynamicTime } from 'shared/helpers/timeHelper'; 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 Policy from 'dashboard/components/policy.vue'; const props = defineProps({ @@ -46,14 +47,27 @@ const props = defineProps({ type: Number, required: true, }, + isSelected: { + type: Boolean, + default: false, + }, + selectable: { + type: Boolean, + default: false, + }, }); -const emit = defineEmits(['action', 'navigate']); +const emit = defineEmits(['action', 'navigate', 'select', 'hover']); const { t } = useI18n(); const [showActionsDropdown, toggleDropdown] = useToggle(); +const modelValue = computed({ + get: () => props.isSelected, + set: () => emit('select', props.id), +}); + const statusAction = computed(() => { if (props.status === 'pending') { return [ @@ -102,8 +116,17 @@ const handleDocumentableClick = () => { diff --git a/app/javascript/dashboard/components-next/copilot/ToggleCopilotAssistant.vue b/app/javascript/dashboard/components-next/copilot/ToggleCopilotAssistant.vue new file mode 100644 index 000000000..ec21cec9f --- /dev/null +++ b/app/javascript/dashboard/components-next/copilot/ToggleCopilotAssistant.vue @@ -0,0 +1,81 @@ + + + diff --git a/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownBody.vue b/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownBody.vue index 246eba5aa..1a67aaa68 100644 --- a/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownBody.vue +++ b/app/javascript/dashboard/components-next/dropdown-menu/base/DropdownBody.vue @@ -19,7 +19,7 @@ const beforeClass = computed(() => { // Add extra blur layer only when strong prop is true, as a hack for Chrome's stacked backdrop-blur limitation // https://issues.chromium.org/issues/40835530 - return "before:content-['\x00A0'] before:absolute before:bottom-0 before:left-0 before:w-full before:h-full before:backdrop-contrast-70 before:backdrop-blur-sm before:z-0 [&>*]:relative"; + return "before:content-['\x00A0'] before:absolute before:bottom-0 before:left-0 before:w-full before:h-full before:rounded-xl before:backdrop-contrast-70 before:backdrop-blur-sm before:z-0 [&>*]:relative"; }); diff --git a/app/javascript/dashboard/components-next/filter/contactProvider.js b/app/javascript/dashboard/components-next/filter/contactProvider.js index 4aceac359..4b2709e62 100644 --- a/app/javascript/dashboard/components-next/filter/contactProvider.js +++ b/app/javascript/dashboard/components-next/filter/contactProvider.js @@ -2,7 +2,10 @@ import { computed } from 'vue'; import { useI18n } from 'vue-i18n'; import { useOperators } from './operators'; import { useMapGetter } from 'dashboard/composables/store.js'; -import { buildAttributesFilterTypes } from './helper/filterHelper.js'; +import { + buildAttributesFilterTypes, + CONTACT_ATTRIBUTES, +} from './helper/filterHelper.js'; import countries from 'shared/constants/countries.js'; /** @@ -59,7 +62,11 @@ export function useContactFilterContext() { * @type {import('vue').ComputedRef} */ const customFilterTypes = computed(() => - buildAttributesFilterTypes(contactAttributes.value, getOperatorTypes) + buildAttributesFilterTypes( + contactAttributes.value, + getOperatorTypes, + 'contact' + ) ); /** @@ -67,8 +74,8 @@ export function useContactFilterContext() { */ const filterTypes = computed(() => [ { - attributeKey: 'name', - value: 'name', + attributeKey: CONTACT_ATTRIBUTES.NAME, + value: CONTACT_ATTRIBUTES.NAME, attributeName: t('CONTACTS_LAYOUT.FILTER.NAME'), label: t('CONTACTS_LAYOUT.FILTER.NAME'), inputType: 'plainText', @@ -77,8 +84,8 @@ export function useContactFilterContext() { attributeModel: 'standard', }, { - attributeKey: 'email', - value: 'email', + attributeKey: CONTACT_ATTRIBUTES.EMAIL, + value: CONTACT_ATTRIBUTES.EMAIL, attributeName: t('CONTACTS_LAYOUT.FILTER.EMAIL'), label: t('CONTACTS_LAYOUT.FILTER.EMAIL'), inputType: 'plainText', @@ -87,8 +94,8 @@ export function useContactFilterContext() { attributeModel: 'standard', }, { - attributeKey: 'phone_number', - value: 'phone_number', + attributeKey: CONTACT_ATTRIBUTES.PHONE_NUMBER, + value: CONTACT_ATTRIBUTES.PHONE_NUMBER, attributeName: t('CONTACTS_LAYOUT.FILTER.PHONE_NUMBER'), label: t('CONTACTS_LAYOUT.FILTER.PHONE_NUMBER'), inputType: 'plainText', @@ -97,8 +104,8 @@ export function useContactFilterContext() { attributeModel: 'standard', }, { - attributeKey: 'identifier', - value: 'identifier', + attributeKey: CONTACT_ATTRIBUTES.IDENTIFIER, + value: CONTACT_ATTRIBUTES.IDENTIFIER, attributeName: t('CONTACTS_LAYOUT.FILTER.IDENTIFIER'), label: t('CONTACTS_LAYOUT.FILTER.IDENTIFIER'), inputType: 'plainText', @@ -107,8 +114,8 @@ export function useContactFilterContext() { attributeModel: 'standard', }, { - attributeKey: 'country_code', - value: 'country_code', + attributeKey: CONTACT_ATTRIBUTES.COUNTRY_CODE, + value: CONTACT_ATTRIBUTES.COUNTRY_CODE, attributeName: t('FILTER.ATTRIBUTES.COUNTRY_NAME'), label: t('FILTER.ATTRIBUTES.COUNTRY_NAME'), inputType: 'searchSelect', @@ -118,8 +125,8 @@ export function useContactFilterContext() { attributeModel: 'additional', }, { - attributeKey: 'city', - value: 'city', + attributeKey: CONTACT_ATTRIBUTES.CITY, + value: CONTACT_ATTRIBUTES.CITY, attributeName: t('CONTACTS_LAYOUT.FILTER.CITY'), label: t('CONTACTS_LAYOUT.FILTER.CITY'), inputType: 'plainText', @@ -128,8 +135,8 @@ export function useContactFilterContext() { attributeModel: 'standard', }, { - attributeKey: 'created_at', - value: 'created_at', + attributeKey: CONTACT_ATTRIBUTES.CREATED_AT, + value: CONTACT_ATTRIBUTES.CREATED_AT, attributeName: t('CONTACTS_LAYOUT.FILTER.CREATED_AT'), label: t('CONTACTS_LAYOUT.FILTER.CREATED_AT'), inputType: 'date', @@ -138,8 +145,8 @@ export function useContactFilterContext() { attributeModel: 'standard', }, { - attributeKey: 'last_activity_at', - value: 'last_activity_at', + attributeKey: CONTACT_ATTRIBUTES.LAST_ACTIVITY_AT, + value: CONTACT_ATTRIBUTES.LAST_ACTIVITY_AT, attributeName: t('CONTACTS_LAYOUT.FILTER.LAST_ACTIVITY'), label: t('CONTACTS_LAYOUT.FILTER.LAST_ACTIVITY'), inputType: 'date', @@ -148,8 +155,8 @@ export function useContactFilterContext() { attributeModel: 'standard', }, { - attributeKey: 'referer', - value: 'referer', + attributeKey: CONTACT_ATTRIBUTES.REFERER, + value: CONTACT_ATTRIBUTES.REFERER, attributeName: t('CONTACTS_LAYOUT.FILTER.REFERER_LINK'), label: t('CONTACTS_LAYOUT.FILTER.REFERER_LINK'), inputType: 'plainText', @@ -158,8 +165,8 @@ export function useContactFilterContext() { attributeModel: 'standard', }, { - attributeKey: 'blocked', - value: 'blocked', + attributeKey: CONTACT_ATTRIBUTES.BLOCKED, + value: CONTACT_ATTRIBUTES.BLOCKED, attributeName: t('CONTACTS_LAYOUT.FILTER.BLOCKED'), label: t('CONTACTS_LAYOUT.FILTER.BLOCKED'), inputType: 'searchSelect', diff --git a/app/javascript/dashboard/components-next/filter/helper/filterHelper.js b/app/javascript/dashboard/components-next/filter/helper/filterHelper.js index 2160365f6..cb5acfe40 100644 --- a/app/javascript/dashboard/components-next/filter/helper/filterHelper.js +++ b/app/javascript/dashboard/components-next/filter/helper/filterHelper.js @@ -1,3 +1,35 @@ +/** + * Standard attributes of the conversation model + */ +export const CONVERSATION_ATTRIBUTES = { + STATUS: 'status', + PRIORITY: 'priority', + ASSIGNEE_ID: 'assignee_id', + INBOX_ID: 'inbox_id', + TEAM_ID: 'team_id', + DISPLAY_ID: 'display_id', + CAMPAIGN_ID: 'campaign_id', + LABELS: 'labels', + BROWSER_LANGUAGE: 'browser_language', + COUNTRY_CODE: 'country_code', + REFERER: 'referer', + CREATED_AT: 'created_at', + LAST_ACTIVITY_AT: 'last_activity_at', +}; + +export const CONTACT_ATTRIBUTES = { + NAME: 'name', + EMAIL: 'email', + PHONE_NUMBER: 'phone_number', + IDENTIFIER: 'identifier', + COUNTRY_CODE: 'country_code', + CITY: 'city', + CREATED_AT: 'created_at', + LAST_ACTIVITY_AT: 'last_activity_at', + REFERER: 'referer', + BLOCKED: 'blocked', +}; + /** * Determines the input type for a custom attribute based on its key * @param {string} key - The attribute display type key @@ -20,24 +52,37 @@ export const getCustomAttributeInputType = key => { /** * Builds filter types for custom attributes + * This also removes any conflicting attributes * @param {Array} attributes - The attributes array * @param {Function} getOperatorTypes - Function to get operator types * @returns {Array} Array of filter types */ -export const buildAttributesFilterTypes = (attributes, getOperatorTypes) => { - return attributes.map(attr => ({ - attributeKey: attr.attributeKey, - value: attr.attributeKey, - attributeName: attr.attributeDisplayName, - label: attr.attributeDisplayName, - inputType: getCustomAttributeInputType(attr.attributeDisplayType), - filterOperators: getOperatorTypes(attr.attributeDisplayType), - options: - attr.attributeDisplayType === 'list' - ? attr.attributeValues.map(item => ({ id: item, name: item })) - : [], - attributeModel: 'customAttributes', - })); +export const buildAttributesFilterTypes = ( + attributes, + getOperatorTypes, + filterModel = 'conversation' +) => { + const standardAttributes = Object.values( + filterModel === 'conversation' + ? CONVERSATION_ATTRIBUTES + : CONTACT_ATTRIBUTES + ); + + return attributes + .filter(attr => !standardAttributes.includes(attr.attributeKey)) + .map(attr => ({ + attributeKey: attr.attributeKey, + value: attr.attributeKey, + attributeName: attr.attributeDisplayName, + label: attr.attributeDisplayName, + inputType: getCustomAttributeInputType(attr.attributeDisplayType), + filterOperators: getOperatorTypes(attr.attributeDisplayType), + options: + attr.attributeDisplayType === 'list' + ? attr.attributeValues.map(item => ({ id: item, name: item })) + : [], + attributeModel: 'customAttributes', + })); }; /** diff --git a/app/javascript/dashboard/components-next/filter/provider.js b/app/javascript/dashboard/components-next/filter/provider.js index bb775a663..f6d078d76 100644 --- a/app/javascript/dashboard/components-next/filter/provider.js +++ b/app/javascript/dashboard/components-next/filter/provider.js @@ -3,7 +3,10 @@ import { useI18n } from 'vue-i18n'; import { useOperators } from './operators'; import { useMapGetter } from 'dashboard/composables/store.js'; import { useChannelIcon } from 'next/icon/provider'; -import { buildAttributesFilterTypes } from './helper/filterHelper'; +import { + buildAttributesFilterTypes, + CONVERSATION_ATTRIBUTES, +} from './helper/filterHelper'; import countries from 'shared/constants/countries.js'; import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages.js'; @@ -70,7 +73,11 @@ export function useConversationFilterContext() { * @type {import('vue').ComputedRef} */ const customFilterTypes = computed(() => - buildAttributesFilterTypes(conversationAttributes.value, getOperatorTypes) + buildAttributesFilterTypes( + conversationAttributes.value, + getOperatorTypes, + 'conversation' + ) ); /** @@ -78,8 +85,8 @@ export function useConversationFilterContext() { */ const filterTypes = computed(() => [ { - attributeKey: 'status', - value: 'status', + attributeKey: CONVERSATION_ATTRIBUTES.STATUS, + value: CONVERSATION_ATTRIBUTES.STATUS, attributeName: t('FILTER.ATTRIBUTES.STATUS'), label: t('FILTER.ATTRIBUTES.STATUS'), inputType: 'multiSelect', @@ -94,8 +101,24 @@ export function useConversationFilterContext() { attributeModel: 'standard', }, { - attributeKey: 'assignee_id', - value: 'assignee_id', + attributeKey: CONVERSATION_ATTRIBUTES.PRIORITY, + value: CONVERSATION_ATTRIBUTES.PRIORITY, + attributeName: t('FILTER.ATTRIBUTES.PRIORITY'), + label: t('FILTER.ATTRIBUTES.PRIORITY'), + inputType: 'multiSelect', + options: ['low', 'medium', 'high', 'urgent'].map(id => { + return { + id, + name: t(`CONVERSATION.PRIORITY.OPTIONS.${id.toUpperCase()}`), + }; + }), + dataType: 'text', + filterOperators: equalityOperators.value, + attributeModel: 'standard', + }, + { + attributeKey: CONVERSATION_ATTRIBUTES.ASSIGNEE_ID, + value: CONVERSATION_ATTRIBUTES.ASSIGNEE_ID, attributeName: t('FILTER.ATTRIBUTES.ASSIGNEE_NAME'), label: t('FILTER.ATTRIBUTES.ASSIGNEE_NAME'), inputType: 'searchSelect', @@ -110,8 +133,8 @@ export function useConversationFilterContext() { attributeModel: 'standard', }, { - attributeKey: 'inbox_id', - value: 'inbox_id', + attributeKey: CONVERSATION_ATTRIBUTES.INBOX_ID, + value: CONVERSATION_ATTRIBUTES.INBOX_ID, attributeName: t('FILTER.ATTRIBUTES.INBOX_NAME'), label: t('FILTER.ATTRIBUTES.INBOX_NAME'), inputType: 'searchSelect', @@ -126,8 +149,8 @@ export function useConversationFilterContext() { attributeModel: 'standard', }, { - attributeKey: 'team_id', - value: 'team_id', + attributeKey: CONVERSATION_ATTRIBUTES.TEAM_ID, + value: CONVERSATION_ATTRIBUTES.TEAM_ID, attributeName: t('FILTER.ATTRIBUTES.TEAM_NAME'), label: t('FILTER.ATTRIBUTES.TEAM_NAME'), inputType: 'searchSelect', @@ -137,8 +160,8 @@ export function useConversationFilterContext() { attributeModel: 'standard', }, { - attributeKey: 'display_id', - value: 'display_id', + attributeKey: CONVERSATION_ATTRIBUTES.DISPLAY_ID, + value: CONVERSATION_ATTRIBUTES.DISPLAY_ID, attributeName: t('FILTER.ATTRIBUTES.CONVERSATION_IDENTIFIER'), label: t('FILTER.ATTRIBUTES.CONVERSATION_IDENTIFIER'), inputType: 'plainText', @@ -147,8 +170,8 @@ export function useConversationFilterContext() { attributeModel: 'standard', }, { - attributeKey: 'campaign_id', - value: 'campaign_id', + attributeKey: CONVERSATION_ATTRIBUTES.CAMPAIGN_ID, + value: CONVERSATION_ATTRIBUTES.CAMPAIGN_ID, attributeName: t('FILTER.ATTRIBUTES.CAMPAIGN_NAME'), label: t('FILTER.ATTRIBUTES.CAMPAIGN_NAME'), inputType: 'searchSelect', @@ -161,8 +184,8 @@ export function useConversationFilterContext() { attributeModel: 'standard', }, { - attributeKey: 'labels', - value: 'labels', + attributeKey: CONVERSATION_ATTRIBUTES.LABELS, + value: CONVERSATION_ATTRIBUTES.LABELS, attributeName: t('FILTER.ATTRIBUTES.LABELS'), label: t('FILTER.ATTRIBUTES.LABELS'), inputType: 'multiSelect', @@ -185,8 +208,8 @@ export function useConversationFilterContext() { attributeModel: 'standard', }, { - attributeKey: 'browser_language', - value: 'browser_language', + attributeKey: CONVERSATION_ATTRIBUTES.BROWSER_LANGUAGE, + value: CONVERSATION_ATTRIBUTES.BROWSER_LANGUAGE, attributeName: t('FILTER.ATTRIBUTES.BROWSER_LANGUAGE'), label: t('FILTER.ATTRIBUTES.BROWSER_LANGUAGE'), inputType: 'searchSelect', @@ -196,8 +219,8 @@ export function useConversationFilterContext() { attributeModel: 'additional', }, { - attributeKey: 'country_code', - value: 'country_code', + attributeKey: CONVERSATION_ATTRIBUTES.COUNTRY_CODE, + value: CONVERSATION_ATTRIBUTES.COUNTRY_CODE, attributeName: t('FILTER.ATTRIBUTES.COUNTRY_NAME'), label: t('FILTER.ATTRIBUTES.COUNTRY_NAME'), inputType: 'searchSelect', @@ -207,8 +230,8 @@ export function useConversationFilterContext() { attributeModel: 'additional', }, { - attributeKey: 'referer', - value: 'referer', + attributeKey: CONVERSATION_ATTRIBUTES.REFERER, + value: CONVERSATION_ATTRIBUTES.REFERER, attributeName: t('FILTER.ATTRIBUTES.REFERER_LINK'), label: t('FILTER.ATTRIBUTES.REFERER_LINK'), inputType: 'plainText', @@ -217,8 +240,8 @@ export function useConversationFilterContext() { attributeModel: 'additional', }, { - attributeKey: 'created_at', - value: 'created_at', + attributeKey: CONVERSATION_ATTRIBUTES.CREATED_AT, + value: CONVERSATION_ATTRIBUTES.CREATED_AT, attributeName: t('FILTER.ATTRIBUTES.CREATED_AT'), label: t('FILTER.ATTRIBUTES.CREATED_AT'), inputType: 'date', @@ -227,8 +250,8 @@ export function useConversationFilterContext() { attributeModel: 'standard', }, { - attributeKey: 'last_activity_at', - value: 'last_activity_at', + attributeKey: CONVERSATION_ATTRIBUTES.LAST_ACTIVITY_AT, + value: CONVERSATION_ATTRIBUTES.LAST_ACTIVITY_AT, attributeName: t('FILTER.ATTRIBUTES.LAST_ACTIVITY'), label: t('FILTER.ATTRIBUTES.LAST_ACTIVITY'), inputType: 'date', diff --git a/app/javascript/dashboard/components-next/message/MessageError.vue b/app/javascript/dashboard/components-next/message/MessageError.vue index e113ada71..d3a03e746 100644 --- a/app/javascript/dashboard/components-next/message/MessageError.vue +++ b/app/javascript/dashboard/components-next/message/MessageError.vue @@ -26,7 +26,7 @@ const { t } = useI18n(); />