Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b7e6e5ab8 | ||
|
|
4a0f0e759f | ||
|
|
8b6fd7ae26 | ||
|
|
5104cdb458 | ||
|
|
ab509b7024 | ||
|
|
c55e159c03 | ||
|
|
9e3783f349 | ||
|
|
1c70da86e3 | ||
|
|
3901abc009 | ||
|
|
73fbfd5782 | ||
|
|
2c1658715e | ||
|
|
c798474f2b |
+129
-109
@@ -1,87 +1,84 @@
|
||||
version: 2.1
|
||||
orbs:
|
||||
node: circleci/node@6.1.0
|
||||
|
||||
# Ruby CircleCI 2.0 configuration file
|
||||
#
|
||||
# Check https://circleci.com/docs/2.0/language-ruby/ for more details
|
||||
#
|
||||
version: 2
|
||||
defaults: &defaults
|
||||
working_directory: ~/build
|
||||
machine:
|
||||
image: ubuntu-2204:2024.05.1
|
||||
resource_class: large
|
||||
docker:
|
||||
# specify the version you desire here
|
||||
- image: cimg/ruby:3.3.3-browsers
|
||||
|
||||
# Specify service dependencies here if necessary
|
||||
# CircleCI maintains a library of pre-built images
|
||||
# documented at https://circleci.com/docs/2.0/circleci-images/
|
||||
- image: cimg/postgres:15.3
|
||||
- image: cimg/redis:6.2.6
|
||||
environment:
|
||||
RAILS_LOG_TO_STDOUT: false
|
||||
COVERAGE: true
|
||||
LOG_LEVEL: warn
|
||||
- RAILS_LOG_TO_STDOUT: false
|
||||
- COVERAGE: true
|
||||
- LOG_LEVEL: warn
|
||||
parallelism: 4
|
||||
resource_class: large
|
||||
|
||||
jobs:
|
||||
build:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- checkout
|
||||
- node/install:
|
||||
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: Install System Dependencies
|
||||
name: Configure Bundler
|
||||
command: |
|
||||
sudo apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt-get install -y \
|
||||
libpq-dev \
|
||||
redis-server \
|
||||
postgresql \
|
||||
build-essential \
|
||||
git \
|
||||
curl \
|
||||
libssl-dev \
|
||||
zlib1g-dev \
|
||||
libreadline-dev \
|
||||
libyaml-dev \
|
||||
openjdk-11-jdk \
|
||||
jq \
|
||||
software-properties-common \
|
||||
ca-certificates \
|
||||
imagemagick \
|
||||
libxml2-dev \
|
||||
libxslt1-dev \
|
||||
file \
|
||||
g++ \
|
||||
gcc \
|
||||
autoconf \
|
||||
gnupg2 \
|
||||
patch \
|
||||
ruby-dev \
|
||||
liblzma-dev \
|
||||
libgmp-dev \
|
||||
libncurses5-dev \
|
||||
libffi-dev \
|
||||
libgdbm6 \
|
||||
libgdbm-dev \
|
||||
libvips
|
||||
|
||||
- run:
|
||||
name: Install RVM and Ruby 3.3.3
|
||||
command: |
|
||||
sudo apt-get install -y gpg
|
||||
gpg --keyserver hkp://keyserver.ubuntu.com --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
|
||||
\curl -sSL https://get.rvm.io | bash -s stable
|
||||
echo 'source ~/.rvm/scripts/rvm' >> $BASH_ENV
|
||||
source ~/.rvm/scripts/rvm
|
||||
rvm install "3.3.3"
|
||||
rvm use 3.3.3 --default
|
||||
echo 'export BUNDLER_VERSION=$(cat Gemfile.lock | tail -1 | tr -d " ")' >> $BASH_ENV
|
||||
source $BASH_ENV
|
||||
gem install bundler
|
||||
|
||||
- run:
|
||||
name: Install Application Dependencies
|
||||
name: Which bundler?
|
||||
command: bundle -v
|
||||
|
||||
- run:
|
||||
name: Swap node versions
|
||||
command: |
|
||||
source ~/.rvm/scripts/rvm
|
||||
bundle install
|
||||
# pnpm install
|
||||
set +e
|
||||
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
|
||||
export NVM_DIR="$HOME/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
|
||||
nvm install v20
|
||||
echo 'export NVM_DIR="$HOME/.nvm"' >> $BASH_ENV
|
||||
echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $BASH_ENV
|
||||
|
||||
# Run bundler
|
||||
# Load installed gems from cache if possible, bundle install then save cache
|
||||
# Multiple caches are used to increase the chance of a cache hit
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-v20220524-{{ checksum "Gemfile.lock" }}
|
||||
|
||||
- run: bundle install --frozen --path ~/.bundle
|
||||
- save_cache:
|
||||
paths:
|
||||
- ~/.bundle
|
||||
key: chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-v20220524-{{ checksum "Gemfile.lock" }}
|
||||
|
||||
# Only necessary if app uses webpacker or yarn in some other way
|
||||
- restore_cache:
|
||||
keys:
|
||||
- chatwoot-yarn-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }}
|
||||
- chatwoot-yarn-
|
||||
|
||||
- run:
|
||||
name: yarn
|
||||
command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
|
||||
|
||||
# Store yarn / webpacker cache
|
||||
- save_cache:
|
||||
key: chatwoot-yarn-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }}
|
||||
paths:
|
||||
- ~/.cache/yarn
|
||||
|
||||
- run:
|
||||
name: Download cc-test-reporter
|
||||
@@ -89,8 +86,12 @@ jobs:
|
||||
mkdir -p ~/tmp
|
||||
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
|
||||
chmod +x ~/tmp/cc-test-reporter
|
||||
- persist_to_workspace:
|
||||
root: ~/tmp
|
||||
paths:
|
||||
- cc-test-reporter
|
||||
|
||||
# Swagger verification
|
||||
# verify swagger specification
|
||||
- run:
|
||||
name: Verify swagger API specification
|
||||
command: |
|
||||
@@ -103,62 +104,45 @@ jobs:
|
||||
curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/6.3.0/openapi-generator-cli-6.3.0.jar > ~/tmp/openapi-generator-cli-6.3.0.jar
|
||||
java -jar ~/tmp/openapi-generator-cli-6.3.0.jar validate -i swagger/swagger.json
|
||||
|
||||
# we remove the FRONTED_URL from the .env before running the tests
|
||||
- run:
|
||||
name: Database Setup and Configure Environment Variables
|
||||
command: |
|
||||
pg_pass=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 15 ; echo '')
|
||||
sed -i "s/REPLACE_WITH_PASSWORD/${pg_pass}/g" ${PWD}/.circleci/setup_chatwoot.sql
|
||||
chmod 644 ${PWD}/.circleci/setup_chatwoot.sql
|
||||
mv ${PWD}/.circleci/setup_chatwoot.sql /tmp/
|
||||
sudo -i -u postgres psql -f /tmp/setup_chatwoot.sql
|
||||
cp .env.example .env
|
||||
sed -i '/^FRONTEND_URL/d' .env
|
||||
sed -i -e '/REDIS_URL/ s/=.*/=redis:\/\/localhost:6379/' .env
|
||||
sed -i -e '/POSTGRES_HOST/ s/=.*/=localhost/' .env
|
||||
sed -i -e '/POSTGRES_USERNAME/ s/=.*/=chatwoot/' .env
|
||||
sed -i -e "/POSTGRES_PASSWORD/ s/=.*/=$pg_pass/" .env
|
||||
echo -en "\nINSTALLATION_ENV=circleci" >> ".env"
|
||||
|
||||
# Database setup
|
||||
- run:
|
||||
name: Run DB migrations
|
||||
command: bundle exec rails db:chatwoot_prepare
|
||||
- run: bundle exec rake db:create
|
||||
- run: bundle exec rake db:schema:load
|
||||
|
||||
# Bundle audit
|
||||
- run:
|
||||
name: Bundle audit
|
||||
command: bundle exec bundle audit update && bundle exec bundle audit check -v
|
||||
|
||||
# Rubocop linting
|
||||
- run:
|
||||
name: Rubocop
|
||||
command: bundle exec rubocop
|
||||
|
||||
# ESLint linting
|
||||
# - run:
|
||||
# name: Brakeman
|
||||
# command: bundle exec brakeman
|
||||
|
||||
- run:
|
||||
name: eslint
|
||||
command: pnpm run eslint
|
||||
command: yarn run eslint
|
||||
|
||||
# Run frontend tests
|
||||
- run:
|
||||
name: Run frontend tests
|
||||
command: |
|
||||
mkdir -p ~/build/coverage/frontend
|
||||
mkdir -p ~/tmp/test-results/frontend_specs
|
||||
~/tmp/cc-test-reporter before-build
|
||||
pnpm run test:coverage
|
||||
|
||||
yarn test:coverage
|
||||
- run:
|
||||
name: Code Climate Test Coverage (Frontend)
|
||||
name: Code Climate Test Coverage
|
||||
command: |
|
||||
~/tmp/cc-test-reporter format-coverage -t lcov -o "~/build/coverage/frontend/codeclimate.frontend_$CIRCLE_NODE_INDEX.json"
|
||||
~/tmp/cc-test-reporter format-coverage -t lcov -o "coverage/codeclimate.frontend_$CIRCLE_NODE_INDEX.json"
|
||||
|
||||
# Run backend tests
|
||||
# Run rails tests
|
||||
- run:
|
||||
name: Run backend tests
|
||||
command: |
|
||||
mkdir -p ~/tmp/test-results/rspec
|
||||
mkdir -p ~/tmp/test-artifacts
|
||||
mkdir -p ~/build/coverage/backend
|
||||
mkdir -p coverage
|
||||
~/tmp/cc-test-reporter before-build
|
||||
TESTFILES=$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
|
||||
bundle exec rspec --format progress \
|
||||
@@ -166,18 +150,54 @@ jobs:
|
||||
--out ~/tmp/test-results/rspec.xml \
|
||||
-- ${TESTFILES}
|
||||
no_output_timeout: 30m
|
||||
|
||||
- run:
|
||||
name: Code Climate Test Coverage (Backend)
|
||||
name: Code Climate Test Coverage
|
||||
command: |
|
||||
~/tmp/cc-test-reporter format-coverage -t simplecov -o "~/build/coverage/backend/codeclimate.$CIRCLE_NODE_INDEX.json"
|
||||
|
||||
- run:
|
||||
name: List coverage directory contents
|
||||
command: |
|
||||
ls -R ~/build/coverage
|
||||
~/tmp/cc-test-reporter format-coverage -t simplecov -o "coverage/codeclimate.$CIRCLE_NODE_INDEX.json"
|
||||
|
||||
- persist_to_workspace:
|
||||
root: ~/build
|
||||
root: coverage
|
||||
paths:
|
||||
- coverage
|
||||
- codeclimate.*.json
|
||||
# collect reports
|
||||
- store_test_results:
|
||||
path: ~/tmp/test-results
|
||||
- store_artifacts:
|
||||
path: ~/tmp/test-artifacts
|
||||
- store_artifacts:
|
||||
path: log
|
||||
|
||||
upload-coverage:
|
||||
working_directory: ~/build
|
||||
docker:
|
||||
# specify the version you desire here
|
||||
- image: circleci/ruby:3.0.2-node-browsers
|
||||
environment:
|
||||
- CC_TEST_REPORTER_ID: caf26a895e937974a90860cfadfded20891cfd1373a5aaafb3f67406ab9d433f
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/build
|
||||
- run:
|
||||
name: Download cc-test-reporter
|
||||
command: |
|
||||
mkdir -p ~/tmp
|
||||
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
|
||||
chmod +x ~/tmp/cc-test-reporter
|
||||
- persist_to_workspace:
|
||||
root: ~/tmp
|
||||
paths:
|
||||
- cc-test-reporter
|
||||
- run:
|
||||
name: Upload coverage results to Code Climate
|
||||
command: |
|
||||
~/tmp/cc-test-reporter sum-coverage --output - codeclimate.*.json | ~/tmp/cc-test-reporter upload-coverage --debug --input -
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
|
||||
commit:
|
||||
jobs:
|
||||
- build
|
||||
- upload-coverage:
|
||||
requires:
|
||||
- build
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
CREATE USER chatwoot CREATEDB;
|
||||
ALTER USER chatwoot PASSWORD 'REPLACE_WITH_PASSWORD';
|
||||
ALTER ROLE chatwoot SUPERUSER;
|
||||
|
||||
UPDATE pg_database SET datistemplate = FALSE WHERE datname = 'template1';
|
||||
DROP DATABASE template1;
|
||||
CREATE DATABASE template1 WITH TEMPLATE = template0 ENCODING = 'UNICODE';
|
||||
UPDATE pg_database SET datistemplate = TRUE WHERE datname = 'template1';
|
||||
|
||||
\c template1;
|
||||
VACUUM FREEZE;
|
||||
+1
-2
@@ -27,8 +27,7 @@ checks:
|
||||
threshold: 50
|
||||
exclude_patterns:
|
||||
- 'spec/'
|
||||
- '**/specs/**/**'
|
||||
- '**/spec/**/**'
|
||||
- '**/specs/'
|
||||
- 'db/*'
|
||||
- 'bin/**/*'
|
||||
- 'db/**/*'
|
||||
|
||||
+13
-26
@@ -2,36 +2,16 @@ module.exports = {
|
||||
extends: [
|
||||
'airbnb-base/legacy',
|
||||
'prettier',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'plugin:vitest-globals/recommended',
|
||||
'plugin:vue/recommended',
|
||||
'plugin:storybook/recommended',
|
||||
'plugin:cypress/recommended',
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*.spec.{j,t}s?(x)'],
|
||||
env: {
|
||||
'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: {
|
||||
ecmaVersion: 'latest',
|
||||
parser: '@babel/eslint-parser',
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['html', 'prettier', 'babel'],
|
||||
rules: {
|
||||
'prettier/prettier': ['error'],
|
||||
camelcase: 'off',
|
||||
@@ -230,6 +210,13 @@ module.exports = {
|
||||
'import/extensions': ['off'],
|
||||
'no-console': 'error',
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
webpack: {
|
||||
config: 'config/webpack/resolve.js',
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
name: Frontend Lint & Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
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
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.3.0
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install pnpm dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run eslint
|
||||
run: pnpm run eslint
|
||||
|
||||
- name: Run frontend tests with coverage
|
||||
run: |
|
||||
mkdir -p coverage
|
||||
pnpm run test:coverage
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
image: postgres:15.3
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: ''
|
||||
POSTGRES_PASSWORD: ""
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
ports:
|
||||
@@ -41,49 +41,47 @@ jobs:
|
||||
options: --entrypoint redis-server
|
||||
|
||||
steps:
|
||||
- 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 }}
|
||||
- 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: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: yarn
|
||||
|
||||
- name: Install pnpm dependencies
|
||||
run: pnpm i
|
||||
- name: yarn
|
||||
run: yarn install
|
||||
|
||||
- name: Strip enterprise code
|
||||
run: |
|
||||
rm -rf enterprise
|
||||
rm -rf spec/enterprise
|
||||
- name: Strip enterprise code
|
||||
run: |
|
||||
rm -rf enterprise
|
||||
rm -rf spec/enterprise
|
||||
|
||||
- name: Create database
|
||||
run: bundle exec rake db:create
|
||||
- name: Create database
|
||||
run: bundle exec rake db:create
|
||||
|
||||
- name: Seed database
|
||||
run: bundle exec rake db:schema:load
|
||||
- name: Seed database
|
||||
run: bundle exec rake db:schema:load
|
||||
|
||||
- name: Run frontend tests
|
||||
run: pnpm run test:coverage
|
||||
- name: yarn check-files
|
||||
run: yarn install --check-files
|
||||
|
||||
# Run rails tests
|
||||
- name: Run backend tests
|
||||
run: |
|
||||
bundle exec rspec --profile=10 --format documentation
|
||||
env:
|
||||
NODE_OPTIONS: --openssl-legacy-provider
|
||||
# Run rails tests
|
||||
- name: Run backend tests
|
||||
run: |
|
||||
bundle exec rspec --profile=10 --format documentation
|
||||
env:
|
||||
NODE_OPTIONS: --openssl-legacy-provider
|
||||
|
||||
- name: Upload rails log folder
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: rails-log-folder
|
||||
path: log
|
||||
- name: Upload rails log folder
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: rails-log-folder
|
||||
path: log
|
||||
|
||||
@@ -49,17 +49,13 @@ jobs:
|
||||
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
|
||||
cache: yarn
|
||||
|
||||
- name: pnpm
|
||||
run: pnpm install
|
||||
- name: yarn
|
||||
run: yarn install
|
||||
|
||||
- name: Create database
|
||||
run: bundle exec rake db:create
|
||||
|
||||
@@ -19,33 +19,23 @@ jobs:
|
||||
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'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: pnpm
|
||||
run: pnpm install
|
||||
- name: yarn
|
||||
run: yarn install
|
||||
|
||||
- name: Strip enterprise code
|
||||
run: |
|
||||
rm -rf enterprise
|
||||
rm -rf spec/enterprise
|
||||
|
||||
- name: setup env
|
||||
run: |
|
||||
cp .env.example .env
|
||||
|
||||
- name: Run asset compile
|
||||
run: bundle exec rake assets:precompile
|
||||
env:
|
||||
RAILS_ENV: production
|
||||
NODE_OPTIONS: --openssl-legacy-provider
|
||||
|
||||
- name: Size Check
|
||||
run: pnpm run size
|
||||
|
||||
|
||||
run: yarn run size
|
||||
|
||||
+1
-15
@@ -32,16 +32,6 @@ master.key
|
||||
|
||||
public/uploads
|
||||
public/packs*
|
||||
public/assets/administrate*
|
||||
public/assets/action*.js
|
||||
public/assets/activestorage*.js
|
||||
public/assets/trix*
|
||||
public/assets/belongs_to*.js
|
||||
public/assets/manifest*.js
|
||||
public/assets/manifest*.js
|
||||
public/assets/*.js.gz
|
||||
public/assets/secretField*
|
||||
public/assets/.sprockets-manifest-*.json
|
||||
|
||||
# VIM files
|
||||
*.swp
|
||||
@@ -85,8 +75,4 @@ yalc.lock
|
||||
yarn-debug.log*
|
||||
.yarn-integrity
|
||||
|
||||
# Vite Ruby
|
||||
/public/vite*
|
||||
# Vite uses dotenv and suggests to ignore local-only env files. See
|
||||
# https://vitejs.dev/guide/env-and-mode.html#env-files
|
||||
*.local
|
||||
/storybook-static
|
||||
+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
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
const path = require('path');
|
||||
const resolve = require('../config/webpack/resolve');
|
||||
|
||||
// Chatwoot's webpack.config.js
|
||||
process.env.NODE_ENV = 'development';
|
||||
const custom = require('../config/webpack/environment');
|
||||
|
||||
module.exports = {
|
||||
stories: [
|
||||
'../stories/**/*.stories.mdx',
|
||||
'../app/javascript/**/*.stories.@(js|jsx|ts|tsx)',
|
||||
],
|
||||
addons: [
|
||||
{
|
||||
name: '@storybook/addon-docs',
|
||||
options: {
|
||||
vueDocgenOptions: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '../'),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
{
|
||||
/**
|
||||
* Fix Storybook issue with PostCSS@8
|
||||
* @see https://github.com/storybookjs/storybook/issues/12668#issuecomment-773958085
|
||||
*/
|
||||
name: '@storybook/addon-postcss',
|
||||
options: {
|
||||
postcssLoaderOptions: {
|
||||
implementation: require('postcss'),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
webpackFinal: config => {
|
||||
const newConfig = {
|
||||
...config,
|
||||
resolve: {
|
||||
...config.resolve,
|
||||
modules: custom.resolvedModules.map(i => i.value),
|
||||
},
|
||||
};
|
||||
|
||||
newConfig.module.rules.push({
|
||||
test: /\.scss$/,
|
||||
use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'],
|
||||
include: path.resolve(__dirname, '../app/javascript'),
|
||||
});
|
||||
|
||||
return newConfig;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import { addDecorator } from '@storybook/vue';
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import VueI18n from 'vue-i18n';
|
||||
import Multiselect from 'vue-multiselect';
|
||||
import VueDOMPurifyHTML from 'vue-dompurify-html';
|
||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon';
|
||||
|
||||
import WootUiKit from '../app/javascript/dashboard/components';
|
||||
import i18n from '../app/javascript/dashboard/i18n';
|
||||
import { domPurifyConfig } from 'shared/helpers/HTMLSanitizer';
|
||||
|
||||
import '../app/javascript/dashboard/assets/scss/storybook.scss';
|
||||
|
||||
Vue.use(VueI18n);
|
||||
Vue.use(WootUiKit);
|
||||
Vue.use(Vuex);
|
||||
Vue.use(VueDOMPurifyHTML, domPurifyConfig);
|
||||
|
||||
Vue.component('multiselect', Multiselect);
|
||||
Vue.component('fluent-icon', FluentIcon);
|
||||
|
||||
const store = new Vuex.Store({});
|
||||
const i18nConfig = new VueI18n({
|
||||
locale: 'en',
|
||||
messages: i18n,
|
||||
});
|
||||
|
||||
addDecorator(() => ({
|
||||
template: '<story/>',
|
||||
i18n: i18nConfig,
|
||||
store,
|
||||
beforeCreate: function () {
|
||||
this.$root._i18n = this.$i18n;
|
||||
},
|
||||
}));
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -64,7 +64,7 @@ gem 'activerecord-import'
|
||||
gem 'dotenv-rails', '>= 3.0.0'
|
||||
gem 'foreman'
|
||||
gem 'puma'
|
||||
gem 'vite_rails'
|
||||
gem 'webpacker'
|
||||
# metrics on heroku
|
||||
gem 'barnes'
|
||||
|
||||
@@ -204,6 +204,8 @@ group :development do
|
||||
end
|
||||
|
||||
group :test do
|
||||
# Cypress in rails.
|
||||
gem 'cypress-on-rails'
|
||||
# fast cleaning of database
|
||||
gem 'database_cleaner'
|
||||
# mock http calls
|
||||
@@ -226,7 +228,7 @@ group :development, :test do
|
||||
gem 'mock_redis'
|
||||
gem 'pry-rails'
|
||||
gem 'rspec_junit_formatter'
|
||||
gem 'rspec-rails', '>= 6.1.5'
|
||||
gem 'rspec-rails', '>= 6.1.4'
|
||||
gem 'rubocop', require: false
|
||||
gem 'rubocop-performance', require: false
|
||||
gem 'rubocop-rails', require: false
|
||||
|
||||
+85
-84
@@ -33,70 +33,70 @@ GIT
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (7.0.8.5)
|
||||
actionpack (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
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.5)
|
||||
actionpack (= 7.0.8.5)
|
||||
activejob (= 7.0.8.5)
|
||||
activerecord (= 7.0.8.5)
|
||||
activestorage (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
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.5)
|
||||
actionpack (= 7.0.8.5)
|
||||
actionview (= 7.0.8.5)
|
||||
activejob (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
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.5)
|
||||
actionview (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
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.5)
|
||||
actionpack (= 7.0.8.5)
|
||||
activerecord (= 7.0.8.5)
|
||||
activestorage (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
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.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
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.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
activejob (7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
activerecord (7.0.8.5)
|
||||
activemodel (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
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.5)
|
||||
actionpack (= 7.0.8.5)
|
||||
activejob (= 7.0.8.5)
|
||||
activerecord (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
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.5)
|
||||
activesupport (7.0.8.4)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
@@ -178,6 +178,8 @@ GEM
|
||||
csv (3.3.0)
|
||||
csv-safe (3.3.1)
|
||||
csv (~> 3.0)
|
||||
cypress-on-rails (1.16.0)
|
||||
rack
|
||||
database_cleaner (2.0.2)
|
||||
database_cleaner-active_record (>= 2, < 3)
|
||||
database_cleaner-active_record (2.1.0)
|
||||
@@ -220,7 +222,6 @@ GEM
|
||||
railties (>= 6.1)
|
||||
down (5.4.0)
|
||||
addressable (~> 2.8)
|
||||
dry-cli (1.1.0)
|
||||
ecma-re-validator (0.4.0)
|
||||
regexp_parser (~> 2.2)
|
||||
elastic-apm (4.6.2)
|
||||
@@ -321,10 +322,10 @@ GEM
|
||||
google-cloud-translate-v3 (0.10.0)
|
||||
gapic-common (>= 0.20.0, < 2.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-protobuf (3.25.5)
|
||||
google-protobuf (3.25.5-arm64-darwin)
|
||||
google-protobuf (3.25.5-x86_64-darwin)
|
||||
google-protobuf (3.25.5-x86_64-linux)
|
||||
google-protobuf (3.25.3)
|
||||
google-protobuf (3.25.3-arm64-darwin)
|
||||
google-protobuf (3.25.3-x86_64-darwin)
|
||||
google-protobuf (3.25.3-x86_64-linux)
|
||||
googleapis-common-protos (1.6.0)
|
||||
google-protobuf (>= 3.18, < 5.a)
|
||||
googleapis-common-protos-types (~> 1.7)
|
||||
@@ -373,7 +374,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)
|
||||
@@ -479,7 +480,7 @@ GEM
|
||||
uri
|
||||
net-http-persistent (4.0.2)
|
||||
connection_pool (~> 2.2)
|
||||
net-imap (0.4.17)
|
||||
net-imap (0.4.14)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
@@ -495,14 +496,14 @@ GEM
|
||||
newrelic_rpm (9.6.0)
|
||||
base64
|
||||
nio4r (2.7.3)
|
||||
nokogiri (1.16.7)
|
||||
nokogiri (1.16.6)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.7-arm64-darwin)
|
||||
nokogiri (1.16.6-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.7-x86_64-darwin)
|
||||
nokogiri (1.16.6-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.7-x86_64-linux)
|
||||
nokogiri (1.16.6-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
oauth (1.1.0)
|
||||
oauth-tty (~> 1.0, >= 1.0.1)
|
||||
@@ -551,13 +552,13 @@ GEM
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (6.0.0)
|
||||
puma (6.4.3)
|
||||
puma (6.4.2)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.3.0)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (2.2.10)
|
||||
racc (1.8.0)
|
||||
rack (2.2.9)
|
||||
rack-attack (6.7.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-contrib (2.5.0)
|
||||
@@ -569,25 +570,25 @@ GEM
|
||||
rack-protection (3.2.0)
|
||||
base64 (>= 0.1.0)
|
||||
rack (~> 2.2, >= 2.2.4)
|
||||
rack-proxy (0.7.7)
|
||||
rack-proxy (0.7.6)
|
||||
rack
|
||||
rack-test (2.1.0)
|
||||
rack (>= 1.3)
|
||||
rack-timeout (0.6.3)
|
||||
rails (7.0.8.5)
|
||||
actioncable (= 7.0.8.5)
|
||||
actionmailbox (= 7.0.8.5)
|
||||
actionmailer (= 7.0.8.5)
|
||||
actionpack (= 7.0.8.5)
|
||||
actiontext (= 7.0.8.5)
|
||||
actionview (= 7.0.8.5)
|
||||
activejob (= 7.0.8.5)
|
||||
activemodel (= 7.0.8.5)
|
||||
activerecord (= 7.0.8.5)
|
||||
activestorage (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
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.5)
|
||||
railties (= 7.0.8.4)
|
||||
rails-dom-testing (2.2.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
@@ -595,9 +596,9 @@ GEM
|
||||
rails-html-sanitizer (1.6.0)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (~> 1.14)
|
||||
railties (7.0.8.5)
|
||||
actionpack (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
railties (7.0.8.4)
|
||||
actionpack (= 7.0.8.4)
|
||||
activesupport (= 7.0.8.4)
|
||||
method_source
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0)
|
||||
@@ -637,16 +638,16 @@ GEM
|
||||
strscan
|
||||
rspec-core (3.13.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-expectations (3.13.2)
|
||||
rspec-expectations (3.13.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-mocks (3.13.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-rails (7.0.1)
|
||||
actionpack (>= 7.0)
|
||||
activesupport (>= 7.0)
|
||||
railties (>= 7.0)
|
||||
rspec-rails (6.1.4)
|
||||
actionpack (>= 6.1)
|
||||
activesupport (>= 6.1)
|
||||
railties (>= 6.1)
|
||||
rspec-core (~> 3.13)
|
||||
rspec-expectations (~> 3.13)
|
||||
rspec-mocks (~> 3.13)
|
||||
@@ -708,6 +709,7 @@ GEM
|
||||
activerecord (>= 4)
|
||||
activesupport (>= 4)
|
||||
selectize-rails (0.12.6)
|
||||
semantic_range (3.0.0)
|
||||
sentry-rails (5.19.0)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby (~> 5.19.0)
|
||||
@@ -798,13 +800,6 @@ GEM
|
||||
activemodel (>= 3.2)
|
||||
mail (~> 2.5)
|
||||
version_gem (1.1.4)
|
||||
vite_rails (3.0.17)
|
||||
railties (>= 5.1, < 8)
|
||||
vite_ruby (~> 3.0, >= 3.2.2)
|
||||
vite_ruby (3.8.0)
|
||||
dry-cli (>= 0.7, < 2)
|
||||
rack-proxy (~> 0.6, >= 0.6.1)
|
||||
zeitwerk (~> 2.2)
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
web-console (4.2.1)
|
||||
@@ -819,7 +814,12 @@ GEM
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webrick (1.8.2)
|
||||
webpacker (5.4.4)
|
||||
activesupport (>= 5.2)
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 5.2)
|
||||
semantic_range (>= 2.3.0)
|
||||
webrick (1.8.1)
|
||||
websocket-driver (0.7.6)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
@@ -827,7 +827,7 @@ GEM
|
||||
working_hours (1.4.1)
|
||||
activesupport (>= 3.2)
|
||||
tzinfo
|
||||
zeitwerk (2.6.17)
|
||||
zeitwerk (2.6.16)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-20
|
||||
@@ -862,6 +862,7 @@ DEPENDENCIES
|
||||
climate_control
|
||||
commonmarker
|
||||
csv-safe
|
||||
cypress-on-rails
|
||||
database_cleaner
|
||||
ddtrace
|
||||
debug (~> 1.8)
|
||||
@@ -929,7 +930,7 @@ DEPENDENCIES
|
||||
responders (>= 3.1.1)
|
||||
rest-client
|
||||
reverse_markdown
|
||||
rspec-rails (>= 6.1.5)
|
||||
rspec-rails (>= 6.1.4)
|
||||
rspec_junit_formatter
|
||||
rubocop
|
||||
rubocop-performance
|
||||
@@ -959,10 +960,10 @@ DEPENDENCIES
|
||||
tzinfo-data
|
||||
uglifier
|
||||
valid_email2
|
||||
vite_rails
|
||||
web-console (>= 4.2.1)
|
||||
web-push (>= 3.0.1)
|
||||
webmock
|
||||
webpacker
|
||||
wisper (= 2.0.0)
|
||||
working_hours
|
||||
|
||||
|
||||
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
backend: bin/rails s -p 3000
|
||||
frontend: export NODE_OPTIONS=--openssl-legacy-provider && bin/webpack-dev-server
|
||||
# https://github.com/mperham/sidekiq/issues/3090#issuecomment-389748695
|
||||
worker: dotenv bundle exec sidekiq -C config/sidekiq.yml
|
||||
vite: bin/vite dev
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
backend: RAILS_ENV=test bin/rails s -p 5050
|
||||
vite: bin/vite dev
|
||||
frontend: export NODE_OPTIONS=--openssl-legacy-provider && bin/webpack-dev-server
|
||||
worker: RAILS_ENV=test dotenv bundle exec sidekiq -C config/sidekiq.yml
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
## 🚨 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
|
||||
# Chatwoot
|
||||
|
||||
Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.
|
||||
<p>
|
||||
@@ -101,7 +98,7 @@ Chatwoot now supports 1-Click deployment to DigitalOcean as a kubernetes app.
|
||||
|
||||
### Other deployment options
|
||||
|
||||
For other supported options, checkout our [deployment page](https://chatwoot.com/deploy).
|
||||
For other supported options, checkout our [deployment page](https://chatwoot.com/deploy).
|
||||
|
||||
## Security
|
||||
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
3.13.0
|
||||
3.9.0
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
3.0.0
|
||||
2.8.0
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
//= link administrate/application.css
|
||||
//= link administrate/application.js
|
||||
//= link administrate-field-active_storage/application.css
|
||||
//= link dashboardChart.js
|
||||
//= link secretField.js
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// eslint-disable-next-line
|
||||
function prepareData(data) {
|
||||
var labels = [];
|
||||
var dataSet = [];
|
||||
data.forEach(item => {
|
||||
labels.push(item[0]);
|
||||
dataSet.push(item[1]);
|
||||
});
|
||||
return { labels, dataSet };
|
||||
}
|
||||
|
||||
function getChartOptions() {
|
||||
var fontFamily =
|
||||
'PlusJakarta,-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
|
||||
return {
|
||||
responsive: true,
|
||||
legend: { labels: { fontFamily } },
|
||||
scales: {
|
||||
xAxes: [
|
||||
{
|
||||
barPercentage: 1.26,
|
||||
ticks: { fontFamily },
|
||||
gridLines: { display: false },
|
||||
},
|
||||
],
|
||||
yAxes: [
|
||||
{
|
||||
ticks: { fontFamily },
|
||||
gridLines: { display: false },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
function drawSuperAdminDashboard(data) {
|
||||
var ctx = document.getElementById('dashboard-chart').getContext('2d');
|
||||
var chartData = prepareData(data);
|
||||
// eslint-disable-next-line
|
||||
new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: chartData.labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Conversations',
|
||||
data: chartData.dataSet,
|
||||
backgroundColor: '#1f93ff',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: getChartOptions(),
|
||||
});
|
||||
}
|
||||
@@ -86,5 +86,8 @@ $swift-ease-out-duration: .4s !default;
|
||||
$swift-ease-out-timing-function: cubic-bezier(.25, .8, .25, 1) !default;
|
||||
$swift-ease-out: all $swift-ease-out-duration $swift-ease-out-timing-function !default;
|
||||
|
||||
// Ionicons
|
||||
$ionicons-font-path: '~ionicons/fonts';
|
||||
|
||||
// Transitions
|
||||
$transition-ease-in: all 0.250s ease-in;
|
||||
|
||||
@@ -13,7 +13,7 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
|
||||
|
||||
def destroy
|
||||
ActiveRecord::Base.transaction do
|
||||
message.update!(content: I18n.t('conversations.messages.deleted'), content_type: :text, content_attributes: { deleted: true })
|
||||
message.update!(content: I18n.t('conversations.messages.deleted'), content_attributes: { deleted: true })
|
||||
message.attachments.destroy_all
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,8 +11,6 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
process_update_contact
|
||||
@conversation = create_conversation
|
||||
conversation.messages.create!(message_params)
|
||||
# TODO: Temporary fix for message type cast issue, since message_type is returning as string instead of integer
|
||||
conversation.reload
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -66,9 +66,7 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def check_authorization
|
||||
return if Current.account_user.administrator?
|
||||
|
||||
raise Pundit::NotAuthorizedError
|
||||
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
|
||||
end
|
||||
|
||||
def common_params
|
||||
@@ -137,5 +135,3 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
V2::ReportBuilder.new(Current.account, conversation_params).conversation_metrics
|
||||
end
|
||||
end
|
||||
|
||||
Api::V2::Accounts::ReportsController.prepend_mod_with('Api::V2::Accounts::ReportsController')
|
||||
|
||||
@@ -72,7 +72,7 @@ class DashboardController < ActionController::Base
|
||||
@application_pack = if request.path.include?('/auth') || request.path.include?('/login')
|
||||
'v3app'
|
||||
else
|
||||
'dashboard'
|
||||
'application'
|
||||
end
|
||||
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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -6,7 +6,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
|
||||
layout 'portal'
|
||||
|
||||
def index
|
||||
@articles = @portal.articles.published
|
||||
@articles = @portal.articles
|
||||
search_articles
|
||||
order_by_sort_param
|
||||
@articles.page(list_params[:page]) if list_params[:page].present?
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import router from '../dashboard/routes';
|
||||
import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal.vue';
|
||||
import LoadingState from './components/widgets/LoadingState.vue';
|
||||
import NetworkNotification from './components/NetworkNotification.vue';
|
||||
@@ -8,12 +9,9 @@ import UpgradeBanner from './components/app/UpgradeBanner.vue';
|
||||
import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue';
|
||||
import PendingEmailVerificationBanner from './components/app/PendingEmailVerificationBanner.vue';
|
||||
import vueActionCable from './helper/actionCable';
|
||||
import { useRouter } from 'vue-router';
|
||||
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 {
|
||||
registerSubscription,
|
||||
verifyServiceWorkerExistence,
|
||||
@@ -33,13 +31,6 @@ export default {
|
||||
UpgradeBanner,
|
||||
PendingEmailVerificationBanner,
|
||||
},
|
||||
setup() {
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
const { accountId } = useAccount();
|
||||
|
||||
return { router, store, currentAccountId: accountId };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showAddAccountModal: false,
|
||||
@@ -47,6 +38,7 @@ export default {
|
||||
reconnectService: null,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
getAccount: 'accounts/getAccount',
|
||||
@@ -54,6 +46,7 @@ export default {
|
||||
currentUser: 'getCurrentUser',
|
||||
authUIFlags: 'getAuthUIFlags',
|
||||
accountUIFlags: 'accounts/getUIFlags',
|
||||
currentAccountId: 'getCurrentAccountId',
|
||||
}),
|
||||
hasAccounts() {
|
||||
const { accounts = [] } = this.currentUser || {};
|
||||
@@ -70,13 +63,10 @@ export default {
|
||||
this.showAddAccountModal = true;
|
||||
}
|
||||
},
|
||||
currentAccountId: {
|
||||
immediate: true,
|
||||
handler() {
|
||||
if (this.currentAccountId) {
|
||||
this.initializeAccount();
|
||||
}
|
||||
},
|
||||
currentAccountId() {
|
||||
if (this.currentAccountId) {
|
||||
this.initializeAccount();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
@@ -84,7 +74,7 @@ export default {
|
||||
this.listenToThemeChanges();
|
||||
this.setLocale(window.chatwootConfig.selectedLocale);
|
||||
},
|
||||
unmounted() {
|
||||
beforeDestroy() {
|
||||
if (this.reconnectService) {
|
||||
this.reconnectService.disconnect();
|
||||
}
|
||||
@@ -110,9 +100,8 @@ export default {
|
||||
const { pubsub_token: pubsubToken } = this.currentUser || {};
|
||||
this.setLocale(locale);
|
||||
this.latestChatwootVersion = latestChatwootVersion;
|
||||
vueActionCable.init(this.store, pubsubToken);
|
||||
this.reconnectService = new ReconnectService(this.store, this.router);
|
||||
window.reconnectService = this.reconnectService;
|
||||
vueActionCable.init(pubsubToken);
|
||||
this.reconnectService = new ReconnectService(this.$store, router);
|
||||
|
||||
verifyServiceWorkerExistence(registration =>
|
||||
registration.pushManager.getSubscription().then(subscription => {
|
||||
@@ -140,11 +129,9 @@ export default {
|
||||
<PaymentPendingBanner v-if="hideOnOnboardingView" />
|
||||
<UpgradeBanner />
|
||||
</template>
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
<transition name="fade" mode="out-in">
|
||||
<router-view />
|
||||
</transition>
|
||||
<AddAccountModal :show="showAddAccountModal" :has-accounts="hasAccounts" />
|
||||
<WootSnackbarBox />
|
||||
<NetworkNotification />
|
||||
@@ -154,22 +141,6 @@ export default {
|
||||
|
||||
<style lang="scss">
|
||||
@import './assets/scss/app';
|
||||
|
||||
.v-popper--theme-tooltip .v-popper__inner {
|
||||
background: black !important;
|
||||
font-size: 0.75rem;
|
||||
padding: 4px 8px !important;
|
||||
border-radius: 6px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.v-popper--theme-tooltip .v-popper__arrow-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.multiselect__input {
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
|
||||
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class CustomRole extends ApiClient {
|
||||
constructor() {
|
||||
super('custom_roles', { accountScoped: true });
|
||||
}
|
||||
}
|
||||
|
||||
export default new CustomRole();
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 76 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="800" height="800" viewBox="0 0 800 800" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M800 0H600V200H400V400H200V600H0V800H200H400H600H800V600V400V200V0Z" fill="#2773E4" fill-opacity="0.42"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 262 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="600" height="600" viewBox="0 0 600 600" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M200 0H0V200V400V600H200V400H400V200H600V0H400H200Z" fill="#2773E4" fill-opacity="0.42"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 246 B |
@@ -31,6 +31,23 @@
|
||||
transform: translateX($space-medium);
|
||||
}
|
||||
|
||||
.menu-list-enter-active,
|
||||
.menu-list-leave-active {
|
||||
transition: opacity 0.3s var(--ease-out-cubic),
|
||||
transform 0.2s var(--ease-out-cubic);
|
||||
}
|
||||
|
||||
.menu-list-leave-to {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
transform: translateX($space-small);
|
||||
}
|
||||
|
||||
.menu-list-enter {
|
||||
opacity: 0;
|
||||
transform: translateX(-$space-small);
|
||||
}
|
||||
|
||||
.slide-up-enter-active {
|
||||
transition: all 0.3s var(--ease-in-cubic);
|
||||
}
|
||||
@@ -48,8 +65,7 @@
|
||||
.menu-slide-enter-active,
|
||||
.menu-slide-leave-active {
|
||||
transform: translateY(0);
|
||||
transition:
|
||||
transform 0.25s var(--ease-in-cubic),
|
||||
transition: transform 0.25s var(--ease-in-cubic),
|
||||
opacity 0.15s var(--ease-in-cubic);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import 'vue-datepicker-next/scss/index';
|
||||
@import '~vue2-datepicker/scss/index';
|
||||
|
||||
.date-picker {
|
||||
// To be removed one SLA reports date picker is created
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import 'dashboard/assets/scss/variables';
|
||||
@import '~dashboard/assets/scss/variables';
|
||||
|
||||
.formulate-input {
|
||||
.formulate-input-errors {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// scss-lint:disable SpaceAfterPropertyColon
|
||||
@import 'shared/assets/fonts/inter';
|
||||
|
||||
// Inter,
|
||||
html,
|
||||
body {
|
||||
font-family:
|
||||
'Inter',
|
||||
'PlusJakarta',
|
||||
-apple-system,
|
||||
system-ui,
|
||||
BlinkMacSystemFont,
|
||||
@@ -24,7 +23,7 @@ body {
|
||||
}
|
||||
|
||||
.app-wrapper {
|
||||
@apply h-screen flex-grow-0 min-h-0 w-full;
|
||||
@apply h-full flex-grow-0 min-h-0 w-full;
|
||||
|
||||
.button--fixed-top {
|
||||
@apply fixed ltr:right-2 rtl:left-2 top-2 flex flex-row;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@import 'dashboard/assets/scss/variables';
|
||||
@import 'widget/assets/scss/mixins';
|
||||
@import '~dashboard/assets/scss/variables';
|
||||
@import '~widget/assets/scss/mixins';
|
||||
|
||||
$spinner-before-border-color: rgba(255, 255, 255, 0.7);
|
||||
|
||||
|
||||
@@ -89,6 +89,9 @@ $swift-ease-out-duration: .4s !default;
|
||||
$swift-ease-out-function: cubic-bezier(0.37, 0, 0.63, 1) !default;
|
||||
$swift-ease-out: all $swift-ease-out-duration $swift-ease-out-function !default;
|
||||
|
||||
// Ionicons
|
||||
$ionicons-font-path: '~ionicons/fonts';
|
||||
|
||||
// Transitions
|
||||
$transition-ease-in: all 0.250s ease-in;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
@import 'tailwindcss/components';
|
||||
@import 'tailwindcss/utilities';
|
||||
|
||||
@import 'shared/assets/fonts/plus-jakarta';
|
||||
@import 'shared/assets/fonts/InterDisplay/inter-display';
|
||||
@import 'shared/assets/fonts/inter';
|
||||
|
||||
@@ -33,171 +34,19 @@
|
||||
|
||||
@import 'plugins/multiselect';
|
||||
@import 'plugins/dropdown';
|
||||
@import '~shared/assets/stylesheets/ionicons';
|
||||
|
||||
.tooltip {
|
||||
@apply bg-slate-900 text-white py-1 px-2 z-40 text-xs rounded-md dark:bg-slate-200 dark:text-slate-900 max-w-96;
|
||||
}
|
||||
|
||||
#app {
|
||||
@apply h-full w-full;
|
||||
@apply bg-slate-900 text-white py-1 px-2 z-40 text-xs rounded-md dark:bg-slate-200 dark:text-slate-900;
|
||||
}
|
||||
|
||||
.hide {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
--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;
|
||||
|
||||
--background-color: 248 248 248;
|
||||
--solid-1: 255 255 255;
|
||||
--solid-2: 252 252 252;
|
||||
--solid-3: 255 255 255;
|
||||
--solid-active: 250 251 251;
|
||||
--solid-amber: 252 232 193;
|
||||
--solid-blue: 218 236 255;
|
||||
--white-alpha: 255 255 255 0.1;
|
||||
--border-weak: 231 231 231;
|
||||
--border-strong: 235 235 235;
|
||||
--blue: 39 129 246;
|
||||
|
||||
/* alpha is added by default */
|
||||
--alpha-1: 36, 38, 48, 0.06;
|
||||
--alpha-2: 130, 134, 150, 0.12;
|
||||
--alpha-3: 255, 255, 255, 0.9;
|
||||
--black-alpha-1: 0, 0, 0, 0.12;
|
||||
--black-alpha-2: 0, 0, 0, 0.04;
|
||||
}
|
||||
|
||||
body.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;
|
||||
|
||||
--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;
|
||||
|
||||
--background-color: 18 18 19;
|
||||
--solid-1: 23 23 26;
|
||||
--solid-2: 29 30 36;
|
||||
--solid-3: 36 38 48;
|
||||
--solid-amber: 42 37 30;
|
||||
--solid-blue: 16 49 91;
|
||||
--solid-active: 51 53 64;
|
||||
--border-weak: 34 34 37;
|
||||
--border-strong: 46 47 49;
|
||||
--blue: 126 182 255;
|
||||
|
||||
/* alpha is added by default */
|
||||
--alpha-1: 35, 37, 45, 0.8;
|
||||
--alpha-2: 130, 134, 150, 0.15;
|
||||
--alpha-3: 32, 33, 37, 0.9;
|
||||
--black-alpha-1: 0, 0, 0, 0.3;
|
||||
--black-alpha-2: 0, 0, 0, 0.2;
|
||||
--white-alpha: 255, 255, 255, 0.1;
|
||||
}
|
||||
/* NEXT COLORS END */
|
||||
|
||||
// scss-lint:disable PropertySortOrder
|
||||
:root {
|
||||
--color-amber-25: 254 253 251;
|
||||
--color-amber-50: 255 249 237;
|
||||
|
||||
@@ -222,7 +222,6 @@
|
||||
|
||||
.multiselect__input {
|
||||
@apply h-[2.875rem] min-h-[2.875rem];
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
|
||||
.multiselect__single {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
@import 'shared/assets/fonts/inter';
|
||||
@import 'shared/assets/fonts/plus-jakarta';
|
||||
@import 'shared/assets/stylesheets/animations';
|
||||
@import 'shared/assets/stylesheets/colors';
|
||||
@import 'shared/assets/stylesheets/spacing';
|
||||
@import 'shared/assets/stylesheets/font-size';
|
||||
@import 'shared/assets/stylesheets/font-weights';
|
||||
@import 'shared/assets/stylesheets/shadows';
|
||||
@import 'shared/assets/stylesheets/border-radius';
|
||||
@import 'variables';
|
||||
|
||||
@import 'vue-multiselect/dist/vue-multiselect.min.css';
|
||||
@import '~shared/assets/stylesheets/ionicons';
|
||||
|
||||
@import 'mixins';
|
||||
@import 'helper-classes';
|
||||
|
||||
@import 'typography';
|
||||
@import 'layout';
|
||||
@import 'animations';
|
||||
|
||||
@import 'widgets/buttons';
|
||||
@import 'widgets/base';
|
||||
|
||||
@import 'plugins/multiselect';
|
||||
|
||||
@import 'widget/assets/scss/reset';
|
||||
@import 'tailwindcss/base';
|
||||
@import 'tailwindcss/components';
|
||||
@import 'tailwindcss/utilities';
|
||||
@import 'widget/assets/scss/utilities';
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: 'PlusJakarta', sans-serif;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<script setup>
|
||||
const emit = defineEmits(['click']);
|
||||
const handleClick = () => {
|
||||
emit('click');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="relative flex flex-col w-full gap-3 px-6 py-5 group/cardLayout rounded-2xl bg-slate-25 dark:bg-slate-800/50"
|
||||
@click="handleClick"
|
||||
>
|
||||
<slot name="header" />
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,47 +0,0 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="relative flex flex-col items-center justify-center w-full h-full min-h-screen p-4 overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="relative w-full max-w-[940px] mx-auto overflow-hidden h-full max-h-[448px]"
|
||||
>
|
||||
<div
|
||||
class="w-full h-full space-y-4 overflow-y-hidden opacity-50 pointer-events-none"
|
||||
>
|
||||
<slot name="empty-state-item" />
|
||||
</div>
|
||||
<div
|
||||
class="absolute inset-x-0 bottom-0 flex flex-col items-center justify-end w-full h-full pb-9 bg-gradient-to-t from-white dark:from-slate-900 to-transparent font-interDisplay"
|
||||
>
|
||||
<div class="flex flex-col items-center justify-center gap-6">
|
||||
<div class="flex flex-col items-center justify-center gap-2">
|
||||
<h2
|
||||
class="text-3xl font-medium text-center text-slate-900 dark:text-white"
|
||||
>
|
||||
{{ title }}
|
||||
</h2>
|
||||
<p
|
||||
class="max-w-lg text-base text-center text-slate-600 dark:text-slate-300"
|
||||
>
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
</div>
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -1,56 +0,0 @@
|
||||
<script setup>
|
||||
import ArticleCard from './ArticleCard.vue';
|
||||
|
||||
const articles = [
|
||||
{
|
||||
title: "How to get an SSL certificate for your Help Center's custom domain",
|
||||
status: 'draft',
|
||||
updatedAt: '2 days ago',
|
||||
author: 'Michael',
|
||||
category: '⚡️ Marketing',
|
||||
views: 400,
|
||||
},
|
||||
{
|
||||
title: 'Setting up your first Help Center portal',
|
||||
status: '',
|
||||
updatedAt: '1 week ago',
|
||||
author: 'John',
|
||||
category: '🛠️ Development',
|
||||
views: 1400,
|
||||
},
|
||||
{
|
||||
title: 'Best practices for organizing your Help Center content',
|
||||
status: 'archived',
|
||||
updatedAt: '3 days ago',
|
||||
author: 'Fernando',
|
||||
category: '💰 Finance',
|
||||
views: 4300,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/no-bare-strings-in-template -->
|
||||
<!-- eslint-disable vue/no-undef-components -->
|
||||
<template>
|
||||
<Story
|
||||
title="Components/HelpCenter/ArticleCard"
|
||||
:layout="{ type: 'grid', width: '700px' }"
|
||||
>
|
||||
<Variant title="Article Card">
|
||||
<div
|
||||
v-for="(article, index) in articles"
|
||||
:key="index"
|
||||
class="px-20 py-4 bg-white dark:bg-slate-900"
|
||||
>
|
||||
<ArticleCard
|
||||
:title="article.title"
|
||||
:status="article.status"
|
||||
:author="article.author"
|
||||
:category="article.category"
|
||||
:views="article.views"
|
||||
:updated-at="article.updatedAt"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -1,142 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { OnClickOutside } from '@vueuse/components';
|
||||
|
||||
import CardLayout from 'dashboard/components-next/CardLayout.vue';
|
||||
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
author: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
category: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
views: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
updatedAt: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const isOpen = ref(false);
|
||||
|
||||
const menuItems = computed(() => {
|
||||
const baseItems = [{ label: 'Delete', action: 'delete', icon: 'delete' }];
|
||||
const menuOptions = {
|
||||
archived: [
|
||||
{ label: 'Publish', action: 'publish', icon: 'checkmark' },
|
||||
{ label: 'Draft', action: 'draft', icon: 'draft' },
|
||||
],
|
||||
draft: [
|
||||
{ label: 'Publish', action: 'publish', icon: 'checkmark' },
|
||||
{ label: 'Archive', action: 'archive', icon: 'archive' },
|
||||
],
|
||||
'': [
|
||||
// Empty string represents published status
|
||||
{ label: 'Draft', action: 'draft', icon: 'draft' },
|
||||
{ label: 'Archive', action: 'archive', icon: 'archive' },
|
||||
],
|
||||
};
|
||||
return [...(menuOptions[props.status] || menuOptions['']), ...baseItems];
|
||||
});
|
||||
|
||||
const statusTextColor = computed(() => {
|
||||
switch (props.status) {
|
||||
case 'archived':
|
||||
return '!text-slate-600 dark:!text-slate-200';
|
||||
case 'draft':
|
||||
return '!text-amber-700 dark:!text-amber-400';
|
||||
default:
|
||||
return '!text-teal-700 dark:!text-teal-400';
|
||||
}
|
||||
});
|
||||
|
||||
const statusText = computed(() => {
|
||||
switch (props.status) {
|
||||
case 'archived':
|
||||
return 'Archived';
|
||||
case 'draft':
|
||||
return 'Draft';
|
||||
default:
|
||||
return 'Published';
|
||||
}
|
||||
});
|
||||
|
||||
const handleAction = () => {
|
||||
isOpen.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- TODO: Add i18n -->
|
||||
<!-- eslint-disable vue/no-bare-strings-in-template -->
|
||||
<template>
|
||||
<CardLayout>
|
||||
<template #header>
|
||||
<div class="flex justify-between gap-1">
|
||||
<span class="text-base text-slate-900 dark:text-slate-50 line-clamp-1">
|
||||
{{ title }}
|
||||
</span>
|
||||
<div class="relative group">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="text-xs bg-slate-50 !font-normal group-hover:bg-slate-100/50 dark:group-hover:bg-slate-700/50 !h-6 dark:bg-slate-800 rounded-md border-0 !px-2 !py-0.5"
|
||||
:label="statusText"
|
||||
:class="statusTextColor"
|
||||
@click="isOpen = !isOpen"
|
||||
/>
|
||||
<OnClickOutside @trigger="isOpen = false">
|
||||
<DropdownMenu
|
||||
v-if="isOpen"
|
||||
:menu-items="menuItems"
|
||||
class="right-0 mt-2 xl:left-0 top-full"
|
||||
@action="handleAction"
|
||||
/>
|
||||
</OnClickOutside>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="w-4 h-4 rounded-full bg-slate-100 dark:bg-slate-700" />
|
||||
<span class="text-sm text-slate-500 dark:text-slate-400">
|
||||
{{ author }}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="block text-sm whitespace-nowrap text-slate-500 dark:text-slate-400"
|
||||
>
|
||||
{{ category }}
|
||||
</span>
|
||||
<div
|
||||
class="inline-flex items-center gap-1 text-slate-500 dark:text-slate-400 whitespace-nowrap"
|
||||
>
|
||||
<FluentIcon icon="eye-show" size="18" />
|
||||
<span class="text-sm"> {{ views }} views </span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-sm text-slate-600 dark:text-slate-400 line-clamp-1">
|
||||
{{ updatedAt }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</CardLayout>
|
||||
</template>
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
<script setup>
|
||||
import CategoryCard from './CategoryCard.vue';
|
||||
const categories = [
|
||||
{
|
||||
id: 'getting-started',
|
||||
title: '🚀 Getting started',
|
||||
description:
|
||||
'Learn how to use Chatwoot effectively and make the most of its features to enhance customer support and engagement.',
|
||||
articlesCount: '5',
|
||||
},
|
||||
{
|
||||
id: 'marketing',
|
||||
title: '📈 Marketing',
|
||||
description: '',
|
||||
articlesCount: '4',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/no-bare-strings-in-template -->
|
||||
<!-- eslint-disable vue/no-undef-components -->
|
||||
<template>
|
||||
<Story
|
||||
title="Components/HelpCenter/CategoryCard"
|
||||
:layout="{ type: 'grid', width: '800px' }"
|
||||
>
|
||||
<Variant title="Category Card">
|
||||
<div
|
||||
v-for="(category, index) in categories"
|
||||
:key="index"
|
||||
class="px-20 py-4 bg-white dark:bg-slate-900"
|
||||
>
|
||||
<CategoryCard
|
||||
:title="category.title"
|
||||
:description="category.description"
|
||||
:articles-count="category.articlesCount"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -1,115 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { OnClickOutside } from '@vueuse/components';
|
||||
|
||||
import CardLayout from 'dashboard/components-next/CardLayout.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
articlesCount: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['click']);
|
||||
|
||||
const isOpen = ref(false);
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
label: 'Edit',
|
||||
action: 'edit',
|
||||
icon: 'edit',
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
action: 'delete',
|
||||
icon: 'delete',
|
||||
},
|
||||
];
|
||||
|
||||
const description = computed(() => {
|
||||
return props.description ? props.description : 'No description added';
|
||||
});
|
||||
|
||||
const hasDescription = computed(() => {
|
||||
return props.description.length > 0;
|
||||
});
|
||||
|
||||
const handleClick = id => {
|
||||
emit('click', id);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const handleAction = action => {
|
||||
// TODO: Implement action
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- TODO: Add i18n -->
|
||||
<!-- eslint-disable vue/no-bare-strings-in-template -->
|
||||
<template>
|
||||
<CardLayout @click="handleClick(id)">
|
||||
<template #header>
|
||||
<div class="flex gap-2">
|
||||
<div class="flex justify-between w-full">
|
||||
<div class="flex items-center justify-start gap-2">
|
||||
<span
|
||||
class="text-base cursor-pointer group-hover/cardLayout:underline text-slate-900 dark:text-slate-50 line-clamp-1"
|
||||
>
|
||||
{{ title }}
|
||||
</span>
|
||||
<span
|
||||
class="inline-flex items-center justify-center h-6 px-2 py-1 text-xs text-center border rounded-lg text-slate-500 w-fit border-slate-200 dark:border-slate-800 dark:text-slate-400"
|
||||
>
|
||||
{{ articlesCount }} articles
|
||||
</span>
|
||||
</div>
|
||||
<div class="relative group" @click.stop>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
icon="more-vertical"
|
||||
class="w-8 z-60 group-hover:bg-slate-100 dark:group-hover:bg-slate-800"
|
||||
@click="isOpen = !isOpen"
|
||||
/>
|
||||
<OnClickOutside @trigger="isOpen = false">
|
||||
<DropdownMenu
|
||||
v-if="isOpen"
|
||||
:menu-items="menuItems"
|
||||
class="right-0 mt-1 xl:left-0 top-full z-60"
|
||||
@action="handleAction"
|
||||
/>
|
||||
</OnClickOutside>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span
|
||||
class="text-sm line-clamp-3"
|
||||
:class="
|
||||
hasDescription
|
||||
? 'text-slate-500 dark:text-slate-400'
|
||||
: 'text-slate-400 dark:text-slate-700'
|
||||
"
|
||||
>
|
||||
{{ description }}
|
||||
</span>
|
||||
</template>
|
||||
</CardLayout>
|
||||
</template>
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
<script setup>
|
||||
import ArticleEmptyState from './ArticleEmptyState.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Components/HelpCenter/EmptyState/ArticleEmptyState"
|
||||
:layout="{ type: 'single', width: '1100px' }"
|
||||
>
|
||||
<Variant title="Article Empty State">
|
||||
<div class="w-full h-full px-20 mx-auto bg-white dark:bg-slate-900">
|
||||
<ArticleEmptyState />
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
-87
@@ -1,87 +0,0 @@
|
||||
<script setup>
|
||||
// import { ref } from 'vue';
|
||||
import EmptyStateLayout from 'dashboard/components-next/EmptyStateLayout.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import ArticleCard from 'dashboard/components-next/HelpCenter/ArticleCard/ArticleCard.vue';
|
||||
// import AddLocaleDialog from 'dashboard/playground/HelpCenter/components/AddLocaleDialog.vue';
|
||||
|
||||
const articles = [
|
||||
{
|
||||
title: "How to get an SSL certificate for your Help Center's custom domain",
|
||||
status: 'draft',
|
||||
updatedAt: '2 days ago',
|
||||
author: 'Michael',
|
||||
category: '⚡️ Marketing',
|
||||
views: 3400,
|
||||
},
|
||||
{
|
||||
title: 'Setting up your first Help Center portal',
|
||||
status: '',
|
||||
updatedAt: '1 week ago',
|
||||
author: 'John',
|
||||
category: '🛠️ Development',
|
||||
views: 400,
|
||||
},
|
||||
{
|
||||
title: 'Best practices for organizing your Help Center content',
|
||||
status: 'archived',
|
||||
updatedAt: '3 days ago',
|
||||
author: 'Fernando',
|
||||
category: '💰 Finance',
|
||||
views: 400,
|
||||
},
|
||||
{
|
||||
title: 'Customizing the appearance of your Help Center',
|
||||
status: '',
|
||||
updatedAt: '5 days ago',
|
||||
author: 'Jane',
|
||||
category: '💰 Finance',
|
||||
views: 400,
|
||||
},
|
||||
];
|
||||
|
||||
// const addLocaleDialogRef = ref(null);
|
||||
// const openDialog = () => {
|
||||
// addLocaleDialogRef.value.dialogRef.open();
|
||||
// };
|
||||
// const handleDialogConfirm = () => {
|
||||
// // Add logic to create a new portal
|
||||
// };
|
||||
</script>
|
||||
|
||||
<!-- TODO: Add i18n -->
|
||||
<!-- eslint-disable vue/no-bare-strings-in-template -->
|
||||
|
||||
<template>
|
||||
<EmptyStateLayout
|
||||
title="Write an article"
|
||||
subtitle="Write a rich article, let's get started!"
|
||||
>
|
||||
<template #empty-state-item>
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<ArticleCard
|
||||
v-for="(article, index) in articles"
|
||||
:key="`article-${index}`"
|
||||
:title="article.title"
|
||||
:status="article.status"
|
||||
:updated-at="article.updatedAt"
|
||||
:author="article.author"
|
||||
:category="article.category"
|
||||
:views="article.views"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<Button
|
||||
variant="default"
|
||||
label="New article"
|
||||
icon="add"
|
||||
@click="openDialog"
|
||||
/>
|
||||
<!-- <AddLocaleDialog
|
||||
ref="addLocaleDialogRef"
|
||||
@confirm="handleDialogConfirm"
|
||||
/> -->
|
||||
</template>
|
||||
</EmptyStateLayout>
|
||||
</template>
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
<script setup>
|
||||
import PortalEmptyState from './PortalEmptyState.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Components/HelpCenter/EmptyState/PortalEmptyState"
|
||||
:layout="{ type: 'single', width: '1100px' }"
|
||||
>
|
||||
<Variant title="Portal Empty State">
|
||||
<div class="w-full h-full px-20 mx-auto bg-white dark:bg-slate-900">
|
||||
<PortalEmptyState />
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
-123
@@ -1,123 +0,0 @@
|
||||
<script setup>
|
||||
// import { ref } from 'vue';
|
||||
import EmptyStateLayout from 'dashboard/components-next/EmptyStateLayout.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import ArticleCard from 'dashboard/components-next/HelpCenter/ArticleCard/ArticleCard.vue';
|
||||
import CategoryCard from 'dashboard/components-next/HelpCenter/CategoryCard/CategoryCard.vue';
|
||||
import LocaleCard from 'dashboard/components-next/HelpCenter/LocaleCard/LocaleCard.vue';
|
||||
// import CreatePortalDialog from 'dashboard/playground/HelpCenter/components/CreatePortalDialog.vue';
|
||||
|
||||
const articles = [
|
||||
{
|
||||
title: "How to get an SSL certificate for your Help Center's custom domain",
|
||||
status: 'draft',
|
||||
updatedAt: '2 days ago',
|
||||
author: 'Michael',
|
||||
category: '⚡️ Marketing',
|
||||
views: 3400,
|
||||
},
|
||||
{
|
||||
title: 'Setting up your first Help Center portal',
|
||||
status: '',
|
||||
updatedAt: '1 week ago',
|
||||
author: 'John',
|
||||
category: '🛠️ Development',
|
||||
views: 400,
|
||||
},
|
||||
{
|
||||
title: 'Best practices for organizing your Help Center content',
|
||||
status: 'archived',
|
||||
updatedAt: '3 days ago',
|
||||
author: 'Fernando',
|
||||
category: '💰 Finance',
|
||||
views: 400,
|
||||
},
|
||||
{
|
||||
title: 'Customizing the appearance of your Help Center',
|
||||
status: '',
|
||||
updatedAt: '5 days ago',
|
||||
author: 'Jane',
|
||||
category: '💰 Finance',
|
||||
views: 400,
|
||||
},
|
||||
];
|
||||
const categories = [
|
||||
{
|
||||
title: 'Getting Started',
|
||||
description: 'Essential guides for new users',
|
||||
articlesCount: '5',
|
||||
},
|
||||
{
|
||||
title: 'Advanced Features',
|
||||
description: 'In-depth tutorials for power users',
|
||||
articlesCount: '8',
|
||||
},
|
||||
];
|
||||
|
||||
const locales = [
|
||||
{ name: 'English', isDefault: true },
|
||||
{ name: 'Spanish', isDefault: false },
|
||||
{ name: 'Malayalam', isDefault: false },
|
||||
];
|
||||
|
||||
// const createPortalDialogRef = ref(null);
|
||||
// const openDialog = () => {
|
||||
// createPortalDialogRef.value.dialogRef.open();
|
||||
// };
|
||||
// const handleDialogConfirm = () => {
|
||||
// // Add logic to create a new portal
|
||||
// };
|
||||
</script>
|
||||
|
||||
<!-- TODO: Add i18n -->
|
||||
<!-- eslint-disable vue/no-bare-strings-in-template -->
|
||||
<template>
|
||||
<EmptyStateLayout
|
||||
title="Help Center"
|
||||
subtitle="Create self-service portals to access articles and information. Streamline queries, enhance agent efficiency, and elevate customer support."
|
||||
>
|
||||
<template #empty-state-item>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-4">
|
||||
<ArticleCard
|
||||
v-for="(article, index) in articles"
|
||||
:key="`article-${index}`"
|
||||
:title="article.title"
|
||||
:status="article.status"
|
||||
:updated-at="article.updatedAt"
|
||||
:author="article.author"
|
||||
:category="article.category"
|
||||
:views="article.views"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<CategoryCard
|
||||
v-for="(category, index) in categories"
|
||||
:key="`category-${index}`"
|
||||
:title="category.title"
|
||||
:description="category.description"
|
||||
:articles-count="category.articlesCount"
|
||||
/>
|
||||
<LocaleCard
|
||||
v-for="(locale, index) in locales"
|
||||
:key="`locale-${index}`"
|
||||
:locale="locale.name"
|
||||
:is-default="locale.isDefault"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<Button
|
||||
variant="default"
|
||||
label="Create Portal"
|
||||
icon="add"
|
||||
@click="openDialog"
|
||||
/>
|
||||
<!-- <CreatePortalDialog
|
||||
ref="createPortalDialogRef"
|
||||
@confirm="handleDialogConfirm"
|
||||
/> -->
|
||||
</template>
|
||||
</EmptyStateLayout>
|
||||
</template>
|
||||
@@ -1,99 +0,0 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { OnClickOutside } from '@vueuse/components';
|
||||
|
||||
import PaginationFooter from 'dashboard/components-next/pagination/PaginationFooter.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import PortalSwitcher from 'dashboard/components-next/HelpCenter/PortalSwitcher/PortalSwitcher.vue';
|
||||
|
||||
defineProps({
|
||||
header: {
|
||||
type: String,
|
||||
default: 'Chatwoot Help Center',
|
||||
},
|
||||
currentPage: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
totalItems: {
|
||||
type: Number,
|
||||
default: 100,
|
||||
},
|
||||
itemsPerPage: {
|
||||
type: Number,
|
||||
default: 25,
|
||||
},
|
||||
showHeaderTitle: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showPaginationFooter: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:currentPage']);
|
||||
|
||||
const showPortalSwitcher = ref(false);
|
||||
|
||||
const updateCurrentPage = page => {
|
||||
emit('update:currentPage', page);
|
||||
};
|
||||
const togglePortalSwitcher = () => {
|
||||
showPortalSwitcher.value = !showPortalSwitcher.value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="flex flex-col w-full h-full overflow-hidden bg-white dark:bg-slate-900"
|
||||
>
|
||||
<header
|
||||
class="sticky top-0 z-10 px-6 pb-3 bg-white lg:px-0 dark:bg-slate-900"
|
||||
>
|
||||
<div class="w-full max-w-[900px] mx-auto">
|
||||
<div
|
||||
v-if="showHeaderTitle"
|
||||
class="flex items-center justify-start h-20 gap-2"
|
||||
>
|
||||
<span class="text-xl font-medium text-slate-900 dark:text-white">
|
||||
{{ header }}
|
||||
</span>
|
||||
<div class="relative group">
|
||||
<Button
|
||||
icon="more-vertical"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="group-hover:bg-slate-100 dark:group-hover:bg-slate-800"
|
||||
@click="togglePortalSwitcher"
|
||||
/>
|
||||
<OnClickOutside @trigger="showPortalSwitcher = false">
|
||||
<PortalSwitcher
|
||||
v-if="showPortalSwitcher"
|
||||
class="absolute left-0 top-9"
|
||||
/>
|
||||
</OnClickOutside>
|
||||
</div>
|
||||
</div>
|
||||
<slot name="header-actions" />
|
||||
</div>
|
||||
</header>
|
||||
<main class="flex-1 px-6 overflow-y-auto lg:px-0">
|
||||
<div class="w-full max-w-[900px] mx-auto py-3">
|
||||
<slot name="content" />
|
||||
</div>
|
||||
</main>
|
||||
<footer
|
||||
v-if="showPaginationFooter"
|
||||
class="sticky bottom-0 z-10 px-4 pt-3 pb-4 bg-white dark:bg-slate-900"
|
||||
>
|
||||
<PaginationFooter
|
||||
:current-page="currentPage"
|
||||
:total-items="totalItems"
|
||||
:items-per-page="itemsPerPage"
|
||||
@update:current-page="updateCurrentPage"
|
||||
/>
|
||||
</footer>
|
||||
</section>
|
||||
</template>
|
||||
@@ -1,29 +0,0 @@
|
||||
<script setup>
|
||||
import LocaleCard from './LocaleCard.vue';
|
||||
const locales = [
|
||||
{ name: 'English', isDefault: true, articleCount: 29, categoryCount: 5 },
|
||||
{ name: 'Spanish', isDefault: false, articleCount: 29, categoryCount: 5 },
|
||||
];
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/no-bare-strings-in-template -->
|
||||
<!-- eslint-disable vue/no-undef-components -->
|
||||
<template>
|
||||
<Story
|
||||
title="Components/HelpCenter/LocaleCard"
|
||||
:layout="{ type: 'grid', width: '800px' }"
|
||||
>
|
||||
<Variant title="Locale Card">
|
||||
<div class="px-10 py-4 bg-white dark:bg-slate-900">
|
||||
<div v-for="(locale, index) in locales" :key="index" class="px-20 py-2">
|
||||
<LocaleCard
|
||||
:locale="locale.name"
|
||||
:is-default="locale.isDefault"
|
||||
:article-count="locale.articleCount"
|
||||
:category-count="locale.categoryCount"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -1,103 +0,0 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { OnClickOutside } from '@vueuse/components';
|
||||
|
||||
import CardLayout from 'dashboard/components-next/CardLayout.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
|
||||
|
||||
defineProps({
|
||||
locale: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isDefault: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
articleCount: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
categoryCount: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const isOpen = ref(false);
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
label: 'Make default',
|
||||
action: 'default',
|
||||
icon: 'star-emphasis',
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
action: 'delete',
|
||||
icon: 'delete',
|
||||
},
|
||||
];
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const handleAction = action => {
|
||||
// TODO: Implement action
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- TODO: Add i18n -->
|
||||
<!-- eslint-disable vue/no-bare-strings-in-template -->
|
||||
<template>
|
||||
<CardLayout class="ltr:pr-2 rtl:pl-2">
|
||||
<template #header>
|
||||
<div class="flex justify-between gap-2">
|
||||
<div class="flex items-center justify-start gap-2">
|
||||
<span
|
||||
class="text-sm font-medium text-slate-900 dark:text-slate-50 line-clamp-1"
|
||||
>
|
||||
{{ locale }}
|
||||
</span>
|
||||
<span
|
||||
v-if="isDefault"
|
||||
class="bg-slate-100 dark:bg-slate-800 h-6 inline-flex items-center justify-center rounded-md text-xs border-px border-transparent text-woot-500 dark:text-woot-400 px-2 py-0.5"
|
||||
>
|
||||
Default
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<div class="flex items-center gap-4">
|
||||
<span
|
||||
class="text-sm text-slate-500 dark:text-slate-400 whitespace-nowrap"
|
||||
>
|
||||
{{ articleCount }} articles
|
||||
</span>
|
||||
<div class="w-px h-3 bg-slate-75 dark:bg-slate-800" />
|
||||
<span
|
||||
class="text-sm text-slate-500 dark:text-slate-400 whitespace-nowrap"
|
||||
>
|
||||
{{ categoryCount }} categories
|
||||
</span>
|
||||
</div>
|
||||
<div class="relative group">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
icon="more-vertical"
|
||||
class="w-8 group-hover:bg-slate-100 dark:group-hover:bg-slate-800"
|
||||
@click="isOpen = !isOpen"
|
||||
/>
|
||||
<OnClickOutside @trigger="isOpen = false">
|
||||
<DropdownMenu
|
||||
v-if="isOpen"
|
||||
:menu-items="menuItems"
|
||||
class="right-0 mt-1 xl:left-0 top-full z-60 min-w-[147px]"
|
||||
@action="handleAction"
|
||||
/>
|
||||
</OnClickOutside>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</CardLayout>
|
||||
</template>
|
||||
@@ -1,25 +0,0 @@
|
||||
<script setup>
|
||||
import ArticleCard from 'dashboard/components-next/HelpCenter/ArticleCard/ArticleCard.vue';
|
||||
|
||||
defineProps({
|
||||
articles: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul role="list" class="w-full h-full space-y-4">
|
||||
<ArticleCard
|
||||
v-for="article in articles"
|
||||
:key="article.title"
|
||||
:title="article.title"
|
||||
:status="article.status"
|
||||
:author="article.author"
|
||||
:category="article.category"
|
||||
:views="article.views"
|
||||
:updated-at="article.updatedAt"
|
||||
/>
|
||||
</ul>
|
||||
</template>
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
<script setup>
|
||||
import ArticlesPage from './ArticlesPage.vue';
|
||||
|
||||
const articles = [
|
||||
{
|
||||
title: "How to get an SSL certificate for your Help Center's custom domain",
|
||||
status: 'draft',
|
||||
updatedAt: '2 days ago',
|
||||
author: 'Michael',
|
||||
category: '⚡️ Marketing',
|
||||
views: 3400,
|
||||
},
|
||||
{
|
||||
title: 'Setting up your first Help Center portal',
|
||||
status: '',
|
||||
updatedAt: '1 week ago',
|
||||
author: 'John',
|
||||
category: '🛠️ Development',
|
||||
views: 400,
|
||||
},
|
||||
{
|
||||
title: 'Best practices for organizing your Help Center content',
|
||||
status: 'archived',
|
||||
updatedAt: '3 days ago',
|
||||
author: 'Fernando',
|
||||
category: '💰 Finance',
|
||||
views: 400,
|
||||
},
|
||||
{
|
||||
title: 'Customizing the appearance of your Help Center',
|
||||
status: '',
|
||||
updatedAt: '5 days ago',
|
||||
author: 'Jane',
|
||||
category: '💰 Finance',
|
||||
views: 400,
|
||||
},
|
||||
{
|
||||
title: 'Best practices for organizing your Help Center content',
|
||||
status: 'archived',
|
||||
updatedAt: '3 days ago',
|
||||
author: 'Fernando',
|
||||
category: '💰 Finance',
|
||||
views: 400,
|
||||
},
|
||||
{
|
||||
title: 'Customizing the appearance of your Help Center',
|
||||
status: '',
|
||||
updatedAt: '5 days ago',
|
||||
author: 'Jane',
|
||||
category: '💰 Finance',
|
||||
views: 400,
|
||||
},
|
||||
{
|
||||
title: 'Best practices for organizing your Help Center content',
|
||||
status: 'archived',
|
||||
updatedAt: '3 days ago',
|
||||
author: 'Fernando',
|
||||
category: '💰 Finance',
|
||||
views: 400,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story title="Pages/HelpCenter/ArticlesPage" :layout="{ type: 'single' }">
|
||||
<Variant title="All Articles">
|
||||
<div class="w-full min-h-screen bg-white dark:bg-slate-900">
|
||||
<ArticlesPage :articles="articles" />
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
<script setup>
|
||||
import HelpCenterLayout from 'dashboard/components-next/HelpCenter/HelpCenterLayout.vue';
|
||||
import TabBar from 'dashboard/components-next/tabbar/TabBar.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import ArticleList from 'dashboard/components-next/HelpCenter/Pages/ArticlePage/ArticleList.vue';
|
||||
|
||||
defineProps({
|
||||
articles: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const tabs = [
|
||||
{ label: 'All articles', count: 24 },
|
||||
{ label: 'Mine', count: 13 },
|
||||
{ label: 'Draft', count: 5 },
|
||||
{ label: 'Archived', count: 11 },
|
||||
];
|
||||
// TODO: remove comments
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const handleTabChange = tab => {
|
||||
// TODO: Implement tab change logic
|
||||
};
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const handlePageChange = page => {
|
||||
// TODO: Implement page change logic
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<HelpCenterLayout
|
||||
:current-page="1"
|
||||
:total-items="100"
|
||||
:items-per-page="10"
|
||||
@update:current-page="handlePageChange"
|
||||
>
|
||||
<template #header-actions>
|
||||
<div class="flex items-end justify-between">
|
||||
<div class="flex flex-col items-start w-full gap-2 lg:flex-row">
|
||||
<TabBar
|
||||
:tabs="tabs"
|
||||
:initial-active-tab="1"
|
||||
@tab-changed="handleTabChange"
|
||||
/>
|
||||
<div class="flex items-start justify-between w-full gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
label="English"
|
||||
size="sm"
|
||||
icon-position="right"
|
||||
icon="chevron-lucide-down"
|
||||
icon-lib="lucide"
|
||||
variant="secondary"
|
||||
/>
|
||||
<Button
|
||||
label="All categories"
|
||||
size="sm"
|
||||
icon-position="right"
|
||||
icon="chevron-lucide-down"
|
||||
icon-lib="lucide"
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
<Button label="New article" icon="add" size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<ArticleList :articles="articles" />
|
||||
</template>
|
||||
</HelpCenterLayout>
|
||||
</template>
|
||||
-209
@@ -1,209 +0,0 @@
|
||||
<script setup>
|
||||
import CategoriesPage from './CategoriesPage.vue';
|
||||
|
||||
const categories = [
|
||||
{
|
||||
id: 'getting-started',
|
||||
title: '🚀 Getting started',
|
||||
description:
|
||||
'Learn how to use Chatwoot effectively and make the most of its features to enhance customer support and engagement.',
|
||||
articlesCount: '2',
|
||||
articles: [
|
||||
{
|
||||
variant: 'Draft article',
|
||||
title:
|
||||
"How to get an SSL certificate for your Help Center's custom domain",
|
||||
status: 'draft',
|
||||
updatedAt: '2 days ago',
|
||||
author: 'Michael',
|
||||
category: '⚡️ Marketing',
|
||||
views: 3400,
|
||||
},
|
||||
{
|
||||
variant: 'Published article',
|
||||
title: 'Setting up your first Help Center portal',
|
||||
status: '',
|
||||
updatedAt: '1 week ago',
|
||||
author: 'John',
|
||||
category: '🛠️ Development',
|
||||
views: 400,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'marketing',
|
||||
title: 'Marketing',
|
||||
description:
|
||||
'Learn how to use Chatwoot effectively and make the most of its features to enhance customer support.',
|
||||
articlesCount: '4',
|
||||
articles: [
|
||||
{
|
||||
variant: 'Draft article',
|
||||
title:
|
||||
"How to get an SSL certificate for your Help Center's custom domain",
|
||||
status: 'draft',
|
||||
updatedAt: '2 days ago',
|
||||
author: 'Michael',
|
||||
category: '⚡️ Marketing',
|
||||
views: 3400,
|
||||
},
|
||||
{
|
||||
variant: 'Published article',
|
||||
title: 'Setting up your first Help Center portal',
|
||||
status: '',
|
||||
updatedAt: '1 week ago',
|
||||
author: 'John',
|
||||
category: '🛠️ Development',
|
||||
views: 400,
|
||||
},
|
||||
{
|
||||
variant: 'Archived article',
|
||||
title: 'Best practices for organizing your Help Center content',
|
||||
status: 'archived',
|
||||
updatedAt: '3 days ago',
|
||||
author: 'Fernando',
|
||||
category: '💰 Finance',
|
||||
views: 400,
|
||||
},
|
||||
{
|
||||
variant: 'Published article',
|
||||
title: 'Customizing the appearance of your Help Center',
|
||||
status: '',
|
||||
updatedAt: '5 days ago',
|
||||
author: 'Jane',
|
||||
category: '💰 Finance',
|
||||
views: 400,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'development',
|
||||
title: 'Development',
|
||||
description: '',
|
||||
articlesCount: '5',
|
||||
articles: [
|
||||
{
|
||||
variant: 'Draft article',
|
||||
title:
|
||||
"How to get an SSL certificate for your Help Center's custom domain",
|
||||
status: 'draft',
|
||||
updatedAt: '2 days ago',
|
||||
author: 'Michael',
|
||||
category: '⚡️ Marketing',
|
||||
views: 3400,
|
||||
},
|
||||
{
|
||||
variant: 'Published article',
|
||||
title: 'Setting up your first Help Center portal',
|
||||
status: '',
|
||||
updatedAt: '1 week ago',
|
||||
author: 'John',
|
||||
category: '🛠️ Development',
|
||||
views: 400,
|
||||
},
|
||||
{
|
||||
variant: 'Archived article',
|
||||
title: 'Best practices for organizing your Help Center content',
|
||||
status: 'archived',
|
||||
updatedAt: '3 days ago',
|
||||
author: 'Fernando',
|
||||
category: '💰 Finance',
|
||||
views: 400,
|
||||
},
|
||||
{
|
||||
variant: 'Archived article',
|
||||
title: 'Best practices for organizing your Help Center content',
|
||||
status: 'archived',
|
||||
updatedAt: '3 days ago',
|
||||
author: 'Fernando',
|
||||
category: '💰 Finance',
|
||||
views: 400,
|
||||
},
|
||||
{
|
||||
variant: 'Published article',
|
||||
title: 'Customizing the appearance of your Help Center',
|
||||
status: '',
|
||||
updatedAt: '5 days ago',
|
||||
author: 'Jane',
|
||||
category: '💰 Finance',
|
||||
views: 400,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'roadmap',
|
||||
title: '🛣️ Roadmap',
|
||||
description:
|
||||
'Learn how to use Chatwoot effectively and make the most of its features to enhance customer support and engagement.',
|
||||
articlesCount: '3',
|
||||
articles: [
|
||||
{
|
||||
variant: 'Draft article',
|
||||
title:
|
||||
"How to get an SSL certificate for your Help Center's custom domain",
|
||||
status: 'draft',
|
||||
updatedAt: '2 days ago',
|
||||
author: 'Michael',
|
||||
category: '⚡️ Marketing',
|
||||
views: 3400,
|
||||
},
|
||||
{
|
||||
variant: 'Published article',
|
||||
title: 'Setting up your first Help Center portal',
|
||||
status: '',
|
||||
updatedAt: '1 week ago',
|
||||
author: 'John',
|
||||
category: '🛠️ Development',
|
||||
views: 400,
|
||||
},
|
||||
{
|
||||
variant: 'Published article',
|
||||
title: 'Setting up your first Help Center portal',
|
||||
status: '',
|
||||
updatedAt: '1 week ago',
|
||||
author: 'John',
|
||||
category: '🛠️ Development',
|
||||
views: 400,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'finance',
|
||||
title: '💰 Finance',
|
||||
description:
|
||||
'Learn how to use Chatwoot effectively and make the most of its features to enhance customer support and engagement.',
|
||||
articlesCount: '2',
|
||||
articles: [
|
||||
{
|
||||
variant: 'Draft article',
|
||||
title:
|
||||
"How to get an SSL certificate for your Help Center's custom domain",
|
||||
status: 'draft',
|
||||
updatedAt: '2 days ago',
|
||||
author: 'Michael',
|
||||
category: '⚡️ Marketing',
|
||||
views: 3400,
|
||||
},
|
||||
{
|
||||
variant: 'Published article',
|
||||
title: 'Setting up your first Help Center portal',
|
||||
status: '',
|
||||
updatedAt: '1 week ago',
|
||||
author: 'John',
|
||||
category: '🛠️ Development',
|
||||
views: 400,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story title="Pages/HelpCenter/CategoryPage" :layout="{ type: 'single' }">
|
||||
<Variant title="All Categories">
|
||||
<div class="w-full min-h-screen bg-white dark:bg-slate-900">
|
||||
<CategoriesPage :categories="categories" />
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
-101
@@ -1,101 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
// import { OnClickOutside } from '@vueuse/components';
|
||||
import HelpCenterLayout from 'dashboard/components-next/HelpCenter/HelpCenterLayout.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Breadcrumb from 'dashboard/components-next/breadcrumb/Breadcrumb.vue';
|
||||
import CategoryList from 'dashboard/components-next/HelpCenter/Pages/CategoryPage/CategoryList.vue';
|
||||
import ArticleList from 'dashboard/components-next/HelpCenter/Pages/ArticlePage/ArticleList.vue';
|
||||
// import EditCategory from 'dashboard/playground/HelpCenter/components/EditCategory.vue';
|
||||
|
||||
const props = defineProps({
|
||||
categories: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const selectedCategory = ref(null);
|
||||
// const showEditCategory = ref(false);
|
||||
|
||||
// const openEditCategory = () => {
|
||||
// showEditCategory.value = true;
|
||||
// };
|
||||
// const closeEditCategory = () => {
|
||||
// showEditCategory.value = false;
|
||||
// };
|
||||
|
||||
const breadcrumbItems = computed(() => {
|
||||
const items = [{ label: 'Categories (en-US)', link: '#' }];
|
||||
if (selectedCategory.value) {
|
||||
items.push({
|
||||
label: selectedCategory.value.title,
|
||||
count: selectedCategory.value.articles.length,
|
||||
});
|
||||
}
|
||||
return items;
|
||||
});
|
||||
const openCategoryArticles = id => {
|
||||
selectedCategory.value = props.categories.find(
|
||||
category => category.id === id
|
||||
);
|
||||
};
|
||||
const resetCategory = () => {
|
||||
selectedCategory.value = null;
|
||||
};
|
||||
const displayedArticles = computed(() => {
|
||||
return selectedCategory.value ? selectedCategory.value.articles : [];
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/no-bare-strings-in-template -->
|
||||
<template>
|
||||
<HelpCenterLayout :show-pagination-footer="false">
|
||||
<template #header-actions>
|
||||
<div class="flex items-center justify-between">
|
||||
<div v-if="!selectedCategory" class="flex items-center gap-4">
|
||||
<Button
|
||||
label="English"
|
||||
size="sm"
|
||||
icon-position="right"
|
||||
icon="chevron-lucide-down"
|
||||
icon-lib="lucide"
|
||||
variant="secondary"
|
||||
/>
|
||||
<div
|
||||
class="w-px h-3.5 rounded my-auto bg-slate-75 dark:bg-slate-800"
|
||||
/>
|
||||
<span class="text-sm font-medium text-slate-800 dark:text-slate-100">
|
||||
{{ categories.length }} categories
|
||||
</span>
|
||||
</div>
|
||||
<Breadcrumb v-else :items="breadcrumbItems" @click="resetCategory" />
|
||||
<Button
|
||||
v-if="!selectedCategory"
|
||||
label="New category"
|
||||
icon="add"
|
||||
size="sm"
|
||||
/>
|
||||
<div v-else class="relative">
|
||||
<Button
|
||||
label="Edit category"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
@click="openEditCategory"
|
||||
/>
|
||||
<!-- <OnClickOutside @trigger="closeEditCategory">
|
||||
<EditCategory v-if="showEditCategory" @close="closeEditCategory" />
|
||||
</OnClickOutside> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<CategoryList
|
||||
v-if="!selectedCategory"
|
||||
:categories="categories"
|
||||
@click="openCategoryArticles"
|
||||
/>
|
||||
<ArticleList v-else :articles="displayedArticles" />
|
||||
</template>
|
||||
</HelpCenterLayout>
|
||||
</template>
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
<script setup>
|
||||
import CategoryCard from 'dashboard/components-next/HelpCenter/CategoryCard/CategoryCard.vue';
|
||||
|
||||
defineProps({
|
||||
categories: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['click']);
|
||||
|
||||
const handleClick = id => {
|
||||
emit('click', id);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul role="list" class="grid w-full h-full grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<CategoryCard
|
||||
v-for="category in categories"
|
||||
:id="category.id"
|
||||
:key="category.title"
|
||||
:title="category.title"
|
||||
:description="category.description"
|
||||
:articles-count="category.articlesCount"
|
||||
@click="handleClick(category.id)"
|
||||
/>
|
||||
</ul>
|
||||
</template>
|
||||
-146
@@ -1,146 +0,0 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
import { ARTICLE_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor';
|
||||
|
||||
import HelpCenterLayout from 'dashboard/components-next/HelpCenter/HelpCenterLayout.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import TextArea from 'dashboard/components-next/textarea/TextArea.vue';
|
||||
import FullEditor from 'dashboard/components/widgets/WootWriter/FullEditor.vue';
|
||||
|
||||
const { article } = defineProps({
|
||||
article: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['saveArticle']);
|
||||
|
||||
const saveArticle = debounce(value => emit('saveArticle', value), 400, false);
|
||||
|
||||
const articleTitle = computed({
|
||||
get: () => article.title,
|
||||
set: title => {
|
||||
saveArticle({ title });
|
||||
},
|
||||
});
|
||||
|
||||
const articleContent = computed({
|
||||
get: () => article.content,
|
||||
set: content => {
|
||||
saveArticle({ content });
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/no-bare-strings-in-template -->
|
||||
<template>
|
||||
<HelpCenterLayout :show-header-title="false" :show-pagination-footer="false">
|
||||
<template #header-actions>
|
||||
<div class="flex items-center justify-between h-20">
|
||||
<Button
|
||||
label="Back to articles"
|
||||
icon="chevron-lucide-left"
|
||||
icon-lib="lucide"
|
||||
variant="link"
|
||||
text-variant="info"
|
||||
size="sm"
|
||||
/>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="text-xs font-medium text-slate-500 dark:text-slate-400">
|
||||
Saved
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button label="Preview" variant="secondary" size="sm" />
|
||||
<Button
|
||||
label="Publish"
|
||||
icon="chevron-lucide-down"
|
||||
icon-position="right"
|
||||
icon-lib="lucide"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="flex flex-col gap-3 pl-4 mb-3 rtl:pr-3 rtl:pl-0">
|
||||
<TextArea
|
||||
v-model="articleTitle"
|
||||
class="h-12"
|
||||
custom-text-area-class="border-0 !text-[32px] !bg-transparent !py-0 !px-0 !h-auto !leading-[48px] !font-medium !tracking-[0.2px]"
|
||||
placeholder="Title"
|
||||
/>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-5 h-5 rounded-full bg-slate-100 dark:bg-slate-700" />
|
||||
<span class="text-sm text-slate-500 dark:text-slate-400">
|
||||
John Doe
|
||||
</span>
|
||||
</div>
|
||||
<div class="w-px h-3 bg-slate-50 dark:bg-slate-800" />
|
||||
<Button
|
||||
label="Uncategorized"
|
||||
icon="play-shape"
|
||||
variant="ghost"
|
||||
class="!px-2 font-normal"
|
||||
text-variant="info"
|
||||
/>
|
||||
<div class="w-px h-3 bg-slate-50 dark:bg-slate-800" />
|
||||
<Button
|
||||
label="More properties"
|
||||
icon="add"
|
||||
variant="ghost"
|
||||
class="!px-2 font-normal"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<FullEditor
|
||||
v-model="articleContent"
|
||||
class="py-0 pb-10 pl-4 rtl:pr-4 rtl:pl-0 h-fit"
|
||||
placeholder="Write something"
|
||||
:enabled-menu-options="ARTICLE_EDITOR_MENU_OPTIONS"
|
||||
/>
|
||||
</template>
|
||||
</HelpCenterLayout>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep {
|
||||
.ProseMirror .empty-node::before {
|
||||
@apply text-slate-200 dark:text-slate-500 text-base;
|
||||
}
|
||||
|
||||
.ProseMirror-menubar-wrapper {
|
||||
.ProseMirror-woot-style {
|
||||
@apply min-h-[15rem] max-h-full;
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror-menubar {
|
||||
display: none; // Hide by default
|
||||
}
|
||||
|
||||
.editor-root .has-selection {
|
||||
.ProseMirror-menubar {
|
||||
@apply h-8 rounded-lg !px-2 z-50 bg-slate-50 dark:bg-slate-800 items-center gap-4 ml-0 mb-0 shadow-md border border-slate-75 dark:border-slate-700/50;
|
||||
display: flex;
|
||||
top: var(--selection-top, auto) !important;
|
||||
left: var(--selection-left, 0) !important;
|
||||
width: fit-content !important;
|
||||
position: absolute !important;
|
||||
|
||||
.ProseMirror-menuitem {
|
||||
@apply mr-0;
|
||||
.ProseMirror-icon {
|
||||
@apply p-0 mt-1 !mr-0;
|
||||
svg {
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,23 +0,0 @@
|
||||
<script setup>
|
||||
import LocaleCard from 'dashboard/components-next/HelpCenter/LocaleCard/LocaleCard.vue';
|
||||
|
||||
defineProps({
|
||||
locales: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul role="list" class="w-full h-full space-y-4">
|
||||
<LocaleCard
|
||||
v-for="(locale, index) in locales"
|
||||
:key="index"
|
||||
:locale="locale.name"
|
||||
:is-default="locale.isDefault"
|
||||
:article-count="locale.articleCount"
|
||||
:category-count="locale.categoryCount"
|
||||
/>
|
||||
</ul>
|
||||
</template>
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
<script setup>
|
||||
import LocalesPage from './LocalesPage.vue';
|
||||
|
||||
const locales = [
|
||||
{
|
||||
name: 'English (en-US)',
|
||||
isDefault: true,
|
||||
articleCount: 5,
|
||||
categoryCount: 5,
|
||||
},
|
||||
{
|
||||
name: 'Spanish (es-ES)',
|
||||
isDefault: false,
|
||||
articleCount: 20,
|
||||
categoryCount: 10,
|
||||
},
|
||||
{
|
||||
name: 'English (en-UK)',
|
||||
isDefault: false,
|
||||
articleCount: 15,
|
||||
categoryCount: 7,
|
||||
},
|
||||
{
|
||||
name: 'Malay (ms-MY)',
|
||||
isDefault: false,
|
||||
articleCount: 15,
|
||||
categoryCount: 7,
|
||||
},
|
||||
{
|
||||
name: 'Malayalam (ml-IN)',
|
||||
isDefault: false,
|
||||
articleCount: 10,
|
||||
categoryCount: 5,
|
||||
},
|
||||
{
|
||||
name: 'Hindi (hi-IN)',
|
||||
isDefault: false,
|
||||
articleCount: 15,
|
||||
categoryCount: 7,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story title="Pages/HelpCenter/LocalePage" :layout="{ type: 'single' }">
|
||||
<Variant title="All Locales">
|
||||
<div class="w-full min-h-screen bg-white dark:bg-slate-900">
|
||||
<LocalesPage :locales="locales" />
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -1,47 +0,0 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import HelpCenterLayout from 'dashboard/components-next/HelpCenter/HelpCenterLayout.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import LocaleList from 'dashboard/components-next/HelpCenter/Pages/LocalePage/LocaleList.vue';
|
||||
|
||||
const props = defineProps({
|
||||
locales: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const localeCount = computed(() => props.locales?.length);
|
||||
|
||||
// TODO: remove comments
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const handleTabChange = tab => {
|
||||
// TODO: Implement tab change logic
|
||||
};
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const handlePageChange = page => {
|
||||
// TODO: Implement page change logic
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<HelpCenterLayout :show-pagination-footer="false">
|
||||
<template #header-actions>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="text-sm font-medium text-slate-800 dark:text-slate-100">
|
||||
{{ $t('HELP_CENTER.LOCALES_PAGE.LOCALES_COUNT', localeCount) }}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
:label="$t('HELP_CENTER.LOCALES_PAGE.NEW_LOCALE_BUTTON_TEXT')"
|
||||
icon="add"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<LocaleList :locales="locales" />
|
||||
</template>
|
||||
</HelpCenterLayout>
|
||||
</template>
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
<script setup>
|
||||
import PortalSettings from './PortalSettings.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story title="Pages/HelpCenter/PortalSettings" :layout="{ type: 'single' }">
|
||||
<Variant title="Default">
|
||||
<div class="w-[1000px] min-h-screen bg-white dark:bg-slate-900">
|
||||
<PortalSettings />
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
-113
@@ -1,113 +0,0 @@
|
||||
<script setup>
|
||||
import HelpCenterLayout from 'dashboard/components-next/HelpCenter/HelpCenterLayout.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import InlineInput from 'dashboard/components-next/inline-input/InlineInput.vue';
|
||||
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||
|
||||
const handleUploadAvatar = () => {};
|
||||
</script>
|
||||
|
||||
<!-- TODO: Add i18n -->
|
||||
<!-- eslint-disable vue/no-bare-strings-in-template -->
|
||||
<template>
|
||||
<HelpCenterLayout :show-pagination-footer="false">
|
||||
<template #content>
|
||||
<div class="flex flex-col w-full gap-10 max-w-[640px] pt-2 pb-8">
|
||||
<div class="flex flex-col w-full gap-4">
|
||||
<div class="flex flex-col w-full gap-2">
|
||||
<label
|
||||
class="mb-0.5 text-sm font-medium text-gray-900 dark:text-gray-50"
|
||||
>
|
||||
Avatar
|
||||
</label>
|
||||
<Avatar
|
||||
label="Avatar"
|
||||
src="https://api.dicebear.com/9.x/avataaars/svg?seed=Amaya"
|
||||
class="bg-ruby-300 dark:bg-ruby-400"
|
||||
@upload="handleUploadAvatar"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col w-full gap-2">
|
||||
<div class="flex justify-between w-full h-10 gap-2 py-1">
|
||||
<label
|
||||
class="text-sm font-medium whitespace-nowrap min-w-[100px] text-slate-900 dark:text-slate-50"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
<Input placeholder="Name" class="w-[432px]" />
|
||||
</div>
|
||||
<div class="flex justify-between w-full h-10 gap-2 py-1">
|
||||
<label
|
||||
class="text-sm font-medium whitespace-nowrap min-w-[100px] text-slate-900 dark:text-slate-50"
|
||||
>
|
||||
Header text
|
||||
</label>
|
||||
<Input placeholder="Header text" class="w-[432px]" />
|
||||
</div>
|
||||
<div class="flex justify-between w-full h-10 gap-2 py-1">
|
||||
<label
|
||||
class="text-sm font-medium whitespace-nowrap min-w-[100px] text-slate-900 dark:text-slate-50"
|
||||
>
|
||||
Page title
|
||||
</label>
|
||||
<Input placeholder="Page title" class="w-[432px]" />
|
||||
</div>
|
||||
<div class="flex justify-between w-full h-10 gap-2 py-1">
|
||||
<label
|
||||
class="text-sm font-medium whitespace-nowrap min-w-[100px] text-slate-900 dark:text-slate-50"
|
||||
>
|
||||
Widget color
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex justify-end w-full gap-2 py-2">
|
||||
<Button label="Save changes" size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full h-px bg-slate-50 dark:bg-slate-800/50" />
|
||||
</div>
|
||||
<div class="flex flex-col w-full gap-6">
|
||||
<div class="flex flex-col w-full gap-6">
|
||||
<h6 class="text-base font-medium text-slate-900 dark:text-slate-50">
|
||||
Configuration
|
||||
</h6>
|
||||
<div class="flex flex-col w-full gap-4">
|
||||
<div class="flex justify-between w-full gap-2 py-1">
|
||||
<InlineInput
|
||||
placeholder="Slug"
|
||||
label="Slug:"
|
||||
custom-label-class="min-w-[100px]"
|
||||
custom-input-class="!w-[430px]"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-between w-full gap-2 py-1">
|
||||
<InlineInput
|
||||
placeholder="Custom domain"
|
||||
label="Custom domain:"
|
||||
custom-label-class="min-w-[100px]"
|
||||
custom-input-class="!w-[430px]"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-between w-full gap-2 py-1">
|
||||
<InlineInput
|
||||
placeholder="Home page link"
|
||||
label="Home page link:"
|
||||
custom-label-class="min-w-[100px]"
|
||||
custom-input-class="!w-[430px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end w-full gap-3 py-4">
|
||||
<Button label="Edit configuration" size="sm" variant="secondary" />
|
||||
<Button
|
||||
label="Delete Test-Help Center"
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</HelpCenterLayout>
|
||||
</template>
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
<script setup>
|
||||
import PortalSwitcher from './PortalSwitcher.vue';
|
||||
|
||||
const portals = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Chatwoot Help Center',
|
||||
articles: 67,
|
||||
domain: 'chatwoot.help',
|
||||
slug: 'help-center',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Chatwoot Handbook',
|
||||
articles: 42,
|
||||
domain: 'chatwoot.help',
|
||||
slug: 'handbook',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Developer Documentation',
|
||||
articles: 89,
|
||||
domain: 'dev.chatwoot.com',
|
||||
slug: 'docs',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Components/HelpCenter/PortalSwitcher"
|
||||
:layout="{ type: 'grid', width: '510px' }"
|
||||
>
|
||||
<Variant title="Portal Switcher">
|
||||
<div class="h-[500px] p-4 bg-slate-100 dark:bg-slate-900">
|
||||
<PortalSwitcher
|
||||
:portals="portals"
|
||||
header="Choose a Portal"
|
||||
description="Select from available help center portals"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
-111
@@ -1,111 +0,0 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
defineProps({
|
||||
portals: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Chatwoot Help Center',
|
||||
articles: 67,
|
||||
domain: 'chatwoot.help',
|
||||
slug: 'help-center',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Chatwoot Handbook',
|
||||
articles: 42,
|
||||
domain: 'chatwoot.help',
|
||||
slug: 'handbook',
|
||||
},
|
||||
],
|
||||
},
|
||||
header: {
|
||||
type: String,
|
||||
default: 'Portals',
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: 'Create and manage multiple portals',
|
||||
},
|
||||
});
|
||||
|
||||
const selectedPortal = ref(1);
|
||||
|
||||
const handlePortalChange = id => {
|
||||
selectedPortal.value = id;
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- TODO: Add i18n -->
|
||||
<!-- eslint-disable vue/no-bare-strings-in-template -->
|
||||
<template>
|
||||
<div
|
||||
class="pt-5 pb-3 bg-white z-50 dark:bg-slate-800 absolute w-[440px] rounded-xl shadow-md flex flex-col gap-4"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-4 px-6 pb-2">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h2 class="text-base font-medium text-slate-900 dark:text-slate-50">
|
||||
{{ header }}
|
||||
</h2>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-300">
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
<Button label="New portal" variant="secondary" icon="add" size="sm" />
|
||||
</div>
|
||||
<div v-if="portals.length > 0" class="flex flex-col gap-3">
|
||||
<template v-for="(portal, index) in portals" :key="portal.id">
|
||||
<div class="flex flex-col gap-2 px-6 py-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
:id="portal.id"
|
||||
v-model="selectedPortal"
|
||||
type="radio"
|
||||
:value="portal.id"
|
||||
class="mr-3"
|
||||
@change="handlePortalChange(portal.id)"
|
||||
/>
|
||||
<label
|
||||
:for="portal.id"
|
||||
class="text-sm font-medium text-slate-900 dark:text-slate-100"
|
||||
>
|
||||
{{ portal.name }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="w-4 h-4 rounded-full bg-slate-100 dark:bg-slate-700" />
|
||||
</div>
|
||||
<div class="inline-flex items-center gap-2 py-1 text-sm">
|
||||
<span class="text-slate-600 dark:text-slate-400">
|
||||
articles:
|
||||
<span class="text-slate-800 dark:text-slate-200">
|
||||
{{ portal.articles }}
|
||||
</span>
|
||||
</span>
|
||||
<div class="w-px h-3 bg-slate-50 dark:bg-slate-700" />
|
||||
<span class="text-slate-600 dark:text-slate-400">
|
||||
domain:
|
||||
<span class="text-slate-800 dark:text-slate-200">
|
||||
{{ portal.domain }}
|
||||
</span>
|
||||
</span>
|
||||
<div class="w-px h-3 bg-slate-50 dark:bg-slate-700" />
|
||||
<span class="text-slate-600 dark:text-slate-400">
|
||||
slug:
|
||||
<span class="text-slate-800 dark:text-slate-200">
|
||||
{{ portal.slug }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="index < portals.length - 1 && portals.length > 1"
|
||||
class="w-full h-px bg-slate-50 dark:bg-slate-700/50"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,36 +0,0 @@
|
||||
<script setup>
|
||||
import Avatar from './Avatar.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story title="Components/Avatar" :layout="{ type: 'grid', width: '400' }">
|
||||
<Variant title="Default">
|
||||
<div class="p-4 bg-white dark:bg-slate-900">
|
||||
<Avatar
|
||||
src="https://api.dicebear.com/9.x/avataaars/svg?seed=Amaya"
|
||||
class="bg-ruby-300 dark:bg-ruby-900"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Different Sizes">
|
||||
<div class="flex flex-wrap gap-4 p-4 bg-white dark:bg-slate-900">
|
||||
<Avatar
|
||||
src="https://api.dicebear.com/9.x/avataaars/svg?seed=Felix"
|
||||
:size="48"
|
||||
class="bg-green-300 dark:bg-green-900"
|
||||
/>
|
||||
<Avatar
|
||||
:size="72"
|
||||
src="https://api.dicebear.com/9.x/avataaars/svg?seed=Jade"
|
||||
class="bg-indigo-300 dark:bg-indigo-900"
|
||||
/>
|
||||
<Avatar
|
||||
src="https://api.dicebear.com/9.x/avataaars/svg?seed=Emery"
|
||||
:size="96"
|
||||
class="bg-woot-300 dark:bg-woot-900"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -1,51 +0,0 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import Icon from '../icon/Icon.vue';
|
||||
|
||||
const props = defineProps({
|
||||
src: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 72,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['upload']);
|
||||
|
||||
const avatarSize = computed(() => `${props.size}px`);
|
||||
const iconSize = computed(() => `${props.size / 2}px`);
|
||||
|
||||
const handleUploadAvatar = () => {
|
||||
emit('upload');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="relative flex flex-col items-center gap-2 select-none rounded-xl group/avatar"
|
||||
:style="{
|
||||
width: avatarSize,
|
||||
height: avatarSize,
|
||||
}"
|
||||
>
|
||||
<img
|
||||
v-if="src"
|
||||
:src="props.src"
|
||||
alt="avatar"
|
||||
class="w-full h-full shadow-sm rounded-xl"
|
||||
/>
|
||||
<div
|
||||
class="absolute inset-0 flex items-center justify-center invisible w-full h-full transition-all duration-500 ease-in-out opacity-0 rounded-xl dark:bg-slate-900/50 bg-slate-900/20 group-hover/avatar:visible group-hover/avatar:opacity-100"
|
||||
@click="handleUploadAvatar"
|
||||
>
|
||||
<Icon
|
||||
icon="i-lucide-upload"
|
||||
:size="iconSize"
|
||||
class="text-white dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,63 +0,0 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import Breadcrumb from './Breadcrumb.vue';
|
||||
|
||||
const singleItem = ref([{ label: 'Home', link: '#' }]);
|
||||
const twoItems = ref([
|
||||
{ label: 'Home', link: '#' },
|
||||
{ label: 'Categories', link: '#' },
|
||||
]);
|
||||
const threeItems = ref([
|
||||
{ label: 'Home', link: '#' },
|
||||
{ label: 'Categories', link: '#' },
|
||||
{ label: 'Marketing', count: 6 },
|
||||
]);
|
||||
const longBreadcrumb = ref([
|
||||
{ label: 'Home', link: '#' },
|
||||
{ label: 'Categories', link: '#' },
|
||||
{ label: 'Marketing', link: '#' },
|
||||
{ label: 'Digital', link: '#' },
|
||||
{ label: 'Social Media', count: 12 },
|
||||
]);
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/no-bare-strings-in-template -->
|
||||
<!-- eslint-disable vue/no-undef-components -->
|
||||
<template>
|
||||
<Story
|
||||
title="Components/Breadcrumb"
|
||||
:layout="{ type: 'grid', width: '800px' }"
|
||||
>
|
||||
<Variant title="Single Item">
|
||||
<div class="w-full p-4 bg-white dark:bg-slate-900">
|
||||
<Breadcrumb :items="singleItem" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Two Items">
|
||||
<div class="w-full p-4 bg-white dark:bg-slate-900">
|
||||
<Breadcrumb :items="twoItems" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Three Items with Count">
|
||||
<div class="w-full p-4 bg-white dark:bg-slate-900">
|
||||
<Breadcrumb :items="threeItems" count-label="articles" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Long Breadcrumb">
|
||||
<div class="w-full p-4 bg-white dark:bg-slate-900">
|
||||
<Breadcrumb :items="longBreadcrumb" count-label="articles" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="RTL Support">
|
||||
<div dir="rtl">
|
||||
<div class="w-full p-4 bg-white dark:bg-slate-900">
|
||||
<Breadcrumb :items="threeItems" count-label="articles" />
|
||||
</div>
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -1,60 +0,0 @@
|
||||
<script setup>
|
||||
import { defineProps } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
||||
|
||||
defineProps({
|
||||
items: {
|
||||
type: Array,
|
||||
required: true,
|
||||
validator: value => {
|
||||
return value.every(
|
||||
item =>
|
||||
typeof item.label === 'string' &&
|
||||
(item.link === undefined || typeof item.link === 'string') &&
|
||||
(item.count === undefined || typeof item.count === 'number')
|
||||
);
|
||||
},
|
||||
},
|
||||
countLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav :aria-label="t('BREADCRUMB.ARIA_LABEL')" class="flex items-center h-8">
|
||||
<ol class="flex items-center mb-0">
|
||||
<li
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
class="flex items-center gap-3"
|
||||
>
|
||||
<template v-if="index === items.length - 1">
|
||||
<span class="text-sm text-slate-900 dark:text-slate-50">
|
||||
{{
|
||||
`${item.label}${item.count ? ` (${item.count} ${countLabel})` : ''}`
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
<a
|
||||
v-else
|
||||
:href="item.link"
|
||||
class="text-sm transition-colors duration-200 text-slate-300 dark:text-slate-500 hover:text-slate-700 dark:hover:text-slate-100"
|
||||
>
|
||||
{{ item.label }}
|
||||
</a>
|
||||
<FluentIcon
|
||||
v-if="index < items.length - 1"
|
||||
icon="chevron-lucide-right"
|
||||
size="18"
|
||||
icon-lib="lucide"
|
||||
class="flex-shrink-0 text-slate-300 dark:text-slate-500 ltr:mr-3 rtl:mr-0 rtl:ml-3"
|
||||
/>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</template>
|
||||
@@ -1,126 +0,0 @@
|
||||
<script setup>
|
||||
import Button from './Button.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story title="Components/Button" :layout="{ type: 'grid', width: '400' }">
|
||||
<Variant title="Default">
|
||||
<div class="p-4 bg-white dark:bg-slate-900">
|
||||
<Button label="Default Button" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Disabled">
|
||||
<div class="flex flex-wrap gap-2 p-4 bg-white dark:bg-slate-900">
|
||||
<Button label="Disabled" disabled />
|
||||
<Button label="Disabled" variant="outline" disabled />
|
||||
<Button label="Disabled" disabled icon="delete" variant="outline" />
|
||||
<Button
|
||||
label="Disabled"
|
||||
disabled
|
||||
icon="delete"
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
/>
|
||||
<Button
|
||||
label="Disabled"
|
||||
disabled
|
||||
icon="delete"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
/>
|
||||
<Button
|
||||
label="Disabled"
|
||||
disabled
|
||||
icon="delete"
|
||||
variant="link"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Disabled with icon">
|
||||
<div class="p-4 bg-white dark:bg-slate-900">
|
||||
<Button label="Disabled Button" icon="emoji-add" disabled />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Different variant">
|
||||
<div class="flex flex-wrap gap-2 p-4 bg-white dark:bg-slate-900">
|
||||
<Button label="Default" variant="default" />
|
||||
<Button label="Destructive" variant="destructive" />
|
||||
<Button label="Outline" variant="outline" />
|
||||
<Button label="Secondary" variant="secondary" />
|
||||
<Button label="Ghost" variant="ghost" />
|
||||
<Button label="Link" variant="link" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Different variant with icon only">
|
||||
<div class="flex flex-wrap gap-2 p-4 bg-white dark:bg-slate-900">
|
||||
<Button icon="emoji-add" variant="default" />
|
||||
<Button icon="emoji-add" variant="destructive" />
|
||||
<Button icon="emoji-add" variant="outline" />
|
||||
<Button icon="emoji-add" variant="secondary" />
|
||||
<Button icon="emoji-add" variant="ghost" />
|
||||
<Button icon="emoji-add" variant="link" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Different size">
|
||||
<div class="flex flex-wrap gap-2 p-4 bg-white dark:bg-slate-900">
|
||||
<Button label="Default" />
|
||||
<Button label="Large" size="lg" />
|
||||
<Button label="Small" size="sm" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Different text variant">
|
||||
<div class="flex flex-wrap gap-2 p-4 bg-white dark:bg-slate-900">
|
||||
<Button label="Default" text-variant="default" variant="outline" />
|
||||
<Button label="Success" text-variant="success" variant="outline" />
|
||||
<Button label="Warning" text-variant="warning" variant="outline" />
|
||||
<Button label="Danger" text-variant="danger" variant="outline" />
|
||||
<Button label="Info" text-variant="info" variant="outline" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Button with left icon with different sizes and icon only">
|
||||
<div class="flex flex-wrap gap-2 p-4 bg-white dark:bg-slate-900">
|
||||
<Button label="Default" icon="emoji-add" icon-position="left" />
|
||||
<Button
|
||||
label="Default LG"
|
||||
icon="emoji-add"
|
||||
icon-position="left"
|
||||
size="lg"
|
||||
/>
|
||||
<Button
|
||||
label="Default SM"
|
||||
icon="emoji-add"
|
||||
icon-position="left"
|
||||
size="sm"
|
||||
/>
|
||||
<Button icon="emoji-add" size="icon" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Button with right icon with different sizes and icon only">
|
||||
<div class="flex flex-wrap gap-2 p-4 bg-white dark:bg-slate-900">
|
||||
<Button label="Default" icon="emoji-add" icon-position="right" />
|
||||
<Button
|
||||
label="Default LG"
|
||||
icon="emoji-add"
|
||||
icon-position="right"
|
||||
size="lg"
|
||||
/>
|
||||
<Button
|
||||
label="Default SM"
|
||||
icon="emoji-add"
|
||||
icon-position="right"
|
||||
size="sm"
|
||||
/>
|
||||
<Button icon="emoji-add" size="icon" />
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -1,130 +0,0 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator: value =>
|
||||
[
|
||||
'default',
|
||||
'destructive',
|
||||
'outline',
|
||||
'secondary',
|
||||
'ghost',
|
||||
'link',
|
||||
].includes(value),
|
||||
},
|
||||
textVariant: {
|
||||
type: String,
|
||||
default: '',
|
||||
validator: value =>
|
||||
['', 'default', 'success', 'warning', 'danger', 'info'].includes(value),
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator: value => ['default', 'sm', 'lg', 'icon'].includes(value),
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
iconPosition: {
|
||||
type: String,
|
||||
default: 'left',
|
||||
validator: value => ['left', 'right'].includes(value),
|
||||
},
|
||||
iconLib: {
|
||||
type: String,
|
||||
default: 'fluent',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['click']);
|
||||
|
||||
const buttonVariants = {
|
||||
variant: {
|
||||
default:
|
||||
'bg-woot-500 dark:bg-woot-500 text-white dark:text-white hover:bg-woot-600 dark:hover:bg-woot-600',
|
||||
destructive:
|
||||
'bg-ruby-700 dark:bg-ruby-700 text-white dark:text-white hover:bg-ruby-800 dark:hover:bg-ruby-800',
|
||||
outline:
|
||||
'border border-slate-200 dark:border-slate-700/50 hover:border-slate-300 dark:hover:border-slate-600',
|
||||
secondary:
|
||||
'bg-slate-50 text-slate-900 dark:bg-slate-700/50 dark:text-slate-100 hover:bg-slate-100 dark:hover:bg-slate-600',
|
||||
ghost:
|
||||
'text-slate-900 dark:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800',
|
||||
link: 'text-woot-500 underline-offset-4 hover:underline dark:hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-10 px-4 py-2',
|
||||
sm: 'h-8 px-3',
|
||||
lg: 'h-11 px-4',
|
||||
icon: 'h-auto w-auto px-2',
|
||||
},
|
||||
text: {
|
||||
default:
|
||||
'!text-woot-500 dark:!text-woot-500 hover:!text-woot-600 dark:hover:!text-woot-600',
|
||||
success:
|
||||
'!text-green-500 dark:!text-green-500 hover:!text-green-600 dark:hover:!text-green-600',
|
||||
warning:
|
||||
'!text-amber-600 dark:!text-amber-600 hover:!text-amber-600 dark:hover:!text-amber-600',
|
||||
danger:
|
||||
'!text-ruby-700 dark:!text-ruby-700 hover:!text-ruby-800 dark:hover:!text-ruby-800',
|
||||
info: '!text-slate-500 dark:!text-slate-400 hover:!text-slate-600 dark:hover:!text-slate-500',
|
||||
},
|
||||
};
|
||||
|
||||
const buttonClasses = computed(() => {
|
||||
const classes = [
|
||||
buttonVariants.variant[props.variant],
|
||||
buttonVariants.size[props.size],
|
||||
];
|
||||
|
||||
if (props.textVariant && buttonVariants.text[props.textVariant]) {
|
||||
classes.push(buttonVariants.text[props.textVariant]);
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
});
|
||||
|
||||
const iconSize = computed(() => {
|
||||
if (props.size === 'sm') return 16;
|
||||
if (props.size === 'lg') return 20;
|
||||
return 18;
|
||||
});
|
||||
|
||||
const handleClick = () => {
|
||||
emit('click');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
:class="buttonClasses"
|
||||
class="inline-flex items-center justify-center h-10 min-w-0 gap-2 text-sm font-medium transition-all duration-200 ease-in-out rounded-lg disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-50"
|
||||
@click="handleClick"
|
||||
>
|
||||
<FluentIcon
|
||||
v-if="icon && iconPosition === 'left'"
|
||||
:icon="icon"
|
||||
:size="iconSize"
|
||||
:icon-lib="iconLib"
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
<span v-if="label" class="min-w-0 truncate">{{ label }}</span>
|
||||
<FluentIcon
|
||||
v-if="icon && iconPosition === 'right'"
|
||||
:icon="icon"
|
||||
:size="iconSize"
|
||||
:icon-lib="iconLib"
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
@@ -1,30 +0,0 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import ComboBox from './ComboBox.vue';
|
||||
|
||||
const options = [
|
||||
{ value: 1, label: 'Option 1' },
|
||||
{ value: 2, label: 'Option 2' },
|
||||
{ value: 3, label: 'Option 3' },
|
||||
{ value: 4, label: 'Option 4' },
|
||||
{ value: 5, label: 'Option 5' },
|
||||
];
|
||||
const selectedValue = ref('');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story title="Components/ComboBox" :layout="{ type: 'grid', width: '300px' }">
|
||||
<Variant title="Default">
|
||||
<div class="w-full p-4 bg-white h-80 dark:bg-slate-900">
|
||||
<ComboBox v-model="selectedValue" :options="options" />
|
||||
<p class="mt-2">Selected value: {{ selectedValue }}</p>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Disabled">
|
||||
<div class="w-full p-4 bg-white h-80 dark:bg-slate-900">
|
||||
<ComboBox :options="options" disabled />
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -1,163 +0,0 @@
|
||||
<script setup>
|
||||
import { nextTick, ref, computed, watch } from 'vue';
|
||||
import { onClickOutside } from '@vueuse/core';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Array,
|
||||
required: true,
|
||||
validator: value =>
|
||||
value.every(option => 'value' in option && 'label' in option),
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
searchPlaceholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
emptyState: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const selectedValue = ref(props.modelValue);
|
||||
const open = ref(false);
|
||||
const search = ref('');
|
||||
const searchInput = ref(null);
|
||||
const comboboxRef = ref(null);
|
||||
|
||||
const filteredOptions = computed(() => {
|
||||
const searchTerm = search.value.toLowerCase();
|
||||
return props.options.filter(option =>
|
||||
option.label.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
});
|
||||
const selectPlaceholder = computed(() => {
|
||||
return props.placeholder || t('COMBOBOX.PLACEHOLDER');
|
||||
});
|
||||
const selectedLabel = computed(() => {
|
||||
const selected = props.options.find(
|
||||
option => option.value === selectedValue.value
|
||||
);
|
||||
return selected?.label ?? selectPlaceholder.value;
|
||||
});
|
||||
|
||||
const selectOption = option => {
|
||||
selectedValue.value = option.value;
|
||||
emit('update:modelValue', option.value);
|
||||
open.value = false;
|
||||
search.value = '';
|
||||
};
|
||||
const toggleDropdown = () => {
|
||||
open.value = !open.value;
|
||||
if (open.value) {
|
||||
search.value = '';
|
||||
nextTick(() => searchInput.value.focus());
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
newValue => {
|
||||
selectedValue.value = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
onClickOutside(comboboxRef, () => {
|
||||
open.value = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="comboboxRef"
|
||||
class="relative w-full"
|
||||
:class="{
|
||||
'cursor-not-allowed': disabled,
|
||||
'group/combobox': !disabled,
|
||||
}"
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
:label="selectedLabel"
|
||||
icon-position="right"
|
||||
size="sm"
|
||||
:disabled="disabled"
|
||||
class="justify-between w-full text-slate-900 dark:text-slate-100 group-hover/combobox:border-slate-300 dark:group-hover/combobox:border-slate-600"
|
||||
:icon="open ? 'chevron-up' : 'chevron-down'"
|
||||
@click="toggleDropdown"
|
||||
/>
|
||||
<div
|
||||
v-show="open"
|
||||
class="absolute z-50 w-full mt-1 transition-opacity duration-200 bg-white border rounded-md shadow-lg border-slate-200 dark:bg-slate-900 dark:border-slate-700/50"
|
||||
>
|
||||
<div class="relative border-b border-slate-100 dark:border-slate-700/50">
|
||||
<FluentIcon
|
||||
icon="search"
|
||||
:size="14"
|
||||
class="absolute text-gray-400 dark:text-slate-500 top-3 left-3"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<input
|
||||
ref="searchInput"
|
||||
v-model="search"
|
||||
type="search"
|
||||
:placeholder="searchPlaceholder || t('COMBOBOX.SEARCH_PLACEHOLDER')"
|
||||
class="w-full py-2 pl-10 pr-2 text-sm bg-white border-none rounded-t-md dark:bg-slate-900 text-slate-900 dark:text-slate-50"
|
||||
/>
|
||||
</div>
|
||||
<ul
|
||||
class="py-1 overflow-auto max-h-60"
|
||||
role="listbox"
|
||||
:aria-activedescendant="selectedValue"
|
||||
>
|
||||
<li
|
||||
v-for="option in filteredOptions"
|
||||
:key="option.value"
|
||||
class="flex items-center justify-between w-full gap-2 px-3 py-2 text-sm transition-colors duration-150 cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800/50"
|
||||
:class="{
|
||||
'bg-slate-50 dark:bg-slate-800/50': option.value === selectedValue,
|
||||
}"
|
||||
role="option"
|
||||
:aria-selected="option.value === selectedValue"
|
||||
@click="selectOption(option)"
|
||||
>
|
||||
<span :class="{ 'font-medium': option.value === selectedValue }">
|
||||
{{ option.label }}
|
||||
</span>
|
||||
<FluentIcon
|
||||
v-if="option.value === selectedValue"
|
||||
icon="checkmark"
|
||||
:size="16"
|
||||
class="flex-shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
v-if="filteredOptions.length === 0"
|
||||
class="px-3 py-2 text-sm text-slate-600 dark:text-slate-300"
|
||||
>
|
||||
{{ emptyState || t('COMBOBOX.EMPTY_STATE') }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,81 +0,0 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import Dialog from './Dialog.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
|
||||
const alertDialog = ref(null);
|
||||
const editDialog = ref(null);
|
||||
const confirmDialog = ref(null);
|
||||
|
||||
const openAlertDialog = () => {
|
||||
alertDialog.value.open();
|
||||
};
|
||||
const openEditDialog = () => {
|
||||
editDialog.value.open();
|
||||
};
|
||||
const openConfirmDialog = () => {
|
||||
confirmDialog.value.open();
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const onConfirm = dialog => {};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story title="Components/Dialog" :layout="{ type: 'grid', width: '100%' }">
|
||||
<Variant title="Alert Dialog">
|
||||
<Button label="Open Alert Dialog" @click="openAlertDialog" />
|
||||
<Dialog
|
||||
ref="alertDialog"
|
||||
type="alert"
|
||||
title="Alert"
|
||||
description="This is an alert message."
|
||||
/>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Edit Dialog">
|
||||
<Button label="Open Edit Dialog" @click="openEditDialog" />
|
||||
<Dialog
|
||||
ref="editDialog"
|
||||
type="edit"
|
||||
description="You can create a new portal here, by providing a name and a slug."
|
||||
title="Create Portal"
|
||||
confirm-button-label="Save"
|
||||
@confirm="onConfirm()"
|
||||
>
|
||||
<template #form>
|
||||
<div class="flex flex-col gap-6">
|
||||
<Input
|
||||
id="portal-name"
|
||||
type="text"
|
||||
placeholder="User Guide | Chatwoot"
|
||||
label="Name"
|
||||
message="This will be the name of your public facing portal"
|
||||
/>
|
||||
<Input
|
||||
id="portal-slug"
|
||||
type="text"
|
||||
placeholder="user-guide"
|
||||
label="Slug"
|
||||
message="app.chatwoot.com/hc/my-portal/en-US/categories/my-slug"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Confirm Dialog">
|
||||
<Button label="Open Confirm Dialog" @click="openConfirmDialog" />
|
||||
<Dialog
|
||||
ref="confirmDialog"
|
||||
type="confirm"
|
||||
title="Confirm Action"
|
||||
description="Are you sure you want to perform this action?"
|
||||
confirm-button-label="Yes, I'm sure"
|
||||
cancel-button-label="No, cancel"
|
||||
@confirm="onConfirm()"
|
||||
/>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -1,109 +0,0 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { onClickOutside } from '@vueuse/core';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: 'edit',
|
||||
validator: value => ['alert', 'edit'].includes(value),
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
cancelButtonLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
confirmButtonLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['confirm']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const dialogRef = ref(null);
|
||||
const dialogContentRef = ref(null);
|
||||
|
||||
const open = () => {
|
||||
dialogRef.value?.showModal();
|
||||
};
|
||||
const close = () => {
|
||||
dialogRef.value?.close();
|
||||
};
|
||||
const confirm = () => {
|
||||
emit('confirm');
|
||||
};
|
||||
|
||||
defineExpose({ open });
|
||||
|
||||
onClickOutside(dialogContentRef, event => {
|
||||
if (
|
||||
dialogRef.value &&
|
||||
dialogRef.value.open &&
|
||||
event.target === dialogRef.value
|
||||
) {
|
||||
close();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<dialog
|
||||
ref="dialogRef"
|
||||
class="w-full max-w-lg overflow-visible shadow-xl bg-modal-backdrop-light dark:bg-modal-backdrop-dark rounded-xl"
|
||||
@close="close"
|
||||
>
|
||||
<div
|
||||
ref="dialogContentRef"
|
||||
class="flex flex-col w-full h-auto gap-6 p-6 overflow-visible text-left align-middle transition-all duration-300 ease-in-out transform bg-white shadow-xl dark:bg-slate-800 rounded-xl"
|
||||
@click.stop
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<h3
|
||||
class="text-base font-medium leading-6 text-gray-900 dark:text-white"
|
||||
>
|
||||
{{ title }}
|
||||
</h3>
|
||||
<p
|
||||
v-if="description"
|
||||
class="mb-0 text-sm text-slate-500 dark:text-slate-400"
|
||||
>
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
<slot name="form">
|
||||
<!-- Form content will be injected here -->
|
||||
</slot>
|
||||
<div class="flex items-center justify-between w-full gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
:label="cancelButtonLabel || t('DIALOG.BUTTONS.CANCEL')"
|
||||
class="w-full"
|
||||
size="sm"
|
||||
@click="close"
|
||||
/>
|
||||
<Button
|
||||
v-if="type !== 'alert'"
|
||||
:variant="type === 'edit' ? 'default' : 'destructive'"
|
||||
:label="confirmButtonLabel || t('DIALOG.BUTTONS.CONFIRM')"
|
||||
class="w-full"
|
||||
size="sm"
|
||||
@click="confirm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</Teleport>
|
||||
</template>
|
||||
@@ -1,55 +0,0 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import DropdownMenu from './DropdownMenu.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
const menuItems = [
|
||||
{ label: 'Edit', action: 'edit', icon: 'edit' },
|
||||
{ label: 'Publish', action: 'publish', icon: 'checkmark' },
|
||||
{ label: 'Archive', action: 'archive', icon: 'archive' },
|
||||
{ label: 'Delete', action: 'delete', icon: 'delete' },
|
||||
];
|
||||
const isOpen = ref(false);
|
||||
const toggleDropdown = () => {
|
||||
isOpen.value = !isOpen.value;
|
||||
};
|
||||
const handleAction = () => {
|
||||
isOpen.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story title="Components/DropdownMenu" :layout="{ type: 'grid', width: 300 }">
|
||||
<Variant title="Default">
|
||||
<div class="p-4 bg-white h-72 dark:bg-slate-900">
|
||||
<div class="relative">
|
||||
<Button label="Open Menu" size="sm" @click="toggleDropdown" />
|
||||
<DropdownMenu
|
||||
v-if="isOpen"
|
||||
:menu-items="menuItems"
|
||||
class="absolute left-0 top-10"
|
||||
@action="handleAction"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Always Open">
|
||||
<div class="p-4 bg-white h-72 dark:bg-slate-900">
|
||||
<DropdownMenu :menu-items="menuItems" @action="handleAction" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Custom Items">
|
||||
<div class="p-4 bg-white h-72 dark:bg-slate-900">
|
||||
<DropdownMenu
|
||||
:menu-items="[
|
||||
{ label: 'Custom 1', action: 'custom1', icon: 'file-upload' },
|
||||
{ label: 'Custom 2', action: 'custom2', icon: 'document' },
|
||||
{ label: 'Danger', action: 'delete', icon: 'delete' },
|
||||
]"
|
||||
@action="handleAction"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -1,35 +0,0 @@
|
||||
<script setup>
|
||||
import { defineProps, defineEmits } from 'vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
defineProps({
|
||||
menuItems: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['action']);
|
||||
|
||||
const handleAction = action => {
|
||||
emit('action', action);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="bg-white dark:bg-slate-800 absolute rounded-xl z-50 py-3 px-1 gap-2 flex flex-col min-w-[136px] shadow-lg"
|
||||
>
|
||||
<Button
|
||||
v-for="item in menuItems"
|
||||
:key="item.action"
|
||||
:label="item.label"
|
||||
:icon="item.icon"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="!justify-start w-full hover:bg-white dark:hover:bg-slate-800 z-60 font-normal"
|
||||
:text-variant="item.action === 'delete' ? 'danger' : ''"
|
||||
@click="handleAction(item.action)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,17 +0,0 @@
|
||||
<script setup>
|
||||
import { h, isVNode } from 'vue';
|
||||
const props = defineProps({
|
||||
icon: { type: [String, Object, Function], required: true },
|
||||
});
|
||||
const renderIcon = () => {
|
||||
if (!props.icon) return null;
|
||||
if (typeof props.icon === 'function' || isVNode(props.icon)) {
|
||||
return props.icon;
|
||||
}
|
||||
return h('span', { class: props.icon });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="renderIcon" />
|
||||
</template>
|
||||
@@ -1,82 +0,0 @@
|
||||
<script setup>
|
||||
import InlineInput from './InlineInput.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Components/InlineInput"
|
||||
:layout="{ type: 'grid', width: '400' }"
|
||||
>
|
||||
<Variant title="Default">
|
||||
<div class="p-4 bg-white dark:bg-slate-800">
|
||||
<InlineInput id="inline-input-1" placeholder="Default InlineInput" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="With Label">
|
||||
<div class="p-4 bg-white dark:bg-slate-800">
|
||||
<InlineInput
|
||||
id="inline-input-2"
|
||||
label="Username"
|
||||
placeholder="Enter your username"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Disabled">
|
||||
<div class="p-4 bg-white dark:bg-slate-800">
|
||||
<InlineInput
|
||||
id="inline-input-3"
|
||||
label="Disabled InlineInput"
|
||||
placeholder="Can't type here"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="With Custom Classes">
|
||||
<div class="flex flex-col gap-4 p-4 bg-white dark:bg-slate-800">
|
||||
<InlineInput
|
||||
id="inline-input-4"
|
||||
label="Custom Input Class"
|
||||
placeholder="Custom input style"
|
||||
custom-input-class="placeholder:text-green-200 dark:placeholder:text-green-800"
|
||||
/>
|
||||
<InlineInput
|
||||
id="inline-input-5"
|
||||
label="Custom Label Class"
|
||||
placeholder="Custom label style"
|
||||
custom-label-class="text-green-600 dark:text-green-400"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Different Types">
|
||||
<div class="flex flex-col gap-4 p-4 bg-white dark:bg-slate-800">
|
||||
<InlineInput
|
||||
id="inline-input-6"
|
||||
label="Text"
|
||||
placeholder="Text input"
|
||||
/>
|
||||
<InlineInput
|
||||
id="inline-input-7"
|
||||
label="Number"
|
||||
placeholder="Number input"
|
||||
type="number"
|
||||
/>
|
||||
<InlineInput
|
||||
id="inline-input-8"
|
||||
label="Password"
|
||||
placeholder="Password input"
|
||||
type="password"
|
||||
/>
|
||||
<InlineInput
|
||||
id="inline-input-9"
|
||||
label="Email"
|
||||
placeholder="Email input"
|
||||
type="email"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -1,65 +0,0 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text',
|
||||
},
|
||||
customInputClass: {
|
||||
type: [String, Object, Array],
|
||||
default: '',
|
||||
},
|
||||
customLabelClass: {
|
||||
type: [String, Object, Array],
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits(['update:modelValue']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="relative flex items-center justify-between w-full gap-2 whitespace-nowrap"
|
||||
>
|
||||
<label
|
||||
v-if="label"
|
||||
:for="id"
|
||||
:class="customLabelClass"
|
||||
class="mb-0.5 text-sm font-medium text-gray-900 dark:text-gray-50"
|
||||
>
|
||||
{{ label }}
|
||||
</label>
|
||||
<!-- Added prefix slot to allow adding custom labels to the input -->
|
||||
<slot name="prefix" />
|
||||
<input
|
||||
:id="id"
|
||||
:value="modelValue"
|
||||
:type="type"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:class="customInputClass"
|
||||
class="flex w-full reset-base text-sm h-6 !mb-0 border-0 rounded-lg bg-transparent dark:bg-transparent placeholder:text-slate-200 dark:placeholder:text-slate-500 disabled:cursor-not-allowed disabled:opacity-50 text-slate-900 dark:text-white transition-all duration-500 ease-in-out"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,68 +0,0 @@
|
||||
<script setup>
|
||||
import Input from './Input.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story title="Components/Input" :layout="{ type: 'grid', width: '400' }">
|
||||
<Variant title="Default">
|
||||
<div class="p-4 bg-white dark:bg-slate-800">
|
||||
<Input placeholder="Default Input" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="With Label">
|
||||
<div class="p-4 bg-white dark:bg-slate-800">
|
||||
<Input label="Username" placeholder="Enter your username" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Disabled">
|
||||
<div class="p-4 bg-white dark:bg-slate-800">
|
||||
<Input label="Disabled Input" placeholder="Can't type here" disabled />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="With Message">
|
||||
<div class="flex flex-col gap-4 p-4 bg-white dark:bg-slate-800">
|
||||
<Input
|
||||
label="Email"
|
||||
placeholder="Enter your email"
|
||||
message="We'll never share your email."
|
||||
/>
|
||||
<Input
|
||||
label="Password"
|
||||
type="password"
|
||||
placeholder="Enter your password"
|
||||
message="Password is incorrect"
|
||||
message-type="error"
|
||||
/>
|
||||
<Input
|
||||
label="Verification Code"
|
||||
placeholder="Enter the code"
|
||||
message="Code verified successfully"
|
||||
message-type="success"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Different Types">
|
||||
<div class="flex flex-col gap-4 p-4 bg-white dark:bg-slate-800">
|
||||
<Input label="Text" type="text" placeholder="Text input" />
|
||||
<Input label="Number" type="number" placeholder="Number input" />
|
||||
<Input label="Password" type="password" placeholder="Password input" />
|
||||
<Input label="Email" type="email" placeholder="Email input" />
|
||||
<Input label="Date" type="date" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Custom Input Class">
|
||||
<div class="p-4 bg-white dark:bg-slate-800">
|
||||
<Input
|
||||
label="Custom Style"
|
||||
placeholder="Custom input class"
|
||||
custom-input-class="border-yellow-500 dark:border-yellow-700"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -1,92 +0,0 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text',
|
||||
},
|
||||
customInputClass: {
|
||||
type: [String, Object, Array],
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
messageType: {
|
||||
type: String,
|
||||
default: 'info',
|
||||
validator: value => ['info', 'error', 'success'].includes(value),
|
||||
},
|
||||
});
|
||||
defineEmits(['update:modelValue']);
|
||||
const messageClass = computed(() => {
|
||||
switch (props.messageType) {
|
||||
case 'error':
|
||||
return 'text-red-500 dark:text-red-400';
|
||||
case 'success':
|
||||
return 'text-green-500 dark:text-green-400';
|
||||
default:
|
||||
return 'text-slate-500 dark:text-slate-400';
|
||||
}
|
||||
});
|
||||
const inputBorderClass = computed(() => {
|
||||
switch (props.messageType) {
|
||||
case 'error':
|
||||
return 'border-red-500 dark:border-red-400';
|
||||
default:
|
||||
return 'border-slate-100 dark:border-slate-700/50';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative flex flex-col gap-1">
|
||||
<label
|
||||
v-if="label"
|
||||
:for="id"
|
||||
class="mb-0.5 text-sm font-medium text-gray-900 dark:text-gray-50"
|
||||
>
|
||||
{{ label }}
|
||||
</label>
|
||||
<!-- Added prefix slot to allow adding icons to the input -->
|
||||
<slot name="prefix" />
|
||||
<input
|
||||
:id="id"
|
||||
:value="modelValue"
|
||||
:class="[customInputClass, inputBorderClass]"
|
||||
:type="type"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
class="flex w-full reset-base text-sm h-8 pl-3 pr-2 rtl:pr-3 rtl:pl-2 py-1.5 !mb-0 border rounded-lg focus:border-woot-500 dark:focus:border-woot-600 bg-white dark:bg-slate-900 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-slate-200 dark:placeholder:text-slate-500 disabled:cursor-not-allowed disabled:opacity-50 text-slate-900 dark:text-white transition-all duration-500 ease-in-out"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
/>
|
||||
<p
|
||||
v-if="message"
|
||||
class="mt-1 mb-0 text-xs transition-all duration-500 ease-in-out"
|
||||
:class="messageClass"
|
||||
>
|
||||
{{ message }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,80 +0,0 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import PaginationFooter from './PaginationFooter.vue';
|
||||
|
||||
const createPaginationState = (initialPage, totalItems, itemsPerPage) => {
|
||||
const currentPage = ref(initialPage);
|
||||
const handlePageChange = newPage => {
|
||||
currentPage.value = newPage;
|
||||
};
|
||||
return { currentPage, totalItems, itemsPerPage, handlePageChange };
|
||||
};
|
||||
|
||||
const defaultState = createPaginationState(1, 100, 16);
|
||||
const middlePageState = createPaginationState(3, 100, 16);
|
||||
const lastPageState = createPaginationState(7, 100, 16);
|
||||
const customItemsState = createPaginationState(2, 100, 10);
|
||||
const singlePageState = createPaginationState(1, 10, 16);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Components/PaginationFooter"
|
||||
:layout="{ type: 'grid', width: '957' }"
|
||||
>
|
||||
<Variant title="Default">
|
||||
<div class="p-4 bg-white dark:bg-slate-900">
|
||||
<PaginationFooter
|
||||
:current-page="defaultState.currentPage.value"
|
||||
:total-items="defaultState.totalItems"
|
||||
:items-per-page="defaultState.itemsPerPage"
|
||||
@update:current-page="defaultState.handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Middle Page">
|
||||
<div class="p-4 bg-white dark:bg-slate-900">
|
||||
<PaginationFooter
|
||||
:current-page="middlePageState.currentPage.value"
|
||||
:total-items="middlePageState.totalItems"
|
||||
:items-per-page="middlePageState.itemsPerPage"
|
||||
@update:current-page="middlePageState.handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Last Page">
|
||||
<div class="p-4 bg-white dark:bg-slate-900">
|
||||
<PaginationFooter
|
||||
:current-page="lastPageState.currentPage.value"
|
||||
:total-items="lastPageState.totalItems"
|
||||
:items-per-page="lastPageState.itemsPerPage"
|
||||
@update:current-page="lastPageState.handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Custom Items Per Page">
|
||||
<div class="p-4 bg-white dark:bg-slate-900">
|
||||
<PaginationFooter
|
||||
:current-page="customItemsState.currentPage.value"
|
||||
:total-items="customItemsState.totalItems"
|
||||
:items-per-page="customItemsState.itemsPerPage"
|
||||
@update:current-page="customItemsState.handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Single Page">
|
||||
<div class="p-4 bg-white dark:bg-slate-900">
|
||||
<PaginationFooter
|
||||
:current-page="singlePageState.currentPage.value"
|
||||
:total-items="singlePageState.totalItems"
|
||||
:items-per-page="singlePageState.itemsPerPage"
|
||||
@update:current-page="singlePageState.handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -1,113 +0,0 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
currentPage: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
totalItems: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
itemsPerPage: {
|
||||
type: Number,
|
||||
default: 16,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['update:currentPage']);
|
||||
const { t } = useI18n();
|
||||
|
||||
const totalPages = computed(() =>
|
||||
Math.ceil(props.totalItems / props.itemsPerPage)
|
||||
);
|
||||
const startItem = computed(
|
||||
() => (props.currentPage - 1) * props.itemsPerPage + 1
|
||||
);
|
||||
const endItem = computed(() =>
|
||||
Math.min(startItem.value + props.itemsPerPage - 1, props.totalItems)
|
||||
);
|
||||
const isFirstPage = computed(() => props.currentPage === 1);
|
||||
const isLastPage = computed(() => props.currentPage === totalPages.value);
|
||||
const changePage = newPage => {
|
||||
if (newPage >= 1 && newPage <= totalPages.value) {
|
||||
emit('update:currentPage', newPage);
|
||||
}
|
||||
};
|
||||
|
||||
const currentPageInformation = computed(() => {
|
||||
return t('PAGINATION_FOOTER.SHOWING', {
|
||||
startItem: startItem.value,
|
||||
endItem: endItem.value,
|
||||
totalItems: props.totalItems,
|
||||
});
|
||||
});
|
||||
|
||||
const pageInfo = computed(() => {
|
||||
return t('PAGINATION_FOOTER.CURRENT_PAGE_INFO', {
|
||||
currentPage: '',
|
||||
totalPages: totalPages.value,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex justify-between h-12 w-full max-w-[957px] mx-auto bg-slate-25 dark:bg-slate-800/50 rounded-xl py-2 px-3 items-center"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<span
|
||||
class="min-w-0 text-sm font-normal line-clamp-1 text-slate-600 dark:text-slate-300"
|
||||
>
|
||||
{{ currentPageInformation }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
icon="chevrons-lucide-left"
|
||||
icon-lib="lucide"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
:disabled="isFirstPage"
|
||||
@click="changePage(1)"
|
||||
/>
|
||||
<Button
|
||||
icon="chevron-lucide-left"
|
||||
icon-lib="lucide"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
:disabled="isFirstPage"
|
||||
@click="changePage(currentPage - 1)"
|
||||
/>
|
||||
<div
|
||||
class="inline-flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400"
|
||||
>
|
||||
<span
|
||||
class="px-3 tabular-nums py-0.5 bg-white dark:bg-slate-900 rounded-md"
|
||||
>
|
||||
{{ currentPage }}
|
||||
</span>
|
||||
<span>{{ pageInfo }}</span>
|
||||
</div>
|
||||
<Button
|
||||
icon="chevron-lucide-right"
|
||||
icon-lib="lucide"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
:disabled="isLastPage"
|
||||
@click="changePage(currentPage + 1)"
|
||||
/>
|
||||
<Button
|
||||
icon="chevrons-lucide-right"
|
||||
icon-lib="lucide"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
:disabled="isLastPage"
|
||||
@click="changePage(totalPages)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,15 +0,0 @@
|
||||
<script setup>
|
||||
import Spinner from './Spinner.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story title="Components/Spinner" :layout="{ type: 'grid', width: '400' }">
|
||||
<Variant title="Default">
|
||||
<Spinner size="24" class="text-n-slate-10" />
|
||||
<Spinner size="32" class="text-n-ruby-10" />
|
||||
<Spinner size="40" class="text-n-amber-10" />
|
||||
<Spinner size="48" class="text-n-teal-10" />
|
||||
<Spinner size="64" class="text-n-brand" />
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -1,25 +0,0 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
size: {
|
||||
type: Number,
|
||||
default: 24,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="8"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
class="animate-spin"
|
||||
>
|
||||
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
||||
</svg>
|
||||
</template>
|
||||
@@ -1,61 +0,0 @@
|
||||
<script setup>
|
||||
import TabBar from './TabBar.vue';
|
||||
|
||||
const defaultTabs = [
|
||||
{ label: 'All articles', count: 24 },
|
||||
{ label: 'Mine', count: 13 },
|
||||
{ label: 'Draft', count: 5 },
|
||||
{ label: 'Archived', count: 11 },
|
||||
{ label: 'Unpublished', count: 23 },
|
||||
{ label: 'Published', count: 100 },
|
||||
{ label: 'Scheduled', count: 49 },
|
||||
];
|
||||
|
||||
const customTabs = [
|
||||
{ label: 'Active', count: 10 },
|
||||
{ label: 'Pending', count: 5 },
|
||||
{ label: 'Completed', count: 15 },
|
||||
];
|
||||
|
||||
const handleTabChanged = tab => {
|
||||
console.log('Tab changed:', tab);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story title="Components/TabBar" :layout="{ type: 'grid', width: '920px' }">
|
||||
<Variant title="Default">
|
||||
<div class="p-4 bg-white dark:bg-slate-900">
|
||||
<TabBar :tabs="defaultTabs" @tab-changed="handleTabChanged" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Custom Tabs with Initial Tab">
|
||||
<div class="p-4 bg-white dark:bg-slate-900">
|
||||
<TabBar
|
||||
:tabs="customTabs"
|
||||
:initial-active-tab="1"
|
||||
@tab-changed="handleTabChanged"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="No Counts">
|
||||
<div class="p-4 bg-white dark:bg-slate-900">
|
||||
<TabBar
|
||||
:tabs="[{ label: 'Tab 1' }, { label: 'Tab 2' }, { label: 'Tab 3' }]"
|
||||
@tab-changed="handleTabChanged"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Single Tab">
|
||||
<div class="p-4 bg-white dark:bg-slate-900">
|
||||
<TabBar
|
||||
:tabs="[{ label: 'Single Tab', count: 42 }]"
|
||||
@tab-changed="handleTabChanged"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -1,58 +0,0 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
const props = defineProps({
|
||||
initialActiveTab: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
tabs: {
|
||||
type: Array,
|
||||
required: true,
|
||||
validator: value => {
|
||||
return value.every(
|
||||
tab => typeof tab.label === 'string' && typeof tab.count === 'number'
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['tabChanged']);
|
||||
const activeTab = ref(props.initialActiveTab);
|
||||
const selectTab = index => {
|
||||
activeTab.value = index;
|
||||
emit('tabChanged', props.tabs[index]);
|
||||
};
|
||||
const showDivider = index => {
|
||||
return (
|
||||
// Show dividers after the active tab, but not after the last tab
|
||||
(index > activeTab.value && index < props.tabs.length - 1) ||
|
||||
// Show dividers before the active tab, but not immediately before it and not before the first tab
|
||||
(index < activeTab.value - 1 && index > -1)
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-8 rounded-lg bg-slate-25 dark:bg-slate-800/50 w-fit">
|
||||
<template v-for="(tab, index) in tabs" :key="index">
|
||||
<button
|
||||
class="relative px-4 truncate py-1.5 text-sm border-0 rounded-lg transition-colors duration-300 ease-in-out"
|
||||
:class="[
|
||||
activeTab === index
|
||||
? 'text-woot-500 bg-woot-500/10 dark:bg-woot-500/10'
|
||||
: 'text-slate-500 dark:text-slate-400 hover:text-woot-500 dark:hover:text-woot-400',
|
||||
]"
|
||||
@click="selectTab(index)"
|
||||
>
|
||||
{{ tab.label }} {{ tab.count ? `(${tab.count})` : '' }}
|
||||
</button>
|
||||
<div
|
||||
class="w-px h-3.5 rounded my-auto transition-colors duration-300 ease-in-out"
|
||||
:class="
|
||||
showDivider(index)
|
||||
? 'bg-slate-75 dark:bg-slate-800'
|
||||
: 'bg-transparent dark:bg-transparent'
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,67 +0,0 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import TextArea from './TextArea.vue';
|
||||
|
||||
const bio = ref('');
|
||||
const description = ref('');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story title="Components/TextArea" :layout="{ type: 'grid', width: '400' }">
|
||||
<Variant title="Default">
|
||||
<div class="p-4 bg-white dark:bg-slate-800">
|
||||
<TextArea placeholder="Default TextArea" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="With Label">
|
||||
<div class="p-4 bg-white dark:bg-slate-800">
|
||||
<TextArea label="Description" placeholder="Enter your description" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Disabled">
|
||||
<div class="p-4 bg-white dark:bg-slate-800">
|
||||
<TextArea
|
||||
label="Disabled TextArea"
|
||||
placeholder="Can't type here"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="With Character Count">
|
||||
<div class="p-4 bg-white dark:bg-slate-800">
|
||||
<TextArea
|
||||
v-model="bio"
|
||||
label="Bio"
|
||||
placeholder="Tell us about yourself"
|
||||
:max-length="100"
|
||||
show-character-count
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Custom Max Length">
|
||||
<div class="p-4 bg-white dark:bg-slate-800">
|
||||
<TextArea
|
||||
v-model="description"
|
||||
label="Long Description"
|
||||
placeholder="Enter a longer description"
|
||||
:max-length="500"
|
||||
show-character-count
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Custom TextArea Class">
|
||||
<div class="p-4 bg-white dark:bg-slate-800">
|
||||
<TextArea
|
||||
label="Custom Style"
|
||||
placeholder="Custom textarea class"
|
||||
custom-text-area-class="border-yellow-500 dark:border-yellow-700"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -1,69 +0,0 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
maxLength: {
|
||||
type: Number,
|
||||
default: 200,
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
customTextAreaClass: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
showCharacterCount: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
defineEmits(['update:modelValue']);
|
||||
const characterCount = computed(() => props.modelValue.length);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative flex flex-col gap-1">
|
||||
<label
|
||||
v-if="label"
|
||||
:for="id"
|
||||
class="mb-0.5 text-sm font-medium text-gray-900 dark:text-gray-50"
|
||||
>
|
||||
{{ label }}
|
||||
</label>
|
||||
<textarea
|
||||
:id="id"
|
||||
:value="modelValue"
|
||||
:placeholder="placeholder"
|
||||
:maxlength="maxLength"
|
||||
class="flex w-full reset-base text-sm h-24 px-3 pt-3 !mb-0 border rounded-lg focus:border-woot-500 dark:focus:border-woot-600 bg-white dark:bg-slate-900 placeholder:text-slate-200 dark:placeholder:text-slate-500 text-slate-900 dark:text-white transition-all duration-500 ease-in-out resize-none disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-slate-25 dark:disabled:bg-slate-900"
|
||||
:class="[customTextAreaClass, showCharacterCount ? 'pb-9' : 'pb-3']"
|
||||
:disabled="disabled"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
/>
|
||||
<div
|
||||
v-if="showCharacterCount"
|
||||
class="absolute flex items-center justify-between mt-1 bottom-3 ltr:right-3 rtl:left-3"
|
||||
>
|
||||
<span class="text-xs tabular-nums text-slate-300 dark:text-slate-600">
|
||||
{{ characterCount }} / {{ maxLength }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,30 @@
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import AccordionItemComponent from './AccordionItem';
|
||||
|
||||
export default {
|
||||
title: 'Components/Generic/Accordion',
|
||||
component: AccordionItemComponent,
|
||||
argTypes: {
|
||||
title: {
|
||||
control: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: { AccordionItem: AccordionItemComponent },
|
||||
template: `
|
||||
<accordion-item v-bind="$props" @click="onClick">
|
||||
This is a sample content you can pass as a slot
|
||||
</accordion-item>
|
||||
`,
|
||||
});
|
||||
|
||||
export const AccordionItem = Template.bind({});
|
||||
AccordionItem.args = {
|
||||
onClick: action('Added'),
|
||||
title: 'Title of the accordion item',
|
||||
};
|
||||
@@ -1,34 +1,32 @@
|
||||
<script setup>
|
||||
<script>
|
||||
import EmojiOrIcon from 'shared/components/EmojiOrIcon.vue';
|
||||
import { defineEmits } from 'vue';
|
||||
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
export default {
|
||||
components: {
|
||||
EmojiOrIcon,
|
||||
},
|
||||
compact: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
compact: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
emoji: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isOpen: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
emoji: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isOpen: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['toggle']);
|
||||
|
||||
const onToggle = () => {
|
||||
emit('toggle');
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -36,7 +34,7 @@ const onToggle = () => {
|
||||
<div class="-mt-px text-sm">
|
||||
<button
|
||||
class="flex items-center select-none w-full rounded-none bg-slate-50 dark:bg-slate-800 border border-l-0 border-r-0 border-solid m-0 border-slate-100 dark:border-slate-700/50 cursor-grab justify-between py-2 px-4 drag-handle"
|
||||
@click.stop="onToggle"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<div class="flex justify-between mb-0.5">
|
||||
<EmojiOrIcon class="inline-block w-5" :icon="icon" :emoji="emoji" />
|
||||
|
||||
@@ -16,6 +16,7 @@ export default {
|
||||
<template>
|
||||
<button
|
||||
class="bg-white dark:bg-slate-900 cursor-pointer flex flex-col justify-end transition-all duration-200 ease-in -m-px py-4 px-0 items-center border border-solid border-slate-25 dark:border-slate-800 hover:border-woot-500 dark:hover:border-woot-500 hover:shadow-md hover:z-50 disabled:opacity-60"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<img :src="src" :alt="title" class="w-1/2 my-4 mx-auto" />
|
||||
<h3
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user