decidim-collaborative_texts 0.31.0.rc1
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 +7 -0
- data/README.md +24 -0
- data/Rakefile +3 -0
- data/app/cells/decidim/collaborative_texts/document_cell.rb +23 -0
- data/app/cells/decidim/collaborative_texts/document_l_cell.rb +15 -0
- data/app/commands/decidim/collaborative_texts/admin/create_document.rb +25 -0
- data/app/commands/decidim/collaborative_texts/admin/publish_document.rb +52 -0
- data/app/commands/decidim/collaborative_texts/admin/unpublish_document.rb +44 -0
- data/app/commands/decidim/collaborative_texts/admin/update_document.rb +101 -0
- data/app/commands/decidim/collaborative_texts/admin/update_document_settings.rb +13 -0
- data/app/commands/decidim/collaborative_texts/create_suggestion.rb +28 -0
- data/app/commands/decidim/collaborative_texts/rollout.rb +50 -0
- data/app/controllers/concerns/decidim/collaborative_texts/admin/filterable.rb +23 -0
- data/app/controllers/decidim/collaborative_texts/admin/application_controller.rb +15 -0
- data/app/controllers/decidim/collaborative_texts/admin/documents_controller.rb +142 -0
- data/app/controllers/decidim/collaborative_texts/application_controller.rb +14 -0
- data/app/controllers/decidim/collaborative_texts/documents_controller.rb +57 -0
- data/app/controllers/decidim/collaborative_texts/suggestions_controller.rb +55 -0
- data/app/events/decidim/collaborative_texts/suggestion_accepted_event.rb +6 -0
- data/app/forms/decidim/collaborative_texts/admin/document_form.rb +29 -0
- data/app/forms/decidim/collaborative_texts/rollout_form.rb +26 -0
- data/app/forms/decidim/collaborative_texts/suggestion_form.rb +42 -0
- data/app/helpers/decidim/collaborative_texts/application_helper.rb +20 -0
- data/app/models/decidim/collaborative_texts/application_record.rb +10 -0
- data/app/models/decidim/collaborative_texts/document.rb +78 -0
- data/app/models/decidim/collaborative_texts/suggestion.rb +36 -0
- data/app/models/decidim/collaborative_texts/version.rb +35 -0
- data/app/packs/entrypoints/decidim_collaborative_texts.js +7 -0
- data/app/packs/images/decidim/collaborative_texts/decidim_collaborative_texts.svg +1 -0
- data/app/packs/src/decidim/collaborative_texts/document.js +168 -0
- data/app/packs/src/decidim/collaborative_texts/editor.js +80 -0
- data/app/packs/src/decidim/collaborative_texts/init_documents.js +27 -0
- data/app/packs/src/decidim/collaborative_texts/manager.js +106 -0
- data/app/packs/src/decidim/collaborative_texts/selection.js +106 -0
- data/app/packs/src/decidim/collaborative_texts/suggestion.js +243 -0
- data/app/packs/src/decidim/collaborative_texts/suggestions_list.js +103 -0
- data/app/packs/src/decidim/collaborative_texts/test/document.test.js +83 -0
- data/app/packs/src/decidim/collaborative_texts/test/manager.test.js +149 -0
- data/app/packs/src/decidim/collaborative_texts/test/selection.test.js +125 -0
- data/app/packs/src/decidim/collaborative_texts/test/suggestions.test.js +233 -0
- data/app/packs/src/decidim/collaborative_texts/test/toc.test.js +70 -0
- data/app/packs/src/decidim/collaborative_texts/toc.js +48 -0
- data/app/packs/stylesheets/decidim/collaborative_texts/collaborative_texts.scss +287 -0
- data/app/permissions/decidim/collaborative_texts/admin/permissions.rb +28 -0
- data/app/permissions/decidim/collaborative_texts/permissions.rb +36 -0
- data/app/presenters/decidim/collaborative_texts/admin_log/document_presenter.rb +54 -0
- data/app/presenters/decidim/collaborative_texts/admin_log/suggestion_presenter.rb +62 -0
- data/app/presenters/decidim/collaborative_texts/admin_log/suggestion_resource_presenter.rb +20 -0
- data/app/presenters/decidim/collaborative_texts/admin_log/version_presenter.rb +53 -0
- data/app/presenters/decidim/collaborative_texts/official_author_presenter.rb +11 -0
- data/app/presenters/decidim/collaborative_texts/suggestion_presenter.rb +57 -0
- data/app/views/decidim/collaborative_texts/admin/documents/_actions.html.erb +82 -0
- data/app/views/decidim/collaborative_texts/admin/documents/_document-tr.html.erb +15 -0
- data/app/views/decidim/collaborative_texts/admin/documents/_documents-thead.html.erb +7 -0
- data/app/views/decidim/collaborative_texts/admin/documents/_draft_options.html.erb +6 -0
- data/app/views/decidim/collaborative_texts/admin/documents/_form.html.erb +16 -0
- data/app/views/decidim/collaborative_texts/admin/documents/_non_draft_options.html.erb +9 -0
- data/app/views/decidim/collaborative_texts/admin/documents/_versions.html.erb +18 -0
- data/app/views/decidim/collaborative_texts/admin/documents/edit.html.erb +33 -0
- data/app/views/decidim/collaborative_texts/admin/documents/edit_settings.html.erb +18 -0
- data/app/views/decidim/collaborative_texts/admin/documents/index.html.erb +32 -0
- data/app/views/decidim/collaborative_texts/admin/documents/manage_trash.html.erb +19 -0
- data/app/views/decidim/collaborative_texts/admin/documents/new.html.erb +18 -0
- data/app/views/decidim/collaborative_texts/admin/settings/_form.html.erb +9 -0
- data/app/views/decidim/collaborative_texts/documents/_editor_template.html.erb +15 -0
- data/app/views/decidim/collaborative_texts/documents/_manager.html.erb +9 -0
- data/app/views/decidim/collaborative_texts/documents/_suggestions_box_item_template.html.erb +33 -0
- data/app/views/decidim/collaborative_texts/documents/_suggestions_box_template.html.erb +20 -0
- data/app/views/decidim/collaborative_texts/documents/index.html.erb +29 -0
- data/app/views/decidim/collaborative_texts/documents/show.html.erb +70 -0
- data/config/assets.rb +8 -0
- data/config/locales/am-ET.yml +1 -0
- data/config/locales/ar.yml +1 -0
- data/config/locales/bg.yml +1 -0
- data/config/locales/bn-BD.yml +1 -0
- data/config/locales/bs-BA.yml +1 -0
- data/config/locales/ca-IT.yml +154 -0
- data/config/locales/ca.yml +154 -0
- data/config/locales/cs.yml +122 -0
- data/config/locales/da.yml +1 -0
- data/config/locales/de.yml +154 -0
- data/config/locales/el.yml +1 -0
- data/config/locales/en.yml +154 -0
- data/config/locales/eo.yml +1 -0
- data/config/locales/es-MX.yml +154 -0
- data/config/locales/es-PY.yml +154 -0
- data/config/locales/es.yml +154 -0
- data/config/locales/et.yml +1 -0
- data/config/locales/eu.yml +154 -0
- data/config/locales/fa-IR.yml +1 -0
- data/config/locales/fi-plain.yml +154 -0
- data/config/locales/fi.yml +154 -0
- data/config/locales/fr-CA.yml +125 -0
- data/config/locales/fr.yml +125 -0
- data/config/locales/ga-IE.yml +1 -0
- data/config/locales/gl.yml +1 -0
- data/config/locales/gn-PY.yml +1 -0
- data/config/locales/he-IL.yml +1 -0
- data/config/locales/hr.yml +1 -0
- data/config/locales/hu.yml +1 -0
- data/config/locales/id-ID.yml +1 -0
- data/config/locales/is-IS.yml +1 -0
- data/config/locales/it.yml +1 -0
- data/config/locales/ja.yml +153 -0
- data/config/locales/ka-GE.yml +1 -0
- data/config/locales/kaa.yml +1 -0
- data/config/locales/ko.yml +1 -0
- data/config/locales/lb.yml +1 -0
- data/config/locales/lo-LA.yml +1 -0
- data/config/locales/lt.yml +1 -0
- data/config/locales/lv.yml +1 -0
- data/config/locales/mt.yml +1 -0
- data/config/locales/nl.yml +1 -0
- data/config/locales/no.yml +6 -0
- data/config/locales/oc-FR.yml +1 -0
- data/config/locales/om-ET.yml +1 -0
- data/config/locales/pl.yml +1 -0
- data/config/locales/pt-BR.yml +1 -0
- data/config/locales/pt.yml +1 -0
- data/config/locales/ro-RO.yml +89 -0
- data/config/locales/ru.yml +1 -0
- data/config/locales/si-LK.yml +1 -0
- data/config/locales/sk.yml +1 -0
- data/config/locales/sl.yml +1 -0
- data/config/locales/so-SO.yml +1 -0
- data/config/locales/sq-AL.yml +1 -0
- data/config/locales/sr-CS.yml +1 -0
- data/config/locales/sv.yml +117 -0
- data/config/locales/sw-KE.yml +1 -0
- data/config/locales/th-TH.yml +1 -0
- data/config/locales/ti-ER.yml +1 -0
- data/config/locales/tr-TR.yml +69 -0
- data/config/locales/uk.yml +1 -0
- data/config/locales/val-ES.yml +1 -0
- data/config/locales/vi.yml +1 -0
- data/config/locales/zh-CN.yml +1 -0
- data/config/locales/zh-TW.yml +1 -0
- data/db/migrate/20250205215038_create_decidim_collaborative_texts_documents.rb +16 -0
- data/db/migrate/20250213113536_create_collaborative_texts_versions.rb +13 -0
- data/db/migrate/20250227204839_create_collaborative_texts_suggestions.rb +13 -0
- data/db/migrate/20250312140133_add_counter_caches_to_collaborative_texts_documents.rb +13 -0
- data/db/migrate/20250408205231_add_counter_caches_to_collaborative_text_versions.rb +8 -0
- data/decidim-collaborative_texts.gemspec +36 -0
- data/lib/decidim/api/document_input_filter.rb +29 -0
- data/lib/decidim/api/document_input_sort.rb +14 -0
- data/lib/decidim/api/document_type.rb +31 -0
- data/lib/decidim/api/documents_type.rb +39 -0
- data/lib/decidim/api/suggestion_type.rb +18 -0
- data/lib/decidim/api/version_type.rb +21 -0
- data/lib/decidim/collaborative_texts/admin.rb +10 -0
- data/lib/decidim/collaborative_texts/admin_engine.rb +34 -0
- data/lib/decidim/collaborative_texts/api.rb +12 -0
- data/lib/decidim/collaborative_texts/component.rb +54 -0
- data/lib/decidim/collaborative_texts/engine.rb +28 -0
- data/lib/decidim/collaborative_texts/seeds.rb +117 -0
- data/lib/decidim/collaborative_texts/test/factories.rb +80 -0
- data/lib/decidim/collaborative_texts/version.rb +9 -0
- data/lib/decidim/collaborative_texts.rb +13 -0
- metadata +233 -0
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module CollaborativeTexts
|
5
|
+
class SuggestionsController < Decidim::CollaborativeTexts::ApplicationController
|
6
|
+
include Decidim::FormFactory
|
7
|
+
include Decidim::AjaxPermissionHandler
|
8
|
+
|
9
|
+
helper_method :documents, :document
|
10
|
+
|
11
|
+
def index
|
12
|
+
render json: suggestions_for(document.consolidated_version)
|
13
|
+
end
|
14
|
+
|
15
|
+
def create
|
16
|
+
enforce_permission_to :suggest, :collaborative_text
|
17
|
+
@form = form(SuggestionForm).from_params(params)
|
18
|
+
|
19
|
+
CreateSuggestion.call(@form) do
|
20
|
+
on(:ok) do
|
21
|
+
render json: { message: I18n.t("suggestions.create.success", scope: "decidim.collaborative_texts") }
|
22
|
+
end
|
23
|
+
|
24
|
+
on(:invalid) do
|
25
|
+
message = [I18n.t("suggestions.create.invalid", scope: "decidim.collaborative_texts")]
|
26
|
+
message.push(@form.errors.full_messages.join(", ")) if @form.errors.any?
|
27
|
+
render json: { message: message.join(" ") }, status: :unprocessable_entity
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def suggestions_for(version)
|
35
|
+
version.suggestions.map do |suggestion|
|
36
|
+
suggestion.presenter.safe_json.merge(
|
37
|
+
profileHtml: cell("decidim/author", suggestion.author.presenter).to_s.strip
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def document
|
43
|
+
@document ||= documents.find(params[:document_id])
|
44
|
+
end
|
45
|
+
|
46
|
+
def documents
|
47
|
+
@documents ||= if current_user&.admin?
|
48
|
+
Document.where(component: current_component)
|
49
|
+
else
|
50
|
+
Document.published.where(component: current_component)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module CollaborativeTexts
|
5
|
+
module Admin
|
6
|
+
# This class holds a Form to create/update documents from Decidim's admin panel.
|
7
|
+
class DocumentForm < Decidim::Form
|
8
|
+
include Decidim::TranslatableAttributes
|
9
|
+
include Decidim::ApplicationHelper
|
10
|
+
|
11
|
+
mimic :document
|
12
|
+
|
13
|
+
attribute :title, String
|
14
|
+
attribute :body, Decidim::Attributes::RichText
|
15
|
+
attribute :accepting_suggestions, Boolean, default: false
|
16
|
+
attribute :draft, Boolean, default: false
|
17
|
+
translatable_attribute :announcement, Decidim::Attributes::RichText
|
18
|
+
|
19
|
+
validates :title, presence: true, etiquette: true, unless: ->(form) { form.title.nil? }
|
20
|
+
validates :body, presence: true, unless: ->(form) { form.body.nil? } # no etiquette validation for body as it might trigger false positives
|
21
|
+
|
22
|
+
def coauthorships
|
23
|
+
# ensures the first author is always the organization in case "official?" is ever used
|
24
|
+
[Decidim::Coauthorship.new(author: current_organization)]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module CollaborativeTexts
|
5
|
+
class RolloutForm < Decidim::Form
|
6
|
+
attribute :body, Decidim::Attributes::RichText
|
7
|
+
attribute :draft, Boolean, default: true
|
8
|
+
attribute :accepted, Array[Integer], default: []
|
9
|
+
attribute :pending, Array[Integer], default: []
|
10
|
+
|
11
|
+
validates :body, presence: true
|
12
|
+
|
13
|
+
def document
|
14
|
+
@document ||= context[:document]
|
15
|
+
end
|
16
|
+
|
17
|
+
def accepted_suggestions
|
18
|
+
@accepted_suggestions ||= document.suggestions.where(id: accepted)
|
19
|
+
end
|
20
|
+
|
21
|
+
def pending_suggestions
|
22
|
+
@pending_suggestions ||= document.suggestions.where(id: pending)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module CollaborativeTexts
|
5
|
+
class SuggestionForm < Decidim::Form
|
6
|
+
include JsonbAttributes
|
7
|
+
|
8
|
+
mimic :document
|
9
|
+
|
10
|
+
jsonb_attribute :changeset, [
|
11
|
+
[:original, Array[String]],
|
12
|
+
[:replace, Array[String]],
|
13
|
+
[:firstNode, Integer],
|
14
|
+
[:lastNode, Integer]
|
15
|
+
]
|
16
|
+
attribute :status, String, default: "pending"
|
17
|
+
attribute :document_id, Integer
|
18
|
+
|
19
|
+
validate :changes_exists
|
20
|
+
|
21
|
+
alias author current_user
|
22
|
+
|
23
|
+
def document
|
24
|
+
@document ||= ::Decidim::CollaborativeTexts::Document.find(document_id)
|
25
|
+
end
|
26
|
+
|
27
|
+
def document_version
|
28
|
+
@document_version ||= document.current_version
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def changes_exists
|
34
|
+
errors.add(:base, I18n.t("errors.blank_changeset", scope: "decidim.collaborative_texts.suggestions")) if changeset.blank?
|
35
|
+
if changeset["firstNode"].to_i.zero? || changeset["lastNode"].to_i.zero?
|
36
|
+
errors.add(:base,
|
37
|
+
I18n.t("errors.invalid_nodes", scope: "decidim.collaborative_texts.suggestions"))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module CollaborativeTexts
|
5
|
+
# Custom helpers, scoped to the collaborative texts engine.
|
6
|
+
module ApplicationHelper
|
7
|
+
def component_name
|
8
|
+
(defined?(current_component) && translated_attribute(current_component&.name).presence) || t("decidim.components.collaborative_texts.name")
|
9
|
+
end
|
10
|
+
|
11
|
+
def document_i18n
|
12
|
+
{
|
13
|
+
selectionActive: t("decidim.collaborative_texts.document.status.selection_active"),
|
14
|
+
rolloutConfirm: t("decidim.collaborative_texts.document.rollout.confirm"),
|
15
|
+
consolidateConfirm: t("decidim.collaborative_texts.document.consolidate.confirm")
|
16
|
+
}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module CollaborativeTexts
|
5
|
+
# The data store for a document in the Decidim::CollaborativeTexts component. It stores a
|
6
|
+
# title, description and any other useful information to render a custom
|
7
|
+
# document.
|
8
|
+
class Document < CollaborativeTexts::ApplicationRecord
|
9
|
+
include Decidim::Resourceable
|
10
|
+
include Decidim::SoftDeletable
|
11
|
+
include Decidim::HasComponent
|
12
|
+
include Decidim::Publicable
|
13
|
+
include Decidim::Traceable
|
14
|
+
include Decidim::Loggable
|
15
|
+
include Decidim::Searchable
|
16
|
+
include Decidim::Coauthorable
|
17
|
+
|
18
|
+
component_manifest_name "collaborative_texts"
|
19
|
+
|
20
|
+
after_save :save_version
|
21
|
+
|
22
|
+
has_many :document_versions, class_name: "Decidim::CollaborativeTexts::Version", inverse_of: :document, dependent: :destroy
|
23
|
+
has_many :suggestions, through: :document_versions
|
24
|
+
|
25
|
+
validates :title, presence: true
|
26
|
+
|
27
|
+
scope :enabled_desc, -> { order(arel_table[:accepting_suggestions].desc, arel_table[:created_at].desc) }
|
28
|
+
|
29
|
+
delegate :organization, :participatory_space, to: :component
|
30
|
+
delegate :draft?, :draft, :draft=, :body, :body=, to: :current_version
|
31
|
+
|
32
|
+
searchable_fields(
|
33
|
+
participatory_space: { component: :participatory_space },
|
34
|
+
A: :title,
|
35
|
+
D: :consolidated_body,
|
36
|
+
datetime: :published_at
|
37
|
+
)
|
38
|
+
|
39
|
+
def self.log_presenter_class_for(_log)
|
40
|
+
Decidim::CollaborativeTexts::AdminLog::DocumentPresenter
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns the current version of the document. Currently, the last one
|
44
|
+
def current_version
|
45
|
+
document_versions.last || document_versions.build
|
46
|
+
end
|
47
|
+
|
48
|
+
def consolidated_version
|
49
|
+
document_versions.consolidated.last
|
50
|
+
end
|
51
|
+
|
52
|
+
def consolidated_body
|
53
|
+
consolidated_version&.body
|
54
|
+
end
|
55
|
+
|
56
|
+
# The paranoia gem (used in soft-delete) applies the removed status to the "document_versions" association
|
57
|
+
# but it does not recursively restore them by default.
|
58
|
+
# This model needs to have the document_versions synchronized always
|
59
|
+
def restore
|
60
|
+
super(recursive: true)
|
61
|
+
end
|
62
|
+
|
63
|
+
def has_suggestions?
|
64
|
+
current_version.suggestions.any?
|
65
|
+
end
|
66
|
+
|
67
|
+
def suggestions_enabled?
|
68
|
+
published? && accepting_suggestions? && !draft?
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def save_version
|
74
|
+
current_version.save if current_version.changed?
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module CollaborativeTexts
|
5
|
+
# The data store for a document in the Decidim::CollaborativeTexts component. It stores a
|
6
|
+
# title, description and any other useful information to render a custom
|
7
|
+
# document.
|
8
|
+
class Suggestion < CollaborativeTexts::ApplicationRecord
|
9
|
+
include Decidim::Traceable
|
10
|
+
include Decidim::Authorable
|
11
|
+
|
12
|
+
after_save :update_document_counters
|
13
|
+
|
14
|
+
enum :status, [:pending, :accepted, :rejected]
|
15
|
+
belongs_to :document_version, class_name: "Decidim::CollaborativeTexts::Version", counter_cache: true, inverse_of: :suggestions
|
16
|
+
has_one :document, class_name: "Decidim::CollaborativeTexts::Document", through: :document_version
|
17
|
+
|
18
|
+
delegate :participatory_space, :organization, to: :document_version
|
19
|
+
|
20
|
+
def self.log_presenter_class_for(_log)
|
21
|
+
Decidim::CollaborativeTexts::AdminLog::SuggestionPresenter
|
22
|
+
end
|
23
|
+
|
24
|
+
def presenter
|
25
|
+
@presenter ||= Decidim::CollaborativeTexts::SuggestionPresenter.new(self)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def update_document_counters
|
31
|
+
# Increment the counter cache for the document
|
32
|
+
document.update_column(:suggestions_count, document.suggestions.count) # rubocop:disable Rails/SkipsModelValidations
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module CollaborativeTexts
|
5
|
+
# The data store for a document in the Decidim::CollaborativeTexts component. It stores a
|
6
|
+
# title, description and any other useful information to render a custom
|
7
|
+
# document.
|
8
|
+
class Version < CollaborativeTexts::ApplicationRecord
|
9
|
+
include Decidim::Resourceable
|
10
|
+
include Decidim::SoftDeletable
|
11
|
+
include Decidim::Traceable
|
12
|
+
include Decidim::Loggable
|
13
|
+
|
14
|
+
belongs_to :document, class_name: "Decidim::CollaborativeTexts::Document", counter_cache: :document_versions_count, inverse_of: :document_versions
|
15
|
+
has_many :suggestions, class_name: "Decidim::CollaborativeTexts::Suggestion", foreign_key: "document_version_id", dependent: :destroy
|
16
|
+
|
17
|
+
validates :body, presence: true
|
18
|
+
validates :draft, presence: true, uniqueness: { scope: :document_id }, if: :draft
|
19
|
+
|
20
|
+
default_scope { order(created_at: :asc) }
|
21
|
+
scope :consolidated, -> { where(draft: false) }
|
22
|
+
scope :draft, -> { where(draft: true) }
|
23
|
+
|
24
|
+
delegate :participatory_space, :organization, to: :document
|
25
|
+
|
26
|
+
def self.log_presenter_class_for(_log)
|
27
|
+
Decidim::CollaborativeTexts::AdminLog::VersionPresenter
|
28
|
+
end
|
29
|
+
|
30
|
+
def version_number
|
31
|
+
@version_number ||= document.document_versions.where(created_at: ..created_at).count
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 2C20.5523 2 21 2.44772 21 3V6.757L19 8.757V4H5V20H19V17.242L21 15.242V21C21 21.5523 20.5523 22 20 22H4C3.44772 22 3 21.5523 3 21V3C3 2.44772 3.44772 2 4 2H20ZM21.7782 8.80761L23.1924 10.2218L15.4142 18L13.9979 17.9979L14 16.5858L21.7782 8.80761ZM13 12V14H8V12H13ZM16 8V10H8V8H16Z"></path></svg>
|
@@ -0,0 +1,168 @@
|
|
1
|
+
import Selection from "src/decidim/collaborative_texts/selection";
|
2
|
+
import SuggestionsList from "src/decidim/collaborative_texts/suggestions_list";
|
3
|
+
import Manager from "src/decidim/collaborative_texts/manager";
|
4
|
+
|
5
|
+
export default class Document {
|
6
|
+
constructor(doc) {
|
7
|
+
this.doc = doc;
|
8
|
+
this.selecting = false;
|
9
|
+
this.applying = false;
|
10
|
+
this.active = false;
|
11
|
+
this.suggestionsList = null;
|
12
|
+
this.templates = {
|
13
|
+
suggestionsEditor: window.document.querySelector(this.doc.dataset.collaborativeTextsSuggestionsEditorTemplate),
|
14
|
+
suggestionsBox: window.document.querySelector(this.doc.dataset.collaborativeTextsSuggestionsBoxTemplate),
|
15
|
+
suggestionsBoxItem: window.document.querySelector(this.doc.dataset.collaborativeTextsSuggestionsBoxItemTemplate)
|
16
|
+
}
|
17
|
+
try {
|
18
|
+
this.active = JSON.parse(this.doc.dataset.collaborativeTextsDocument);
|
19
|
+
} catch (_e) {
|
20
|
+
console.error("Error parsing collaborativeTextsDocument", this.doc.dataset.collaborativeTextsDocument);
|
21
|
+
}
|
22
|
+
this.i18n = {};
|
23
|
+
try {
|
24
|
+
this.i18n = JSON.parse(this.doc.dataset.collaborativeTextsI18n);
|
25
|
+
} catch (_e) {
|
26
|
+
console.error("Error parsing collaborativeTextsI18n", this.doc.dataset.collaborativeTextsI18n);
|
27
|
+
}
|
28
|
+
this.csrfToken = document.querySelector("meta[name=csrf-token]") && document.querySelector('meta[name="csrf-token"]').getAttribute("content");
|
29
|
+
this.alertWrapper = window.document.querySelector(".collaborative-texts-alert");
|
30
|
+
this.alertDiv = this.alertWrapper.querySelector("div");
|
31
|
+
this._prepareNodes();
|
32
|
+
this._bindManager();
|
33
|
+
}
|
34
|
+
|
35
|
+
// listen to new selections and allows the user to participate in the collaborative text
|
36
|
+
enableSuggestions() {
|
37
|
+
window.document.addEventListener("selectstart", this._onSelectionStart.bind(this));
|
38
|
+
window.document.addEventListener("mouseup", this._onSelectionEnd.bind(this));
|
39
|
+
return this;
|
40
|
+
}
|
41
|
+
|
42
|
+
// fetches suggestions from the server and updates the UI with the wraps
|
43
|
+
fetchSuggestions() {
|
44
|
+
this.suggestionsList = new SuggestionsList(this);
|
45
|
+
return this;
|
46
|
+
}
|
47
|
+
|
48
|
+
// show an alert message for 5 seconds
|
49
|
+
alert(message) {
|
50
|
+
this.alertDiv.textContent = message;
|
51
|
+
this.alertWrapper.classList.remove("hidden");
|
52
|
+
setTimeout(() => {
|
53
|
+
this.alertWrapper.classList.add("hidden");
|
54
|
+
}, 5000);
|
55
|
+
}
|
56
|
+
|
57
|
+
// bind the manager to the document
|
58
|
+
_bindManager() {
|
59
|
+
this.doc.addEventListener("collaborative-texts:applied", this._onApply.bind(this));
|
60
|
+
this.doc.addEventListener("collaborative-texts:restored", this._onRestore.bind(this));
|
61
|
+
this.doc.addEventListener("collaborative-texts:suggest", this._onSuggest.bind(this));
|
62
|
+
}
|
63
|
+
|
64
|
+
// For all first level nodes of type ELEMENT_NODE, ensure they have a unique id if they do not have one starting with "ct-node-"
|
65
|
+
_prepareNodes() {
|
66
|
+
this.nodes = [];
|
67
|
+
[...this.doc.childNodes].forEach((node) => {
|
68
|
+
if (node.nodeType !== Node.ELEMENT_NODE) {
|
69
|
+
return;
|
70
|
+
}
|
71
|
+
if (!node.id || !node.id.startsWith("ct-node-")) {
|
72
|
+
node.id = `ct-node-${this.nodes.length + 1}`;
|
73
|
+
}
|
74
|
+
this.nodes.push(node);
|
75
|
+
});
|
76
|
+
}
|
77
|
+
|
78
|
+
_onSelectionStart() {
|
79
|
+
this.selecting = true;
|
80
|
+
}
|
81
|
+
|
82
|
+
_onSelectionEnd() {
|
83
|
+
if (window.document.getSelection().toString() === "") {
|
84
|
+
this.selecting = false;
|
85
|
+
return;
|
86
|
+
}
|
87
|
+
if (!this.selecting) {
|
88
|
+
return;
|
89
|
+
}
|
90
|
+
this.selecting = false;
|
91
|
+
this._showEditable();
|
92
|
+
}
|
93
|
+
|
94
|
+
_showEditable() {
|
95
|
+
this.selection = this.selection || new Selection(this);
|
96
|
+
if (this.applying) {
|
97
|
+
return;
|
98
|
+
}
|
99
|
+
if (this.selection.isEditing()) {
|
100
|
+
this.alert(this.i18n.selectionActive);
|
101
|
+
this.selection.scrollIntoView();
|
102
|
+
return;
|
103
|
+
}
|
104
|
+
if (this.selection.isValid()) {
|
105
|
+
this.selection.clear().wrap().showEditor();
|
106
|
+
}
|
107
|
+
}
|
108
|
+
|
109
|
+
_onApply() {
|
110
|
+
if (!this.doc.dataset.collaborativeTextsRolloutUrl) {
|
111
|
+
return;
|
112
|
+
}
|
113
|
+
this.applying = true;
|
114
|
+
this.manager = this.manager || new Manager(this);
|
115
|
+
this.manager.show();
|
116
|
+
this.manager.updateCounters(this.suggestionsList.getApplied().length, this.suggestionsList.getPending().length);
|
117
|
+
}
|
118
|
+
|
119
|
+
_onRestore() {
|
120
|
+
if (!this.suggestionsList.suggestions.find((suggestion) => suggestion.applied)) {
|
121
|
+
this.applying = false;
|
122
|
+
this.manager.hide();
|
123
|
+
}
|
124
|
+
}
|
125
|
+
|
126
|
+
_sanitizeNodes(nodes) {
|
127
|
+
return [...nodes].filter((node) => node).map((node) => (node.nodeType === Node.TEXT_NODE
|
128
|
+
? node.textContent
|
129
|
+
: node.outerHTML));
|
130
|
+
}
|
131
|
+
|
132
|
+
_onSuggest(event) {
|
133
|
+
let original = this._sanitizeNodes(event.detail.nodes);
|
134
|
+
let replace = this._sanitizeNodes(event.detail.replaceNodes);
|
135
|
+
fetch(this.doc.dataset.collaborativeTextsSuggestionsUrl, {
|
136
|
+
method: "POST",
|
137
|
+
headers: {
|
138
|
+
"Content-Type": "application/json",
|
139
|
+
"X-Requested-With": "XMLHttpRequest",
|
140
|
+
"X-CSRF-Token": this.csrfToken
|
141
|
+
},
|
142
|
+
body: JSON.stringify({
|
143
|
+
firstNode: event.detail.firstNode.id.replace(/^ct-node-/, ""),
|
144
|
+
lastNode: event.detail.lastNode.id.replace(/^ct-node-/, ""),
|
145
|
+
original: original,
|
146
|
+
replace: replace
|
147
|
+
})
|
148
|
+
}).
|
149
|
+
then(async (response) => {
|
150
|
+
const data = await response.json();
|
151
|
+
if (!response.ok) {
|
152
|
+
throw new Error(data.message
|
153
|
+
? data.message
|
154
|
+
: data);
|
155
|
+
}
|
156
|
+
|
157
|
+
if (this.suggestionsList) {
|
158
|
+
this.suggestionsList.destroy();
|
159
|
+
}
|
160
|
+
this.fetchSuggestions();
|
161
|
+
}).
|
162
|
+
catch((error) => {
|
163
|
+
console.error("Error sending suggestion:", error);
|
164
|
+
this.alert(error);
|
165
|
+
});
|
166
|
+
}
|
167
|
+
}
|
168
|
+
|
@@ -0,0 +1,80 @@
|
|
1
|
+
export default class Editor {
|
2
|
+
constructor(selection) {
|
3
|
+
this.selection = selection;
|
4
|
+
this.templates = selection.doc.templates;
|
5
|
+
this.doc = selection.doc.doc;
|
6
|
+
this.wrapper = selection.wrapper;
|
7
|
+
this.nodes = selection.nodes;
|
8
|
+
this.firstNode = selection.firstNode;
|
9
|
+
this.lastNode = selection.lastNode;
|
10
|
+
this.editor = null;
|
11
|
+
this.container = null;
|
12
|
+
this._createEditor();
|
13
|
+
this._setupContainer();
|
14
|
+
}
|
15
|
+
|
16
|
+
destroy() {
|
17
|
+
if (!this.editor) {
|
18
|
+
return;
|
19
|
+
}
|
20
|
+
this.editor.remove();
|
21
|
+
this.editor = null;
|
22
|
+
}
|
23
|
+
|
24
|
+
_createEditor() {
|
25
|
+
this.editor = window.document.createElement("div");
|
26
|
+
this.editor.classList.add("collaborative-texts-editor");
|
27
|
+
this.editor.innerHTML = this.templates.suggestionsEditor.innerHTML;
|
28
|
+
this.saveButton = this.editor.querySelector(".collaborative-texts-button-save");
|
29
|
+
this.cancelButton = this.editor.querySelector(".collaborative-texts-button-cancel");
|
30
|
+
this.editor.addEventListener("keydown", (event) => {
|
31
|
+
if (event.ctrlKey && event.key === "Enter") {
|
32
|
+
event.preventDefault();
|
33
|
+
this._save();
|
34
|
+
}
|
35
|
+
});
|
36
|
+
this.saveButton.addEventListener("click", this._save.bind(this));
|
37
|
+
this.cancelButton.addEventListener("click", this._cancel.bind(this));
|
38
|
+
this.wrapper.after(this.editor);
|
39
|
+
}
|
40
|
+
|
41
|
+
_setupContainer() {
|
42
|
+
// This in the future should be the tiptap editor
|
43
|
+
this.container = this.editor.querySelector(".collaborative-texts-editor-container");
|
44
|
+
this.container.innerHTML = this.nodes.map((node) => node.outerHTML).join("");
|
45
|
+
this.originalHtml = this.container.innerHTML;
|
46
|
+
this.container.contentEditable = true;
|
47
|
+
this.container.addEventListener("input", this._change.bind(this));
|
48
|
+
this.container.addEventListener("focusout", this._change.bind(this));
|
49
|
+
this.container.focus();
|
50
|
+
}
|
51
|
+
|
52
|
+
_change() {
|
53
|
+
const newHtml = this.container.innerHTML;
|
54
|
+
if (newHtml === this.originalHtml) {
|
55
|
+
this.saveButton.classList.add("disabled");
|
56
|
+
this.saveButton.disabled = true;
|
57
|
+
} else {
|
58
|
+
this.saveButton.classList.remove("disabled");
|
59
|
+
this.saveButton.disabled = false;
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
_save() {
|
64
|
+
const event = new CustomEvent("collaborative-texts:suggest", {
|
65
|
+
detail: {
|
66
|
+
nodes: this.nodes,
|
67
|
+
firstNode: this.firstNode,
|
68
|
+
lastNode: this.lastNode,
|
69
|
+
replaceNodes: this.container.childNodes
|
70
|
+
}
|
71
|
+
});
|
72
|
+
this.doc.dispatchEvent(event);
|
73
|
+
this.selection.clear();
|
74
|
+
}
|
75
|
+
|
76
|
+
_cancel() {
|
77
|
+
this.selection.clear();
|
78
|
+
}
|
79
|
+
}
|
80
|
+
|
@@ -0,0 +1,27 @@
|
|
1
|
+
import Document from "src/decidim/collaborative_texts/document";
|
2
|
+
import Toc from "src/decidim/collaborative_texts/toc";
|
3
|
+
|
4
|
+
window.CollaborativeTextsDocuments = window.CollaborativeTextsDocuments || {};
|
5
|
+
window.CollaborativeTextsToc = window.CollaborativeTextsToc || {};
|
6
|
+
document.addEventListener("turbo:load", () => {
|
7
|
+
const documents = window.document.querySelectorAll("[data-collaborative-texts-document]");
|
8
|
+
const tableOfContents = window.document.querySelectorAll("[data-collaborative-texts-toc]");
|
9
|
+
documents.forEach((doc) => {
|
10
|
+
let document = new Document(doc);
|
11
|
+
document.fetchSuggestions();
|
12
|
+
if (document.active) {
|
13
|
+
document.enableSuggestions();
|
14
|
+
}
|
15
|
+
|
16
|
+
window.CollaborativeTextsDocuments[doc.id] = document;
|
17
|
+
});
|
18
|
+
|
19
|
+
tableOfContents.forEach((tocEl) => {
|
20
|
+
let document = window.CollaborativeTextsDocuments[tocEl.dataset.collaborativeTextsToc];
|
21
|
+
if (document) {
|
22
|
+
let toc = new Toc(tocEl, document.doc);
|
23
|
+
toc.render();
|
24
|
+
window.CollaborativeTextsToc[tocEl.id] = toc;
|
25
|
+
}
|
26
|
+
});
|
27
|
+
});
|