From 9bacc9861c988482dcf90a426ea348168179e520 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Thu, 15 Jun 2023 14:14:48 +0200 Subject: [PATCH] feat(NcDatetime): Add new component for displaying a timestamp as time from now This implements a component showing for displaying timestamps like *x seconds ago* without the need of huge libraries like moment.js Signed-off-by: Ferdinand Thiessen --- l10n/messages.pot | 11 + package.json | 10 +- src/components/NcDatetime/NcDatetime.vue | 258 ++++++++++++++++++ src/components/NcDatetime/index.js | 23 ++ src/components/index.js | 1 + .../components/NcDatetime/NcDatetime.spec.js | 209 ++++++++++++++ 6 files changed, 507 insertions(+), 5 deletions(-) create mode 100644 src/components/NcDatetime/NcDatetime.vue create mode 100644 src/components/NcDatetime/index.js create mode 100644 tests/unit/components/NcDatetime/NcDatetime.spec.js diff --git a/l10n/messages.pot b/l10n/messages.pot index 2c6f50bda6..aff566a148 100644 --- a/l10n/messages.pot +++ b/l10n/messages.pot @@ -8,6 +8,9 @@ msgstr "" msgid "{tag} (restricted)" msgstr "" +msgid "a few seconds ago" +msgstr "" + msgid "Actions" msgstr "" @@ -179,6 +182,14 @@ msgstr "" msgid "Search results" msgstr "" +#. FOR TRANSLATORS: If possible in your language an even shorter version of 'a few seconds ago' +msgid "sec. ago" +msgstr "" + +#. FOR TRANSLATORS: Shorter version of 'a few seconds ago' +msgid "seconds ago" +msgstr "" + msgid "Select a tag" msgstr "" diff --git a/package.json b/package.json index f90f7a050f..7d6e4c34fb 100644 --- a/package.json +++ b/package.json @@ -21,15 +21,15 @@ "l10n:extract": "node build/extract-l10n.js", "lint": "eslint --ext .js,.vue src", "lint:fix": "eslint --ext .js,.vue src --fix", - "test": "jest --verbose", - "test:coverage": "jest --verbose --coverage --no-cache", + "test": "TZ=UTC jest --verbose", + "test:coverage": "TZ=UTC jest --verbose --coverage --no-cache", "stylelint": "stylelint src/**/*.vue src/**/*.scss src/**/*.css", "stylelint:fix": "stylelint src/**/*.vue src/**/*.scss src/**/*.css --fix", "styleguide": "vue-styleguidist server", "styleguide:build": "vue-styleguidist build", - "cypress": "cypress run --component", - "cypress:gui": "cypress open --component", - "cypress:update-snapshots": "cypress run --component --spec cypress/visual/**/*.cy.js --env type=base --config screenshotsFolder=cypress/snapshots/base" + "cypress": "TZ=UTC cypress run --component", + "cypress:gui": "TZ=UTC cypress open --component", + "cypress:update-snapshots": "TZ=UTC cypress run --component --spec cypress/visual/**/*.cy.js --env type=base --config screenshotsFolder=cypress/snapshots/base" }, "main": "dist/ncvuecomponents.js", "module": "dist/index.module.js", diff --git a/src/components/NcDatetime/NcDatetime.vue b/src/components/NcDatetime/NcDatetime.vue new file mode 100644 index 0000000000..592960c869 --- /dev/null +++ b/src/components/NcDatetime/NcDatetime.vue @@ -0,0 +1,258 @@ + + + + +### General description + +This components purpose is to display a timestamp in the users local time format. +It also supports relative time, for examples *6 seconds ago*. + +#### Standard usage + +Without any optional parameters the timestamp is displayed as a relative datetime and a title with the full date is added. + +```vue + + +``` + +#### Ignore seconds + +If you do not want the seconds to be counted up until minutes are reached you can simply use the `ignore-seconds` property. + +```vue + + +``` + +#### Custom date or time format + +The component allows to format the full date for the title by settings the `format` property. +It is also possible to disable relative time by setting the `relativeTime` property to `false`. + +```vue + + + +``` + + + + + diff --git a/src/components/NcDatetime/index.js b/src/components/NcDatetime/index.js new file mode 100644 index 0000000000..7f0296c74c --- /dev/null +++ b/src/components/NcDatetime/index.js @@ -0,0 +1,23 @@ +/** + * @copyright 2023 Ferdinand Thiessen + * + * @author Ferdinand Thiessen + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +export { default } from './NcDatetime.vue' diff --git a/src/components/index.js b/src/components/index.js index 1b4179643b..82b08271b0 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -57,6 +57,7 @@ export { default as NcContent } from './NcContent/index.js' export { default as NcCounterBubble } from './NcCounterBubble/index.js' export { default as NcDashboardWidget } from './NcDashboardWidget/index.js' export { default as NcDashboardWidgetItem } from './NcDashboardWidgetItem/index.js' +export { default as NcDatetime } from './NcDatetime/index.js' export { default as NcDatetimePicker } from './NcDatetimePicker/index.js' export { default as NcDateTimePickerNative } from './NcDateTimePickerNative/index.js' // Not exported on purpose diff --git a/tests/unit/components/NcDatetime/NcDatetime.spec.js b/tests/unit/components/NcDatetime/NcDatetime.spec.js new file mode 100644 index 0000000000..ad1f8918f2 --- /dev/null +++ b/tests/unit/components/NcDatetime/NcDatetime.spec.js @@ -0,0 +1,209 @@ +/** + * @copyright 2023 Ferdinand Thiessen + * + * @author Ferdinand Thiessen + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import { mount } from '@vue/test-utils' +import NcDatetime from '../../../../src/components/NcDatetime/NcDatetime.vue' + +describe('NcDatetime.vue', () => { + 'use strict' + + it('Sets the title property correctly', () => { + const time = Date.UTC(2023, 5, 23, 14, 30) + Date.now = jest.fn(() => new Date(time).valueOf()) + const wrapper = mount(NcDatetime, { + propsData: { + timestamp: time, + }, + }) + + expect(wrapper.element.hasAttribute('title')).toBe(true) + expect(wrapper.element.getAttribute('title')).toMatch('6/23/23, 2:30:00 PM') + }) + + it('Can set format of the title property', () => { + const time = Date.UTC(2023, 5, 23, 14, 30) + Date.now = jest.fn(() => new Date(time).valueOf()) + const wrapper = mount(NcDatetime, { + propsData: { + timestamp: time, + format: { dateStyle: 'long' }, + }, + }) + + expect(wrapper.element.hasAttribute('title')).toBe(true) + expect(wrapper.element.getAttribute('title')).toMatch('June 23, 2023') + }) + + it('Can disable relative time', () => { + const time = Date.UTC(2023, 5, 23, 14, 30) + Date.now = jest.fn(() => new Date(time).valueOf()) + const wrapper = mount(NcDatetime, { + propsData: { + timestamp: time, + relativeTime: false, + }, + }) + + expect(wrapper.element.hasAttribute('title')).toBe(true) + expect(wrapper.element.textContent).toMatch(wrapper.element.getAttribute('title')) + }) + + describe('Work with different locales', () => { + beforeAll(() => { + // mock the locale + document.documentElement.dataset.locale = 'de_DE' + }) + afterAll(() => { + // revert mock + document.documentElement.dataset.locale = 'en' + }) + + /** + * Use German locale as it uses a different date format than English + */ + it('', () => { + const time = Date.UTC(2023, 5, 23, 14, 30) + Date.now = jest.fn(() => new Date(time).valueOf()) + const wrapper = mount(NcDatetime, { + propsData: { + timestamp: time, + }, + }) + + expect(wrapper.element.hasAttribute('title')).toBe(true) + expect(wrapper.element.getAttribute('title')).toMatch('23.06.23, 14:30:00') + }) + }) + + describe('Shows relative time', () => { + it('works with currentTime == timestamp', () => { + const time = Date.UTC(2023, 5, 23, 14, 30) + Date.now = jest.fn(() => new Date(time).valueOf()) + const wrapper = mount(NcDatetime, { + propsData: { + timestamp: time, + }, + }) + + expect(wrapper.vm.currentTime).toEqual(time) + expect(wrapper.element.textContent).toContain('now') + }) + + it('shows seconds from now (updating)', async () => { + const time = Date.UTC(2023, 5, 23, 14, 30, 30) + let currentTime = Date.UTC(2023, 5, 23, 14, 30, 33) + Date.now = jest.fn(() => new Date(currentTime).valueOf()) + const wrapper = mount(NcDatetime, { + propsData: { + timestamp: time, + }, + }) + + expect(wrapper.vm.currentTime).toEqual(currentTime) + expect(wrapper.element.textContent).toContain('3 seconds') + currentTime = Date.UTC(2023, 5, 23, 14, 30, 34) + // wait for timer + await new Promise((resolve) => setTimeout(resolve, 1100)) + expect(wrapper.element.textContent).toContain('4 seconds') + }) + + it('shows seconds from now - also as short variant', () => { + const time = Date.UTC(2023, 5, 23, 14, 30, 30) + const currentTime = Date.UTC(2023, 5, 23, 14, 30, 33) + Date.now = jest.fn(() => new Date(currentTime).valueOf()) + const wrapper = mount(NcDatetime, { + propsData: { + timestamp: time, + relativeTime: 'short', + }, + }) + + expect(wrapper.vm.currentTime).toEqual(currentTime) + expect(wrapper.element.textContent).toContain('3 sec.') + }) + + it('shows minutes from now', () => { + const time = Date.UTC(2023, 5, 23, 14, 30, 30) + const currentTime = Date.UTC(2023, 5, 23, 14, 33, 30) + Date.now = jest.fn(() => new Date(currentTime).valueOf()) + const wrapper = mount(NcDatetime, { + propsData: { + timestamp: time, + }, + }) + + expect(wrapper.vm.currentTime).toEqual(currentTime) + expect(wrapper.element.textContent).toContain('3 minutes') + }) + + it('shows hours from now', () => { + const time = Date.UTC(2023, 5, 23, 14, 30, 30) + const currentTime = Date.UTC(2023, 5, 23, 17, 30, 30) + Date.now = jest.fn(() => new Date(currentTime).valueOf()) + const wrapper = mount(NcDatetime, { + propsData: { + timestamp: time, + }, + }) + + expect(wrapper.vm.currentTime).toEqual(currentTime) + expect(wrapper.element.textContent).toContain('3 hours') + }) + + it('shows weeks from now', () => { + const time = Date.UTC(2023, 5, 23, 14, 30, 30) + const currentTime = Date.UTC(2023, 6, 13, 14, 30, 30) + Date.now = jest.fn(() => new Date(currentTime).valueOf()) + const wrapper = mount(NcDatetime, { + propsData: { + timestamp: time, + }, + }) + + expect(wrapper.vm.currentTime).toEqual(currentTime) + expect(wrapper.element.textContent).toContain('3 weeks') + }) + + it('shows years from now', () => { + const time = Date.UTC(2023, 5, 23, 14, 30, 30) + const time2 = Date.UTC(2022, 5, 23, 14, 30, 30) + const currentTime = Date.UTC(2024, 6, 13, 14, 30, 30) + Date.now = jest.fn(() => new Date(currentTime).valueOf()) + + const wrapper = mount(NcDatetime, { + propsData: { + timestamp: time, + }, + }) + const wrapper2 = mount(NcDatetime, { + propsData: { + timestamp: time2, + }, + }) + + expect(wrapper.vm.currentTime).toEqual(currentTime) + expect(wrapper2.vm.currentTime).toEqual(currentTime) + expect(wrapper.element.textContent).toContain('last year') + expect(wrapper2.element.textContent).toContain('2 years') + }) + }) +})