diff --git a/client/package.json b/client/package.json index dc12e2742..a24a94ddd 100644 --- a/client/package.json +++ b/client/package.json @@ -37,6 +37,7 @@ "@angular/core": "^8.0.3", "@angular/forms": "^8.0.3", "@angular/material": "^8.0.1", + "@angular/material-moment-adapter": "^8.1.2", "@angular/platform-browser": "^8.0.3", "@angular/platform-browser-dynamic": "^8.0.3", "@angular/pwa": "^0.800.6", @@ -57,9 +58,11 @@ "hammerjs": "^2.0.8", "lz4js": "^0.2.0", "material-icon-font": "git+https://github.com/petergng/materialIconFont.git", + "moment": "^2.24.0", "ng2-pdf-viewer": "^5.2.3", "ngx-file-drop": "^8.0.3", "ngx-mat-select-search": "^1.7.2", + "ngx-material-timepicker": "^4.0.2", "ngx-papaparse": "^3.0.2", "pdfmake": "^0.1.53", "po2json": "^1.0.0-alpha", diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index 79b198010..ad2671448 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -100,6 +100,7 @@ export class AppComponent { const browserLang = translate.getBrowserLang(); // try to use the browser language if it is available. If not, uses english. translate.use(translate.getLangs().includes(browserLang) ? browserLang : 'en'); + // change default JS functions this.overloadArrayToString(); this.overloadFlatMap(); diff --git a/client/src/app/shared/date-adapter.ts b/client/src/app/shared/date-adapter.ts index dd9f5a65b..4404d48f4 100644 --- a/client/src/app/shared/date-adapter.ts +++ b/client/src/app/shared/date-adapter.ts @@ -1,29 +1,30 @@ -import { Injectable } from '@angular/core'; -import { NativeDateAdapter } from '@angular/material/core'; +import { Inject, Injectable, Optional } from '@angular/core'; +import { MAT_DATE_LOCALE } from '@angular/material'; +import { + MAT_MOMENT_DATE_ADAPTER_OPTIONS, + MatMomentDateAdapterOptions, + MomentDateAdapter +} from '@angular/material-moment-adapter'; + +import { LangChangeEvent, TranslateService } from '@ngx-translate/core'; /** - * A custom DateAdapter for the datetimepicker in the config. This is still not fully working and needs to be done later. - * See comments in PR #3895. + * A custom DateAdapter for the datetimepicker in the config. Uses MomentDateAdapter for localisation. + * Is needed to subscribe to language changes */ @Injectable() -export class OpenSlidesDateAdapter extends NativeDateAdapter { - public format(date: Date, displayFormat: Object): string { - if (displayFormat === 'input') { - return this.toFullIso8601(date); - } else { - return date.toDateString(); - } - } - - private to2digit(n: number): string { - return ('00' + n).slice(-2); - } - - public toFullIso8601(date: Date): string { - return ( - [date.getUTCFullYear(), this.to2digit(date.getUTCMonth() + 1), this.to2digit(date.getUTCDate())].join('-') + - 'T' + - [this.to2digit(date.getUTCHours()), this.to2digit(date.getUTCMinutes())].join(':') - ); +export class OpenSlidesDateAdapter extends MomentDateAdapter { + public constructor( + translate: TranslateService, + @Optional() @Inject(MAT_DATE_LOCALE) dateLocale: string, + @Optional() @Inject(MAT_MOMENT_DATE_ADAPTER_OPTIONS) _options?: MatMomentDateAdapterOptions + ) { + super(dateLocale, _options); + // subscribe to language changes to change localisation of dates accordingly + // DateAdapter seems not to be a singleton so we do that in this subclass instead of app.component + this.setLocale(translate.currentLang); + translate.onLangChange.subscribe((e: LangChangeEvent) => { + this.setLocale(e.lang); + }); } } diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index dedfd91fb..767b59cb7 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts @@ -10,7 +10,8 @@ import { MatButtonModule } from '@angular/material/button'; import { MatButtonToggleModule } from '@angular/material/button-toggle'; import { MatCardModule } from '@angular/material/card'; import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatNativeDateModule, DateAdapter } from '@angular/material/core'; +import { DateAdapter, MAT_DATE_LOCALE } from '@angular/material/core'; +import { MatMomentDateModule, MAT_MOMENT_DATE_ADAPTER_OPTIONS } from '@angular/material-moment-adapter'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDividerModule } from '@angular/material/divider'; import { MatIconModule } from '@angular/material/icon'; @@ -42,7 +43,7 @@ import { CdkTreeModule } from '@angular/cdk/tree'; import { ScrollingModule } from '@angular/cdk/scrolling'; // ngx-translate -import { TranslateModule } from '@ngx-translate/core'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; // ngx-file-drop import { NgxFileDropModule } from 'ngx-file-drop'; @@ -61,6 +62,9 @@ import { PblNgridModule } from '@pebula/ngrid'; import { PblNgridMaterialModule } from '@pebula/ngrid-material'; import { PblNgridTargetEventsModule } from '@pebula/ngrid/target-events'; +// time picker because angular still doesnt offer one!! +import { NgxMaterialTimepickerModule } from 'ngx-material-timepicker'; + // components import { HeadBarComponent } from './components/head-bar/head-bar.component'; import { LegalNoticeContentComponent } from './components/legal-notice-content/legal-notice-content.component'; @@ -125,7 +129,7 @@ import { HeightResizingDirective } from './directives/height-resizing.directive' MatCheckboxModule, MatToolbarModule, MatDatepickerModule, - MatNativeDateModule, + MatMomentDateModule, MatCardModule, MatInputModule, MatTableModule, @@ -164,7 +168,8 @@ import { HeightResizingDirective } from './directives/height-resizing.directive' PblNgridModule, PblNgridMaterialModule, PblNgridTargetEventsModule, - PdfViewerModule + PdfViewerModule, + NgxMaterialTimepickerModule ], exports: [ FormsModule, @@ -248,7 +253,8 @@ import { HeightResizingDirective } from './directives/height-resizing.directive' RoundedInputComponent, GlobalSpinnerComponent, OverlayComponent, - PreviewComponent + PreviewComponent, + NgxMaterialTimepickerModule ], declarations: [ PermsDirective, @@ -296,7 +302,11 @@ import { HeightResizingDirective } from './directives/height-resizing.directive' HeightResizingDirective ], providers: [ - { provide: DateAdapter, useClass: OpenSlidesDateAdapter }, + { + provide: DateAdapter, + useClass: OpenSlidesDateAdapter, + deps: [TranslateService, MAT_DATE_LOCALE, MAT_MOMENT_DATE_ADAPTER_OPTIONS] + }, // see remarks in OpenSlidesDateAdapter SearchValueSelectorComponent, SortingListComponent, SortingTreeComponent, diff --git a/client/src/app/site/config/components/config-field/config-field.component.html b/client/src/app/site/config/components/config-field/config-field.component.html index caf196af8..e6399130c 100644 --- a/client/src/app/site/config/components/config-field/config-field.component.html +++ b/client/src/app/site/config/components/config-field/config-field.component.html @@ -66,16 +66,36 @@
- - - {{ configItem.label | translate }} - - check_circle - - {{ error }} +
+ + {{ configItem.label | translate }} + + {{ configItem.helpText | translate }} +
+ check_circle + +
+ {{ error }} + +
+ + +
+ check_circle + +
+ {{ error }} + +
+
diff --git a/client/src/app/site/config/components/config-field/config-field.component.scss b/client/src/app/site/config/components/config-field/config-field.component.scss index b7475bb00..6980b01da 100644 --- a/client/src/app/site/config/components/config-field/config-field.component.scss +++ b/client/src/app/site/config/components/config-field/config-field.component.scss @@ -3,6 +3,35 @@ width: 100%; } +.datetimepicker-container { + margin-left: -20px; + + .mat-form-field { + width: 50%; + box-sizing: border-box; + padding-left: 20px; + + .suffix-wrapper { + display: flex; + align-items: center; + + .mat-datepicker-toggle { + padding-left: 4px; + padding-right: 4px; + + .mat-datepicker-toggle-default-icon { + width: 20px; + margin-bottom: 10px; + } + } + + .ngx-material-timepicker-toggle { + width: 28px; + } + } + } +} + /* limit the color bar of color inputs */ input[type='color'] { max-width: 100px; diff --git a/client/src/app/site/config/components/config-field/config-field.component.ts b/client/src/app/site/config/components/config-field/config-field.component.ts index 1da619738..0696ee6e3 100644 --- a/client/src/app/site/config/components/config-field/config-field.component.ts +++ b/client/src/app/site/config/components/config-field/config-field.component.ts @@ -1,8 +1,10 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit, ViewEncapsulation } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; import { Title } from '@angular/platform-browser'; import { TranslateService } from '@ngx-translate/core'; +import * as moment from 'moment'; +import { Moment } from 'moment'; import { distinctUntilChanged } from 'rxjs/operators'; import { BaseComponent } from 'app/base.component'; @@ -12,7 +14,7 @@ import { ViewConfig } from '../../models/view-config'; /** * Component for a config field, used by the {@link ConfigListComponent}. Handles - * all inpu types defined by the server, as well as updating the configs + * all input types defined by the server, as well as updating the configs * * @example * ```ts @@ -23,16 +25,12 @@ import { ViewConfig } from '../../models/view-config'; selector: 'os-config-field', templateUrl: './config-field.component.html', styleUrls: ['./config-field.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None // to style the date and time pickers }) export class ConfigFieldComponent extends BaseComponent implements OnInit { public configItem: ViewConfig; - /** - * Date representation od the config value, used by the datetimepicker - */ - public dateValue: Date; - /** * Option to show a green check-icon. */ @@ -58,8 +56,6 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit { */ public translatedValue: object; - public rawDate: Date; - /** * The config item for this component. Just accepts components with already * populated constants-info. @@ -70,12 +66,18 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit { this.configItem = value; if (this.form) { - this.form.patchValue( - { - value: this.configItem.value - }, - { emitEvent: false } - ); + if (this.configItem.inputType === 'datetimepicker') { + // datetime has to be converted + const datetimeObj = this.unixToDateAndTime(this.configItem.value as number); + this.form.patchValue(datetimeObj, { emitEvent: false }); + } else { + this.form.patchValue( + { + value: this.configItem.value + }, + { emitEvent: false } + ); + } } } } @@ -99,7 +101,6 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit { * @param formBuilder FormBuilder * @param cd ChangeDetectorRef * @param repo ConfigRepositoryService - * @param dateTimeAdapter DateTimeAdapter */ public constructor( protected titleService: Title, @@ -116,7 +117,9 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit { */ public ngOnInit(): void { this.form = this.formBuilder.group({ - value: [''] + value: [''], + date: [''], + time: [''] }); this.translatedValue = this.configItem.value; if ( @@ -128,8 +131,9 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit { this.translatedValue = this.translate.instant(this.configItem.value); } } - if (this.configItem.inputType === 'datetimepicker') { - this.dateValue = new Date(this.configItem.value as number); + if (this.configItem.inputType === 'datetimepicker' && this.configItem.value) { + const datetimeObj = this.unixToDateAndTime(this.configItem.value as number); + this.form.patchValue(datetimeObj); } this.form.patchValue({ value: this.translatedValue @@ -143,6 +147,41 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit { }); } + /** + * Helper function to split a unix timestamp into a date as a moment object and a time string in the form of HH:SS + * + * @param unix the timestamp + * + * @return an object with a date and a time field + */ + private unixToDateAndTime(unix: number): { date: Moment; time: string } { + const date = moment.unix(unix); + const time = date.hours() + ':' + date.minutes(); + return { date: date, time: time }; + } + + /** + * Helper function to fuse a moment object as the date part and a time string (HH:SS) as the time part. + * + * @param date the moment date object + * @param time the time string + * + * @return a unix timestamp + */ + private dateAndTimeToUnix(date: Moment, time: string): number { + if (date) { + if (time) { + const timeSplit = time.split(':'); + // + is faster than parseint and number(). ~~ would be fastest but prevented by linter... + date.hour(+timeSplit[0]); + date.minute(+timeSplit[1]); + } + return date.unix(); + } else { + return null; + } + } + /** * Trigger an update of the data */ @@ -152,7 +191,10 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit { return; } if (this.configItem.inputType === 'datetimepicker') { - this.dateValue = new Date(value as number); + // datetime has to be converted + const date = this.form.get('date').value; + const time = this.form.get('time').value; + value = this.dateAndTimeToUnix(date, time); } if (this.debounceTimeout !== null) { clearTimeout(this.debounceTimeout); @@ -214,6 +256,8 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit { /** * Sets the error on this field. + * + * @param error The error as string. */ private setError(error: string): void { this.error = error; @@ -226,6 +270,7 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit { * input, textarea, choice or date * * @param type: the type of a config item + * * @returns the template type */ public formType(type: string): string { @@ -243,6 +288,7 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit { * Checks of the config.type can be part of the form * * @param type the config.type of a setting + * * @returns wheather it should be excluded or not */ public isExcludedType(type: string): boolean { @@ -250,17 +296,6 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit { return excluded.includes(type); } - /** - * custom handler for datetime picker updates. Sets the form's value - * to the timestamp of the Date being the event's value - * - * @param event an event-like object with a Date as value property - */ - public updateTime(event: { value: Date }): void { - this.dateValue = event.value; - this.onChange(event.value.valueOf()); - } - /** * Determines if a reset buton should be offered. * TODO: is 'null' a valid default in some cases? diff --git a/openslides/core/config_variables.py b/openslides/core/config_variables.py index 74be5f693..d1cec9c2a 100644 --- a/openslides/core/config_variables.py +++ b/openslides/core/config_variables.py @@ -36,6 +36,7 @@ def get_config_variables(): yield ConfigVariable( name="general_event_date", default_value="", + input_type="datetimepicker", label="Event date", weight=120, group="General",