From d9d722d74b2cb9bf30fa4cc98af6ddbeca003ebc Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 28 Oct 2022 12:56:32 +0200
Subject: [PATCH] Change admin announcement edition interface to use
datetime-local (#18321)
* Change admin announcement edition interface to use datetime-local
* Dynamically set announcement stop date as required if start date is set, set minimum date for stop date
* Change `all_day` to not be bound to presence of time-range
* Add pattern and placeholder as minimal fallback for browsers not supporting datetime-local
* Display datetime-local inputs as local time
Co-authored-by: Eugen Rochko <eugen@zeonfederated.com>
---
app/javascript/packs/admin.js | 54 ++++++++++++++++++++
app/javascript/styles/mastodon/forms.scss | 5 +-
app/models/announcement.rb | 5 --
app/views/admin/announcements/edit.html.haml | 7 ++-
app/views/admin/announcements/new.html.haml | 9 ++--
5 files changed, 69 insertions(+), 11 deletions(-)
diff --git a/app/javascript/packs/admin.js b/app/javascript/packs/admin.js
index b733d6b186..de86e0e117 100644
--- a/app/javascript/packs/admin.js
+++ b/app/javascript/packs/admin.js
@@ -2,6 +2,24 @@ import './public-path';
import { delegate } from '@rails/ujs';
import ready from '../mastodon/ready';
+const setAnnouncementEndsAttributes = (target) => {
+ const valid = target?.value && target?.validity?.valid;
+ const element = document.querySelector('input[type="datetime-local"]#announcement_ends_at');
+ if (valid) {
+ element.classList.remove('optional');
+ element.required = true;
+ element.min = target.value;
+ } else {
+ element.classList.add('optional');
+ element.removeAttribute('required');
+ element.removeAttribute('min');
+ }
+};
+
+delegate(document, 'input[type="datetime-local"]#announcement_starts_at', 'change', ({ target }) => {
+ setAnnouncementEndsAttributes(target);
+});
+
const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]';
const showSelectAll = () => {
@@ -141,6 +159,20 @@ const onChangeRegistrationMode = (target) => {
});
};
+const convertUTCDateTimeToLocal = (value) => {
+ const date = new Date(value + 'Z');
+ const twoChars = (x) => (x.toString().padStart(2, '0'));
+ return `${date.getFullYear()}-${twoChars(date.getMonth()+1)}-${twoChars(date.getDate())}T${twoChars(date.getHours())}:${twoChars(date.getMinutes())}`;
+};
+
+const convertLocalDatetimeToUTC = (value) => {
+ const re = /^([0-9]{4,})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2})/;
+ const match = re.exec(value);
+ const date = new Date(match[1], match[2] - 1, match[3], match[4], match[5]);
+ const fullISO8601 = date.toISOString();
+ return fullISO8601.slice(0, fullISO8601.indexOf('T') + 6);
+};
+
delegate(document, '#form_admin_settings_registrations_mode', 'change', ({ target }) => onChangeRegistrationMode(target));
ready(() => {
@@ -163,6 +195,28 @@ ready(() => {
}
});
+ [].forEach.call(document.querySelectorAll('input[type="datetime-local"]'), element => {
+ if (element.value) {
+ element.value = convertUTCDateTimeToLocal(element.value);
+ }
+ if (element.placeholder) {
+ element.placeholder = convertUTCDateTimeToLocal(element.placeholder);
+ }
+ });
+
+ delegate(document, 'form', 'submit', ({ target }) => {
+ [].forEach.call(target.querySelectorAll('input[type="datetime-local"]'), element => {
+ if (element.value && element.validity.valid) {
+ element.value = convertLocalDatetimeToUTC(element.value);
+ }
+ });
+ });
+
+ const announcementStartsAt = document.querySelector('input[type="datetime-local"]#announcement_starts_at');
+ if (announcementStartsAt) {
+ setAnnouncementEndsAttributes(announcementStartsAt);
+ }
+
const React = require('react');
const ReactDOM = require('react-dom');
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index 25c9c9fe55..a3ddc76362 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -405,6 +405,7 @@ code {
input[type="email"],
input[type="password"],
input[type="url"],
+ input[type="datetime-local"],
textarea {
box-sizing: border-box;
font-size: 16px;
@@ -445,7 +446,8 @@ code {
input[type="text"],
input[type="number"],
input[type="email"],
- input[type="password"] {
+ input[type="password"],
+ input[type="datetime-local"] {
&:focus:invalid:not(:placeholder-shown),
&:required:invalid:not(:placeholder-shown) {
border-color: lighten($error-red, 12%);
@@ -461,6 +463,7 @@ code {
input[type="number"],
input[type="email"],
input[type="password"],
+ input[type="datetime-local"],
textarea,
select {
border-color: lighten($error-red, 12%);
diff --git a/app/models/announcement.rb b/app/models/announcement.rb
index bedced9ded..4b2cb4c6d0 100644
--- a/app/models/announcement.rb
+++ b/app/models/announcement.rb
@@ -31,7 +31,6 @@ class Announcement < ApplicationRecord
validates :starts_at, presence: true, if: -> { ends_at.present? }
validates :ends_at, presence: true, if: -> { starts_at.present? }
- before_validation :set_all_day
before_validation :set_published, on: :create
def to_log_human_identifier
@@ -89,10 +88,6 @@ class Announcement < ApplicationRecord
private
- def set_all_day
- self.all_day = false if starts_at.blank? || ends_at.blank?
- end
-
def set_published
return unless scheduled_at.blank? || scheduled_at.past?
diff --git a/app/views/admin/announcements/edit.html.haml b/app/views/admin/announcements/edit.html.haml
index 5f56db5e7f..e20a839b95 100644
--- a/app/views/admin/announcements/edit.html.haml
+++ b/app/views/admin/announcements/edit.html.haml
@@ -1,12 +1,15 @@
- content_for :page_title do
= t('.title')
+- content_for :header_tags do
+ = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
+
= simple_form_for @announcement, url: admin_announcement_path(@announcement) do |f|
= render 'shared/error_messages', object: @announcement
.fields-group
- = f.input :starts_at, include_blank: true, wrapper: :with_block_label
- = f.input :ends_at, include_blank: true, wrapper: :with_block_label
+ = f.input :starts_at, include_blank: true, wrapper: :with_block_label, html5: true, input_html: { pattern: '[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}(:[0-9]{2}){1,2}', placeholder: Time.now.strftime('%FT%R') }
+ = f.input :ends_at, include_blank: true, wrapper: :with_block_label, html5: true, input_html: { pattern: '[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}(:[0-9]{2}){1,2}', placeholder: Time.now.strftime('%FT%R') }
.fields-group
= f.input :all_day, as: :boolean, wrapper: :with_label
diff --git a/app/views/admin/announcements/new.html.haml b/app/views/admin/announcements/new.html.haml
index a5298c5f66..d5881b7aa1 100644
--- a/app/views/admin/announcements/new.html.haml
+++ b/app/views/admin/announcements/new.html.haml
@@ -1,12 +1,15 @@
- content_for :page_title do
= t('.title')
+- content_for :header_tags do
+ = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
+
= simple_form_for @announcement, url: admin_announcements_path do |f|
= render 'shared/error_messages', object: @announcement
.fields-group
- = f.input :starts_at, include_blank: true, wrapper: :with_block_label
- = f.input :ends_at, include_blank: true, wrapper: :with_block_label
+ = f.input :starts_at, include_blank: true, wrapper: :with_block_label, html5: true, input_html: { pattern: '[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}(:[0-9]{2}){1,2}', placeholder: Time.now.strftime('%FT%R') }
+ = f.input :ends_at, include_blank: true, wrapper: :with_block_label, html5: true, input_html: { pattern: '[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}(:[0-9]{2}){1,2}', placeholder: Time.now.strftime('%FT%R') }
.fields-group
= f.input :all_day, as: :boolean, wrapper: :with_label
@@ -15,7 +18,7 @@
= f.input :text, wrapper: :with_block_label
.fields-group
- = f.input :scheduled_at, include_blank: true, wrapper: :with_block_label
+ = f.input :scheduled_at, include_blank: true, wrapper: :with_block_label, html5: true, input_html: { pattern: '[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}(:[0-9]{2}){1,2}', placeholder: Time.now.strftime('%FT%R') }
.actions
= f.button :button, t('.create'), type: :submit
--
GitLab