5 Commits

Author SHA1 Message Date
Linus Groh
8399476195 Release 2.0.0-beta.4 2019-12-14 09:43:07 -05:00
Linus Groh
7767a06875 Add support for time selection 2019-12-14 09:35:46 -05:00
Linus Groh
5bcb7a63bc Add missing translation of "to" 2019-12-14 13:37:56 +00:00
Linus Groh
f0b3ed2632 Add example commit for new locale 2019-12-13 20:33:54 +00:00
Linus Groh
185d6fd842 Clarify i18n development notes 2019-12-13 20:29:10 +00:00
25 changed files with 307 additions and 208 deletions

View File

@@ -1,3 +1,12 @@
# 2.0.0-beta.4 (2019-12-14)
- Add support for time selection
- New date/time picker component is properly translated/localised and keyboard accessible
- Config options are now `startDateTime`/`endDateTime` and format of URL parameters changed
- Changed default start/end date and time to use local timezone
- Fix missing translation of "[date] to [date]"
- Update i18n development notes in `README.md`
# 2.0.0-beta.3 (2019-12-13)
- Add i18 support (currently English and German, `locale` config option)
@@ -9,7 +18,6 @@
- `README.md` enhancements
- Upgrade dependencies
# 2.0.0-beta.2 (2019-11-02)
- Add `onLocationChange.reloadHistory` config option

View File

@@ -135,7 +135,10 @@ $ yarn i18n:report
```
To add a new locale, copy `en.json` to `<locale>.json` in [`src/locales`](src/locales)
and start translating the individual strings. Make sure to add the new locale to the docs!
and start translating the individual strings. Make sure to [mention the new locale to the docs](docs/config.md#locale)!
For a specific example see commit [`b2edda4`](https://github.com/owntracks/frontend/commit/b2edda410f16633aa6fd9cd4e5250f2031536c7d)
where German translations were added.
## Screenshots

View File

@@ -6,7 +6,7 @@ COPY . ./
RUN yarn build
FROM nginx:1.17-alpine
LABEL version="2.0.0-beta.3"
LABEL version="2.0.0-beta.4"
LABEL description="OwnTracks UI"
LABEL maintainer="Linus Groh <mail@linusgroh.de>"
ENV LISTEN_PORT=80 \

View File

@@ -24,7 +24,7 @@ window.owntracks.config = {};
- `api`
- [`baseUrl`](#apibaseurl)
- [`endDate`](#enddate)
- [`endDateTime`](#enddatetime)
- [`ignorePingLocation`](#ignorepinglocation)
- [`locale`](#locale)
- `map`
@@ -65,7 +65,7 @@ window.owntracks.config = {};
- [`primaryColor`](#primarycolor)
- [`selectedDevice`](#selecteddevice)
- [`selectedUser`](#selecteduser)
- [`startDate`](#startdate)
- [`startDateTime`](#startdatetime)
- [`verbose`](#verbose)
### `api.baseUrl`
@@ -92,17 +92,17 @@ Base URL for the recorder's HTTP and WebSocket API. Keep CORS in mind.
};
```
### `endDate`
### `endDateTime`
Initial end date for fetched data.
Initial end date and time (browser timezone) for fetched data.
- Type: [`Date`]
- Default: today
- Default: today, 23:59:59
- Example:
```js
// Data will be fetched up to 1970-01-01
window.owntracks.config = {
endDate: new Date(1970, 1, 1)
endDateTime: new Date(1970, 1, 1)
};
```
@@ -432,20 +432,20 @@ amount of data fetched after page load.
};
```
### `startDate`
### `startDateTime`
Initial start date for fetched data.
Initial start date and time (browser timezone) for fetched data.
- Type: [`Date`]
- Default: one month ago
- Default: one month ago, 00:00:00
- Example:
```js
// Data will be fetched from the first day of the current month
const startDate = new Date();
startDate.setUTCHours(0, 0, 0, 0);
startDate.setUTCDate(1);
const startDateTime = new Date();
startDateTime.setHours(0, 0, 0, 0);
startDateTime.setDate(1);
window.owntracks.config = {
startDate
startDateTime
};
```

View File

@@ -1,6 +1,6 @@
{
"name": "owntracks-ui",
"version": "2.0.0-beta.3",
"version": "2.0.0-beta.4",
"author": {
"name": "Linus Groh",
"email": "mail@linusgroh.de"
@@ -19,14 +19,15 @@
"deepmerge": "^4.2.2",
"leaflet": "^1.6.0",
"leaflet.heat": "^0.2.0",
"moment": "^2.24.0",
"vue": "^2.6.6",
"vue-ctk-date-time-picker": "^2.4.0",
"vue-feather-icons": "^5.0.0",
"vue-i18n": "^8.0.0",
"vue-js-modal": "^1.3.31",
"vue-outside-events": "^1.1.3",
"vue-router": "^3.1.3",
"vue2-leaflet": "^2.2.1",
"vuejs-datepicker": "^1.6.2",
"vuex": "^3.1.2"
},
"devDependencies": {
@@ -45,6 +46,7 @@
"eslint-plugin-vue": "^6.0.1",
"jest-fetch-mock": "^2.1.2",
"lint-staged": "^9.5.0",
"moment-locales-webpack-plugin": "^1.1.2",
"node-sass": "^4.13.0",
"sass-loader": "^8.0.0",
"vue-cli-plugin-i18n": "^0.6.0",

View File

@@ -40,8 +40,8 @@ export default {
[
types.SET_SELECTED_USER,
types.SET_SELECTED_DEVICE,
types.SET_START_DATE,
types.SET_END_DATE,
types.SET_START_DATE_TIME,
types.SET_END_DATE_TIME,
types.SET_MAP_CENTER,
types.SET_MAP_ZOOM,
types.SET_MAP_LAYER_VISIBILITY,
@@ -68,8 +68,8 @@ export default {
updateUrlQuery() {
const {
map,
startDate: start,
endDate: end,
startDateTime: start,
endDateTime: end,
selectedUser: user,
selectedDevice: device,
} = this.$store.state;
@@ -80,8 +80,8 @@ export default {
lat: map.center.lat,
lng: map.center.lng,
zoom: map.zoom,
start: start.toISOString().split("T")[0],
end: end.toISOString().split("T")[0],
start,
end,
...(user !== null && { user }),
...(user !== null && device !== null && { device }),
...(activeLayers.length > 0 && { layers: activeLayers.join(",") }),

View File

@@ -95,8 +95,8 @@ export const getLastLocations = async (user, device) => {
*
* @param {User} user Username
* @param {Device} device Device name
* @param {Date} start Start date
* @param {Date} end End date
* @param {String} start Start date and time in UTC
* @param {String} end End date and time in UTC
* @return {LocationHistory} Array of location history objects
*/
export const getUserDeviceLocationHistory = async (
@@ -105,11 +105,9 @@ export const getUserDeviceLocationHistory = async (
start,
end
) => {
const startDate = start.toISOString().split("T")[0];
const endDate = end.toISOString().split("T")[0];
const response = await fetchApi("/api/0/locations", {
from: `${startDate}T00:00:00`,
to: `${endDate}T23:59:59`,
from: start,
to: end,
user,
device,
format: "json",
@@ -122,8 +120,8 @@ export const getUserDeviceLocationHistory = async (
* Get the location history of multiple devices.
*
* @param {Object.<User, Array.<Device>>} devices Devices of which the history should be fetched
* @param {Date} start Start date
* @param {Date} end End date
* @param {String} start Start date and time in UTC
* @param {String} end End date and time in UTC
* @return {Object.<User, Object.<Device, LocationHistory>>} Array of location history objects
*/
export const getLocationHistory = async (devices, start, end) => {

View File

@@ -32,19 +32,35 @@
</div>
<div class="nav-item">
<CalendarIcon size="1x" />
<Datepicker
v-model="startDate"
:use-utc="true"
:disabled-dates="startDateDisabledDates"
:title="$t('Select start date')"
/>
to
<Datepicker
v-model="endDate"
:use-utc="true"
:disabled-dates="endDateDisabledDates"
:title="$t('Select end date')"
/>
<VueCtkDateTimePicker
v-model="startDateTime"
:format="DATE_TIME_FORMAT"
:color="config.primaryColor"
:locale="config.locale"
:max-date="endDateTime"
:button-now-translation="$t('Now')"
>
<button
type="button"
class="dropdown-button button"
:title="$t('Select start date')"
/>
</VueCtkDateTimePicker>
{{ $t("to") }}
<VueCtkDateTimePicker
v-model="endDateTime"
:format="DATE_TIME_FORMAT"
:color="config.primaryColor"
:locale="config.locale"
:min-date="startDateTime"
:button-now-translation="$t('Now')"
>
<button
type="button"
class="dropdown-button button"
:title="$t('Select end date')"
/>
</VueCtkDateTimePicker>
</div>
<div class="nav-item">
<UserIcon size="1x" />
@@ -105,6 +121,7 @@
</template>
<script>
import moment from "moment";
import { mapActions, mapGetters, mapMutations, mapState } from "vuex";
import {
CalendarIcon,
@@ -114,9 +131,12 @@ import {
SmartphoneIcon,
UserIcon,
} from "vue-feather-icons";
import Datepicker from "vuejs-datepicker";
import VueCtkDateTimePicker from "vue-ctk-date-time-picker";
import "vue-ctk-date-time-picker/dist/vue-ctk-date-time-picker.css";
import Dropdown from "@/components/Dropdown";
import config from "@/config";
import { DATE_TIME_FORMAT } from "@/constants";
import * as types from "@/store/mutation-types";
export default {
@@ -127,11 +147,13 @@ export default {
LayersIcon,
SmartphoneIcon,
UserIcon,
Datepicker,
VueCtkDateTimePicker,
Dropdown,
},
data() {
return {
DATE_TIME_FORMAT,
config,
layerSettingsOptions: [
{ layer: "last", label: this.$t("Show last known locations") },
{ layer: "line", label: this.$t("Show location history (line)") },
@@ -142,7 +164,6 @@ export default {
},
computed: {
...mapState(["users", "devices", "map"]),
...mapGetters(["startDateDisabledDates", "endDateDisabledDates"]),
selectedUser: {
get() {
return this.$store.state.selectedUser;
@@ -159,20 +180,35 @@ export default {
this.setSelectedDevice(value);
},
},
startDate: {
startDateTime: {
get() {
return this.$store.state.startDate;
return moment
.utc(this.$store.state.startDateTime, DATE_TIME_FORMAT)
.local()
.format(DATE_TIME_FORMAT);
},
set(value) {
this.setStartDate(value);
this.setStartDateTime(
moment(value, DATE_TIME_FORMAT)
.utc()
.format(DATE_TIME_FORMAT)
);
},
},
endDate: {
endDateTime: {
get() {
return this.$store.state.endDate;
return moment
.utc(this.$store.state.endDateTime, DATE_TIME_FORMAT)
.local()
.format(DATE_TIME_FORMAT);
},
set(value) {
this.setEndDate(value);
this.setEndDateTime(
moment(value, DATE_TIME_FORMAT)
.set("seconds", 59)
.utc()
.format(DATE_TIME_FORMAT)
);
},
},
},
@@ -183,8 +219,8 @@ export default {
...mapActions([
"setSelectedUser",
"setSelectedDevice",
"setStartDate",
"setEndDate",
"setStartDateTime",
"setEndDateTime",
]),
},
};

View File

@@ -65,8 +65,8 @@ export default {
},
computed: {
...mapState([
"startDate",
"endDate",
"startDateTime",
"endDateTime",
"selectedUser",
"selectedDevice",
"locationHistory",
@@ -90,8 +90,8 @@ export default {
null,
this.options.minifyJson ? 0 : 2
);
const start = this.startDate.toISOString().split("T")[0];
const end = this.endDate.toISOString().split("T")[0];
const start = this.startDateTime;
const end = this.endDateTime;
const user = this.selectedUser ? `_${this.selectedUser}` : "";
const device = this.selectedDevice ? `_${this.selectedDevice}` : "";
const filename = `data_${start}_${end}${user}${device}.json`;

View File

@@ -1,16 +1,17 @@
import deepmerge from "deepmerge";
const endDate = new Date();
endDate.setUTCHours(0, 0, 0, 0);
const endDateTime = new Date();
endDateTime.setHours(23, 59, 59, 0);
const startDate = new Date(endDate);
startDate.setUTCMonth(startDate.getMonth() - 1);
const startDateTime = new Date(endDateTime);
startDateTime.setMonth(startDateTime.getMonth() - 1);
startDateTime.setHours(0, 0, 0, 0);
const DEFAULT_CONFIG = {
api: {
baseUrl: `${window.location.protocol}//${window.location.host}`,
},
endDate,
endDateTime,
ignorePingLocation: true,
locale: "en",
map: {
@@ -72,7 +73,7 @@ const DEFAULT_CONFIG = {
primaryColor: "#3f51b5",
selectedDevice: null,
selectedUser: null,
startDate,
startDateTime,
verbose: false,
};

View File

@@ -1,8 +1,7 @@
// Regular expression for an ISO 8601 YYYY-MM-DD date.
// Used to validate dates from URL query parameters.
export const ISO_DATE_REGEXP = new RegExp(
/^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/
);
// date and time format as expected by the OwnTracks recorder,
// using moment.js formatting tokens.
// https://momentjs.com/docs/#/displaying/format/
export const DATE_TIME_FORMAT = "YYYY-MM-DDTHH:mm:ss";
// https://en.wikipedia.org/wiki/Earth_radius
// Used to calculate the distance between two coordinates.

View File

@@ -3,7 +3,9 @@
"Fit view": "Ansicht anpassen",
"Layer settings": "Ebeneneinstellungen",
"Show/hide layers": "Ebenen ein-/ausblenden",
"Now": "Jetzt",
"Select start date": "Startdatum auswählen",
"to": "bis",
"Select end date": "Enddatum auswählen",
"Select user": "Benutzer auswählen",
"Show all": "Alle anzeigen",

View File

@@ -3,7 +3,9 @@
"Fit view": "Fit view",
"Layer settings": "Layer settings",
"Show/hide layers": "Show/hide layers",
"Now": "Now",
"Select start date": "Select start date",
"to": "to",
"Select end date": "Select end date",
"Select user": "Select user",
"Show all": "Show all",

View File

@@ -1,7 +1,7 @@
import * as types from "@/store/mutation-types";
import * as api from "@/api";
import config from "@/config";
import { isIsoDate } from "@/util";
import { isIsoDateTime } from "@/util";
/** @typedef {import("./types").QueryParams} QueryParams */
/** @typedef {import("./types").User} User */
@@ -28,11 +28,11 @@ const populateStateFromQuery = ({ state, commit }, query) => {
if (query.zoom && !isNaN(parseInt(query.zoom))) {
commit(types.SET_MAP_ZOOM, parseInt(query.zoom));
}
if (query.start && isIsoDate(query.start)) {
commit(types.SET_START_DATE, new Date(query.start));
if (query.start && isIsoDateTime(query.start)) {
commit(types.SET_START_DATE_TIME, query.start);
}
if (query.end && isIsoDate(query.end)) {
commit(types.SET_END_DATE, new Date(query.end));
if (query.end && isIsoDateTime(query.end)) {
commit(types.SET_END_DATE_TIME, query.end);
}
if (query.user) {
commit(types.SET_SELECTED_USER, query.user);
@@ -138,7 +138,11 @@ const getLocationHistory = async ({ commit, state }) => {
}
commit(
types.SET_LOCATION_HISTORY,
await api.getLocationHistory(devices, state.startDate, state.endDate)
await api.getLocationHistory(
devices,
state.startDateTime,
state.endDateTime
)
);
commit(types.SET_IS_LOADING, false);
};
@@ -172,22 +176,22 @@ const setSelectedDevice = async ({ commit, dispatch }, device) => {
};
/**
* Set the start date for loading data and reload the location history.
* Set the start date and time for loading data and reload the location history.
*
* @param {Date} startDate Start date for loading data
* @param {String} startDateTime Start date and time in UTC for loading data
*/
const setStartDate = async ({ commit, dispatch }, startDate) => {
commit(types.SET_START_DATE, startDate);
const setStartDateTime = async ({ commit, dispatch }, startDateTime) => {
commit(types.SET_START_DATE_TIME, startDateTime);
await dispatch("reloadData");
};
/**
* Set the end date for loading data and reload the location history.
* Set the end date and time for loading data and reload the location history.
*
* @param {Date} endDate End date for loading data
* @param {String} endDateTime End date and time in UTC for loading data
*/
const setEndDate = async ({ commit, dispatch }, endDate) => {
commit(types.SET_END_DATE, endDate);
const setEndDateTime = async ({ commit, dispatch }, endDateTime) => {
commit(types.SET_END_DATE_TIME, endDateTime);
await dispatch("reloadData");
};
@@ -203,6 +207,6 @@ export default {
getRecorderVersion,
setSelectedUser,
setSelectedDevice,
setStartDate,
setEndDate,
setStartDateTime,
setEndDateTime,
};

View File

@@ -65,37 +65,7 @@ const locationHistoryLatLngGroups = state => {
return groups;
};
/**
* For the start date selector, disable all dates above the end date
* or current date.
*
* @param {State} state
* @param {Date} state.endDate End date
* @return {DatepickerConfig} Configuration for the `disabled-dates` prop of the `vuejs-datepicker` component
*/
const startDateDisabledDates = state => {
return {
customPredictor: date => date > state.endDate || date > new Date(),
};
};
/**
* For the end date selector, disable all dates below the start date
* or above the current date.
*
* @param {State} state
* @param {Date} state.startDate Start date
* @return {DatepickerConfig} Configuration for the `disabled-dates` prop of the `vuejs-datepicker` component
*/
const endDateDisabledDates = state => {
return {
customPredictor: date => date < state.startDate || date > new Date(),
};
};
export default {
locationHistoryLatLngs,
locationHistoryLatLngGroups,
startDateDisabledDates,
endDateDisabledDates,
};

View File

@@ -19,8 +19,9 @@ export default new Vuex.Store({
locationHistory: {},
selectedUser: config.selectedUser,
selectedDevice: config.selectedUser !== null ? config.selectedDevice : null,
startDate: config.startDate,
endDate: config.endDate,
// Convert to UTC and get rid of milliseconds
startDateTime: config.startDateTime.toISOString().slice(0, 19),
endDateTime: config.endDateTime.toISOString().slice(0, 19),
map: {
center: config.map.center,
zoom: config.map.zoom,

View File

@@ -6,8 +6,8 @@ export const SET_LAST_LOCATIONS = "SET_LAST_LOCATIONS";
export const SET_LOCATION_HISTORY = "SET_LOCATION_HISTORY";
export const SET_SELECTED_USER = "SET_SELECTED_USER";
export const SET_SELECTED_DEVICE = "SET_SELECTED_DEVICE";
export const SET_START_DATE = "SET_START_DATE";
export const SET_END_DATE = "SET_END_DATE";
export const SET_START_DATE_TIME = "SET_START_DATE_TIME";
export const SET_END_DATE_TIME = "SET_END_DATE_TIME";
export const SET_MAP_CENTER = "SET_MAP_CENTER";
export const SET_MAP_ZOOM = "SET_MAP_ZOOM";
export const SET_MAP_LAYER_VISIBILITY = "SET_MAP_LAYER_VISIBILITY";

View File

@@ -25,11 +25,11 @@ export default {
[types.SET_SELECTED_DEVICE](state, selectedDevice) {
state.selectedDevice = selectedDevice;
},
[types.SET_START_DATE](state, startDate) {
state.startDate = startDate;
[types.SET_START_DATE_TIME](state, startDateTime) {
state.startDateTime = startDateTime;
},
[types.SET_END_DATE](state, endDate) {
state.endDate = endDate;
[types.SET_END_DATE_TIME](state, endDateTime) {
state.endDateTime = endDateTime;
},
[types.SET_MAP_CENTER](state, center) {
state.map.center = center;

View File

@@ -102,7 +102,6 @@ pre {
display: flex;
padding: 20px;
white-space: nowrap;
overflow-x: auto;
color: var(--color-primary-text);
background: var(--color-primary);
@@ -163,16 +162,6 @@ pre {
}
}
.vdp-datepicker input {
cursor: pointer;
color: var(--color-text);
background: var(--color-background);
border: 0;
border-radius: 18px;
padding: 8px 16px;
min-width: 130px;
}
.button {
cursor: pointer;
color: var(--color-text);
@@ -229,12 +218,11 @@ pre {
.dropdown {
display: inline-block;
position: relative;
}
.dropdown-button,
.vdp-datepicker input {
-webkit-appearance: none;
-moz-appearance: none;
// Not nested so it works as the button alone
.dropdown-button {
appearance: none;
background-image: var(--dropdown-arrow);
background-repeat: no-repeat;
@@ -253,6 +241,17 @@ pre {
filter: var(--drop-shadow);
z-index: 1;
&::before {
content: "";
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 10px solid transparent;
border-bottom: 10px solid var(--color-background);
position: absolute;
top: -20px;
left: 20px;
}
label {
cursor: pointer;
display: block;
@@ -264,18 +263,6 @@ pre {
}
}
.dropdown-body::before,
.vdp-datepicker .vdp-datepicker__calendar::before {
content: "";
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 10px solid transparent;
border-bottom: 10px solid var(--color-background);
position: absolute;
top: -20px;
left: 20px;
}
.feather {
vertical-align: middle;
}

View File

@@ -1,32 +1,9 @@
.vdp-datepicker {
position: static !important;
.date-time-picker {
display: inline-block;
white-space: initial;
overflow: initial;
width: auto;
input {
width: 120px;
}
.vdp-datepicker__calendar {
color: var(--color-text);
border: 0;
border-radius: 3px;
margin-top: 12px;
.datepicker {
box-shadow: none !important;
filter: var(--drop-shadow);
.cell {
&:not(.blank):not(.disabled).day:hover,
&:not(.blank):not(.disabled).month:hover,
&:not(.blank):not(.disabled).year:hover {
border-color: var(--color-primary);
}
&.selected,
&.selected:hover {
background: var(--color-primary);
color: var(--color-primary-text);
}
}
}
}

View File

@@ -1,5 +1,7 @@
import moment from "moment";
import config from "@/config";
import { ISO_DATE_REGEXP, EARTH_RADIUS_IN_KM } from "@/constants";
import { DATE_TIME_FORMAT, EARTH_RADIUS_IN_KM } from "@/constants";
/** @typedef {import("./types").Coordinate} Coordinate */
@@ -19,12 +21,12 @@ export const getApiUrl = path => {
};
/**
* Check if the given string is an ISO 8601 YYYY-MM-DD date.
* Check if the given string is an ISO 8601 YYYY-MM-DDTHH:MM:SS datetime.
*
* @param {String} s Input value to be tested
* @return {Boolean} Whether the input is an ISO 8601 date
* @return {Boolean} Whether the input matches the expected format
*/
export const isIsoDate = s => ISO_DATE_REGEXP.test(s);
export const isIsoDateTime = s => moment(s, DATE_TIME_FORMAT, true).isValid();
/**
* Convert degrees to radians.

View File

@@ -144,8 +144,8 @@ describe("API", () => {
const locationHistory = await api.getUserDeviceLocationHistory(
"foo",
"phone",
new Date(Date.UTC(1970, 0, 1)),
new Date(Date.UTC(1970, 11, 31))
"1970-01-01T00:00:00",
"1970-12-31T23:59:59"
);
expect(locationHistory).toEqual(response.data);
@@ -194,8 +194,8 @@ describe("API", () => {
const locationHistory = await api.getLocationHistory(
{ foo: ["phone", "tablet"], bar: ["laptop"] },
new Date(Date.UTC(1970, 0, 1)),
new Date(Date.UTC(1970, 11, 31))
"1970-01-01T00:00:00",
"1970-12-31T23:59:59"
);
expect(locationHistory).toEqual({
foo: {

View File

@@ -1,7 +1,7 @@
import config from "@/config";
import {
getApiUrl,
isIsoDate,
isIsoDateTime,
degreesToRadians,
distanceBetweenCoordinates,
} from "@/util";
@@ -24,26 +24,36 @@ describe("getApiUrl", () => {
});
});
describe("isIsoDate", () => {
describe("isIsoDateTime", () => {
test("no match", () => {
expect(isIsoDate("foo")).toBe(false);
expect(isIsoDate("2019")).toBe(false);
expect(isIsoDate("2019-09")).toBe(false);
expect(isIsoDate("2019.09.27")).toBe(false);
expect(isIsoDate("2019_09_27")).toBe(false);
expect(isIsoDate("2019/09/27")).toBe(false);
expect(isIsoDate("27-09-2019")).toBe(false);
expect(isIsoDate("27.09.2019")).toBe(false);
expect(isIsoDate("27_09_2019")).toBe(false);
expect(isIsoDate("27/09/2019")).toBe(false);
expect(isIsoDate("0000-00-00")).toBe(false);
expect(isIsoDate("1234-56-78")).toBe(false);
expect(isIsoDateTime("foo")).toBe(false);
expect(isIsoDateTime("2019")).toBe(false);
expect(isIsoDateTime("2019-09")).toBe(false);
expect(isIsoDateTime("2019.09.27")).toBe(false);
expect(isIsoDateTime("2019_09_27")).toBe(false);
expect(isIsoDateTime("2019/09/27")).toBe(false);
expect(isIsoDateTime("27-09-2019")).toBe(false);
expect(isIsoDateTime("27.09.2019")).toBe(false);
expect(isIsoDateTime("27_09_2019")).toBe(false);
expect(isIsoDateTime("27/09/2019")).toBe(false);
expect(isIsoDateTime("0000-00-00")).toBe(false);
expect(isIsoDateTime("1234-56-78")).toBe(false);
expect(isIsoDateTime("0000-00-00T00:00:00")).toBe(false);
expect(isIsoDateTime("0000-01-01T25:60:60")).toBe(false);
expect(isIsoDateTime("2019-12-14T99:00:00")).toBe(false);
expect(isIsoDateTime("2019-12-14 25:60:60")).toBe(false);
});
test("match", () => {
expect(isIsoDate("0000-01-01")).toBe(true);
expect(isIsoDate("2019-09-27")).toBe(true);
expect(isIsoDate("9999-12-31")).toBe(true);
expect(isIsoDateTime("0000-01-01T00:00:00")).toBe(true);
expect(isIsoDateTime("0000-01-01T12:34:56")).toBe(true);
expect(isIsoDateTime("0000-01-01T23:59:59")).toBe(true);
expect(isIsoDateTime("2019-09-27T00:00:00")).toBe(true);
expect(isIsoDateTime("2019-09-27T12:34:56")).toBe(true);
expect(isIsoDateTime("2019-09-27T23:59:59")).toBe(true);
expect(isIsoDateTime("9999-12-31T00:00:00")).toBe(true);
expect(isIsoDateTime("9999-12-31T12:34:56")).toBe(true);
expect(isIsoDateTime("9999-12-31T23:59:59")).toBe(true);
});
});

View File

@@ -1,5 +1,6 @@
const fs = require("fs");
const webpack = require("webpack");
const MomentLocalesPlugin = require("moment-locales-webpack-plugin");
const packageJson = fs.readFileSync("./package.json");
const version = JSON.parse(packageJson).version;
@@ -12,6 +13,7 @@ module.exports = {
PACKAGE_VERSION: `"${version}"`,
},
}),
new MomentLocalesPlugin(),
],
},

103
yarn.lock
View File

@@ -3480,6 +3480,14 @@ cyclist@^1.0.1:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
d@1, d@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"
integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==
dependencies:
es5-ext "^0.10.50"
type "^1.0.1"
dashdash@^1.12.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
@@ -4018,6 +4026,32 @@ es-to-primitive@^1.2.0:
is-date-object "^1.0.1"
is-symbol "^1.0.2"
es5-ext@^0.10.35, es5-ext@^0.10.50:
version "0.10.53"
resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1"
integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==
dependencies:
es6-iterator "~2.0.3"
es6-symbol "~3.1.3"
next-tick "~1.0.0"
es6-iterator@~2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c=
dependencies:
d "1"
es5-ext "^0.10.35"
es6-symbol "^3.1.1"
es6-symbol@^3.1.0, es6-symbol@^3.1.1, es6-symbol@~3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18"
integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==
dependencies:
d "^1.0.1"
ext "^1.1.2"
escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
@@ -4368,6 +4402,13 @@ express@^4.16.3, express@^4.17.1:
utils-merge "1.0.1"
vary "~1.1.2"
ext@^1.1.2:
version "1.4.0"
resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244"
integrity sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==
dependencies:
type "^2.0.0"
extend-shallow@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
@@ -6753,6 +6794,11 @@ lodash.defaultsdeep@^4.6.1:
resolved "https://registry.yarnpkg.com/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz#512e9bd721d272d94e3d3a63653fa17516741ca6"
integrity sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==
lodash.difference@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c"
integrity sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=
lodash.kebabcase@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36"
@@ -7189,6 +7235,25 @@ mkdirp@0.5.1, mkdirp@0.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp
dependencies:
minimist "0.0.8"
moment-locales-webpack-plugin@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/moment-locales-webpack-plugin/-/moment-locales-webpack-plugin-1.1.2.tgz#9ae5263ac38d5cba227fc9d76efad025bb685a0c"
integrity sha512-s+JE7lADQjUyeQvqB3sVcfxXncg1o+t5hrRl2GBY66vXuLO2tXIjD+4mNUXQMS10qCGoeK3R3skBrW34gHobBQ==
dependencies:
lodash.difference "^4.5.0"
moment-range@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/moment-range/-/moment-range-4.0.2.tgz#f7c3863df2a1ed7fd1822ba5a7bcf53a78701be9"
integrity sha512-n8sceWwSTjmz++nFHzeNEUsYtDqjgXgcOBzsHi+BoXQU2FW+eU92LUaK8gqOiSu5PG57Q9sYj1Fz4LRDj4FtKA==
dependencies:
es6-symbol "^3.1.0"
moment@^2.24.0:
version "2.24.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
move-concurrently@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
@@ -7289,6 +7354,11 @@ neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==
next-tick@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
nice-try@^1.0.4:
version "1.0.5"
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
@@ -10396,6 +10466,16 @@ type-is@~1.6.17, type-is@~1.6.18:
media-typer "0.3.0"
mime-types "~2.1.24"
type@^1.0.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0"
integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==
type@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/type/-/type-2.0.0.tgz#5f16ff6ef2eb44f260494dae271033b29c09a9c3"
integrity sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==
typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
@@ -10591,6 +10671,11 @@ uuid@^3.0.1, uuid@^3.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866"
integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==
v-click-outside@^2.0.2:
version "2.1.5"
resolved "https://registry.yarnpkg.com/v-click-outside/-/v-click-outside-2.1.5.tgz#aa69172fb41fcc79b26b9a4bc72a30ccf03f7a3c"
integrity sha512-VPNCOTZK6WZy73lcWc+R7IW1uaBFEO3/Csrs5CzWVOdvE30V8Y1+BE/BtTlcEmeDGx0eqdE7bSCg55Jj37PMJg==
v8-compile-cache@^2.0.3:
version "2.1.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e"
@@ -10642,6 +10727,16 @@ vue-cli-plugin-i18n@^0.6.0:
vue-i18n "^8.0.0"
vue-i18n-extract "^0.4.13"
vue-ctk-date-time-picker@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/vue-ctk-date-time-picker/-/vue-ctk-date-time-picker-2.4.0.tgz#dde39958ba964027db1e3a78667061e7580ea5a4"
integrity sha512-E4s0kacbL+oqURB+b6o1RNUEdsX8B5WSt6YoQKVZ019ytCv351Bs4FfZajEzo8oc9TVoI3BfcR0hLOYtaGyhEw==
dependencies:
moment "^2.24.0"
moment-range "^4.0.1"
v-click-outside "^2.0.2"
vue "^2.6.9"
vue-eslint-parser@^6.0.5:
version "6.0.5"
resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-6.0.5.tgz#c1c067c2755748e28f3872cd42e8c1c4c1a8059f"
@@ -10756,10 +10851,10 @@ vue@^2.5.16, vue@^2.6.6:
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.10.tgz#a72b1a42a4d82a721ea438d1b6bf55e66195c637"
integrity sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ==
vuejs-datepicker@^1.6.2:
version "1.6.2"
resolved "https://registry.yarnpkg.com/vuejs-datepicker/-/vuejs-datepicker-1.6.2.tgz#83c1e8fd4108e7f1d01c061a7e344918f25e47ae"
integrity sha512-PkC4vxzFBo7i6FSCUAJfnaWOx6VkKbOqxijSGHHlWxh8FIUKEZVtFychkonVWtK3iwWfhmYtqHcwsmgxefLpLQ==
vue@^2.6.9:
version "2.6.11"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35275c6d514125c5"
integrity sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ==
vuex@^3.1.2:
version "3.1.2"