Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68ed5a4b4f | ||
|
|
7ecb993d06 | ||
|
|
c00a157c03 | ||
|
|
3750689bee | ||
|
|
0511bf4ba6 | ||
|
|
9f50638a40 | ||
|
|
f5ac272e3b | ||
|
|
2ba7318817 | ||
|
|
412303d1cd | ||
|
|
47b9399208 | ||
|
|
0830ad78b6 | ||
|
|
0b8df762f9 | ||
|
|
f24c87f9b2 | ||
|
|
f62a4170b6 | ||
|
|
2f090dbc61 | ||
|
|
ec17682d7a | ||
|
|
7861ce8c47 | ||
|
|
e5de41fea4 | ||
|
|
adb0e94cbd | ||
|
|
42fb083491 | ||
|
|
9101e1e221 | ||
|
|
91bac16c29 | ||
|
|
97699f3087 | ||
|
|
1242cfd704 | ||
|
|
6963c668bf |
+3
-11
@@ -19,19 +19,13 @@ jobs:
|
||||
steps:
|
||||
- checkout
|
||||
- node/install:
|
||||
node-version: '23.7'
|
||||
node-version: '20.12'
|
||||
- node/install-pnpm
|
||||
- node/install-packages:
|
||||
pkg-manager: pnpm
|
||||
override-ci-command: pnpm i
|
||||
- run: node --version
|
||||
- run: pnpm --version
|
||||
- run:
|
||||
name: Add PostgreSQL repository and update
|
||||
command: |
|
||||
sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
|
||||
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
|
||||
sudo apt-get update -y
|
||||
|
||||
- run:
|
||||
name: Install System Dependencies
|
||||
@@ -40,9 +34,7 @@ jobs:
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt-get install -y \
|
||||
libpq-dev \
|
||||
redis-server \
|
||||
postgresql-common \
|
||||
postgresql-16 \
|
||||
postgresql-16-pgvector \
|
||||
postgresql \
|
||||
build-essential \
|
||||
git \
|
||||
curl \
|
||||
@@ -82,7 +74,7 @@ jobs:
|
||||
source ~/.rvm/scripts/rvm
|
||||
rvm install "3.3.3"
|
||||
rvm use 3.3.3 --default
|
||||
gem install bundler -v 2.5.16
|
||||
gem install bundler
|
||||
|
||||
- run:
|
||||
name: Install Application Dependencies
|
||||
|
||||
@@ -51,12 +51,9 @@ exclude_patterns:
|
||||
- 'app/javascript/dashboard/routes/dashboard/settings/automation/constants.js'
|
||||
- 'app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.js'
|
||||
- 'app/javascript/dashboard/routes/dashboard/settings/reports/constants.js'
|
||||
- 'app/javascript/dashboard/store/captain/storeFactory.js'
|
||||
- 'app/javascript/dashboard/i18n/index.js'
|
||||
- 'app/javascript/widget/i18n/index.js'
|
||||
- 'app/javascript/survey/i18n/index.js'
|
||||
- 'app/javascript/shared/constants/locales.js'
|
||||
- 'app/javascript/dashboard/helper/specs/macrosFixtures.js'
|
||||
- 'app/javascript/dashboard/routes/dashboard/settings/macros/constants.js'
|
||||
- '**/fixtures/**'
|
||||
- '**/*/fixtures.js'
|
||||
|
||||
@@ -5,30 +5,30 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
base:
|
||||
base:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: .devcontainer/Dockerfile.base
|
||||
args:
|
||||
VARIANT: 'ubuntu-22.04'
|
||||
NODE_VERSION: '23.7.0'
|
||||
RUBY_VERSION: '3.3.3'
|
||||
VARIANT: "ubuntu-22.04"
|
||||
NODE_VERSION: "20.9.0"
|
||||
RUBY_VERSION: "3.3.3"
|
||||
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
|
||||
USER_UID: '1000'
|
||||
USER_GID: '1000'
|
||||
USER_UID: "1000"
|
||||
USER_GID: "1000"
|
||||
image: base:latest
|
||||
|
||||
|
||||
app:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: .devcontainer/Dockerfile
|
||||
args:
|
||||
VARIANT: 'ubuntu-22.04'
|
||||
NODE_VERSION: '23.7.0'
|
||||
RUBY_VERSION: '3.3.3'
|
||||
VARIANT: "ubuntu-22.04"
|
||||
NODE_VERSION: "20.9.0"
|
||||
RUBY_VERSION: "3.3.3"
|
||||
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
|
||||
USER_UID: '1000'
|
||||
USER_GID: '1000'
|
||||
USER_UID: "1000"
|
||||
USER_GID: "1000"
|
||||
|
||||
volumes:
|
||||
- ..:/workspace:cached
|
||||
@@ -40,7 +40,7 @@ services:
|
||||
network_mode: service:db
|
||||
|
||||
db:
|
||||
image: pgvector/pgvector:pg16
|
||||
image: postgres:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
|
||||
+6
-28
@@ -4,8 +4,6 @@ module.exports = {
|
||||
'prettier',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'plugin:vitest-globals/recommended',
|
||||
// use recommended-legacy when upgrading the plugin to v4
|
||||
'plugin:@intlify/vue-i18n/recommended',
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
@@ -14,20 +12,6 @@ module.exports = {
|
||||
'vitest-globals/env': true,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.story.vue'],
|
||||
rules: {
|
||||
'vue/no-undef-components': [
|
||||
'error',
|
||||
{
|
||||
ignorePatterns: ['Variant', 'Story'],
|
||||
},
|
||||
],
|
||||
// Story files can have static strings, it doesn't need to handle i18n always.
|
||||
'vue/no-bare-strings-in-template': 'off',
|
||||
'no-console': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
plugins: ['html', 'prettier'],
|
||||
parserOptions: {
|
||||
@@ -231,18 +215,6 @@ module.exports = {
|
||||
'vue/singleline-html-element-content-newline': 'off',
|
||||
'import/extensions': ['off'],
|
||||
'no-console': 'error',
|
||||
'@intlify/vue-i18n/no-dynamic-keys': 'warn',
|
||||
'@intlify/vue-i18n/no-unused-keys': [
|
||||
'warn',
|
||||
{
|
||||
extensions: ['.js', '.vue'],
|
||||
},
|
||||
],
|
||||
},
|
||||
settings: {
|
||||
'vue-i18n': {
|
||||
localeDir: './app/javascript/*/i18n/**.json',
|
||||
},
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
@@ -251,5 +223,11 @@ module.exports = {
|
||||
globals: {
|
||||
bus: true,
|
||||
vi: true,
|
||||
// beforeEach: true,
|
||||
// afterEach: true,
|
||||
// test: true,
|
||||
// describe: true,
|
||||
// it: true,
|
||||
// expect: true,
|
||||
},
|
||||
};
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 285 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 966 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 934 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 40 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 45 KiB |
@@ -23,10 +23,12 @@ jobs:
|
||||
bundler-cache: true
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.3.0
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 23
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install pnpm dependencies
|
||||
|
||||
@@ -16,7 +16,7 @@ on:
|
||||
|
||||
jobs:
|
||||
nightly:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
|
||||
- name: get installer
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
# #
|
||||
# # This action will publish Chatwoot EE docker image.
|
||||
# # This is set to run against merges to develop, master
|
||||
# # and when tags are created.
|
||||
# #
|
||||
|
||||
name: Publish Chatwoot EE docker images
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- master
|
||||
tags:
|
||||
- v*
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
DOCKER_REPO: chatwoot/chatwoot
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-22.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
env:
|
||||
GIT_REF: ${{ github.head_ref || github.ref_name }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Set Chatwoot edition
|
||||
run: |
|
||||
echo -en '\nENV CW_EDITION="ee"' >> docker/Dockerfile
|
||||
|
||||
- name: Set Docker Tags
|
||||
run: |
|
||||
SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g')
|
||||
if [ "${{ github.ref_name }}" = "master" ]; then
|
||||
echo "DOCKER_TAG=${DOCKER_REPO}:latest" >> $GITHUB_ENV
|
||||
else
|
||||
echo "DOCKER_TAG=${DOCKER_REPO}:${SANITIZED_REF}" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
|
||||
outputs: type=image,name=${{ env.DOCKER_REPO }},push-by-digest=true,name-canonical=true,push=true
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p ${{ runner.temp }}/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: ${{ runner.temp }}/digests
|
||||
env:
|
||||
GIT_REF: ${{ github.head_ref || github.ref_name }}
|
||||
run: |
|
||||
SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g')
|
||||
if [ "${{ github.ref_name }}" = "master" ]; then
|
||||
TAG="${DOCKER_REPO}:latest"
|
||||
else
|
||||
TAG="${DOCKER_REPO}:${SANITIZED_REF}"
|
||||
fi
|
||||
|
||||
docker buildx imagetools create -t $TAG \
|
||||
$(printf '${{ env.DOCKER_REPO }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
env:
|
||||
GIT_REF: ${{ github.head_ref || github.ref_name }}
|
||||
run: |
|
||||
SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g')
|
||||
if [ "${{ github.ref_name }}" = "master" ]; then
|
||||
TAG="${DOCKER_REPO}:latest"
|
||||
else
|
||||
TAG="${DOCKER_REPO}:${SANITIZED_REF}"
|
||||
fi
|
||||
|
||||
docker buildx imagetools inspect $TAG
|
||||
@@ -5,7 +5,6 @@
|
||||
# #
|
||||
|
||||
name: Publish Chatwoot CE docker images
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -15,30 +14,20 @@ on:
|
||||
- v*
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
DOCKER_REPO: chatwoot/chatwoot
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-22.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GIT_REF: ${{ github.head_ref || github.ref_name }}
|
||||
GIT_REF: ${{ github.head_ref || github.ref_name }} # ref_name to get tags/branches
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Strip enterprise code
|
||||
run: |
|
||||
@@ -49,97 +38,26 @@ jobs:
|
||||
run: |
|
||||
echo -en '\nENV CW_EDITION="ce"' >> docker/Dockerfile
|
||||
|
||||
- name: Set Docker Tags
|
||||
- name: set docker tag
|
||||
run: |
|
||||
SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g')
|
||||
if [ "${{ github.ref_name }}" = "master" ]; then
|
||||
echo "DOCKER_TAG=${DOCKER_REPO}:latest-ce" >> $GITHUB_ENV
|
||||
else
|
||||
echo "DOCKER_TAG=${DOCKER_REPO}:${SANITIZED_REF}-ce" >> $GITHUB_ENV
|
||||
fi
|
||||
echo "DOCKER_TAG=chatwoot/chatwoot:$GIT_REF-ce" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: replace docker tag if master
|
||||
if: github.ref_name == 'master'
|
||||
run: |
|
||||
echo "DOCKER_TAG=chatwoot/chatwoot:latest-ce" >> $GITHUB_ENV
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
|
||||
outputs: type=image,name=${{ env.DOCKER_REPO }},push-by-digest=true,name-canonical=true,push=true
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p ${{ runner.temp }}/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: ${{ runner.temp }}/digests
|
||||
env:
|
||||
GIT_REF: ${{ github.head_ref || github.ref_name }}
|
||||
run: |
|
||||
SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g')
|
||||
if [ "${{ github.ref_name }}" = "master" ]; then
|
||||
TAG="${DOCKER_REPO}:latest-ce"
|
||||
else
|
||||
TAG="${DOCKER_REPO}:${SANITIZED_REF}-ce"
|
||||
fi
|
||||
|
||||
docker buildx imagetools create -t $TAG \
|
||||
$(printf '${{ env.DOCKER_REPO }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
env:
|
||||
GIT_REF: ${{ github.head_ref || github.ref_name }}
|
||||
run: |
|
||||
SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g')
|
||||
if [ "${{ github.ref_name }}" = "master" ]; then
|
||||
TAG="${DOCKER_REPO}:latest-ce"
|
||||
else
|
||||
TAG="${DOCKER_REPO}:${SANITIZED_REF}-ce"
|
||||
fi
|
||||
|
||||
docker buildx imagetools inspect $TAG
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ env.DOCKER_TAG }}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
# #
|
||||
# # This action will strip the enterprise folder
|
||||
# # and run the spec.
|
||||
# # This is set to run against every PR.
|
||||
# #
|
||||
|
||||
name: Run Chatwoot CE spec
|
||||
on:
|
||||
push:
|
||||
@@ -12,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
services:
|
||||
postgres:
|
||||
image: pgvector/pgvector:pg15
|
||||
image: postgres:15.3
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: ''
|
||||
@@ -38,6 +44,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
@@ -47,7 +54,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 23
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install pnpm dependencies
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# #
|
||||
# # This workflow will run specs related to response bot
|
||||
# # This can only be activated in installations Where vector extension is available.
|
||||
# #
|
||||
|
||||
name: Run Response Bot spec
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- master
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-20.04
|
||||
services:
|
||||
postgres:
|
||||
image: ankane/pgvector
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: ""
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
ports:
|
||||
- 5432:5432
|
||||
# needed because the postgres container does not provide a healthcheck
|
||||
# tmpfs makes DB faster by using RAM
|
||||
options: >-
|
||||
--mount type=tmpfs,destination=/var/lib/postgresql/data
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
options: --entrypoint redis-server
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 9.3.0
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
|
||||
- name: pnpm
|
||||
run: pnpm install
|
||||
|
||||
- name: Create database
|
||||
run: bundle exec rake db:create
|
||||
|
||||
- name: Seed database
|
||||
run: bundle exec rake db:schema:load
|
||||
|
||||
- name: Enable ResponseBotService in installation
|
||||
run: RAILS_ENV=test bundle exec rails runner "Features::ResponseBotService.new.enable_in_installation"
|
||||
|
||||
# Run Response Bot specs
|
||||
- name: Run backend tests
|
||||
run: |
|
||||
bundle exec rspec \
|
||||
spec/enterprise/controllers/api/v1/accounts/response_sources_controller_spec.rb \
|
||||
spec/enterprise/services/enterprise/message_templates/response_bot_service_spec.rb \
|
||||
spec/enterprise/controllers/enterprise/api/v1/accounts/inboxes_controller_spec.rb:47 \
|
||||
spec/enterprise/jobs/enterprise/account/conversations_resolution_scheduler_job_spec.rb \
|
||||
--profile=10 \
|
||||
--format documentation
|
||||
|
||||
- name: Upload rails log folder
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: rails-log-folder
|
||||
path: log
|
||||
@@ -19,11 +19,13 @@ jobs:
|
||||
with:
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 9.3.0
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 23
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: pnpm
|
||||
@@ -37,7 +39,7 @@ jobs:
|
||||
- name: setup env
|
||||
run: |
|
||||
cp .env.example .env
|
||||
|
||||
|
||||
- name: Run asset compile
|
||||
run: bundle exec rake assets:precompile
|
||||
env:
|
||||
@@ -45,3 +47,5 @@ jobs:
|
||||
|
||||
- name: Size Check
|
||||
run: pnpm run size
|
||||
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
name: Test Docker Build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test-build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-22.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: false
|
||||
load: false
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -73,7 +73,6 @@ test/cypress/videos/*
|
||||
|
||||
#ignore files under .vscode directory
|
||||
.vscode
|
||||
.cursor
|
||||
|
||||
# yalc for local testing
|
||||
.yalc
|
||||
|
||||
+8
-8
@@ -1,11 +1,11 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
# #!/bin/sh
|
||||
# . "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
# lint js and vue files
|
||||
npx --no-install lint-staged
|
||||
# # lint js and vue files
|
||||
# npx --no-install lint-staged
|
||||
|
||||
# lint only staged ruby files
|
||||
git diff --name-only --cached | xargs ls -1 2>/dev/null | grep '\.rb$' | xargs bundle exec rubocop --force-exclusion -a
|
||||
# # lint only staged ruby files
|
||||
# git diff --name-only --cached | xargs ls -1 2>/dev/null | grep '\.rb$' | xargs bundle exec rubocop --force-exclusion -a
|
||||
|
||||
# stage rubocop changes to files
|
||||
git diff --name-only --cached | xargs git add
|
||||
# # stage rubocop changes to files
|
||||
# git diff --name-only --cached | xargs git add
|
||||
|
||||
@@ -283,4 +283,3 @@ exclude:
|
||||
- 'app/javascript/widget/assets/scss/sdk.css'
|
||||
- 'app/assets/stylesheets/administrate/reset/_normalize.scss'
|
||||
- 'app/javascript/shared/assets/stylesheets/*.scss'
|
||||
- 'app/javascript/dashboard/assets/scss/_woot.scss'
|
||||
|
||||
@@ -49,7 +49,7 @@ gem 'aws-sdk-s3', require: false
|
||||
# original gem isn't maintained actively
|
||||
# we wanted updated version of faraday which is a dependency for slack-ruby-client
|
||||
gem 'azure-storage-blob', git: 'https://github.com/chatwoot/azure-storage-ruby', branch: 'chatwoot', require: false
|
||||
gem 'google-cloud-storage', '>= 1.48.0', require: false
|
||||
gem 'google-cloud-storage', require: false
|
||||
gem 'image_processing'
|
||||
|
||||
##-- gems for database --#
|
||||
@@ -94,7 +94,7 @@ gem 'twitty', '~> 0.1.5'
|
||||
# facebook client
|
||||
gem 'koala'
|
||||
# slack client
|
||||
gem 'slack-ruby-client', '~> 2.5.2'
|
||||
gem 'slack-ruby-client', '~> 2.2.0'
|
||||
# for dialogflow integrations
|
||||
gem 'google-cloud-dialogflow-v2', '>= 0.24.0'
|
||||
gem 'grpc'
|
||||
@@ -138,7 +138,9 @@ gem 'procore-sift'
|
||||
# parse email
|
||||
gem 'email_reply_trimmer'
|
||||
|
||||
gem 'html2text'
|
||||
# TODO: we might have to fork this gem since 0.3.1 has hard depency on nokogir 1.10.
|
||||
# and this gem hasn't been updated for a while.
|
||||
gem 'html2text', git: 'https://github.com/chatwoot/html2text_ruby', branch: 'chatwoot'
|
||||
|
||||
# to calculate working hours
|
||||
gem 'working_hours'
|
||||
@@ -173,20 +175,12 @@ gem 'pgvector'
|
||||
# Convert Website HTML to Markdown
|
||||
gem 'reverse_markdown'
|
||||
|
||||
gem 'iso-639'
|
||||
gem 'ruby-openai'
|
||||
|
||||
gem 'shopify_api'
|
||||
|
||||
### Gems required only in specific deployment environments ###
|
||||
##############################################################
|
||||
|
||||
group :production do
|
||||
# we dont want request timing out in development while using byebug
|
||||
gem 'rack-timeout'
|
||||
# for heroku autoscaling
|
||||
gem 'judoscale-rails', require: false
|
||||
gem 'judoscale-sidekiq', require: false
|
||||
end
|
||||
|
||||
group :development do
|
||||
|
||||
+107
-137
@@ -22,73 +22,81 @@ GIT
|
||||
devise (>= 4.0.0, < 5.0.0)
|
||||
railties (>= 5.0.0, < 8.0.0)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/chatwoot/html2text_ruby
|
||||
revision: cdbdbbbf898d846d0136d69d688a003c6b26074b
|
||||
branch: chatwoot
|
||||
specs:
|
||||
html2text (0.3.1)
|
||||
nokogiri (>= 1.13.6)
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (7.0.8.7)
|
||||
actionpack (= 7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
actioncable (7.0.8.4)
|
||||
actionpack (= 7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (7.0.8.7)
|
||||
actionpack (= 7.0.8.7)
|
||||
activejob (= 7.0.8.7)
|
||||
activerecord (= 7.0.8.7)
|
||||
activestorage (= 7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
actionmailbox (7.0.8.4)
|
||||
actionpack (= 7.0.8.4)
|
||||
activejob (= 7.0.8.4)
|
||||
activerecord (= 7.0.8.4)
|
||||
activestorage (= 7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
mail (>= 2.7.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
actionmailer (7.0.8.7)
|
||||
actionpack (= 7.0.8.7)
|
||||
actionview (= 7.0.8.7)
|
||||
activejob (= 7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
actionmailer (7.0.8.4)
|
||||
actionpack (= 7.0.8.4)
|
||||
actionview (= 7.0.8.4)
|
||||
activejob (= 7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (7.0.8.7)
|
||||
actionview (= 7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
actionpack (7.0.8.4)
|
||||
actionview (= 7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
rack (~> 2.0, >= 2.2.4)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (7.0.8.7)
|
||||
actionpack (= 7.0.8.7)
|
||||
activerecord (= 7.0.8.7)
|
||||
activestorage (= 7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
actiontext (7.0.8.4)
|
||||
actionpack (= 7.0.8.4)
|
||||
activerecord (= 7.0.8.4)
|
||||
activestorage (= 7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
actionview (7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
active_record_query_trace (1.8)
|
||||
activejob (7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
activejob (7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
activerecord (7.0.8.7)
|
||||
activemodel (= 7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
activemodel (7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
activerecord (7.0.8.4)
|
||||
activemodel (= 7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
activerecord-import (1.4.1)
|
||||
activerecord (>= 4.2)
|
||||
activestorage (7.0.8.7)
|
||||
actionpack (= 7.0.8.7)
|
||||
activejob (= 7.0.8.7)
|
||||
activerecord (= 7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
activestorage (7.0.8.4)
|
||||
actionpack (= 7.0.8.4)
|
||||
activejob (= 7.0.8.4)
|
||||
activerecord (= 7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
marcel (~> 1.0)
|
||||
mini_mime (>= 1.1.0)
|
||||
activesupport (7.0.8.7)
|
||||
activesupport (7.0.8.4)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
@@ -178,7 +186,7 @@ GEM
|
||||
database_cleaner-core (2.0.1)
|
||||
datadog-ci (0.8.3)
|
||||
msgpack
|
||||
date (3.4.1)
|
||||
date (3.3.4)
|
||||
ddtrace (1.23.2)
|
||||
datadog-ci (~> 0.8.1)
|
||||
debase-ruby_core_source (= 3.3.1)
|
||||
@@ -201,7 +209,7 @@ GEM
|
||||
devise (> 3.5.2, < 5)
|
||||
rails (>= 4.2.0, < 7.2)
|
||||
diff-lcs (1.5.1)
|
||||
digest-crc (0.6.5)
|
||||
digest-crc (0.6.4)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
docile (1.4.0)
|
||||
domain_name (0.5.20190701)
|
||||
@@ -223,7 +231,6 @@ GEM
|
||||
erubi (1.13.0)
|
||||
et-orbi (1.2.11)
|
||||
tzinfo
|
||||
event_stream_parser (1.0.0)
|
||||
execjs (2.8.1)
|
||||
facebook-messenger (2.0.1)
|
||||
httparty (~> 0.13, >= 0.13.7)
|
||||
@@ -272,45 +279,44 @@ GEM
|
||||
googleauth (~> 1.0)
|
||||
grpc (~> 1.36)
|
||||
geocoder (1.8.1)
|
||||
gli (2.22.2)
|
||||
ostruct
|
||||
gli (2.21.1)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
gmail_xoauth (0.4.3)
|
||||
oauth (>= 0.3.6)
|
||||
google-apis-core (0.15.1)
|
||||
google-apis-core (0.11.0)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (~> 1.9)
|
||||
httpclient (>= 2.8.3, < 3.a)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
mini_mime (~> 1.0)
|
||||
mutex_m
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.a)
|
||||
google-apis-iamcredentials_v1 (0.22.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-apis-storage_v1 (0.47.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-cloud-core (1.7.1)
|
||||
google-cloud-env (>= 1.0, < 3.a)
|
||||
rexml
|
||||
webrick
|
||||
google-apis-iamcredentials_v1 (0.17.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-storage_v1 (0.19.0)
|
||||
google-apis-core (>= 0.9.0, < 2.a)
|
||||
google-cloud-core (1.6.0)
|
||||
google-cloud-env (~> 1.0)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-dialogflow-v2 (0.31.0)
|
||||
gapic-common (>= 0.20.0, < 2.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-location (>= 0.4, < 2.a)
|
||||
google-cloud-env (2.2.1)
|
||||
faraday (>= 1.0, < 3.a)
|
||||
google-cloud-env (1.6.0)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
google-cloud-errors (1.3.1)
|
||||
google-cloud-location (0.6.0)
|
||||
gapic-common (>= 0.20.0, < 2.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-storage (1.52.0)
|
||||
google-cloud-storage (1.44.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
google-apis-core (~> 0.13)
|
||||
google-apis-iamcredentials_v1 (~> 0.18)
|
||||
google-apis-storage_v1 (~> 0.38)
|
||||
google-apis-iamcredentials_v1 (~> 0.1)
|
||||
google-apis-storage_v1 (~> 0.19.0)
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (~> 1.9)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
mini_mime (~> 1.0)
|
||||
google-cloud-translate-v3 (0.10.0)
|
||||
gapic-common (>= 0.20.0, < 2.a)
|
||||
@@ -325,10 +331,10 @@ GEM
|
||||
grpc (~> 1.41)
|
||||
googleapis-common-protos-types (1.14.0)
|
||||
google-protobuf (~> 3.18)
|
||||
googleauth (1.11.2)
|
||||
faraday (>= 1.0, < 3.a)
|
||||
google-cloud-env (~> 2.1)
|
||||
googleauth (1.5.2)
|
||||
faraday (>= 0.17.3, < 3.a)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
@@ -352,11 +358,8 @@ GEM
|
||||
ruby2ruby (~> 2.4)
|
||||
ruby_parser (~> 3.10)
|
||||
hana (1.3.7)
|
||||
hash_diff (1.1.1)
|
||||
hashdiff (1.1.0)
|
||||
hashie (5.0.0)
|
||||
html2text (0.4.0)
|
||||
nokogiri (>= 1.0, < 2.0)
|
||||
http (5.1.1)
|
||||
addressable (~> 2.8)
|
||||
http-cookie (~> 1.0)
|
||||
@@ -370,7 +373,7 @@ GEM
|
||||
mini_mime (>= 1.0.0)
|
||||
multi_xml (>= 0.5.2)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.14.6)
|
||||
i18n (1.14.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
image_processing (1.12.2)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
@@ -378,8 +381,6 @@ GEM
|
||||
io-console (0.6.0)
|
||||
irb (1.7.2)
|
||||
reline (>= 0.3.6)
|
||||
iso-639 (0.3.8)
|
||||
csv
|
||||
jbuilder (2.11.5)
|
||||
actionview (>= 5.0.0)
|
||||
activesupport (>= 5.0.0)
|
||||
@@ -396,13 +397,6 @@ GEM
|
||||
hana (~> 1.3)
|
||||
regexp_parser (~> 2.0)
|
||||
uri_template (~> 0.7)
|
||||
judoscale-rails (1.8.2)
|
||||
judoscale-ruby (= 1.8.2)
|
||||
railties
|
||||
judoscale-ruby (1.8.2)
|
||||
judoscale-sidekiq (1.8.2)
|
||||
judoscale-ruby (= 1.8.2)
|
||||
sidekiq (>= 5.0)
|
||||
jwt (2.8.1)
|
||||
base64
|
||||
kaminari (1.2.2)
|
||||
@@ -451,7 +445,7 @@ GEM
|
||||
activesupport (>= 4)
|
||||
railties (>= 4)
|
||||
request_store (~> 1.0)
|
||||
loofah (2.23.1)
|
||||
loofah (2.22.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
mail (2.8.1)
|
||||
@@ -461,6 +455,7 @@ GEM
|
||||
net-smtp
|
||||
marcel (1.0.4)
|
||||
maxminddb (0.1.22)
|
||||
memoist (0.16.2)
|
||||
meta_request (0.8.3)
|
||||
rack-contrib (>= 1.1, < 3)
|
||||
railties (>= 3.0.0, < 8)
|
||||
@@ -470,22 +465,21 @@ GEM
|
||||
mime-types-data (3.2023.0218.1)
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.8)
|
||||
minitest (5.25.4)
|
||||
mini_portile2 (2.8.7)
|
||||
minitest (5.25.1)
|
||||
mock_redis (0.36.0)
|
||||
ruby2_keywords
|
||||
msgpack (1.7.0)
|
||||
multi_json (1.15.0)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.3.0)
|
||||
mutex_m (0.3.0)
|
||||
neighbor (0.2.3)
|
||||
activerecord (>= 5.2)
|
||||
net-http (0.4.1)
|
||||
uri
|
||||
net-http-persistent (4.0.2)
|
||||
connection_pool (~> 2.2)
|
||||
net-imap (0.4.19)
|
||||
net-imap (0.4.14)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
@@ -501,14 +495,14 @@ GEM
|
||||
newrelic_rpm (9.6.0)
|
||||
base64
|
||||
nio4r (2.7.3)
|
||||
nokogiri (1.18.4)
|
||||
nokogiri (1.16.7)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.4-arm64-darwin)
|
||||
nokogiri (1.16.7-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.4-x86_64-darwin)
|
||||
nokogiri (1.16.7-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.4-x86_64-linux-gnu)
|
||||
nokogiri (1.16.7-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
oauth (1.1.0)
|
||||
oauth-tty (~> 1.0, >= 1.0.1)
|
||||
@@ -523,9 +517,6 @@ GEM
|
||||
rack (>= 1.2, < 4)
|
||||
snaky_hash (~> 2.0)
|
||||
version_gem (~> 1.1)
|
||||
oj (3.16.10)
|
||||
bigdecimal (>= 3.0)
|
||||
ostruct (>= 0.2)
|
||||
omniauth (2.1.2)
|
||||
hashie (>= 3.4.6)
|
||||
rack (>= 2.2.3)
|
||||
@@ -544,7 +535,6 @@ GEM
|
||||
openssl (3.2.0)
|
||||
orm_adapter (0.5.0)
|
||||
os (1.1.4)
|
||||
ostruct (0.6.1)
|
||||
parallel (1.23.0)
|
||||
parser (3.2.2.1)
|
||||
ast (~> 2.4.1)
|
||||
@@ -567,7 +557,7 @@ GEM
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (2.2.13)
|
||||
rack (2.2.9)
|
||||
rack-attack (6.7.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-contrib (2.5.0)
|
||||
@@ -584,30 +574,30 @@ GEM
|
||||
rack-test (2.1.0)
|
||||
rack (>= 1.3)
|
||||
rack-timeout (0.6.3)
|
||||
rails (7.0.8.7)
|
||||
actioncable (= 7.0.8.7)
|
||||
actionmailbox (= 7.0.8.7)
|
||||
actionmailer (= 7.0.8.7)
|
||||
actionpack (= 7.0.8.7)
|
||||
actiontext (= 7.0.8.7)
|
||||
actionview (= 7.0.8.7)
|
||||
activejob (= 7.0.8.7)
|
||||
activemodel (= 7.0.8.7)
|
||||
activerecord (= 7.0.8.7)
|
||||
activestorage (= 7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
rails (7.0.8.4)
|
||||
actioncable (= 7.0.8.4)
|
||||
actionmailbox (= 7.0.8.4)
|
||||
actionmailer (= 7.0.8.4)
|
||||
actionpack (= 7.0.8.4)
|
||||
actiontext (= 7.0.8.4)
|
||||
actionview (= 7.0.8.4)
|
||||
activejob (= 7.0.8.4)
|
||||
activemodel (= 7.0.8.4)
|
||||
activerecord (= 7.0.8.4)
|
||||
activestorage (= 7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 7.0.8.7)
|
||||
railties (= 7.0.8.4)
|
||||
rails-dom-testing (2.2.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.6.1)
|
||||
rails-html-sanitizer (1.6.0)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||
railties (7.0.8.7)
|
||||
actionpack (= 7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
nokogiri (~> 1.14)
|
||||
railties (7.0.8.4)
|
||||
actionpack (= 7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
method_source
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0)
|
||||
@@ -643,7 +633,8 @@ GEM
|
||||
retriable (3.1.2)
|
||||
reverse_markdown (2.1.1)
|
||||
nokogiri
|
||||
rexml (3.3.9)
|
||||
rexml (3.3.6)
|
||||
strscan
|
||||
rspec-core (3.13.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-expectations (3.13.2)
|
||||
@@ -687,10 +678,6 @@ GEM
|
||||
rubocop-rspec (2.21.0)
|
||||
rubocop (~> 1.33)
|
||||
rubocop-capybara (~> 2.17)
|
||||
ruby-openai (7.3.1)
|
||||
event_stream_parser (>= 0.3.0, < 2.0.0)
|
||||
faraday (>= 1)
|
||||
faraday-multipart (>= 1)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-vips (2.1.4)
|
||||
ffi (~> 1.12)
|
||||
@@ -717,7 +704,6 @@ GEM
|
||||
parser
|
||||
scss_lint (0.60.0)
|
||||
sass (~> 3.5, >= 3.5.5)
|
||||
securerandom (0.4.1)
|
||||
seed_dump (3.3.1)
|
||||
activerecord (>= 4)
|
||||
activesupport (>= 4)
|
||||
@@ -732,17 +718,6 @@ GEM
|
||||
sentry-ruby (~> 5.19.0)
|
||||
sidekiq (>= 3.0)
|
||||
sexp_processor (4.17.0)
|
||||
shopify_api (14.8.0)
|
||||
activesupport
|
||||
concurrent-ruby
|
||||
hash_diff
|
||||
httparty
|
||||
jwt
|
||||
oj
|
||||
openssl
|
||||
securerandom
|
||||
sorbet-runtime
|
||||
zeitwerk (~> 2.5)
|
||||
shoulda-matchers (5.3.0)
|
||||
activesupport (>= 5.2.0)
|
||||
sidekiq (7.3.1)
|
||||
@@ -765,17 +740,15 @@ GEM
|
||||
json (>= 1.8, < 3)
|
||||
simplecov-html (~> 0.10.0)
|
||||
simplecov-html (0.10.2)
|
||||
slack-ruby-client (2.5.2)
|
||||
slack-ruby-client (2.2.0)
|
||||
faraday (>= 2.0)
|
||||
faraday-mashify
|
||||
faraday-multipart
|
||||
gli
|
||||
hashie
|
||||
logger
|
||||
snaky_hash (2.0.1)
|
||||
hashie
|
||||
version_gem (~> 1.1, >= 1.1.1)
|
||||
sorbet-runtime (0.5.11934)
|
||||
spring (4.1.1)
|
||||
spring-watcher-listen (2.1.0)
|
||||
listen (>= 2.7, < 4.0)
|
||||
@@ -791,6 +764,7 @@ GEM
|
||||
stackprof (0.2.25)
|
||||
statsd-ruby (1.5.0)
|
||||
stripe (8.5.0)
|
||||
strscan (3.1.0)
|
||||
telephone_number (1.4.20)
|
||||
test-prof (1.2.1)
|
||||
thor (1.3.1)
|
||||
@@ -798,7 +772,7 @@ GEM
|
||||
time_diff (0.3.0)
|
||||
activesupport
|
||||
i18n
|
||||
timeout (0.4.3)
|
||||
timeout (0.4.1)
|
||||
trailblazer-option (0.1.2)
|
||||
twilio-ruby (5.77.0)
|
||||
faraday (>= 0.9, < 3.0)
|
||||
@@ -818,7 +792,7 @@ GEM
|
||||
unf_ext (0.0.8.2)
|
||||
unicode-display_width (2.4.2)
|
||||
uniform_notifier (1.16.0)
|
||||
uri (1.0.3)
|
||||
uri (0.13.0)
|
||||
uri_template (0.7.0)
|
||||
valid_email2 (5.2.6)
|
||||
activemodel (>= 3.2)
|
||||
@@ -845,6 +819,7 @@ GEM
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webrick (1.8.2)
|
||||
websocket-driver (0.7.6)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
@@ -906,21 +881,18 @@ DEPENDENCIES
|
||||
geocoder
|
||||
gmail_xoauth
|
||||
google-cloud-dialogflow-v2 (>= 0.24.0)
|
||||
google-cloud-storage (>= 1.48.0)
|
||||
google-cloud-storage
|
||||
google-cloud-translate-v3 (>= 0.7.0)
|
||||
groupdate
|
||||
grpc
|
||||
haikunator
|
||||
hairtrigger
|
||||
hashie
|
||||
html2text
|
||||
html2text!
|
||||
image_processing
|
||||
iso-639
|
||||
jbuilder
|
||||
json_refs
|
||||
json_schemer
|
||||
judoscale-rails
|
||||
judoscale-sidekiq
|
||||
jwt
|
||||
kaminari
|
||||
koala
|
||||
@@ -963,19 +935,17 @@ DEPENDENCIES
|
||||
rubocop-performance
|
||||
rubocop-rails
|
||||
rubocop-rspec
|
||||
ruby-openai
|
||||
scout_apm
|
||||
scss_lint
|
||||
seed_dump
|
||||
sentry-rails (>= 5.19.0)
|
||||
sentry-ruby
|
||||
sentry-sidekiq (>= 5.19.0)
|
||||
shopify_api
|
||||
shoulda-matchers
|
||||
sidekiq (>= 7.3.1)
|
||||
sidekiq-cron (>= 1.12.0)
|
||||
simplecov (= 0.17.1)
|
||||
slack-ruby-client (~> 2.5.2)
|
||||
slack-ruby-client (~> 2.2.0)
|
||||
spring
|
||||
spring-watcher-listen
|
||||
squasher
|
||||
|
||||
@@ -6,7 +6,7 @@ RAILS_ENV ?= development
|
||||
setup:
|
||||
gem install bundler
|
||||
bundle install
|
||||
pnpm install
|
||||
yarn install
|
||||
|
||||
db_create:
|
||||
RAILS_ENV=$(RAILS_ENV) bundle exec rails db:create
|
||||
@@ -30,7 +30,7 @@ server:
|
||||
RAILS_ENV=$(RAILS_ENV) bundle exec rails server -b 0.0.0.0 -p 3000
|
||||
|
||||
burn:
|
||||
bundle && pnpm install
|
||||
bundle && yarn
|
||||
|
||||
run:
|
||||
@if [ -f ./.overmind.sock ]; then \
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
<img src="./.github/screenshots/header.png#gh-light-mode-only" width="100%" alt="Header light mode"/>
|
||||
<img src="./.github/screenshots/header-dark.png#gh-dark-mode-only" width="100%" alt="Header dark mode"/>
|
||||
## 🚨 Note: This branch is unstable. For the stable branch's source code, please use the branch [3.x](https://github.com/chatwoot/chatwoot/tree/3.x)
|
||||
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/2246121/282256557-1570674b-d142-4198-9740-69404cc6a339.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
|
||||
<img src="https://user-images.githubusercontent.com/2246121/282256632-87f6a01b-6467-4e0e-8a93-7bbf66d03a17.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
|
||||
|
||||
___
|
||||
|
||||
# Chatwoot
|
||||
|
||||
The modern customer support platform, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.
|
||||
Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.
|
||||
<p>
|
||||
<a href="https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master" alt="Deploy to Heroku">
|
||||
<img width="150" alt="Deploy" src="https://www.herokucdn.com/deploy/button.svg"/>
|
||||
</a>
|
||||
<a href="https://marketplace.digitalocean.com/apps/chatwoot?refcode=f2238426a2a8" alt="Deploy to DigitalOcean">
|
||||
<img width="200" alt="Deploy to DO" src="https://www.deploytodo.com/do-btn-blue.svg"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="https://codeclimate.com/github/chatwoot/chatwoot/maintainability"><img src="https://api.codeclimate.com/v1/badges/e6e3f66332c91e5a4c0c/maintainability" alt="Maintainability"></a>
|
||||
@@ -20,71 +31,41 @@ The modern customer support platform, an open-source alternative to Intercom, Ze
|
||||
<a href="https://artifacthub.io/packages/helm/chatwoot/chatwoot"><img src="https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/artifact-hub" alt="Artifact HUB"></a>
|
||||
</p>
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/2246121/282255783-ee8a50c9-f42d-4752-8201-2d59965a663d.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
|
||||
<img src="https://user-images.githubusercontent.com/2246121/282255784-3d1994ec-d895-4ff5-ac68-d819987e1869.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
|
||||
|
||||
<p>
|
||||
<a href="https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master" alt="Deploy to Heroku">
|
||||
<img width="150" alt="Deploy" src="https://www.herokucdn.com/deploy/button.svg"/>
|
||||
</a>
|
||||
<a href="https://marketplace.digitalocean.com/apps/chatwoot?refcode=f2238426a2a8" alt="Deploy to DigitalOcean">
|
||||
<img width="200" alt="Deploy to DO" src="https://www.deploytodo.com/do-btn-blue.svg"/>
|
||||
</a>
|
||||
</p>
|
||||
Chatwoot is an open-source, self-hosted customer engagement suite. Chatwoot lets you view and manage your customer data, communicate with them irrespective of which medium they use, and re-engage them based on their profile.
|
||||
|
||||
<img src="./.github/screenshots/dashboard.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
|
||||
<img src="./.github/screenshots/dashboard-dark.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
|
||||
## Features
|
||||
|
||||
---
|
||||
Chatwoot supports the following conversation channels:
|
||||
|
||||
Chatwoot is the modern, open-source, and self-hosted customer support platform designed to help businesses deliver exceptional customer support experience. Built for scale and flexibility, Chatwoot gives you full control over your customer data while providing powerful tools to manage conversations across channels.
|
||||
- **Website**: Talk to your customers using our live chat widget and make use of our SDK to identify a user and provide contextual support.
|
||||
- **Facebook**: Connect your Facebook pages and start replying to the direct messages to your page.
|
||||
- **Instagram**: Connect your Instagram profile and start replying to the direct messages.
|
||||
- **Twitter**: Connect your Twitter profiles and reply to direct messages or the tweets where you are mentioned.
|
||||
- **Telegram**: Connect your Telegram bot and reply to your customers right from a single dashboard.
|
||||
- **WhatsApp**: Connect your WhatsApp business account and manage the conversation in Chatwoot.
|
||||
- **Line**: Connect your Line account and manage the conversations in Chatwoot.
|
||||
- **SMS**: Connect your Twilio SMS account and reply to the SMS queries in Chatwoot.
|
||||
- **API Channel**: Build custom communication channels using our API channel.
|
||||
- **Email**: Forward all your email queries to Chatwoot and view it in our integrated dashboard.
|
||||
|
||||
### ✨ Captain – AI Agent for Support
|
||||
And more.
|
||||
|
||||
Supercharge your support with Captain, Chatwoot’s AI agent. Captain helps automate responses, handle common queries, and reduce agent workload—ensuring customers get instant, accurate answers. With Captain, your team can focus on complex conversations while routine questions are resolved automatically. Read more about Captain [here](https://chwt.app/captain-docs).
|
||||
|
||||
### 💬 Omnichannel Support Desk
|
||||
|
||||
Chatwoot centralizes all customer conversations into one powerful inbox, no matter where your customers reach out from. It supports live chat on your website, email, Facebook, Instagram, Twitter, WhatsApp, Telegram, Line, SMS etc.
|
||||
|
||||
### 📚 Help center portal
|
||||
|
||||
Publish help articles, FAQs, and guides through the built-in Help Center Portal. Enable customers to find answers on their own, reduce repetitive queries, and keep your support team focused on more complex issues.
|
||||
|
||||
### 🗂️ Other features
|
||||
|
||||
#### Collaboration & Productivity
|
||||
|
||||
- Private Notes and @mentions for internal team discussions.
|
||||
- Labels to organize and categorize conversations.
|
||||
- Keyboard Shortcuts and a Command Bar for quick navigation.
|
||||
- Canned Responses to reply faster to frequently asked questions.
|
||||
- Auto-Assignment to route conversations based on agent availability.
|
||||
- Multi-lingual Support to serve customers in multiple languages.
|
||||
- Custom Views and Filters for better inbox organization.
|
||||
- Business Hours and Auto-Responders to manage response expectations.
|
||||
- Teams and Automation tools for scaling support workflows.
|
||||
- Agent Capacity Management to balance workload across the team.
|
||||
|
||||
#### Customer Data & Segmentation
|
||||
- Contact Management with profiles and interaction history.
|
||||
- Contact Segments and Notes for targeted communication.
|
||||
- Campaigns to proactively engage customers.
|
||||
- Custom Attributes for storing additional customer data.
|
||||
- Pre-Chat Forms to collect user information before starting conversations.
|
||||
|
||||
#### Integrations
|
||||
- Slack Integration to manage conversations directly from Slack.
|
||||
- Dialogflow Integration for chatbot automation.
|
||||
- Dashboard Apps to embed internal tools within Chatwoot.
|
||||
- Shopify Integration to view and manage customer orders right within Chatwoot.
|
||||
- Use Google Translate to translate messages from your customers in realtime.
|
||||
- Create and manage Linear tickets within Chatwoot.
|
||||
|
||||
#### Reports & Insights
|
||||
- Live View of ongoing conversations for real-time monitoring.
|
||||
- Conversation, Agent, Inbox, Label, and Team Reports for operational visibility.
|
||||
- CSAT Reports to measure customer satisfaction.
|
||||
- Downloadable Reports for offline analysis and reporting.
|
||||
Other features include:
|
||||
|
||||
- **CRM**: Save all your customer information right inside Chatwoot, use contact notes to log emails, phone calls, or meeting notes.
|
||||
- **Custom Attributes**: Define custom attribute attributes to store information about a contact or a conversation and extend the product to match your workflow.
|
||||
- **Shared multi-brand inboxes**: Manage multiple brands or pages using a shared inbox.
|
||||
- **Private notes**: Use @mentions and private notes to communicate internally about a conversation.
|
||||
- **Canned responses (Saved replies)**: Improve the response rate by adding saved replies for frequently asked questions.
|
||||
- **Conversation Labels**: Use conversation labels to create custom workflows.
|
||||
- **Auto assignment**: Chatwoot intelligently assigns a ticket to the agents who have access to the inbox depending on their availability and load.
|
||||
- **Conversation continuity**: If the user has provided an email address through the chat widget, Chatwoot will send an email to the customer under the agent name so that the user can continue the conversation over the email.
|
||||
- **Multi-lingual support**: Chatwoot supports 10+ languages.
|
||||
- **Powerful API & Webhooks**: Extend the capability of the software using Chatwoot’s webhooks and APIs.
|
||||
- **Integrations**: Chatwoot natively integrates with Slack right now. Manage your conversations in Slack without logging into the dashboard.
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -126,15 +107,17 @@ For other supported options, checkout our [deployment page](https://chatwoot.com
|
||||
|
||||
Looking to report a vulnerability? Please refer our [SECURITY.md](./SECURITY.md) file.
|
||||
|
||||
## Community
|
||||
|
||||
## Community? Questions? Support ?
|
||||
|
||||
If you need help or just want to hang out, come, say hi on our [Discord](https://discord.gg/cJXdrwS) server.
|
||||
|
||||
## Contributors
|
||||
|
||||
## Contributors ✨
|
||||
|
||||
Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contributors):
|
||||
|
||||
<a href="https://github.com/chatwoot/chatwoot/graphs/contributors"><img src="https://opencollective.com/chatwoot/contributors.svg?width=890&button=false" /></a>
|
||||
|
||||
|
||||
*Chatwoot* © 2017-2025, Chatwoot Inc - Released under the MIT License.
|
||||
*Chatwoot* © 2017-2024, Chatwoot Inc - Released under the MIT License.
|
||||
|
||||
+4
-4
@@ -2,13 +2,13 @@ Chatwoot is looking forward to working with security researchers worldwide to ke
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
We use Github to track the security issues that affect our project. If you believe you have found a vulnerability, please disclose it via this [form](https://github.com/chatwoot/chatwoot/security/advisories/new). This will enable us to review the vulnerability, fix it promptly, and reward you for your efforts.
|
||||
We use [huntr.dev](https://huntr.dev/) for security issues that affect our project. If you believe you have found a vulnerability, please disclose it via this [form](https://huntr.dev/bounties/disclose). This will enable us to review the vulnerability, fix it promptly, and reward you for your efforts.
|
||||
|
||||
If you have any questions about the process, contact security@chatwoot.com.
|
||||
|
||||
Please try your best to describe a clear and realistic impact for your report, and please don't open any public issues on GitHub or social media; we're doing our best to respond through Github as quickly as possible.
|
||||
Please try your best to describe a clear and realistic impact for your report, and please don't open any public issues on GitHub or social media; we're doing our best to respond through Huntr as quickly as possible.
|
||||
|
||||
> Note: Please use the email for questions related to the process. Disclosures should be done via [Github](https://github.com/chatwoot/chatwoot/security/advisories/new)
|
||||
> Note: Please use the email for questions related to the process. Disclosures should be done via [huntr.dev](https://huntr.dev/)
|
||||
## Supported versions
|
||||
|
||||
| Version | Supported |
|
||||
@@ -48,7 +48,7 @@ We consider the following out of scope, though there may be exceptions.
|
||||
- Brute force attacks
|
||||
- DNSSEC
|
||||
|
||||
If you are unsure about the scope, please create a [report](https://github.com/chatwoot/chatwoot/security/advisories/new).
|
||||
If you are unsure about the scope, please create a [report](https://huntr.dev/repos/chatwoot/chatwoot/).
|
||||
|
||||
|
||||
## Thanks
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
3.2.0
|
||||
3.0.0
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"size": "basic"
|
||||
}
|
||||
},
|
||||
"stack": "heroku-24",
|
||||
"stack": "heroku-20",
|
||||
"image": "heroku/ruby",
|
||||
"addons": [
|
||||
{
|
||||
@@ -58,7 +58,7 @@
|
||||
"plan": "heroku-postgresql:essential-0"
|
||||
}
|
||||
],
|
||||
"stack": "heroku-24",
|
||||
"stack": "heroku-20",
|
||||
"buildpacks": [
|
||||
{
|
||||
"url": "heroku/nodejs"
|
||||
|
||||
@@ -54,7 +54,7 @@ class ContactMergeAction
|
||||
# attributes in base contact are given preference
|
||||
merged_attributes = mergee_contact_attributes.deep_merge(base_contact_attributes)
|
||||
|
||||
@mergee_contact.reload.destroy!
|
||||
@mergee_contact.destroy!
|
||||
Rails.configuration.dispatcher.dispatch(CONTACT_MERGED, Time.zone.now, contact: @base_contact,
|
||||
tokens: [@base_contact.contact_inboxes.filter_map(&:pubsub_token)])
|
||||
@base_contact.update!(merged_attributes)
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
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) {
|
||||
button,
|
||||
input[type="button"],
|
||||
input[type="reset"],
|
||||
input[type="submit"],
|
||||
.button {
|
||||
appearance: none;
|
||||
background-color: $color-woot;
|
||||
border: 0;
|
||||
border-radius: $base-border-radius;
|
||||
color: $white;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
display: inline-block;
|
||||
font-size: $font-size-small;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-weight: $font-weight-medium;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
.icon-container {
|
||||
margin-right: 2px;
|
||||
|
||||
}
|
||||
|
||||
.value-container {
|
||||
|
||||
@@ -64,40 +64,5 @@ class ContactInboxBuilder
|
||||
inbox_id: @inbox.id,
|
||||
source_id: @source_id
|
||||
)
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
Rails.logger.info("[ContactInboxBuilder] RecordNotUnique #{@source_id} #{@contact.id} #{@inbox.id}")
|
||||
update_old_contact_inbox
|
||||
retry
|
||||
end
|
||||
|
||||
def update_old_contact_inbox
|
||||
# The race condition occurs when there’s a contact inbox with the
|
||||
# same source ID but linked to a different contact. This can happen
|
||||
# if the agent updates the contact’s email or phone number, or
|
||||
# if the contact is merged with another.
|
||||
#
|
||||
# We update the old contact inbox source_id to a random value to
|
||||
# avoid disrupting the current flow. However, the root cause of
|
||||
# this issue is a flaw in the contact inbox model design.
|
||||
# Contact inbox is essentially tracking a session and is not
|
||||
# needed for non-live chat channels.
|
||||
raise ActiveRecord::RecordNotUnique unless allowed_channels?
|
||||
|
||||
contact_inbox = ::ContactInbox.find_by(inbox_id: @inbox.id, source_id: @source_id)
|
||||
return if contact_inbox.blank?
|
||||
|
||||
contact_inbox.update!(source_id: new_source_id)
|
||||
end
|
||||
|
||||
def new_source_id
|
||||
if @inbox.whatsapp? || @inbox.sms? || @inbox.twilio?
|
||||
"whatsapp:#{@source_id}#{rand(100)}"
|
||||
else
|
||||
"#{rand(10)}#{@source_id}"
|
||||
end
|
||||
end
|
||||
|
||||
def allowed_channels?
|
||||
@inbox.email? || @inbox.sms? || @inbox.twilio? || @inbox.whatsapp?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,8 +25,6 @@ class NotificationBuilder
|
||||
def build_notification
|
||||
# Create conversation_creation notification only if user is subscribed to it
|
||||
return if notification_type == 'conversation_creation' && !user_subscribed_to_notification?
|
||||
# skip notifications for blocked conversations except for user mentions
|
||||
return if primary_actor.contact.blocked? && notification_type != 'conversation_mention'
|
||||
|
||||
user.notifications.create!(
|
||||
notification_type: notification_type,
|
||||
|
||||
@@ -2,38 +2,52 @@ class V2::Reports::AgentSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
||||
pattr_initialize [:account!, :params!]
|
||||
|
||||
def build
|
||||
load_data
|
||||
set_grouped_conversations_count
|
||||
set_grouped_avg_reply_time
|
||||
set_grouped_avg_first_response_time
|
||||
set_grouped_avg_resolution_time
|
||||
prepare_report
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :conversations_count, :resolved_count,
|
||||
:avg_resolution_time, :avg_first_response_time, :avg_reply_time
|
||||
|
||||
def fetch_conversations_count
|
||||
account.conversations.where(created_at: range).group('assignee_id').count
|
||||
def set_grouped_conversations_count
|
||||
@grouped_conversations_count = Current.account.conversations.where(created_at: range).group('assignee_id').count
|
||||
end
|
||||
|
||||
def prepare_report
|
||||
account.account_users.map do |account_user|
|
||||
build_agent_stats(account_user)
|
||||
end
|
||||
def set_grouped_avg_resolution_time
|
||||
@grouped_avg_resolution_time = get_grouped_average(reporting_events.where(name: 'conversation_resolved'))
|
||||
end
|
||||
|
||||
def build_agent_stats(account_user)
|
||||
user_id = account_user.user_id
|
||||
{
|
||||
id: user_id,
|
||||
conversations_count: conversations_count[user_id] || 0,
|
||||
resolved_conversations_count: resolved_count[user_id] || 0,
|
||||
avg_resolution_time: avg_resolution_time[user_id],
|
||||
avg_first_response_time: avg_first_response_time[user_id],
|
||||
avg_reply_time: avg_reply_time[user_id]
|
||||
}
|
||||
def set_grouped_avg_first_response_time
|
||||
@grouped_avg_first_response_time = get_grouped_average(reporting_events.where(name: 'first_response'))
|
||||
end
|
||||
|
||||
def set_grouped_avg_reply_time
|
||||
@grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time'))
|
||||
end
|
||||
|
||||
def group_by_key
|
||||
:user_id
|
||||
end
|
||||
|
||||
def reporting_events
|
||||
@reporting_events ||= Current.account.reporting_events.where(created_at: range)
|
||||
end
|
||||
|
||||
def prepare_report
|
||||
account.account_users.each_with_object([]) do |account_user, arr|
|
||||
arr << {
|
||||
id: account_user.user_id,
|
||||
conversations_count: @grouped_conversations_count[account_user.user_id],
|
||||
avg_resolution_time: @grouped_avg_resolution_time[account_user.user_id],
|
||||
avg_first_response_time: @grouped_avg_first_response_time[account_user.user_id],
|
||||
avg_reply_time: @grouped_avg_reply_time[account_user.user_id]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def average_value_key
|
||||
ActiveModel::Type::Boolean.new.cast(params[:business_hours]).present? ? :value_in_business_hours : :value
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,50 +1,17 @@
|
||||
class V2::Reports::BaseSummaryBuilder
|
||||
include DateRangeHelper
|
||||
|
||||
def build
|
||||
load_data
|
||||
prepare_report
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_data
|
||||
@conversations_count = fetch_conversations_count
|
||||
@resolved_count = fetch_resolved_count
|
||||
@avg_resolution_time = fetch_average_time('conversation_resolved')
|
||||
@avg_first_response_time = fetch_average_time('first_response')
|
||||
@avg_reply_time = fetch_average_time('reply_time')
|
||||
end
|
||||
|
||||
def reporting_events
|
||||
@reporting_events ||= account.reporting_events.where(created_at: range)
|
||||
end
|
||||
|
||||
def fetch_conversations_count
|
||||
# Override this method
|
||||
end
|
||||
|
||||
def fetch_average_time(event_name)
|
||||
get_grouped_average(reporting_events.where(name: event_name))
|
||||
end
|
||||
|
||||
def fetch_resolved_count
|
||||
reporting_events.where(name: 'conversation_resolved').group(group_by_key).count
|
||||
end
|
||||
|
||||
def group_by_key
|
||||
# Override this method
|
||||
end
|
||||
|
||||
def prepare_report
|
||||
# Override this method
|
||||
end
|
||||
|
||||
def get_grouped_average(events)
|
||||
events.group(group_by_key).average(average_value_key)
|
||||
end
|
||||
|
||||
def average_value_key
|
||||
ActiveModel::Type::Boolean.new.cast(params[:business_hours]).present? ? :value_in_business_hours : :value
|
||||
params[:business_hours].present? ? :value_in_business_hours : :value
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
class V2::Reports::InboxSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
||||
pattr_initialize [:account!, :params!]
|
||||
|
||||
def build
|
||||
load_data
|
||||
prepare_report
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :conversations_count, :resolved_count,
|
||||
:avg_resolution_time, :avg_first_response_time, :avg_reply_time
|
||||
|
||||
def load_data
|
||||
@conversations_count = fetch_conversations_count
|
||||
@resolved_count = fetch_resolved_count
|
||||
@avg_resolution_time = fetch_average_time('conversation_resolved')
|
||||
@avg_first_response_time = fetch_average_time('first_response')
|
||||
@avg_reply_time = fetch_average_time('reply_time')
|
||||
end
|
||||
|
||||
def fetch_conversations_count
|
||||
account.conversations.where(created_at: range).group(group_by_key).count
|
||||
end
|
||||
|
||||
def prepare_report
|
||||
account.inboxes.map do |inbox|
|
||||
build_inbox_stats(inbox)
|
||||
end
|
||||
end
|
||||
|
||||
def build_inbox_stats(inbox)
|
||||
{
|
||||
id: inbox.id,
|
||||
conversations_count: conversations_count[inbox.id] || 0,
|
||||
resolved_conversations_count: resolved_count[inbox.id] || 0,
|
||||
avg_resolution_time: avg_resolution_time[inbox.id],
|
||||
avg_first_response_time: avg_first_response_time[inbox.id],
|
||||
avg_reply_time: avg_reply_time[inbox.id]
|
||||
}
|
||||
end
|
||||
|
||||
def group_by_key
|
||||
:inbox_id
|
||||
end
|
||||
|
||||
def average_value_key
|
||||
ActiveModel::Type::Boolean.new.cast(params[:business_hours]) ? :value_in_business_hours : :value
|
||||
end
|
||||
end
|
||||
@@ -1,37 +1,49 @@
|
||||
class V2::Reports::TeamSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
||||
pattr_initialize [:account!, :params!]
|
||||
|
||||
def build
|
||||
set_grouped_conversations_count
|
||||
set_grouped_avg_reply_time
|
||||
set_grouped_avg_first_response_time
|
||||
set_grouped_avg_resolution_time
|
||||
prepare_report
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :conversations_count, :resolved_count,
|
||||
:avg_resolution_time, :avg_first_response_time, :avg_reply_time
|
||||
def set_grouped_conversations_count
|
||||
@grouped_conversations_count = Current.account.conversations.where(created_at: range).group('team_id').count
|
||||
end
|
||||
|
||||
def fetch_conversations_count
|
||||
account.conversations.where(created_at: range).group(:team_id).count
|
||||
def set_grouped_avg_resolution_time
|
||||
@grouped_avg_resolution_time = get_grouped_average(reporting_events.where(name: 'conversation_resolved'))
|
||||
end
|
||||
|
||||
def set_grouped_avg_first_response_time
|
||||
@grouped_avg_first_response_time = get_grouped_average(reporting_events.where(name: 'first_response'))
|
||||
end
|
||||
|
||||
def set_grouped_avg_reply_time
|
||||
@grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time'))
|
||||
end
|
||||
|
||||
def reporting_events
|
||||
@reporting_events ||= account.reporting_events.where(created_at: range).joins(:conversation)
|
||||
end
|
||||
|
||||
def prepare_report
|
||||
account.teams.map do |team|
|
||||
build_team_stats(team)
|
||||
end
|
||||
end
|
||||
|
||||
def build_team_stats(team)
|
||||
{
|
||||
id: team.id,
|
||||
conversations_count: conversations_count[team.id] || 0,
|
||||
resolved_conversations_count: resolved_count[team.id] || 0,
|
||||
avg_resolution_time: avg_resolution_time[team.id],
|
||||
avg_first_response_time: avg_first_response_time[team.id],
|
||||
avg_reply_time: avg_reply_time[team.id]
|
||||
}
|
||||
@reporting_events ||= Current.account.reporting_events.where(created_at: range).joins(:conversation)
|
||||
end
|
||||
|
||||
def group_by_key
|
||||
'conversations.team_id'
|
||||
end
|
||||
|
||||
def prepare_report
|
||||
account.teams.each_with_object([]) do |team, arr|
|
||||
arr << {
|
||||
id: team.id,
|
||||
conversations_count: @grouped_conversations_count[team.id],
|
||||
avg_resolution_time: @grouped_avg_resolution_time[team.id],
|
||||
avg_first_response_time: @grouped_avg_first_response_time[team.id],
|
||||
avg_reply_time: @grouped_avg_reply_time[team.id]
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -28,7 +28,7 @@ class V2::Reports::Timeseries::AverageReportBuilder < V2::Reports::Timeseries::B
|
||||
end
|
||||
|
||||
def object_scope
|
||||
scope.reporting_events.where(name: event_name, created_at: range, account_id: account.id)
|
||||
scope.reporting_events.where(name: event_name, created_at: range)
|
||||
end
|
||||
|
||||
def reporting_events
|
||||
|
||||
@@ -2,9 +2,10 @@ class RoomChannel < ApplicationCable::Channel
|
||||
def subscribed
|
||||
# TODO: should we only do ensure stream if current account is present?
|
||||
# for now going ahead with guard clauses in update_subscription and broadcast_presence
|
||||
|
||||
ensure_stream
|
||||
current_user
|
||||
current_account
|
||||
ensure_stream
|
||||
update_subscription
|
||||
broadcast_presence
|
||||
end
|
||||
@@ -21,12 +22,12 @@ class RoomChannel < ApplicationCable::Channel
|
||||
|
||||
data = { account_id: @current_account.id, users: ::OnlineStatusTracker.get_available_users(@current_account.id) }
|
||||
data[:contacts] = ::OnlineStatusTracker.get_available_contacts(@current_account.id) if @current_user.is_a? User
|
||||
ActionCable.server.broadcast(pubsub_token, { event: 'presence.update', data: data })
|
||||
ActionCable.server.broadcast(@pubsub_token, { event: 'presence.update', data: data })
|
||||
end
|
||||
|
||||
def ensure_stream
|
||||
stream_from pubsub_token
|
||||
stream_from "account_#{@current_account.id}" if @current_account.present? && @current_user.is_a?(User)
|
||||
@pubsub_token = params[:pubsub_token]
|
||||
stream_from @pubsub_token
|
||||
end
|
||||
|
||||
def update_subscription
|
||||
@@ -35,15 +36,11 @@ class RoomChannel < ApplicationCable::Channel
|
||||
::OnlineStatusTracker.update_presence(@current_account.id, @current_user.class.name, @current_user.id)
|
||||
end
|
||||
|
||||
def pubsub_token
|
||||
@pubsub_token ||= params[:pubsub_token]
|
||||
end
|
||||
|
||||
def current_user
|
||||
@current_user ||= if params[:user_id].blank?
|
||||
ContactInbox.find_by!(pubsub_token: pubsub_token).contact
|
||||
ContactInbox.find_by!(pubsub_token: @pubsub_token).contact
|
||||
else
|
||||
User.find_by!(pubsub_token: pubsub_token, id: params[:user_id])
|
||||
User.find_by!(pubsub_token: @pubsub_token, id: params[:user_id])
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -6,15 +6,13 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
||||
|
||||
def index
|
||||
@portal_articles = @portal.articles
|
||||
|
||||
set_article_count
|
||||
|
||||
@articles = @articles.search(list_params)
|
||||
@all_articles = @portal_articles.search(list_params)
|
||||
@articles_count = @all_articles.count
|
||||
|
||||
@articles = if list_params[:category_slug].present?
|
||||
@articles.order_by_position.page(@current_page)
|
||||
@all_articles.order_by_position.page(@current_page)
|
||||
else
|
||||
@articles.order_by_updated_at.page(@current_page)
|
||||
@all_articles.order_by_updated_at.page(@current_page)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -45,19 +43,6 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
||||
|
||||
private
|
||||
|
||||
def set_article_count
|
||||
# Search the params without status and author_id, use this to
|
||||
# compute mine count published draft etc
|
||||
base_search_params = list_params.except(:status, :author_id)
|
||||
@articles = @portal_articles.search(base_search_params)
|
||||
|
||||
@articles_count = @articles.count
|
||||
@mine_articles_count = @articles.search_by_author(Current.user.id).count
|
||||
@published_articles_count = @articles.published.count
|
||||
@draft_articles_count = @articles.draft.count
|
||||
@archived_articles_count = @articles.archived.count
|
||||
end
|
||||
|
||||
def fetch_article
|
||||
@article = @portal.articles.find(params[:id])
|
||||
end
|
||||
@@ -68,10 +53,9 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
||||
|
||||
def article_params
|
||||
params.require(:article).permit(
|
||||
:title, :slug, :position, :content, :description, :position, :category_id, :author_id, :associated_article_id, :status,
|
||||
:locale, meta: [:title,
|
||||
:description,
|
||||
{ tags: [] }]
|
||||
:title, :slug, :position, :content, :description, :position, :category_id, :author_id, :associated_article_id, :status, meta: [:title,
|
||||
:description,
|
||||
{ tags: [] }]
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts::
|
||||
def index
|
||||
@conversations = Current.account.conversations.includes(
|
||||
:assignee, :contact, :inbox, :taggings
|
||||
).where(inbox_id: inbox_ids, contact_id: @contact.id).order(last_activity_at: :desc).limit(20)
|
||||
).where(inbox_id: inbox_ids, contact_id: @contact.id).order(id: :desc).limit(20)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -14,7 +14,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
before_action :check_authorization
|
||||
before_action :set_current_page, only: [:index, :active, :search, :filter]
|
||||
before_action :fetch_contact, only: [:show, :update, :destroy, :avatar, :contactable_inboxes, :destroy_custom_attributes]
|
||||
before_action :set_include_contact_inboxes, only: [:index, :search, :filter, :show, :update]
|
||||
before_action :set_include_contact_inboxes, only: [:index, :search, :filter]
|
||||
|
||||
def index
|
||||
@contacts_count = resolved_contacts.count
|
||||
@@ -68,7 +68,6 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
@contacts = fetch_contacts(contacts)
|
||||
rescue CustomExceptions::CustomFilter::InvalidAttribute,
|
||||
CustomExceptions::CustomFilter::InvalidOperator,
|
||||
CustomExceptions::CustomFilter::InvalidQueryOperator,
|
||||
CustomExceptions::CustomFilter::InvalidValue => e
|
||||
render_could_not_create_error(e.message)
|
||||
end
|
||||
|
||||
@@ -6,8 +6,6 @@ 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]
|
||||
@@ -26,12 +24,7 @@ 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
|
||||
@@ -53,7 +46,6 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
@conversations_count = result[:count]
|
||||
rescue CustomExceptions::CustomFilter::InvalidAttribute,
|
||||
CustomExceptions::CustomFilter::InvalidOperator,
|
||||
CustomExceptions::CustomFilter::InvalidQueryOperator,
|
||||
CustomExceptions::CustomFilter::InvalidValue => e
|
||||
render_could_not_create_error(e.message)
|
||||
end
|
||||
@@ -131,10 +123,6 @@ 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)
|
||||
|
||||
@@ -10,7 +10,7 @@ class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseControl
|
||||
def create
|
||||
authorize @inbox, :create?
|
||||
ActiveRecord::Base.transaction do
|
||||
@inbox.add_members(agents_to_be_added_ids)
|
||||
agents_to_be_added_ids.map { |user_id| @inbox.add_member(user_id) }
|
||||
end
|
||||
fetch_updated_agents
|
||||
end
|
||||
@@ -24,7 +24,7 @@ class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseControl
|
||||
def destroy
|
||||
authorize @inbox, :destroy?
|
||||
ActiveRecord::Base.transaction do
|
||||
@inbox.remove_members(params[:user_ids])
|
||||
params[:user_ids].map { |user_id| @inbox.remove_member(user_id) }
|
||||
end
|
||||
head :ok
|
||||
end
|
||||
@@ -41,8 +41,8 @@ class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseControl
|
||||
# the missing ones are the agents which are to be deleted from the inbox
|
||||
# the new ones are the agents which are to be added to the inbox
|
||||
ActiveRecord::Base.transaction do
|
||||
@inbox.add_members(agents_to_be_added_ids)
|
||||
@inbox.remove_members(agents_to_be_removed_ids)
|
||||
agents_to_be_added_ids.each { |user_id| @inbox.add_member(user_id) }
|
||||
agents_to_be_removed_ids.each { |user_id| @inbox.remove_member(user_id) }
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
class Api::V1::Accounts::Integrations::CaptainController < Api::V1::Accounts::BaseController
|
||||
before_action :check_admin_authorization?
|
||||
before_action :fetch_hook
|
||||
|
||||
def sso_url
|
||||
params_string =
|
||||
"token=#{URI.encode_www_form_component(@hook['settings']['access_token'])}" \
|
||||
"&email=#{URI.encode_www_form_component(@hook['settings']['account_email'])}" \
|
||||
"&account_id=#{URI.encode_www_form_component(@hook['settings']['account_id'])}"
|
||||
|
||||
installation_config = InstallationConfig.find_by(name: 'CAPTAIN_APP_URL')
|
||||
|
||||
sso_url = "#{installation_config.value}/sso?#{params_string}"
|
||||
render json: { sso_url: sso_url }, status: :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_hook
|
||||
@hook = Current.account.hooks.find_by!(app_id: 'captain')
|
||||
end
|
||||
end
|
||||
@@ -1,11 +1,5 @@
|
||||
class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_conversation, only: [:link_issue, :linked_issues]
|
||||
before_action :fetch_hook, only: [:destroy]
|
||||
|
||||
def destroy
|
||||
@hook.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
def teams
|
||||
teams = linear_processor_service.teams
|
||||
@@ -96,8 +90,4 @@ class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::Bas
|
||||
def permitted_params
|
||||
params.permit(:team_id, :project_id, :conversation_id, :issue_id, :link_id, :title, :description, :assignee_id, :priority, label_ids: [])
|
||||
end
|
||||
|
||||
def fetch_hook
|
||||
@hook = Integrations::Hook.where(account: Current.account).find_by(app_id: 'linear')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
class Api::V1::Accounts::Integrations::ShopifyController < Api::V1::Accounts::BaseController
|
||||
include Shopify::IntegrationHelper
|
||||
before_action :setup_shopify_context, only: [:orders]
|
||||
before_action :fetch_hook, except: [:auth]
|
||||
before_action :validate_contact, only: [:orders]
|
||||
|
||||
def auth
|
||||
shop_domain = params[:shop_domain]
|
||||
return render json: { error: 'Shop domain is required' }, status: :unprocessable_entity if shop_domain.blank?
|
||||
|
||||
state = generate_shopify_token(Current.account.id)
|
||||
|
||||
auth_url = "https://#{shop_domain}/admin/oauth/authorize?"
|
||||
auth_url += URI.encode_www_form(
|
||||
client_id: client_id,
|
||||
scope: REQUIRED_SCOPES.join(','),
|
||||
redirect_uri: redirect_uri,
|
||||
state: state
|
||||
)
|
||||
|
||||
render json: { redirect_url: auth_url }
|
||||
end
|
||||
|
||||
def orders
|
||||
customers = fetch_customers
|
||||
return render json: { orders: [] } if customers.empty?
|
||||
|
||||
orders = fetch_orders(customers.first['id'])
|
||||
render json: { orders: orders }
|
||||
rescue ShopifyAPI::Errors::HttpResponseError => e
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def destroy
|
||||
@hook.destroy!
|
||||
head :ok
|
||||
rescue StandardError => e
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def redirect_uri
|
||||
"#{ENV.fetch('FRONTEND_URL', '')}/shopify/callback"
|
||||
end
|
||||
|
||||
def contact
|
||||
@contact ||= Current.account.contacts.find_by(id: params[:contact_id])
|
||||
end
|
||||
|
||||
def fetch_hook
|
||||
@hook = Integrations::Hook.find_by!(account: Current.account, app_id: 'shopify')
|
||||
end
|
||||
|
||||
def fetch_customers
|
||||
query = []
|
||||
query << "email:#{contact.email}" if contact.email.present?
|
||||
query << "phone:#{contact.phone_number}" if contact.phone_number.present?
|
||||
|
||||
shopify_client.get(
|
||||
path: 'customers/search.json',
|
||||
query: {
|
||||
query: query.join(' OR '),
|
||||
fields: 'id,email,phone'
|
||||
}
|
||||
).body['customers'] || []
|
||||
end
|
||||
|
||||
def fetch_orders(customer_id)
|
||||
orders = shopify_client.get(
|
||||
path: 'orders.json',
|
||||
query: {
|
||||
customer_id: customer_id,
|
||||
status: 'any',
|
||||
fields: 'id,email,created_at,total_price,currency,fulfillment_status,financial_status'
|
||||
}
|
||||
).body['orders'] || []
|
||||
|
||||
orders.map do |order|
|
||||
order.merge('admin_url' => "https://#{@hook.reference_id}/admin/orders/#{order['id']}")
|
||||
end
|
||||
end
|
||||
|
||||
def setup_shopify_context
|
||||
return if client_id.blank? || client_secret.blank?
|
||||
|
||||
ShopifyAPI::Context.setup(
|
||||
api_key: client_id,
|
||||
api_secret_key: client_secret,
|
||||
api_version: '2025-01'.freeze,
|
||||
scope: REQUIRED_SCOPES.join(','),
|
||||
is_embedded: true,
|
||||
is_private: false
|
||||
)
|
||||
end
|
||||
|
||||
def shopify_session
|
||||
ShopifyAPI::Auth::Session.new(shop: @hook.reference_id, access_token: @hook.access_token)
|
||||
end
|
||||
|
||||
def shopify_client
|
||||
@shopify_client ||= ShopifyAPI::Clients::Rest::Admin.new(session: shopify_session)
|
||||
end
|
||||
|
||||
def validate_contact
|
||||
return unless contact.blank? || (contact.email.blank? && contact.phone_number.blank?)
|
||||
|
||||
render json: { error: 'Contact information missing' },
|
||||
status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
@@ -20,7 +20,7 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def create
|
||||
@portal = Current.account.portals.build(portal_params.merge(live_chat_widget_params))
|
||||
@portal = Current.account.portals.build(portal_params)
|
||||
@portal.custom_domain = parsed_custom_domain
|
||||
@portal.save!
|
||||
process_attached_logo
|
||||
@@ -28,7 +28,7 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||
|
||||
def update
|
||||
ActiveRecord::Base.transaction do
|
||||
@portal.update!(portal_params.merge(live_chat_widget_params)) if params[:portal].present?
|
||||
@portal.update!(portal_params) if params[:portal].present?
|
||||
# @portal.custom_domain = parsed_custom_domain
|
||||
process_attached_logo if params[:blob_id].present?
|
||||
rescue StandardError => e
|
||||
@@ -70,21 +70,11 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||
|
||||
def portal_params
|
||||
params.require(:portal).permit(
|
||||
:account_id, :color, :custom_domain, :header_text, :homepage_link,
|
||||
:name, :page_title, :slug, :archived, { config: [:default_locale, { allowed_locales: [] }] }
|
||||
:account_id, :color, :custom_domain, :header_text, :homepage_link, :name, :page_title, :slug, :archived, { config: [:default_locale,
|
||||
{ allowed_locales: [] }] }
|
||||
)
|
||||
end
|
||||
|
||||
def live_chat_widget_params
|
||||
permitted_params = params.permit(:inbox_id)
|
||||
return {} if permitted_params[:inbox_id].blank?
|
||||
|
||||
inbox = Inbox.find(permitted_params[:inbox_id])
|
||||
return {} unless inbox.web_widget?
|
||||
|
||||
{ channel_web_widget_id: inbox.channel.id }
|
||||
end
|
||||
|
||||
def portal_member_params
|
||||
params.require(:portal).permit(:account_id, member_ids: [])
|
||||
end
|
||||
|
||||
@@ -9,14 +9,14 @@ class Api::V1::Accounts::TeamMembersController < Api::V1::Accounts::BaseControll
|
||||
|
||||
def create
|
||||
ActiveRecord::Base.transaction do
|
||||
@team_members = @team.add_members(members_to_be_added_ids)
|
||||
@team_members = members_to_be_added_ids.map { |user_id| @team.add_member(user_id) }
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
ActiveRecord::Base.transaction do
|
||||
@team.add_members(members_to_be_added_ids)
|
||||
@team.remove_members(members_to_be_removed_ids)
|
||||
members_to_be_added_ids.each { |user_id| @team.add_member(user_id) }
|
||||
members_to_be_removed_ids.each { |user_id| @team.remove_member(user_id) }
|
||||
end
|
||||
@team_members = @team.members
|
||||
render action: 'create'
|
||||
@@ -24,7 +24,7 @@ class Api::V1::Accounts::TeamMembersController < Api::V1::Accounts::BaseControll
|
||||
|
||||
def destroy
|
||||
ActiveRecord::Base.transaction do
|
||||
@team.remove_members(params[:user_ids])
|
||||
params[:user_ids].map { |user_id| @team.remove_member(user_id) }
|
||||
end
|
||||
head :ok
|
||||
end
|
||||
|
||||
@@ -17,7 +17,7 @@ class Api::V1::ProfilesController < Api::BaseController
|
||||
|
||||
def avatar
|
||||
@user.avatar.attachment.destroy! if @user.avatar.attached?
|
||||
@user.reload
|
||||
head :ok
|
||||
end
|
||||
|
||||
def auto_offline
|
||||
|
||||
@@ -2,10 +2,6 @@ class Api::V1::Widget::CampaignsController < Api::V1::Widget::BaseController
|
||||
skip_before_action :set_contact
|
||||
|
||||
def index
|
||||
@campaigns = @web_widget
|
||||
.inbox
|
||||
.campaigns
|
||||
.where(enabled: true, account_id: @web_widget.inbox.account_id)
|
||||
.includes(:sender)
|
||||
@campaigns = @web_widget.inbox.campaigns.where(enabled: true)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,6 +2,6 @@ class Api::V1::Widget::InboxMembersController < Api::V1::Widget::BaseController
|
||||
skip_before_action :set_contact
|
||||
|
||||
def index
|
||||
@inbox_members = @web_widget.inbox.inbox_members.includes(user: { avatar_attachment: :blob })
|
||||
@inbox_members = @web_widget.inbox.inbox_members.includes(:user)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
class Api::V2::Accounts::LiveReportsController < Api::V1::Accounts::BaseController
|
||||
before_action :load_conversations, only: [:conversation_metrics, :grouped_conversation_metrics]
|
||||
before_action :set_group_scope, only: [:grouped_conversation_metrics]
|
||||
|
||||
before_action :check_authorization
|
||||
|
||||
def conversation_metrics
|
||||
render json: {
|
||||
open: @conversations.open.count,
|
||||
unattended: @conversations.open.unattended.count,
|
||||
unassigned: @conversations.open.unassigned.count,
|
||||
pending: @conversations.pending.count
|
||||
}
|
||||
end
|
||||
|
||||
def grouped_conversation_metrics
|
||||
count_by_group = @conversations.open.group(@group_scope).count
|
||||
unattended_by_group = @conversations.open.unattended.group(@group_scope).count
|
||||
unassigned_by_group = @conversations.open.unassigned.group(@group_scope).count
|
||||
|
||||
group_metrics = count_by_group.map do |group_id, count|
|
||||
metric = {
|
||||
open: count,
|
||||
unattended: unattended_by_group[group_id] || 0,
|
||||
unassigned: unassigned_by_group[group_id] || 0
|
||||
}
|
||||
metric[@group_scope] = group_id
|
||||
metric
|
||||
end
|
||||
|
||||
render json: group_metrics
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
authorize :report, :view?
|
||||
end
|
||||
|
||||
def set_group_scope
|
||||
render json: { error: 'invalid group_by' }, status: :unprocessable_entity and return unless %w[
|
||||
team_id
|
||||
assignee_id
|
||||
].include?(permitted_params[:group_by])
|
||||
|
||||
@group_scope = permitted_params[:group_by]
|
||||
end
|
||||
|
||||
def team
|
||||
return unless permitted_params[:team_id]
|
||||
|
||||
@team ||= Current.account.teams.find(permitted_params[:team_id])
|
||||
end
|
||||
|
||||
def load_conversations
|
||||
scope = Current.account.conversations
|
||||
scope = scope.where(team_id: team.id) if team.present?
|
||||
@conversations = scope
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:team_id, :group_by)
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,6 @@
|
||||
class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseController
|
||||
before_action :check_authorization
|
||||
before_action :prepare_builder_params, only: [:agent, :team, :inbox]
|
||||
before_action :prepare_builder_params, only: [:agent, :team]
|
||||
|
||||
def agent
|
||||
render_report_with(V2::Reports::AgentSummaryBuilder)
|
||||
@@ -10,10 +10,6 @@ class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseContr
|
||||
render_report_with(V2::Reports::TeamSummaryBuilder)
|
||||
end
|
||||
|
||||
def inbox
|
||||
render_report_with(V2::Reports::InboxSummaryBuilder)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
@@ -30,7 +26,8 @@ class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseContr
|
||||
|
||||
def render_report_with(builder_class)
|
||||
builder = builder_class.new(account: Current.account, params: @builder_params)
|
||||
render json: builder.build
|
||||
data = builder.build
|
||||
render json: data
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
|
||||
@@ -5,11 +5,10 @@ module SwitchLocale
|
||||
|
||||
def switch_locale(&)
|
||||
# priority is for locale set in query string (mostly for widget/from js sdk)
|
||||
locale ||= params[:locale]
|
||||
|
||||
locale ||= locale_from_params
|
||||
locale ||= locale_from_custom_domain
|
||||
# if locale is not set in account, let's use DEFAULT_LOCALE env variable
|
||||
locale ||= ENV.fetch('DEFAULT_LOCALE', nil)
|
||||
locale ||= locale_from_env_variable
|
||||
set_locale(locale, &)
|
||||
end
|
||||
|
||||
@@ -33,30 +32,26 @@ module SwitchLocale
|
||||
end
|
||||
|
||||
def set_locale(locale, &)
|
||||
safe_locale = validate_and_get_locale(locale)
|
||||
# if locale is empty, use default_locale
|
||||
locale ||= I18n.default_locale
|
||||
# Ensure locale won't bleed into other requests
|
||||
# https://guides.rubyonrails.org/i18n.html#managing-the-locale-across-requests
|
||||
I18n.with_locale(safe_locale, &)
|
||||
I18n.with_locale(locale, &)
|
||||
end
|
||||
|
||||
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
|
||||
def locale_from_params
|
||||
I18n.available_locales.map(&:to_s).include?(params[:locale]) ? params[:locale] : nil
|
||||
end
|
||||
|
||||
def locale_from_account(account)
|
||||
return unless account
|
||||
|
||||
account.locale
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,17 +7,13 @@ class DashboardController < ActionController::Base
|
||||
around_action :switch_locale
|
||||
before_action :ensure_installation_onboarding, only: [:index]
|
||||
before_action :render_hc_if_custom_domain, only: [:index]
|
||||
before_action :ensure_html_format
|
||||
|
||||
layout 'vueapp'
|
||||
|
||||
def index; end
|
||||
|
||||
private
|
||||
|
||||
def ensure_html_format
|
||||
head :not_acceptable unless request.format.html?
|
||||
end
|
||||
|
||||
def set_global_config
|
||||
@global_config = GlobalConfig.get(
|
||||
'LOGO', 'LOGO_DARK', 'LOGO_THUMBNAIL',
|
||||
@@ -36,7 +32,7 @@ class DashboardController < ActionController::Base
|
||||
'LOGOUT_REDIRECT_LINK',
|
||||
'DISABLE_USER_PROFILE_UPDATE',
|
||||
'DEPLOYMENT_ENV',
|
||||
'CSML_EDITOR_HOST', 'INSTALLATION_PRICING_PLAN'
|
||||
'CSML_EDITOR_HOST'
|
||||
).merge(app_config)
|
||||
end
|
||||
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
class Google::CallbacksController < OauthCallbackController
|
||||
include GoogleConcern
|
||||
|
||||
def find_channel_by_email
|
||||
# find by imap_login first, and then by email
|
||||
# this ensures the legacy users can migrate correctly even if inbox email address doesn't match
|
||||
imap_channel = Channel::Email.find_by(imap_login: users_data['email'], account: account)
|
||||
return imap_channel if imap_channel
|
||||
|
||||
Channel::Email.find_by(email: users_data['email'], account: account)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def provider_name
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
class Linear::CallbacksController < ApplicationController
|
||||
include Linear::IntegrationHelper
|
||||
|
||||
def show
|
||||
@response = oauth_client.auth_code.get_token(
|
||||
params[:code],
|
||||
redirect_uri: "#{base_url}/linear/callback"
|
||||
)
|
||||
|
||||
handle_response
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("Linear callback error: #{e.message}")
|
||||
redirect_to linear_redirect_uri
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def oauth_client
|
||||
app_id = GlobalConfigService.load('LINEAR_CLIENT_ID', nil)
|
||||
app_secret = GlobalConfigService.load('LINEAR_CLIENT_SECRET', nil)
|
||||
|
||||
OAuth2::Client.new(
|
||||
app_id,
|
||||
app_secret,
|
||||
{
|
||||
site: 'https://api.linear.app',
|
||||
token_url: '/oauth/token',
|
||||
authorize_url: '/oauth/authorize'
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def handle_response
|
||||
hook = account.hooks.new(
|
||||
access_token: parsed_body['access_token'],
|
||||
status: 'enabled',
|
||||
app_id: 'linear',
|
||||
settings: {
|
||||
token_type: parsed_body['token_type'],
|
||||
expires_in: parsed_body['expires_in'],
|
||||
scope: parsed_body['scope']
|
||||
}
|
||||
)
|
||||
# You may wonder why we're not handling the refresh token update, since the token will expire only after 10 years, https://github.com/linear/linear/issues/251
|
||||
hook.save!
|
||||
redirect_to linear_redirect_uri
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("Linear callback error: #{e.message}")
|
||||
redirect_to linear_redirect_uri
|
||||
end
|
||||
|
||||
def account
|
||||
@account ||= Account.find(account_id)
|
||||
end
|
||||
|
||||
def account_id
|
||||
return unless params[:state]
|
||||
|
||||
verify_linear_token(params[:state])
|
||||
end
|
||||
|
||||
def linear_redirect_uri
|
||||
"#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/settings/integrations/linear"
|
||||
end
|
||||
|
||||
def parsed_body
|
||||
@parsed_body ||= @response.response.parsed
|
||||
end
|
||||
|
||||
def base_url
|
||||
ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
|
||||
end
|
||||
end
|
||||
@@ -25,7 +25,7 @@ class OauthCallbackController < ApplicationController
|
||||
end
|
||||
|
||||
def find_or_create_inbox
|
||||
channel_email = find_channel_by_email
|
||||
channel_email = Channel::Email.find_by(email: users_data['email'], account: account)
|
||||
# we need this value to know where to redirect on sucessful processing of the callback
|
||||
channel_exists = channel_email.present?
|
||||
|
||||
@@ -39,10 +39,6 @@ class OauthCallbackController < ApplicationController
|
||||
[channel_email.inbox, channel_exists]
|
||||
end
|
||||
|
||||
def find_channel_by_email
|
||||
Channel::Email.find_by(email: users_data['email'], account: account)
|
||||
end
|
||||
|
||||
def update_channel(channel_email)
|
||||
channel_email.update!({
|
||||
imap_login: users_data['email'], imap_address: imap_address,
|
||||
|
||||
@@ -4,7 +4,6 @@ class Platform::Api::V1::AccountsController < PlatformController
|
||||
def create
|
||||
@resource = Account.create!(account_params)
|
||||
update_resource_features
|
||||
@resource.save!
|
||||
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
|
||||
end
|
||||
|
||||
|
||||
@@ -6,25 +6,16 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
|
||||
layout 'portal'
|
||||
|
||||
def index
|
||||
@articles = @portal.articles.published.includes(:category, :author)
|
||||
@articles_count = @articles.count
|
||||
@articles = @portal.articles.published
|
||||
search_articles
|
||||
order_by_sort_param
|
||||
limit_results
|
||||
@articles.page(list_params[:page]) if list_params[:page].present?
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
private
|
||||
|
||||
def limit_results
|
||||
return if list_params[:per_page].blank?
|
||||
|
||||
per_page = [list_params[:per_page].to_i, 100].min
|
||||
per_page = 25 if per_page < 1
|
||||
@articles = @articles.page(list_params[:page]).per(per_page)
|
||||
end
|
||||
|
||||
def search_articles
|
||||
@articles = @articles.search(list_params) if list_params.present?
|
||||
end
|
||||
@@ -39,7 +30,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
|
||||
|
||||
def set_article
|
||||
@article = @portal.articles.find_by(slug: permitted_params[:article_slug])
|
||||
@article.increment_view_count if @article.published?
|
||||
@article.increment_view_count
|
||||
@parsed_content = render_article_content(@article.content)
|
||||
end
|
||||
|
||||
@@ -53,7 +44,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
|
||||
end
|
||||
|
||||
def list_params
|
||||
params.permit(:query, :locale, :sort, :status, :page, :per_page)
|
||||
params.permit(:query, :locale, :sort, :status)
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
class Public::Api::V1::Portals::BaseController < PublicController
|
||||
include SwitchLocale
|
||||
|
||||
before_action :show_plain_layout
|
||||
before_action :set_color_scheme
|
||||
before_action :set_global_config
|
||||
@@ -29,7 +27,14 @@ class Public::Api::V1::Portals::BaseController < PublicController
|
||||
end
|
||||
|
||||
def switch_locale_with_portal(&)
|
||||
@locale = validate_and_get_locale(params[:locale])
|
||||
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
|
||||
|
||||
I18n.with_locale(@locale, &)
|
||||
end
|
||||
@@ -39,12 +44,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?
|
||||
|
||||
article_locale = if article.category.present?
|
||||
article.category.locale
|
||||
else
|
||||
article.portal.default_locale
|
||||
end
|
||||
@locale = validate_and_get_locale(article_locale)
|
||||
@locale = if article.category.present?
|
||||
article.category.locale
|
||||
else
|
||||
article.portal.default_locale
|
||||
end
|
||||
|
||||
I18n.with_locale(@locale, &)
|
||||
end
|
||||
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
class Shopify::CallbacksController < ApplicationController
|
||||
include Shopify::IntegrationHelper
|
||||
|
||||
def show
|
||||
verify_account!
|
||||
|
||||
@response = oauth_client.auth_code.get_token(
|
||||
params[:code],
|
||||
redirect_uri: '/shopify/callback'
|
||||
)
|
||||
|
||||
handle_response
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("Shopify callback error: #{e.message}")
|
||||
redirect_to "#{redirect_uri}?error=true"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def verify_account!
|
||||
@account_id = verify_shopify_token(params[:state])
|
||||
raise StandardError, 'Invalid state parameter' if account.blank?
|
||||
end
|
||||
|
||||
def handle_response
|
||||
account.hooks.create!(
|
||||
app_id: 'shopify',
|
||||
access_token: parsed_body['access_token'],
|
||||
status: 'enabled',
|
||||
reference_id: params[:shop],
|
||||
settings: {
|
||||
scope: parsed_body['scope']
|
||||
}
|
||||
)
|
||||
|
||||
redirect_to shopify_integration_url
|
||||
end
|
||||
|
||||
def parsed_body
|
||||
@parsed_body ||= @response.response.parsed
|
||||
end
|
||||
|
||||
def oauth_client
|
||||
OAuth2::Client.new(
|
||||
client_id,
|
||||
client_secret,
|
||||
{
|
||||
site: "https://#{params[:shop]}",
|
||||
authorize_url: '/admin/oauth/authorize',
|
||||
token_url: '/admin/oauth/access_token'
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def account
|
||||
@account ||= Account.find(@account_id)
|
||||
end
|
||||
|
||||
def account_id
|
||||
@account_id ||= params[:state].split('_').first
|
||||
end
|
||||
|
||||
def shopify_integration_url
|
||||
"#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/settings/integrations/shopify"
|
||||
end
|
||||
|
||||
def redirect_uri
|
||||
return shopify_integration_url if account
|
||||
|
||||
ENV.fetch('FRONTEND_URL', nil)
|
||||
end
|
||||
end
|
||||
@@ -35,14 +35,10 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
|
||||
@allowed_configs = case @config
|
||||
when 'facebook'
|
||||
%w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET IG_VERIFY_TOKEN FACEBOOK_API_VERSION ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT]
|
||||
when 'shopify'
|
||||
%w[SHOPIFY_CLIENT_ID SHOPIFY_CLIENT_SECRET]
|
||||
when 'microsoft'
|
||||
%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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
class Twilio::CallbackController < ApplicationController
|
||||
def create
|
||||
Webhooks::TwilioEventsJob.perform_later(permitted_params.to_unsafe_hash)
|
||||
::Twilio::IncomingMessageService.new(params: permitted_params).perform
|
||||
|
||||
head :no_content
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
class Twilio::DeliveryStatusController < ApplicationController
|
||||
def create
|
||||
Webhooks::TwilioDeliveryStatusJob.perform_later(permitted_params.to_unsafe_hash)
|
||||
::Twilio::DeliveryStatusService.new(params: permitted_params).perform
|
||||
|
||||
head :no_content
|
||||
end
|
||||
|
||||
@@ -2,12 +2,6 @@ class Webhooks::WhatsappController < ActionController::API
|
||||
include MetaTokenVerifyConcern
|
||||
|
||||
def process_payload
|
||||
if inactive_whatsapp_number?
|
||||
Rails.logger.warn("Rejected webhook for inactive WhatsApp number: #{params[:phone_number]}")
|
||||
render json: { error: 'Inactive WhatsApp number' }, status: :unprocessable_entity
|
||||
return
|
||||
end
|
||||
|
||||
Webhooks::WhatsappEventsJob.perform_later(params.to_unsafe_hash)
|
||||
head :ok
|
||||
end
|
||||
@@ -19,15 +13,4 @@ class Webhooks::WhatsappController < ActionController::API
|
||||
whatsapp_webhook_verify_token = channel.provider_config['webhook_verify_token'] if channel.present?
|
||||
token == whatsapp_webhook_verify_token if whatsapp_webhook_verify_token.present?
|
||||
end
|
||||
|
||||
def inactive_whatsapp_number?
|
||||
phone_number = params[:phone_number]
|
||||
return false if phone_number.blank?
|
||||
|
||||
inactive_numbers = GlobalConfig.get_value('INACTIVE_WHATSAPP_NUMBERS').to_s
|
||||
return false if inactive_numbers.blank?
|
||||
|
||||
inactive_numbers_array = inactive_numbers.split(',').map(&:strip)
|
||||
inactive_numbers_array.include?(phone_number)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -26,8 +26,7 @@ class AccountDashboard < Administrate::BaseDashboard
|
||||
conversations: CountField,
|
||||
locale: Field::Select.with_options(collection: LANGUAGES_CONFIG.map { |_x, y| y[:iso_639_1_code] }),
|
||||
status: Field::Select.with_options(collection: [%w[Active active], %w[Suspended suspended]]),
|
||||
account_users: Field::HasMany,
|
||||
custom_attributes: Field::String
|
||||
account_users: Field::HasMany
|
||||
}.merge(enterprise_attribute_types).freeze
|
||||
|
||||
# COLLECTION_ATTRIBUTES
|
||||
@@ -46,7 +45,7 @@ class AccountDashboard < Administrate::BaseDashboard
|
||||
|
||||
# SHOW_PAGE_ATTRIBUTES
|
||||
# an array of attributes that will be displayed on the model's show page.
|
||||
enterprise_show_page_attributes = ChatwootApp.enterprise? ? %i[custom_attributes limits all_features] : []
|
||||
enterprise_show_page_attributes = ChatwootApp.enterprise? ? %i[limits all_features] : []
|
||||
SHOW_PAGE_ATTRIBUTES = (%i[
|
||||
id
|
||||
name
|
||||
@@ -78,11 +77,7 @@ class AccountDashboard < Administrate::BaseDashboard
|
||||
# COLLECTION_FILTERS = {
|
||||
# open: ->(resources) { resources.where(open: true) }
|
||||
# }.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
|
||||
COLLECTION_FILTERS = {}.freeze
|
||||
|
||||
# Overwrite this method to customize how accounts are displayed
|
||||
# across all pages of the admin dashboard.
|
||||
|
||||
@@ -94,12 +94,7 @@ class UserDashboard < Administrate::BaseDashboard
|
||||
# COLLECTION_FILTERS = {
|
||||
# open: ->(resources) { resources.where(open: true) }
|
||||
# }.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
|
||||
COLLECTION_FILTERS = {}.freeze
|
||||
|
||||
# Overwrite this method to customize how users are displayed
|
||||
# across all pages of the admin dashboard.
|
||||
|
||||
@@ -22,5 +22,3 @@ class AsyncDispatcher < BaseDispatcher
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
AsyncDispatcher.prepend_mod_with('AsyncDispatcher')
|
||||
|
||||
@@ -2,6 +2,6 @@ require 'administrate/field/base'
|
||||
|
||||
class Enterprise::AccountLimitsField < Administrate::Field::Base
|
||||
def to_s
|
||||
data.present? ? data.to_json : { agents: nil, inboxes: nil, captain_responses: nil, captain_documents: nil }.to_json
|
||||
data.present? ? data.to_json : { agents: nil, inboxes: nil }.to_json
|
||||
end
|
||||
end
|
||||
|
||||
@@ -32,7 +32,6 @@ class ConversationFinder
|
||||
def initialize(current_user, params)
|
||||
@current_user = current_user
|
||||
@current_account = current_user.account
|
||||
@is_admin = current_account.account_users.find_by(user_id: current_user.id)&.administrator?
|
||||
@params = params
|
||||
end
|
||||
|
||||
@@ -87,8 +86,7 @@ class ConversationFinder
|
||||
end
|
||||
|
||||
def find_all_conversations
|
||||
@conversations = current_account.conversations
|
||||
@conversations = @conversations.where(inbox_id: @inbox_ids) unless @is_admin
|
||||
@conversations = current_account.conversations.where(inbox_id: @inbox_ids)
|
||||
filter_by_conversation_type if params[:conversation_type]
|
||||
@conversations
|
||||
end
|
||||
|
||||
@@ -94,8 +94,7 @@ module Api::V2::Accounts::HeatmapHelper
|
||||
end
|
||||
|
||||
def since_timestamp(date)
|
||||
number_of_days = params[:days_before].present? ? params[:days_before].to_i.days : 6.days
|
||||
(date - number_of_days).to_i.to_s
|
||||
(date - 6.days).to_i.to_s
|
||||
end
|
||||
|
||||
def until_timestamp(date)
|
||||
|
||||
@@ -1,70 +1,58 @@
|
||||
module Api::V2::Accounts::ReportsHelper
|
||||
def generate_agents_report
|
||||
reports = V2::Reports::AgentSummaryBuilder.new(
|
||||
account: Current.account,
|
||||
params: build_params(type: :agent)
|
||||
).build
|
||||
|
||||
Current.account.users.map do |agent|
|
||||
report = reports.find { |r| r[:id] == agent.id }
|
||||
[agent.name] + generate_readable_report_metrics(report)
|
||||
agent_report = report_builder({ type: :agent, id: agent.id }).summary
|
||||
[agent.name] + generate_readable_report_metrics(agent_report)
|
||||
end
|
||||
end
|
||||
|
||||
def generate_inboxes_report
|
||||
reports = V2::Reports::InboxSummaryBuilder.new(
|
||||
account: Current.account,
|
||||
params: build_params(type: :inbox)
|
||||
).build
|
||||
|
||||
Current.account.inboxes.map do |inbox|
|
||||
report = reports.find { |r| r[:id] == inbox.id }
|
||||
[inbox.name, inbox.channel&.name] + generate_readable_report_metrics(report)
|
||||
inbox_report = generate_report({ type: :inbox, id: inbox.id })
|
||||
[inbox.name, inbox.channel&.name] + generate_readable_report_metrics(inbox_report)
|
||||
end
|
||||
end
|
||||
|
||||
def generate_teams_report
|
||||
reports = V2::Reports::TeamSummaryBuilder.new(
|
||||
account: Current.account,
|
||||
params: build_params(type: :team)
|
||||
).build
|
||||
|
||||
Current.account.teams.map do |team|
|
||||
report = reports.find { |r| r[:id] == team.id }
|
||||
[team.name] + generate_readable_report_metrics(report)
|
||||
team_report = report_builder({ type: :team, id: team.id }).summary
|
||||
[team.name] + generate_readable_report_metrics(team_report)
|
||||
end
|
||||
end
|
||||
|
||||
def generate_labels_report
|
||||
Current.account.labels.map do |label|
|
||||
label_report = report_builder({ type: :label, id: label.id }).short_summary
|
||||
label_report = generate_report({ type: :label, id: label.id })
|
||||
[label.title] + generate_readable_report_metrics(label_report)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_params(base_params)
|
||||
base_params.merge(
|
||||
{
|
||||
since: params[:since],
|
||||
until: params[:until],
|
||||
business_hours: ActiveModel::Type::Boolean.new.cast(params[:business_hours])
|
||||
}
|
||||
def report_builder(report_params)
|
||||
V2::ReportBuilder.new(
|
||||
Current.account,
|
||||
report_params.merge(
|
||||
{
|
||||
since: params[:since],
|
||||
until: params[:until],
|
||||
business_hours: ActiveModel::Type::Boolean.new.cast(params[:business_hours])
|
||||
}
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
def report_builder(report_params)
|
||||
V2::ReportBuilder.new(Current.account, build_params(report_params))
|
||||
def generate_report(report_params)
|
||||
report_builder(report_params).short_summary
|
||||
end
|
||||
|
||||
def generate_readable_report_metrics(report)
|
||||
private
|
||||
|
||||
def generate_readable_report_metrics(report_metric)
|
||||
[
|
||||
report[:conversations_count],
|
||||
Reports::TimeFormatPresenter.new(report[:avg_first_response_time]).format,
|
||||
Reports::TimeFormatPresenter.new(report[:avg_resolution_time]).format,
|
||||
Reports::TimeFormatPresenter.new(report[:avg_reply_time]).format,
|
||||
report[:resolved_conversations_count]
|
||||
report_metric[:conversations_count],
|
||||
Reports::TimeFormatPresenter.new(report_metric[:avg_first_response_time]).format,
|
||||
Reports::TimeFormatPresenter.new(report_metric[:avg_resolution_time]).format,
|
||||
Reports::TimeFormatPresenter.new(report_metric[:reply_time]).format,
|
||||
report_metric[:resolutions_count]
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
module Filters::FilterHelper
|
||||
module FilterHelper
|
||||
def build_condition_query(model_filters, query_hash, current_index)
|
||||
current_filter = model_filters[query_hash['attribute_key']]
|
||||
|
||||
@@ -81,26 +81,4 @@ module Filters::FilterHelper
|
||||
def default_filter(query_hash, filter_operator_value)
|
||||
"#{filter_config[:table_name]}.#{query_hash[:attribute_key]} #{filter_operator_value} #{query_hash[:query_operator]}"
|
||||
end
|
||||
|
||||
def validate_single_condition(condition)
|
||||
return if condition['query_operator'].nil?
|
||||
return if condition['query_operator'].empty?
|
||||
|
||||
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
|
||||
@@ -1,47 +0,0 @@
|
||||
module Linear::IntegrationHelper
|
||||
# Generates a signed JWT token for Linear integration
|
||||
#
|
||||
# @param account_id [Integer] The account ID to encode in the token
|
||||
# @return [String, nil] The encoded JWT token or nil if client secret is missing
|
||||
def generate_linear_token(account_id)
|
||||
return if client_secret.blank?
|
||||
|
||||
JWT.encode(token_payload(account_id), client_secret, 'HS256')
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("Failed to generate Linear token: #{e.message}")
|
||||
nil
|
||||
end
|
||||
|
||||
def token_payload(account_id)
|
||||
{
|
||||
sub: account_id,
|
||||
iat: Time.current.to_i
|
||||
}
|
||||
end
|
||||
|
||||
# Verifies and decodes a Linear JWT token
|
||||
#
|
||||
# @param token [String] The JWT token to verify
|
||||
# @return [Integer, nil] The account ID from the token or nil if invalid
|
||||
def verify_linear_token(token)
|
||||
return if token.blank? || client_secret.blank?
|
||||
|
||||
decode_token(token, client_secret)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def client_secret
|
||||
@client_secret ||= GlobalConfigService.load('LINEAR_CLIENT_SECRET', nil)
|
||||
end
|
||||
|
||||
def decode_token(token, secret)
|
||||
JWT.decode(token, secret, true, {
|
||||
algorithm: 'HS256',
|
||||
verify_expiration: true
|
||||
}).first['sub']
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("Unexpected error verifying Linear token: #{e.message}")
|
||||
nil
|
||||
end
|
||||
end
|
||||
@@ -5,7 +5,8 @@ module PortalHelper
|
||||
end
|
||||
|
||||
def generate_portal_bg(portal_color, theme)
|
||||
generate_portal_bg_color(portal_color, theme)
|
||||
bg_image = theme == 'dark' ? 'hexagon-dark.svg' : 'hexagon-light.svg'
|
||||
"url(/assets/images/hc/#{bg_image}) #{generate_portal_bg_color(portal_color, theme)}"
|
||||
end
|
||||
|
||||
def generate_gradient_to_bottom(theme)
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
module Shopify::IntegrationHelper
|
||||
REQUIRED_SCOPES = %w[read_customers read_orders read_fulfillments].freeze
|
||||
|
||||
# Generates a signed JWT token for Shopify integration
|
||||
#
|
||||
# @param account_id [Integer] The account ID to encode in the token
|
||||
# @return [String, nil] The encoded JWT token or nil if client secret is missing
|
||||
def generate_shopify_token(account_id)
|
||||
return if client_secret.blank?
|
||||
|
||||
JWT.encode(token_payload(account_id), client_secret, 'HS256')
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("Failed to generate Shopify token: #{e.message}")
|
||||
nil
|
||||
end
|
||||
|
||||
def token_payload(account_id)
|
||||
{
|
||||
sub: account_id,
|
||||
iat: Time.current.to_i
|
||||
}
|
||||
end
|
||||
|
||||
# Verifies and decodes a Shopify JWT token
|
||||
#
|
||||
# @param token [String] The JWT token to verify
|
||||
# @return [Integer, nil] The account ID from the token or nil if invalid
|
||||
def verify_shopify_token(token)
|
||||
return if token.blank? || client_secret.blank?
|
||||
|
||||
decode_token(token, client_secret)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def client_id
|
||||
@client_id ||= GlobalConfigService.load('SHOPIFY_CLIENT_ID', nil)
|
||||
end
|
||||
|
||||
def client_secret
|
||||
@client_secret ||= GlobalConfigService.load('SHOPIFY_CLIENT_SECRET', nil)
|
||||
end
|
||||
|
||||
def decode_token(token, secret)
|
||||
JWT.decode(
|
||||
token,
|
||||
secret,
|
||||
true,
|
||||
{
|
||||
algorithm: 'HS256',
|
||||
verify_expiration: true
|
||||
}
|
||||
).first['sub']
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("Unexpected error verifying Shopify token: #{e.message}")
|
||||
nil
|
||||
end
|
||||
end
|
||||
@@ -6,47 +6,4 @@ module SuperAdmin::AccountFeaturesHelper
|
||||
def self.account_premium_features
|
||||
account_features.filter { |feature| feature['premium'] }.pluck('name')
|
||||
end
|
||||
|
||||
# Returns a hash mapping feature names to their display names
|
||||
def self.feature_display_names
|
||||
account_features.each_with_object({}) do |feature, hash|
|
||||
hash[feature['name']] = feature['display_name']
|
||||
end
|
||||
end
|
||||
|
||||
def self.filter_internal_features(features)
|
||||
return features if GlobalConfig.get_value('DEPLOYMENT_ENV') == 'cloud'
|
||||
|
||||
internal_features = account_features.select { |f| f['chatwoot_internal'] }.pluck('name')
|
||||
features.except(*internal_features)
|
||||
end
|
||||
|
||||
def self.filter_deprecated_features(features)
|
||||
deprecated_features = account_features.select { |f| f['deprecated'] }.pluck('name')
|
||||
features.except(*deprecated_features)
|
||||
end
|
||||
|
||||
def self.sort_and_transform_features(features, display_names)
|
||||
features.sort_by { |key, _| display_names[key] || key }
|
||||
.to_h
|
||||
.transform_keys { |key| [key, display_names[key]] }
|
||||
end
|
||||
|
||||
def self.partition_features(features)
|
||||
filtered = filter_internal_features(features)
|
||||
filtered = filter_deprecated_features(filtered)
|
||||
display_names = feature_display_names
|
||||
|
||||
regular, premium = filtered.partition { |key, _value| account_premium_features.exclude?(key) }
|
||||
|
||||
[
|
||||
sort_and_transform_features(regular, display_names),
|
||||
sort_and_transform_features(premium, display_names)
|
||||
]
|
||||
end
|
||||
|
||||
def self.filtered_features(features)
|
||||
regular, premium = partition_features(features)
|
||||
regular.merge(premium)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,8 +13,6 @@ import { useStore } from 'dashboard/composables/store';
|
||||
import WootSnackbarBox from './components/SnackbarContainer.vue';
|
||||
import { setColorTheme } from './helper/themeHelper';
|
||||
import { isOnOnboardingView } from 'v3/helpers/RouteHelper';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { useFontSize } from 'dashboard/composables/useFontSize';
|
||||
import {
|
||||
registerSubscription,
|
||||
verifyServiceWorkerExistence,
|
||||
@@ -37,16 +35,8 @@ export default {
|
||||
setup() {
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
const { accountId } = useAccount();
|
||||
// Use the font size composable (it automatically sets up the watcher)
|
||||
const { currentFontSize } = useFontSize();
|
||||
|
||||
return {
|
||||
router,
|
||||
store,
|
||||
currentAccountId: accountId,
|
||||
currentFontSize,
|
||||
};
|
||||
return { router, store };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -62,7 +52,11 @@ export default {
|
||||
currentUser: 'getCurrentUser',
|
||||
authUIFlags: 'getAuthUIFlags',
|
||||
accountUIFlags: 'accounts/getUIFlags',
|
||||
// currentAccountId: 'getCurrentAccountId',
|
||||
}),
|
||||
currentAccountId() {
|
||||
return this.$route.params.accountId;
|
||||
},
|
||||
hasAccounts() {
|
||||
const { accounts = [] } = this.currentUser || {};
|
||||
return accounts.length > 0;
|
||||
@@ -78,19 +72,23 @@ export default {
|
||||
this.showAddAccountModal = true;
|
||||
}
|
||||
},
|
||||
currentAccountId: {
|
||||
immediate: true,
|
||||
handler() {
|
||||
if (this.currentAccountId) {
|
||||
this.initializeAccount();
|
||||
}
|
||||
},
|
||||
currentAccountId() {
|
||||
console.log('THIS CALL', this.currentAccountId);
|
||||
if (this.currentAccountId) {
|
||||
this.initializeAccount();
|
||||
} else {
|
||||
console.log('NO ACCOUNT ID');
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
created() {
|
||||
this.setLocale(window.chatwootConfig.selectedLocale);
|
||||
},
|
||||
async mounted() {
|
||||
this.setLocale(window.chatwootConfig.selectedLocale);
|
||||
this.initializeColorTheme();
|
||||
this.listenToThemeChanges();
|
||||
this.setLocale(window.chatwootConfig.selectedLocale);
|
||||
await this.$store.dispatch('accounts/get');
|
||||
},
|
||||
unmounted() {
|
||||
if (this.reconnectService) {
|
||||
@@ -106,7 +104,7 @@ export default {
|
||||
mql.onchange = e => setColorTheme(e.matches);
|
||||
},
|
||||
setLocale(locale) {
|
||||
this.$root.$i18n.locale = locale;
|
||||
this.$i18n.locale = locale;
|
||||
},
|
||||
async initializeAccount() {
|
||||
await this.$store.dispatch('accounts/get');
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
/* global axios */
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
class CaptainAssistant extends ApiClient {
|
||||
constructor() {
|
||||
super('captain/assistants', { accountScoped: true });
|
||||
}
|
||||
|
||||
get({ page = 1, searchKey } = {}) {
|
||||
return axios.get(this.url, {
|
||||
params: {
|
||||
page,
|
||||
searchKey,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new CaptainAssistant();
|
||||
@@ -1,9 +0,0 @@
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
class CaptainBulkActionsAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('captain/bulk_actions', { accountScoped: true });
|
||||
}
|
||||
}
|
||||
|
||||
export default new CaptainBulkActionsAPI();
|
||||
@@ -1,20 +0,0 @@
|
||||
/* global axios */
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
class CaptainDocument extends ApiClient {
|
||||
constructor() {
|
||||
super('captain/documents', { accountScoped: true });
|
||||
}
|
||||
|
||||
get({ page = 1, searchKey, assistantId } = {}) {
|
||||
return axios.get(this.url, {
|
||||
params: {
|
||||
page,
|
||||
searchKey,
|
||||
assistant_id: assistantId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new CaptainDocument();
|
||||
@@ -1,26 +0,0 @@
|
||||
/* global axios */
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
class CaptainInboxes extends ApiClient {
|
||||
constructor() {
|
||||
super('captain/assistants', { accountScoped: true });
|
||||
}
|
||||
|
||||
get({ assistantId } = {}) {
|
||||
return axios.get(`${this.url}/${assistantId}/inboxes`);
|
||||
}
|
||||
|
||||
create(params = {}) {
|
||||
const { assistantId, inboxId } = params;
|
||||
return axios.post(`${this.url}/${assistantId}/inboxes`, {
|
||||
inbox: { inbox_id: inboxId },
|
||||
});
|
||||
}
|
||||
|
||||
delete(params = {}) {
|
||||
const { assistantId, inboxId } = params;
|
||||
return axios.delete(`${this.url}/${assistantId}/inboxes/${inboxId}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new CaptainInboxes();
|
||||
@@ -1,22 +0,0 @@
|
||||
/* global axios */
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
class CaptainResponses extends ApiClient {
|
||||
constructor() {
|
||||
super('captain/assistant_responses', { accountScoped: true });
|
||||
}
|
||||
|
||||
get({ page = 1, searchKey, assistantId, documentId, status } = {}) {
|
||||
return axios.get(this.url, {
|
||||
params: {
|
||||
page,
|
||||
searchKey,
|
||||
assistant_id: assistantId,
|
||||
document_id: documentId,
|
||||
status,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new CaptainResponses();
|
||||
@@ -27,14 +27,6 @@ class ContactAPI extends ApiClient {
|
||||
return axios.get(requestURL);
|
||||
}
|
||||
|
||||
show(id) {
|
||||
return axios.get(`${this.url}/${id}?include_contact_inboxes=false`);
|
||||
}
|
||||
|
||||
update(id, data) {
|
||||
return axios.patch(`${this.url}/${id}?include_contact_inboxes=false`, data);
|
||||
}
|
||||
|
||||
getConversations(contactId) {
|
||||
return axios.get(`${this.url}/${contactId}/conversations`);
|
||||
}
|
||||
|
||||
@@ -52,13 +52,12 @@ class ArticlesAPI extends PortalsAPI {
|
||||
}
|
||||
|
||||
createArticle({ portalSlug, articleObj }) {
|
||||
const { content, title, authorId, categoryId, locale } = articleObj;
|
||||
const { content, title, author_id, category_id } = articleObj;
|
||||
return axios.post(`${this.url}/${portalSlug}/articles`, {
|
||||
content,
|
||||
title,
|
||||
author_id: authorId,
|
||||
category_id: categoryId,
|
||||
locale,
|
||||
author_id,
|
||||
category_id,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -133,14 +133,6 @@ class ConversationApi extends ApiClient {
|
||||
getAllAttachments(conversationId) {
|
||||
return axios.get(`${this.url}/${conversationId}/attachments`);
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -33,10 +33,8 @@ class IntegrationsAPI extends ApiClient {
|
||||
return axios.delete(`${this.baseUrl()}/integrations/hooks/${hookId}`);
|
||||
}
|
||||
|
||||
connectShopify({ shopDomain }) {
|
||||
return axios.post(`${this.baseUrl()}/integrations/shopify/auth`, {
|
||||
shop_domain: shopDomain,
|
||||
});
|
||||
fetchCaptainURL() {
|
||||
return axios.get(`${this.baseUrl()}/integrations/captain/sso_url`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
/* global axios */
|
||||
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
class ShopifyAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('integrations/shopify', { accountScoped: true });
|
||||
}
|
||||
|
||||
getOrders(contactId) {
|
||||
return axios.get(`${this.url}/orders`, {
|
||||
params: { contact_id: contactId },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new ShopifyAPI();
|
||||
@@ -1,20 +0,0 @@
|
||||
/* global axios */
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class LiveReportsAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('live_reports', { accountScoped: true, apiVersion: 'v2' });
|
||||
}
|
||||
|
||||
getConversationMetric(params = {}) {
|
||||
return axios.get(`${this.url}/conversation_metrics`, { params });
|
||||
}
|
||||
|
||||
getGroupedConversations({ groupBy } = { groupBy: 'assignee_id' }) {
|
||||
return axios.get(`${this.url}/grouped_conversation_metrics`, {
|
||||
params: { group_by: groupBy },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new LiveReportsAPI();
|
||||
@@ -61,9 +61,9 @@ class ReportsAPI extends ApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
getConversationTrafficCSV({ daysBefore = 6 } = {}) {
|
||||
getConversationTrafficCSV() {
|
||||
return axios.get(`${this.url}/conversation_traffic`, {
|
||||
params: { timezone_offset: getTimeOffset(), days_before: daysBefore },
|
||||
params: { timezone_offset: getTimeOffset() },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -14,29 +14,26 @@ class SearchAPI extends ApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
contacts({ q, page = 1 }) {
|
||||
contacts({ q }) {
|
||||
return axios.get(`${this.url}/contacts`, {
|
||||
params: {
|
||||
q,
|
||||
page: page,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
conversations({ q, page = 1 }) {
|
||||
conversations({ q }) {
|
||||
return axios.get(`${this.url}/conversations`, {
|
||||
params: {
|
||||
q,
|
||||
page: page,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
messages({ q, page = 1 }) {
|
||||
messages({ q }) {
|
||||
return axios.get(`${this.url}/messages`, {
|
||||
params: {
|
||||
q,
|
||||
page: page,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
/* global axios */
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class SummaryReportsAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('summary_reports', { accountScoped: true, apiVersion: 'v2' });
|
||||
}
|
||||
|
||||
getTeamReports({ since, until, businessHours } = {}) {
|
||||
return axios.get(`${this.url}/team`, {
|
||||
params: {
|
||||
since,
|
||||
until,
|
||||
business_hours: businessHours,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getAgentReports({ since, until, businessHours } = {}) {
|
||||
return axios.get(`${this.url}/agent`, {
|
||||
params: {
|
||||
since,
|
||||
until,
|
||||
business_hours: businessHours,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getInboxReports({ since, until, businessHours } = {}) {
|
||||
return axios.get(`${this.url}/inbox`, {
|
||||
params: {
|
||||
since,
|
||||
until,
|
||||
business_hours: businessHours,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new SummaryReportsAPI();
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
&.no-margin {
|
||||
.mx-input {
|
||||
margin-bottom: 0 !important;
|
||||
@apply mb-0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
}
|
||||
|
||||
.mx-input {
|
||||
@apply h-[2.5rem] flex border border-solid border-n-weak rounded-md shadow-none;
|
||||
@apply h-[2.5rem] flex border border-solid border-slate-200 dark:border-slate-600 rounded-md shadow-none;
|
||||
}
|
||||
|
||||
.mx-input:disabled,
|
||||
@@ -39,7 +39,7 @@
|
||||
}
|
||||
|
||||
.mx-datepicker-main {
|
||||
@apply border-0 bg-n-solid-2 rounded-xl;
|
||||
@apply border-0 bg-white dark:bg-slate-800;
|
||||
|
||||
.cell {
|
||||
&.disabled {
|
||||
@@ -53,14 +53,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.mx-calendar+.mx-calendar {
|
||||
@apply border-l border-n-weak;
|
||||
}
|
||||
|
||||
.mx-datepicker-footer {
|
||||
@apply border border-n-weak;
|
||||
}
|
||||
|
||||
.mx-time {
|
||||
@apply border-0 bg-white dark:bg-slate-800;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
@apply inline-block h-6 py-0 px-6 relative align-middle w-6;
|
||||
|
||||
&.message {
|
||||
@include normal-shadow;
|
||||
@apply bg-white dark:bg-slate-800 rounded-full left-0 my-3 mx-auto p-4 top-0;
|
||||
|
||||
&::before {
|
||||
|
||||
@@ -1,7 +1,79 @@
|
||||
@import 'dashboard/assets/scss/variables';
|
||||
@import 'widget/assets/scss/mixins';
|
||||
|
||||
$spinner-before-border-color: rgba(255, 255, 255, 0.7);
|
||||
|
||||
//borders
|
||||
@mixin border-nil() {
|
||||
border-color: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@mixin thin-border($color) {
|
||||
border: 1px solid $color;
|
||||
}
|
||||
|
||||
@mixin custom-border-bottom($size, $color) {
|
||||
border-bottom: $size solid $color;
|
||||
}
|
||||
|
||||
@mixin custom-border-top($size, $color) {
|
||||
border-top: $size solid $color;
|
||||
}
|
||||
|
||||
@mixin border-normal() {
|
||||
@apply border border-slate-50 dark:border-slate-700;
|
||||
}
|
||||
|
||||
@mixin border-normal-left() {
|
||||
@apply border-l border-slate-50 dark:border-slate-700;
|
||||
}
|
||||
|
||||
@mixin border-normal-top() {
|
||||
@apply border-t border-slate-50 dark:border-slate-700;
|
||||
}
|
||||
|
||||
@mixin border-normal-right() {
|
||||
@apply border-r border-slate-50 dark:border-slate-700;
|
||||
}
|
||||
|
||||
@mixin border-normal-bottom() {
|
||||
@apply border-b border-slate-50 dark:border-slate-700;
|
||||
}
|
||||
|
||||
@mixin border-light() {
|
||||
@apply border border-slate-25 dark:border-slate-700;
|
||||
}
|
||||
|
||||
@mixin border-light-left() {
|
||||
@apply border-l border-slate-25 dark:border-slate-700;
|
||||
}
|
||||
|
||||
@mixin border-light-top() {
|
||||
@apply border-t border-slate-25 dark:border-slate-700;
|
||||
}
|
||||
|
||||
@mixin border-light-right() {
|
||||
@apply border-r border-slate-25 dark:border-slate-700;
|
||||
}
|
||||
|
||||
@mixin border-light-bottom() {
|
||||
@apply border-b border-slate-25 dark:border-slate-700;
|
||||
}
|
||||
|
||||
// background
|
||||
@mixin background-gray() {
|
||||
background: $color-background;
|
||||
}
|
||||
|
||||
@mixin background-light() {
|
||||
@apply bg-slate-50 dark:bg-slate-800;
|
||||
}
|
||||
|
||||
@mixin background-white() {
|
||||
@apply bg-white dark:bg-slate-900;
|
||||
}
|
||||
|
||||
// input form
|
||||
@mixin ghost-input() {
|
||||
box-shadow: none;
|
||||
@@ -15,6 +87,65 @@ $spinner-before-border-color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
// flex-layout
|
||||
@mixin space-between() {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@mixin space-between-column() {
|
||||
@include space-between;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@mixin space-between-row() {
|
||||
@include space-between;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
@mixin flex-shrink() {
|
||||
flex: 0 0 auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@mixin flex-weight($value) {
|
||||
// Grab flex-grow for older browsers.
|
||||
$flex-grow: nth($value, 1);
|
||||
|
||||
// 2009
|
||||
@include prefixer(box-flex, $flex-grow, webkit moz spec);
|
||||
|
||||
// 2011 (IE 10), 2012
|
||||
@include prefixer(flex, $value, webkit moz ms spec);
|
||||
}
|
||||
|
||||
// full height
|
||||
@mixin full-height() {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@mixin round-corner() {
|
||||
border-radius: 1000px;
|
||||
}
|
||||
|
||||
@mixin scroll-on-hover() {
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@mixin horizontal-scroll() {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@mixin elegant-card() {
|
||||
@include normal-shadow;
|
||||
border-radius: $space-small;
|
||||
}
|
||||
|
||||
@mixin color-spinner() {
|
||||
@keyframes spinner {
|
||||
to {
|
||||
@@ -99,3 +230,17 @@ $spinner-before-border-color: rgba(255, 255, 255, 0.7);
|
||||
border-left: $size solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin text-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@mixin three-column-grid($column-one-width: 16rem,
|
||||
$column-three-width: 16rem) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: minmax($column-one-width, 6fr) 10fr minmax($column-three-width, 6fr);
|
||||
}
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
// scss-lint:disable PropertySortOrder
|
||||
@layer base {
|
||||
// NEXT COLORS START
|
||||
:root {
|
||||
// slate
|
||||
--slate-1: 252 252 253;
|
||||
--slate-2: 249 249 251;
|
||||
--slate-3: 240 240 243;
|
||||
--slate-4: 232 232 236;
|
||||
--slate-5: 224 225 230;
|
||||
--slate-6: 217 217 224;
|
||||
--slate-7: 205 206 214;
|
||||
--slate-8: 185 187 198;
|
||||
--slate-9: 139 141 152;
|
||||
--slate-10: 128 131 141;
|
||||
--slate-11: 96 100 108;
|
||||
--slate-12: 28 32 36;
|
||||
|
||||
--iris-1: 253 253 255;
|
||||
--iris-2: 248 248 255;
|
||||
--iris-3: 240 241 254;
|
||||
--iris-4: 230 231 255;
|
||||
--iris-5: 218 220 255;
|
||||
--iris-6: 203 205 255;
|
||||
--iris-7: 184 186 248;
|
||||
--iris-8: 155 158 240;
|
||||
--iris-9: 91 91 214;
|
||||
--iris-10: 81 81 205;
|
||||
--iris-11: 87 83 198;
|
||||
--iris-12: 39 41 98;
|
||||
|
||||
--ruby-1: 255 252 253;
|
||||
--ruby-2: 255 247 248;
|
||||
--ruby-3: 254 234 237;
|
||||
--ruby-4: 255 220 225;
|
||||
--ruby-5: 255 206 214;
|
||||
--ruby-6: 248 191 200;
|
||||
--ruby-7: 239 172 184;
|
||||
--ruby-8: 229 146 163;
|
||||
--ruby-9: 229 70 102;
|
||||
--ruby-10: 220 59 93;
|
||||
--ruby-11: 202 36 77;
|
||||
--ruby-12: 100 23 43;
|
||||
|
||||
--amber-1: 254 253 251;
|
||||
--amber-2: 254 251 233;
|
||||
--amber-3: 255 247 194;
|
||||
--amber-4: 255 238 156;
|
||||
--amber-5: 251 229 119;
|
||||
--amber-6: 243 214 115;
|
||||
--amber-7: 233 193 98;
|
||||
--amber-8: 226 163 54;
|
||||
--amber-9: 255 197 61;
|
||||
--amber-10: 255 186 24;
|
||||
--amber-11: 171 100 0;
|
||||
--amber-12: 79 52 34;
|
||||
|
||||
--teal-1: 250 254 253;
|
||||
--teal-2: 243 251 249;
|
||||
--teal-3: 224 248 243;
|
||||
--teal-4: 204 243 234;
|
||||
--teal-5: 184 234 224;
|
||||
--teal-6: 161 222 210;
|
||||
--teal-7: 131 205 193;
|
||||
--teal-8: 83 185 171;
|
||||
--teal-9: 18 165 148;
|
||||
--teal-10: 13 155 138;
|
||||
--teal-11: 0 133 115;
|
||||
--teal-12: 13 61 56;
|
||||
|
||||
--gray-1: 252 252 252;
|
||||
--gray-2: 249 249 249;
|
||||
--gray-3: 240 240 240;
|
||||
--gray-4: 232 232 232;
|
||||
--gray-5: 224 224 224;
|
||||
--gray-6: 217 217 217;
|
||||
--gray-7: 206 206 206;
|
||||
--gray-8: 187 187 187;
|
||||
--gray-9: 141 141 141;
|
||||
--gray-10: 131 131 131;
|
||||
--gray-11: 100 100 100;
|
||||
--gray-12: 32 32 32;
|
||||
|
||||
--background-color: 253 253 253;
|
||||
--text-blue: 8 109 224;
|
||||
--border-container: 236 236 236;
|
||||
--border-strong: 235 235 235;
|
||||
--border-weak: 234 234 234;
|
||||
--solid-1: 255 255 255;
|
||||
--solid-2: 255 255 255;
|
||||
--solid-3: 255 255 255;
|
||||
--solid-active: 255 255 255;
|
||||
--solid-amber: 252 232 193;
|
||||
--solid-blue: 218 236 255;
|
||||
--solid-iris: 230 231 255;
|
||||
|
||||
--alpha-1: 67, 67, 67, 0.06;
|
||||
--alpha-2: 201, 202, 207, 0.15;
|
||||
--alpha-3: 255, 255, 255, 0.96;
|
||||
--black-alpha-1: 0, 0, 0, 0.12;
|
||||
--black-alpha-2: 0, 0, 0, 0.04;
|
||||
--border-blue: 39, 129, 246, 0.5;
|
||||
--white-alpha: 255, 255, 255, 0.8;
|
||||
}
|
||||
|
||||
.dark {
|
||||
// slate
|
||||
--slate-1: 17 17 19;
|
||||
--slate-2: 24 25 27;
|
||||
--slate-3: 33 34 37;
|
||||
--slate-4: 39 42 45;
|
||||
--slate-5: 46 49 53;
|
||||
--slate-6: 54 58 63;
|
||||
--slate-7: 67 72 78;
|
||||
--slate-8: 90 97 105;
|
||||
--slate-9: 105 110 119;
|
||||
--slate-10: 119 123 132;
|
||||
--slate-11: 176 180 186;
|
||||
--slate-12: 237 238 240;
|
||||
|
||||
--iris-1: 19 19 30;
|
||||
--iris-2: 23 22 37;
|
||||
--iris-3: 32 34 72;
|
||||
--iris-4: 38 42 101;
|
||||
--iris-5: 48 51 116;
|
||||
--iris-6: 61 62 130;
|
||||
--iris-7: 74 74 149;
|
||||
--iris-8: 89 88 177;
|
||||
--iris-9: 91 91 214;
|
||||
--iris-10: 84 114 228;
|
||||
--iris-11: 158 177 255;
|
||||
--iris-12: 224 223 254;
|
||||
|
||||
--ruby-1: 25 17 19;
|
||||
--ruby-2: 30 21 23;
|
||||
--ruby-3: 58 20 30;
|
||||
--ruby-4: 78 19 37;
|
||||
--ruby-5: 94 26 46;
|
||||
--ruby-6: 111 37 57;
|
||||
--ruby-7: 136 52 71;
|
||||
--ruby-8: 179 68 90;
|
||||
--ruby-9: 229 70 102;
|
||||
--ruby-10: 236 90 114;
|
||||
--ruby-11: 255 148 157;
|
||||
--ruby-12: 254 210 225;
|
||||
|
||||
--amber-1: 22 18 12;
|
||||
--amber-2: 29 24 15;
|
||||
--amber-3: 48 32 8;
|
||||
--amber-4: 63 39 0;
|
||||
--amber-5: 77 48 0;
|
||||
--amber-6: 92 61 5;
|
||||
--amber-7: 113 79 25;
|
||||
--amber-8: 143 100 36;
|
||||
--amber-9: 255 197 61;
|
||||
--amber-10: 255 214 10;
|
||||
--amber-11: 255 202 22;
|
||||
--amber-12: 255 231 179;
|
||||
|
||||
--teal-1: 13 21 20;
|
||||
--teal-2: 17 28 27;
|
||||
--teal-3: 13 45 42;
|
||||
--teal-4: 2 59 55;
|
||||
--teal-5: 8 72 67;
|
||||
--teal-6: 20 87 80;
|
||||
--teal-7: 28 105 97;
|
||||
--teal-8: 32 126 115;
|
||||
--teal-9: 18 165 148;
|
||||
--teal-10: 14 179 158;
|
||||
--teal-11: 11 216 182;
|
||||
--teal-12: 173 240 221;
|
||||
|
||||
--gray-1: 17 17 17;
|
||||
--gray-2: 25 25 25;
|
||||
--gray-3: 34 34 34;
|
||||
--gray-4: 42 42 42;
|
||||
--gray-5: 49 49 49;
|
||||
--gray-6: 58 58 58;
|
||||
--gray-7: 72 72 72;
|
||||
--gray-8: 96 96 96;
|
||||
--gray-9: 110 110 110;
|
||||
--gray-10: 123 123 123;
|
||||
--gray-11: 180 180 180;
|
||||
--gray-12: 238 238 238;
|
||||
|
||||
--background-color: 18 18 19;
|
||||
--border-strong: 52 52 52;
|
||||
--border-weak: 38 38 42;
|
||||
--solid-1: 23 23 26;
|
||||
--solid-2: 29 30 36;
|
||||
--solid-3: 44 45 54;
|
||||
--solid-active: 53 57 66;
|
||||
--solid-amber: 42 37 30;
|
||||
--solid-blue: 16 49 91;
|
||||
--solid-iris: 38 42 101;
|
||||
--text-blue: 126 182 255;
|
||||
|
||||
--alpha-1: 36, 36, 36, 0.8;
|
||||
--alpha-2: 139, 147, 182, 0.15;
|
||||
--alpha-3: 36, 38, 45, 0.9;
|
||||
--black-alpha-1: 0, 0, 0, 0.3;
|
||||
--black-alpha-2: 0, 0, 0, 0.2;
|
||||
--border-blue: 39, 129, 246, 0.5;
|
||||
--border-container: 236, 236, 236, 0;
|
||||
--white-alpha: 255, 255, 255, 0.1;
|
||||
}
|
||||
}
|
||||
// NEXT COLORS END
|
||||
@@ -5,9 +5,6 @@
|
||||
@import 'shared/assets/fonts/InterDisplay/inter-display';
|
||||
@import 'shared/assets/fonts/inter';
|
||||
|
||||
// Next Colors
|
||||
@import 'next-colors';
|
||||
|
||||
@import 'shared/assets/stylesheets/animations';
|
||||
@import 'shared/assets/stylesheets/colors';
|
||||
@import 'shared/assets/stylesheets/spacing';
|
||||
@@ -49,27 +46,8 @@
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
.n-blue-border {
|
||||
@apply border-n-blue-border;
|
||||
}
|
||||
|
||||
.n-blue-text {
|
||||
@apply text-n-blue-text;
|
||||
}
|
||||
|
||||
.custom-dashed-border {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25'%3E%3Crect x='0' y='0' width='100%25' height='100%25' fill='none' rx='16' ry='16' stroke='%23E2E3E7' stroke-width='2' stroke-dasharray='6, 8' stroke-dashoffset='0' stroke-linecap='round'/%3E%3C/svg%3E");
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.dark .custom-dashed-border {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25'%3E%3Crect x='0' y='0' width='100%25' height='100%25' fill='none' rx='16' ry='16' stroke='%23343434' stroke-width='2' stroke-dasharray='6, 8' stroke-dashoffset='0' stroke-linecap='round'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
// scss-lint:disable PropertySortOrder
|
||||
@layer base {
|
||||
// scss-lint:disable PropertySortOrder
|
||||
:root {
|
||||
--color-amber-25: 254 253 251;
|
||||
--color-amber-50: 255 249 237;
|
||||
@@ -412,15 +390,3 @@
|
||||
--color-orange-900: 255 224 194;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.dropdown-pane {
|
||||
@apply border rounded-lg hidden relative invisible shadow-lg border-n-strong dark:border-n-strong box-content p-2 w-fit z-[9999];
|
||||
@apply border rounded-lg hidden relative invisible shadow-lg border-slate-25 dark:border-slate-700 box-content p-2 w-fit z-[9999];
|
||||
|
||||
&.dropdown-pane--open {
|
||||
@apply bg-n-alpha-3 backdrop-blur-[100px] absolute block visible;
|
||||
@apply bg-white absolute dark:bg-slate-800 block visible;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
@mixin label-multiselect-hover {
|
||||
&::after {
|
||||
@apply text-n-brand;
|
||||
@apply text-woot-600 dark:text-woot-600;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply bg-n-slate-3;
|
||||
@apply bg-slate-50 dark:bg-slate-700;
|
||||
|
||||
&::after {
|
||||
@apply text-n-blue-text;
|
||||
@apply text-woot-500 dark:text-woot-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,16 +18,20 @@
|
||||
}
|
||||
|
||||
&.multiselect--disabled {
|
||||
@apply opacity-50 rounded-lg cursor-not-allowed pointer-events-auto;
|
||||
@apply opacity-50 border border-slate-200 dark:border-slate-600 rounded-md cursor-not-allowed;
|
||||
|
||||
.multiselect__select {
|
||||
@apply cursor-not-allowed bg-transparent rounded-lg;
|
||||
@apply cursor-not-allowed bg-white dark:bg-slate-900 rounded-md;
|
||||
}
|
||||
|
||||
.multiselect__tags {
|
||||
@apply border-0;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect--active {
|
||||
> .multiselect__tags {
|
||||
@apply outline-n-blue-border;
|
||||
@apply border-woot-500 dark:border-woot-500;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +44,7 @@
|
||||
}
|
||||
|
||||
.multiselect__content-wrapper {
|
||||
@apply bg-n-alpha-black2 text-n-slate-12 backdrop-blur-[100px] border-0 border-none outline outline-1 outline-n-weak rounded-b-lg;
|
||||
@apply bg-white dark:bg-slate-900 border border-solid border-slate-200 dark:border-slate-600 text-slate-800 dark:text-slate-100;
|
||||
}
|
||||
|
||||
.multiselect__content {
|
||||
@@ -57,19 +61,15 @@
|
||||
@apply mb-0;
|
||||
}
|
||||
|
||||
&::after {
|
||||
@apply bottom-0 flex items-center justify-center text-center;
|
||||
}
|
||||
|
||||
&.multiselect__option--highlight {
|
||||
@apply bg-n-alpha-black2 text-n-slate-12;
|
||||
@apply bg-white dark:bg-slate-800 text-slate-800 dark:text-slate-100;
|
||||
}
|
||||
|
||||
&.multiselect__option--highlight:hover {
|
||||
@apply bg-n-brand/10 text-n-slate-12;
|
||||
@apply bg-woot-50 dark:bg-woot-600 text-slate-800 dark:text-slate-100;
|
||||
|
||||
&::after {
|
||||
@apply bg-transparent text-center text-n-slate-12;
|
||||
@apply bg-woot-50 dark:bg-woot-600 text-slate-600 dark:text-slate-200;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,17 +78,17 @@
|
||||
}
|
||||
|
||||
&.multiselect__option--selected {
|
||||
@apply bg-n-brand/20 text-n-slate-12;
|
||||
|
||||
&::after {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
@apply bg-woot-50 dark:bg-woot-600 text-slate-800 dark:text-slate-100;
|
||||
|
||||
&.multiselect__option--highlight:hover {
|
||||
@apply bg-transparent;
|
||||
@apply bg-woot-75 dark:bg-woot-600;
|
||||
|
||||
&::after {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
|
||||
&::after:hover {
|
||||
@apply text-n-slate-12 bg-transparent;
|
||||
@apply text-slate-800 dark:text-slate-100;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,27 +96,23 @@
|
||||
}
|
||||
|
||||
.multiselect__tags {
|
||||
@apply bg-n-alpha-black2 border-0 grid items-center w-full border-none outline-1 outline outline-n-weak hover:outline-n-slate-6 m-0 min-h-[2.875rem] rounded-lg pt-0;
|
||||
@apply bg-white dark:bg-slate-900 border border-solid border-slate-200 dark:border-slate-600 m-0 min-h-[2.875rem] pt-0;
|
||||
|
||||
input {
|
||||
@apply border-0 border-none bg-transparent dark:bg-transparent text-n-slate-12 placeholder:text-n-slate-10;
|
||||
@apply border-0 border-none;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect__spinner {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.multiselect__tags-wrap {
|
||||
@apply inline-block leading-none mt-1;
|
||||
}
|
||||
|
||||
.multiselect__placeholder {
|
||||
@apply text-n-slate-10 font-normal pt-3;
|
||||
@apply text-slate-400 dark:text-slate-400 font-normal pt-3;
|
||||
}
|
||||
|
||||
.multiselect__tag {
|
||||
@apply bg-n-alpha-white mt-1 text-n-slate-12 pr-6 pl-2.5 py-1.5;
|
||||
@apply bg-slate-50 dark:bg-slate-800 mt-1 text-slate-800 dark:text-slate-100 pr-6 pl-2.5 py-1.5;
|
||||
}
|
||||
|
||||
.multiselect__tag-icon {
|
||||
@@ -129,7 +125,7 @@
|
||||
}
|
||||
|
||||
.multiselect__single {
|
||||
@apply bg-transparent text-n-slate-12 inline-block mb-0 py-3 px-2.5 overflow-hidden whitespace-nowrap text-ellipsis;
|
||||
@apply bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 inline-block mb-0 py-3 px-2.5 overflow-hidden whitespace-nowrap text-ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,11 +143,11 @@
|
||||
}
|
||||
|
||||
> .multiselect__tags {
|
||||
@apply outline-transparent;
|
||||
@apply border-transparent;
|
||||
}
|
||||
|
||||
&.multiselect--active > .multiselect__tags {
|
||||
@apply outline-n-blue-border;
|
||||
@apply border-woot-500 dark:border-woot-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,7 +179,7 @@
|
||||
.multiselect__tags,
|
||||
.multiselect__input,
|
||||
.multiselect {
|
||||
@apply text-n-slate-12 rounded-lg text-sm min-h-[2.5rem];
|
||||
@apply bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 rounded-[5px] text-sm min-h-[2.5rem];
|
||||
}
|
||||
|
||||
.multiselect__input {
|
||||
@@ -191,7 +187,7 @@
|
||||
}
|
||||
|
||||
.multiselect__single {
|
||||
@apply items-center flex m-0 text-sm max-h-[2.375rem] bg-transparent text-n-slate-12 py-3 px-0.5;
|
||||
@apply items-center flex m-0 text-sm max-h-[2.375rem] text-slate-800 dark:text-slate-100 bg-white dark:bg-slate-900 py-3 px-0.5;
|
||||
}
|
||||
|
||||
.multiselect__placeholder {
|
||||
@@ -212,24 +208,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect--disabled {
|
||||
background-color: rgba(var(--black-alpha-2)) !important;
|
||||
|
||||
.multiselect__tags {
|
||||
@apply hover:outline-n-weak;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect--active {
|
||||
.multiselect__select::before {
|
||||
@apply top-[62%];
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect__select::before {
|
||||
top: 60% !important;
|
||||
}
|
||||
|
||||
.multiselect-wrap--medium {
|
||||
.multiselect__tags,
|
||||
.multiselect__input {
|
||||
@@ -239,16 +217,16 @@
|
||||
.multiselect__tags,
|
||||
.multiselect__input,
|
||||
.multiselect {
|
||||
@apply bg-n-alpha-black2 text-n-slate-12 text-sm h-12 min-h-[3rem];
|
||||
@apply bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 text-sm h-12 min-h-[3rem];
|
||||
}
|
||||
|
||||
.multiselect__input {
|
||||
@apply h-[2.875rem] min-h-[2.875rem];
|
||||
margin-bottom: 0 !important;
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
|
||||
.multiselect__single {
|
||||
@apply items-center flex m-0 text-sm py-1 px-0.5 bg-transparent text-n-slate-12;
|
||||
@apply items-center flex m-0 text-sm py-1 px-0.5 text-slate-800 dark:text-slate-100 bg-white dark:bg-slate-900;
|
||||
}
|
||||
|
||||
.multiselect__placeholder {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user