diff --git a/app/controllers/admin/articles_controller.rb b/app/controllers/admin/articles_controller.rb index bdd196609..197375c74 100644 --- a/app/controllers/admin/articles_controller.rb +++ b/app/controllers/admin/articles_controller.rb @@ -40,7 +40,7 @@ module Admin def update article = Article.find(params[:id]) - if article.update(article_params) + if article.update(article_params.merge(admin_update: true)) flash[:success] = I18n.t("admin.articles_controller.saved") else flash[:danger] = article.errors_as_sentence diff --git a/app/controllers/articles_controller.rb b/app/controllers/articles_controller.rb index c22caf85c..3654f4f0c 100644 --- a/app/controllers/articles_controller.rb +++ b/app/controllers/articles_controller.rb @@ -146,7 +146,6 @@ class ArticlesController < ApplicationController def create authorize Article - @user = current_user article = Articles::Creator.call(@user, article_params_json) @@ -160,7 +159,6 @@ class ArticlesController < ApplicationController def update authorize @article @user = @article.user || current_user - updated = Articles::Updater.call(@user, @article, article_params_json) respond_to do |format| @@ -303,6 +301,8 @@ class ArticlesController < ApplicationController # TODO: refactor all of this update logic into the Articles::Updater possibly, # ideally there should only be one place to handle the update logic def article_params_json + return @article_params_json if @article_params_json + params.require(:article) # to trigger the correct exception in case `:article` is missing params["article"].transform_keys!(&:underscore) @@ -312,7 +312,7 @@ class ArticlesController < ApplicationController else %i[ title body_markdown main_image published description video_thumbnail_url - tag_list canonical_url series collection_id archived + tag_list canonical_url series collection_id archived published_at timezone ] end @@ -325,7 +325,14 @@ class ArticlesController < ApplicationController allowed_params << :organization_id end - params.require(:article).permit(allowed_params) + time_zone_str = params["article"].delete("timezone") + if params["article"]["published_at"] + time_zone = Time.find_zone(time_zone_str) + time_zone ||= Time.find_zone("UTC") + params["article"]["published_at"] = time_zone.parse(params["article"]["published_at"]) + end + + @article_params_json = params.require(:article).permit(allowed_params) end def allowed_to_change_org_id? diff --git a/app/controllers/stories_controller.rb b/app/controllers/stories_controller.rb index d973d0ca3..2016df528 100644 --- a/app/controllers/stories_controller.rb +++ b/app/controllers/stories_controller.rb @@ -274,7 +274,7 @@ class StoriesController < ApplicationController end def permission_denied? - !@article.published && params[:preview] != @article.password + (!@article.published || @article.scheduled?) && params[:preview] != @article.password end def assign_co_authors diff --git a/app/decorators/article_decorator.rb b/app/decorators/article_decorator.rb index 7fe4f930b..f5b31751c 100644 --- a/app/decorators/article_decorator.rb +++ b/app/decorators/article_decorator.rb @@ -10,8 +10,19 @@ class ArticleDecorator < ApplicationDecorator DataInfo.to_json(object: cached_user, class_name: "User", id: user_id, style: "full") end + def current_state + state = if !published? + "unpublished" + elsif scheduled? + "scheduled" + else + "published" + end + ActiveSupport::StringInquirer.new(state) + end + def current_state_path - published ? "/#{username}/#{slug}" : "/#{username}/#{slug}?preview=#{password}" + current_state.published? ? "/#{username}/#{slug}" : "/#{username}/#{slug}?preview=#{password}" end def processed_canonical_url diff --git a/app/javascript/article-form/articleForm.jsx b/app/javascript/article-form/articleForm.jsx index 94f3e9a30..9838e3793 100644 --- a/app/javascript/article-form/articleForm.jsx +++ b/app/javascript/article-form/articleForm.jsx @@ -18,7 +18,7 @@ import { getOSKeyboardModifierKeyString } from '@utilities/runtime'; /* global activateRunkitTags */ /* - Although the state fields: id, description, canonicalUrl, series, allSeries and + Although the state fields: id, description, canonicalUrl, publishedAt, series, allSeries and editing are not used in this file, they are important to the editor. */ @@ -63,6 +63,7 @@ export class ArticleForm extends Component { article: PropTypes.string.isRequired, organizations: PropTypes.string, siteLogo: PropTypes.string.isRequired, + schedulingEnabled: PropTypes.bool.isRequired, }; static defaultProps = { @@ -71,7 +72,7 @@ export class ArticleForm extends Component { constructor(props) { super(props); - const { article, version, siteLogo } = this.props; + const { article, version, siteLogo, schedulingEnabled } = this.props; let { organizations } = this.props; this.article = JSON.parse(article); organizations = organizations ? JSON.parse(organizations) : null; @@ -102,10 +103,13 @@ export class ArticleForm extends Component { tagList: this.article.cached_tag_list || '', description: '', // eslint-disable-line react/no-unused-state canonicalUrl: this.article.canonical_url || '', // eslint-disable-line react/no-unused-state + publishedAt: this.article.published_at || '', // eslint-disable-line react/no-unused-state + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || '', // eslint-disable-line react/no-unused-state series: this.article.series || '', // eslint-disable-line react/no-unused-state allSeries: this.article.all_series || [], // eslint-disable-line react/no-unused-state bodyMarkdown: this.article.body_markdown || '', published: this.article.published || false, + schedulingEnabled, previewShowing: false, previewLoading: false, previewResponse: { processed_html: '' }, @@ -331,6 +335,7 @@ export class ArticleForm extends Component { tagList: this.article.cached_tag_list || '', description: '', // eslint-disable-line react/no-unused-state canonicalUrl: this.article.canonical_url || '', // eslint-disable-line react/no-unused-state + publishedAt: this.article.published_at || '', // eslint-disable-line react/no-unused-state series: this.article.series || '', // eslint-disable-line react/no-unused-state allSeries: this.article.all_series || [], // eslint-disable-line react/no-unused-state bodyMarkdown: this.article.body_markdown || '', @@ -392,9 +397,11 @@ export class ArticleForm extends Component { tagList, bodyMarkdown, published, + publishedAt, previewShowing, previewLoading, previewResponse, + schedulingEnabled, submitting, organizations, organizationId, @@ -491,6 +498,8 @@ export class ArticleForm extends Component { now; + + const saveButtonText = schedule + ? 'Schedule' + : published || isVersion1 + ? 'Save changes' + : 'Publish'; + return (
{!(published || isVersion1) && ( @@ -59,6 +71,7 @@ export const EditorActions = ({ {isVersion2 && ( { let publishedField = ''; let existingSeries = ''; + let publishedAtField = ''; + const publishedDate = new Date(publishedAt); + const currentDate = new Date(); + + const localPublishedAt = toISOStringLocal(publishedDate); + const minPublishedAt = toISOStringLocal(currentDate); + + const readonlyPublishedAt = published && publishedDate < currentDate; if (allSeries.length > 0) { const seriesNames = allSeries.map((name, index) => { @@ -66,6 +92,34 @@ export const Options = ({
); } + if (schedulingEnabled && !readonlyPublishedAt) { + publishedAtField = ( +
+ + + +
+ ); + } + return (
+ {publishedAtField}