mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-15 14:13:06 +00:00
refactor: change opening hours handling
fix: opening hours not updating feat: lazy-load opening hours module feat: add e2e tests for opening hours refactor: migrate opening hours to on-push change detection feat: show exact minutes in opening hours starting one hour before next change
This commit is contained in:
11
.changeset/cold-squids-remain.md
Normal file
11
.changeset/cold-squids-remain.md
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
'@openstapps/app': patch
|
||||
---
|
||||
|
||||
Refactored Opening Hours
|
||||
|
||||
- Migrated Opening Hours to use OnPush change detection
|
||||
- Fixed a bug where opening hours would not update correctly
|
||||
- Lazy-load opening hours module to keep it out of the main bundle
|
||||
- Added e2e tests to verify functionality
|
||||
- Changed live update status to show exact minutes starting one hour before the next change
|
||||
@@ -15,8 +15,12 @@
|
||||
buildToolsVersions = [ "${buildToolsVersion}" ];
|
||||
platformVersions = [ "32" ];
|
||||
};
|
||||
cypress = prev.cypress.overrideAttrs(prev: {
|
||||
version = "12.17.1";
|
||||
cypress = prev.cypress.overrideAttrs(cyPrev: rec {
|
||||
version = "13.2.0";
|
||||
src = prev.fetchzip {
|
||||
url = "https://cdn.cypress.io/desktop/${version}/linux-x64/cypress.zip";
|
||||
hash = "sha256-9o0nprGcJhudS1LNm+T7Vf0Dwd1RBauYKI+w1FBQ3ZM=";
|
||||
};
|
||||
});
|
||||
})
|
||||
];
|
||||
|
||||
@@ -49,10 +49,16 @@ The command `ionic cordova run ios` runs into the error `/platforms/ios/build/em
|
||||
|
||||
The browser doesn't open or the tests don't connect to a browser
|
||||
|
||||
#### Cause
|
||||
|
||||
Cypress was installed to a read-only location, see
|
||||
[this issue](https://github.com/cypress-io/cypress/issues/18893).
|
||||
This can be the case if you use NixOS.
|
||||
|
||||
#### Solution
|
||||
|
||||
Delete the Cypress config file
|
||||
Make sure the cypress folder is writable before each launch
|
||||
|
||||
```shell
|
||||
rm -rf ~/.config/Cypress
|
||||
chmod -R +rw ~/.config/Cypress
|
||||
```
|
||||
|
||||
@@ -182,6 +182,7 @@
|
||||
"builder": "@cypress/schematic:cypress",
|
||||
"options": {
|
||||
"devServerTarget": "app:serve",
|
||||
"liveReload": false,
|
||||
"watch": true,
|
||||
"headless": false
|
||||
},
|
||||
|
||||
141
frontend/app/cypress/integration/opening-hours.spec.ts
Normal file
141
frontend/app/cypress/integration/opening-hours.spec.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
describe('opening hours', () => {
|
||||
beforeEach(function () {
|
||||
cy.intercept('POST', 'https://mobile.server.uni-frankfurt.de/search', {
|
||||
fixture: 'search/types/canteen/canteen-search-result.json',
|
||||
}).as('search');
|
||||
});
|
||||
|
||||
it('should specify relative closing time', () => {
|
||||
cy.clock(new Date(2023, 9, 16, 15, 29), ['Date']);
|
||||
cy.visit('/canteen');
|
||||
|
||||
cy.get('stapps-opening-hours')
|
||||
.first()
|
||||
.should('contain', 'Geöffnet')
|
||||
.should('contain', 'Schließt heute um 22:00');
|
||||
});
|
||||
|
||||
it('should specify relative opening time', () => {
|
||||
cy.clock(new Date(2023, 9, 16, 6, 29), ['Date']);
|
||||
cy.visit('/canteen');
|
||||
|
||||
cy.get('stapps-opening-hours')
|
||||
.first()
|
||||
.should('contain', 'Geschlossen')
|
||||
.should('contain', 'Öffnet heute um 08:30');
|
||||
});
|
||||
|
||||
it('should specify soon opening time', () => {
|
||||
cy.clock(new Date(2023, 9, 16, 8, 0), ['Date']);
|
||||
cy.visit('/canteen');
|
||||
|
||||
cy.get('stapps-opening-hours')
|
||||
.first()
|
||||
.should('contain', 'Geschlossen')
|
||||
.should('contain', 'Öffnet in 30 Minuten');
|
||||
});
|
||||
|
||||
it('should specify soon closing time', () => {
|
||||
cy.clock(new Date(2023, 9, 16, 21, 30), ['Date']);
|
||||
cy.visit('/canteen');
|
||||
|
||||
cy.get('stapps-opening-hours')
|
||||
.first()
|
||||
.should('contain', 'Geöffnet')
|
||||
.should('contain', 'Schließt in 30 Minuten');
|
||||
});
|
||||
|
||||
it('should update the soon closing time every minute', () => {
|
||||
cy.clock(new Date(2023, 9, 16, 21, 30));
|
||||
cy.visit('/canteen');
|
||||
cy.tick(500);
|
||||
|
||||
cy.get('stapps-opening-hours')
|
||||
.first()
|
||||
.should('contain', 'Geöffnet')
|
||||
.should('contain', 'Schließt in 30 Minuten');
|
||||
|
||||
cy.tick(60_000);
|
||||
cy.tick(50);
|
||||
|
||||
cy.get('stapps-opening-hours')
|
||||
.first()
|
||||
.should('contain', 'Geöffnet')
|
||||
.should('contain', 'Schließt in 29 Minuten');
|
||||
});
|
||||
|
||||
it('should update the status when it changes', () => {
|
||||
cy.clock(new Date(2023, 9, 16, 21, 59));
|
||||
cy.visit('/canteen');
|
||||
cy.tick(500);
|
||||
|
||||
cy.get('stapps-opening-hours')
|
||||
.first()
|
||||
.should('contain', 'Geöffnet')
|
||||
.should('contain', 'Schließt in 1 Minute');
|
||||
|
||||
cy.tick(60_000);
|
||||
|
||||
cy.get('stapps-opening-hours')
|
||||
.first()
|
||||
.should('contain', 'Geschlossen')
|
||||
.should('contain', 'Öffnet morgen um 08:30');
|
||||
});
|
||||
|
||||
// This one takes long to execute!
|
||||
it('should update as expected', () => {
|
||||
cy.clock(new Date(2023, 9, 16, 20, 59));
|
||||
cy.visit('/canteen');
|
||||
cy.tick(500);
|
||||
|
||||
cy.get('stapps-opening-hours')
|
||||
.first()
|
||||
.should('contain', 'Geöffnet')
|
||||
.should('contain', 'Schließt heute um 22:00');
|
||||
|
||||
cy.tick(60_000);
|
||||
|
||||
cy.get('stapps-opening-hours')
|
||||
.first()
|
||||
.should('contain', 'Geöffnet')
|
||||
.should('contain', 'Schließt in 60 Minuten');
|
||||
|
||||
cy.tick(30 * 60_000);
|
||||
|
||||
cy.get('stapps-opening-hours')
|
||||
.first()
|
||||
.should('contain', 'Geöffnet')
|
||||
.should('contain', 'Schließt in 30 Minuten');
|
||||
|
||||
cy.tick(30 * 60_000);
|
||||
|
||||
cy.get('stapps-opening-hours')
|
||||
.first()
|
||||
.should('contain', 'Geschlossen')
|
||||
.should('contain', 'Öffnet morgen um 08:30');
|
||||
|
||||
cy.tick(9.5 * 60 * 60_000);
|
||||
|
||||
cy.get('stapps-opening-hours')
|
||||
.first()
|
||||
.should('contain', 'Geschlossen')
|
||||
.should('contain', 'Öffnet in 60 Minuten');
|
||||
|
||||
cy.tick(30 * 60_000);
|
||||
|
||||
cy.get('stapps-opening-hours')
|
||||
.first()
|
||||
.should('contain', 'Geschlossen')
|
||||
.should('contain', 'Öffnet in 30 Minuten');
|
||||
|
||||
cy.tick(30 * 60_000);
|
||||
|
||||
cy.get('stapps-opening-hours')
|
||||
.first()
|
||||
.should('contain', 'Geöffnet')
|
||||
.should('contain', 'Schließt heute um 22:00');
|
||||
|
||||
// Long tick warps will cause network requests to time out
|
||||
cy.get('@consoleError').invoke('resetHistory');
|
||||
});
|
||||
});
|
||||
@@ -31,36 +31,24 @@
|
||||
// When a command from ./commands is ready to use, import with `import './commands'` syntax
|
||||
// import './commands';
|
||||
|
||||
beforeEach(async function () {
|
||||
let databases: string[];
|
||||
if (window.indexedDB.databases) {
|
||||
databases = (await window.indexedDB.databases()).map(it => it.name);
|
||||
console.log('Trying to clear all databases');
|
||||
} else {
|
||||
console.log("Browser doesn't support database enumeration, deleting just ionic storage");
|
||||
databases = ['_ionicstorage'];
|
||||
}
|
||||
for (const database of databases) {
|
||||
if (database) {
|
||||
console.log(`Deleting database ${database}`);
|
||||
await new Promise(resolve => (window.indexedDB.deleteDatabase(database).onsuccess = resolve));
|
||||
console.log(`Deleted database ${database}`);
|
||||
}
|
||||
}
|
||||
beforeEach(function () {
|
||||
cy.wrap(
|
||||
new Promise(resolve => {
|
||||
window.indexedDB.deleteDatabase('_ionicstorage').onsuccess = resolve;
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
Cypress.on('window:before:load', window => {
|
||||
// Fake that user is using its browser in german language
|
||||
// Fake that user is using its browser in German
|
||||
Object.defineProperty(window.navigator, 'language', {value: 'de-DE'});
|
||||
Object.defineProperty(window.navigator, 'languages', [{value: 'de-DE'}]);
|
||||
|
||||
// Fail tests on console error
|
||||
cy.stub(window.console, 'error').callsFake(message => {
|
||||
// log out to the terminal
|
||||
cy.now('task', 'error', message);
|
||||
// log to Command Log and fail the test
|
||||
throw new Error(message);
|
||||
});
|
||||
cy.spy(window.console, 'error').as('consoleError');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
cy.get('@consoleError').should('not.have.been.called');
|
||||
});
|
||||
|
||||
Cypress.on('uncaught:exception', error => {
|
||||
|
||||
@@ -91,6 +91,8 @@
|
||||
"@types/dom-view-transitions": "1.0.1",
|
||||
"capacitor-secure-storage-plugin": "0.8.1",
|
||||
"cordova-plugin-calendar": "5.1.6",
|
||||
"date-fns": "2.30.0",
|
||||
"ngx-date-fns": "10.0.1",
|
||||
"deepmerge": "4.3.1",
|
||||
"form-data": "4.0.0",
|
||||
"geojson": "0.5.0",
|
||||
@@ -148,7 +150,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "5.60.1",
|
||||
"@typescript-eslint/parser": "5.60.1",
|
||||
"cordova-res": "0.15.4",
|
||||
"cypress": "12.17.1",
|
||||
"cypress": "13.2.0",
|
||||
"eslint": "8.43.0",
|
||||
"eslint-plugin-jsdoc": "46.4.2",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
|
||||
@@ -58,12 +58,15 @@ import {StorageProvider} from './modules/storage/storage.provider';
|
||||
import {AssessmentsModule} from './modules/assessments/assessments.module';
|
||||
import {ServiceHandlerInterceptor} from './_helpers/service-handler.interceptor';
|
||||
import {RoutingStackService} from './util/routing-stack.service';
|
||||
import {SCSettingValue} from '@openstapps/core';
|
||||
import {SCLanguageCode, SCSettingValue} from '@openstapps/core';
|
||||
import {DefaultAuthService} from './modules/auth/default-auth.service';
|
||||
import {PAIAAuthService} from './modules/auth/paia/paia-auth.service';
|
||||
import {IonIconModule} from './util/ion-icon/ion-icon.module';
|
||||
import {NavigationModule} from './modules/menu/navigation/navigation.module';
|
||||
import {browserFactory, SimpleBrowser} from './util/browser.factory';
|
||||
import {getDateFnsLocale} from './translation/dfns-locale';
|
||||
import {setDefaultOptions} from 'date-fns';
|
||||
import {DateFnsConfigurationService} from 'ngx-date-fns';
|
||||
|
||||
registerLocaleData(localeDe);
|
||||
|
||||
@@ -71,12 +74,6 @@ SwiperCore.use([FreeMode, Navigation]);
|
||||
|
||||
/**
|
||||
* Initializes data needed on startup
|
||||
* @param storageProvider provider of the saved data (using framework's storage)
|
||||
* @param logger TODO
|
||||
* @param settingsProvider provider of settings (e.g. language that has been set)
|
||||
* @param configProvider TODO
|
||||
* @param translateService TODO
|
||||
* @param _routingStackService Just for init and to track the stack from the get go
|
||||
*/
|
||||
export function initializerFactory(
|
||||
storageProvider: StorageProvider,
|
||||
@@ -87,6 +84,7 @@ export function initializerFactory(
|
||||
_routingStackService: RoutingStackService,
|
||||
defaultAuthService: DefaultAuthService,
|
||||
paiaAuthService: PAIAAuthService,
|
||||
dateFnsConfigurationService: DateFnsConfigurationService,
|
||||
) {
|
||||
return async () => {
|
||||
initLogger(logger);
|
||||
@@ -107,6 +105,10 @@ export function initializerFactory(
|
||||
translateService.setDefaultLang('en');
|
||||
translateService.use(languageCode);
|
||||
moment.locale(languageCode);
|
||||
const dateFnsLocale = await getDateFnsLocale(languageCode as SCLanguageCode);
|
||||
setDefaultOptions({locale: dateFnsLocale});
|
||||
dateFnsConfigurationService.setLocale(dateFnsLocale);
|
||||
|
||||
await defaultAuthService.init();
|
||||
await paiaAuthService.init();
|
||||
} catch (error) {
|
||||
@@ -198,6 +200,7 @@ export function createTranslateLoader(http: HttpClient) {
|
||||
RoutingStackService,
|
||||
DefaultAuthService,
|
||||
PAIAAuthService,
|
||||
DateFnsConfigurationService,
|
||||
],
|
||||
useFactory: initializerFactory,
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@ import {MapPosition} from '../../map/position.service';
|
||||
import {SearchPageComponent} from './search-page.component';
|
||||
import {Geolocation} from '@capacitor/geolocation';
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
import {pauseWhen} from '../../../util/pause-when';
|
||||
import {pauseWhen} from '../../../util/rxjs/pause-when';
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,7 +27,7 @@ import {MapProvider} from '../map.provider';
|
||||
import {MapPosition, PositionService} from '../position.service';
|
||||
import {Geolocation, PermissionStatus} from '@capacitor/geolocation';
|
||||
import {Capacitor} from '@capacitor/core';
|
||||
import {pauseWhen} from '../../../util/pause-when';
|
||||
import {pauseWhen} from '../../../util/rxjs/pause-when';
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
import {startViewTransition} from '../../../util/view-transition';
|
||||
|
||||
|
||||
@@ -12,13 +12,11 @@
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Injectable, OnDestroy, Pipe, PipeTransform} from '@angular/core';
|
||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
||||
import moment from 'moment';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {logger} from '../_helpers/ts-logger';
|
||||
import opening_hours from 'opening_hours';
|
||||
|
||||
@Injectable()
|
||||
@Pipe({
|
||||
@@ -110,141 +108,6 @@ export class StringSplitPipe implements PipeTransform {
|
||||
return this.value as never;
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@Pipe({
|
||||
name: 'openingHours',
|
||||
pure: true,
|
||||
})
|
||||
export class OpeningHoursPipe implements PipeTransform, OnDestroy {
|
||||
locale: string;
|
||||
|
||||
onLangChange?: Subscription;
|
||||
|
||||
value: string[] = [];
|
||||
|
||||
constructor(private readonly translate: TranslateService) {
|
||||
this.locale = translate.currentLang;
|
||||
}
|
||||
|
||||
private _dispose(): void {
|
||||
if (this.onLangChange?.closed === false) {
|
||||
this.onLangChange?.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._dispose();
|
||||
}
|
||||
|
||||
transform(aString: string | unknown): string[] {
|
||||
this.updateValue(aString);
|
||||
this._dispose();
|
||||
if (this.onLangChange?.closed === true) {
|
||||
this.onLangChange = this.translate.onLangChange.subscribe((event: LangChangeEvent) => {
|
||||
this.locale = event.lang;
|
||||
this.updateValue(aString);
|
||||
});
|
||||
}
|
||||
|
||||
return this.value;
|
||||
}
|
||||
|
||||
updateValue(aString: string | unknown) {
|
||||
if (typeof aString !== 'string') {
|
||||
logger.warn(`openingHours pipe unable to parse input: ${aString}`);
|
||||
|
||||
return;
|
||||
}
|
||||
let openingHours;
|
||||
|
||||
try {
|
||||
openingHours = new opening_hours(aString, {
|
||||
address: {
|
||||
country_code: 'de',
|
||||
state: 'Hessen',
|
||||
},
|
||||
lon: 8.667_97,
|
||||
lat: 50.129_16,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.warn(error);
|
||||
this.value = [];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const isOpen: boolean = openingHours.getState();
|
||||
const isUnknown: boolean = openingHours.getUnknown();
|
||||
|
||||
const nextChange = openingHours.getNextChange();
|
||||
const nextChangeIsOpen: boolean = openingHours.getState(nextChange);
|
||||
const nextChangeUnknown: boolean = openingHours.getUnknown(nextChange);
|
||||
const nextChangeIsToday: boolean = moment().isSame(nextChange, 'day');
|
||||
|
||||
let stateKey = isOpen ? 'common.openingHours.state_open' : 'common.openingHours.state_closed';
|
||||
|
||||
stateKey = isUnknown ? 'common.openingHours.state_maybe' : stateKey;
|
||||
|
||||
this.value = [isOpen ? 'success' : 'danger', `${this.translate.instant(stateKey)}`];
|
||||
|
||||
if (isUnknown) {
|
||||
const comment = openingHours.getComment();
|
||||
this.value = ['light', `${this.translate.instant(stateKey)}`];
|
||||
if (typeof comment === 'string') {
|
||||
this.value.push(comment);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextChangeUnknown) {
|
||||
return;
|
||||
}
|
||||
|
||||
let nextChangeKey: string | undefined;
|
||||
|
||||
let formattedCalender = moment(nextChange).calendar();
|
||||
|
||||
if (moment(nextChange).isBefore(moment().add(1, 'hours'))) {
|
||||
this.value[0] = 'warning';
|
||||
nextChangeKey = nextChangeIsOpen
|
||||
? 'common.openingHours.opening_soon_warning'
|
||||
: 'common.openingHours.closing_soon_warning';
|
||||
this.value.push(
|
||||
`${this.translate.instant(nextChangeKey, {
|
||||
time: new Intl.DateTimeFormat(this.locale, {
|
||||
timeStyle: 'short',
|
||||
}).format(nextChange),
|
||||
})}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextChangeIsToday) {
|
||||
nextChangeKey = nextChangeIsOpen
|
||||
? 'common.openingHours.opening_today'
|
||||
: 'common.openingHours.closing_today';
|
||||
this.value.push(
|
||||
`${this.translate.instant(nextChangeKey, {
|
||||
time: new Intl.DateTimeFormat(this.locale, {
|
||||
timeStyle: 'short',
|
||||
}).format(nextChange),
|
||||
})}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
nextChangeKey = nextChangeIsOpen ? 'common.openingHours.opening' : 'common.openingHours.closing';
|
||||
formattedCalender = formattedCalender.slice(0, 1).toUpperCase() + formattedCalender.slice(1);
|
||||
this.value.push(
|
||||
`${this.translate.instant(nextChangeKey, {
|
||||
relativeDateTime: formattedCalender,
|
||||
})}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@Pipe({
|
||||
name: 'durationLocalized',
|
||||
|
||||
35
frontend/app/src/app/translation/dfns-locale.ts
Normal file
35
frontend/app/src/app/translation/dfns-locale.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2023 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* 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 General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {SCLanguageCode} from '@openstapps/core';
|
||||
import type {Locale} from 'date-fns';
|
||||
|
||||
type LocalesMap = Record<SCLanguageCode, () => Promise<{default: Locale}>>;
|
||||
|
||||
const LOCALES = {
|
||||
en: () => import('date-fns/locale/en-GB'),
|
||||
de: () => import('date-fns/locale/de'),
|
||||
} satisfies Partial<LocalesMap>;
|
||||
|
||||
/**
|
||||
* Get a Date Fns Locale
|
||||
*/
|
||||
export async function getDateFnsLocale(code: SCLanguageCode): Promise<Locale> {
|
||||
if (code in LOCALES) {
|
||||
return LOCALES[code as keyof typeof LOCALES]().then(it => it.default);
|
||||
} else {
|
||||
console.warn(`Unknown Locale "${code}" for Date Fns. Falling back to English.`);
|
||||
return LOCALES.en().then(it => it.default);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {ModuleWithProviders, NgModule, Provider} from '@angular/core';
|
||||
import {
|
||||
ArrayJoinPipe,
|
||||
@@ -23,7 +22,6 @@ import {
|
||||
IsNumericPipe,
|
||||
MetersLocalizedPipe,
|
||||
NumberLocalizedPipe,
|
||||
OpeningHoursPipe,
|
||||
SentenceCasePipe,
|
||||
StringSplitPipe,
|
||||
ToUnixPipe,
|
||||
@@ -51,7 +49,6 @@ export interface ThingTranslateModuleConfig {
|
||||
ThingTranslatePipe,
|
||||
TranslateSimplePipe,
|
||||
DateLocalizedFormatPipe,
|
||||
OpeningHoursPipe,
|
||||
SentenceCasePipe,
|
||||
ToUnixPipe,
|
||||
EntriesPipe,
|
||||
@@ -69,7 +66,6 @@ export interface ThingTranslateModuleConfig {
|
||||
ThingTranslatePipe,
|
||||
TranslateSimplePipe,
|
||||
DateLocalizedFormatPipe,
|
||||
OpeningHoursPipe,
|
||||
SentenceCasePipe,
|
||||
ToUnixPipe,
|
||||
EntriesPipe,
|
||||
|
||||
@@ -25,6 +25,9 @@ import {
|
||||
import moment from 'moment';
|
||||
import {isDefined, ThingTranslateParser} from './thing-translate.parser';
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
import {setDefaultOptions} from 'date-fns';
|
||||
import {DateFnsConfigurationService} from 'ngx-date-fns';
|
||||
import {getDateFnsLocale} from './dfns-locale';
|
||||
|
||||
// export const DEFAULT_LANGUAGE = new InjectionToken<string>('DEFAULT_LANGUAGE');
|
||||
|
||||
@@ -40,8 +43,13 @@ export class ThingTranslateService {
|
||||
*
|
||||
* @param translateService Instance of Angular TranslateService
|
||||
* @param parser An instance of the parser currently used
|
||||
* @param dfnsConfiguration the date fns configuration
|
||||
*/
|
||||
constructor(private readonly translateService: TranslateService, public parser: ThingTranslateParser) {
|
||||
constructor(
|
||||
private readonly translateService: TranslateService,
|
||||
public parser: ThingTranslateParser,
|
||||
private dfnsConfiguration: DateFnsConfigurationService,
|
||||
) {
|
||||
this.translator = new SCThingTranslator(
|
||||
(translateService.currentLang ?? translateService.defaultLang) as SCLanguageCode,
|
||||
);
|
||||
@@ -49,6 +57,10 @@ export class ThingTranslateService {
|
||||
this.translateService.onLangChange.pipe(takeUntilDestroyed()).subscribe((event: LangChangeEvent) => {
|
||||
this.translator.language = event.lang as keyof SCTranslations<SCLanguage>;
|
||||
moment.locale(event.lang);
|
||||
getDateFnsLocale(event.lang as SCLanguageCode).then(locale => {
|
||||
setDefaultOptions({locale});
|
||||
this.dfnsConfiguration.setLocale(locale);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -12,64 +12,49 @@
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Component, ContentChild, Input, OnDestroy, OnInit, TemplateRef} from '@angular/core';
|
||||
import opening_hours from 'opening_hours';
|
||||
import {ChangeDetectionStrategy, Component, ContentChild, Input, TemplateRef} from '@angular/core';
|
||||
import {interval, Observable} from 'rxjs';
|
||||
import {fromOpeningHours} from './opening-hours';
|
||||
import {map, startWith} from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'stapps-opening-hours',
|
||||
templateUrl: 'opening-hours.html',
|
||||
styleUrls: ['opening-hours.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class OpeningHoursComponent implements OnDestroy, OnInit {
|
||||
export class OpeningHoursComponent {
|
||||
@ContentChild(TemplateRef) content: TemplateRef<unknown>;
|
||||
|
||||
@Input() openingHours?: string;
|
||||
|
||||
@Input() colorize = true;
|
||||
|
||||
@Input() showNextChange = true;
|
||||
|
||||
timer: NodeJS.Timeout;
|
||||
openingHours$?: Observable<{
|
||||
color: 'light' | 'warning' | 'success' | 'danger';
|
||||
statusName: string;
|
||||
statusText?: string;
|
||||
nextChangeAction: 'closing' | 'opening';
|
||||
nextChangeSoon?: Observable<Date | undefined>;
|
||||
nextChange?: Date;
|
||||
}>;
|
||||
|
||||
updateTimer() {
|
||||
if (typeof this.openingHours !== 'string') {
|
||||
return;
|
||||
}
|
||||
clearTimeout(this.timer);
|
||||
|
||||
const ohObject = new opening_hours(this.openingHours, {
|
||||
address: {
|
||||
country_code: 'de',
|
||||
state: 'Hessen',
|
||||
},
|
||||
lon: 8.667_97,
|
||||
lat: 50.129_16,
|
||||
});
|
||||
|
||||
const millisecondsRemaining =
|
||||
// eslint-disable-next-line unicorn/prefer-date-now
|
||||
(ohObject.getNextChange()?.getTime() ?? 0) - new Date().getTime() + 1000;
|
||||
|
||||
if (millisecondsRemaining > 1_209_600_000) {
|
||||
// setTimeout has upper bound of 0x7FFFFFFF
|
||||
// ignore everything over a week
|
||||
return;
|
||||
}
|
||||
|
||||
if (millisecondsRemaining > 0) {
|
||||
this.timer = setTimeout(() => {
|
||||
// pseudo update value to tigger openingHours pipe
|
||||
this.openingHours = `${this.openingHours}`;
|
||||
this.updateTimer();
|
||||
}, millisecondsRemaining);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.updateTimer();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
clearTimeout(this.timer);
|
||||
@Input() set openingHours(value: string | undefined) {
|
||||
if (!value) return;
|
||||
this.openingHours$ = fromOpeningHours(value).pipe(
|
||||
map(({isUnknown, isOpen, changesSoon, nextChange, comment}) => ({
|
||||
color: isUnknown ? 'light' : changesSoon ? 'warning' : isOpen ? 'success' : 'danger',
|
||||
statusName: `common.openingHours.state_${isUnknown ? 'maybe' : isOpen ? 'open' : 'closed'}`,
|
||||
statusText: comment,
|
||||
nextChangeAction: isOpen ? 'closing' : 'opening',
|
||||
nextChangeSoon: changesSoon
|
||||
? interval(60_000).pipe(
|
||||
startWith(nextChange),
|
||||
map(() => nextChange),
|
||||
)
|
||||
: undefined,
|
||||
nextChange,
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,18 +13,20 @@
|
||||
~ this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<ng-container *ngIf="openingHours">
|
||||
<div>
|
||||
<ng-template [ngIf]="colorize" [ngIfElse]="blank">
|
||||
<ion-badge
|
||||
[color]="openingHours | openingHours | slice : 0 : 1 | join : ' '"
|
||||
slot="start"
|
||||
style="vertical-align: bottom"
|
||||
>
|
||||
{{ openingHours | openingHours | slice : 1 : 2 }}
|
||||
</ion-badge>
|
||||
</ng-template>
|
||||
<ng-template #blank> {{ openingHours | openingHours | slice : 1 : 2 }} </ng-template>
|
||||
<ng-container *ngIf="showNextChange"> {{ openingHours | openingHours | slice : 2 : 3 }} </ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div *ngIf="openingHours$ | async as openingHours">
|
||||
<ion-badge *ngIf="colorize; else blank" [color]="openingHours.color" slot="start">
|
||||
{{ openingHours.statusName | translate }}
|
||||
</ion-badge>
|
||||
<ng-template #blank>{{ openingHours.statusName | translate }}</ng-template>
|
||||
<ng-container *ngIf="openingHours.statusText; else nextChange"> {{openingHours.statusText}} </ng-container>
|
||||
<ng-template #nextChange>
|
||||
<ng-container *ngIf="showNextChange && openingHours.nextChangeSoon">
|
||||
{{ ('common.openingHours.' + openingHours.nextChangeAction + '_soon') | translate: {duration:
|
||||
(openingHours.nextChangeSoon | async | dfnsFormatDistanceToNowStrict: {unit: 'minute'})} }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="showNextChange && !openingHours.nextChangeSoon">
|
||||
{{ ('common.openingHours.' + openingHours.nextChangeAction) | translate: {date: openingHours.nextChange
|
||||
| dfnsFormatRelativeToNow} }}
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
3
frontend/app/src/app/util/opening-hours.scss
Normal file
3
frontend/app/src/app/util/opening-hours.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
ion-badge {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
72
frontend/app/src/app/util/opening-hours.ts
Normal file
72
frontend/app/src/app/util/opening-hours.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (C) 2023 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* 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 General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import type {nominatim_object} from 'opening_hours';
|
||||
import {from, Observable, map, expand, of, delay} from 'rxjs';
|
||||
import {lazy} from './rxjs/lazy';
|
||||
import {isAfter, subHours} from 'date-fns';
|
||||
|
||||
export const OPENING_HOURS_REFERENCE = {
|
||||
address: {
|
||||
country_code: 'de',
|
||||
state: 'Hessen',
|
||||
},
|
||||
lon: 8.667_97,
|
||||
lat: 50.129_16,
|
||||
} satisfies nominatim_object;
|
||||
|
||||
export interface OpeningHoursInfo {
|
||||
isOpen: boolean;
|
||||
changesSoon: boolean;
|
||||
isUnknown: boolean;
|
||||
nextChange?: Date;
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opening Hours is a pretty huge CommonJS module,
|
||||
* so we lazy-load it
|
||||
*/
|
||||
const OpeningHours = lazy(() => import('opening_hours').then(it => it.default));
|
||||
|
||||
/**
|
||||
* Create an observable from opening hours
|
||||
* @param openingHours The opening hours string
|
||||
* @param soonThresholdHours The number of hours before which a change is marked as "soon"
|
||||
*/
|
||||
export function fromOpeningHours(openingHours: string, soonThresholdHours = 1): Observable<OpeningHoursInfo> {
|
||||
return from(OpeningHours).pipe(
|
||||
map(OpeningHours => new OpeningHours(openingHours, OPENING_HOURS_REFERENCE)),
|
||||
expand(it => {
|
||||
const now = new Date();
|
||||
const nextChange = it.getNextChange(now);
|
||||
const changesSoon = nextChange ? isAfter(now, subHours(nextChange, soonThresholdHours)) : false;
|
||||
|
||||
const changeTime = nextChange && !changesSoon ? subHours(nextChange, soonThresholdHours) : nextChange;
|
||||
return changeTime ? of(it).pipe(delay(changeTime)) : of();
|
||||
}),
|
||||
map(it => {
|
||||
const now = new Date();
|
||||
const nextChange = it.getNextChange(now);
|
||||
|
||||
return {
|
||||
isOpen: it.getState(now),
|
||||
isUnknown: it.getUnknown(now),
|
||||
changesSoon: nextChange ? isAfter(now, subHours(nextChange, soonThresholdHours)) : false,
|
||||
comment: it.getComment(now),
|
||||
nextChange,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
17
frontend/app/src/app/util/rxjs/lazy.ts
Normal file
17
frontend/app/src/app/util/rxjs/lazy.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import {Observable} from 'rxjs';
|
||||
|
||||
/**
|
||||
* Lazy-load something
|
||||
* @param construct the constructing function
|
||||
* @example lazy(() => import('module').then(it => it.default))
|
||||
*/
|
||||
export function lazy<T>(construct: () => Promise<T>): Observable<T> {
|
||||
let value: Promise<T>;
|
||||
return new Observable<T>(subscriber => {
|
||||
value ??= construct();
|
||||
value.then(it => {
|
||||
subscriber.next(it);
|
||||
subscriber.complete();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -32,9 +32,18 @@ import {SearchbarAutofocusDirective} from './searchbar-autofocus.directive';
|
||||
import {SectionComponent} from './section.component';
|
||||
import {RouterModule} from '@angular/router';
|
||||
import {IonContentParallaxDirective} from './ion-content-parallax.directive';
|
||||
import {FormatDistanceToNowStrictPipeModule, FormatRelativeToNowPipeModule} from 'ngx-date-fns';
|
||||
|
||||
@NgModule({
|
||||
imports: [BrowserModule, IonicModule, TranslateModule, ThingTranslateModule.forChild(), RouterModule],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
IonicModule,
|
||||
TranslateModule,
|
||||
ThingTranslateModule.forChild(),
|
||||
RouterModule,
|
||||
FormatRelativeToNowPipeModule,
|
||||
FormatDistanceToNowStrictPipeModule,
|
||||
],
|
||||
declarations: [
|
||||
IonContentParallaxDirective,
|
||||
ElementSizeChangeDirective,
|
||||
|
||||
@@ -69,15 +69,13 @@
|
||||
},
|
||||
"common": {
|
||||
"openingHours": {
|
||||
"closing": "Schließt {{relativeDateTime}}",
|
||||
"closing_soon_warning": "Schließt bald! Um {{time}} Uhr",
|
||||
"closing_today": "Schließt um {{time}} Uhr",
|
||||
"closing": "Schließt {{date}}",
|
||||
"closing_soon": "Schließt in {{duration}}",
|
||||
"state_closed": "Geschlossen",
|
||||
"state_maybe": "Vielleicht Geöffnet",
|
||||
"state_open": "Geöffnet",
|
||||
"opening": "Öffnet {{relativeDateTime}}",
|
||||
"opening_today": "Öffnet um {{time}} Uhr",
|
||||
"opening_soon_warning": "Öffnet bald! Um {{time}} Uhr"
|
||||
"opening": "Öffnet {{date}}",
|
||||
"opening_soon": "Öffnet in {{duration}}"
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
|
||||
@@ -69,15 +69,13 @@
|
||||
},
|
||||
"common": {
|
||||
"openingHours": {
|
||||
"closing": "Closing {{relativeDateTime}}",
|
||||
"closing_soon_warning": "Closing soon! At {{time}}",
|
||||
"closing_today": "Closing at {{time}}",
|
||||
"closing": "Closing {{date}}",
|
||||
"closing_soon": "Closing in {{duration}}",
|
||||
"state_closed": "Closed",
|
||||
"state_maybe": "Maybe open",
|
||||
"state_open": "Open",
|
||||
"opening": "Opens {{relativeDateTime}}",
|
||||
"opening_today": "Opens at {{time}}",
|
||||
"opening_soon_warning": "Opens soon! At {{time}}"
|
||||
"opening": "Opens {{date}}",
|
||||
"opening_soon": "Opens in {{duration}}"
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
|
||||
93
pnpm-lock.yaml
generated
93
pnpm-lock.yaml
generated
@@ -836,6 +836,9 @@ importers:
|
||||
cordova-plugin-calendar:
|
||||
specifier: 5.1.6
|
||||
version: 5.1.6
|
||||
date-fns:
|
||||
specifier: 2.30.0
|
||||
version: 2.30.0
|
||||
deepmerge:
|
||||
specifier: 4.3.1
|
||||
version: 4.3.1
|
||||
@@ -863,6 +866,9 @@ importers:
|
||||
moment:
|
||||
specifier: 2.29.4
|
||||
version: 2.29.4
|
||||
ngx-date-fns:
|
||||
specifier: 10.0.1
|
||||
version: 10.0.1(@angular/common@16.1.4)(@angular/core@16.1.4)(date-fns@2.30.0)
|
||||
ngx-logger:
|
||||
specifier: 5.0.12
|
||||
version: 5.0.12(rxjs@7.8.1)
|
||||
@@ -1003,8 +1009,8 @@ importers:
|
||||
specifier: 0.15.4
|
||||
version: 0.15.4
|
||||
cypress:
|
||||
specifier: 12.17.1
|
||||
version: 12.17.1
|
||||
specifier: 13.2.0
|
||||
version: 13.2.0
|
||||
eslint:
|
||||
specifier: 8.43.0
|
||||
version: 8.43.0
|
||||
@@ -2656,7 +2662,7 @@ packages:
|
||||
rxjs: ^5.5.0 || ^6.5.0 || ^7.3.0
|
||||
dependencies:
|
||||
'@awesome-cordova-plugins/core': 5.45.0(rxjs@7.8.1)
|
||||
'@types/cordova': 11.0.0
|
||||
'@types/cordova': 11.0.1
|
||||
rxjs: 7.8.1
|
||||
dev: false
|
||||
|
||||
@@ -2665,7 +2671,7 @@ packages:
|
||||
peerDependencies:
|
||||
rxjs: ^5.5.0 || ^6.5.0 || ^7.3.0
|
||||
dependencies:
|
||||
'@types/cordova': 11.0.0
|
||||
'@types/cordova': 11.0.1
|
||||
rxjs: 7.8.1
|
||||
dev: false
|
||||
|
||||
@@ -5352,8 +5358,8 @@ packages:
|
||||
postcss-selector-parser: 6.0.13
|
||||
dev: true
|
||||
|
||||
/@cypress/request@2.88.11:
|
||||
resolution: {integrity: sha512-M83/wfQ1EkspjkE2lNWNV5ui2Cv7UCv1swW1DqljahbzLVWltcsexQh8jYtuS/vzFXP+HySntGM83ZXA9fn17w==}
|
||||
/@cypress/request@3.0.1:
|
||||
resolution: {integrity: sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==}
|
||||
engines: {node: '>= 6'}
|
||||
dependencies:
|
||||
aws-sign2: 0.7.0
|
||||
@@ -5371,7 +5377,7 @@ packages:
|
||||
performance-now: 2.1.0
|
||||
qs: 6.10.4
|
||||
safe-buffer: 5.2.1
|
||||
tough-cookie: 2.5.0
|
||||
tough-cookie: 4.1.3
|
||||
tunnel-agent: 0.6.0
|
||||
uuid: 8.3.2
|
||||
dev: true
|
||||
@@ -5755,7 +5761,7 @@ packages:
|
||||
peerDependencies:
|
||||
rxjs: ^5.5.0 || ^6.5.0
|
||||
dependencies:
|
||||
'@types/cordova': 11.0.0
|
||||
'@types/cordova': 11.0.1
|
||||
rxjs: 7.8.1
|
||||
dev: false
|
||||
|
||||
@@ -5767,7 +5773,7 @@ packages:
|
||||
rxjs: ^5.5.0 || ^6.5.0
|
||||
dependencies:
|
||||
'@ionic-native/core': 5.36.0(rxjs@7.8.1)
|
||||
'@types/cordova': 11.0.0
|
||||
'@types/cordova': 11.0.1
|
||||
rxjs: 7.8.1
|
||||
dev: false
|
||||
optional: true
|
||||
@@ -5780,7 +5786,7 @@ packages:
|
||||
rxjs: ^5.5.0 || ^6.5.0
|
||||
dependencies:
|
||||
'@ionic-native/core': 5.36.0(rxjs@7.8.1)
|
||||
'@types/cordova': 11.0.0
|
||||
'@types/cordova': 11.0.1
|
||||
rxjs: 7.8.1
|
||||
dev: false
|
||||
optional: true
|
||||
@@ -5793,7 +5799,7 @@ packages:
|
||||
rxjs: ^5.5.0 || ^6.5.0
|
||||
dependencies:
|
||||
'@ionic-native/core': 5.36.0(rxjs@7.8.1)
|
||||
'@types/cordova': 11.0.0
|
||||
'@types/cordova': 11.0.1
|
||||
rxjs: 7.8.1
|
||||
dev: false
|
||||
optional: true
|
||||
@@ -5806,7 +5812,7 @@ packages:
|
||||
rxjs: ^5.5.0 || ^6.5.0
|
||||
dependencies:
|
||||
'@ionic-native/core': 5.36.0(rxjs@7.8.1)
|
||||
'@types/cordova': 11.0.0
|
||||
'@types/cordova': 11.0.1
|
||||
rxjs: 7.8.1
|
||||
dev: false
|
||||
optional: true
|
||||
@@ -6728,8 +6734,8 @@ packages:
|
||||
resolution: {integrity: sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==}
|
||||
dev: true
|
||||
|
||||
/@types/cordova@11.0.0:
|
||||
resolution: {integrity: sha512-AtBm1IAqqXsXszJe6XxuA2iXLhraNCj25p/FHRyikPeW0Z3YfgM6qzWb+VJglJTmZc5lqRNy84cYM/sQI5v6Vw==}
|
||||
/@types/cordova@11.0.1:
|
||||
resolution: {integrity: sha512-Zd6LAhYUAdn0mL0SbxHeF4fO/3uzkcW3fzE0ZIK1wDlTRCWlI4/0i+Phb+otP9ryziyeW2LKofRNSP5yil85hA==}
|
||||
dev: false
|
||||
|
||||
/@types/cors@2.8.13:
|
||||
@@ -6964,6 +6970,10 @@ packages:
|
||||
/@types/node@18.15.3:
|
||||
resolution: {integrity: sha512-p6ua9zBxz5otCmbpb5D3U4B5Nanw6Pk3PPyX05xnxbB/fRv71N7CPmORg7uAD5P70T0xmx1pzAx/FUfa5X+3cw==}
|
||||
|
||||
/@types/node@18.17.17:
|
||||
resolution: {integrity: sha512-cOxcXsQ2sxiwkykdJqvyFS+MLQPLvIdwh5l6gNg8qF6s+C7XSkEWOZjK+XhUZd+mYvHV/180g2cnCcIl4l06Pw==}
|
||||
dev: true
|
||||
|
||||
/@types/nodemailer@6.4.7:
|
||||
resolution: {integrity: sha512-f5qCBGAn/f0qtRcd4SEn88c8Fp3Swct1731X4ryPKqS61/A3LmmzN8zaEz7hneJvpjFbUUgY7lru/B/7ODTazg==}
|
||||
dependencies:
|
||||
@@ -9471,15 +9481,15 @@ packages:
|
||||
resolution: {integrity: sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==}
|
||||
dev: true
|
||||
|
||||
/cypress@12.17.1:
|
||||
resolution: {integrity: sha512-eKfBgO6t8waEyhegL4gxD7tcI6uTCGttu+ZU7y9Hq8BlpMztd7iLeIF4AJFAnbZH1xjX+wwgg4cRKFNSvv3VWQ==}
|
||||
engines: {node: ^14.0.0 || ^16.0.0 || >=18.0.0}
|
||||
/cypress@13.2.0:
|
||||
resolution: {integrity: sha512-AvDQxBydE771GTq0TR4ZUBvv9m9ffXuB/ueEtpDF/6gOcvFR96amgwSJP16Yhqw6VhmwqspT5nAGzoxxB+D89g==}
|
||||
engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
'@cypress/request': 2.88.11
|
||||
'@cypress/request': 3.0.1
|
||||
'@cypress/xvfb': 1.2.4(supports-color@8.1.1)
|
||||
'@types/node': 14.18.38
|
||||
'@types/node': 18.17.17
|
||||
'@types/sinonjs__fake-timers': 8.1.1
|
||||
'@types/sizzle': 2.3.3
|
||||
arch: 2.2.0
|
||||
@@ -9512,6 +9522,7 @@ packages:
|
||||
minimist: 1.2.8
|
||||
ospath: 1.2.2
|
||||
pretty-bytes: 5.6.0
|
||||
process: 0.11.10
|
||||
proxy-from-env: 1.0.0
|
||||
request-progress: 3.0.0
|
||||
semver: 7.5.4
|
||||
@@ -14272,6 +14283,19 @@ packages:
|
||||
resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==}
|
||||
dev: true
|
||||
|
||||
/ngx-date-fns@10.0.1(@angular/common@16.1.4)(@angular/core@16.1.4)(date-fns@2.30.0):
|
||||
resolution: {integrity: sha512-8IdwrblaMULNQgqwA0Yo8HPpUMQDLKC1pvkoFTToElf7gZDPWroOva2cogUrrNXXWLkwA9YDpc1skZVfI+mmwA==}
|
||||
peerDependencies:
|
||||
'@angular/common': '>=14'
|
||||
'@angular/core': '>=14'
|
||||
date-fns: '>=2'
|
||||
dependencies:
|
||||
'@angular/common': 16.1.4(@angular/core@16.1.4)(rxjs@7.8.1)
|
||||
'@angular/core': 16.1.4(rxjs@7.8.1)(zone.js@0.13.1)
|
||||
date-fns: 2.30.0
|
||||
tslib: 2.4.1
|
||||
dev: false
|
||||
|
||||
/ngx-logger@5.0.12(rxjs@7.8.1):
|
||||
resolution: {integrity: sha512-4kTtPvxQoV2ka6pigtvkbtaLKpMYWqZm7Slu0YQVcwzBKoVR2K+oLmMVcA50S6kCxkZXq7iKcrXUKR2vhMXPqQ==}
|
||||
peerDependencies:
|
||||
@@ -15419,6 +15443,11 @@ packages:
|
||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||
dev: true
|
||||
|
||||
/process@0.11.10:
|
||||
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
|
||||
engines: {node: '>= 0.6.0'}
|
||||
dev: true
|
||||
|
||||
/progress@1.1.8:
|
||||
resolution: {integrity: sha512-UdA8mJ4weIkUBO224tIarHzuHs4HuYiJvsuGT7j/SPQiUJVjYvNDBIPa0hAorduOfjGohB/qHWRa/lrrWX/mXw==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
@@ -15563,6 +15592,10 @@ packages:
|
||||
engines: {node: '>=0.6'}
|
||||
dev: true
|
||||
|
||||
/querystringify@2.2.0:
|
||||
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
|
||||
dev: true
|
||||
|
||||
/queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
@@ -17574,6 +17607,16 @@ packages:
|
||||
punycode: 2.3.0
|
||||
dev: true
|
||||
|
||||
/tough-cookie@4.1.3:
|
||||
resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==}
|
||||
engines: {node: '>=6'}
|
||||
dependencies:
|
||||
psl: 1.9.0
|
||||
punycode: 2.3.0
|
||||
universalify: 0.2.0
|
||||
url-parse: 1.5.10
|
||||
dev: true
|
||||
|
||||
/tr46@1.0.1:
|
||||
resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
|
||||
dependencies:
|
||||
@@ -18116,6 +18159,11 @@ packages:
|
||||
engines: {node: '>= 4.0.0'}
|
||||
dev: true
|
||||
|
||||
/universalify@0.2.0:
|
||||
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
|
||||
engines: {node: '>= 4.0.0'}
|
||||
dev: true
|
||||
|
||||
/universalify@2.0.0:
|
||||
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
@@ -18157,6 +18205,13 @@ packages:
|
||||
resolution: {integrity: sha512-1WJ8YX1Kcec9wgxy8d/ATzGP1ayO6BRnd3iB6NlM+7cOnn6U8p5PKppRTCPLobh3CSdJ4d0TdPjopzyU2KcVFw==}
|
||||
dev: true
|
||||
|
||||
/url-parse@1.5.10:
|
||||
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
|
||||
dependencies:
|
||||
querystringify: 2.2.0
|
||||
requires-port: 1.0.0
|
||||
dev: true
|
||||
|
||||
/url-value-parser@2.2.0:
|
||||
resolution: {integrity: sha512-yIQdxJpgkPamPPAPuGdS7Q548rLhny42tg8d4vyTNzFqvOnwqrgHXvgehT09U7fwrzxi3RxCiXjoNUNnNOlQ8A==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
Reference in New Issue
Block a user