Allow archiving of posts (#2798)
* This commit adds the "archived" attribute to the Article model, which will enable users to automatically filter archived articles/drafts from appearing on their dashboard. It also adds an ellipsis menu with a link to archive the article. I've also moved the Mute Notifications button into the ellipsis menu as well. * Add ellipsis menu and make it work * Fix logic error * Hide ellipsis menus when you click elsewhere on the body * Extract function to remove duplication * Ensure that menu is hidden when the ellipsis button is clicked on an open menu * Ensure that archiving post button works and that 'archived' param is accepted by the controller * Enable link to toggle showing/hiding archived articles * Make 'mute' and 'archive' buttons in ellipsis dropdown list work * Refactor to make Code Climate happy * Minor JS refactors
This commit is contained in:
parent
ab344801db
commit
b2a8f69f83
12 changed files with 290 additions and 5 deletions
|
|
@ -42,6 +42,8 @@ function callInitalizers(){
|
|||
initializeTimeFixer();
|
||||
initializeDashboardSort();
|
||||
initializePWAFunctionality();
|
||||
initializeEllipsisMenu();
|
||||
initializeArchivedPostFilter();
|
||||
|
||||
if (!initializeLiveArticle.called){
|
||||
initializeLiveArticle();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
function archivedPosts() {
|
||||
return document.getElementsByClassName('single-article-archived');
|
||||
}
|
||||
|
||||
function showArchivedPosts() {
|
||||
var posts = archivedPosts();
|
||||
|
||||
for (var i = 0; i < posts.length; i += 1) {
|
||||
posts[i].classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function hideArchivedPosts() {
|
||||
var posts = archivedPosts();
|
||||
|
||||
for (var i = 0; i < posts.length; i += 1) {
|
||||
posts[i].classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleArchivedPosts(e) {
|
||||
var link = e.target;
|
||||
|
||||
if (link.innerHTML.match(/Show/)) {
|
||||
link.innerHTML = 'Hide Archived';
|
||||
showArchivedPosts();
|
||||
} else {
|
||||
link.innerHTML = 'Show Archived';
|
||||
hideArchivedPosts();
|
||||
}
|
||||
}
|
||||
|
||||
function initializeArchivedPostFilter() {
|
||||
var link = document.getElementById('toggleArchivedLink');
|
||||
|
||||
link.addEventListener('click', toggleArchivedPosts);
|
||||
}
|
||||
150
app/assets/javascripts/initializers/initializeEllipsisMenu.js
Normal file
150
app/assets/javascripts/initializers/initializeEllipsisMenu.js
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
// SUBMITTING FORM //
|
||||
|
||||
function getFormValues(form) {
|
||||
var articleId = form.action.match(/\/(\d+)$/)[1];
|
||||
var inputs = form.querySelectorAll('input');
|
||||
var formData = { id: articleId, article: {} };
|
||||
|
||||
for (var i = 0; i < inputs.length; i += 1) {
|
||||
var input = inputs[i];
|
||||
var name = input.getAttribute('name');
|
||||
var value = input.getAttribute('value');
|
||||
|
||||
if (name.match(/\[(.*)\]/)) {
|
||||
var key = name.match(/\[(.*)\]$/)[1];
|
||||
formData.article[key] = value;
|
||||
} else {
|
||||
formData[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
function toggleArchived(article, needsArchived) {
|
||||
if (needsArchived === 'true') {
|
||||
article.classList.add('single-article-archived', 'hidden');
|
||||
} else {
|
||||
article.classList.remove('single-article-archived');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleNotifications(submit, action) {
|
||||
if (action === 'Mute Notifications') {
|
||||
submit.setAttribute('value', 'Receive Notifications');
|
||||
} else {
|
||||
submit.setAttribute('value', 'Mute Notifications');
|
||||
}
|
||||
}
|
||||
|
||||
function onXhrSuccess(form, article, values) {
|
||||
if (values.article.archived) {
|
||||
toggleArchived(article, values.article.archived);
|
||||
} else {
|
||||
var submit = form.querySelector('input[type="submit"]');
|
||||
var submitValue = submit.getAttribute('value');
|
||||
|
||||
toggleNotifications(submit, submitValue);
|
||||
}
|
||||
|
||||
article.querySelector('ul.ellipsis-menu').classList.add('hidden');
|
||||
}
|
||||
|
||||
function handleFormSubmit(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
var form = e.target;
|
||||
var values = getFormValues(form);
|
||||
var data = JSON.stringify(values);
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('PATCH', form.action);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.send(data);
|
||||
|
||||
xhr.onload = function() {
|
||||
var article = form.closest('div.single-article');
|
||||
|
||||
if (xhr.status === 200) {
|
||||
onXhrSuccess(form, article, values);
|
||||
} else {
|
||||
article.querySelector('.dashboard-meta-details').innerHTML =
|
||||
'Failed to update article.';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function initializeFormSubmit() {
|
||||
var forms = document.querySelectorAll('ul.ellipsis-menu > li > form');
|
||||
|
||||
for (var i = 0; i < forms.length; i += 1) {
|
||||
forms[i].addEventListener('submit', handleFormSubmit);
|
||||
}
|
||||
}
|
||||
|
||||
// TOGGLING MENU //
|
||||
|
||||
function getMenu(el) {
|
||||
var parentDiv = el.closest('div.ellipsis-menu');
|
||||
var menu = parentDiv.querySelector('ul.ellipsis-menu');
|
||||
return menu;
|
||||
}
|
||||
|
||||
function hideIfNotAlreadyHidden(menu) {
|
||||
if (!menu.classList.contains('hidden')) {
|
||||
menu.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function hideAllEllipsisMenusExcept(menu) {
|
||||
var menus = document.querySelectorAll('ul.ellipsis-menu');
|
||||
|
||||
for (var i = 0; i < menus.length; i += 1) {
|
||||
if (menus[i] !== menu) {
|
||||
hideIfNotAlreadyHidden(menus[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hideEllipsisMenus(e) {
|
||||
if (!e.target.closest('div.ellipsis-menu')) {
|
||||
var menus = document.querySelectorAll('ul.ellipsis-menu');
|
||||
|
||||
for (var i = 0; i < menus.length; i += 1) {
|
||||
hideIfNotAlreadyHidden(menus[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleEllipsisMenu(e) {
|
||||
var menu = getMenu(e.target);
|
||||
|
||||
// Make sure other ellipsis menus close when a new one
|
||||
// is opened
|
||||
hideAllEllipsisMenusExcept(menu);
|
||||
|
||||
if (menu.classList.contains('hidden')) {
|
||||
menu.classList.remove('hidden');
|
||||
} else {
|
||||
menu.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function initializeEllipsisMenuToggle() {
|
||||
var buttons = document.getElementsByClassName('ellipsis-menu-btn');
|
||||
|
||||
for (var i = 0; i < buttons.length; i += 1) {
|
||||
buttons[i].addEventListener('click', toggleEllipsisMenu);
|
||||
}
|
||||
|
||||
// Hide ellipsis menus when you click outside of the ellipsis menu parent div
|
||||
document
|
||||
.getElementsByTagName('BODY')[0]
|
||||
.addEventListener('click', hideEllipsisMenus);
|
||||
}
|
||||
|
||||
function initializeEllipsisMenu() {
|
||||
initializeEllipsisMenuToggle();
|
||||
initializeFormSubmit();
|
||||
}
|
||||
|
|
@ -318,6 +318,59 @@
|
|||
}
|
||||
}
|
||||
|
||||
& div.ellipsis-menu {
|
||||
float: right;
|
||||
position: relative;
|
||||
|
||||
& button.ellipsis-menu-btn {
|
||||
display: block;
|
||||
float: right;
|
||||
margin-top: 1em;
|
||||
border-radius: 15px;
|
||||
|
||||
&:hover {
|
||||
background-color: #eee;
|
||||
}
|
||||
}
|
||||
|
||||
& ul.ellipsis-menu {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
display: block;
|
||||
background-color: white;
|
||||
padding-left: 0;
|
||||
border-style: solid;
|
||||
border-color: #ccc;
|
||||
border-radius: 5px;
|
||||
border-width: 1px;
|
||||
margin-top: 2.75em;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& hr {
|
||||
margin: 0;
|
||||
color: #ccc;
|
||||
background-color: #ccc;
|
||||
height: 1px;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
& li.ellipsis-menu-item {
|
||||
list-style: none;
|
||||
|
||||
& form * {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #eee;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-collection-org-details {
|
||||
.dashboard-top-pill {
|
||||
@include themeable(
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ class ArticleMutesController < ApplicationController
|
|||
@article = Article.find_by(id: params[:id])
|
||||
authorize @article
|
||||
@article.update(receive_notifications: permitted_attributes(@article)[:receive_notifications])
|
||||
redirect_to "/dashboard"
|
||||
respond_to do |format|
|
||||
format.json { head :ok }
|
||||
format.html { redirect_to "/dashboard" }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -145,9 +145,16 @@ class ArticlesController < ApplicationController
|
|||
Notification.remove_all_without_delay(notifiable_id: @article.id, notifiable_type: "Article", action: "Published")
|
||||
path = "/#{@article.username}/#{@article.slug}?preview=#{@article.password}"
|
||||
end
|
||||
redirect_to(params[:destination] || path)
|
||||
|
||||
respond_to do |format|
|
||||
format.json { head :ok }
|
||||
format.html { redirect_to(params[:destination] || path) }
|
||||
end
|
||||
else
|
||||
render :edit
|
||||
respond_to do |format|
|
||||
format.html { redirect_to :edit }
|
||||
format.json { head :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ class ArticlePolicy < ApplicationPolicy
|
|||
def permitted_attributes
|
||||
%i[title body_html body_markdown main_image published canonical_url
|
||||
description allow_small_edits allow_big_edits tag_list publish_under_org
|
||||
video video_code video_source_url video_thumbnail_url receive_notifications]
|
||||
video video_code video_source_url video_thumbnail_url receive_notifications
|
||||
archived]
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<div class="single-article <%= "single-article-unpublished" unless article.published %>">
|
||||
<div class="single-article <%= "single-article-unpublished" unless article.published %> <%= "single-article-archived hidden" if article.archived %>">
|
||||
<div class="dashboard-collection-org-details">
|
||||
<% if article.organization_id %>
|
||||
<span class="dashboard-top-pill"><%= article.organization&.name %></span>
|
||||
|
|
@ -7,6 +7,26 @@
|
|||
<span class="dashboard-top-pill"><strong>Series:</strong> <%= article.series %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="ellipsis-menu">
|
||||
<button class="ellipsis-menu-btn"><i class="material-icons">more_horiz</i></button>
|
||||
<ul class="ellipsis-menu hidden">
|
||||
<li class="ellipsis-menu-item">
|
||||
<%= form_for(article, url: "/article_mutes/#{article.id}", method: :patch, authenticity_token: true, html: { "data-type": "json" }) do |f| %>
|
||||
<%= f.hidden_field :receive_notifications, value: !article.receive_notifications %>
|
||||
<%= f.submit article.receive_notifications ? "Mute Notifications" : "Receive Notifications" %>
|
||||
<% end %>
|
||||
</li>
|
||||
<hr />
|
||||
<li class="ellipsis-menu-item">
|
||||
<%= form_for(article, method: :patch, remote: true, authenticity_token: true, html: { "data-type": "json" }) do |f| %>
|
||||
<%= f.hidden_field :archived, value: !article.archived %>
|
||||
<%= f.submit article.archived ? "Unarchive Article" : "Archive Article" %>
|
||||
<% end %>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<a href="<%= article.current_state_path %>"><h2><%= article.title %></h2></a>
|
||||
<div class="dashboard-meta-details">
|
||||
<% if article.published %>
|
||||
|
|
@ -29,6 +49,8 @@
|
|||
<a href="<%= article.path %>/delete_confirm" data-no-instant class="cta pill black">DELETE</a>
|
||||
<% elsif article.published %>
|
||||
<a href="<%= article.path %>/manage" class="cta pill black">MANAGE</a>
|
||||
<% else %>
|
||||
<a href="<%= article.path %>/delete_confirm" data-no-instant class="cta pill black">DELETE</a>
|
||||
<% end %>
|
||||
<% if article.published %>
|
||||
<span id="pageviews-<%= article.id %>" class="cta pill dashboard-pageviews-indicator" data-analytics-pageviews data-article-id="<%= article.id %>">
|
||||
|
|
|
|||
|
|
@ -29,6 +29,9 @@
|
|||
<% end %>
|
||||
|
||||
<%= select_tag "dashhboard_sort", options_for_select(sort_options, params[:sort]) %>
|
||||
<% if @articles.any? {|article| article.archived } %>
|
||||
<%= link_to "Show archived", "javascript:;", id: "toggleArchivedLink" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% @articles.each do |article| %>
|
||||
<%= render "dashboard_article", article: article, org_admin: false, manage_view: false %>
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@
|
|||
<%= yield(:page_meta) %>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<%= favicon_link_tag %>
|
||||
<%= stylesheet_link_tag "https://fonts.googleapis.com/icon?family=Material+Icons" %>
|
||||
<link rel="apple-touch-icon" href="<%= asset_path "apple-icon.png" %>">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="<%= asset_path "apple-icon-152x152.png" %>">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="<%= asset_path "apple-icon-180x180.png" %>">
|
||||
|
|
|
|||
5
db/migrate/20190504015859_add_archived_to_articles.rb
Normal file
5
db/migrate/20190504015859_add_archived_to_articles.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
class AddArchivedToArticles < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
add_column :articles, :archived, :boolean, default: false
|
||||
end
|
||||
end
|
||||
|
|
@ -52,6 +52,7 @@ ActiveRecord::Schema.define(version: 2019_05_04_131412) do
|
|||
t.float "amount_due", default: 0.0
|
||||
t.float "amount_paid", default: 0.0
|
||||
t.boolean "approved", default: false
|
||||
t.boolean "archived", default: false
|
||||
t.boolean "automatically_renew", default: false
|
||||
t.text "body_html"
|
||||
t.text "body_markdown"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue