katalyst-content 3.2.2 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fb3b9ee3efb967450397857049504a2fdc5e4cc845fbd9a53f3e51d728c578ba
4
- data.tar.gz: b0befb4e9df360379236e4520f0e86a97f7daaaa4e3a38d83bf7066e2ce3def7
3
+ metadata.gz: 67386b67c7add01c35e38a9a2615c09fb56dbc3a77f74289671e3df3539e82ef
4
+ data.tar.gz: efbadc3abde98aaf0591b9ac8566c982b1d3c3094f7375b2e4555d8e21a269a9
5
5
  SHA512:
6
- metadata.gz: b4d094dcd0b84d54d521e75b8d4cd75f766e957a15642dbc065f4a0fae00aa7646719a4f1dfa2fe7309481f32e74e02cf0db9d88ce0ecbc0c3fcbdcb3ffbeba6
7
- data.tar.gz: 9666b55c1a490a23e2deb084f4277b75380ba759be9f2071a7b593287c7b05ea14be776ec4e5dc13275c4d2c0ec80a829aa6f3db2db5412857f1b7d6910294bc
6
+ metadata.gz: 74d3ac936c53778bee4c15cac9ced3ae219eb00f7338aba68d8ac713844e7a83fd74ee0a195b21fba03880fd8ad2a0d5e39f2e3d03f50931088de7134a263dba
7
+ data.tar.gz: 4649815a1c692d4b1d6ab7e30c99b4a76e605dbbe8f2771415a5e44eb90cbb716e55e3d46336e67a68530f33051f7e51e50781c5387a60438f930ebadad35377
data/README.md CHANGED
@@ -1,232 +1,10 @@
1
- # Katalyst::Content
1
+ # Katalyst Content
2
2
 
3
- Katalyst Content provides tools for creating and publishing content on Rails
4
- applications.
3
+ A library for editing and publishing rich content in Ruby on Rails.
5
4
 
6
- ## Installation
5
+ ## Documentation
7
6
 
8
- Install the gem as usual
9
-
10
- ```ruby
11
- gem "katalyst-content"
12
- ```
13
-
14
- Mount the engine in your `routes.rb` file:
15
-
16
- ```ruby
17
- mount Katalyst::Content::Engine, at: "content"
18
- ```
19
-
20
- Add the Gem's migrations to your application:
21
-
22
- ```ruby
23
- rake katalyst_content:install:migrations
24
- ```
25
-
26
- Add the Gem's javascript and CSS to your build pipeline. This assumes that
27
- you're using `propshaft` and `importmaps` to manage your assets.
28
-
29
- ```javascript
30
- // app/javascript/controllers/application.js
31
- import { application } from "controllers/application";
32
- import content from "@katalyst/content";
33
- application.load(content);
34
- ```
35
-
36
- Import the editor styles as css:
37
-
38
- ```css
39
- /** In your admin/editor */
40
- @import url("/katalyst/content/editor.css");
41
- /** In your frontend */
42
- @import url("/katalyst/content/frontend.css");
43
- ```
44
-
45
- Or, if you're using `dartsass-rails`:
46
-
47
- ```scss
48
- // app/assets/stylesheets/admin.scss
49
- @use "katalyst/content/editor";
50
- // app/assets/stylesheets/application.scss
51
- @use "katalyst/content/frontend";
52
- ```
53
-
54
- ## Usage
55
-
56
- Content can be added to multiple models in your application. These examples
57
- assume a `Page` model.
58
-
59
- Assuming your model already exists, create a table for versions and add
60
- published and draft version columns to your model. For example, if you have a
61
- pages model:
62
-
63
- ```ruby
64
- class CreatePageVersions < ActiveRecord::Migration[7.0]
65
- def change
66
- create_table :page_versions do |t|
67
- t.references :parent, foreign_key: { to_table: :pages }, null: false
68
- t.json :nodes
69
-
70
- t.timestamps
71
- end
72
-
73
- change_table :pages do |t|
74
- t.references :published_version, foreign_key: { to_table: :page_versions }
75
- t.references :draft_version, foreign_key: { to_table: :page_versions }
76
- end
77
- end
78
- end
79
- ```
80
-
81
- If you don't have a pages model yet, add it before the page_versions change:
82
- ```ruby
83
- class CreatePages < ActiveRecord::Migration[7.0]
84
- def change
85
- create_table :pages do |t|
86
- t.string :title
87
- t.string :slug
88
- t.boolean :show_title, default: true, null: false
89
-
90
- t.timestamps
91
- end
92
- add_index :pages, :slug, unique: true
93
-
94
- create_table :page_versions do |t|
95
- t.references :parent, foreign_key: { to_table: :pages }, null: false
96
- t.json :nodes
97
-
98
- t.timestamps
99
- end
100
-
101
- change_table :pages do |t|
102
- t.references :published_version, foreign_key: { to_table: :page_versions }
103
- t.references :draft_version, foreign_key: { to_table: :page_versions }
104
- end
105
- end
106
- end
107
- ```
108
-
109
-
110
- Next, include the `Katalyst::Content` concerns into your model, and add a nested
111
- model for storing content version information:
112
-
113
- ```ruby
114
- class Page < ApplicationRecord
115
- include Katalyst::Content::Container
116
-
117
- class Version < ApplicationRecord
118
- include Katalyst::Content::Version
119
- end
120
- end
121
- ```
122
-
123
- You may also want to configure your factory to add container information to
124
- items:
125
-
126
- ```ruby
127
- FactoryBot.define do
128
- factory :page do
129
- title { Faker::Beer.unique.name }
130
- slug { title.parameterize }
131
-
132
- after(:build) do |page, _context|
133
- page.items.each { |item| item.container = page }
134
- end
135
-
136
- after(:create) do |page, _context|
137
- page.items_attributes = page.items.map.with_index { |item, index| { id: item.id, index: index, depth: 0 } }
138
- page.publish!
139
- end
140
- end
141
- end
142
- ```
143
-
144
- Create a controller for editing content. This example assumes you're rendering the editor on the 'show' route of an
145
- admin controller.
146
-
147
- ```ruby
148
- class Admin::PagesController < Admin::BaseController
149
- before_action :set_page, only: %i[show update]
150
-
151
- def show; end
152
-
153
- def update
154
- @page.attributes = page_params
155
-
156
- unless @page.valid?
157
- return respond_to do |format|
158
- format.turbo_stream { render @editor.errors, status: :unprocessable_entity }
159
- end
160
- end
161
-
162
- case params[:commit]
163
- when "publish"
164
- @page.save!
165
- @page.publish!
166
- when "save"
167
- @page.save!
168
- when "revert"
169
- @page.revert!
170
- end
171
-
172
- redirect_to [:admin, @page], status: :see_other
173
- end
174
-
175
- private
176
-
177
- def set_page
178
- @page = Page.find(params[:id])
179
- @editor = Katalyst::Content::EditorComponent.new(container: @page)
180
- end
181
-
182
- def page_params
183
- params.require(:page).permit(items_attributes: %i[id index depth])
184
- end
185
- end
186
- ```
187
-
188
- And the view:
189
-
190
- ```erb
191
- <%# app/views/admin/pages/show.html.erb %>
192
- <%= render @editor.status_bar %>
193
- <%= render @editor %>
194
- ```
195
-
196
- ### New items dialog customisation
197
-
198
- The new items dialog can be customised by providing content to the ViewComponent slot:
199
-
200
- ```erb
201
- <%# app/views/admin/pages/show.html.erb %>
202
- <%= render @editor.status_bar %>
203
- <%= render @editor do |editor_component| %>
204
- <% editor_component.with_new_items do |component| %>
205
- <h3>Layouts</h3>
206
- <ul role="list" class="items-list">
207
- <%= component.item(:section) %>
208
- <%= component.item(:group) %>
209
- <%= component.item(:column) %>
210
- <%= component.item(:aside) %>
211
- </ul>
212
- <h3>Content</h3>
213
- <ul role="list" class="items-list">
214
- <%= component.item(:content) %>
215
- <%= component.item(:figure) %>
216
- <%= component.item(:table) %>
217
- </ul>
218
- <% end %>
219
- <% end %>
220
-
221
- ```
222
-
223
- ## Development
224
-
225
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake` to run the tests.
226
-
227
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the
228
- version number and run `bundle exec rake release`, which will create a git tag for the version, push git commits and
229
- the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
7
+ See [katalyst.github.io/content](https://katalyst.github.io/content/) for documentation.
230
8
 
231
9
  ## Contributing
232
10
 
@@ -234,4 +12,4 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/kataly
234
12
 
235
13
  ## License
236
14
 
237
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
15
+ Katalyst Content is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Content
5
+ # Copies declared associations when a record is duplicated, e.g. for
6
+ # copy-on-write. Intended for owned detail records that extend an STI
7
+ # item with attributes beyond the shared table:
8
+ #
9
+ # has_one :detail, as: :detailable, autosave: true, dependent: :destroy
10
+ # accepts_nested_attributes_for :detail, update_only: true
11
+ # duplicates_association :detail
12
+ #
13
+ # Unsaved changes on the source's association are carried over to the
14
+ # duplicate. Records marked for destruction are dropped.
15
+ module DuplicatesAssociations
16
+ extend ActiveSupport::Concern
17
+
18
+ included do
19
+ class_attribute :duplicated_associations, instance_writer: false, default: []
20
+ end
21
+
22
+ class_methods do
23
+ def duplicates_association(*names)
24
+ self.duplicated_associations += names.map(&:to_sym)
25
+ end
26
+ end
27
+
28
+ def initialize_dup(source)
29
+ super
30
+
31
+ duplicated_associations.each do |name|
32
+ macro = self.class.reflect_on_association(name).macro
33
+ DUPLICATORS.fetch(macro).new(name).apply(source, self)
34
+ end
35
+ end
36
+
37
+ # Duplicates a has_one association from source to target.
38
+ # Public API: can be used directly for models that want to duplicate a
39
+ # specific association without including the concern, e.g.
40
+ # DuplicatesAssociations::DupOne.new(:detail).apply(source, self)
41
+ class DupOne
42
+ attr_reader :name
43
+
44
+ def initialize(name)
45
+ @name = name
46
+ end
47
+
48
+ def apply(source, target)
49
+ record = source.public_send(name)
50
+
51
+ target.public_send("#{name}=", record.dup) unless record.nil? || record.marked_for_destruction?
52
+ end
53
+ end
54
+
55
+ # Duplicates a has_many association from source to target.
56
+ # Public API: can be used directly for models that want to duplicate a
57
+ # specific association without including the concern, e.g.
58
+ # DuplicatesAssociations::DupMany.new(:notes).apply(source, self)
59
+ class DupMany
60
+ attr_reader :name
61
+
62
+ def initialize(name)
63
+ @name = name
64
+ end
65
+
66
+ def apply(source, target)
67
+ records = source.public_send(name).reject(&:marked_for_destruction?).map(&:dup)
68
+
69
+ target.public_send("#{name}=", records) if records.any?
70
+ end
71
+ end
72
+
73
+ DUPLICATORS = {
74
+ has_one: DupOne,
75
+ has_many: DupMany,
76
+ }.freeze
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Content
5
+ # Copies ActiveStorage attachments when a record is duplicated, e.g. for
6
+ # copy-on-write. Pending changes on the source are carried over: new
7
+ # uploads are copied to the duplicate, while attachments that are being
8
+ # removed (either by assigning "" as govuk_attachment_field does, or via
9
+ # nested attributes with _destroy) are dropped from the duplicate.
10
+ module DuplicatesAttachments
11
+ extend ActiveSupport::Concern
12
+
13
+ def initialize_dup(source)
14
+ super
15
+
16
+ self.class.reflect_on_all_attachments.each do |reflection|
17
+ DUPLICATORS.fetch(reflection.macro).new(reflection.name).apply(source, self)
18
+ end
19
+ end
20
+
21
+ # Duplicates a has_one_attached attachment from source to target.
22
+ # Public API: can be used directly for models that want to duplicate a
23
+ # specific attachment without including the concern, e.g.
24
+ # DuplicatesAttachments::DupOne.new(:image).apply(source, self)
25
+ class DupOne
26
+ attr_reader :name
27
+
28
+ def initialize(name)
29
+ @name = name.to_s
30
+ end
31
+
32
+ def apply(source, target)
33
+ current = source.public_send(name)
34
+ change = source.attachment_changes[name]
35
+
36
+ # if attachment has changed, duplicate the change, otherwise attach the existing blob
37
+ if change.is_a?(ActiveStorage::Attached::Changes::CreateOne)
38
+ target.public_send("#{name}=", change.attachable)
39
+ elsif change.is_a?(ActiveStorage::Attached::Changes::DeleteOne) ||
40
+ current.attachment&.marked_for_destruction?
41
+ # no-op, drop the attachment, if any
42
+ elsif current.attached?
43
+ target.public_send("#{name}=", current.blob)
44
+ end
45
+ end
46
+ end
47
+
48
+ # Duplicates has_many_attached attachments from source to target.
49
+ # Public API: can be used directly for models that want to duplicate a
50
+ # specific attachment without including the concern, e.g.
51
+ # DuplicatesAttachments::DupMany.new(:slides).apply(source, self)
52
+ class DupMany
53
+ attr_reader :name
54
+
55
+ def initialize(name)
56
+ @name = name.to_s
57
+ end
58
+
59
+ def apply(source, target)
60
+ current = source.public_send(name)
61
+ change = source.attachment_changes[name]
62
+
63
+ # if attachments have changed, duplicate the change, otherwise attach the existing blobs
64
+ if change.is_a?(ActiveStorage::Attached::Changes::CreateMany)
65
+ target.public_send("#{name}=", change.attachables)
66
+ elsif change.is_a?(ActiveStorage::Attached::Changes::DeleteMany)
67
+ # no-op, drop the attachments, if any
68
+ elsif current.attached?
69
+ blobs = current.attachments.reject(&:marked_for_destruction?).map(&:blob)
70
+ target.public_send("#{name}=", blobs) if blobs.any?
71
+ end
72
+ end
73
+ end
74
+
75
+ DUPLICATORS = {
76
+ has_one_attached: DupOne,
77
+ has_many_attached: DupMany,
78
+ }.freeze
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Content
5
+ # Copies ActionText rich text attributes when a record is duplicated,
6
+ # e.g. for copy-on-write. Unsaved changes on the source are carried over
7
+ # to the duplicate.
8
+ module DuplicatesRichText
9
+ extend ActiveSupport::Concern
10
+
11
+ def initialize_dup(source)
12
+ super
13
+
14
+ self.class.rich_text_association_names.each do |association|
15
+ DupRichText.new(association.to_s.delete_prefix("rich_text_")).apply(source, self)
16
+ end
17
+ end
18
+
19
+ # Duplicates a has_rich_text attribute from source to target.
20
+ # Public API: can be used directly for models that want to duplicate a
21
+ # specific rich text attribute without including the concern, e.g.
22
+ # DuplicatesRichText::DupRichText.new(:subtitle).apply(source, self)
23
+ class DupRichText
24
+ attr_reader :name
25
+
26
+ def initialize(name)
27
+ @name = name.to_s
28
+ end
29
+
30
+ def apply(source, target)
31
+ rich_text = source.public_send("rich_text_#{name}")
32
+
33
+ # copy via body assignment so that custom attribute writers
34
+ # (e.g. Table#content=) are not re-run on the duplicate
35
+ target.public_send(name).body = rich_text.body if rich_text
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -9,12 +9,6 @@ module Katalyst
9
9
 
10
10
  default_scope { with_rich_text_content }
11
11
 
12
- def initialize_copy(source)
13
- super
14
-
15
- self.content = source.content&.body if source.content.is_a?(ActionText::RichText)
16
- end
17
-
18
12
  def self.permitted_params
19
13
  super + %i[content]
20
14
  end
@@ -12,17 +12,6 @@ module Katalyst
12
12
 
13
13
  default_scope { with_attached_image }
14
14
 
15
- def initialize_dup(source)
16
- super
17
-
18
- # if image has changed, duplicate the change, otherwise attach the existing blob
19
- if source.attachment_changes["image"]
20
- self.image = source.attachment_changes["image"].attachable
21
- elsif source.image.attached? && !source.image.marked_for_destruction?
22
- image.attach(source.image.blob)
23
- end
24
- end
25
-
26
15
  def self.permitted_params
27
16
  super - %i[heading_style] + %i[image caption]
28
17
  end
@@ -4,6 +4,9 @@ module Katalyst
4
4
  module Content
5
5
  # STI base class for content items
6
6
  class Item < ApplicationRecord
7
+ include DuplicatesAssociations
8
+ include DuplicatesAttachments
9
+ include DuplicatesRichText
7
10
  include HasStyle
8
11
 
9
12
  def self.config
@@ -14,20 +14,10 @@ module Katalyst
14
14
 
15
15
  after_initialize :set_defaults
16
16
 
17
- def initialize_copy(source)
18
- super
19
-
20
- content.body = source.content&.body if source.content.is_a?(ActionText::RichText)
21
- end
22
-
23
17
  def self.permitted_params
24
18
  super + %i[content heading_rows heading_columns]
25
19
  end
26
20
 
27
- def valid?(context = nil)
28
- super
29
- end
30
-
31
21
  def to_plain_text
32
22
  content.to_plain_text if visible?
33
23
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: katalyst-content
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.2
4
+ version: 3.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Katalyst Interactive
@@ -153,6 +153,9 @@ files:
153
153
  - app/javascript/content/editor/status_bar_controller.js
154
154
  - app/javascript/content/editor/table_controller.js
155
155
  - app/models/concerns/katalyst/content/container.rb
156
+ - app/models/concerns/katalyst/content/duplicates_associations.rb
157
+ - app/models/concerns/katalyst/content/duplicates_attachments.rb
158
+ - app/models/concerns/katalyst/content/duplicates_rich_text.rb
156
159
  - app/models/concerns/katalyst/content/garbage_collection.rb
157
160
  - app/models/concerns/katalyst/content/has_style.rb
158
161
  - app/models/concerns/katalyst/content/has_tree.rb