[ADF-1599] [Destination Picker] Updates to match the designed component (#2642)

* [ADF-1599] fix design issues

Search Term Highlight text to be orange (primary color)
Main action button text label to be orange (primary color)
Main action button text to be in Uppercase
Main action button to relate to the action i.e. MOVE or COPY
Dialog title: Name of item to move/copy should be in 'quotes'

* [ADF-1599] fix Dropdown	Placeholder text from 'Site List' to 'Select Location'

* [ADF-1599] fix Dropdown	Placeholder text from 'Site List' to 'Select Location'

* fix 'Select Location' width and bottom margin

* [ADF-1599] update document picker to match design

* [ADF-1599] fix failing tests

* [ADF-1599] update the unit tests

* [ADF-1599] use only colors from $theme on component scss file

* [ADF-1599] change needed after resolving conflict on merge
This commit is contained in:
suzanadirla
2017-11-22 15:40:47 +02:00
committed by Eugenio Romano
parent ed6aa1a0c1
commit 68239cd002
21 changed files with 148 additions and 34 deletions

View File

@@ -30,6 +30,7 @@ Displays a dropdown menu to show and interact with the sites of the current user
| --- | --- | --- | --- | | --- | --- | --- | --- |
| hideMyFiles | boolean | false | Hide the "My Files" option added to the list by default | | hideMyFiles | boolean | false | Hide the "My Files" option added to the list by default |
| siteList | any[] | null | A custom list of sites to be displayed by the dropdown. If no value is given, the sites of the current user are displayed by default. A list of objects only with properties 'title' and 'guid' is enough to be able to display the dropdown. | | siteList | any[] | null | A custom list of sites to be displayed by the dropdown. If no value is given, the sites of the current user are displayed by default. A list of objects only with properties 'title' and 'guid' is enough to be able to display the dropdown. |
| placeholder | string | 'DROPDOWN.PLACEHOLDER_LABEL' | The placeholder text/the key from translation files for the placeholder text to be shown by default|
### Events ### Events

View File

@@ -20,6 +20,7 @@ import { MinimalNodeEntryEntity } from 'alfresco-js-api';
export interface ContentNodeSelectorComponentData { export interface ContentNodeSelectorComponentData {
title: string; title: string;
actionName?: string;
currentFolderId?: string; currentFolderId?: string;
dropdownHideMyFiles?: boolean; dropdownHideMyFiles?: boolean;
dropdownSiteList?: any[]; dropdownSiteList?: any[];

View File

@@ -28,6 +28,7 @@
<adf-sites-dropdown <adf-sites-dropdown
(change)="siteChanged($event)" (change)="siteChanged($event)"
[placeholder]="'NODE_SELECTOR.SELECT_LOCATION'"
[hideMyFiles]="dropdownHideMyFiles" [hideMyFiles]="dropdownHideMyFiles"
[siteList]="dropdownSiteList" [siteList]="dropdownSiteList"
data-automation-id="content-node-selector-sites-combo"></adf-sites-dropdown> data-automation-id="content-node-selector-sites-combo"></adf-sites-dropdown>
@@ -96,7 +97,7 @@
[disabled]="!chosenNode" [disabled]="!chosenNode"
class="adf-content-node-selector-actions-choose" class="adf-content-node-selector-actions-choose"
(click)="choose()" (click)="choose()"
data-automation-id="content-node-selector-actions-choose">{{ 'NODE_SELECTOR.CHOOSE' | translate }} data-automation-id="content-node-selector-actions-choose">{{ buttonActionName | translate }}
</button> </button>
</footer> </footer>

View File

@@ -1,6 +1,7 @@
@mixin adf-content-node-selector-theme($theme) { @mixin adf-content-node-selector-theme($theme) {
$primary: map-get($theme, primary); $primary: map-get($theme, primary);
$accent: map-get($theme, accent); $foreground: map-get($theme, foreground);
$background: map-get($theme, background);
.adf-content-node-selector-dialog { .adf-content-node-selector-dialog {
@@ -27,11 +28,11 @@
width: 100%; width: 100%;
&-icon { &-icon {
color: rgba(0, 0, 0, 0.38); color: mat-color($foreground, disabled-button);
cursor: pointer; cursor: pointer;
&:hover { &:hover {
color: rgba(0, 0, 0, 1); color: mat-color($foreground, base);
} }
} }
} }
@@ -41,9 +42,16 @@
transition: none; transition: none;
} }
.adf-site-dropdown-container {
.mat-form-field {
display: block;
margin-bottom: 15px;
}
}
.adf-site-dropdown-list-element { .adf-site-dropdown-list-element {
width: 100%; width: 100%;
margin-bottom: 20px; margin-bottom: 0;
.mat-select-trigger { .mat-select-trigger {
font-size: 14px; font-size: 14px;
@@ -52,16 +60,17 @@
} }
.adf-toolbar .mat-toolbar { .adf-toolbar .mat-toolbar {
border: none; border-bottom-width: 0;
font-size: 14px;
} }
&-list { &-list {
height: 200px; height: 200px;
overflow: auto; overflow: auto;
border: 1px solid rgba(0, 0, 0, 0.07); border: 1px solid mat-color($foreground, base, 0.07);
.adf-highlight { .adf-highlight {
color: mat-color($accent);; color: mat-color($primary);
} }
.adf-data-table { .adf-data-table {
@@ -97,10 +106,14 @@
&-actions { &-actions {
padding: 8px; padding: 8px;
background-color: rgb(250, 250, 250); background-color: mat-color($background, background);
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
color: rgb(121, 121, 121); color: mat-color($foreground, secondary-text);
button {
text-transform: uppercase;
}
&:last-child { &:last-child {
margin-bottom: 0px; margin-bottom: 0px;
@@ -116,6 +129,10 @@
&[disabled] { &[disabled] {
opacity: 0.6; opacity: 0.6;
} }
&:enabled {
color: mat-color($primary);
}
} }
} }
} }

View File

@@ -107,6 +107,7 @@ describe('ContentNodeSelectorComponent', () => {
beforeEach(async(() => { beforeEach(async(() => {
data = { data = {
title: 'Move along citizen...', title: 'Move along citizen...',
actionName: 'move',
select: new EventEmitter<MinimalNodeEntryEntity>(), select: new EventEmitter<MinimalNodeEntryEntity>(),
rowFilter: () => {}, rowFilter: () => {},
imageResolver: () => 'piccolo', imageResolver: () => 'piccolo',
@@ -131,6 +132,12 @@ describe('ContentNodeSelectorComponent', () => {
expect(titleElement.nativeElement.innerText).toBe('Move along citizen...'); expect(titleElement.nativeElement.innerText).toBe('Move along citizen...');
}); });
it('should have the INJECTED actionName on the name of the choose button', () => {
const actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(actionButton).not.toBeNull();
expect(actionButton.nativeElement.innerText).toBe('NODE_SELECTOR.MOVE');
});
it('should pass through the injected currentFolderId to the documentlist', () => { it('should pass through the injected currentFolderId to the documentlist', () => {
let documentList = fixture.debugElement.query(By.directive(DocumentListComponent)); let documentList = fixture.debugElement.query(By.directive(DocumentListComponent));
expect(documentList).not.toBeNull('Document list should be shown'); expect(documentList).not.toBeNull('Document list should be shown');
@@ -595,7 +602,7 @@ describe('ContentNodeSelectorComponent', () => {
}); });
}); });
describe('Choose button', () => { describe('Action button for the chosen node', () => {
const entry: MinimalNodeEntryEntity = <MinimalNodeEntryEntity> {}; const entry: MinimalNodeEntryEntity = <MinimalNodeEntryEntity> {};
let hasPermission; let hasPermission;
@@ -608,8 +615,8 @@ describe('ContentNodeSelectorComponent', () => {
it('should be disabled by default', () => { it('should be disabled by default', () => {
fixture.detectChanges(); fixture.detectChanges();
let chooseButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]')); let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(chooseButton.nativeElement.disabled).toBe(true); expect(actionButton.nativeElement.disabled).toBe(true);
}); });
it('should become enabled after loading node with the necessary permissions', () => { it('should become enabled after loading node with the necessary permissions', () => {
@@ -618,8 +625,8 @@ describe('ContentNodeSelectorComponent', () => {
component.documentList.ready.emit(); component.documentList.ready.emit();
fixture.detectChanges(); fixture.detectChanges();
let chooseButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]')); let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(chooseButton.nativeElement.disabled).toBe(false); expect(actionButton.nativeElement.disabled).toBe(false);
}); });
it('should remain disabled after loading node without the necessary permissions', () => { it('should remain disabled after loading node without the necessary permissions', () => {
@@ -628,8 +635,8 @@ describe('ContentNodeSelectorComponent', () => {
component.documentList.ready.emit(); component.documentList.ready.emit();
fixture.detectChanges(); fixture.detectChanges();
let chooseButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]')); let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(chooseButton.nativeElement.disabled).toBe(true); expect(actionButton.nativeElement.disabled).toBe(true);
}); });
it('should be enabled when clicking on a node (with the right permissions) in the list (onNodeSelect)', () => { it('should be enabled when clicking on a node (with the right permissions) in the list (onNodeSelect)', () => {
@@ -638,8 +645,8 @@ describe('ContentNodeSelectorComponent', () => {
component.onNodeSelect({ detail: { node: { entry } } }); component.onNodeSelect({ detail: { node: { entry } } });
fixture.detectChanges(); fixture.detectChanges();
let chooseButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]')); let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(chooseButton.nativeElement.disabled).toBe(false); expect(actionButton.nativeElement.disabled).toBe(false);
}); });
it('should remain disabled when clicking on a node (with the WRONG permissions) in the list (onNodeSelect)', () => { it('should remain disabled when clicking on a node (with the WRONG permissions) in the list (onNodeSelect)', () => {
@@ -648,8 +655,8 @@ describe('ContentNodeSelectorComponent', () => {
component.onNodeSelect({ detail: { node: { entry } } }); component.onNodeSelect({ detail: { node: { entry } } });
fixture.detectChanges(); fixture.detectChanges();
let chooseButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]')); let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(chooseButton.nativeElement.disabled).toBe(true); expect(actionButton.nativeElement.disabled).toBe(true);
}); });
it('should become disabled when clicking on a node (with the WRONG permissions) after previously selecting a right node', () => { it('should become disabled when clicking on a node (with the WRONG permissions) after previously selecting a right node', () => {
@@ -661,8 +668,8 @@ describe('ContentNodeSelectorComponent', () => {
component.onNodeSelect({ detail: { node: { entry } } }); component.onNodeSelect({ detail: { node: { entry } } });
fixture.detectChanges(); fixture.detectChanges();
let chooseButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]')); let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(chooseButton.nativeElement.disabled).toBe(true); expect(actionButton.nativeElement.disabled).toBe(true);
}); });
it('should be disabled when resetting the chosen node', () => { it('should be disabled when resetting the chosen node', () => {
@@ -673,8 +680,8 @@ describe('ContentNodeSelectorComponent', () => {
component.resetChosenNode(); component.resetChosenNode();
fixture.detectChanges(); fixture.detectChanges();
let chooseButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]')); let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(chooseButton.nativeElement.disabled).toBe(true); expect(actionButton.nativeElement.disabled).toBe(true);
}); });
it('should make the call to get the corresponding node entry to emit when a site node is selected as destination', () => { it('should make the call to get the corresponding node entry to emit when a site node is selected as destination', () => {

View File

@@ -46,10 +46,14 @@ export class ContentNodeSelectorComponent implements OnInit {
pagination: Pagination; pagination: Pagination;
skipCount: number = 0; skipCount: number = 0;
infiniteScroll: boolean = false; infiniteScroll: boolean = false;
buttonActionName: string;
@Input() @Input()
title: string; title: string;
@Input()
actionName: string;
@Input() @Input()
currentFolderId: string | null = null; currentFolderId: string | null = null;
@@ -85,6 +89,7 @@ export class ContentNodeSelectorComponent implements OnInit {
@Optional() private containingDialog?: MatDialogRef<ContentNodeSelectorComponent>) { @Optional() private containingDialog?: MatDialogRef<ContentNodeSelectorComponent>) {
if (data) { if (data) {
this.title = data.title; this.title = data.title;
this.actionName = data.actionName;
this.select = data.select; this.select = data.select;
this.currentFolderId = data.currentFolderId; this.currentFolderId = data.currentFolderId;
this.dropdownHideMyFiles = data.dropdownHideMyFiles; this.dropdownHideMyFiles = data.dropdownHideMyFiles;
@@ -92,6 +97,7 @@ export class ContentNodeSelectorComponent implements OnInit {
this.rowFilter = data.rowFilter; this.rowFilter = data.rowFilter;
this.imageResolver = data.imageResolver; this.imageResolver = data.imageResolver;
} }
this.buttonActionName = this.actionName ? `NODE_SELECTOR.${this.actionName.toUpperCase()}` : 'NODE_SELECTOR.CHOOSE';
if (this.containingDialog) { if (this.containingDialog) {
this.inDialog = true; this.inDialog = true;

View File

@@ -86,7 +86,8 @@ export class NodeActionsService {
if (this.contentService.hasPermission(contentEntry, permission)) { if (this.contentService.hasPermission(contentEntry, permission)) {
const data: ContentNodeSelectorComponentData = { const data: ContentNodeSelectorComponentData = {
title: `${action} ${contentEntry.name} to ...`, title: `${action} '${contentEntry.name}' to ...`,
actionName: action,
currentFolderId: contentEntry.parentId, currentFolderId: contentEntry.parentId,
rowFilter: this.rowFilter.bind(this, contentEntry.id), rowFilter: this.rowFilter.bind(this, contentEntry.id),
imageResolver: this.imageResolver.bind(this), imageResolver: this.imageResolver.bind(this),

View File

@@ -60,6 +60,8 @@
"NODE_SELECTOR": { "NODE_SELECTOR": {
"CANCEL": "Abbrechen", "CANCEL": "Abbrechen",
"CHOOSE": "Auswählen", "CHOOSE": "Auswählen",
"COPY": "Kopieren",
"MOVE": "Verschieben",
"NO_RESULTS": "Keine Ergebnisse gefunden" "NO_RESULTS": "Keine Ergebnisse gefunden"
}, },
"OPERATION": { "OPERATION": {

View File

@@ -60,7 +60,10 @@
"NODE_SELECTOR": { "NODE_SELECTOR": {
"CANCEL": "Cancel", "CANCEL": "Cancel",
"CHOOSE": "Choose", "CHOOSE": "Choose",
"NO_RESULTS": "No results found" "COPY": "Copy",
"MOVE": "Move",
"NO_RESULTS": "No results found",
"SELECT_LOCATION": "Select Location"
}, },
"OPERATION": { "OPERATION": {
"SUCCES": { "SUCCES": {

View File

@@ -60,6 +60,8 @@
"NODE_SELECTOR": { "NODE_SELECTOR": {
"CANCEL": "Cancelar", "CANCEL": "Cancelar",
"CHOOSE": "Elegir", "CHOOSE": "Elegir",
"COPY": "Copiar",
"MOVE": "Mover",
"NO_RESULTS": "Ningún resultado encontrado" "NO_RESULTS": "Ningún resultado encontrado"
}, },
"OPERATION": { "OPERATION": {

View File

@@ -60,6 +60,8 @@
"NODE_SELECTOR": { "NODE_SELECTOR": {
"CANCEL": "Annuler", "CANCEL": "Annuler",
"CHOOSE": "Choisir", "CHOOSE": "Choisir",
"COPY": "Copier",
"MOVE": "Déplacer",
"NO_RESULTS": "Aucun résultat trouvé" "NO_RESULTS": "Aucun résultat trouvé"
}, },
"OPERATION": { "OPERATION": {

View File

@@ -60,6 +60,8 @@
"NODE_SELECTOR": { "NODE_SELECTOR": {
"CANCEL": "Annulla", "CANCEL": "Annulla",
"CHOOSE": "Scegli", "CHOOSE": "Scegli",
"COPY": "Copia",
"MOVE": "Sposta",
"NO_RESULTS": "Nessun risultato trovato" "NO_RESULTS": "Nessun risultato trovato"
}, },
"OPERATION": { "OPERATION": {

View File

@@ -60,6 +60,8 @@
"NODE_SELECTOR": { "NODE_SELECTOR": {
"CANCEL": "キャンセル", "CANCEL": "キャンセル",
"CHOOSE": "選択", "CHOOSE": "選択",
"COPY": "コピー",
"MOVE": "移動",
"NO_RESULTS": "一致するアイテムはありません" "NO_RESULTS": "一致するアイテムはありません"
}, },
"OPERATION": { "OPERATION": {

View File

@@ -60,6 +60,8 @@
"NODE_SELECTOR": { "NODE_SELECTOR": {
"CANCEL": "Avbryt", "CANCEL": "Avbryt",
"CHOOSE": "Velg", "CHOOSE": "Velg",
"COPY": "Kopier",
"MOVE": "Flytt",
"NO_RESULTS": "Ingen resultater funnet" "NO_RESULTS": "Ingen resultater funnet"
}, },
"OPERATION": { "OPERATION": {

View File

@@ -60,6 +60,8 @@
"NODE_SELECTOR": { "NODE_SELECTOR": {
"CANCEL": "Annuleren", "CANCEL": "Annuleren",
"CHOOSE": "Kiezen", "CHOOSE": "Kiezen",
"COPY": "Kopiëren",
"MOVE": "Verplaatsen",
"NO_RESULTS": "Geen resultaten gevonden" "NO_RESULTS": "Geen resultaten gevonden"
}, },
"OPERATION": { "OPERATION": {

View File

@@ -60,6 +60,8 @@
"NODE_SELECTOR": { "NODE_SELECTOR": {
"CANCEL": "Cancelar", "CANCEL": "Cancelar",
"CHOOSE": "Escolher", "CHOOSE": "Escolher",
"COPY": "Copiar",
"MOVE": "Mover",
"NO_RESULTS": "Nenhum resultado encontrado" "NO_RESULTS": "Nenhum resultado encontrado"
}, },
"OPERATION": { "OPERATION": {

View File

@@ -60,6 +60,8 @@
"NODE_SELECTOR": { "NODE_SELECTOR": {
"CANCEL": "Отмена", "CANCEL": "Отмена",
"CHOOSE": "Выбрать", "CHOOSE": "Выбрать",
"COPY": "Копировать",
"MOVE": "Переместить",
"NO_RESULTS": "Результаты не найдены" "NO_RESULTS": "Результаты не найдены"
}, },
"OPERATION": { "OPERATION": {

View File

@@ -60,6 +60,8 @@
"NODE_SELECTOR": { "NODE_SELECTOR": {
"CANCEL": "取消", "CANCEL": "取消",
"CHOOSE": "选择", "CHOOSE": "选择",
"COPY": "复制",
"MOVE": "移动",
"NO_RESULTS": "未找到结果" "NO_RESULTS": "未找到结果"
}, },
"OPERATION": { "OPERATION": {

View File

@@ -3,7 +3,7 @@
<mat-select <mat-select
class="adf-site-dropdown-list-element" class="adf-site-dropdown-list-element"
id="site-dropdown" id="site-dropdown"
placeholder="{{'DROPDOWN.PLACEHOLDER_LABEL' | translate}}" placeholder="{{placeholder | translate}}"
floatPlaceholder="never" floatPlaceholder="never"
data-automation-id="site-my-files-select" data-automation-id="site-my-files-select"
[(ngModel)]="siteSelected" [(ngModel)]="siteSelected"

View File

@@ -102,7 +102,7 @@ describe('DropdownSitesComponent', () => {
TestBed.resetTestingModule(); TestBed.resetTestingModule();
}); });
it('Dropdown sites should be renedered', async(() => { it('Dropdown sites should be rendered', async(() => {
fixture.detectChanges(); fixture.detectChanges();
fixture.whenStable().then(() => { fixture.whenStable().then(() => {
fixture.detectChanges(); fixture.detectChanges();
@@ -145,24 +145,78 @@ describe('DropdownSitesComponent', () => {
}); });
})); }));
// todo: something wrong with the test itself it('should show the default placeholder label by default', async(() => {
xit('should load sites on init', async(() => { fixture.detectChanges();
jasmine.Ajax.requests.mostRecent().respondWith({ status: 200, contentType: 'json', responseText: sitesList });
openSelectbox();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.nativeElement.innerText.trim()).toBe('DROPDOWN.PLACEHOLDER_LABEL');
});
}));
it('should show custom placeholder label when the \'placeholder\' input property is given a value', async(() => {
component.placeholder = 'NODE_SELECTOR.SELECT_LOCATION';
fixture.detectChanges();
jasmine.Ajax.requests.mostRecent().respondWith({ status: 200, contentType: 'json', responseText: sitesList });
openSelectbox();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.nativeElement.innerText.trim()).toBe('NODE_SELECTOR.SELECT_LOCATION');
});
}));
it('should load custom sites when the \'siteList\' input property is given a value', async(() => {
component.siteList = [{title: 'PERSONAL_FILES', guid: '-my-'}, {title: 'FILE_LIBRARIES', guid: '-mysites-'}];
fixture.detectChanges();
openSelectbox();
let options: any = [];
fixture.whenStable().then(() => {
fixture.detectChanges();
options = debug.queryAll(By.css('mat-option'));
options[0].triggerEventHandler('click', null);
fixture.detectChanges();
});
component.change.subscribe(() => {
expect(options[0].attributes['ng-reflect-value']).toBe('default');
expect(options[1].attributes['ng-reflect-value']).toBe('-my-');
expect(options[2].attributes['ng-reflect-value']).toBe('-mysites-');
});
}));
it('should load sites by default', (done) => {
fixture.detectChanges(); fixture.detectChanges();
jasmine.Ajax.requests.mostRecent().respondWith({ jasmine.Ajax.requests.mostRecent().respondWith({
status: 200, status: 200,
contentType: 'json', contentType: 'json',
responseText: sitesList responseText: sitesList
}); });
openSelectbox();
let options: any = [];
fixture.whenStable().then(() => { fixture.whenStable().then(() => {
fixture.detectChanges(); fixture.detectChanges();
debug.query(By.css('.mat-select-trigger')).triggerEventHandler('click', null); options = debug.queryAll(By.css('mat-option'));
options[0].triggerEventHandler('click', null);
fixture.detectChanges(); fixture.detectChanges();
let options: any = debug.queryAll(By.css('mat-option')); });
component.change.subscribe(() => {
expect(options[0].attributes['ng-reflect-value']).toBe('default'); expect(options[0].attributes['ng-reflect-value']).toBe('default');
expect(options[1].attributes['ng-reflect-value']).toBe('fake-1'); expect(options[1].attributes['ng-reflect-value']).toBe('fake-1');
expect(options[2].attributes['ng-reflect-value']).toBe('fake-2'); expect(options[2].attributes['ng-reflect-value']).toBe('fake-2');
done();
});
}); });
}));
it('should raise an event when a site is selected', (done) => { it('should raise an event when a site is selected', (done) => {
fixture.detectChanges(); fixture.detectChanges();

View File

@@ -31,6 +31,9 @@ export class DropdownSitesComponent implements OnInit {
@Input() @Input()
siteList: any[] = null; siteList: any[] = null;
@Input()
placeholder: string = 'DROPDOWN.PLACEHOLDER_LABEL';
@Output() @Output()
change: EventEmitter<SiteModel> = new EventEmitter(); change: EventEmitter<SiteModel> = new EventEmitter();