diff --git a/plugins/discourse-local-dates/assets/javascripts/discourse-local-dates.js b/plugins/discourse-local-dates/assets/javascripts/discourse-local-dates.js index aad239019cf..e39b8434100 100644 --- a/plugins/discourse-local-dates/assets/javascripts/discourse-local-dates.js +++ b/plugins/discourse-local-dates/assets/javascripts/discourse-local-dates.js @@ -12,12 +12,16 @@ } var relativeTime; + + var dateAndTime = options.date; + if (options.time) { + dateAndTime = dateAndTime + " " + options.time; + } + if (options.timezone) { - relativeTime = moment - .tz(options.date + " " + options.time, options.timezone) - .utc(); + relativeTime = moment.tz(dateAndTime, options.timezone).utc(); } else { - relativeTime = moment.utc(options.date + " " + options.time); + relativeTime = moment.utc(dateAndTime); } if (relativeTime < moment().utc()) { @@ -35,9 +39,7 @@ } var previews = options.timezones.split("|").map(function(timezone) { - var dateTime = relativeTime - .tz(timezone) - .format(options.format || "LLL"); + var dateTime = relativeTime.tz(timezone).format(options.format); var timezoneParts = _formatTimezone(timezone); @@ -53,20 +55,23 @@ } }); - var displayTimezone = moment.tz.guess(); - var relativeTime = relativeTime.tz(displayTimezone); + var relativeTime = relativeTime.tz(options.displayedZone); var d = function(key) { var translated = I18n.t("discourse_local_dates.relative_dates." + key, { time: "LT" }); - translated = translated - .split("LT") - .map(function(w) { - return "[" + w + "]"; - }) - .join("LT"); - return translated; + + if (options.time) { + return translated + .split("LT") + .map(function(w) { + return "[" + w + "]"; + }) + .join("LT"); + } else { + return "[" + translated.replace(" LT", "") + "]"; + } }; var relativeFormat = { @@ -77,7 +82,7 @@ }; if ( - options.format !== "YYYY-MM-DD HH:mm:ss" && + options.calendar && relativeTime.isBetween( moment().subtract(1, "day"), moment().add(2, "day") @@ -97,7 +102,7 @@ var displayedTime = relativeTime.replace( "TZ", - _formatTimezone(displayTimezone).join(": ") + _formatTimezone(options.displayedZone).join(": ") ); $element @@ -119,12 +124,16 @@ var $this = $(this); var options = {}; - options.format = $this.attr("data-format"); + options.time = $this.attr("data-time"); + options.format = + $this.attr("data-format") || (options.time ? "LLL" : "LL"); options.date = $this.attr("data-date"); - options.time = $this.attr("data-time") || "00:00:00"; options.recurring = $this.attr("data-recurring"); - options.timezones = $this.attr("data-timezones"); + options.timezones = $this.attr("data-timezones") || "Etc/UTC"; options.timezone = $this.attr("data-timezone"); + options.calendar = ($this.attr("data-calendar") || "on") === "on"; + options.displayedZone = + $this.attr("data-displayed-zone") || moment.tz.guess(); processElement($this, options); }); diff --git a/plugins/discourse-local-dates/assets/javascripts/discourse/components/discourse-local-dates-create-form.js.es6 b/plugins/discourse-local-dates/assets/javascripts/discourse/components/discourse-local-dates-create-form.js.es6 index 6c2010bd152..0f65085993f 100644 --- a/plugins/discourse-local-dates/assets/javascripts/discourse/components/discourse-local-dates-create-form.js.es6 +++ b/plugins/discourse-local-dates/assets/javascripts/discourse/components/discourse-local-dates-create-form.js.es6 @@ -102,9 +102,9 @@ export default Ember.Component.extend({ let dateTime; if (!timeInferred) { - dateTime = moment.tz(`${date} ${time}`, timezone); + dateTime = moment.tz(`${date} ${time}`, timezone).utc(); } else { - dateTime = moment.tz(date, timezone); + dateTime = moment.tz(date, timezone).utc(); } let toDateTime; @@ -123,8 +123,13 @@ export default Ember.Component.extend({ timezone }; - config.time = dateTime.format(this.timeFormat); - config.toTime = toDateTime.format(this.timeFormat); + if (!timeInferred) { + config.time = dateTime.format(this.timeFormat); + } + + if (!toTimeInferred) { + config.toTime = toDateTime.format(this.timeFormat); + } if (toDate) { config.toDate = toDateTime.format(this.dateFormat); @@ -163,7 +168,6 @@ export default Ember.Component.extend({ text += `time=${config.time} `; } - text += `timezone="${config.timezone}" `; text += `format="${config.format}" `; text += `timezones="${config.timezones.join("|")}"`; text += `]`; @@ -176,7 +180,6 @@ export default Ember.Component.extend({ text += `time=${config.toTime} `; } - text += `timezone="${config.timezone}" `; text += `format="${config.format}" `; text += `timezones="${config.timezones.join("|")}"`; text += `]`; diff --git a/plugins/discourse-local-dates/assets/javascripts/lib/discourse-markdown/discourse-local-dates.js.es6 b/plugins/discourse-local-dates/assets/javascripts/lib/discourse-markdown/discourse-local-dates.js.es6 index 5b63766e1ae..231bae93469 100644 --- a/plugins/discourse-local-dates/assets/javascripts/lib/discourse-markdown/discourse-local-dates.js.es6 +++ b/plugins/discourse-local-dates/assets/javascripts/lib/discourse-markdown/discourse-local-dates.js.es6 @@ -7,8 +7,9 @@ function addLocalDate(buffer, matches, state) { date: null, time: null, timezone: null, - format: "YYYY-MM-DD HH:mm:ss", - timezones: "Etc/UTC" + format: null, + timezones: null, + displayedZone: null }; let parsed = parseBBCodeTag( @@ -18,18 +19,18 @@ function addLocalDate(buffer, matches, state) { ); config.date = parsed.attrs.date; + config.format = parsed.attrs.format; + config.calendar = parsed.attrs.calendar; config.time = parsed.attrs.time; config.timezone = parsed.attrs.timezone; config.recurring = parsed.attrs.recurring; - config.format = parsed.attrs.format || config.format; - config.timezones = parsed.attrs.timezones || config.timezones; + config.timezones = parsed.attrs.timezones; + config.displayedZone = parsed.attrs.displayedZone; token = new state.Token("span_open", "span", 1); token.attrs = [ ["class", "discourse-local-date"], - ["data-date", state.md.utils.escapeHtml(config.date)], - ["data-format", state.md.utils.escapeHtml(config.format)], - ["data-timezones", state.md.utils.escapeHtml(config.timezones)] + ["data-date", state.md.utils.escapeHtml(config.date)] ]; let dateTime = config.date; @@ -38,6 +39,31 @@ function addLocalDate(buffer, matches, state) { dateTime = `${dateTime} ${config.time}`; } + if (config.format) { + token.attrs.push(["data-format", state.md.utils.escapeHtml(config.format)]); + } + + if (config.calendar) { + token.attrs.push([ + "data-calendar", + state.md.utils.escapeHtml(config.calendar) + ]); + } + + if (config.displayedZone) { + token.attrs.push([ + "data-displayed-zone", + state.md.utils.escapeHtml(config.displayedZone) + ]); + } + + if (config.timezones) { + token.attrs.push([ + "data-timezones", + state.md.utils.escapeHtml(config.timezones) + ]); + } + if (config.timezone) { token.attrs.push([ "data-timezone", @@ -54,10 +80,11 @@ function addLocalDate(buffer, matches, state) { state.md.utils.escapeHtml(config.recurring) ]); } + buffer.push(token); let emailPreview; - const emailTimezone = config.timezones.split("|")[0]; + const emailTimezone = (config.timezones || "Etc/UTC").split("|")[0]; const formattedDateTime = dateTime.tz(emailTimezone).format(config.format); const formattedTimezone = emailTimezone.replace("/", ": ").replace("_", " "); @@ -66,7 +93,6 @@ function addLocalDate(buffer, matches, state) { } else { emailPreview = `${formattedDateTime} (${formattedTimezone})`; } - token.attrs.push(["data-email-preview", emailPreview]); token = new state.Token("text", "", 0); @@ -74,6 +100,7 @@ function addLocalDate(buffer, matches, state) { buffer.push(token); token = new state.Token("span_close", "span", -1); + buffer.push(token); } diff --git a/plugins/discourse-local-dates/plugin.rb b/plugins/discourse-local-dates/plugin.rb index 54fe359ff00..b5ce949e9b5 100644 --- a/plugins/discourse-local-dates/plugin.rb +++ b/plugins/discourse-local-dates/plugin.rb @@ -52,7 +52,6 @@ after_initialize do on(:reduce_cooked) do |fragment| fragment.css(".discourse-local-date").each do |container| - if container.attributes["data-email-preview"] preview = container.attributes["data-email-preview"].value container.content = preview diff --git a/plugins/discourse-local-dates/spec/integration/local_dates_spec.rb b/plugins/discourse-local-dates/spec/integration/local_dates_spec.rb index 738eb8f332b..abd83c0b4e2 100644 --- a/plugins/discourse-local-dates/spec/integration/local_dates_spec.rb +++ b/plugins/discourse-local-dates/spec/integration/local_dates_spec.rb @@ -2,13 +2,13 @@ require 'rails_helper' RSpec.describe "Local Dates" do before do - freeze_time + freeze_time DateTime.parse('2018-11-10 12:00') end it "should work without timezone" do - post = Fabricate(:post, raw: <<~SQL) + post = Fabricate(:post, raw: <<~TXT) [date=2018-05-08 time=22:00 format="L LTS" timezones="Europe/Paris|America/Los_Angeles"] - SQL + TXT cooked = post.cooked @@ -26,9 +26,9 @@ RSpec.describe "Local Dates" do end it "should work with timezone" do - post = Fabricate(:post, raw: <<~SQL) + post = Fabricate(:post, raw: <<~TXT) [date=2018-05-08 time=22:00 format="L LTS" timezone="Asia/Calcutta" timezones="Europe/Paris|America/Los_Angeles"] - SQL + TXT cooked = post.cooked @@ -37,13 +37,44 @@ RSpec.describe "Local Dates" do end it 'requires the right attributes to convert to a local date' do - post = Fabricate(:post, raw: <<~SQL) + post = Fabricate(:post, raw: <<~TXT) [date] - SQL + TXT cooked = post.cooked expect(post.cooked).to include("

[date]

") expect(cooked).to_not include('data-date=') end + + it 'requires the right attributes to convert to a local date' do + post = Fabricate(:post, raw: <<~TXT) + [date] + TXT + + cooked = post.cooked + + expect(post.cooked).to include("

[date]

") + expect(cooked).to_not include('data-date=') + end + + it 'it works with only a date and time' do + raw = "[date=2018-11-01 time=12:00]" + cooked = Fabricate(:post, raw: raw).cooked + expect(cooked).to include('data-date="2018-11-01"') + expect(cooked).to include('data-time="12:00"') + end + + it 'doesn’t include format by default' do + raw = "[date=2018-11-01 time=12:00]" + cooked = Fabricate(:post, raw: raw).cooked + expect(cooked).not_to include('data-format=') + end + + it 'doesn’t include timezone by default' do + raw = "[date=2018-11-01 time=12:00]" + cooked = Fabricate(:post, raw: raw).cooked + + expect(cooked).not_to include("data-timezone=") + end end diff --git a/plugins/discourse-local-dates/spec/lib/pretty_text_spec.rb b/plugins/discourse-local-dates/spec/lib/pretty_text_spec.rb index 216ac55d258..3a50e304fdb 100644 --- a/plugins/discourse-local-dates/spec/lib/pretty_text_spec.rb +++ b/plugins/discourse-local-dates/spec/lib/pretty_text_spec.rb @@ -1,24 +1,52 @@ require 'rails_helper' +def generate_html(text, opts = {}) + output = "

" +end + describe PrettyText do - it 'uses a simplified syntax in emails' do + before do freeze_time - cooked = PrettyText.cook <<~MD - [date=2018-05-08 time=22:00 format=LLL timezones="Europe/Paris|America/Los_Angeles"] - MD - cooked_mail = <<~HTML -

May 9, 2018 12:00 AM (Europe: Paris)

- HTML + end - expect(PrettyText.format_for_email(cooked)).to match_html(cooked_mail) + context 'emails simplified rendering' do + it 'works with default markup' do + cooked = PrettyText.cook("[date=2018-05-08]") + cooked_mail = generate_html("2018-05-08T00:00:00Z (Etc: UTC)", + date: "2018-05-08", + email_preview: "2018-05-08T00:00:00Z (Etc: UTC)" + ) - cooked = PrettyText.cook <<~MD - [date=2018-05-08 format=LLL timezone="Europe/Berlin" timezones="Europe/Paris|America/Los_Angeles"] - MD - cooked_mail = <<~HTML -

May 8, 2018 12:00 AM (Europe: Paris)

- HTML + expect(PrettyText.format_for_email(cooked)).to match_html(cooked_mail) + end - expect(PrettyText.format_for_email(cooked)).to match_html(cooked_mail) + it 'works with format' do + cooked = PrettyText.cook("[date=2018-05-08 format=LLLL]") + cooked_mail = generate_html("Tuesday, May 8, 2018 12:00 AM (Etc: UTC)", + date: "2018-05-08", + email_preview: "Tuesday, May 8, 2018 12:00 AM (Etc: UTC)", + format: "LLLL" + ) + + expect(PrettyText.format_for_email(cooked)).to match_html(cooked_mail) + end + + it 'works with time' do + cooked = PrettyText.cook("[date=2018-05-08 time=20:00:00]") + cooked_mail = generate_html("2018-05-08T20:00:00Z (Etc: UTC)", + date: "2018-05-08", + email_preview: "2018-05-08T20:00:00Z (Etc: UTC)", + time: "20:00:00" + ) + + expect(PrettyText.format_for_email(cooked)).to match_html(cooked_mail) + end end end diff --git a/plugins/discourse-local-dates/test/javascripts/acceptance/local-dates-composer-test.js.es6 b/plugins/discourse-local-dates/test/javascripts/acceptance/local-dates-composer-test.js.es6 new file mode 100644 index 00000000000..4d28a71b599 --- /dev/null +++ b/plugins/discourse-local-dates/test/javascripts/acceptance/local-dates-composer-test.js.es6 @@ -0,0 +1,50 @@ +import { acceptance } from "helpers/qunit-helpers"; + +acceptance("Local Dates - composer", { + loggedIn: true, + settings: { discourse_local_dates_enabled: true } +}); + +test("composer bbcode", async assert => { + const getAttr = attr => { + return find(".d-editor-preview .discourse-local-date.cooked-date").attr( + `data-${attr}` + ); + }; + + await visit("/"); + await click("#create-topic"); + + await fillIn( + ".d-editor-input", + '[date=2017-10-23 time=01:30:00 displayedZone="America/Chicago" format="LLLL" calendar="off" recurring="1.weeks" timezone="Asia/Calcutta" timezones="Europe/Paris|America/Los_Angeles"]' + ); + + assert.equal(getAttr("date"), "2017-10-23", "it has the correct date"); + assert.equal(getAttr("time"), "01:30:00", "it has the correct time"); + assert.equal( + getAttr("displayed-zone"), + "America/Chicago", + "it has the correct displayed zone" + ); + assert.equal(getAttr("format"), "LLLL", "it has the correct format"); + assert.equal( + getAttr("timezones"), + "Europe/Paris|America/Los_Angeles", + "it has the correct timezones" + ); + assert.equal(getAttr("recurring"), "1.weeks", "it has the correct recurring"); + assert.equal( + getAttr("timezone"), + "Asia/Calcutta", + "it has the correct timezone" + ); + + await fillIn( + ".d-editor-input", + '[date=2017-10-24 format="LL" timezone="Asia/Calcutta" timezones="Europe/Paris|America/Los_Angeles"]' + ); + + assert.equal(getAttr("date"), "2017-10-24", "it has the correct date"); + assert.notOk(getAttr("time"), "it doesn’t have time"); +}); diff --git a/plugins/discourse-local-dates/test/javascripts/acceptance/local-dates-test.js.es6 b/plugins/discourse-local-dates/test/javascripts/acceptance/local-dates-test.js.es6 index a9e9b2461ac..2d7d5e500f2 100644 --- a/plugins/discourse-local-dates/test/javascripts/acceptance/local-dates-test.js.es6 +++ b/plugins/discourse-local-dates/test/javascripts/acceptance/local-dates-test.js.es6 @@ -1,62 +1,254 @@ import { acceptance } from "helpers/qunit-helpers"; -import { clearPopupMenuOptionsCallback } from "discourse/controllers/composer"; + +const sandbox = sinon.createSandbox(); acceptance("Local Dates", { loggedIn: true, settings: { discourse_local_dates_enabled: true }, beforeEach() { - clearPopupMenuOptionsCallback(); + freezeDateAndZone(); }, afterEach() { - sinon.restore(); + sandbox.restore(); + moment.tz.setDefault(); } }); -test("at removal", assert => { - let now = moment("2018-06-20").valueOf(); - let timezone = moment.tz.guess(); +const DEFAULT_DATE = "2018-06-20"; +const DEFAULT_ZONE = "Europe/Paris"; - sinon.useFakeTimers(now); +function advance(count, unit = "days") { + return moment(DEFAULT_DATE) + .add(count, unit) + .format("YYYY-MM-DD"); +} - let html = ``; +function rewind(count, unit = "days") { + return moment(DEFAULT_DATE) + .subtract(count, unit) + .format("YYYY-MM-DD"); +} - let yesterday = $(html.replace("DATE", "2018-06-19")); - yesterday.applyLocalDates(); +function freezeDateAndZone(date, zone, cb) { + date = date || DEFAULT_DATE; + zone = zone || DEFAULT_ZONE; - assert.equal(yesterday.text(), "Yesterday 2:42 PM"); + sandbox.restore(); + sandbox.stub(moment.tz, "guess"); + moment.tz.guess.returns(zone); - let today = $(html.replace("DATE", "2018-06-20")); - today.applyLocalDates(); + const now = moment(date).valueOf(); + sandbox.useFakeTimers(now); - assert.equal(today.text(), "Today 2:42 PM"); + if (cb) { + cb(); - let tomorrow = $(html.replace("DATE", "2018-06-21")); - tomorrow.applyLocalDates(); + moment.tz.guess.returns(DEFAULT_ZONE); + sandbox.useFakeTimers(moment(DEFAULT_DATE).valueOf()); + } +} - assert.equal(tomorrow.text(), "Tomorrow 2:42 PM"); -}); +function generateHTML(options = {}) { + let output = ` { - await visit("/"); - await click("#create-topic"); + output += ` data-date="${options.date || DEFAULT_DATE}"`; + if (options.format) output += ` data-format="${options.format}"`; + if (options.time) output += ` data-time="${options.time}"`; + if (options.calendar) output += ` data-calendar="${options.calendar}"`; + if (options.recurring) output += ` data-recurring="${options.recurring}"`; + if (options.displayedZone) + output += ` data-displayed-zone="${options.displayedZone}"`; - await fillIn( - ".d-editor-input", - '[date=2017-10-23 time=01:30:00 format="LL" timezone="Asia/Calcutta" timezones="Europe/Paris|America/Los_Angeles"]' - ); + return (output += ">"); +} - assert.ok( - exists(".d-editor-preview .discourse-local-date.past.cooked-date"), - "it should contain the cooked output for date & time inputs" - ); +test("default format - time specified", assert => { + const html = generateHTML({ date: advance(3), time: "00:00" }); + const transformed = $(html).applyLocalDates(); - await fillIn( - ".d-editor-input", - '[date=2017-10-23 format="LL" timezone="Asia/Calcutta" timezones="Europe/Paris|America/Los_Angeles"]' - ); - - assert.ok( - exists(".d-editor-preview .discourse-local-date.past.cooked-date"), - "it should contain the cooked output for date only input" + assert.equal( + transformed.text(), + "June 23, 2018 2:00 AM", + "it uses moment LLL format" ); }); + +test("default format - no time specified", assert => { + const html = generateHTML({ date: advance(3) }); + const transformed = $(html).applyLocalDates(); + + assert.equal( + transformed.text(), + "June 23, 2018", + "it uses moment LL format as default if not time is specified" + ); +}); + +test("today", assert => { + const html = generateHTML({ time: "14:00" }); + const transformed = $(html).applyLocalDates(); + + assert.equal(transformed.text(), "Today 4:00 PM", "it display Today"); +}); + +test("today - no time", assert => { + const html = generateHTML(); + const transformed = $(html).applyLocalDates(); + + assert.equal(transformed.text(), "Today", "it display Today without time"); +}); + +test("yesterday", assert => { + const html = generateHTML({ date: rewind(1), time: "14:00" }); + const transformed = $(html).applyLocalDates(); + + assert.equal( + transformed.text(), + "Yesterday 4:00 PM", + "it displays yesterday" + ); +}); + +QUnit.skip("yesterday - no time", assert => { + const html = generateHTML({ date: rewind(1) }); + const transformed = $(html).applyLocalDates(); + + assert.equal( + transformed.text(), + "Yesterday", + "it displays yesterday without time" + ); +}); + +test("tomorrow", assert => { + const html = generateHTML({ date: advance(1), time: "14:00" }); + const transformed = $(html).applyLocalDates(); + + assert.equal(transformed.text(), "Tomorrow 4:00 PM", "it displays tomorrow"); +}); + +test("tomorrow - no time", assert => { + const html = generateHTML({ date: advance(1) }); + const transformed = $(html).applyLocalDates(); + + assert.equal( + transformed.text(), + "Tomorrow", + "it displays tomorrow without time" + ); +}); + +test("today - no time with different zones", assert => { + const html = generateHTML(); + let transformed = $(html).applyLocalDates(); + + assert.equal(transformed.text(), "Today", "it displays today without time"); + + freezeDateAndZone(rewind(12, "hours"), "Pacific/Auckland", () => { + transformed = $(html).applyLocalDates(); + assert.equal( + transformed.text(), + "Tomorrow", + "it displays Tomorrow without time" + ); + }); +}); + +test("calendar off", assert => { + const html = generateHTML({ calendar: "off", time: "14:00" }); + const transformed = $(html).applyLocalDates(); + + assert.equal( + transformed.text(), + "June 20, 2018 4:00 PM", + "it displays the date without Today" + ); +}); + +test("recurring", assert => { + const html = generateHTML({ recurring: "1.week", time: "14:00" }); + let transformed = $(html).applyLocalDates(); + + assert.equal( + transformed.text(), + "Today 4:00 PM", + "it displays the next occurrence" + ); + + freezeDateAndZone(advance(1), () => { + transformed = $(html).applyLocalDates(); + + assert.equal( + transformed.text(), + "June 27, 2018 4:00 PM", + "it displays the next occurrence" + ); + }); +}); + +test("displayedZone", assert => { + const html = generateHTML({ + date: advance(3), + displayedZone: "Etc/UTC", + time: "14:00" + }); + const transformed = $(html).applyLocalDates(); + + assert.equal( + transformed.text(), + "June 23, 2018 2:00 PM", + "it forces display in the given timezone" + ); +}); + +test("format", assert => { + const html = generateHTML({ + date: advance(3), + format: "YYYY | MM - DD" + }); + const transformed = $(html).applyLocalDates(); + + assert.equal( + transformed.text(), + "2018 | 06 - 23", + "it uses the given format" + ); +}); + +test("test utils", assert => { + assert.equal( + moment().format("LLLL"), + moment(DEFAULT_DATE).format("LLLL"), + "it has defaults" + ); + assert.equal(moment.tz.guess(), DEFAULT_ZONE, "it has defaults"); + + freezeDateAndZone(advance(1), DEFAULT_ZONE, () => { + assert.equal( + moment().format("LLLL"), + moment(DEFAULT_DATE) + .add(1, "days") + .format("LLLL"), + "it applies new time" + ); + assert.equal(moment.tz.guess(), DEFAULT_ZONE); + }); + + assert.equal( + moment().format("LLLL"), + moment(DEFAULT_DATE).format("LLLL"), + "it restores time" + ); + + freezeDateAndZone(advance(1), "Pacific/Auckland", () => { + assert.equal( + moment().format("LLLL"), + moment(DEFAULT_DATE) + .add(1, "days") + .format("LLLL") + ); + assert.equal(moment.tz.guess(), "Pacific/Auckland", "it applies new zone"); + }); + + assert.equal(moment.tz.guess(), DEFAULT_ZONE, "it restores zone"); +});