Skip to content

Commit

Permalink
Allow archiving of posts (forem#2798)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
danascheider authored and benhalpern committed May 13, 2019
1 parent ab34480 commit b2a8f69
Show file tree
Hide file tree
Showing 12 changed files with 290 additions and 5 deletions.
2 changes: 2 additions & 0 deletions app/assets/javascripts/initializePage.js.erb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ function callInitalizers(){
initializeTimeFixer();
initializeDashboardSort();
initializePWAFunctionality();
initializeEllipsisMenu();
initializeArchivedPostFilter();

if (!initializeLiveArticle.called){
initializeLiveArticle();
Expand Down
Original file line number Diff line number Diff line change
@@ -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 changes: 150 additions & 0 deletions app/assets/javascripts/initializers/initializeEllipsisMenu.js
Original file line number Diff line number Diff line change
@@ -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();
}
53 changes: 53 additions & 0 deletions app/assets/stylesheets/dashboard.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
5 changes: 4 additions & 1 deletion app/controllers/article_mutes_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ def update
@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
11 changes: 9 additions & 2 deletions app/controllers/articles_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,16 @@ def update
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

Expand Down
3 changes: 2 additions & 1 deletion app/policies/article_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ def preview?
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
Expand Down
24 changes: 23 additions & 1 deletion app/views/dashboards/_dashboard_article.html.erb
Original file line number Diff line number Diff line change
@@ -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>
Expand All @@ -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 %>
Expand All @@ -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 %>">
Expand Down
3 changes: 3 additions & 0 deletions app/views/dashboards/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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 %>
Expand Down
1 change: 1 addition & 0 deletions app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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" %>">
Expand Down
5 changes: 5 additions & 0 deletions db/migrate/20190504015859_add_archived_to_articles.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddArchivedToArticles < ActiveRecord::Migration[5.2]
def change
add_column :articles, :archived, :boolean, default: false
end
end
1 change: 1 addition & 0 deletions db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
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"
Expand Down

0 comments on commit b2a8f69

Please sign in to comment.