Merge pull request #838 from indentlabs/improve-timelines

Improve timelines
This commit is contained in:
Andrew Brown 2021-01-24 02:12:50 -08:00 committed by GitHub
commit cf531094be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 254 additions and 84 deletions

View File

@ -89,7 +89,7 @@ class ContentController < ApplicationController
})
end
if current_user
if user_signed_in?
if @content.updated_at > 30.minutes.ago
Mixpanel::Tracker.new(Rails.application.config.mixpanel_token).track(current_user.id, 'viewed content', {
'content_type': content_type.name,
@ -105,6 +105,14 @@ class ContentController < ApplicationController
end
end
if user_signed_in? && current_user.can_create?(content_type)
@navbar_actions << {
label: "New #{content_type.name.downcase}",
href: main_app.new_polymorphic_path(content_type),
class: 'right'
}
end
respond_to do |format|
format.html { render 'content/show', locals: { content: @content } }
format.json { render json: @serialized_content.data }
@ -168,6 +176,14 @@ class ContentController < ApplicationController
return redirect_to @content, notice: t(:no_do_permission)
end
if user_signed_in? && current_user.can_create?(content_type_class)
@navbar_actions << {
label: "New #{content_type_class.name.downcase}",
href: main_app.new_polymorphic_path(content_type_class),
class: 'right'
}
end
respond_to do |format|
format.html { render 'content/edit', locals: { content: @content } }
format.json { render json: @content }
@ -614,12 +630,6 @@ class ContentController < ApplicationController
href: main_app.polymorphic_path(content_type)
}
end
@navbar_actions << {
label: "New #{content_type.name.downcase}",
href: main_app.new_polymorphic_path(content_type),
class: 'right'
}
end
discussions_link = ForumsLinkbuilderService.worldbuilding_url(content_type)

View File

@ -13,6 +13,20 @@ class TimelinesController < ApplicationController
if @universe_scope
@timelines = @timelines.where(universe: @universe_scope)
end
@page_tags = PageTag.where(
page_type: Timeline.name,
page_id: @timelines.pluck(:id)
).order(:tag)
if params.key?(:slug)
@filtered_page_tags = @page_tags.where(slug: params[:slug])
@timelines = @timelines.select { |timeline| @filtered_page_tags.pluck(:page_id).include?(timeline.id) }
end
# if params.key?(:favorite_only)
# @content.select!(&:favorite?)
# end
end
def show
@ -32,6 +46,8 @@ class TimelinesController < ApplicationController
# GET /timelines/1/edit
def edit
@page_title = "Editing " + @timeline.name
@suggested_page_tags = []
raise "No Access" unless user_signed_in? && current_user == @timeline.user
end
@ -55,6 +71,8 @@ class TimelinesController < ApplicationController
return unless user_signed_in? && current_user == @timeline.user
if @timeline.update(timeline_params)
update_page_tags
render status: 200, json: @timeline.reload
else
render status: 501, json: @timeline.errors
@ -69,6 +87,27 @@ class TimelinesController < ApplicationController
private
# TODO: move this (and the copy in ContentController) into the has_page_tags concern?
def update_page_tags
tag_list = page_tag_params.split(PageTag::SUBMISSION_DELIMITER)
current_tags = @timeline.page_tags.pluck(:tag)
tags_to_add = tag_list - current_tags
tags_to_remove = current_tags - tag_list
tags_to_add.each do |tag|
@timeline.page_tags.find_or_create_by(
tag: tag,
slug: PageTagService.slug_for(tag),
user: @timeline.user
)
end
tags_to_remove.each do |tag|
@timeline.page_tags.find_by(tag: tag).destroy
end
end
# Use callbacks to share common setup or constraints between actions.
def set_timeline
@timeline = Timeline.find(params[:id])
@ -76,7 +115,11 @@ class TimelinesController < ApplicationController
# Only allow a trusted parameter "white list" through.
def timeline_params
params.require(:timeline).permit(:name, :subtitle, :description, :notes, :private_notes, :universe_id, :deleted_at, :archived_at, :privacy)
params.require(:timeline).except(:page_tags).permit(:name, :subtitle, :description, :notes, :private_notes, :universe_id, :deleted_at, :archived_at, :privacy)
end
def page_tag_params
params.require(:timeline).fetch(:page_tags, "")
end
def set_navbar_color

View File

@ -26,4 +26,9 @@ module ApplicationHelper
def title(*parts)
content_for(:title) { (parts << 'Notebook').join(' - ') } unless parts.empty?
end
def clean_links html
html.gsub!(/\<a href=["'](.*?)["']\>(.*?)\<\/a\>/mi, '<a href="\1" rel="nofollow">\2</a>')
html.html_safe
end
end

View File

@ -20,6 +20,10 @@ module HasImageUploads
image_uploads.sample.try(:src, format).presence || "card-headers/#{self.class.name.downcase.pluralize}.jpg"
end
def first_public_image(format: :medium)
public_image_uploads.first.try(:src, format).presence || "card-headers/#{self.class.name.downcase.pluralize}.jpg"
end
def random_public_image(format: :medium)
public_image_uploads.sample.try(:src, format).presence || "card-headers/#{self.class.name.downcase.pluralize}.jpg"
end

View File

@ -51,6 +51,11 @@ class PageCollection < ApplicationRecord
# If all else fails, fall back on default header
"card-headers/#{self.class.name.downcase.pluralize}.jpg"
end
def first_public_image
random_public_image
end
def name
title
end

View File

@ -55,6 +55,7 @@ class Universe < ApplicationRecord
has_many :contributors, dependent: :destroy
has_many :documents
has_many :timelines
scope :in_universe, ->(universe = nil) { where(id: universe.try(:id)) unless universe.nil? }

View File

@ -2,6 +2,7 @@ class Timeline < ApplicationRecord
acts_as_paranoid
include IsContentPage
include HasPageTags
include Authority::Abilities
self.authorizer_name = 'ExtendedContentAuthorizer'

View File

@ -1,7 +1,7 @@
<div class="parallax-header slider" style="height: 1px; position: relative; top: -1px">
<ul class="slides">
<li>
<%= image_tag(collection.random_public_image) %>
<%= image_tag(collection.first_public_image) %>
<div class="caption center-align bordered-text">
<%= link_to collection, class: 'white-text' do %>
<h4>

View File

@ -1,15 +1,18 @@
<div class="filter-bar">
<!-- <div class="btn-group card">
<!--
<div class="btn-group card">
<a href="#" class="btn white black-text"><i class="material-icons">view_module</i></a>
<a href="#" class="btn white black-text"><i class="material-icons">view_headline</i></a>
<a href="#" class="btn white black-text"><i class="material-icons">view_stream</i></a>
<a href="#" class="btn white black-text"><i class="material-icons">grid_on</i></a>
</div> -->
</div>
-->
<% if content_type.new.respond_to?(:universe) %>
<div class="btn-group card">
<a
class='dropdown-trigger btn white tooltipped <%= Universe.color if @universe_scope.present? %> lighten-5'
class='dropdown-trigger btn black-text white tooltipped <%= Universe.color if @universe_scope.present? %> lighten-5'
href='#'
data-position="bottom"
data-delay="500"
@ -78,14 +81,16 @@
</div>
<% end %>
<a class='btn white <%= 'yellow lighten-3' if params.key?(:favorite_only) %> black-text tooltipped'
href="<%= url_for(params.permit(:universe, :favorite_only).merge({favorite_only: params.key?(:favorite_only) ? nil : 1})) %>"
data-position="bottom"
data-delay="500"
data-tooltip="Filter by favorite">
<i class="material-icons amber-text">star_outline</i>
<% if params.key?(:favorite_only) %>Favorites<% end %>
</a>
<% if content_type.new.respond_to?(:favorite?) %>
<a class='btn white <%= 'yellow lighten-3' if params.key?(:favorite_only) %> black-text tooltipped'
href="<%= url_for(params.permit(:universe, :favorite_only).merge({favorite_only: params.key?(:favorite_only) ? nil : 1})) %>"
data-position="bottom"
data-delay="500"
data-tooltip="Filter by favorite">
<i class="material-icons amber-text">star_outline</i>
<% if params.key?(:favorite_only) %>Favorites<% end %>
</a>
<% end %>
&nbsp;
@ -122,4 +127,12 @@
<label for="js-content-name-filter">Filter <%= content_type.name.downcase.pluralize %> by name...</label>
</div>
<div class="btn-group right" style="margin-top: 1.9em;">
<%= link_to send("new_#{content_type.name.downcase}_path"), class: "btn right #{content_type.color} white-text" do %>
<i class="material-icons left">add</i>
New <%= content_type.name %>
<% end %>
</div>
</div>

View File

@ -8,7 +8,7 @@
<%= link_to collection do %>
<div class="hoverable card">
<div class="card-image">
<%= image_tag collection.random_public_image, style: 'max-height: 300px;' %>
<%= image_tag collection.first_public_image, style: 'max-height: 300px;' %>
<span class="card-title">
<%= collection.title %>
<br />

View File

@ -20,7 +20,7 @@
<%= link_to edit_timeline_path(timeline) do %>
<div class="hoverable card">
<div class="card-image" style="height: 140px;">
<%= image_tag timeline.random_public_image(format: :medium), style: 'height: 140px' %>
<%= image_tag timeline.first_public_image(format: :medium), style: 'height: 140px' %>
<span class="card-title">
<%= timeline.name.presence || 'Untitled Timeline' %>
</span>

View File

@ -1,7 +1,7 @@
<%
set_meta_tags title: @content.name,
description: "#{%w(a e i o u).include?(content.class.name.downcase[0]) ? "An" : "A"} #{@content.class.name.downcase} on Notebook.ai",
image_src: @content.random_public_image,
image_src: @content.first_public_image,
og: { type: 'website' }
%>

View File

@ -28,11 +28,11 @@
<div class="card-image">
<% if share.user == content.user %>
<%= link_to [share.user, share] do %>
<%= image_tag secondary_content.random_public_image %>
<%= image_tag secondary_content.first_public_image %>
<% end %>
<% else %>
<%= link_to [share.user, share] do %>
<%= image_tag content.random_public_image %>
<%= image_tag content.first_public_image %>
<% end %>
<% end %>
<span class="card-title">

View File

@ -23,7 +23,7 @@
<div class="col s12 m6 l6">
<div class="card-image">
<%= link_to [share.user, share] do %>
<%= image_tag content.random_public_image(format: :small) %>
<%= image_tag content.first_public_image(format: :small) %>
<% end %>
<span class="card-title">
<%= link_to content do %>

View File

@ -23,7 +23,7 @@
<div class="col s12 m6 l6">
<div class="card-image">
<%= link_to [share.user, share] do %>
<%= image_tag content.random_public_image %>
<%= image_tag content.first_public_image %>
<% end %>
<span class="card-title">
<%= link_to content do %>

View File

@ -23,7 +23,7 @@
<div class="col s12 m6 l6">
<div class="card-image">
<%= link_to [share.user, share] do %>
<%= image_tag content.random_public_image(format: :small) %>
<%= image_tag content.first_public_image(format: :small) %>
<% end %>
<span class="card-title">
<%= link_to content do %>

View File

@ -23,7 +23,7 @@
<div class="col s12 m6 l6">
<div class="card-image">
<%= link_to [share.user, share] do %>
<%= image_tag content.random_public_image(format: :small) %>
<%= image_tag content.first_public_image(format: :small) %>
<% end %>
<span class="card-title">
<%= link_to content do %>

View File

@ -7,7 +7,7 @@
<div class="hoverable card">
<div class="card-image">
<%= link_to @share.content_page do %>
<%= image_tag @share.content_page.random_public_image %>
<%= image_tag @share.content_page.first_public_image %>
<span class="card-title bordered-text">
<i class="material-icons <%= @share.content_page.class.color %>-text left small"><%= @share.content_page.class.icon %></i>
<%= @share.content_page.name %>

View File

@ -2,5 +2,5 @@
<%= content_for :full_width_page_header do %>
<%= render partial: 'documents/components/document_name_bar', locals: { document: @document } %>
<div id="editor"><%= ContentFormatterService.substitute_content_links(@document.body.try(:html_safe) || "", current_user).html_safe %></div>
<div id="editor"><%= ContentFormatterService.substitute_content_links(clean_links(@document.body.try(:html_safe)) || "", current_user).html_safe %></div>
<% end %>

View File

@ -6,30 +6,45 @@
<div class="row">
<div class="col s12">
<div class="card-panel <%= Timeline.color %> lighten-4 black-text">
<p>
<strong>Welcome to the new Timeline Editor!</strong>
</p>
<p>
This editor is still in beta, so any
<%= link_to 'feedback', 'https://docs.google.com/forms/d/e/1FAIpQLSfHHcOpU-CD9EFILPVMd4ZtgWIFkxmzLMW1mdvanJqqLKgMAA/viewform?usp=sf_link' %>
you have is highly appreciated.
</p>
<p>
You are starting with a blank timeline and can add as many events as you'd like. Each event allows you to
write a customized label for when it happens so you can be as exact or as vague as you'd like, and you can
fully control the ordering of events by clicking the links to move them up, down, to the top, or to the end
of your timeline.
</p>
<p>
You can also link your existing worldbuilding pages to each event. For example, if you create an event about
<%= example_character_name %> finding a mysterious egg, you can link <%= example_character_name %>'s page
directly, as well as any other pages relevant to that scene (such as an Item page for the egg, the location it's
happening at, or other characters there). Those worldbuilding pages will automatically have a "Timelines" tab
created (visible only to you) that lists all timelines they appear in.
</p>
<p>
<%= link_to 'Dismiss this message', notice_dismissal_dismiss_path(notice_id: 13) %>.
</p>
<div class="row">
<div class="col s12 m9">
<p>
<strong>Welcome to the new Timeline Editor!</strong>
</p>
<p>
This editor is still in beta, so any
<%= link_to 'feedback', 'https://docs.google.com/forms/d/e/1FAIpQLSfHHcOpU-CD9EFILPVMd4ZtgWIFkxmzLMW1mdvanJqqLKgMAA/viewform?usp=sf_link' %>
you have is highly appreciated.
</p>
<p>
You are starting with a blank timeline and can add as many events as you'd like. Each event allows you to
write a customized label for when it happens so you can be as exact or as vague as you'd like, and you can
fully control the ordering of events by clicking the links to move them up, down, to the top, or to the end
of your timeline.
</p>
<p>
You can also link your existing worldbuilding pages to each event. For example, if you create an event about
<%= example_character_name %> finding a mysterious egg, you can link <%= example_character_name %>'s page
directly, as well as any other pages relevant to that scene (such as an Item page for the egg, the location it's
happening at, or other characters there). Those worldbuilding pages will automatically have a "Timelines" tab
created (visible only to you) that lists all timelines they appear in.
</p>
<p>
<%= link_to 'Dismiss this message', notice_dismissal_dismiss_path(notice_id: 13) %>.
</p>
</div>
<div class="col hide-on-small-only m3">
<%= image_tag 'tristan/small.png',
class: 'tooltipped tristan right',
data: {
position: 'bottom',
enterDelay: '500',
tooltip: "Hey, I'm Tristan! I'm here to help you around the site!"
}
%>
</div>
</div>
</div>
</div>
</div>

View File

@ -20,7 +20,7 @@
<div class="horizontal card <%= content.class.color %> lighten-5 black-text">
<div class="card-image">
<%= link_to content do %>
<%= image_tag content.random_public_image %>
<%= image_tag content.first_public_image %>
<% end %>
</div>
<div class="card-stacked">

View File

@ -7,7 +7,7 @@
<%= link_to collection do %>
<div class="hoverable card <%= PageCollection.color %>" style="height: 250px">
<div class="card-image">
<%= image_tag collection.random_public_image, style: 'max-height: 250px;' %>
<%= image_tag collection.first_public_image, style: 'max-height: 250px;' %>
<span class="card-title bordered-text">
<%= collection.title %>
<br />

View File

@ -4,7 +4,7 @@
<div class="hoverable card horizontal">
<div class="card-image">
<%= link_to content do %>
<%= image_tag content.random_public_image %>
<%= image_tag content.first_public_image %>
<% end %>
</div>
<div class="card-stacked">

View File

@ -5,7 +5,7 @@
<% next unless (current_user || User.new).can_read?(content) %>
<div class="hoverable card" style="min-height: 330px;">
<div class="card-image waves-effect waves-block waves-light">
<%= image_tag content.random_public_image, class: 'activator', style: "height: 300px;" %>
<%= image_tag content.first_public_image, class: 'activator', style: "height: 300px;" %>
</div>
<div class="card-content fixed-card-content" style="border-top: 6px solid <%= content.class.hex_color %>">
<span class="card-title activator grey-text text-darken-4">

View File

@ -1,6 +1,6 @@
<%= render partial: 'notice_dismissal/messages/13' %>
<br />
<%= form_for @timeline, html: { class: 'autosave-form' }, remote: true do |f| %>
<%= form_for @timeline, html: { class: 'autosave-form timeline-meta-form' }, remote: true do |f| %>
<div class="row">
<div class="col s12 m7 l9">
<div class="input-field">
@ -38,8 +38,8 @@
<%= f.label :description, 'Description' %>
</div>
<div class="input-field">
<%= f.select :universe_id, current_user.universes.pluck(:name, :id), { include_blank: true }, class: 'materialize-textarea js-trigger-autosave-on-change' %>
<%= f.label 'Universe' %>
<%= f.select :universe_id, current_user.universes.pluck(:name, :id), { include_blank: true }, class: 'materialize-textarea js-trigger-autosave-on-change' %>
<%= f.label 'Universe' %>
</div>
<!--
<div class="input-field">
@ -47,11 +47,64 @@
<%= f.label :privacy %>
</div>
-->
<!--
<div>
tags
<div class="input-field">
<div class="input-field">
<small><%= f.label :tags %></small>
<div class="chips chips-autocomplete chips-initial"></div>
<%= f.hidden_field :page_tags, value: @timeline.page_tags.join(PageTag::SUBMISSION_DELIMITER),
id: 'hidden_page_tags_value'
%>
<div class="help-text show-when-focused">
<i class="material-icons tiny">label</i>
Type and press enter to create a new tag.
</div>
<div class="tags-container show-when-focused">
<% @suggested_page_tags.each do |tag| %>
<%=
link_to '#', class: 'js-add-tag' do
%>
<span class="new badge grey left hoverable" data-badge-caption="<%= tag %>"></span>
<% end %>
<% end %>
</div>
<div class="clearfix"></div>
</div>
</div>
-->
<%= content_for :javascript do %>
function update_hidden_page_tag_value(e) {
var chips = M.Chips.getInstance($('.chips')).chipsData.map(function (c) {
return c['tag'];
});
var hidden_input = $('#hidden_page_tags_value').first();
hidden_input.val(chips.join('<%= PageTag::SUBMISSION_DELIMITER %>'));
$('form.timeline-meta-form').submit();
}
var chips = $('.chips-autocomplete').chips({
placeholder: 'Tag this page',
secondaryPlaceholder: '+ Tag',
autocompleteOptions: {
data: {
<% @timeline.page_tags.pluck(:tag).each do |tag| %>
'<%= tag %>': null,
<% end %>
},
limit: 100,
minLength: 1
},
data: [
<% @timeline.page_tags.pluck(:tag).each do |tag| %>
{tag: '<%= tag %>'},
<% end %>
],
onChipAdd: update_hidden_page_tag_value,
onChipDelete: update_hidden_page_tag_value
});
<% end %>
<div class="input-field">
<%= f.text_area :notes, class: 'materialize-textarea js-trigger-autosave-on-change' %>
<%= f.label :notes %>

View File

@ -5,18 +5,15 @@
<% if @timelines.any? %>
<div class="row">
<div class="col s12">
<br />
<%= link_to new_timeline_path, class: "hoverable btn right #{Timeline.color} white-text" do %>
<i class="material-icons left">add</i>
Create another timeline
<% end %>
<%= render partial: 'content/components/list_filter_bar', locals: { content_type: Timeline } %>
</div>
</div>
<% @timelines.each do |timeline| %>
<div class="row">
<div class="col s12">
<div class="js-content-cards-list row">
<% @timelines.each do |timeline| %>
<div class="col s12 js-content-card-container">
<%= link_to edit_timeline_path(timeline) do %>
<div class="hoverable card">
@ -31,7 +28,7 @@
</a>
-->
</div>
<div class="card-content black-text">
<div class="card-content black-text js-content-name">
<% if timeline.universe_id? %>
<p class="right">
<%= link_to timeline.universe, class: "#{Universe.color}-text" do %>
@ -50,7 +47,21 @@
Last edited <%= time_ago_in_words timeline.updated_at %> ago
</p>
<div>
<% timeline.page_tags.each do |page_tag| %>
<%=
link_to send(
"page_tag_timelines_path",
slug: PageTagService.slug_for(page_tag.tag)
) do
%>
<span class="new badge <%= params[:slug] == page_tag.slug ? Timeline.color : 'grey' %> right" data-badge-caption="<%= page_tag.tag %>"></span>
<% end %>
<% end %>
</div>
<div class="clearfix"></div>
</div>
<div class="card-action">
<%= link_to timeline_path(timeline), class: 'blue-text left' do %>
@ -67,8 +78,8 @@
<% end %>
</div>
</div>
<% end %>
<% end %>
</div>
<% if @show_scope_notice %>
<p class="center help-text">

View File

@ -4,7 +4,7 @@
<% next unless User.new.can_read?(entity) || (user_signed_in? && current_user.can_read?(entity)) %>
<div class="hoverable card horizontal" style="border: 1px solid <%= entity.class.hex_color %>">
<div class="card-image <%= entity.class.color %>" style="overflow: hidden">
<%= image_tag entity.random_public_image(format: :large), style: 'max-width: 280px; object-fit: cover; min-height: 100%' %>
<%= image_tag entity.first_public_image(format: :large), style: 'max-width: 280px; object-fit: cover; min-height: 100%' %>
</div>
<div class="card-stacked">
<div class="card-content">

View File

@ -101,7 +101,7 @@
<%= link_to universe do %>
<div class="hoverable card">
<div class="card-image">
<%= image_tag universe.random_public_image %>
<%= image_tag universe.first_public_image %>
<span class="card-title"><%= universe.name %></span>
</div>
<div class="card-content <%= Universe.color %> white-text fixed-card-content">
@ -140,7 +140,7 @@
<%= link_to page_collection do %>
<div class="hoverable card">
<div class="card-image">
<%= image_tag page_collection.random_public_image %>
<%= image_tag page_collection.first_public_image %>
<span class="card-title">
<i class="material-icons bordered-text <%= PageCollection.color %>-text"><%= PageCollection.icon %></i>
<%= page_collection.title %><br />
@ -166,7 +166,7 @@
<%= link_to page_collection do %>
<div class="hoverable card"">
<div class="card-image">
<%= image_tag page_collection.random_public_image, height: 250 %>
<%= image_tag page_collection.first_public_image, height: 250 %>
<span class="card-title">
<i class="material-icons bordered-text <%= PageCollection.color %>-text"><%= PageCollection.icon %></i>
<%= page_collection.title %><br />

View File

@ -20,6 +20,8 @@ Thredded.user_display_name_method = :forum_username
# When linking to a user, Thredded will use this lambda to spit out
# the path or url to your user. This lambda is evaluated in the view context.
Thredded.user_path = lambda do |user|
return nil if user && user.private_profile?
user_path = :"#{Thredded.user_class_name.underscore}_path"
main_app.respond_to?(user_path) ? main_app.send(user_path, user) : "/users/#{user.to_param}"
end
@ -149,10 +151,10 @@ Thredded.layout = 'layouts/forum'
# Change how users can choose to be notified, by adding notifiers here, or removing the initializer altogether
#
# default:
# Thredded.notifiers = [Thredded::EmailNotifier.new]
Thredded.notifiers = [Thredded::EmailNotifier.new]
#
# none:
Thredded.notifiers = []
# Thredded.notifiers = []
#
# add in (must install separate gem (under development) as well):
# Thredded.notifiers = [Thredded::EmailNotifier.new, Thredded::PushoverNotifier.new(ENV['PUSHOVER_APP_ID'])]

View File

@ -215,7 +215,9 @@ Rails.application.routes.draw do
get '/tagged/:slug', on: :collection, action: :index, as: :page_tag
end
end
resources :timelines, only: [:index, :show, :new, :update, :edit, :destroy]
resources :timelines, only: [:index, :show, :new, :update, :edit, :destroy] do
get '/tagged/:slug', action: :index, on: :collection, as: :page_tag
end
resources :timeline_events do
scope '/move', as: :move do
get 'up', to: 'timeline_events#move_up', on: :member

View File

@ -33,6 +33,11 @@ module Extensions
def random_public_image
"card-headers/discussions.jpg"
end
def first_public_image
"card-headers/discussions.jpg"
end
def name
self.title
end