diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..1015fe997 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,7 @@ +## All javascript files should be reviewed by pranav before merging +*.js @pranavrajs +*.vue @pranavrajs + + +## All enterprise related files should be reviewed by sojan before merging +/enterprise/* @sojan-official diff --git a/.github/workflows/logging_percentage_check.yml b/.github/workflows/logging_percentage_check.yml index 7b5627d45..e9f84c313 100644 --- a/.github/workflows/logging_percentage_check.yml +++ b/.github/workflows/logging_percentage_check.yml @@ -11,7 +11,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Check for log lines and calculate percentage run: | diff --git a/.github/workflows/nightly_installer.yml b/.github/workflows/nightly_installer.yml index 034c08e8c..d11fe6401 100644 --- a/.github/workflows/nightly_installer.yml +++ b/.github/workflows/nightly_installer.yml @@ -4,7 +4,7 @@ # # This action will try to install and setup # # chatwoot on an Ubuntu 20.04 machine using # # the linux installer script. -# # +# # # # This is set to run daily at midnight. # # @@ -35,7 +35,7 @@ jobs: run: | sudo ./install.sh --install < input - # disabling http verify for now as http + # disabling http verify for now as http # access to port 3000 fails in gh action env # - name: Verify # if: always() @@ -45,7 +45,7 @@ jobs: # curl http://localhost:3000/api - name: Upload chatwoot setup log file as an artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: always() with: name: chatwoot-setup-log-file diff --git a/.github/workflows/publish_codespace_image.yml b/.github/workflows/publish_codespace_image.yml index 60632d906..647608473 100644 --- a/.github/workflows/publish_codespace_image.yml +++ b/.github/workflows/publish_codespace_image.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Login to GitHub Container Registry uses: docker/login-action@v1 diff --git a/.github/workflows/publish_foss_docker.yml b/.github/workflows/publish_foss_docker.yml index 2ddaba7e5..d48b82a58 100644 --- a/.github/workflows/publish_foss_docker.yml +++ b/.github/workflows/publish_foss_docker.yml @@ -21,7 +21,7 @@ jobs: GIT_REF: ${{ github.head_ref || github.ref_name }} # ref_name to get tags/branches steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up QEMU uses: docker/setup-qemu-action@v1 diff --git a/.github/workflows/run_foss_spec.yml b/.github/workflows/run_foss_spec.yml index 7a32f3fd1..b0b2372ae 100644 --- a/.github/workflows/run_foss_spec.yml +++ b/.github/workflows/run_foss_spec.yml @@ -41,7 +41,7 @@ jobs: options: --entrypoint redis-server steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.ref }} repository: ${{ github.event.pull_request.head.repo.full_name }} @@ -50,7 +50,7 @@ jobs: with: bundler-cache: true # runs 'bundle install' and caches installed gems automatically - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: 20 cache: yarn @@ -76,11 +76,11 @@ jobs: - name: Run backend tests run: | bundle exec rspec --profile=10 --format documentation - env: + env: NODE_OPTIONS: --openssl-legacy-provider - name: Upload rails log folder - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: always() with: name: rails-log-folder diff --git a/.github/workflows/run_response_bot_spec.yml b/.github/workflows/run_response_bot_spec.yml index 6fd6a7b22..77b96c48a 100644 --- a/.github/workflows/run_response_bot_spec.yml +++ b/.github/workflows/run_response_bot_spec.yml @@ -40,7 +40,7 @@ jobs: options: --entrypoint redis-server steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.ref }} repository: ${{ github.event.pull_request.head.repo.full_name }} @@ -49,7 +49,7 @@ jobs: with: bundler-cache: true # runs 'bundle install' and caches installed gems automatically - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: 20 cache: yarn @@ -77,7 +77,7 @@ jobs: --format documentation - name: Upload rails log folder - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: always() with: name: rails-log-folder diff --git a/.github/workflows/size-limit.yml b/.github/workflows/size-limit.yml index 01d72339d..0526aeedb 100644 --- a/.github/workflows/size-limit.yml +++ b/.github/workflows/size-limit.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.ref }} repository: ${{ github.event.pull_request.head.repo.full_name }} @@ -19,7 +19,7 @@ jobs: with: bundler-cache: true # runs 'bundle install' and caches installed gems automatically - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: 20 cache: 'yarn' @@ -31,7 +31,7 @@ jobs: run: | rm -rf enterprise rm -rf spec/enterprise - + - name: Run asset compile run: bundle exec rake assets:precompile env: diff --git a/Gemfile.lock b/Gemfile.lock index 74b281b43..35e1e2a48 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -316,16 +316,16 @@ GEM google-cloud-translate-v3 (0.6.0) gapic-common (>= 0.17.1, < 2.a) google-cloud-errors (~> 1.0) - google-protobuf (3.22.3) - google-protobuf (3.22.3-arm64-darwin) - google-protobuf (3.22.3-x86_64-darwin) - google-protobuf (3.22.3-x86_64-linux) + google-protobuf (3.25.2) + google-protobuf (3.25.2-arm64-darwin) + google-protobuf (3.25.2-x86_64-darwin) + google-protobuf (3.25.2-x86_64-linux) googleapis-common-protos (1.4.0) google-protobuf (~> 3.14) googleapis-common-protos-types (~> 1.2) grpc (~> 1.27) - googleapis-common-protos-types (1.6.0) - google-protobuf (~> 3.14) + googleapis-common-protos-types (1.11.0) + google-protobuf (~> 3.18) googleauth (1.5.2) faraday (>= 0.17.3, < 3.a) jwt (>= 1.4, < 3.0) @@ -335,13 +335,13 @@ GEM signet (>= 0.16, < 2.a) groupdate (6.2.1) activesupport (>= 5.2) - grpc (1.54.0) + grpc (1.54.3) google-protobuf (~> 3.21) googleapis-common-protos-types (~> 1.0) - grpc (1.54.0-x86_64-darwin) + grpc (1.54.3-x86_64-darwin) google-protobuf (~> 3.21) googleapis-common-protos-types (~> 1.0) - grpc (1.54.0-x86_64-linux) + grpc (1.54.3-x86_64-linux) google-protobuf (~> 3.21) googleapis-common-protos-types (~> 1.0) haikunator (1.1.1) diff --git a/app/controllers/concerns/access_token_auth_helper.rb b/app/controllers/concerns/access_token_auth_helper.rb index 0a6c89860..cd760f7ce 100644 --- a/app/controllers/concerns/access_token_auth_helper.rb +++ b/app/controllers/concerns/access_token_auth_helper.rb @@ -1,7 +1,8 @@ module AccessTokenAuthHelper BOT_ACCESSIBLE_ENDPOINTS = { - 'api/v1/accounts/conversations' => %w[toggle_status create], - 'api/v1/accounts/conversations/messages' => ['create'] + 'api/v1/accounts/conversations' => %w[toggle_status toggle_priority create], + 'api/v1/accounts/conversations/messages' => ['create'], + 'api/v1/accounts/conversations/assignments' => ['create'] }.freeze def ensure_access_token diff --git a/app/controllers/concerns/ensure_current_account_helper.rb b/app/controllers/concerns/ensure_current_account_helper.rb index 5d02e96c8..3baf9ee1e 100644 --- a/app/controllers/concerns/ensure_current_account_helper.rb +++ b/app/controllers/concerns/ensure_current_account_helper.rb @@ -25,6 +25,6 @@ module EnsureCurrentAccountHelper end def account_accessible_for_bot?(account) - render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id) + render_unauthorized('Bot is not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id) end end diff --git a/app/controllers/super_admin/instance_statuses_controller.rb b/app/controllers/super_admin/instance_statuses_controller.rb index a1dc458ae..b0e97b95d 100644 --- a/app/controllers/super_admin/instance_statuses_controller.rb +++ b/app/controllers/super_admin/instance_statuses_controller.rb @@ -28,8 +28,7 @@ class SuperAdmin::InstanceStatusesController < SuperAdmin::ApplicationController end def sha - sha = `git rev-parse HEAD` - @metrics['Git SHA'] = sha.presence || 'n/a' + @metrics['Git SHA'] = GIT_HASH end def postgres_status diff --git a/app/helpers/api/v2/accounts/heatmap_helper.rb b/app/helpers/api/v2/accounts/heatmap_helper.rb index 597135f47..58dade28d 100644 --- a/app/helpers/api/v2/accounts/heatmap_helper.rb +++ b/app/helpers/api/v2/accounts/heatmap_helper.rb @@ -15,8 +15,8 @@ module Api::V2::Accounts::HeatmapHelper dates = data.pluck(:date).uniq.sort # add the dates as the first row, leave an empty cell for the hour column - # e.g. [nil, '2023-01-01', '2023-1-02', '2023-01-03'] - result_arr << ([nil] + dates) + # e.g. ['Start of the hour', '2023-01-01', '2023-1-02', '2023-01-03'] + result_arr << (['Start of the hour'] + dates) # group the data by hour, we do not need to sort it, because the data is already sorted # given it starts from the beginning of the day @@ -25,7 +25,7 @@ module Api::V2::Accounts::HeatmapHelper # value = [{date: 2023-01-01, value: 1}, {date: 2023-01-02, value: 1}, {date: 2023-01-03, value: 1}, ...] data.group_by { |d| d[:hour] }.each do |hour, items| # create a new row for each hour - row = [hour] + row = [format('%02d:00', hour)] # group the items by date, so we can easily access the value for each date # grouped values will be a hasg with the date as the key, and the value as the value @@ -37,7 +37,7 @@ module Api::V2::Accounts::HeatmapHelper row << (grouped_values[date][0][:value] if grouped_values[date].is_a?(Array)) end - # row will look like [22, 0, 0, 1, 4, 6, 7, 4] + # row will look like ['22:00', 0, 0, 1, 4, 6, 7, 4] # add the row to the result array result_arr << row @@ -46,12 +46,12 @@ module Api::V2::Accounts::HeatmapHelper # return the resultant array # the result looks like this # [ - # [nil, '2023-01-01', '2023-1-02', '2023-01-03'], - # [0, 0, 0, 0], - # [1, 0, 0, 0], - # [2, 0, 0, 0], - # [3, 0, 0, 0], - # [4, 0, 0, 0], + # ['Start of the hour', '2023-01-01', '2023-1-02', '2023-01-03'], + # ['00:00', 0, 0, 0], + # ['01:00', 0, 0, 0], + # ['02:00', 0, 0, 0], + # ['03:00', 0, 0, 0], + # ['04:00', 0, 0, 0], # ] result_arr end diff --git a/app/javascript/dashboard/api/notifications.js b/app/javascript/dashboard/api/notifications.js index 183642742..65fc937b7 100644 --- a/app/javascript/dashboard/api/notifications.js +++ b/app/javascript/dashboard/api/notifications.js @@ -6,8 +6,15 @@ class NotificationsAPI extends ApiClient { super('notifications', { accountScoped: true }); } - get(page) { - return axios.get(`${this.url}?page=${page}`); + get({ page, status, type, sortOrder }) { + return axios.get(this.url, { + params: { + page, + status, + type, + sort_order: sortOrder, + }, + }); } getNotifications(contactId) { diff --git a/app/javascript/dashboard/api/specs/notifications.spec.js b/app/javascript/dashboard/api/specs/notifications.spec.js index 5bdf88d7f..bc06eaa2b 100644 --- a/app/javascript/dashboard/api/specs/notifications.spec.js +++ b/app/javascript/dashboard/api/specs/notifications.spec.js @@ -28,10 +28,20 @@ describe('#NotificationAPI', () => { }); it('#get', () => { - notificationsAPI.get(1); - expect(axiosMock.get).toHaveBeenCalledWith( - '/api/v1/notifications?page=1' - ); + notificationsAPI.get({ + page: 1, + status: 'read', + type: 'Conversation', + sortOrder: 'desc', + }); + expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/notifications', { + params: { + page: 1, + status: 'read', + type: 'Conversation', + sort_order: 'desc', + }, + }); }); it('#getNotifications', () => { @@ -65,5 +75,30 @@ describe('#NotificationAPI', () => { '/api/v1/notifications/read_all' ); }); + + it('#snooze', () => { + notificationsAPI.snooze({ id: 1, snoozedUntil: 12332211 }); + expect(axiosMock.post).toHaveBeenCalledWith( + '/api/v1/notifications/1/snooze', + { + snoozed_until: 12332211, + } + ); + }); + + it('#delete', () => { + notificationsAPI.delete(1); + expect(axiosMock.delete).toHaveBeenCalledWith('/api/v1/notifications/1'); + }); + + it('#deleteAll', () => { + notificationsAPI.deleteAll({ type: 'all' }); + expect(axiosMock.post).toHaveBeenCalledWith( + '/api/v1/notifications/destroy_all', + { + type: 'all', + } + ); + }); }); }); diff --git a/app/javascript/dashboard/constants/globals.js b/app/javascript/dashboard/constants/globals.js index 84c789151..c0b0bdcf0 100644 --- a/app/javascript/dashboard/constants/globals.js +++ b/app/javascript/dashboard/constants/globals.js @@ -46,6 +46,19 @@ export default { }, EXAMPLE_URL: 'https://example.com', EXAMPLE_WEBHOOK_URL: 'https://example/api/webhook', + INBOX_SORT_BY: { + NEWEST: 'desc', + OLDEST: 'asc', + }, + INBOX_DISPLAY_BY: { + SNOOZED: 'snoozed', + READ: 'read', + }, + INBOX_FILTER_TYPE: { + STATUS: 'status', + TYPE: 'type', + SORT_ORDER: 'sort_order', + }, }; export const DEFAULT_REDIRECT_URL = '/app/'; export const ONBOARDING_START_URL = '/app/start/setup-profile'; diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue index eb2bc2bb4..ad2ea5d25 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue @@ -306,7 +306,7 @@ export default { }); } else if (isAInboxViewRoute(this.$route.name)) { this.$router.push({ - name: 'inbox-view', + name: 'inbox_view', }); } else if (this.$route.name !== 'contacts_dashboard') { this.$router.push({ diff --git a/app/javascript/dashboard/routes/dashboard/inbox/InboxList.vue b/app/javascript/dashboard/routes/dashboard/inbox/InboxList.vue index e0924b9c0..bb0f2e7df 100644 --- a/app/javascript/dashboard/routes/dashboard/inbox/InboxList.vue +++ b/app/javascript/dashboard/routes/dashboard/inbox/InboxList.vue @@ -3,13 +3,13 @@ class="flex flex-col h-full w-full ltr:border-r border-slate-50 dark:border-slate-800/50" :class="isOnExpandedLayout ? '' : 'min-w-[360px] max-w-[360px]'" > - +
import { mapGetters } from 'vuex'; +import wootConstants from 'dashboard/constants/globals'; import InboxCard from './components/InboxCard.vue'; import InboxListHeader from './components/InboxListHeader.vue'; import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events'; import IntersectionObserver from 'dashboard/components/IntersectionObserver.vue'; import alertMixin from 'shared/mixins/alertMixin'; +import uiSettingsMixin from 'dashboard/mixins/uiSettings'; export default { components: { InboxCard, InboxListHeader, IntersectionObserver, }, - mixins: [alertMixin], + mixins: [alertMixin, uiSettingsMixin], props: { conversationId: { type: [String, Number], @@ -71,38 +73,64 @@ export default { rootMargin: '100px 0px 100px 0px', }, page: 1, + status: '', + type: '', + sortOrder: wootConstants.INBOX_SORT_BY.NEWEST, }; }, computed: { ...mapGetters({ accountId: 'getCurrentAccountId', meta: 'notifications/getMeta', - records: 'notifications/getNotifications', uiFlags: 'notifications/getUIFlags', + notification: 'notifications/getFilteredNotifications', }), + inboxFilters() { + return { + page: this.page, + status: this.status, + type: this.type, + sortOrder: this.sortOrder, + }; + }, + notifications() { + return this.notification(this.inboxFilters); + }, showEndOfList() { return this.uiFlags.isAllNotificationsLoaded && !this.uiFlags.isFetching; }, showEmptyState() { - return !this.uiFlags.isFetching && !this.records.length; + return !this.uiFlags.isFetching && !this.notifications.length; }, showEndOfListMessage() { - return this.showEndOfList && this.records.length; + return this.showEndOfList && this.notifications.length; }, }, mounted() { - this.$store.dispatch('notifications/clear'); - this.$store.dispatch('notifications/index', { page: 1 }); + this.setSavedFilter(); + this.fetchNotifications(); }, methods: { + fetchNotifications() { + this.page = 1; + this.$store.dispatch('notifications/clear'); + const filter = this.inboxFilters; + + this.$store.dispatch('notifications/index', filter); + }, redirectToInbox() { if (!this.conversationId) return; - if (this.$route.name === 'inbox-view') return; - this.$router.push({ name: 'inbox-view' }); + if (this.$route.name === 'inbox_view') return; + this.$router.push({ name: 'inbox_view' }); }, loadMoreNotifications() { if (this.uiFlags.isAllNotificationsLoaded) return; - this.$store.dispatch('notifications/index', { page: this.page + 1 }); + this.$store.dispatch('notifications/index', { + page: this.page + 1, + status: this.status, + type: this.type, + sortOrder: this.sortOrder, + }); this.page += 1; }, markNotificationAsRead(notification) { @@ -148,6 +176,25 @@ export default { this.showAlert(this.$t('INBOX.ALERTS.DELETE')); }); }, + onFilterChange(option) { + if (option.type === wootConstants.INBOX_FILTER_TYPE.STATUS) { + this.status = option.selected ? option.key : ''; + } + if (option.type === wootConstants.INBOX_FILTER_TYPE.TYPE) { + this.type = option.selected ? option.key : ''; + } + if (option.type === wootConstants.INBOX_FILTER_TYPE.SORT_ORDER) { + this.sortOrder = option.key; + } + this.fetchNotifications(); + }, + setSavedFilter() { + const { inbox_filter_by: filterBy = {} } = this.uiSettings; + const { status, type, sort_by: sortBy } = filterBy; + this.status = status; + this.type = type; + this.sortOrder = sortBy || wootConstants.INBOX_SORT_BY.NEWEST; + }, }, }; diff --git a/app/javascript/dashboard/routes/dashboard/inbox/InboxView.vue b/app/javascript/dashboard/routes/dashboard/inbox/InboxView.vue index 23f32cbbe..edc36da15 100644 --- a/app/javascript/dashboard/routes/dashboard/inbox/InboxView.vue +++ b/app/javascript/dashboard/routes/dashboard/inbox/InboxView.vue @@ -148,6 +148,7 @@ export default { name: 'home', }); } + this.$store.dispatch('agents/get'); }, methods: { async fetchConversationById() { diff --git a/app/javascript/dashboard/routes/dashboard/inbox/components/InboxCard.vue b/app/javascript/dashboard/routes/dashboard/inbox/components/InboxCard.vue index c514f24f7..8e100f7b5 100644 --- a/app/javascript/dashboard/routes/dashboard/inbox/components/InboxCard.vue +++ b/app/javascript/dashboard/routes/dashboard/inbox/components/InboxCard.vue @@ -29,7 +29,8 @@ v-if="assigneeMeta" :src="assigneeMeta.thumbnail" :username="assigneeMeta.name" - size="20px" + size="16px" + class="relative bottom-0.5" />
{{ option.name }} @@ -74,19 +73,19 @@ >