Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Post Templates #1151

Open
wants to merge 28 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9ed3ffe
Implement creating and using post templates
MoshiKoi Aug 1, 2023
cfeebc9
Don't show template button if there's no templates
MoshiKoi Aug 1, 2023
7d3c0f2
Remove the debug log
MoshiKoi Aug 1, 2023
ec44526
Create a templates category
MoshiKoi Aug 1, 2023
fc01f4d
Remove the mod tool since the category exists now
MoshiKoi Aug 1, 2023
f058880
Fix a weird positioning bug
MoshiKoi Aug 2, 2023
d1d18ac
Only show non-deleted templates
MoshiKoi Aug 2, 2023
fa0e842
Add quick link to template post from preview
MoshiKoi Aug 2, 2023
7f3414f
Update app/assets/javascripts/posts.js
MoshiKoi Aug 4, 2023
c286056
Fix the tour
MoshiKoi Aug 2, 2023
fe600a4
Make implicit conversion explicit
MoshiKoi Aug 4, 2023
eef6757
Fix whitespace
MoshiKoi Aug 4, 2023
372242d
Add option to hide categories from the header
MoshiKoi Aug 6, 2023
37b3273
Add template association to Post model
MoshiKoi Aug 6, 2023
cc047ac
Fix the foreign key
MoshiKoi Aug 6, 2023
0f1af46
Make template_post_type optional
MoshiKoi Aug 13, 2023
e30da3c
Add diff when changing a template's post type
MoshiKoi Aug 13, 2023
5c29075
Merge branch 'develop' into templates
MoshiKoi Aug 15, 2023
e24ef2b
Fix markdown editor when the post is not a post
MoshiKoi Aug 15, 2023
52c64c8
Fix template_post_type in PostHistory
MoshiKoi Aug 15, 2023
ada2b8c
Rubocop
MoshiKoi Aug 15, 2023
b4c3dff
Insert into rather than replace post content
MoshiKoi Aug 20, 2023
24e0a58
Merge branch 'develop' into templates
Oaphi Sep 30, 2023
2fddcc5
Use -1 instead of null to hide categories
MoshiKoi Oct 12, 2023
56c239a
Fix disappearance of tools on non-posts
MoshiKoi Nov 11, 2023
a5cadcd
Merge branch 'develop' into templates
Oaphi Nov 11, 2023
3c7f82c
Merge branch 'develop' into templates
MoshiKoi Dec 30, 2023
658f2e1
Close the modal after choosing the template
MoshiKoi Dec 30, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/assets/javascripts/markdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ $(() => {
$field.val(prev.substring(0, $field[0].selectionStart) + text + prev.substring($field[0].selectionEnd));
};


$('.js-template').on('click', async ev => {
const $postField = $('.js-post-field');
$tgt = $(ev.target);
const content = $(`#template-md-${$tgt.attr('data-template-id')}`).val();
replaceSelection($postField, content);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it is intentional that templates are overwriting the selection, or if nothing is selected, just append at the cursor location?

I'm more used to implementations where templates replace the current content. What was the idea behind the different approach? To allow defining sort of common elements as templates?

I can see it happening that a user tries to select text and then accidentally clicks on a template, after which their selection is gone (no confirmation). Not sure if that weighs up against the extra effort of having to confirm something.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See Monica's comment here: #1151 (comment), but also a confirmation would also be nice to have if they still have selected text that might be overridden (undo functionality is broken on all Markdown tools (#1152), so there's no going back).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah in my tests if you select text it does override (which is luckily consistent with what the javascript method is named after). The question is how complex we would want to make it.

$postField.trigger('markdown');
$tgt.parents('.modal,.droppanel').removeClass('is-active');
});

$(document).on('click', '.js-markdown-tool', ev => {
const $tgt = $(ev.target);
const $button = $tgt.is('a') ? $tgt : $tgt.parents('a');
Expand Down
1 change: 0 additions & 1 deletion app/assets/stylesheets/application.scss
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,6 @@ hr {
}

.modal {
position: fixed !important;
z-index: 8998;
}

Expand Down
2 changes: 1 addition & 1 deletion app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ def pull_pinned_links_and_hot_questions

def pull_categories
@header_categories = Rails.cache.fetch('header_categories') do
Category.all.order(sequence: :asc, id: :asc)
Category.where.not(sequence: -1).order(sequence: :asc, id: :asc)
end
end

Expand Down
13 changes: 9 additions & 4 deletions app/controllers/posts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,8 @@ def show
def edit; end

def update
before = { body: @post.body_markdown, title: @post.title, tags: @post.tags.to_a }
before = { body: @post.body_markdown, title: @post.title, tags: @post.tags.to_a,
template_post_type: @post.template_post_type }
body_rendered = helpers.post_markdown(:post, :body_markdown)
new_tags_cache = params[:post][:tags_cache]&.reject(&:empty?)

Expand All @@ -184,7 +185,9 @@ def update
PostHistory.post_edited(post, current_user, before: before[:body],
after: @post.body_markdown, comment: params[:edit_comment],
before_title: before[:title], after_title: @post.title,
before_tags: before[:tags], after_tags: @post.tags)
before_tags: before[:tags], after_tags: @post.tags,
before_template_post_type: before[:template_post_type],
after_template_post_type: @post.template_post_type)
end
flash[:success] = "#{helpers.pluralize(posts.to_a.size, 'post')} updated."
redirect_to help_path(slug: @post.doc_slug)
Expand All @@ -196,7 +199,9 @@ def update
PostHistory.post_edited(@post, current_user, before: before[:body],
after: @post.body_markdown, comment: params[:edit_comment],
before_title: before[:title], after_title: @post.title,
before_tags: before[:tags], after_tags: @post.tags)
before_tags: before[:tags], after_tags: @post.tags,
before_template_post_type: before[:template_post_type],
after_template_post_type: @post.template_post_type)

if params[:redact]
# Hide all previous history
Expand Down Expand Up @@ -541,7 +546,7 @@ def delete_draft

def permitted
[:post_type_id, :category_id, :parent_id, :title, :body_markdown, :license_id,
:doc_slug, :help_category, :help_ordering]
:doc_slug, :help_category, :help_ordering, :template_post_type_id]
end

def post_params
Expand Down
3 changes: 2 additions & 1 deletion app/models/post.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class Post < ApplicationRecord
belongs_to :license, optional: true
belongs_to :close_reason, optional: true
belongs_to :duplicate_post, class_name: 'Question', optional: true
belongs_to :template_post_type, class_name: 'PostType', optional: true
has_and_belongs_to_many :tags, dependent: :destroy
has_many :votes, dependent: :destroy
has_many :comments, dependent: :destroy
Expand Down Expand Up @@ -64,7 +65,7 @@ def self.search(term)

# Double-define: initial definitions are less efficient, so if we have a record of the post type we'll
# override them later with more efficient methods.
['Question', 'Answer', 'PolicyDoc', 'HelpDoc', 'Article'].each do |pt|
['Question', 'Answer', 'PolicyDoc', 'HelpDoc', 'Article', 'PostTemplate'].each do |pt|
define_method "#{pt.underscore}?" do
post_type_id == pt.constantize.post_type_id
end
Expand Down
8 changes: 6 additions & 2 deletions app/models/post_history.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
class PostHistory < ApplicationRecord
include PostRelated
belongs_to :before_template_post_type, class_name: 'PostType', optional: true
belongs_to :after_template_post_type, class_name: 'PostType', optional: true
belongs_to :post_history_type
belongs_to :user
has_many :post_history_tags
Expand All @@ -25,7 +27,8 @@ def self.method_missing(name, *args, **opts)
end

object, user = args
fields = [:before, :after, :comment, :before_title, :after_title, :before_tags, :after_tags, :hidden]
fields = [:before, :after, :comment, :before_title, :after_title, :before_tags, :after_tags, :hidden,
:before_template_post_type, :after_template_post_type]
values = fields.to_h { |f| [f, nil] }.merge(opts)

history_type_name = name.to_s
Expand All @@ -37,7 +40,8 @@ def self.method_missing(name, *args, **opts)

params = { post_history_type: history_type, user: user, post: object, community_id: object.community_id }
{ before: :before_state, after: :after_state, comment: :comment, before_title: :before_title,
after_title: :after_title, hidden: :hidden }.each do |arg, attr|
after_title: :after_title, hidden: :hidden, before_template_post_type: :before_template_post_type,
after_template_post_type: :after_template_post_type }.each do |arg, attr|
next if values[arg].nil?

params = params.merge(attr => values[arg])
Expand Down
5 changes: 5 additions & 0 deletions app/models/post_template.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class PostTemplate < Post
def self.post_type_id
PostType.mapping['PostTemplate']
end
end
4 changes: 4 additions & 0 deletions app/models/post_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ def reactions
end
end

def templates
Post.undeleted.where(post_type_id: PostTemplate.post_type_id).where(template_post_type: self).to_a
end

def self.mapping
Rails.cache.fetch 'network/post_types/post_type_ids', include_community: false do
PostType.all.to_h { |pt| [pt.name, pt.id] }
Expand Down
3 changes: 2 additions & 1 deletion app/views/categories/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@
<%= f.label :sequence, class: 'form-element' %>
<span class="form-caption">
The order in which this category will appear in the header and on the category list page. Higher numbers
appear later.
appear later. Set to -1 to prevent the category from being listed. Note that users will still be able
to access it even when not listed; to change who can access it, change the minimum trust level instead.
</span>
<%= f.number_field :sequence, class: 'form-element' %>
</div>
Expand Down
5 changes: 5 additions & 0 deletions app/views/post_history/post.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,10 @@
<em>The detailed changes of this event are hidden because of a redaction.</em>
</p>
<% end %>
<% if (event.before_template_post_type.present? || event.after_template_post_type.present?) &&
event.before_template_post_type != event.after_template_post_type %>
<h3><%= t('posts.template_post_type_label') %></h3>
<%= render 'diff', before: event.before_template_post_type.name, after: event.after_template_post_type.name, post: @post %>
<% end %>
</details>
<% end %>
10 changes: 9 additions & 1 deletion app/views/posts/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
<% end %>

<%= render 'shared/body_field', f: f, field_name: :body_markdown, field_label: t('posts.body_label'), post: post,
cur_length: post.body_markdown&.length, min_length: min_body_length(category), max_length: max_body_length(category) %>
cur_length: post.body_markdown&.length, min_length: min_body_length(category), max_length: max_body_length(category), post_type: post_type %>

<div class="rejected-elements notice is-warning hide">
<h3>Unsupported HTML detected</h3>
Expand Down Expand Up @@ -176,6 +176,14 @@
<% end %>
<% end %>

<% if post_type.id == PostTemplate.post_type_id %>
<div class="form-group">
<%= f.label :template_post_type_id, t('posts.template_post_type_label'), class: "form-element" %>
<%= f.select :template_post_type_id, options_for_select(PostType.all.map { |pt| [pt.name.underscore.humanize, pt.id] },
selected: post.template_post_type_id), {}, { class: "form-element" } %>
</div>
<% end %>

<% if edit_comment %>
<hr/>
<div class="form-group">
Expand Down
2 changes: 1 addition & 1 deletion app/views/posts/new.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<h1 class="has-margin-bottom-2">
New Post
New <%= @post_type.name.underscore.humanize.titleize %>
<% if @category.present? %>
in <%= @category.name %>
<% end %>
Expand Down
2 changes: 1 addition & 1 deletion app/views/shared/_body_field.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
end[1]
%>
<% end %>
<%= render 'shared/markdown_tools' %>
<%= render 'shared/markdown_tools', post_type: post.try(:post_type) %>
<%= f.text_area field_name, **({ class: classes, rows: 15, placeholder: 'Start typing your post...' }).merge(value),
data: { character_count: ".js-character-count-post-body" } %>
<%= render 'posts/mdhint', cur_length: cur_length, min_length: min_length, max_length: max_length %>
Expand Down
37 changes: 37 additions & 0 deletions app/views/shared/_markdown_tools.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,43 @@
</div>

<div class="markdown-tools widget--header js-markdown-tools">
<% templates = post_type&.templates || [] %>
<% if templates.any? %>
<div class="button-list is-gutterless">
<a href="javascript:void(0)" class="button is-outlined is-muted is-icon-only" data-drop="#templates"
data-drop-self-class-toggle="is-active" aria-label="Templates" title="Templates" role="button">
<i class="fas fa-copy"></i>
</a>
<div class="droppanel" id="templates">
<% templates.each do |template| %>
<a href="javascript:void(0)" class="js-template" aria-label="<%= template.title %>"
title="<%= template.title %>" role="button" data-template-id="<%= template.id %>">
<%= template.title %>
</a>
<a href="javascript:void(0)" data-modal="#template-<%= template.id %>"><i class="fas fa-fw fa-question-circle"></i></a>
<% end %>
</div>
<% templates.each do |template| %>
<div class="modal" id="template-<%= template.id %>">
<div class="modal--container">
<div class="modal--header">
<button class="button is-close-button modal--header-button" data-modal="#template-<%= template.id %>">&times;</button>
<%= link_to template.title, generic_share_link(template) %>
</div>
<div class="modal--body">
<textarea disabled id="template-md-<%= template.id %>"
class='form-element post-field widget--body h-b-0 h-m-0'><%= template.body_markdown %></textarea>
<%= sanitize(template.body) %>
</div>
<div class="modal--footer">
<a href="javascript:void(0)" class="js-template" aria-label="<%= template.title %>"
title="<%= template.title %>" role="button" data-template-id="<%= template.id %>">Apply</a>
</div>
</div>
</div>
<% end %>
</div>
<% end %>
<div class="button-list is-gutterless">
<%= md_button action: 'bold', label: 'Bold', class: 'is-icon-only' do %>
<i class="fas fa-fw fa-bold"></i>
Expand Down
2 changes: 1 addition & 1 deletion app/views/tour/question2.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
<div class="form-group">
<label for="tour-post-body" class="form-element">Body</label>
<div class="widget">
<%= render 'shared/markdown_tools' %>
<%= render 'shared/markdown_tools', post_type: nil %>
<textarea class="form-element post-field js-post-field widget--body h-b-0 h-m-0" rows=15 placeholder="Start typing your post..." id="tour-post-body"></textarea>
<%= render 'posts/mdhint' %>
</div>
Expand Down
2 changes: 2 additions & 0 deletions config/locales/strings/en.posts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ en:
Redact original content by hiding the previous versions from history? Use only for private information such as passwords or personally identifiable information.
licence_label: >
License
template_post_type_label: >
For Post Type
unsaved_changes_confirmation: >
Any unsaved changes will be lost. Are you sure?
category_label: >
Expand Down
5 changes: 5 additions & 0 deletions db/migrate/20230801014134_add_template_post_type_to_posts.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddTemplatePostTypeToPosts < ActiveRecord::Migration[7.0]
def change
add_reference :posts, :template_post_type, foreign_key: { to_table: :post_types }
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class AddBeforeAndAfterTemplatePostTypeToPostHistories < ActiveRecord::Migration[7.0]
def change
add_reference :post_histories, :before_template_post_type, foreign_key: { to_table: :post_types }
add_reference :post_histories, :after_template_post_type, foreign_key: { to_table: :post_types }
end
end
9 changes: 9 additions & 0 deletions db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,11 @@
t.bigint "community_id"
t.string "before_title"
t.string "after_title"
t.bigint "before_template_post_type_id"
t.bigint "after_template_post_type_id"
t.boolean "hidden", default: false, null: false
t.index ["after_template_post_type_id"], name: "index_post_histories_on_after_template_post_type_id"
t.index ["before_template_post_type_id"], name: "index_post_histories_on_before_template_post_type_id"
t.index ["community_id"], name: "index_post_histories_on_community_id"
t.index ["post_history_type_id"], name: "index_post_histories_on_post_history_type_id"
t.index ["post_id"], name: "index_post_histories_on_post_type_and_post_id"
Expand Down Expand Up @@ -474,6 +478,7 @@
t.bigint "locked_by_id"
t.datetime "locked_at", precision: nil
t.datetime "locked_until", precision: nil
t.bigint "template_post_type_id"
t.index ["att_source"], name: "index_posts_on_att_source"
t.index ["body_markdown"], name: "index_posts_on_body_markdown", type: :fulltext
t.index ["category_id"], name: "index_posts_on_category_id"
Expand All @@ -491,6 +496,7 @@
t.index ["post_type_id"], name: "index_posts_on_post_type_id"
t.index ["score"], name: "index_posts_on_score"
t.index ["tags_cache"], name: "index_posts_on_tags_cache"
t.index ["template_post_type_id"], name: "index_posts_on_template_post_type_id"
t.index ["upvote_count"], name: "index_posts_on_upvote_count"
t.index ["user_id"], name: "index_posts_on_user_id"
end
Expand Down Expand Up @@ -794,12 +800,15 @@
add_foreign_key "pinned_links", "communities"
add_foreign_key "pinned_links", "posts"
add_foreign_key "post_histories", "communities"
add_foreign_key "post_histories", "post_types", column: "after_template_post_type_id"
add_foreign_key "post_histories", "post_types", column: "before_template_post_type_id"
add_foreign_key "post_history_tags", "post_histories"
add_foreign_key "post_history_tags", "tags"
add_foreign_key "post_types", "post_types", column: "answer_type_id"
add_foreign_key "posts", "close_reasons"
add_foreign_key "posts", "communities"
add_foreign_key "posts", "licenses"
add_foreign_key "posts", "post_types", column: "template_post_type_id"
add_foreign_key "posts", "posts", column: "duplicate_post_id"
add_foreign_key "posts", "users", column: "locked_by_id"
add_foreign_key "privileges", "communities"
Expand Down
21 changes: 20 additions & 1 deletion db/seeds/categories.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
- name: Q&A
sequence: 1
Taeir marked this conversation as resolved.
Show resolved Hide resolved
short_wiki: General Q&A on the topic of the site.
display_post_types:
- <%= PostType['Question'].id %>
Expand All @@ -12,6 +13,7 @@
license_id: <%= License.unscoped.first.id %>

- name: Meta
sequence: 2
Taeir marked this conversation as resolved.
Show resolved Hide resolved
short_wiki: Discussions and feedback about the site itself in Q&A format.
display_post_types:
- <%= PostType['Question'].id %>
Expand All @@ -22,4 +24,21 @@
use_for_hot_posts: true
use_for_advertisement: false
color_code: bluegray
license_id: <%= License.unscoped.first.id %>
license_id: <%= License.unscoped.first.id %>

- name: Templates
sequence: -1
short_wiki: Templates
# Default to moderators-only
min_trust_level: 4
min_title_length: 1
display_post_types:
- <%= PostType['PostTemplate'].id %>
post_type_ids:
- <%= PostType['PostTemplate'].id %>
tag_set_id: <%= TagSet.unscoped.where(name: 'Meta').first.id %>
use_for_hot_posts: false
use_for_advertisement: false
color_code: bluegray
# The PostTemplate actually has has_license: false, but just in case
license_id: <%= License.unscoped.where(name: 'CC0').first.id %>
15 changes: 15 additions & 0 deletions db/seeds/post_types.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,19 @@
is_top_level: true
is_freely_editable: false
has_reactions: false
has_only_specific_reactions: false

- name: PostTemplate
description: ~
has_answers: false
has_votes: false
has_tags: false
has_parent: false
has_category: true
has_license: false
is_public_editable: false
is_closeable: false
is_top_level: true
is_freely_editable: false
has_reactions: false
has_only_specific_reactions: false
Loading