117 Коммитов

Автор SHA1 Сообщение Дата
Mattia Roccoberton
f291f7e7e6 Merge pull request #62 from blocknotes/chore/bump-to-2.0.0
Bump version to 2.0.0
2025-04-16 22:02:34 +02:00
Mattia Roccoberton
df944277b5 chore: Bump version to 2.0.0 2025-04-16 12:55:31 +02:00
Mattia Roccoberton
3cc5683d44 Merge pull request #63 from blocknotes/chore/internal-improvements
Update Quill imageUploader plugin
2025-04-16 12:52:49 +02:00
Mattia Roccoberton
498cbbd0c9 build: Update Quill imageUploader 2025-04-16 09:03:32 +02:00
Mattia Roccoberton
bfc2e997f1 chore: Minor changes to dummy app migrations 2025-04-16 09:03:24 +02:00
Mattia Roccoberton
9bda446762 chore: internal improvements to quill editor input js 2025-04-16 08:52:13 +02:00
Mattia Roccoberton
533471c0d6 Merge pull request #61 from blocknotes/build/update-quill-to-2.0.3
Update Quill to version 2.0.3
2025-04-15 08:12:47 +02:00
Mattia Roccoberton
3617aab7f2 test: Improve some tests 2025-04-14 13:44:35 +02:00
Mattia Roccoberton
5196a8811a build: Update quill.imageUploader plugin 2025-04-14 13:44:35 +02:00
Mattia Roccoberton
5e9a3b6c5a build: Update Quill to version 2.0.3 2025-04-13 18:14:10 +02:00
Mattia Roccoberton
181aab7fd6 Merge pull request #60 from blocknotes/test/improve-tests
Improve tests
2025-04-13 16:36:07 +02:00
Mattia Roccoberton
300b4a0a03 test: Improve tests 2025-04-13 16:35:14 +02:00
Mattia Roccoberton
451a225277 Merge pull request #59 from blocknotes/build/improve-dev-setup
Improve dev and test setup
2025-04-13 15:04:13 +02:00
Mattia Roccoberton
6cbb39daad test: Improve test suite setup 2025-04-13 15:00:22 +02:00
Mattia Roccoberton
d19fc7ddfc build: Improve dev setup 2025-04-13 14:54:13 +02:00
Mattia Roccoberton
74790fc115 Merge pull request #58 from blocknotes/test/update-build-setup
test: Update build setup
2025-04-05 09:32:24 +02:00
Mattia Roccoberton
c3b129a212 ci: Update GitHub workflows 2025-04-05 09:25:11 +02:00
Mattia Roccoberton
ab858f7c6f test: Remove Appraisal dependency 2025-04-05 09:25:11 +02:00
Mattia Roccoberton
6cf744b84c test: Update build setup 2025-04-05 09:25:11 +02:00
Mattia Roccoberton
0ae4ce2035 Merge pull request #57 from blocknotes/test/setup-coverage
Test: setup coverage
2025-03-22 09:14:34 +01:00
Mattia Roccoberton
dd9aa22cf5 test: Install Appraisal dependencies 2025-03-22 09:10:47 +01:00
Mattia Roccoberton
b6f3c0c3a6 test: Setup coverage 2025-03-22 09:08:36 +01:00
Mattia Roccoberton
7d3a084977 Merge pull request #56 from blocknotes/v1.3.0
chore: Bump to version 1.3.0
2025-03-18 22:25:05 +01:00
Mattia Roccoberton
1569b1ff18 chore: Bump to version 1.3.0 2025-03-18 22:15:04 +01:00
Mattia Roccoberton
3459800dbf Merge pull request #55 from blocknotes/api/quill-editor-query-functions
API: Quill editor query functions
2025-03-18 08:49:00 +01:00
Mattia Roccoberton
af901f4f7d feat: API: expose quill editor query functions 2025-03-18 08:47:16 +01:00
Mattia Roccoberton
328ee0d9fa Merge pull request #54 from blocknotes/test/update-test-suite-2
test: Update test suite (2)
2025-03-17 09:08:23 +01:00
Mattia Roccoberton
751196ef8c test: Update dependencies 2025-03-17 09:06:04 +01:00
Mattia Roccoberton
589b782866 Merge pull request #53 from blocknotes/test/update-test-suite
test: Update test suite
2025-03-17 08:46:08 +01:00
Mattia Roccoberton
554f6086e7 test: Add Rails 8.0 workflow 2025-03-17 08:44:22 +01:00
Mattia Roccoberton
051ada9843 test: Add Rails 7.2 workflow 2025-03-17 08:35:44 +01:00
Mattia Roccoberton
fd90851a6b test: Update ruby versions in specs workflows 2025-03-17 08:30:59 +01:00
Mattia Roccoberton
bd32c5f28f Merge pull request #52 from blocknotes/test/improve-specs
test: Improve specs using page objects
2025-03-16 18:30:55 +01:00
Mattia Roccoberton
21ec7a918b chore: Update Rubocop config 2025-03-16 18:29:42 +01:00
Mattia Roccoberton
b310f9e71b test: Improve specs using page objects 2025-03-16 18:29:42 +01:00
Mattia Roccoberton
bbee6f7681 Merge pull request #51 from blocknotes/dev/docker-setup-2
Update Docker dev setup
2025-03-16 11:07:57 +01:00
Mattia Roccoberton
57e151aa03 test: Fix ProcessTimeoutError when using Docker
- Error details:

Ferrum::ProcessTimeoutError:
  Browser did not produce websocket url within 10 seconds, try to increase `:process_timeout`

- Reference issue: https://github.com/rubycdp/cuprite/issues/180
2025-03-16 11:05:57 +01:00
Mattia Roccoberton
0913bb6915 chore: Update Docker dev setup 2025-03-16 11:05:57 +01:00
Mattia Roccoberton
728e3fa097 Merge pull request #50 from blocknotes/dev/docker-setup
chore: Docker dev setup
2025-03-14 15:57:12 +01:00
Mattia Roccoberton
9d891f0602 chore: Docker dev setup 2025-03-14 15:54:58 +01:00
Mattia Roccoberton
21fc69c2c0 Merge pull request #49 from blocknotes/mat/update-github-actions
Update Github actions
2025-03-09 15:36:22 +01:00
Mattia Roccoberton
b0e770ca9e ci: update Github actions 2025-03-09 15:28:46 +01:00
Mattia Roccoberton
ba53f9febf Merge pull request #47 from Shyrokonis/main
Add check to prevent GET request for non-existent image after upload
2025-03-09 09:17:33 +01:00
Dmytro Shyrokonis
d690b9906f [Fix] Add reject for nil result url 2024-11-12 16:22:31 +01:00
Mattia Roccoberton
99908f97f7 Merge pull request #45 from blocknotes/chore/bump-to-version-1.2.0
chore: bump to version 1.2.0
2024-02-29 17:08:52 +01:00
Mattia Roccoberton
9389f9e32d chore: bump to version 1.2.0 2024-02-29 17:07:32 +01:00
Mattia Roccoberton
4629ac4133 Merge pull request #44 from blocknotes/doc/update-readme
doc: update README
2024-02-29 17:04:06 +01:00
Mattia Roccoberton
0d4485c321 doc: update readme 2024-02-29 17:03:10 +01:00
Mattia Roccoberton
0c1cfefa83 Merge pull request #43 from blocknotes/test/update-test-suite-config
test: update test suite config
2024-02-26 09:16:55 +01:00
Mattia Roccoberton
0c674025a9 test: update test suite config 2024-02-26 09:15:38 +01:00
Mattia Roccoberton
770e069442 Merge pull request #42 from blocknotes/refactor/update-minimum-ruby-version
refactor: update minimum Ruby version to 3.0
2024-02-25 16:11:37 +01:00
Mattia Roccoberton
d346729cd1 build: update Appraisals dependencies 2024-02-25 16:08:53 +01:00
Mattia Roccoberton
ce65cfbb7e refactor!: update minimum Ruby version to 3.0 2024-02-25 16:08:50 +01:00
Mattia Roccoberton
22a1c8714c Merge pull request #41 from blocknotes/chore/rename-master-branch-to-main
chore: rename master branch to main
2024-02-25 11:50:30 +01:00
Mattia Roccoberton
489e1c35eb chore: rename master branch to main 2024-02-25 11:46:50 +01:00
Mattia Roccoberton
62efca80d6 chore: bump to version 1.1.0 2023-08-20 21:37:32 +02:00
Mattia Roccoberton
46168cc7f8 Merge pull request #38 from blocknotes/chore/update-ci-config
chore: update tests config
2023-08-20 21:27:50 +02:00
Mattia Roccoberton
02310804aa chore: add Overcommit config 2023-08-20 21:23:48 +02:00
Mattia Roccoberton
7e2f8733dc chore: update CI ruby versions 2023-08-20 21:23:46 +02:00
Mattia Roccoberton
2d0a8f961f chore: update tests config 2023-08-20 21:11:30 +02:00
Mattia Roccoberton
7262ad5523 Merge pull request #36 from alejandroperea/allow-active-admin-3
Allow ActiveAdmin 3
2023-08-20 20:08:39 +02:00
Alejandro Perea
71f227736d Update Appraisal gemfiles 2023-08-20 17:53:02 +02:00
Mattia Roccoberton
361da5096d Update activeadmin_quill_editor.gemspec
I'm going on with the proposed changes to speed up the PR
2023-08-20 16:13:24 +02:00
Alejandro Perea
17ef9aa5db Allow ActiveAdmin 3 2023-08-13 11:57:03 +02:00
Mattia Roccoberton
b00fcd90ab Add rspec-retry gem 2022-04-19 22:07:02 +02:00
Mattia Roccoberton
edf5cbcb13 Merge pull request #30 from blocknotes/minor-improvements
v1.0.0
2022-04-19 21:55:07 +02:00
Mattia Roccoberton
38e5406d5d Bump to version 1.0.0 2022-04-19 21:49:20 +02:00
Mattia Roccoberton
c560dd5c60 Changelog update 2022-04-19 21:49:20 +02:00
Mattia Roccoberton
597cae7adb Test the Quill JS presence and version 2022-04-19 21:49:20 +02:00
Mattia Roccoberton
6d0599c39e Minor improvements 2022-04-19 21:49:20 +02:00
Mattia Roccoberton
664765ac79 Remove unused gems 2022-04-19 21:49:20 +02:00
Mattia Roccoberton
7be390701f Enable Rails 7.0 specs support 2022-04-19 21:49:20 +02:00
Mattia Roccoberton
e4401a469b Enable Ruby 3.0 specs support 2022-04-19 21:49:20 +02:00
Mattia Roccoberton
d8632f069a Setup Cuprite 2022-04-19 21:49:20 +02:00
Mattia Roccoberton
7498cd65a1 Setup Appraisal 2022-03-29 19:42:43 +02:00
Mattia Roccoberton
038d66f387 Set minimum Ruby version to 2.6.0 and enable MFA 2022-03-26 18:04:33 +01:00
Mattia Roccoberton
7af25106a2 Remove sassc dependency 2022-03-26 18:00:12 +01:00
Mattia Roccoberton
d021f3650e README update 2022-03-26 17:54:26 +01:00
Mattia Roccoberton
8a13dd49a4 Create FUNDING.yml 2022-03-18 18:42:18 +01:00
Mattia Roccoberton
1cad5a10c3 Merge pull request #26 from blocknotes/update-linters-config
Update linters config
2021-07-10 15:41:05 +02:00
Mattia Roccoberton
63b4c118f6 Update linters config 2021-07-10 15:34:00 +02:00
Mattia Roccoberton
277d1a521a Merge pull request #24 from blocknotes/github_actions_improvements2
Add linter workflow
2021-06-02 15:53:24 +02:00
Mattia Roccoberton
4a0ea5e8f2 Add linter workflow 2021-06-02 15:52:19 +02:00
Mattia Roccoberton
ffc57e7d90 Badge for gem downloads 2021-05-23 22:48:09 +02:00
Mattia Roccoberton
2ebc9176b4 Merge pull request #23 from blocknotes/github_actions_improvements
GitHub Actions improvements
2021-05-23 21:23:19 +02:00
Mattia Roccoberton
0b44c683e6 GitHub Actions improvements 2021-05-23 14:51:28 +02:00
Mattia Roccoberton
9b460d36e3 Minor specs changes 2021-05-23 14:23:19 +02:00
Mattia Roccoberton
d41f8450ea Move some dependencies to the Gemfile 2021-05-23 14:22:28 +02:00
Mattia Roccoberton
401d2528e8 Merge pull request #22 from blocknotes/setup_github_actions_specs
Update GitHub Actions
2021-05-13 23:01:05 +02:00
Mattia Roccoberton
e086bac0a7 Update README 2021-05-13 22:59:41 +02:00
Mattia Roccoberton
36cd836ca8 Remove CircleCI config 2021-05-13 22:57:46 +02:00
Mattia Roccoberton
c05666673e Update GitHub Actions 2021-05-13 22:24:14 +02:00
Mattia Roccoberton
2d5429718c Bump to version 0.3.4 2021-03-16 08:32:57 +01:00
Mattia Roccoberton
78126d0cd7 Fix editor loading with Turbolinks 2021-03-14 16:05:21 +01:00
Mattia Roccoberton
3190134973 Small update to the README 2021-03-14 15:42:14 +01:00
Mattia Roccoberton
8d6b05297a Bump to version 0.3.2 2021-03-14 15:30:59 +01:00
Mattia Roccoberton
5d9b6f70fd Improve specs
- Add test examples for bold and italic content
- Move some test examples
- Adjust RSpec options
2021-03-14 15:26:24 +01:00
Mattia Roccoberton
6fa021c0c7 Update README 2021-03-14 14:35:51 +01:00
Mattia Roccoberton
357ef382d6 Update dependencies to test on Rails 6.1 2021-03-14 12:44:55 +01:00
Mattia Roccoberton
43ae70c7c9 Update NPM package files list 2020-10-06 22:37:25 +02:00
Mattia Roccoberton
6daf6f6bc8 Set word-break: break-all to prevent editor overflow 2020-10-06 22:30:16 +02:00
Mattia Roccoberton
98c235d346 Create brakeman-analysis.yml 2020-10-01 22:22:41 +02:00
Mattia Roccoberton
e6e6742753 Merge pull request #15 from blocknotes/feature/webpacker-support
Webpacker support
2020-10-01 07:56:57 +02:00
Mattia Roccoberton
c2c8d48795 Webpacker support 2020-10-01 07:56:03 +02:00
Mattia Roccoberton
72062c1a30 v0.2.14 2020-09-10 07:40:35 +02:00
Mattia Roccoberton
c4aa91225b Minor specs improvement 2020-09-09 12:58:14 +02:00
Mattia Roccoberton
3cc6edb302 JS strict directive 2020-09-09 07:48:45 +02:00
Mattia Roccoberton
d78fb2eb9c v0.2.12 2020-09-08 18:35:23 +02:00
Mattia Roccoberton
e5db64af96 JS refactoring 2020-09-08 18:26:24 +02:00
Mattia Roccoberton
c0d37ad94e v0.2.10 2020-09-05 16:08:46 +02:00
Mattia Roccoberton
96c70106a4 Add a new example: upload plugin using Active Storage 2020-09-05 16:07:19 +02:00
Mattia Roccoberton
4ec6e80760 Editor default options refactoring + include imageUploader plugin
In order to use imageUploader plugin:
- js/css files must included in the app;
- an upload method must be provided in Active Admin entity;
- the image_uploader server_url field option needs to be set.
2020-09-04 10:53:29 +02:00
Mattia Roccoberton
707f4a5cc7 README improvements 2020-09-04 09:14:18 +02:00
Mattia Roccoberton
dc97316303 v0.2.9 2020-09-04 08:49:20 +02:00
Mattia Roccoberton
e9b8abc88e Merge pull request #14 from blocknotes/fix/text-alignment
Reduce the importance of the reset rules
2020-09-03 13:02:47 +02:00
Mattia Roccoberton
582667dc59 Reduce the importance of the reset rules
This allows to let Quill features to override the reset styles.

Closes #8
2020-09-03 13:00:42 +02:00
m.pestov
76c06f5346 Don't send paragraph tag if editor blank 2020-09-03 12:45:42 +02:00
92 изменённых файлов: 2079 добавлений и 22821 удалений

Просмотреть файл

@@ -1,33 +0,0 @@
version: 2.1
orbs:
ruby: circleci/ruby@0.1.2
jobs:
build:
docker:
- image: circleci/ruby:2.6.6-stretch-node-browsers
executor: ruby/default
steps:
- checkout
- restore_cache:
key: gem-cache-{{ checksum "activeadmin_quill_editor.gemspec" }}
- run:
name: Setup Bundler
command: gem install bundler
- run:
name: Bundle Install
command: bundle install --path vendor/bundle
- save_cache:
key: gem-cache-{{ checksum "activeadmin_quill_editor.gemspec" }}
paths: vendor/bundle
- run:
name: Run Specs
command: |
bin/rspec \
--profile 10 \
--format RspecJunitFormatter \
--out test_results/rspec.xml \
--format progress \
$(circleci tests glob "spec/**/*_spec.rb")
- store_test_results:
path: test_results

7
.fasterer.yml Обычный файл
Просмотреть файл

@@ -0,0 +1,7 @@
---
exclude_paths:
- bin/*
- db/schema.rb
- gemfiles/**/*
- spec/dummy/**/*
- vendor/**/*

13
.github/FUNDING.yml поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,13 @@
# These are supported funding model platforms
github: [blocknotes]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

39
.github/workflows/linters.yml поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,39 @@
---
name: Linters
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
reviewdog:
name: Reviewdog
runs-on: ubuntu-latest
env:
RAILS_VERSION: 7.0
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.0
bundler-cache: true
- name: Set up Reviewdog
uses: reviewdog/action-setup@v1
with:
reviewdog_version: latest
- name: Run Reviewdog
env:
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
reviewdog -fail-on-error -reporter=github-pr-review -runners=fasterer,rubocop
# NOTE: check with: reviewdog -fail-on-error -reporter=github-pr-review -runners=fasterer -diff="git diff" -tee

43
.github/workflows/specs_rails61.yml поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,43 @@
---
name: Specs Rails 6.1 with ActiveAdmin 2.9
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
ruby: ['3.0']
env:
RAILS_VERSION: 6.0
ACTIVEADMIN_VERSION: 2.9.0
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- name: Database setup
run: bin/rails db:create db:migrate db:test:prepare
- name: Run tests
run: bundle exec rspec --profile
- name: On failure, archive screenshots as artifacts
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-failed-screenshots
path: spec/dummy/tmp/screenshots

42
.github/workflows/specs_rails70.yml поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,42 @@
---
name: Specs Rails 7.0
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
ruby: ['3.0', '3.2']
env:
RAILS_VERSION: 7.0
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- name: Database setup
run: bin/rails db:create db:migrate db:test:prepare
- name: Run tests
run: bundle exec rspec --profile
- name: On failure, archive screenshots as artifacts
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-failed-screenshots
path: spec/dummy/tmp/screenshots

42
.github/workflows/specs_rails71.yml поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,42 @@
---
name: Specs Rails 7.1
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
ruby: ['3.2', '3.4']
env:
RAILS_VERSION: 7.1
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- name: Database setup
run: bin/rails db:create db:migrate db:test:prepare
- name: Run tests
run: bundle exec rspec --profile
- name: On failure, archive screenshots as artifacts
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-failed-screenshots
path: spec/dummy/tmp/screenshots

42
.github/workflows/specs_rails72.yml поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,42 @@
---
name: Specs Rails 7.2
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
ruby: ['3.2', '3.4']
env:
RAILS_VERSION: 7.2
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- name: Database setup
run: bin/rails db:create db:migrate db:test:prepare
- name: Run tests
run: bundle exec rspec --profile
- name: On failure, archive screenshots as artifacts
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-failed-screenshots
path: spec/dummy/tmp/screenshots

42
.github/workflows/specs_rails80.yml поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,42 @@
---
name: Specs Rails 8.0
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
ruby: ['3.2', '3.4']
env:
RAILS_VERSION: 8.0
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- name: Database setup
run: bin/rails db:create db:migrate db:test:prepare
- name: Run tests
run: bundle exec rspec --profile
- name: On failure, archive screenshots as artifacts
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-failed-screenshots
path: spec/dummy/tmp/screenshots

2
.gitignore поставляемый
Просмотреть файл

@@ -6,7 +6,9 @@
/Gemfile.lock
/_misc/
/coverage/
/spec/dummy/db/*.sqlite3*
/spec/dummy/db/schema-dev.rb
/spec/dummy/log/
/spec/dummy/storage/
/spec/dummy/tmp/

13
.overcommit.yml Обычный файл
Просмотреть файл

@@ -0,0 +1,13 @@
CommitMsg:
CapitalizedSubject:
enabled: false
MessageFormat:
enabled: true
pattern: "^(fixup! ){0,1}(fix|feat|build|chore|ci|docs|style|refactor|perf|test)[!]{0,1}: .+$"
expected_pattern_message: "<Commit type>: <Commit Message>"
sample_message: "chore: add Overcommit config"
TextWidth:
enabled: true
max_subject_width: 72

8
.reviewdog.yml Обычный файл
Просмотреть файл

@@ -0,0 +1,8 @@
---
runner:
fasterer:
cmd: bin/fasterer
level: info
rubocop:
cmd: bin/rubocop
level: info

1
.rspec
Просмотреть файл

@@ -1,3 +1,2 @@
--require rails_helper
--format documentation
--profile

Просмотреть файл

@@ -1,27 +1,33 @@
---
inherit_from:
- https://relaxed.ruby.style/rubocop.yml
plugins:
- rubocop-capybara
- rubocop-packaging
- rubocop-performance
- rubocop-rails
- rubocop-rspec
- rubocop-rspec_rails
AllCops:
Exclude:
- bin/*
- db/schema.rb
- gemfiles/**/*
- spec/dummy/**/*
- vendor/**/*
NewCops: enable
TargetRubyVersion: 3.0
Gemspec/RequiredRubyVersion:
Enabled: false
RSpec/ExampleLength:
# default 5
Max: 12
Naming/FileName:
Enabled: false
RSpec/MultipleExpectations:
# default 1
Max: 4
Layout/LineLength:
Enabled: true
Max: 120
Style/HashEachMethods:
Enabled: true
Style/HashTransformKeys:
Enabled: true
Style/HashTransformValues:
Enabled: true
RSpec/MultipleMemoizedHelpers:
# default 5
Max: 10

72
CHANGELOG.md Обычный файл
Просмотреть файл

@@ -0,0 +1,72 @@
# Quill Editor for ActiveAdmin
An Active Admin plugin to use Quill Rich Text Editor.
## v2.0.0 - 2025-04-15
- Update Quill from 1.3.7 to 2.0.3
## v1.3.0 - 2025-03-18
- Javascript API: expose Quill editor query functions
- ImageUploader: reject the Promise if an url is not provided in the response
## v1.2.0 - 2024-02-29
- Update minimum supported Ruby version to 3.0
## v1.1.0 - 2023-08-20
- Support ActiveAdmin 3.x
- Minor tests config improvements
## v1.0.0 - 2022-04-18
- Set minimum Ruby version to 2.6.0
- Remove `sassc` dependency
- Enable Ruby 3.0 specs support
- Enable Rails 7.0 specs support
- Internal improvements
## v0.3.4 - 2021-03-16
- Fix editor loading with Turbolinks
## v0.3.2 - 2021-03-14
- Specs improvements
- Minor internal changes
## v0.3.0 - 2020-10-01
- Add Webpacker support
## v0.2.14 - 2020-09-10
- JS: enable strict directive
- Minor specs improvement
## v0.2.12 - 2020-09-08
- JS refactoring
## v0.2.10 - 2020-09-05
- Include Image Uploader plugin
## v0.2.9 - 2020-09-04
- Fix not working alignments (issue #8)
- Fix empty editor problem which produces `<p><br></p>` (pull request #9)
## v0.2.8 - 2020-09-03
- Minor style improvements
- Add specs for editor in nested resources
- Add Rubocop and remove SimpleCov gems
## v0.2.4 - 2020-09-01
- Update Quill editor to version 1.3.7
- Add minimum specs (using RSpec)
- Minor internal changes

61
Gemfile
Просмотреть файл

@@ -2,4 +2,63 @@
source 'https://rubygems.org'
gemspec
def eval_version(dependency, version)
return [dependency] if version.empty?
version.count('.') < 2 ? [dependency, "~> #{version}.0"] : [dependency, version]
end
if ENV['DEVEL'] == '1'
gem 'activeadmin_quill_editor', path: './'
else
gemspec
end
ruby_ver = ENV.fetch('RUBY_VERSION', '')
rails_ver = ENV.fetch('RAILS_VERSION', '')
rails = eval_version('rails', rails_ver)
gem(*rails)
active_admin_ver = ENV.fetch('ACTIVEADMIN_VERSION', '')
active_admin = eval_version('activeadmin', active_admin_ver)
gem(*active_admin)
ruby32 = ruby_ver.empty? || Gem::Version.new(ruby_ver) >= Gem::Version.new('3.2')
rails72 = rails_ver.empty? || Gem::Version.new(rails_ver) >= Gem::Version.new('7.2')
sqlite3 = ruby32 && rails72 ? ['sqlite3'] : ['sqlite3', '~> 1.4']
gem(*sqlite3)
gem 'zeitwerk', '~> 2.6.18' unless ruby32
# NOTE: to avoid error: uninitialized constant ActiveSupport::LoggerThreadSafeLevel::Logger
gem 'concurrent-ruby', '1.3.4'
# Misc
gem 'bigdecimal'
gem 'csv'
gem 'mutex_m'
gem 'puma'
gem 'sassc'
gem 'sprockets-rails'
# Testing
gem 'capybara'
gem 'cuprite'
gem 'rspec_junit_formatter'
gem 'rspec-rails'
gem 'simplecov', require: false
gem 'super_diff'
# Linters
gem 'fasterer'
gem 'rubocop'
gem 'rubocop-capybara'
gem 'rubocop-packaging'
gem 'rubocop-performance'
gem 'rubocop-rails'
gem 'rubocop-rspec'
gem 'rubocop-rspec_rails'
# Tools
gem 'pry-rails'

Просмотреть файл

@@ -1,4 +1,4 @@
Copyright (c) 2017-2020 Mattia Roccoberton
Copyright (c) 2017-2022 Mattia Roccoberton
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the

47
Makefile Обычный файл
Просмотреть файл

@@ -0,0 +1,47 @@
include extra/.env
help:
@echo -e "${COMPOSE_PROJECT_NAME} - Main project commands:\n\
make up # starts the dev services (optional env vars: RUBY / RAILS / ACTIVEADMIN)\n\
make specs # run the tests (after up)\n\
make lint # run the linters (after up)\n\
make server # run the server (after up)\n\
make shell # open a shell (after up)\n\
make down # cleanup (after up)\n\
Example: RUBY=3.2 RAILS=7.1 ACTIVEADMIN=3.2.0 make up"
# System commands
build:
@rm -f Gemfile.lock spec/dummy/db/*.sqlite3
@docker compose -f extra/docker-compose.yml build
db_reset:
@docker compose -f extra/docker-compose.yml run --rm app bin/rails db:create db:migrate db:test:prepare
up: build db_reset
@docker compose -f extra/docker-compose.yml up
shell:
@docker compose -f extra/docker-compose.yml exec app bash
down:
@docker compose -f extra/docker-compose.yml down --volumes --rmi local --remove-orphans
# App commands
seed:
@docker compose -f extra/docker-compose.yml exec app bin/rails db:seed
console: seed
@docker compose -f extra/docker-compose.yml exec app bin/rails console
lint:
@docker compose -f extra/docker-compose.yml exec app bin/rubocop
server: seed
@rm -f spec/dummy/tmp/pids/server.pid
@docker compose -f extra/docker-compose.yml exec app bin/rails server -b 0.0.0.0 -p ${SERVER_PORT}
specs:
@docker compose -f extra/docker-compose.yml exec app bin/rspec --fail-fast

Просмотреть файл

@@ -1,11 +1,29 @@
# ActiveAdmin Quill Editor [![Gem Version](https://badge.fury.io/rb/activeadmin_quill_editor.svg)](https://badge.fury.io/rb/activeadmin_quill_editor) [![CircleCI](https://circleci.com/gh/blocknotes/activeadmin_quill_editor.svg?style=svg)](https://circleci.com/gh/blocknotes/activeadmin_quill_editor)
# ActiveAdmin Quill Editor
[![gem version](https://badge.fury.io/rb/activeadmin_quill_editor.svg)](https://badge.fury.io/rb/activeadmin_quill_editor)
[![gem downloads](https://badgen.net/rubygems/dt/activeadmin_quill_editor)](https://rubygems.org/gems/activeadmin_quill_editor)
[![linters](https://github.com/blocknotes/activeadmin_quill_editor/actions/workflows/linters.yml/badge.svg)](https://github.com/blocknotes/activeadmin_quill_editor/actions/workflows/linters.yml)
An Active Admin plugin to use [Quill Rich Text Editor](https://github.com/quilljs/quill)
[![specs Rails 6.1](https://github.com/blocknotes/activeadmin_quill_editor/actions/workflows/specs_rails61.yml/badge.svg)](https://github.com/blocknotes/activeadmin_quill_editor/actions/workflows/specs_rails61.yml)
[![specs Rails 7.0](https://github.com/blocknotes/activeadmin_quill_editor/actions/workflows/specs_rails70.yml/badge.svg)](https://github.com/blocknotes/activeadmin_quill_editor/actions/workflows/specs_rails70.yml)
[![Specs Rails 7.1](https://github.com/blocknotes/activeadmin_quill_editor/actions/workflows/specs_rails71.yml/badge.svg)](https://github.com/blocknotes/activeadmin_quill_editor/actions/workflows/specs_rails71.yml)
[![Specs Rails 7.2](https://github.com/blocknotes/activeadmin_quill_editor/actions/workflows/specs_rails72.yml/badge.svg)](https://github.com/blocknotes/activeadmin_quill_editor/actions/workflows/specs_rails72.yml)
[![Specs Rails 8.0](https://github.com/blocknotes/activeadmin_quill_editor/actions/workflows/specs_rails80.yml/badge.svg)](https://github.com/blocknotes/activeadmin_quill_editor/actions/workflows/specs_rails80.yml)
![screenshot](screenshot.png)
An Active Admin plugin to use [Quill Rich Text Editor](https://github.com/quilljs/quill) in form fields.
![screenshot](extra/screenshot.png)
Please :star: if you like it.
## Install
- After installing Active Admin, add to your Gemfile: `gem 'activeadmin_quill_editor'` (and execute *bundle*)
_NOTE_: the gem 2.x provide Quill 2.x, while Quill 1.x is provided with the gem version 1.x
After installing Active Admin, add to your Gemfile: `gem 'activeadmin_quill_editor'` (and execute *bundle*)
If you installed Active Admin without Webpacker support (default for now):
- Add a SASS/SCSS gem to your Gemfile (ex. `gem 'sassc'`)
- Add at the end of your Active Admin styles (_app/assets/stylesheets/active_admin.scss_):
```scss
@import 'activeadmin/quill_editor/quill.snow';
@@ -16,15 +34,26 @@ An Active Admin plugin to use [Quill Rich Text Editor](https://github.com/quillj
//= require activeadmin/quill_editor/quill
//= require activeadmin/quill_editor_input
```
- Use the input with `as: :quill_editor` in Active Admin model conf
Why 2 separated scripts? In this way you can include a different version of *quill editor* if you like.
If you installed Active Admin with Webpacker support:
> **UPDATE FROM VERSION <= 2.0**: please add to your _app/assets/stylesheets/active_admin.scss_ the line `@import 'activeadmin/quill_editor/quill.snow';`
- Execute in your project root:
```sh
yarn add blocknotes/activeadmin_quill_editor
```
- Add to your *app/javascript/packs/active_admin.js*:
```js
require('activeadmin_quill_editor')
```
## Usage
In your Active Admin models, form configuration, set the text inputs with `as: :quill_editor` where needed.
## Options
**data-options**: permits to set *quill editor* options directly - see [options list](https://quilljs.com/docs/configuration/)
If you are using Action Text (`has_rich_text`) on the same field of the Quill editor please take a look at [this workaround](https://github.com/blocknotes/activeadmin_quill_editor/issues/33#issuecomment-1965996947) to avoid issues.
## Examples
### Basic usage
@@ -44,20 +73,56 @@ Why 2 separated scripts? In this way you can include a different version of *qui
### Toolbar buttons configuration
```ruby
f.input :description, as: :quill_editor, input_html: {data: {options: {modules: {toolbar: [['bold', 'italic', 'underline'], ['link']]}, placeholder: 'Type something...', theme: 'snow'}}}
f.input :description, as: :quill_editor, input_html: { data: { options: { modules: { toolbar: [['bold', 'italic', 'underline'], ['link']] }, placeholder: 'Type something...', theme: 'snow' } } }
```
## Notes
- Upload functions (Images, Documents, Files, etc.) are not implemented yet
### ImageUploader plugin
This plugin allows to upload images to the server (instead of storing them in *base64* by default), reference [here](https://github.com/NoelOConnell/quill-image-uploader).
```ruby
# Upload method (to be included in the admin entity configuration)
member_action :upload, method: [:post] do
result = { success: resource.images.attach(params[:file_upload]) }
result[:url] = url_for(resource.images.last) if result[:success]
render json: result
end
```
```ruby
# Form field
unless object.new_record?
plugin_opts = { image_uploader: { server_url: upload_admin_post_path(object.id), field_name: 'file_upload' } }
f.input :description, as: :quill_editor, input_html: { data: { plugins: plugin_opts } }
end
```
For the relevant files of the upload example see [here](examples/upload_plugin_using_activestorage/).
Consider that this is just a basic example: images are uploaded as soon as they are attached to the
editor (regardless of the form submit), it shows the editor only for an existing record (because of
the *upload_admin_post_path*) and it doesn't provide a way to remove images (just deleting them from
the editor will not destroy them, you'll need to implement a purge logic for that).
## Javascript API
Some methods are provided for advanced use cases:
- `window.getQuillEditors()`: returns all the available Quill editors instances;
- `window.getQuillEditorByIndex(n)`: returns the N-th Quill editor instance;
- `window.getQuillEditorByElementId(id)`: returns the Quill editor instance related to the specified element id (e.g. _article_description_).
## Development
Project created by [Mattia Roccoberton](http://blocknot.es), thanks also to the good guys that opened issues and pull requests from time to time.
For development information please check [this document](extra/development.md).
## Do you like it? Star it!
If you use this component just star it. A developer is more motivated to improve a project when there is some interest.
Take a look at [other Active Admin components](https://github.com/blocknotes?utf8=✓&tab=repositories&q=activeadmin&type=source) that I made if you are curious.
If you use this component just star it. A developer is more motivated to improve a project when there is some interest. My other [Active Admin components](https://github.com/blocknotes?utf8=✓&tab=repositories&q=activeadmin&type=source).
## Contributors
- [Mattia Roccoberton](http://blocknot.es): author
- The good guys that opened issues and pull requests from time to time
Or consider offering me a coffee, it's a small thing but it is greatly appreciated: [about me](https://www.blocknot.es/about-me).
## License
- The gem is available as open-source under the terms of the [MIT](LICENSE.txt)
The gem is available as open-source under the terms of the [MIT](LICENSE.txt).

Просмотреть файл

@@ -1,11 +1,25 @@
# frozen_string_literal: true
begin
require 'bundler/setup'
rescue LoadError
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
end
APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
load 'rails/tasks/engine.rake'
load 'rails/tasks/statistics.rake'
require 'bundler/gem_tasks'
begin
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec)
RSpec::Core::RakeTask.new(:spec) do |t|
# t.ruby_opts = %w[-w]
t.rspec_opts = ['--color', '--format documentation']
end
task default: :spec
rescue LoadError

Просмотреть файл

@@ -14,18 +14,16 @@ Gem::Specification.new do |spec|
spec.email = 'mat@blocknot.es'
spec.homepage = 'https://github.com/blocknotes/activeadmin_quill_editor'
spec.required_ruby_version = '>= 3.0'
spec.metadata['homepage_uri'] = spec.homepage
spec.metadata['changelog_uri'] = 'https://github.com/blocknotes/activeadmin_quill_editor/blob/main/CHANGELOG.md'
spec.metadata['source_code_uri'] = spec.homepage
spec.metadata['rubygems_mfa_required'] = 'true'
spec.files = Dir['{app,lib}/**/*', 'LICENSE.txt', 'Rakefile', 'README.md']
spec.require_paths = ['lib']
spec.add_runtime_dependency 'activeadmin', '~> 2.0'
spec.add_development_dependency 'activestorage', '~> 6.0.3.2'
spec.add_development_dependency 'capybara', '~> 3.33.0'
spec.add_development_dependency 'pry', '~> 0.13.1'
spec.add_development_dependency 'puma', '~> 4.3.5'
spec.add_development_dependency 'rspec_junit_formatter', '~> 0.4.1'
spec.add_development_dependency 'rspec-rails', '~> 4.0.1'
spec.add_development_dependency 'rubocop', '~> 0.90.0'
spec.add_development_dependency 'selenium-webdriver', '~> 3.142.7'
spec.add_development_dependency 'sqlite3', '~> 1.4.2'
spec.add_runtime_dependency 'activeadmin', '>= 2.9', '< 4' # rubocop:disable Gemspec/AddRuntimeDependency
end

Просмотреть файл

@@ -0,0 +1,362 @@
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _blots_image_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
/* harmony import */ var _quill_imageUploader_css__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3);
/* harmony import */ var _quill_imageUploader_css__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_quill_imageUploader_css__WEBPACK_IMPORTED_MODULE_1__);
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var ImageUploader = function () {
function ImageUploader(quill, options) {
_classCallCheck(this, ImageUploader);
this.quill = quill;
this.options = options;
this.range = null;
if (typeof this.options.upload !== "function") {
console.warn("[Missing config] upload function that returns a promise is required");
}
if (this.options.loadingClass) {
_blots_image_js__WEBPACK_IMPORTED_MODULE_0__["default"].className = this.options.loadingClass;
}
var toolbar = this.quill.getModule("toolbar");
toolbar.addHandler("image", this.selectLocalImage.bind(this));
this.handleDrop = this.handleDrop.bind(this);
this.handlePaste = this.handlePaste.bind(this);
this.quill.root.addEventListener("drop", this.handleDrop, false);
this.quill.root.addEventListener("paste", this.handlePaste, false);
}
_createClass(ImageUploader, [{
key: "selectLocalImage",
value: function selectLocalImage() {
var _this = this;
this.range = this.quill.getSelection();
this.fileHolder = document.createElement("input");
this.fileHolder.setAttribute("type", "file");
this.fileHolder.setAttribute("accept", "image/*");
this.fileHolder.setAttribute("style", "visibility:hidden");
this.fileHolder.onchange = this.fileChanged.bind(this);
document.body.appendChild(this.fileHolder);
this.fileHolder.click();
window.requestAnimationFrame(function () {
document.body.removeChild(_this.fileHolder);
});
}
}, {
key: "handleDrop",
value: function handleDrop(evt) {
var _this2 = this;
evt.stopPropagation();
evt.preventDefault();
if (evt.dataTransfer && evt.dataTransfer.files && evt.dataTransfer.files.length) {
if (document.caretRangeFromPoint) {
var selection = document.getSelection();
var range = document.caretRangeFromPoint(evt.clientX, evt.clientY);
if (selection && range) {
selection.setBaseAndExtent(range.startContainer, range.startOffset, range.startContainer, range.startOffset);
}
} else {
var _selection = document.getSelection();
var _range = document.caretPositionFromPoint(evt.clientX, evt.clientY);
if (_selection && _range) {
_selection.setBaseAndExtent(_range.offsetNode, _range.offset, _range.offsetNode, _range.offset);
}
}
this.range = this.quill.getSelection();
var file = evt.dataTransfer.files[0];
setTimeout(function () {
_this2.range = _this2.quill.getSelection();
_this2.readAndUploadFile(file);
}, 0);
}
}
}, {
key: "handlePaste",
value: function handlePaste(evt) {
var _this3 = this;
var clipboard = evt.clipboardData || window.clipboardData;
// IE 11 is .files other browsers are .items
if (clipboard && (clipboard.items || clipboard.files)) {
var items = clipboard.items || clipboard.files;
var IMAGE_MIME_REGEX = /^image\/(jpe?g|gif|png|svg|webp)$/i;
for (var i = 0; i < items.length; i++) {
if (IMAGE_MIME_REGEX.test(items[i].type)) {
(function () {
var file = items[i].getAsFile ? items[i].getAsFile() : items[i];
if (file) {
_this3.range = _this3.quill.getSelection();
evt.preventDefault();
setTimeout(function () {
_this3.range = _this3.quill.getSelection();
_this3.readAndUploadFile(file);
}, 0);
}
})();
}
}
}
}
}, {
key: "readAndUploadFile",
value: function readAndUploadFile(file) {
var _this4 = this;
var isUploadReject = false;
var fileReader = new FileReader();
fileReader.addEventListener("load", function () {
if (!isUploadReject) {
var base64ImageSrc = fileReader.result;
_this4.insertBase64Image(base64ImageSrc);
}
}, false);
if (file) {
fileReader.readAsDataURL(file);
}
this.options.upload(file).then(function (imageUrl) {
_this4.insertToEditor(imageUrl);
}, function (error) {
isUploadReject = true;
_this4.removeBase64Image();
console.warn(error);
});
}
}, {
key: "fileChanged",
value: function fileChanged() {
var file = this.fileHolder.files[0];
this.readAndUploadFile(file);
}
}, {
key: "insertBase64Image",
value: function insertBase64Image(url) {
var range = this.range;
this.quill.insertEmbed(range.index, _blots_image_js__WEBPACK_IMPORTED_MODULE_0__["default"].blotName, "" + url, "user");
}
}, {
key: "insertToEditor",
value: function insertToEditor(url) {
var range = this.range;
// Delete the placeholder image
this.quill.deleteText(range.index, 3, "user");
// Insert the server saved image
this.quill.insertEmbed(range.index, "image", "" + url, "user");
range.index++;
this.quill.setSelection(range, "user");
}
}, {
key: "removeBase64Image",
value: function removeBase64Image() {
var range = this.range;
this.quill.deleteText(range.index, 3, "user");
}
}]);
return ImageUploader;
}();
window.ImageUploader = ImageUploader;
/* harmony default export */ __webpack_exports__["default"] = (ImageUploader);
/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var quill__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
/* harmony import */ var quill__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(quill__WEBPACK_IMPORTED_MODULE_0__);
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } };
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var InlineBlot = quill__WEBPACK_IMPORTED_MODULE_0___default.a.import('blots/block');
var LoadingImage = function (_InlineBlot) {
_inherits(LoadingImage, _InlineBlot);
function LoadingImage() {
_classCallCheck(this, LoadingImage);
return _possibleConstructorReturn(this, (LoadingImage.__proto__ || Object.getPrototypeOf(LoadingImage)).apply(this, arguments));
}
_createClass(LoadingImage, [{
key: 'deleteAt',
value: function deleteAt(index, length) {
_get(LoadingImage.prototype.__proto__ || Object.getPrototypeOf(LoadingImage.prototype), 'deleteAt', this).call(this, index, length);
this.cache = {};
}
}], [{
key: 'create',
value: function create(src) {
var node = _get(LoadingImage.__proto__ || Object.getPrototypeOf(LoadingImage), 'create', this).call(this, src);
if (src === true) return node;
var image = document.createElement('img');
image.setAttribute('src', src);
node.appendChild(image);
return node;
}
}, {
key: 'value',
value: function value(domNode) {
var _domNode$dataset = domNode.dataset,
src = _domNode$dataset.src,
custom = _domNode$dataset.custom;
return { src: src, custom: custom };
}
}]);
return LoadingImage;
}(InlineBlot);
LoadingImage.blotName = 'imageBlot';
LoadingImage.className = 'quill-image-uploading';
LoadingImage.tagName = 'span';
quill__WEBPACK_IMPORTED_MODULE_0___default.a.register({ 'formats/imageBlot': LoadingImage });
/* harmony default export */ __webpack_exports__["default"] = (LoadingImage);
/***/ }),
/* 2 */
/***/ (function(module, exports) {
module.exports = Quill;
/***/ }),
/* 3 */
/***/ (function(module, exports) {
// removed by extract-text-webpack-plugin
/***/ })
/******/ ]);

1
app/assets/javascripts/activeadmin/quill.imageUploader.min.js поставляемый Обычный файл

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@@ -1,45 +1,123 @@
function initQuillEditors() {
var editors = document.querySelectorAll('.quill-editor');
var default_options = {
modules: {
toolbar: [
['bold', 'italic', 'underline'],
['link', 'blockquote', 'code-block'],
[{ 'script': 'sub'}, { 'script': 'super' }],
[{ 'align': [] }, { list: 'ordered' }, { list: 'bullet' }],
[{ 'color': [] }, { 'background': [] }],
['clean'],
]
},
placeholder: '',
theme: 'snow'
/* globals $ Quill ImageUploader */
(function () {
'use strict';
// --- functions ---------------------------------------------------------------
const initQuillEditors = () => {
const defaultTheme = 'snow';
const defaultToolbar = [
['bold', 'italic', 'underline'],
['link', 'blockquote', 'code-block'],
[{ 'script': 'sub' }, { 'script': 'super' }],
[{ 'align': [] }, { list: 'ordered' }, { list: 'bullet' }],
[{ 'color': [] }, { 'background': [] }],
['image'],
['clean'],
];
const editors = document.querySelectorAll('[data-aa-quill-editor]');
const registeredPlugins = {};
for (let i = 0; i < editors.length; i++) {
const content = editors[i].querySelector('[data-aa-quill-content]');
const isActive = editors[i].classList.contains('quill-editor--active');
if (content && !isActive) {
// Setup editor options
const options = editors[i].getAttribute('data-options') ? JSON.parse(editors[i].getAttribute('data-options')) : {};
if (!options.theme) options.theme = defaultTheme;
if (!options.modules) options.modules = {};
if (!options.modules.toolbar) options.modules.toolbar = defaultToolbar;
// Setup plugin options
const pluginOptions = editors[i].getAttribute('data-plugins') ? JSON.parse(editors[i].getAttribute('data-plugins')) : {};
if (pluginOptions.image_uploader && pluginOptions.image_uploader.server_url) {
if (!registeredPlugins.image_uploader) {
Quill.register('modules/imageUploader', ImageUploader);
registeredPlugins.image_uploader = true;
}
const opts = pluginOptions.image_uploader;
options.modules.imageUploader = setupImageUploader(opts.server_url, opts.field_name);
}
// Init editor
editors[i]['_quill-editor'] = new Quill(content, options);
editors[i].classList += ' quill-editor--active';
}
}
const formtastic = document.querySelector('form.formtastic');
if (formtastic) {
formtastic.onsubmit = () => {
for (let i = 0; i < editors.length; i++) {
const input = editors[i].querySelector('input[type="hidden"]');
if (editors[i]['_quill-editor'].editor.isBlank()) {
input.value = '';
} else {
input.value = editors[i]['_quill-editor'].root.innerHTML;
}
}
};
}
};
for(var i = 0; i < editors.length; i++) {
var content = editors[i].querySelector('.quill-editor-content');
var isActive = editors[i].classList.contains('quill-editor--active');
if(content && !isActive) {
var options = editors[i].getAttribute('data-options') ? JSON.parse(editors[i].getAttribute('data-options')) : default_options;
editors[i]['_quill-editor'] = new Quill(content, options);
editors[i].classList += ' quill-editor--active';
const setupImageUploader = (server_url, field_name) => {
return {
upload: (file) => {
return new Promise((resolve, reject) => {
const formData = new FormData();
formData.append(field_name || 'file_upload', file);
fetch(server_url, {
body: formData,
headers: {
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
},
method: 'POST'
}).then(response => response.json())
.then(result => {
if (!result.url) {
reject('Upload failed');
}
resolve(result.url);
})
.catch(error => {
reject('Upload failed');
console.error('Error: ', error);
})
})
}
}
}
var formtastic = document.querySelector('form.formtastic');
if(formtastic) {
formtastic.onsubmit = function() {
for(var i = 0; i < editors.length; i++) {
var input = editors[i].querySelector('input[type="hidden"]');
input.value = editors[i]['_quill-editor'].root.innerHTML;
}
};
// --- public functions --------------------------------------------------------
window.getQuillEditors = function() {
const editors = document.querySelectorAll('[data-aa-quill-editor]');
const list = [];
editors.forEach(function(editor) { list.push(editor['_quill-editor']) });
return list;
}
}
$(document).ready( function() {
initQuillEditors();
});
window.getQuillEditorByIndex = function(index) {
const editors = document.querySelectorAll('[data-aa-quill-editor]');
$(document).on('has_many_add:after', function() {
initQuillEditors();
});
return (index >= 0 && index < editors.length) ? editors[index]['_quill-editor'] : null;
}
window.getQuillEditorByElementId = function(id) {
const editor = document.querySelector(`[data-aa-quill-editor]#${id}`);
return editor ? editor['_quill-editor'] : null;
}
// --- events ------------------------------------------------------------------
$(document).ready(initQuillEditors);
$(document).on('has_many_add:after', '.has_many_container', initQuillEditors);
$(document).on('turbolinks:load', initQuillEditors);
})();

Просмотреть файл

@@ -1,4 +1,11 @@
body.active_admin .quill-editor {
// reset internal elements
.ql-editor * {
margin: initial;
padding: initial;
text-align: initial;
}
body.active_admin [data-aa-quill-editor] {
display: inline-block;
width: calc(80% - 2px);
@@ -22,13 +29,7 @@ body.active_admin .quill-editor {
max-height: 300px;
min-height: 150px;
padding: 10px;
// reset internal elements
* {
margin: initial;
padding: initial;
text-align: initial;
}
word-break: break-all;
ol {
list-style-type: decimal;

33
app/assets/stylesheets/activeadmin/quill.imageUploader.min.css поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,33 @@
.quill-image-uploading {
position: relative;
display: inline-block;
}
.quill-image-uploading img {
max-width: 98% !important;
filter: blur(5px);
opacity: 0.3;
}
.quill-image-uploading::before {
content: "";
box-sizing: border-box;
position: absolute;
top: 50%;
left: 50%;
width: 30px;
height: 30px;
margin-top: -15px;
margin-left: -15px;
border-radius: 50%;
border: 3px solid #ccc;
border-top-color: #1e40af;
z-index: 1;
animation: spinner 0.6s linear infinite;
}
@keyframes spinner {
to {
transform: rotate(360deg);
}
}

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

29
bin/fasterer Исполняемый файл
Просмотреть файл

@@ -0,0 +1,29 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# This file was generated by Bundler.
#
# The application 'fasterer' is installed as part of a gem, and
# this file is here to facilitate running it.
#
require "pathname"
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
Pathname.new(__FILE__).realpath)
bundle_binstub = File.expand_path("../bundle", __FILE__)
if File.file?(bundle_binstub)
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
load(bundle_binstub)
else
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
end
end
require "rubygems"
require "bundler/setup"
load Gem.bin_path("fasterer", "fasterer")

Просмотреть файл

@@ -1,29 +1,16 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# This command will automatically be run when you run "rails" with Rails gems
# installed from the root of your application.
#
# This file was generated by Bundler.
#
# The application 'rails' is installed as part of a gem, and
# this file is here to facilitate running it.
#
ENV['RAILS_ENV'] ||= 'development'
require "pathname"
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
Pathname.new(__FILE__).realpath)
ENGINE_ROOT = File.expand_path('..', __dir__)
ENGINE_PATH = File.expand_path('../lib/activeadmin/quill_editor/engine', __dir__)
APP_PATH = File.expand_path('../spec/dummy/config/application', __dir__)
bundle_binstub = File.expand_path("../bundle", __FILE__)
# Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
if File.file?(bundle_binstub)
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
load(bundle_binstub)
else
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
end
end
require "rubygems"
require "bundler/setup"
load Gem.bin_path("railties", "rails")
require 'rails/all'
require 'rails/engine/commands'

Просмотреть файл

@@ -0,0 +1,73 @@
# frozen_string_literal: true
ActiveAdmin.register Post do
permit_params :author_id,
:title,
:description,
:category,
:dt,
:position,
:published,
tag_ids: []
member_action :upload, method: [:post] do
result = { success: resource.images.attach(params[:file_upload]) }
result[:url] = url_for(resource.images.last) if result[:success]
render json: result
end
index do
selectable_column
id_column
column :title
column :author
column :published
column :created_at
actions
end
show do
attributes_table do
row :author
row :title
row :description
row :category
row :dt
row :position
row :published
row :tags
row :created_at
row :updated_at
row :images do |resurce|
resurce.images.each do |image|
div do
link_to image.filename, image, target: '_blank', rel: 'noopener'
end
end
nil
end
end
active_admin_comments
end
form do |f|
f.inputs 'Post' do
f.input :author
f.input :title
unless object.new_record?
plugin_opts = { image_uploader: { server_url: upload_admin_post_path(object.id), field_name: 'file_upload' } }
f.input :description, as: :quill_editor, input_html: { data: { plugins: plugin_opts } }
end
f.input :category
f.input :dt
f.input :position
f.input :published
end
f.inputs 'Tags' do
f.input :tags
end
f.actions
end
end

Просмотреть файл

@@ -0,0 +1,6 @@
//= require active_admin/base
//= require activeadmin/quill_editor/quill
//= require activeadmin/quill_editor_input
//= require activeadmin/quill.imageUploader.min

Просмотреть файл

@@ -0,0 +1,7 @@
@import 'active_admin/mixins';
@import 'active_admin/base';
@import 'activeadmin/quill_editor/quill.snow';
@import 'activeadmin/quill_editor_input';
@import 'activeadmin/quill.imageUploader.min';

Просмотреть файл

@@ -0,0 +1,7 @@
# frozen_string_literal: true
class Post < ApplicationRecord
has_many_attached :images
validates :title, allow_blank: false, presence: true
end

3
extra/.bashrc Обычный файл
Просмотреть файл

@@ -0,0 +1,3 @@
alias ls='ls --color'
alias ll='ls -l'
alias la='ls -la'

7
extra/.env Обычный файл
Просмотреть файл

@@ -0,0 +1,7 @@
COMPOSE_PROJECT_NAME=activeadmin_quill_editor
BUNDLER_VERSION=2.5.23
SERVER_PORT=4000
UID=1000
GID=1000

31
extra/Dockerfile Обычный файл
Просмотреть файл

@@ -0,0 +1,31 @@
ARG RUBY_IMAGE=ruby:3
FROM ${RUBY_IMAGE}
ENV DEBIAN_FRONTEND=noninteractive
ENV DEVEL=1
ENV LANG=C.UTF-8
RUN apt-get update -qq
RUN apt-get install -yqq --no-install-recommends build-essential chromium less libyaml-dev nano netcat-traditional pkg-config
ARG BUNDLER_VERSION
RUN gem install bundler -v ${BUNDLER_VERSION}
RUN echo 'gem: --no-document' > /etc/gemrc
ARG UID
RUN useradd -u $UID --shell /bin/bash app
RUN mkdir -p /home/app && chown -R app:app /home/app
ARG RAILS_VERSION
ENV RAILS_VERSION=$RAILS_VERSION
ARG ACTIVEADMIN_VERSION
ENV ACTIVEADMIN_VERSION=$ACTIVEADMIN_VERSION
WORKDIR /app
COPY . /app
RUN bundle install
RUN chown -R app:app /usr/local/bundle
RUN ln -s /app/extra/.bashrc /home/app/.bashrc

6
extra/Dockerfile.dockerignore Обычный файл
Просмотреть файл

@@ -0,0 +1,6 @@
# Ignore everything but the required files for bundle install
/**/*
!/*.gemspec
!/Gemfile
!/lib

6
extra/dev_setup.sh Исполняемый файл
Просмотреть файл

@@ -0,0 +1,6 @@
#!/bin/sh
export DEVEL=1
export RAILS_VERSION=7.2.2.1
export ACTIVEADMIN_VERSION=3.3.0

50
extra/development.md Обычный файл
Просмотреть файл

@@ -0,0 +1,50 @@
## Development
### Dev setup
There are 2 ways to interact with this project:
1) Using Docker:
```sh
make up # starts the dev services (optional env vars: RUBY / RAILS / ACTIVEADMIN)
make specs # run the tests (after up)
make lint # run the linters (after up)
make server # run the server (after up)
make shell # open a shell (after up)
make down # cleanup (after up)
# Example using specific versions:
RUBY=3.2 RAILS=7.1 ACTIVEADMIN=3.2.0 make up
```
2) With a local setup:
```sh
# Dev setup (set the required envs):
source extra/dev_setup.sh
# Install dependencies:
bundle update
# Run server (or any command):
bin/rails s
# To try different versions of Rails/ActiveAdmin edit extra/dev_setup.sh
```
### Update the editor
- Update the CSS/JS editor assets:
```sh
wget "https://cdn.jsdelivr.net/npm/quill@2/dist/quill.snow.css" -O "app/assets/stylesheets/activeadmin/quill_editor/quill.snow.css"
wget "https://cdn.jsdelivr.net/npm/quill@2/dist/quill.bubble.css" -O "app/assets/stylesheets/activeadmin/quill_editor/quill.bubble.css"
wget "https://cdn.jsdelivr.net/npm/quill@2/dist/quill.core.css" -O "app/assets/stylesheets/activeadmin/quill_editor/quill.core.css"
wget "https://cdn.jsdelivr.net/npm/quill@2/dist/quill.js" -O "app/assets/javascripts/activeadmin/quill_editor/quill.js"
wget "https://cdn.jsdelivr.net/npm/quill@2/dist/quill.core.js" -O "app/assets/javascripts/activeadmin/quill_editor/quill.core.js"
wget "https://github.com/valentsea/quill2-image-uploader/raw/refs/heads/master/dist/quill.imageUploader.min.js" -O "app/assets/javascripts/activeadmin/quill.imageUploader.min.js"
wget "https://github.com/valentsea/quill2-image-uploader/raw/refs/heads/master/dist/quill.imageUploader.min.css" -O "app/assets/stylesheets/activeadmin/quill.imageUploader.min.css"
```
- Check the changes, most of them should be for updated files plus some new / removed file

19
extra/docker-compose.yml Обычный файл
Просмотреть файл

@@ -0,0 +1,19 @@
services:
app:
build:
context: ..
dockerfile: extra/Dockerfile
args:
BUNDLER_VERSION: ${BUNDLER_VERSION}
RUBY_IMAGE: ruby:${RUBY:-3.4}-slim
RAILS_VERSION: ${RAILS:-}
ACTIVEADMIN_VERSION: ${ACTIVEADMIN:-}
UID: ${UID}
user: ${UID}:${GID}
ports:
- ${SERVER_PORT}:${SERVER_PORT}
working_dir: /app
volumes:
- ..:/app
stdin_open: true
tty: true

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 52 KiB

После

Ширина:  |  Высота:  |  Размер: 52 KiB

7
index.js Обычный файл
Просмотреть файл

@@ -0,0 +1,7 @@
// Styles
import 'activeadmin_quill_editor/app/assets/stylesheets/activeadmin/quill_editor/quill.snow'
import 'activeadmin_quill_editor/app/assets/stylesheets/activeadmin/_quill_editor_input'
// JS
window.Quill = require('activeadmin_quill_editor/app/assets/javascripts/activeadmin/quill_editor/quill')
require('activeadmin_quill_editor/app/assets/javascripts/activeadmin/quill_editor_input')

Просмотреть файл

@@ -2,6 +2,7 @@
module ActiveAdmin
module QuillEditor
VERSION = '0.2.8'
VERSION = '2.0.0'
QUILL_VERSION = '2.0.3'
end
end

Просмотреть файл

@@ -6,9 +6,9 @@ module Formtastic
def to_html
input_wrapping do
label_html <<
template.content_tag(:div, input_html_options.merge(class: 'quill-editor')) do
template.content_tag(:div, input_html_options.merge('data-aa-quill-editor': '1')) do
builder.hidden_field(input_name) <<
template.content_tag(:div, class: 'quill-editor-content') do
template.content_tag(:div, 'data-aa-quill-content': '1') do
object.send(method).try :html_safe
end
end

18
package.json Обычный файл
Просмотреть файл

@@ -0,0 +1,18 @@
{
"name": "activeadmin_quill_editor",
"version": "2.0.0",
"description": "Quill Editor for ActiveAdmin",
"author": "Mattia Roccoberton <mat@blocknot.es>",
"license": "MIT",
"homepage": "https://github.com/blocknotes/activeadmin_quill_editor",
"main": "index.js",
"files": [
"app/**/*",
"index.js"
],
"devDependencies": {
"@eslint/js": "^9.22.0",
"eslint": "^9.22.0",
"globals": "^16.0.0"
}
}

Просмотреть файл

@@ -1 +0,0 @@
ruby-2.7.1

Просмотреть файл

@@ -1 +0,0 @@
ruby 2.6.6

Просмотреть файл

@@ -1,7 +1,13 @@
# frozen_string_literal: true
ActiveAdmin.register Post do
permit_params :author_id, :title, :description, :category, :dt, :position, :published, tag_ids: []
permit_params :author_id, :title, :summary, :description, :category, :dt, :position, :published, tag_ids: []
member_action :upload, method: [:post] do
result = { success: resource.images.attach(params[:file_upload]) }
result[:url] = url_for(resource.images.last) if result[:success]
render json: result
end
index do
selectable_column
@@ -17,6 +23,7 @@ ActiveAdmin.register Post do
attributes_table do
row :author
row :title
row :summary
row :description
row :category
row :dt
@@ -38,10 +45,17 @@ ActiveAdmin.register Post do
end
form do |f|
toolbar = %w[bold italic underline link]
f.inputs 'Post' do
f.input :author
f.input :title
f.input :description, as: :quill_editor
f.input :summary, as: :quill_editor, input_html: { data: { options: { modules: { toolbar: toolbar } } } }
if object.new_record?
f.input :description, as: :quill_editor # using default options
else
plugin_opts = { image_uploader: { server_url: upload_admin_post_path(object.id), field_name: 'file_upload' } }
f.input :description, as: :quill_editor, input_html: { data: { plugins: plugin_opts } }
end
f.input :category
f.input :dt
f.input :position

5
spec/dummy/app/admin/profiles.rb Обычный файл
Просмотреть файл

@@ -0,0 +1,5 @@
# frozen_string_literal: true
ActiveAdmin.register Profile do
permit_params :author_id, :description
end

Просмотреть файл

@@ -1,4 +1,5 @@
# frozen_string_literal: true
ActiveAdmin.register Tag do
permit_params :name
end

Просмотреть файл

@@ -2,3 +2,5 @@
//= require activeadmin/quill_editor/quill
//= require activeadmin/quill_editor_input
//= require activeadmin/quill.imageUploader.min

Просмотреть файл

@@ -14,6 +14,8 @@
@import 'activeadmin/quill_editor/quill.snow';
@import 'activeadmin/quill_editor_input';
@import 'activeadmin/quill.imageUploader.min';
// Overriding any non-variable SASS must be done after the fact.
// For example, to change the default status-tag color:
//

Просмотреть файл

@@ -23,4 +23,10 @@ class Author < ApplicationRecord
def to_s
"#{name} (#{age})"
end
class << self
def ransackable_attributes(_auth_object = nil)
%w[age created_at email id name updated_at]
end
end
end

Просмотреть файл

@@ -1,8 +1,6 @@
# frozen_string_literal: true
class Post < ApplicationRecord
enum state: %i[available unavailable arriving]
belongs_to :author, inverse_of: :posts, autosave: true
has_one :author_profile, through: :author, source: :profile
@@ -26,4 +24,14 @@ class Post < ApplicationRecord
def upper_title
title.upcase
end
class << self
def ransackable_associations(_auth_object = nil)
%w[author author_profile post_tags tags images_attachments images_blobs]
end
def ransackable_attributes(_auth_object = nil)
%w[author_id category created_at description dt id position published title summary updated_at]
end
end
end

Просмотреть файл

@@ -6,4 +6,10 @@ class PostTag < ApplicationRecord
validates :post, presence: true
validates :tag, presence: true
class << self
def ransackable_attributes(auth_object = nil)
%w[created_at id post_id tag_id updated_at]
end
end
end

Просмотреть файл

@@ -6,4 +6,14 @@ class Profile < ApplicationRecord
def to_s
description
end
class << self
def ransackable_associations(_auth_object = nil)
%w[author]
end
def ransackable_attributes(_auth_object = nil)
%w[author_id created_at description id updated_at]
end
end
end

Просмотреть файл

@@ -3,4 +3,14 @@
class Tag < ApplicationRecord
has_many :post_tags, inverse_of: :tag, dependent: :destroy
has_many :posts, through: :post_tags
class << self
def ransackable_associations(auth_object = nil)
%w[post_tags posts]
end
def ransackable_attributes(auth_object = nil)
%w[created_at id id_value name updated_at]
end
end
end

Просмотреть файл

@@ -6,13 +6,18 @@ Bundler.require(*Rails.groups)
module Dummy
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 6.0
config.load_defaults Rails::VERSION::STRING.to_f
# Settings in config/environments/* take precedence over those specified here.
# Application configuration can go into files in config/initializers
# -- all .rb files in that directory are automatically loaded after loading
# the framework and any gems in your application.
config.active_support.deprecation = :raise
if Gem::Version.new(Rails.version) < Gem::Version.new('7.1')
config.active_record.legacy_connection_handling = false
end
if Gem::Version.new(Rails.version) > Gem::Version.new('7.0')
config.before_configuration do
ActiveSupport::Cache.format_version = 7.0
end
end
end
end

Просмотреть файл

@@ -3,6 +3,11 @@ default: &default
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000
development:
<<: *default
database: db/development.sqlite3
schema_dump: schema-dev.rb
test:
<<: *default
database: db/test.sqlite3

Просмотреть файл

@@ -59,4 +59,8 @@ Rails.application.configure do
# Use an evented file watcher to asynchronously detect changes in source code,
# routes, locales, etc. This feature depends on the listen gem.
# config.file_watcher = ActiveSupport::EventedFileUpdateChecker
###
config.active_support.to_time_preserves_timezone = :zone if Gem::Version.new(Rails.version) >= Gem::Version.new('8.0')
end

Просмотреть файл

@@ -1,112 +0,0 @@
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# Code is not reloaded between requests.
config.cache_classes = true
# Eager load code on boot. This eager loads most of Rails and
# your application in memory, allowing both threaded web servers
# and those relying on copy on write to perform better.
# Rake tasks automatically ignore this option for performance.
config.eager_load = true
# Full error reports are disabled and caching is turned on.
config.consider_all_requests_local = false
config.action_controller.perform_caching = true
# Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
# or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
# config.require_master_key = true
# Disable serving static files from the `/public` folder by default since
# Apache or NGINX already handles this.
config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
# Compress CSS using a preprocessor.
# config.assets.css_compressor = :sass
# Do not fallback to assets pipeline if a precompiled asset is missed.
config.assets.compile = false
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
# config.action_controller.asset_host = 'http://assets.example.com'
# Specifies the header that your server uses for sending files.
# config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
# Store uploaded files on the local file system (see config/storage.yml for options).
config.active_storage.service = :local
# Mount Action Cable outside main process or domain.
# config.action_cable.mount_path = nil
# config.action_cable.url = 'wss://example.com/cable'
# config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
# config.force_ssl = true
# Use the lowest log level to ensure availability of diagnostic information
# when problems arise.
config.log_level = :debug
# Prepend all log lines with the following tags.
config.log_tags = [ :request_id ]
# Use a different cache store in production.
# config.cache_store = :mem_cache_store
# Use a real queuing backend for Active Job (and separate queues per environment).
# config.active_job.queue_adapter = :resque
# config.active_job.queue_name_prefix = "dummy_production"
# config.action_mailer.perform_caching = false
# Ignore bad email addresses and do not raise email delivery errors.
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
# config.action_mailer.raise_delivery_errors = false
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
# the I18n.default_locale when a translation cannot be found).
config.i18n.fallbacks = true
# Send deprecation notices to registered listeners.
config.active_support.deprecation = :notify
# Use default logging formatter so that PID and timestamp are not suppressed.
config.log_formatter = ::Logger::Formatter.new
# Use a different logger for distributed setups.
# require 'syslog/logger'
# config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
if ENV["RAILS_LOG_TO_STDOUT"].present?
logger = ActiveSupport::Logger.new(STDOUT)
logger.formatter = config.log_formatter
config.logger = ActiveSupport::TaggedLogging.new(logger)
end
# Do not dump schema after migrations.
config.active_record.dump_schema_after_migration = false
# Inserts middleware to perform automatic connection switching.
# The `database_selector` hash is used to pass options to the DatabaseSelector
# middleware. The `delay` is used to determine how long to wait after a write
# to send a subsequent read to the primary.
#
# The `database_resolver` class is used by the middleware to determine which
# database is appropriate to use based on the time delay.
#
# The `database_resolver_context` class is used by the middleware to set
# timestamps for the last write to the primary. The resolver uses the context
# class timestamps to determine how long to wait before reading from the
# replica.
#
# By default Rails will store a last write timestamp in the session. The
# DatabaseSelector middleware is designed as such you can define your own
# strategy for connection switching and pass that into the middleware through
# these configuration options.
# config.active_record.database_selector = { delay: 2.seconds }
# config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
# config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
end

Просмотреть файл

@@ -26,7 +26,7 @@ Rails.application.configure do
config.cache_store = :null_store
# Raise exceptions instead of rendering exception templates.
config.action_dispatch.show_exceptions = false
config.action_dispatch.show_exceptions = :none
# Disable request forgery protection in test environment.
config.action_controller.allow_forgery_protection = false
@@ -46,4 +46,8 @@ Rails.application.configure do
# Raises error for missing translations.
# config.action_view.raise_on_missing_translations = true
###
config.active_support.to_time_preserves_timezone = :zone if Gem::Version.new(Rails.version) >= Gem::Version.new('8.0')
end

Просмотреть файл

@@ -0,0 +1,9 @@
Rails.application.reloader.to_prepare do
ActiveStorage::Attachment.class_eval do
class << self
def ransackable_attributes(auth_object = nil)
%w[blob_id created_at id name record_id record_type]
end
end
end
end

Просмотреть файл

@@ -1,3 +1,5 @@
Rails.application.routes.draw do
ActiveAdmin.routes(self)
root to: redirect('/admin')
end

Просмотреть файл

@@ -1,27 +0,0 @@
# This migration comes from active_storage (originally 20170806125915)
class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
def change
create_table :active_storage_blobs do |t|
t.string :key, null: false
t.string :filename, null: false
t.string :content_type
t.text :metadata
t.bigint :byte_size, null: false
t.string :checksum, null: false
t.datetime :created_at, null: false
t.index [ :key ], unique: true
end
create_table :active_storage_attachments do |t|
t.string :name, null: false
t.references :record, null: false, polymorphic: true, index: false
t.references :blob, null: false
t.datetime :created_at, null: false
t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true
t.foreign_key :active_storage_blobs, column: :blob_id
end
end
end

Просмотреть файл

@@ -1,3 +1,5 @@
# frozen_string_literal: true
class CreateActiveAdminComments < ActiveRecord::Migration[6.0]
def self.up
create_table :active_admin_comments do |t|

Просмотреть файл

@@ -0,0 +1,59 @@
# frozen_string_literal: true
# This migration comes from active_storage (originally 20170806125915)
class CreateActiveStorageTables < ActiveRecord::Migration[6.0]
def change
# Use Active Record's configured type for primary and foreign keys
primary_key_type, foreign_key_type = primary_and_foreign_key_types
create_table :active_storage_blobs, id: primary_key_type do |t|
t.string :key, null: false
t.string :filename, null: false
t.string :content_type
t.text :metadata
t.string :service_name, null: false
t.bigint :byte_size, null: false
t.string :checksum
if connection.supports_datetime_with_precision?
t.datetime :created_at, precision: 6, null: false
else
t.datetime :created_at, null: false
end
t.index [ :key ], unique: true
end
create_table :active_storage_attachments, id: primary_key_type do |t|
t.string :name, null: false
t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
t.references :blob, null: false, type: foreign_key_type
if connection.supports_datetime_with_precision?
t.datetime :created_at, precision: 6, null: false
else
t.datetime :created_at, null: false
end
t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true
t.foreign_key :active_storage_blobs, column: :blob_id
end
create_table :active_storage_variant_records, id: primary_key_type do |t|
t.belongs_to :blob, null: false, index: false, type: foreign_key_type
t.string :variation_digest, null: false
t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true
t.foreign_key :active_storage_blobs, column: :blob_id
end
end
private
def primary_and_foreign_key_types
config = Rails.configuration.generators
setting = config.options[config.orm][:primary_key_type]
primary_key_type = setting || :primary_key
foreign_key_type = setting || :bigint
[ primary_key_type, foreign_key_type ]
end
end

Просмотреть файл

@@ -1,6 +1,6 @@
# frozen_string_literal: true
class CreateAuthors < ActiveRecord::Migration[5.2]
class CreateAuthors < ActiveRecord::Migration[6.0]
def change
create_table :authors do |t|
t.string :name

Просмотреть файл

@@ -1,6 +1,6 @@
# frozen_string_literal: true
class CreateProfiles < ActiveRecord::Migration[5.2]
class CreateProfiles < ActiveRecord::Migration[6.0]
def change
create_table :profiles do |t|
t.text :description

Просмотреть файл

@@ -1,6 +1,6 @@
# frozen_string_literal: true
class CreateTags < ActiveRecord::Migration[5.2]
class CreateTags < ActiveRecord::Migration[6.0]
def change
create_table :tags do |t|
t.string :name

Просмотреть файл

@@ -1,6 +1,6 @@
# frozen_string_literal: true
class CreatePostTags < ActiveRecord::Migration[5.2]
class CreatePostTags < ActiveRecord::Migration[6.0]
def change
create_table :post_tags do |t|
t.belongs_to :post, foreign_key: true

Просмотреть файл

@@ -1,9 +1,10 @@
# frozen_string_literal: true
class CreatePosts < ActiveRecord::Migration[5.2]
class CreatePosts < ActiveRecord::Migration[6.0]
def change
create_table :posts do |t|
t.string :title
t.text :summary
t.text :description
t.belongs_to :author, foreign_key: true
t.string :category

Просмотреть файл

@@ -2,8 +2,8 @@
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# This file is the source Rails uses to define your schema when running `rails
# db:schema:load`. When creating a new database, `rails db:schema:load` tends to
# This file is the source Rails uses to define your schema when running `bin/rails
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
# be faster and is potentially less error prone than running all of your
# migrations from scratch. Old migrations may fail to apply correctly if those
# migrations use external dependencies or application code.
@@ -11,7 +11,6 @@
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2018_06_07_053739) do
create_table "active_admin_comments", force: :cascade do |t|
t.string "namespace"
t.text "body"
@@ -19,8 +18,8 @@ ActiveRecord::Schema.define(version: 2018_06_07_053739) do
t.integer "resource_id"
t.string "author_type"
t.integer "author_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["author_type", "author_id"], name: "index_active_admin_comments_on_author_type_and_author_id"
t.index ["namespace"], name: "index_active_admin_comments_on_namespace"
t.index ["resource_type", "resource_id"], name: "index_active_admin_comments_on_resource_type_and_resource_id"
@@ -29,8 +28,8 @@ ActiveRecord::Schema.define(version: 2018_06_07_053739) do
create_table "active_storage_attachments", force: :cascade do |t|
t.string "name", null: false
t.string "record_type", null: false
t.integer "record_id", null: false
t.integer "blob_id", null: false
t.bigint "record_id", null: false
t.bigint "blob_id", null: false
t.datetime "created_at", null: false
t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
@@ -41,57 +40,66 @@ ActiveRecord::Schema.define(version: 2018_06_07_053739) do
t.string "filename", null: false
t.string "content_type"
t.text "metadata"
t.string "service_name", null: false
t.bigint "byte_size", null: false
t.string "checksum", null: false
t.string "checksum"
t.datetime "created_at", null: false
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
end
create_table "active_storage_variant_records", force: :cascade do |t|
t.bigint "blob_id", null: false
t.string "variation_digest", null: false
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
end
create_table "authors", force: :cascade do |t|
t.string "name"
t.integer "age"
t.string "email"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime "created_at", precision: nil, null: false
t.datetime "updated_at", precision: nil, null: false
end
create_table "post_tags", force: :cascade do |t|
t.integer "post_id"
t.integer "tag_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime "created_at", precision: nil, null: false
t.datetime "updated_at", precision: nil, null: false
t.index ["post_id"], name: "index_post_tags_on_post_id"
t.index ["tag_id"], name: "index_post_tags_on_tag_id"
end
create_table "posts", force: :cascade do |t|
t.string "title"
t.text "summary"
t.text "description"
t.integer "author_id"
t.string "category"
t.datetime "dt"
t.datetime "dt", precision: nil
t.float "position"
t.boolean "published"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime "created_at", precision: nil, null: false
t.datetime "updated_at", precision: nil, null: false
t.index ["author_id"], name: "index_posts_on_author_id"
end
create_table "profiles", force: :cascade do |t|
t.text "description"
t.integer "author_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime "created_at", precision: nil, null: false
t.datetime "updated_at", precision: nil, null: false
t.index ["author_id"], name: "index_profiles_on_author_id"
end
create_table "tags", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime "created_at", precision: nil, null: false
t.datetime "updated_at", precision: nil, null: false
end
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
add_foreign_key "post_tags", "posts"
add_foreign_key "post_tags", "tags"
add_foreign_key "posts", "authors"

39
spec/dummy/db/seeds.rb Обычный файл
Просмотреть файл

@@ -0,0 +1,39 @@
# frozen_string_literal: true
(11..16).each do |i|
Tag.find_or_create_by!(name: "Tag #{i}")
end
(11..20).each do |i|
age = 21 + 3 * (i - 10)
attrs = { name: "Author #{i}", age: age, email: "some@email#{i}.com" }
Author.find_or_create_by!(name: "Author #{i}") do |author|
author.assign_attributes(attrs)
author.profile = Profile.new(description: "Profile description for Author #{i}") if (i % 3).zero?
end
end
authors = Author.pluck(:id)
tags = Tag.where.not(name: 'A test tag').pluck(:id)
(11..40).each do |i|
attrs = {
title: "Post #{i}",
author_id: authors.sample,
position: rand(100),
summary: "<p><em>Summary</em> for post #{i}</p>",
description: "<p><strong>Some bold</strong> <em>Some italic</em> <u>Some underline</u> [#{i}]</p>",
created_at: Time.now - rand(3600).seconds
}
attrs[:category] = 'news' if (i % 4).zero?
attrs[:dt] = Time.now - rand(30).days if (i % 3).zero?
Post.find_or_create_by!(title: "Post #{i}") do |post|
post.assign_attributes(attrs)
post.tags = Tag.where(id: tags.sample(2)) if (i % 2).zero?
end
end
Author.find_or_create_by!(name: 'A test author', email: 'aaa@bbb.ccc', age: 30)
Tag.find_or_create_by!(name: 'A test tag')
puts '> Seeds: done.'

Просмотреть файл

@@ -0,0 +1,9 @@
# frozen_string_literal: true
module Admin
module Authors
class EditPage < BasePage
include Capybara::DSL
end
end
end

9
spec/page_objects/admin/posts/edit_page.rb Обычный файл
Просмотреть файл

@@ -0,0 +1,9 @@
# frozen_string_literal: true
module Admin
module Posts
class EditPage < BasePage
include Capybara::DSL
end
end
end

12
spec/page_objects/base_object.rb Обычный файл
Просмотреть файл

@@ -0,0 +1,12 @@
# frozen_string_literal: true
class BaseObject
include Capybara::DSL
attr_reader :element, :selector
def initialize(selector:)
@selector = selector
@element = find(selector)
end
end

19
spec/page_objects/base_page.rb Обычный файл
Просмотреть файл

@@ -0,0 +1,19 @@
# frozen_string_literal: true
class BasePage
include Capybara::DSL
attr_reader :path
def initialize(path:)
@path = path
end
def load
visit(path)
end
def lookup_editor(editor_container:)
@editor = Shared::QuillEditor.new(editor_container)
end
end

35
spec/page_objects/shared/html_editor.rb Обычный файл
Просмотреть файл

@@ -0,0 +1,35 @@
# frozen_string_literal: true
module Shared
class HtmlEditor < BaseObject
def content_element
raise NotImplementedError
end
def clear
select_all
content_element.send_keys(:delete)
self
end
# @return [self]
def open_dropdown
raise NotImplementedError
end
def select_all
content_element.send_keys([:control, "a"])
self
end
def toolbar_control(control, ...)
send(:"toggle_#{control}", ...)
self
end
def <<(content)
content_element.send_keys(content)
self
end
end
end

64
spec/page_objects/shared/quill_editor.rb Обычный файл
Просмотреть файл

@@ -0,0 +1,64 @@
# frozen_string_literal: true
module Shared
class QuillEditor < HtmlEditor
attr_reader :editor_selector, :toolbar_selector
def initialize(container)
@editor_selector = "#{container} .ql-container"
@toolbar_selector = "#{container} .ql-toolbar"
super(selector: editor_selector)
end
def content = content_element['innerHTML']
def content_element
@content_element ||= find("#{selector} .ql-editor")
end
def open_dropdown(dropdown)
find("#{toolbar_selector} .ql-#{dropdown} .ql-picker-label").click
self
end
def toggle_bold
find("#{toolbar_selector} button.ql-bold").click
end
def toggle_italic
find("#{toolbar_selector} button.ql-italic").click
end
def toggle_underline
find("#{toolbar_selector} button.ql-underline").click
end
def toggle_link
find("#{toolbar_selector} button.ql-link").click
end
def toggle_blockquote
find("#{toolbar_selector} button.ql-blockquote").click
end
def toggle_code_block
find("#{toolbar_selector} button.ql-code-block").click
end
def toggle_sub
find("#{toolbar_selector} button.ql-script[value='sub']").click
end
def toggle_super
find("#{toolbar_selector} button.ql-script[value='super']").click
end
def toggle_align_right
find("#{toolbar_selector} .ql-picker-item[data-value='right']").click
end
def tooltip_editing
find("#{editor_selector} .ql-tooltip.ql-editing")
end
end
end

Просмотреть файл

@@ -1,9 +1,19 @@
# frozen_string_literal: true
require 'spec_helper'
require 'super_diff/rspec'
require 'zeitwerk'
loader = Zeitwerk::Loader.new
loader.push_dir("#{__dir__}/page_objects")
loader.setup
require_relative 'spec_helper'
ENV['RAILS_ENV'] = 'test'
require 'simplecov'
SimpleCov.start 'rails'
require File.expand_path('dummy/config/environment.rb', __dir__)
abort('The Rails environment is running in production mode!') if Rails.env.production?
@@ -11,13 +21,8 @@ abort('The Rails environment is running in production mode!') if Rails.env.produ
require 'rspec/rails'
require 'capybara/rails'
Dir[File.expand_path('support/**/*.rb', __dir__)].sort.each { |f| require f }
Dir[File.expand_path('support/**/*.rb', __dir__)].each { |f| require_relative f }
# Force deprecations to raise an exception.
ActiveSupport::Deprecation.behavior = :raise
# Checks for pending migrations and applies them before tests are run.
# If you are not using ActiveRecord, you can remove these lines.
begin
ActiveRecord::Migration.maintain_test_schema!
rescue ActiveRecord::PendingMigrationError => e
@@ -26,11 +31,27 @@ rescue ActiveRecord::PendingMigrationError => e
end
RSpec.configure do |config|
config.fixture_path = "#{::Rails.root}/spec/fixtures"
if Gem::Version.new(Rails.version) >= Gem::Version.new('7.1')
config.fixture_paths = [Rails.root.join('spec/fixtures')]
else
config.fixture_path = Rails.root.join('spec/fixtures')
end
config.infer_spec_type_from_file_location!
config.filter_rails_from_backtrace!
config.use_transactional_fixtures = true
config.use_instantiated_fixtures = false
config.render_views = false
config.before(:suite) do
intro = ('-' * 80)
intro << "\n"
intro << "- Ruby: #{RUBY_VERSION}\n"
intro << "- Rails: #{Rails.version}\n"
intro << "- ActiveAdmin: #{ActiveAdmin::VERSION}\n"
intro << ('-' * 80)
RSpec.configuration.reporter.message(intro)
end
end

Просмотреть файл

@@ -5,10 +5,12 @@ RSpec.configure do |config|
config.filter_run focus: true
config.filter_run_excluding changes_filesystem: true
config.run_all_when_everything_filtered = true
config.color = true
config.order = :random
config.example_status_persistence_file_path = '.rspec_failures'
config.color = true
config.tty = true
config.example_status_persistence_file_path = '.rspec_failures'
config.order = :random
config.shared_context_metadata_behavior = :apply_to_host_groups
config.expect_with :rspec do |expectations|

Просмотреть файл

@@ -1,3 +1,28 @@
# frozen_string_literal: true
Capybara.server = :puma
require 'capybara/cuprite'
Capybara.register_driver(:capybara_cuprite) do |app|
browser_options = {}.tap do |opts|
opts['no-sandbox'] = nil if ENV['DEVEL']
end
Capybara::Cuprite::Driver.new(
app,
window_size: [1600, 1024],
browser_options: browser_options,
process_timeout: 30,
timeout: 30,
inspector: true,
headless: !ENV['CUPRITE_HEADLESS'].in?(%w[n 0 no false])
)
end
# Capybara.server = :puma
Capybara.default_driver = Capybara.javascript_driver = :capybara_cuprite
RSpec.configure do |config|
config.prepend_before(:each, type: :system) do
driven_by Capybara.javascript_driver
end
end

Просмотреть файл

@@ -1,7 +0,0 @@
# frozen_string_literal: true
RSpec.configure do |config|
config.before(:each, type: :system) do
driven_by(:selenium_chrome_headless)
end
end

10
spec/support/string_clean_multiline.rb Обычный файл
Просмотреть файл

@@ -0,0 +1,10 @@
# frozen_string_literal: true
module StringCleanMultiline
refine String do
def clean_multiline
# Get rid of newlines and indentation spaces
strip.gsub(/\s*\n\s*/, "")
end
end
end

Просмотреть файл

@@ -1,46 +1,149 @@
# frozen_string_literal: true
RSpec.describe 'Quill editor', type: :system do
using StringCleanMultiline
RSpec.describe 'Quill editor' do
let(:author) { Author.create!(email: 'some_email@example.com', name: 'John Doe', age: 30) }
let(:post) { Post.create!(title: 'Test', author: author, description: 'Some content...') }
before do
post
let(:post) do
Post.create!(title: 'Test', author: author, description: '<p>Some content</p>', summary: '<p>Post summary</p>')
end
after do
Post.destroy_all
author.destroy
end
let(:submit_button) { find('#post_submit_action [type="submit"]') }
context 'with a Quill editor' do
it 'updates some HTML content' do
visit "/admin/posts/#{post.id}/edit"
let(:edit_page) do
path = edit_admin_post_path(post)
Admin::Posts::EditPage.new(path: path)
end
let(:editor) { edit_page.lookup_editor(editor_container: '#post_description_input') }
let(:input_field) { find('#post_description[data-aa-quill-editor]') }
expect(page).to have_css('#post_description_input .ql-editor', text: 'Some content...')
find('#post_description_input .ql-editor').click
find('#post_description_input .ql-editor').base.send_keys('more text')
before do
edit_page.load
end
it 'initializes the editor', :aggregate_failures do
expect(editor.content_element).to be_present
expect(editor.content).to eq('<p>Some content</p>')
expect(input_field).to be_present
end
it 'edits some content using the editor' do
editor.select_all
editor.toggle_link
editor.tooltip_editing.send_keys(["https://blocknot.es", :return])
editor << :right << :return << 'More content'
editor.toggle_bold
editor << 'Some bold'
editor.toggle_italic
editor << 'Some italic'
editor.toggle_underline
editor << 'Some underline' << :return
editor.toggle_blockquote
editor << 'blockquote enabled' << :return
editor.toggle_blockquote
editor.toggle_code_block
editor << 'code block enabled' << :return
editor.toggle_code_block
editor << "Some text"
editor.toggle_sub
editor << "sub text"
editor.toggle_sub
editor << " More text"
editor.toggle_super
editor << "sup text"
editor.toggle_super
editor << :return
editor.open_dropdown(:align).toggle_align_right
editor << "Text aligned on the right"
expect(editor.content).to eq <<~HTML.clean_multiline
<p><a href="https://blocknot.es" rel="noopener noreferrer" target="_blank">Some content</a></p>
<p>More content<strong>Some bold<em>Some italic<u>Some underline</u></em></strong></p>
<blockquote>blockquote enabled</blockquote>
<div class="ql-code-block-container" spellcheck="false">
<div class="ql-code-block">code block enabled</div>
</div>
<p>Some text<sub>sub text</sub> More text<sup>sup text</sup></p>
<p class="ql-align-right">Text aligned on the right</p>
HTML
end
it 'updates the field when submitting', :aggregate_failures do
editor.toggle_bold
editor << 'More content'
before = '<p>Some content</p>'
after = '<p><strong>More content</strong>Some content</p>'
expect { submit_button.click }.to change { post.reload.description }.from(before).to(after)
find('[type="submit"]').click
expect(page).to have_content('was successfully updated')
expect(post.reload.description).to eq '<p>Some content...more text</p>'
end
end
context 'with 2 Quill editors' do
let(:edit_page) do
path = edit_admin_post_path(post)
Admin::Posts::EditPage.new(path: path)
end
let(:first_editor) { edit_page.lookup_editor(editor_container: '#post_description_input') }
let(:second_editor) { edit_page.lookup_editor(editor_container: '#post_summary_input') }
before do
edit_page.load
end
it 'updates some HTML content for 2 fields', :aggregate_failures do
# Check the initial states
expect(first_editor.content).to eq('<p>Some content</p>')
expect(second_editor.content).to eq('<p>Post summary</p>')
# Add some content to both the editors
first_editor.toggle_bold
first_editor << 'Some bold'
second_editor.toggle_italic
second_editor << 'Some italic'
# Check that both the fields change
before = '<p>Some content</p>'
after = '<p><strong>Some bold</strong>Some content</p>'
expect { submit_button.click }.to change { post.reload.description }.from(before).to(after)
expect(post.summary).to eq '<p><em>Some italic</em>Post summary</p>'
end
end
context 'with a Quill editor in a nested resource' do
it 'updates some HTML content of a new nested resource' do
visit "/admin/authors/#{author.id}/edit"
let(:edit_page) do
path = edit_admin_author_path(author)
Admin::Authors::EditPage.new(path: path)
end
let(:submit_button) { find('#author_submit_action [type="submit"]') }
expect(page).to have_css('.posts.has_many_container .ql-editor', text: 'Some content...')
find('.posts.has_many_container .has_many_add').click
expect(page).to have_css('.posts.has_many_container .ql-editor', count: 2)
before do
post
edit_page.load
end
fill_in('author[posts_attributes][1][title]', with: 'A new post')
find('#author_posts_attributes_1_description_input .ql-editor').base.send_keys('new post text')
find('[type="submit"]').click
it 'updates some HTML content of a new nested resource', :aggregate_failures do
click_on 'Add New Post'
expect(page).to have_content('was successfully updated')
expect(author.posts.last.description).to eq '<p>new post text</p>'
first_editor = edit_page.lookup_editor(editor_container: '#author_posts_attributes_0_description_input')
expect(first_editor.content).to eq('<p>Some content</p>')
fill_in('author[posts_attributes][1][title]', with: 'Some title')
second_editor = edit_page.lookup_editor(editor_container: '#author_posts_attributes_1_description_input')
second_editor.toggle_underline
second_editor << 'Some underline'
expect { submit_button.click }.to change(Post, :count).by(1)
expect(Post.last.description).to eq '<p><u>Some underline</u></p>'
end
end
end

65
spec/system/quill_js_spec.rb Обычный файл
Просмотреть файл

@@ -0,0 +1,65 @@
# frozen_string_literal: true
RSpec.describe 'Quill JS' do
it 'has a Javascript function defined and returns the version', :aggregate_failures do
visit '/admin/posts'
expect(page.evaluate_script('typeof Quill')).to eq 'function'
expect(page.evaluate_script('Quill.version')).to eq(ActiveAdmin::QuillEditor::QUILL_VERSION)
end
describe '.getQuillEditors' do
let(:author) { Author.create!(email: 'some_email@example.com', name: 'John Doe', age: 30) }
let!(:post) { Post.create!(title: 'Test', author: author, description: '') }
before do
path = edit_admin_post_path(post)
Admin::Posts::EditPage.new(path: path).load
end
it "returns the available editors", :aggregate_failures do
editors_count = page.evaluate_script('window.getQuillEditors().length')
expect(editors_count).to eq 2
expected_element = find('#post_summary > .ql-container')
first_editor = page.evaluate_script('window.getQuillEditors()[0].container')
expect(first_editor).to eq expected_element
expected_element = find('#post_description > .ql-container')
first_editor = page.evaluate_script('window.getQuillEditors()[1].container')
expect(first_editor).to eq expected_element
end
end
describe '.getQuillEditorByIndex' do
let(:author) { Author.create!(email: 'some_email@example.com', name: 'John Doe', age: 30) }
let!(:post) { Post.create!(title: 'Test', author: author, description: '') }
before do
path = edit_admin_post_path(post)
Admin::Posts::EditPage.new(path: path).load
end
it "returns the expected editor instance" do
expected_element = find('#post_description > .ql-container')
editor = page.evaluate_script('window.getQuillEditorByIndex(1).container')
expect(editor).to eq expected_element
end
end
describe '.getQuillEditorByElementId' do
let(:author) { Author.create!(email: 'some_email@example.com', name: 'John Doe', age: 30) }
let!(:post) { Post.create!(title: 'Test', author: author, description: '') }
before do
path = edit_admin_post_path(post)
Admin::Posts::EditPage.new(path: path).load
end
it "returns the expected editor instance" do
expected_element = find('#post_description > .ql-container')
editor = page.evaluate_script('window.getQuillEditorByElementId("post_description").container')
expect(editor).to eq expected_element
end
end
end