mirror of
https://github.com/Alfresco/alfresco-content-app.git
synced 2025-05-19 17:14:45 +00:00
[ACA-2322] Sidenav - support dynamic components (#1066)
* dynamic components support * subscribe to sidenav selector * update module for extension * sidenav selector * support dynamic components definistion * update docs * stabilise tests * Update src/app/components/sidenav/sidenav.module.ts Co-Authored-By: pionnegru <pionnegru@users.noreply.github.com> * Update src/app/components/sidenav/sidenav.module.ts Co-Authored-By: pionnegru <pionnegru@users.noreply.github.com> * rename selector
This commit is contained in:
parent
bbdf3a9b27
commit
5bf77dfc81
@ -186,4 +186,28 @@ Map the `/custom-route` in `app.routes.ts` as a child of `LayoutComponent` defin
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
### Rendering custom components
|
||||||
|
|
||||||
|
Navigation definition also supports custom components to be dynamically render. The schema for this is as follows:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"navbar": [
|
||||||
|
{
|
||||||
|
"id": "app.navbar.primary",
|
||||||
|
"items": [
|
||||||
|
...
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "custom-component",
|
||||||
|
"component": "custom-menu-item"
|
||||||
|
}
|
||||||
|
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that components must be declared as entryComponents under the app module.
|
||||||
|
|
||||||
For more information about the content of a custom page see [Document List Layout](/features/document-list-layout) section.
|
For more information about the content of a custom page see [Document List Layout](/features/document-list-layout) section.
|
||||||
|
@ -56,22 +56,22 @@ export abstract class Page {
|
|||||||
|
|
||||||
constructor(public url: string = '') {}
|
constructor(public url: string = '') {}
|
||||||
|
|
||||||
getTitle() {
|
async getTitle() {
|
||||||
return browser.getTitle();
|
return await browser.getTitle();
|
||||||
}
|
}
|
||||||
|
|
||||||
load(relativeUrl: string = '') {
|
async load(relativeUrl: string = '') {
|
||||||
const hash = USE_HASH_STRATEGY ? '/#' : '';
|
const hash = USE_HASH_STRATEGY ? '/#' : '';
|
||||||
const path = `${browser.baseUrl}${hash}${this.url}${relativeUrl}`;
|
const path = `${browser.baseUrl}${hash}${this.url}${relativeUrl}`;
|
||||||
return browser.get(path);
|
return await browser.get(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
waitForApp() {
|
async waitForApp() {
|
||||||
return browser.wait(EC.presenceOf(this.layout), BROWSER_WAIT_TIMEOUT);
|
return await browser.wait(EC.presenceOf(this.layout), BROWSER_WAIT_TIMEOUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
waitForSnackBarToAppear() {
|
async waitForSnackBarToAppear() {
|
||||||
return browser.wait(until.elementLocated(by.css('.mat-snack-bar-container')), BROWSER_WAIT_TIMEOUT, '------- timeout waiting for snackbar to appear');
|
return await browser.wait(until.elementLocated(by.css('.mat-snack-bar-container')), BROWSER_WAIT_TIMEOUT, '------- timeout waiting for snackbar to appear');
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitForSnackBarToClose() {
|
async waitForSnackBarToClose() {
|
||||||
|
@ -730,7 +730,7 @@ describe('Upload new version', () => {
|
|||||||
it('file is updated after uploading a new version - major - [C307004]', async () => {
|
it('file is updated after uploading a new version - major - [C307004]', async () => {
|
||||||
await searchInput.clickSearchButton();
|
await searchInput.clickSearchButton();
|
||||||
await searchInput.checkFilesAndFolders();
|
await searchInput.checkFilesAndFolders();
|
||||||
await searchInput.searchFor('search-f');
|
await searchInput.searchFor(fileSearch1);
|
||||||
await dataTable.waitForBody();
|
await dataTable.waitForBody();
|
||||||
await dataTable.selectItem(fileSearch1, parentSearch);
|
await dataTable.selectItem(fileSearch1, parentSearch);
|
||||||
await toolbar.clickMoreActionsUploadNewVersion();
|
await toolbar.clickMoreActionsUploadNewVersion();
|
||||||
@ -751,7 +751,7 @@ describe('Upload new version', () => {
|
|||||||
it('file is updated after uploading a new version - minor - [C307005]', async () => {
|
it('file is updated after uploading a new version - minor - [C307005]', async () => {
|
||||||
await searchInput.clickSearchButton();
|
await searchInput.clickSearchButton();
|
||||||
await searchInput.checkFilesAndFolders();
|
await searchInput.checkFilesAndFolders();
|
||||||
await searchInput.searchFor('search-f');
|
await searchInput.searchFor(fileSearch2);
|
||||||
await dataTable.waitForBody();
|
await dataTable.waitForBody();
|
||||||
await dataTable.selectItem(fileSearch2, parentSearch);
|
await dataTable.selectItem(fileSearch2, parentSearch);
|
||||||
await toolbar.clickMoreActionsUploadNewVersion();
|
await toolbar.clickMoreActionsUploadNewVersion();
|
||||||
@ -772,7 +772,7 @@ describe('Upload new version', () => {
|
|||||||
it('file is not updated when clicking Cancel - [C307006]', async () => {
|
it('file is not updated when clicking Cancel - [C307006]', async () => {
|
||||||
await searchInput.clickSearchButton();
|
await searchInput.clickSearchButton();
|
||||||
await searchInput.checkFilesAndFolders();
|
await searchInput.checkFilesAndFolders();
|
||||||
await searchInput.searchFor('search-f');
|
await searchInput.searchFor(fileSearch3);
|
||||||
await dataTable.waitForBody();
|
await dataTable.waitForBody();
|
||||||
await dataTable.selectItem(fileSearch3, parentSearch);
|
await dataTable.selectItem(fileSearch3, parentSearch);
|
||||||
await toolbar.clickMoreActionsUploadNewVersion();
|
await toolbar.clickMoreActionsUploadNewVersion();
|
||||||
@ -792,7 +792,7 @@ describe('Upload new version', () => {
|
|||||||
it('upload new version fails when new file name already exists - [C307007]', async () => {
|
it('upload new version fails when new file name already exists - [C307007]', async () => {
|
||||||
await searchInput.clickSearchButton();
|
await searchInput.clickSearchButton();
|
||||||
await searchInput.checkFilesAndFolders();
|
await searchInput.checkFilesAndFolders();
|
||||||
await searchInput.searchFor('search-f');
|
await searchInput.searchFor(fileSearch4);
|
||||||
await dataTable.waitForBody();
|
await dataTable.waitForBody();
|
||||||
await dataTable.selectItem(fileSearch4, parentSearch);
|
await dataTable.selectItem(fileSearch4, parentSearch);
|
||||||
await toolbar.clickMoreActionsUploadNewVersion();
|
await toolbar.clickMoreActionsUploadNewVersion();
|
||||||
@ -814,7 +814,7 @@ describe('Upload new version', () => {
|
|||||||
it('file is unlocked after uploading a new version - [C307008]', async () => {
|
it('file is unlocked after uploading a new version - [C307008]', async () => {
|
||||||
await searchInput.clickSearchButton();
|
await searchInput.clickSearchButton();
|
||||||
await searchInput.checkFilesAndFolders();
|
await searchInput.checkFilesAndFolders();
|
||||||
await searchInput.searchFor('search-f');
|
await searchInput.searchFor(fileLockedSearch1);
|
||||||
await dataTable.waitForBody();
|
await dataTable.waitForBody();
|
||||||
await dataTable.selectItem(fileLockedSearch1, parentSearch);
|
await dataTable.selectItem(fileLockedSearch1, parentSearch);
|
||||||
await toolbar.clickMoreActionsUploadNewVersion();
|
await toolbar.clickMoreActionsUploadNewVersion();
|
||||||
@ -836,7 +836,7 @@ describe('Upload new version', () => {
|
|||||||
it('file remains locked after canceling of uploading a new version - [C307009]', async () => {
|
it('file remains locked after canceling of uploading a new version - [C307009]', async () => {
|
||||||
await searchInput.clickSearchButton();
|
await searchInput.clickSearchButton();
|
||||||
await searchInput.checkFilesAndFolders();
|
await searchInput.checkFilesAndFolders();
|
||||||
await searchInput.searchFor('search-f');
|
await searchInput.searchFor(fileLockedSearch2);
|
||||||
await dataTable.waitForBody();
|
await dataTable.waitForBody();
|
||||||
await dataTable.selectItem(fileLockedSearch2, parentSearch);
|
await dataTable.selectItem(fileLockedSearch2, parentSearch);
|
||||||
await toolbar.clickMoreActionsUploadNewVersion();
|
await toolbar.clickMoreActionsUploadNewVersion();
|
||||||
|
@ -1,79 +0,0 @@
|
|||||||
/*!
|
|
||||||
* @license
|
|
||||||
* Alfresco Example Content Application
|
|
||||||
*
|
|
||||||
* Copyright (C) 2005 - 2019 Alfresco Software Limited
|
|
||||||
*
|
|
||||||
* This file is part of the Alfresco Example Content Application.
|
|
||||||
* If the software was purchased under a paid Alfresco license, the terms of
|
|
||||||
* the paid license agreement will prevail. Otherwise, the software is
|
|
||||||
* provided under the following open source license terms:
|
|
||||||
*
|
|
||||||
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* The Alfresco Example Content Application 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 Lesser General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Lesser General Public License
|
|
||||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* tslint:disable */
|
|
||||||
const chalk = require('chalk');
|
|
||||||
/* tslint:enable */
|
|
||||||
|
|
||||||
export const log = {
|
|
||||||
i: 0,
|
|
||||||
|
|
||||||
get indentation(): string {
|
|
||||||
return new Array(this.i).fill(' ').join('');
|
|
||||||
},
|
|
||||||
|
|
||||||
indent() {
|
|
||||||
this.i++;
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
unindent() {
|
|
||||||
this.i--;
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
log(message: string = '', options: any = { ignoreIndentation: false }) {
|
|
||||||
const indentation = (!options.ignoreIndentation)
|
|
||||||
? this.indentation
|
|
||||||
: '';
|
|
||||||
|
|
||||||
console.log(`${indentation}${message}`);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
blank() {
|
|
||||||
return this.log();
|
|
||||||
},
|
|
||||||
|
|
||||||
info(message: string = '', options: any = { bold: false, title: false }) {
|
|
||||||
const { bold } = options;
|
|
||||||
const style = (bold ? chalk.bold : chalk).gray;
|
|
||||||
|
|
||||||
return this.log(style(message), options);
|
|
||||||
},
|
|
||||||
|
|
||||||
success(message: string = '', options: any = { bold: false }) {
|
|
||||||
const style = options.bold ? chalk.bold.green : chalk.green;
|
|
||||||
|
|
||||||
return this.log(style(message), options);
|
|
||||||
},
|
|
||||||
|
|
||||||
error(message: string = '', options: any = { bold: false }) {
|
|
||||||
const style = options.bold ? chalk.bold.red : chalk.red;
|
|
||||||
|
|
||||||
return this.log(style(message), options);
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,90 +0,0 @@
|
|||||||
/*!
|
|
||||||
* @license
|
|
||||||
* Alfresco Example Content Application
|
|
||||||
*
|
|
||||||
* Copyright (C) 2005 - 2019 Alfresco Software Limited
|
|
||||||
*
|
|
||||||
* This file is part of the Alfresco Example Content Application.
|
|
||||||
* If the software was purchased under a paid Alfresco license, the terms of
|
|
||||||
* the paid license agreement will prevail. Otherwise, the software is
|
|
||||||
* provided under the following open source license terms:
|
|
||||||
*
|
|
||||||
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* The Alfresco Example Content Application 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 Lesser General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Lesser General Public License
|
|
||||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { log } from './console-logger';
|
|
||||||
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
export const consoleReporter = {
|
|
||||||
jasmineStarted(suiteInfo) {
|
|
||||||
log.blank().info(
|
|
||||||
`Running ${suiteInfo.totalSpecsDefined} tests`,
|
|
||||||
{ bold: true, title: true }
|
|
||||||
).blank();
|
|
||||||
},
|
|
||||||
|
|
||||||
suiteStarted(suite) {
|
|
||||||
log.info(suite.description).indent();
|
|
||||||
},
|
|
||||||
|
|
||||||
specDone: (spec) => {
|
|
||||||
const {
|
|
||||||
status,
|
|
||||||
description,
|
|
||||||
failedExpectations
|
|
||||||
} = spec;
|
|
||||||
|
|
||||||
if (status === 'passed') {
|
|
||||||
log.success(`∙ ${description}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === 'failed') {
|
|
||||||
log.error(`✕ ${description}`, { bold: true });
|
|
||||||
|
|
||||||
errors.push(spec);
|
|
||||||
|
|
||||||
failedExpectations.forEach((failed) => {
|
|
||||||
log.error(` ${failed.message}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
suiteDone: (result) => {
|
|
||||||
log.unindent();
|
|
||||||
},
|
|
||||||
|
|
||||||
jasmineDone: (result) => {
|
|
||||||
if (!!errors.length) {
|
|
||||||
log .blank()
|
|
||||||
.blank()
|
|
||||||
.info(`${errors.length} failing tests`, { bold: true, title: true });
|
|
||||||
|
|
||||||
errors.forEach(error => {
|
|
||||||
log .blank()
|
|
||||||
.error(`✕ ${error.fullName}`, { bold: true });
|
|
||||||
|
|
||||||
error.failedExpectations.forEach(failed => {
|
|
||||||
log .info(`${failed.message}`)
|
|
||||||
.blank()
|
|
||||||
.error(`${failed.stack}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
log.success(`All tests passed!`, { bold: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
log.blank().blank();
|
|
||||||
}
|
|
||||||
};
|
|
@ -14,14 +14,24 @@
|
|||||||
<ng-container *ngIf="expandedTemplate">
|
<ng-container *ngIf="expandedTemplate">
|
||||||
<ng-template
|
<ng-template
|
||||||
[ngTemplateOutlet]="expandedTemplate"
|
[ngTemplateOutlet]="expandedTemplate"
|
||||||
[ngTemplateOutletContext]="{ $implicit: item }"
|
[ngTemplateOutletContext]="{
|
||||||
|
$implicit: item,
|
||||||
|
state: 'expanded'
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="!expandedTemplate">
|
<ng-container *ngIf="!item.component">
|
||||||
<app-expand-menu [item]="item"></app-expand-menu>
|
<app-expand-menu [item]="item"></app-expand-menu>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="item.component">
|
||||||
|
<adf-dynamic-component
|
||||||
|
[data]="{ item: item, state: 'expanded' }"
|
||||||
|
[id]="item.component"
|
||||||
|
></adf-dynamic-component>
|
||||||
|
</ng-container>
|
||||||
</mat-list-item>
|
</mat-list-item>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
@ -33,14 +43,25 @@
|
|||||||
<ng-container *ngIf="collapsedTemplate">
|
<ng-container *ngIf="collapsedTemplate">
|
||||||
<ng-template
|
<ng-template
|
||||||
[ngTemplateOutlet]="collapsedTemplate"
|
[ngTemplateOutlet]="collapsedTemplate"
|
||||||
[ngTemplateOutletContext]="{ $implicit: item }"
|
[ngTemplateOutletContext]="{
|
||||||
|
$implicit: item,
|
||||||
|
state: 'collapsed'
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="!collapsedTemplate">
|
<ng-container *ngIf="!item.component">
|
||||||
<app-button-menu [item]="item"></app-button-menu>
|
<app-button-menu [item]="item"></app-button-menu>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="item.component">
|
||||||
|
<adf-dynamic-component
|
||||||
|
[data]="{ item: item, state: 'collapsed' }"
|
||||||
|
[id]="item.component"
|
||||||
|
>
|
||||||
|
</adf-dynamic-component>
|
||||||
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
@ -38,9 +38,9 @@ import { AppExtensionService } from '../../extensions/extension.service';
|
|||||||
import { NavBarGroupRef } from '@alfresco/adf-extensions';
|
import { NavBarGroupRef } from '@alfresco/adf-extensions';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { AppStore } from '../../store/states';
|
import { AppStore } from '../../store/states';
|
||||||
import { ruleContext } from '../../store/selectors/app.selectors';
|
import { sidenavState } from '../../store/selectors/app.selectors';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil, distinctUntilChanged, map } from 'rxjs/operators';
|
import { takeUntil, distinctUntilChanged, debounceTime } from 'rxjs/operators';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-sidenav',
|
selector: 'app-sidenav',
|
||||||
@ -68,9 +68,9 @@ export class SidenavComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.store
|
this.store
|
||||||
.select(ruleContext)
|
.select(sidenavState)
|
||||||
.pipe(
|
.pipe(
|
||||||
map(rules => rules.repository),
|
debounceTime(300),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
takeUntil(this.onDestroy$)
|
takeUntil(this.onDestroy$)
|
||||||
)
|
)
|
||||||
|
@ -28,6 +28,8 @@ import { AppCreateMenuModule } from '../create-menu/create-menu.module';
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CoreModule } from '@alfresco/adf-core';
|
import { CoreModule } from '@alfresco/adf-core';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { ExtensionsModule } from '@alfresco/adf-extensions';
|
||||||
|
import { CoreExtensionsModule } from '../../extensions/core.extensions.module';
|
||||||
import { ExpansionPanelDirective } from './directives/expansion-panel.directive';
|
import { ExpansionPanelDirective } from './directives/expansion-panel.directive';
|
||||||
import { MenuPanelDirective } from './directives/menu-panel.directive';
|
import { MenuPanelDirective } from './directives/menu-panel.directive';
|
||||||
import { CollapsedTemplateDirective } from './directives/collapsed-template.directive';
|
import { CollapsedTemplateDirective } from './directives/collapsed-template.directive';
|
||||||
@ -41,6 +43,8 @@ import { ActionDirective } from './directives/action.directive';
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
CoreModule.forChild(),
|
CoreModule.forChild(),
|
||||||
|
CoreExtensionsModule.forChild(),
|
||||||
|
ExtensionsModule.forChild(),
|
||||||
RouterModule,
|
RouterModule,
|
||||||
AppCreateMenuModule
|
AppCreateMenuModule
|
||||||
],
|
],
|
||||||
|
@ -244,6 +244,12 @@ export class AppExtensionService implements RuleContext {
|
|||||||
.filter(child => this.filterVisible(child))
|
.filter(child => this.filterVisible(child))
|
||||||
.sort(sortByOrder)
|
.sort(sortByOrder)
|
||||||
.map(child => {
|
.map(child => {
|
||||||
|
if (child.component) {
|
||||||
|
return {
|
||||||
|
...child
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!child.click) {
|
if (!child.click) {
|
||||||
const childRouteRef = this.extensions.getRouteById(
|
const childRouteRef = this.extensions.getRouteById(
|
||||||
child.route
|
child.route
|
||||||
@ -268,6 +274,10 @@ export class AppExtensionService implements RuleContext {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item.component) {
|
||||||
|
return { ...item };
|
||||||
|
}
|
||||||
|
|
||||||
if (!item.click) {
|
if (!item.click) {
|
||||||
const routeRef = this.extensions.getRouteById(item.route);
|
const routeRef = this.extensions.getRouteById(item.route);
|
||||||
const url = `/${routeRef ? routeRef.path : item.route}`;
|
const url = `/${routeRef ? routeRef.path : item.route}`;
|
||||||
|
@ -103,6 +103,17 @@ export const isAdmin = createSelector(
|
|||||||
state => state.user.isAdmin
|
state => state.user.isAdmin
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const sidenavState = createSelector(
|
||||||
|
appSelection,
|
||||||
|
appNavigation,
|
||||||
|
(selection, navigation) => {
|
||||||
|
return {
|
||||||
|
selection,
|
||||||
|
navigation
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const ruleContext = createSelector(
|
export const ruleContext = createSelector(
|
||||||
appSelection,
|
appSelection,
|
||||||
appNavigation,
|
appNavigation,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user