Browse Source

feature/utc (#561)

* Code for UTC implementation

* Modified text on 'Today' button as it 'Date-Only' fields have this button and Date-Only buttons only work with UTC

Ensure that Date Only field only works in UTC and no conversion is made

* fixed code error for UTC conversion of just date-only fields

* Displaying date-times on frontend as Local

* Removed UTC text from translation JSON files

* Code review comment changes

* Simplified logic for 'Now' button when using the two toggles for time mode

* code review changes (pairing with Sebastian)

Co-authored-by: segalj <jason.segal@reedbusiness.com>
pull/564/head
jasonsegal23 5 years ago
committed by GitHub
parent
commit
749854110c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      backend/i18n/frontend_en.json
  2. 2
      backend/i18n/frontend_nl.json
  3. 2
      backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs
  4. 4
      backend/src/Squidex/appsettings.json
  5. 2
      frontend/app/features/content/shared/content-status.component.ts
  6. 26
      frontend/app/framework/angular/forms/editors/date-time-editor.component.html
  7. 15
      frontend/app/framework/angular/forms/editors/date-time-editor.component.scss
  8. 83
      frontend/app/framework/angular/forms/editors/date-time-editor.component.ts
  9. 6
      frontend/app/framework/utils/date-time.ts
  10. 6
      frontend/app/shared/state/contents.forms.spec.ts
  11. 4
      frontend/app/shared/state/contents.forms.visitors.ts

2
backend/i18n/frontend_en.json

@ -210,7 +210,7 @@
"common.created": "Created", "common.created": "Created",
"common.date": "Date", "common.date": "Date",
"common.dateTimeEditor.now": "Now", "common.dateTimeEditor.now": "Now",
"common.dateTimeEditor.nowTooltip": "Use Now (UTC)", "common.dateTimeEditor.nowTooltip": "Use Now",
"common.dateTimeEditor.today": "Today", "common.dateTimeEditor.today": "Today",
"common.dateTimeEditor.todayTooltip": "Use Today (UTC)", "common.dateTimeEditor.todayTooltip": "Use Today (UTC)",
"common.delete": "Delete", "common.delete": "Delete",

2
backend/i18n/frontend_nl.json

@ -210,7 +210,7 @@
"common.created": "Gemaakt", "common.created": "Gemaakt",
"common.date": "Datum", "common.date": "Datum",
"common.dateTimeEditor.now": "Nu", "common.dateTimeEditor.now": "Nu",
"common.dateTimeEditor.nowTooltip": "Nu gebruiken (UTC)", "common.dateTimeEditor.nowTooltip": "Nu gebruiken",
"common.dateTimeEditor.today": "Vandaag", "common.dateTimeEditor.today": "Vandaag",
"common.dateTimeEditor.todayTooltip": "Gebruik vandaag (UTC)", "common.dateTimeEditor.todayTooltip": "Gebruik vandaag (UTC)",
"common.delete": "Verwijderen", "common.delete": "Verwijderen",

2
backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs

@ -29,6 +29,8 @@ namespace Squidex.Areas.Api.Controllers.UI
public bool HideDateButtons { get; set; } public bool HideDateButtons { get; set; }
public bool HideDateTimeModeButton { get; set; }
public bool DisableScheduledChanges { get; set; } public bool DisableScheduledChanges { get; set; }
public bool RedirectToLogin { get; set; } public bool RedirectToLogin { get; set; }

4
backend/src/Squidex/appsettings.json

@ -118,6 +118,10 @@
*/ */
"hideDateButtons": false, "hideDateButtons": false,
/*
* Hide the Local/UTC button
*/
"hideDateTimeModeButton": false,
/* /*
* True to disable scheduled content status changed, for example when you have your own scheduling system. * True to disable scheduled content status changed, for example when you have your own scheduling system.
*/ */

2
frontend/app/features/content/shared/content-status.component.ts

@ -43,7 +43,7 @@ export class ContentStatusComponent {
public get tooltipText() { public get tooltipText() {
if (this.scheduled) { if (this.scheduled) {
return `Will be set to '${this.scheduled.status}' at ${this.scheduled.dueTime.toStringFormatUTC('PPpp')}`; return `Will be set to '${this.scheduled.status}' at ${this.scheduled.dueTime.toStringFormat('PPpp')}`;
} else { } else {
return this.status; return this.status;
} }

26
frontend/app/framework/angular/forms/editors/date-time-editor.component.html

@ -1,25 +1,37 @@
<div> <div (sqxResizeCondition)="setCompact($event)" [sqxResizeMinWidth]="500" [sqxResizeMaxWidth]="0">
<div class="form-inline"> <div class="form-inline">
<div class="form-group mr-1"> <div class="form-group mr-1">
<div *ngIf="!isCompact && isDateTimeMode && shouldShowDateTimeModeButton">
<button type="button" class="btn btn-text-secondary btn-sm btn-time-mode" (click)="setLocalMode(false)"
*ngIf="isLocalMode">
Local
</button>
<button type="button" class="btn btn-text-secondary btn-sm btn-time-mode" (click)="setLocalMode(true)"
*ngIf="!isLocalMode">
UTC
</button>
</div>
<div class="input-group"> <div class="input-group">
<input type="text" class="form-control form-date" [formControl]="dateControl" placeholder="{{ 'common.date' | sqxTranslate }}" [class.form-date-only]="!showTime" (blur)="callTouched()" maxlength="10" #dateInput> <input type="text" class="form-control form-date" [formControl]="dateControl" placeholder="{{ 'common.date' | sqxTranslate }}" [class.form-date-only]="!isDateTimeMode" [class.form-date-time-only]="isDateTimeMode && shouldShowDateTimeModeButton"
(blur)="callTouched()" maxlength="10" #dateInput>
<input type="text" class="form-control form-time" [formControl]="timeControl" placeholder="{{ 'common.time' | sqxTranslate }}" (blur)="callTouched()" *ngIf="showTime"> <input type="text" class="form-control form-time" [formControl]="timeControl" placeholder="{{ 'common.time' | sqxTranslate }}" (blur)="callTouched()" *ngIf="isDateTimeMode">
</div> </div>
<button type="button" class="btn btn-text-secondary btn-sm btn-clear" [class.hidden]="!hasValue" [disabled]="snapshot.isDisabled" (click)="reset()" *ngIf="!hideClear"> <button type="button" class="btn btn-text-secondary btn-sm btn-clear" [class.hidden]="!hasValue"
[disabled]="snapshot.isDisabled" (click)="reset()" *ngIf="!hideClear">
<i class="icon-close"></i> <i class="icon-close"></i>
</button> </button>
</div> </div>
<div class="form-group" *ngIf="showTime && shouldShowDateButtons"> <div class="form-group" *ngIf="isDateTimeMode && shouldShowDateButtons">
<button type="button" class="btn btn-text-secondary" [disabled]="snapshot.isDisabled" (click)="writeNow()" title="i18n:common.dateTimeEditor.nowTooltip"> <button type="button" class="btn btn-text-secondary" [disabled]="snapshot.isDisabled" (click)="writeNow()" title="i18n:common.dateTimeEditor.nowTooltip">
{{ 'common.dateTimeEditor.now' | sqxTranslate }} {{ 'common.dateTimeEditor.now' | sqxTranslate }}
</button> </button>
</div> </div>
<div class="form-group" *ngIf="!showTime && shouldShowDateButtons"> <div class="form-group" *ngIf="!isDateTimeMode && shouldShowDateButtons">
<button type="button" class="btn btn-text-secondary" [disabled]="snapshot.isDisabled" (click)="writeNow()" title="i18n:common.dateTimeEditor.todayTooltip"> <button type="button" class="btn btn-text-secondary" [disabled]="snapshot.isDisabled" (click)="writeNow()" title="i18n:common.dateTimeEditor.todayTooltip">
{{ 'common.dateTimeEditor.today' | sqxTranslate }} {{ 'common.dateTimeEditor.today' | sqxTranslate }}
</button> </button>
</div> </div>
</div> </div>
</div> </div>

15
frontend/app/framework/angular/forms/editors/date-time-editor.component.scss

@ -33,16 +33,27 @@
width: 8.5rem; width: 8.5rem;
} }
.form-date-time-only {
padding-left: 3.3rem;
width: 11rem;
}
.form-time { .form-time {
width: 7rem; width: 7rem;
} }
} }
.btn-clear { .btn {
& { &-clear {
@include absolute(auto, 4px, 3px, auto); @include absolute(auto, 4px, 3px, auto);
} }
&-time-mode {
@include absolute(1px, auto, -1px, auto);
z-index: 1000;
}
&:focus { &:focus {
box-shadow: none; box-shadow: none;
} }

83
frontend/app/framework/angular/forms/editors/date-time-editor.component.ts

@ -32,6 +32,7 @@ export class DateTimeEditorComponent extends StatefulControlComponent<{}, string
private picker: any; private picker: any;
private dateTime: DateTime | null; private dateTime: DateTime | null;
private hideDateButtonsSettings: boolean; private hideDateButtonsSettings: boolean;
private hideDateTimeModeButtonSetting: boolean;
private suppressEvents = false; private suppressEvents = false;
@Input() @Input()
@ -46,17 +47,29 @@ export class DateTimeEditorComponent extends StatefulControlComponent<{}, string
@Input() @Input()
public hideDateButtons: boolean; public hideDateButtons: boolean;
@Input()
public hideDateTimeModeButton: boolean;
@Input()
public isCompact: boolean;
@ViewChild('dateInput', { static: false }) @ViewChild('dateInput', { static: false })
public dateInput: ElementRef<HTMLInputElement>; public dateInput: ElementRef<HTMLInputElement>;
public timeControl = new FormControl(); public timeControl = new FormControl();
public dateControl = new FormControl(); public dateControl = new FormControl();
public isLocalMode = true;
public get shouldShowDateButtons() { public get shouldShowDateButtons() {
return !this.hideDateButtonsSettings && !this.hideDateButtons; return !this.hideDateButtonsSettings && !this.hideDateButtons;
} }
public get showTime() { public get shouldShowDateTimeModeButton() {
return !this.hideDateTimeModeButtonSetting && !this.hideDateTimeModeButton;
}
public get isDateTimeMode() {
return this.mode === 'DateTime'; return this.mode === 'DateTime';
} }
@ -68,23 +81,28 @@ export class DateTimeEditorComponent extends StatefulControlComponent<{}, string
super(changeDetector, {}); super(changeDetector, {});
this.hideDateButtonsSettings = !!uiOptions.get('hideDateButtons'); this.hideDateButtonsSettings = !!uiOptions.get('hideDateButtons');
this.hideDateTimeModeButtonSetting = !!uiOptions.get('hideDateTimeModeButton');
} }
public ngOnInit() { public ngOnInit() {
this.own( this.own(
this.timeControl.valueChanges.subscribe(() => { this.timeControl.valueChanges.subscribe(() => {
this.dateTime = this.getValue();
this.callChangeFormatted(); this.callChangeFormatted();
})); }));
this.own( this.own(
this.dateControl.valueChanges.subscribe(() => { this.dateControl.valueChanges.subscribe(() => {
this.dateTime = this.getValue();
this.callChangeFormatted(); this.callChangeFormatted();
})); }));
} }
public writeValue(obj: any) { public writeValue(obj: any) {
try { try {
this.dateTime = DateTime.parseISO(obj); this.dateTime = DateTime.parseISO(obj, false);
} catch (ex) { } catch (ex) {
this.dateTime = null; this.dateTime = null;
} }
@ -109,7 +127,7 @@ export class DateTimeEditorComponent extends StatefulControlComponent<{}, string
} }
public ngAfterViewInit() { public ngAfterViewInit() {
this.picker = new Pikaday({ field: this.dateInput.nativeElement, format: 'YYYY-MM-DD', this.picker = new Pikaday({field: this.dateInput.nativeElement, format: 'YYYY-MM-DD',
onSelect: () => { onSelect: () => {
if (this.suppressEvents) { if (this.suppressEvents) {
return; return;
@ -125,7 +143,7 @@ export class DateTimeEditorComponent extends StatefulControlComponent<{}, string
} }
public writeNow() { public writeNow() {
this.writeValue(DateTime.now().toISOString()); this.dateTime = DateTime.now();
this.updateControls(); this.updateControls();
this.callChangeFormatted(); this.callChangeFormatted();
@ -138,58 +156,55 @@ export class DateTimeEditorComponent extends StatefulControlComponent<{}, string
this.dateTime = null; this.dateTime = null;
this.updateControls(); this.updateControls();
this.callChangeFormatted();
this.callChange(null);
this.callTouched(); this.callTouched();
return false; return false;
} }
private callChangeFormatted() { private callChangeFormatted() {
this.callChange(this.getValue()); this.callChange(this.dateTime?.toISOString());
} }
private getValue(): string | null { private getValue(): DateTime | null {
if (!this.dateControl.value) { if (!this.dateControl.value) {
return null; return null;
} }
let result: string | null = null; if (this.isDateTimeMode && this.timeControl.value) {
if (this.showTime && this.timeControl.value) {
const combined = `${this.dateControl.value}T${this.timeControl.value}`; const combined = `${this.dateControl.value}T${this.timeControl.value}`;
const parsed = DateTime.tryParseISO(combined, true); return DateTime.tryParseISO(combined, !this.isLocalMode);
if (parsed) {
result = parsed.toISOString();
}
}
if (!result) {
const parsed = DateTime.tryParseISO(this.dateControl.value, true);
if (parsed) {
result = parsed.toISOString();
}
} }
return result; return DateTime.tryParseISO(this.dateControl.value);
} }
private updateControls() { private updateControls() {
this.suppressEvents = true; this.suppressEvents = true;
if (this.dateTime && this.mode === 'DateTime') { if (this.dateTime && this.isDateTimeMode) {
this.timeControl.setValue(this.dateTime.toStringFormatUTC('HH:mm:ss'), NO_EMIT); if (this.isLocalMode) {
this.timeControl.setValue(this.dateTime.toStringFormat('HH:mm:ss'), NO_EMIT);
} else {
this.timeControl.setValue(this.dateTime.toStringFormatUTC('HH:mm:ss'), NO_EMIT);
}
} else { } else {
this.timeControl.setValue(null, NO_EMIT); this.timeControl.setValue(null, NO_EMIT);
} }
if (this.dateTime && this.picker) { if (this.dateTime && this.picker) {
const dateString = this.dateTime.toStringFormatUTC('yyyy-MM-dd'); let dateString: string;
if (this.isDateTimeMode && this.isLocalMode) {
dateString = this.dateTime.toStringFormat('yyyy-MM-dd');
this.picker.setDate(DateHelper.getLocalDate(this.dateTime.raw), true);
} else {
dateString = this.dateTime.toStringFormatUTC('yyyy-MM-dd');
this.picker.setDate(DateHelper.getUTCDate(this.dateTime.raw), true); this.picker.setDate(DateHelper.getUTCDate(this.dateTime.raw), true);
}
this.dateControl.setValue(dateString, NO_EMIT); this.dateControl.setValue(dateString, NO_EMIT);
} else { } else {
@ -198,4 +213,14 @@ export class DateTimeEditorComponent extends StatefulControlComponent<{}, string
this.suppressEvents = false; this.suppressEvents = false;
} }
public setLocalMode(isLocalMode: boolean) {
this.isLocalMode = isLocalMode;
this.updateControls();
}
public setCompact(isCompact: boolean) {
this.next(s => ({ ...s, isCompact }));
}
} }

6
frontend/app/framework/utils/date-time.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { addDays, addHours, addMilliseconds, addMinutes, addMonths, addSeconds, addYears, format, formatDistanceToNow, parse, parseISO, startOfDay, startOfMonth, startOfTomorrow, startOfWeek, startOfYesterday } from 'date-fns'; import { addDays, addHours, addMilliseconds, addMinutes, addMonths, addSeconds, addYears, format, formatDistanceToNow, formatISO, parse, parseISO, startOfDay, startOfMonth, startOfTomorrow, startOfWeek, startOfYesterday } from 'date-fns';
import { DateHelper } from './date-helper'; import { DateHelper } from './date-helper';
const DATE_FORMAT = 'yyyy-MM-dd'; const DATE_FORMAT = 'yyyy-MM-dd';
@ -177,6 +177,10 @@ export class DateTime {
return format(this.value, DATE_FORMAT); return format(this.value, DATE_FORMAT);
} }
public toISODateTime(): string {
return formatISO(this.value);
}
public toStringFormat(pattern: string): string { public toStringFormat(pattern: string): string {
return format(this.value, pattern); return format(this.value, pattern);
} }

6
frontend/app/shared/state/contents.forms.spec.ts

@ -250,19 +250,19 @@ describe('DateTimeField', () => {
it('should format old format to date', () => { it('should format old format to date', () => {
const dateField = createField({ properties: createProperties('DateTime', { editor: 'Date' }) }); const dateField = createField({ properties: createProperties('DateTime', { editor: 'Date' }) });
expect(FieldFormatter.format(dateField, '2017-12-12')).toBe('2017-12-12'); expect(FieldFormatter.format(dateField, '2017-12-12')).toBe('12/12/2017');
}); });
it('should format to date', () => { it('should format to date', () => {
const dateField = createField({ properties: createProperties('DateTime', { editor: 'Date' }) }); const dateField = createField({ properties: createProperties('DateTime', { editor: 'Date' }) });
expect(FieldFormatter.format(dateField, '2017-12-12T16:00:00Z')).toBe('2017-12-12'); expect(FieldFormatter.format(dateField, '2017-12-12T16:00:00Z')).toBe('12/12/2017');
}); });
it('should format to date time', () => { it('should format to date time', () => {
const field2 = createField({ properties: createProperties('DateTime', { editor: 'DateTime' }) }); const field2 = createField({ properties: createProperties('DateTime', { editor: 'DateTime' }) });
expect(FieldFormatter.format(field2, '2017-12-12T16:00:00Z')).toBe('2017-12-12 16:00:00'); expect(FieldFormatter.format(field2, '2017-12-12T16:00:00Z')).toBe('12/12/2017, 4:00:00 PM');
}); });
it('should return default for DateFieldProperties', () => { it('should return default for DateFieldProperties', () => {

4
frontend/app/shared/state/contents.forms.visitors.ts

@ -130,9 +130,9 @@ export class FieldFormatter implements FieldPropertiesVisitor<FieldValue> {
const parsed = DateTime.parseISO(this.value); const parsed = DateTime.parseISO(this.value);
if (properties.editor === 'Date') { if (properties.editor === 'Date') {
return parsed.toStringFormatUTC('yyyy-MM-dd'); return parsed.toStringFormat('P');
} else { } else {
return parsed.toStringFormatUTC('yyyy-MM-dd HH:mm:ss'); return parsed.toStringFormat('Ppp');
} }
} catch (ex) { } catch (ex) {
return this.value; return this.value;

Loading…
Cancel
Save