[ACA-19] Toggle join request - library action (#800)

* [ACA-19] join/cancel join library actions

* [ACA-19] show the info drawer

* [ACA-19] custom icon for join library

* [ACA-19] css to 'see' custom icon for extension

* [ACA-19] reformat with prettier

* [ACA-19] better role display

* [ACA-19] simplify cancel request rule

* [ACA-19] refactor and use toggle join/cancel join component & directive

* [ACA-19] reformat with Prettier

* [ACA-19] fix title for svgIcon

* [ACA-19] fix translation

* [ACA-19] unit test
This commit is contained in:
Suzana Dirla 2018-11-14 14:43:14 +02:00 committed by Denys Vuika
parent 7734844893
commit 49e80ddce1
17 changed files with 553 additions and 13 deletions

View File

@ -70,6 +70,8 @@ import { AppSearchResultsModule } from './components/search/search-results.modul
import { AppLoginModule } from './components/login/login.module';
import { AppHeaderModule } from './components/header/header.module';
import { environment } from '../environments/environment';
import { LibraryMembershipDirective } from './directives/library-membership.directive';
import { ToggleJoinLibraryComponent } from './components/toolbar/toggle-join-library/toggle-join-library.component';
@NgModule({
imports: [
@ -110,7 +112,9 @@ import { environment } from '../environments/environment';
LibrariesComponent,
FavoriteLibrariesComponent,
NodeVersionsDialogComponent,
LibraryDialogComponent
LibraryDialogComponent,
LibraryMembershipDirective,
ToggleJoinLibraryComponent
],
providers: [
{ provide: RouteReuseStrategy, useClass: AppRouteReuseStrategy },
@ -124,7 +128,11 @@ import { environment } from '../environments/environment';
}
}
],
entryComponents: [LibraryDialogComponent, NodeVersionsDialogComponent],
entryComponents: [
LibraryDialogComponent,
NodeVersionsDialogComponent,
ToggleJoinLibraryComponent
],
bootstrap: [AppComponent]
})
export class AppModule {}

View File

@ -44,6 +44,8 @@ import { currentFolder } from '../../../store/selectors/app.selectors';
import { AppStore } from '../../../store/states';
import { BreakpointObserver } from '@angular/cdk/layout';
import { SetSelectedNodesAction } from '../../../store/actions';
import { MatIconRegistry } from '@angular/material';
import { DomSanitizer } from '@angular/platform-browser';
@Component({
selector: 'app-layout',
@ -68,14 +70,25 @@ export class AppLayoutComponent implements OnInit, OnDestroy {
private minimizeConditions: string[] = ['search'];
private hideConditions: string[] = ['preview'];
customIcon: any = {
join_library: './assets/images/join-library.svg'
};
constructor(
protected store: Store<AppStore>,
private permission: NodePermissionService,
private router: Router,
private userPreferenceService: UserPreferencesService,
private appConfigService: AppConfigService,
private breakpointObserver: BreakpointObserver
) {}
private breakpointObserver: BreakpointObserver,
matIconRegistry: MatIconRegistry,
sanitizer: DomSanitizer
) {
matIconRegistry.addSvgIcon(
'join_library',
sanitizer.bypassSecurityTrustResourceUrl(this.customIcon['join_library'])
);
}
ngOnInit() {
this.isSmallScreen$ = this.breakpointObserver

View File

@ -36,6 +36,7 @@ import { PageLayoutComponent } from './page-layout/page-layout.component';
import { PageLayoutHeaderComponent } from './page-layout/page-layout-header.component';
import { PageLayoutContentComponent } from './page-layout/page-layout-content.component';
import { PageLayoutErrorComponent } from './page-layout/page-layout-error.component';
import { HttpClientModule } from '@angular/common/http';
@NgModule({
imports: [
@ -45,7 +46,8 @@ import { PageLayoutErrorComponent } from './page-layout/page-layout-error.compon
ContentModule.forChild(),
AppCommonModule,
AppSidenavModule,
AppHeaderModule
AppHeaderModule,
HttpClientModule
],
declarations: [
AppLayoutComponent,

View File

@ -73,6 +73,13 @@ export class SearchLibrariesResultsComponent extends PageComponent
this.columns = this.extensions.documentListPresets.searchLibraries || [];
this.subscriptions.push(
this.content.libraryJoined.subscribe(() =>
this.librariesQueryBuilder.update()
),
this.content.libraryDeleted.subscribe(() =>
this.librariesQueryBuilder.update()
),
this.librariesQueryBuilder.updated.subscribe(() => {
this.isLoading = true;

View File

@ -0,0 +1,131 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2018 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 { ToggleJoinLibraryComponent } from './toggle-join-library.component';
import { of } from 'rxjs';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AlfrescoApiService, AlfrescoApiServiceMock } from '@alfresco/adf-core';
import { LibraryMembershipDirective } from '../../../directives/library-membership.directive';
import { Store } from '@ngrx/store';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import {
SnackbarErrorAction,
SnackbarInfoAction
} from '../../../store/actions/snackbar.actions';
import { AppTestingModule } from '../../../testing/app-testing.module';
import { ContentManagementService } from '../../../services/content-management.service';
describe('ToggleJoinLibraryComponent', () => {
let component: ToggleJoinLibraryComponent;
let fixture: ComponentFixture<ToggleJoinLibraryComponent>;
let alfrescoApi: AlfrescoApiService;
let contentManagementService: ContentManagementService;
let entry;
const storeMock = {
select: () => of({ library: { entry, isLibrary: true } }),
dispatch: jasmine.createSpy('dispatch')
};
beforeEach(() => {
entry = {
id: 'lib-id',
joinRequested: true,
title: 'test',
visibility: 'MODERATED'
};
TestBed.configureTestingModule({
imports: [AppTestingModule],
declarations: [ToggleJoinLibraryComponent, LibraryMembershipDirective],
providers: [
{ provide: Store, useValue: storeMock },
{ provide: AlfrescoApiService, useClass: AlfrescoApiServiceMock }
],
schemas: [NO_ERRORS_SCHEMA]
});
fixture = TestBed.createComponent(ToggleJoinLibraryComponent);
component = fixture.componentInstance;
alfrescoApi = TestBed.get(AlfrescoApiService);
contentManagementService = TestBed.get(ContentManagementService);
spyOn(alfrescoApi.peopleApi, 'getSiteMembershipRequest').and.stub();
});
afterEach(() => {
fixture.destroy();
storeMock.dispatch.calls.reset();
});
it('should get Store selection entry on initialization', done => {
component.selection$.subscribe(selection => {
expect(selection.library.entry).toEqual(entry);
done();
});
});
it('should dispatch `SnackbarErrorAction` action on error', () => {
const event = { event: {}, i18nKey: 'ERROR_i18nKey' };
component.onErrorEvent(event);
expect(storeMock.dispatch).toHaveBeenCalledWith(
new SnackbarErrorAction(event.i18nKey)
);
});
it('should dispatch `SnackbarInfoAction` action on onToggleEvent', () => {
const event = { shouldReload: true, i18nKey: 'SOME_i18nKey' };
component.onToggleEvent(event);
expect(storeMock.dispatch).toHaveBeenCalledWith(
new SnackbarInfoAction(event.i18nKey)
);
});
it('should call libraryJoined.next on contentManagementService onToggleEvent', done => {
spyOn(contentManagementService.libraryJoined, 'next').and.callThrough();
contentManagementService.libraryJoined.subscribe(() => {
expect(contentManagementService.libraryJoined.next).toHaveBeenCalled();
done();
});
const event = { shouldReload: true };
component.onToggleEvent(event);
});
it('should call joinLibraryToggle.next on contentManagementService onToggleEvent', done => {
spyOn(contentManagementService.joinLibraryToggle, 'next').and.callThrough();
contentManagementService.joinLibraryToggle.subscribe(() => {
expect(
contentManagementService.joinLibraryToggle.next
).toHaveBeenCalled();
done();
});
const event = { shouldReload: false };
component.onToggleEvent(event);
});
});

View File

@ -0,0 +1,88 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2018 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 { Component, ViewEncapsulation } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppStore } from '../../../store/states';
import { appSelection } from '../../../store/selectors/app.selectors';
import { Observable } from 'rxjs';
import { SelectionState } from '@alfresco/adf-extensions';
import { ContentManagementService } from '../../../services/content-management.service';
import {
SnackbarErrorAction,
SnackbarInfoAction
} from '../../../store/actions/snackbar.actions';
@Component({
selector: 'app-toggle-join-library',
template: `
<button
mat-icon-button
[color]="'primary'"
#membership="libraryMembership"
(toggle)="onToggleEvent($event)"
(error)="onErrorEvent($event)"
[acaLibraryMembership]="(selection$ | async).library"
[attr.title]="
(membership.isJoinRequested | async)
? ('APP.ACTIONS.CANCEL_JOIN' | translate)
: ('APP.ACTIONS.JOIN' | translate)
"
>
<mat-icon *ngIf="(membership.isJoinRequested | async)">cancel</mat-icon>
<mat-icon
*ngIf="!(membership.isJoinRequested | async)"
svgIcon="join_library"
style="pointer-events: none;"
></mat-icon>
</button>
`,
encapsulation: ViewEncapsulation.None,
host: { class: 'app-toggle-join-library' }
})
export class ToggleJoinLibraryComponent {
selection$: Observable<SelectionState>;
constructor(
private store: Store<AppStore>,
private content: ContentManagementService
) {
this.selection$ = this.store.select(appSelection);
}
onToggleEvent(event) {
this.store.dispatch(new SnackbarInfoAction(event.i18nKey));
if (event.shouldReload) {
this.content.libraryJoined.next();
} else {
this.content.joinLibraryToggle.next();
}
}
onErrorEvent(event) {
this.store.dispatch(new SnackbarErrorAction(event.i18nKey));
}
}

View File

@ -58,7 +58,8 @@ export class DocumentListDirective implements OnInit, OnDestroy {
this.isLibrary =
this.documentList.currentFolderId === '-mysites-' ||
// workaround for custom node list
this.router.url.endsWith('/libraries');
this.router.url.endsWith('/libraries') ||
this.router.url.startsWith('/search-libraries');
if (this.sortingPreferenceKey) {
const current = this.documentList.sorting;

View File

@ -0,0 +1,185 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2018 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 {
Directive,
EventEmitter,
HostListener,
Input,
OnChanges,
Output
} from '@angular/core';
import { SiteEntry, SiteMembershipRequestBody } from 'alfresco-js-api';
import { AlfrescoApiService } from '@alfresco/adf-core';
import { BehaviorSubject, from } from 'rxjs';
@Directive({
selector: '[acaLibraryMembership]',
exportAs: 'libraryMembership'
})
export class LibraryMembershipDirective implements OnChanges {
targetSite: any = null;
isJoinRequested: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(
false
);
/** Site for which to toggle the membership request. */
@Input('acaLibraryMembership')
selection: SiteEntry = null;
@Output() toggle: EventEmitter<any> = new EventEmitter();
@Output() error: EventEmitter<any> = new EventEmitter();
@HostListener('click')
onClick() {
this.toggleMembershipRequest();
}
constructor(private alfrescoApiService: AlfrescoApiService) {}
ngOnChanges(changes) {
if (
!changes.selection.currentValue ||
!changes.selection.currentValue.entry
) {
this.targetSite = null;
return;
}
this.targetSite = changes.selection.currentValue.entry;
this.markMembershipRequest();
}
toggleMembershipRequest() {
if (!this.targetSite) {
return;
}
if (this.targetSite.joinRequested) {
this.cancelJoinRequest().subscribe(
() => {
this.targetSite.joinRequested = false;
this.isJoinRequested.next(false);
const info = {
shouldReload: false,
i18nKey: 'APP.MESSAGES.INFO.JOIN_CANCELED'
};
this.toggle.emit(info);
},
error => {
const errWitMessage = {
error,
i18nKey: 'APP.MESSAGES.ERRORS.JOIN_CANCEL_FAILED'
};
this.error.emit(errWitMessage);
}
);
}
if (!this.targetSite.joinRequested) {
this.joinLibraryRequest().subscribe(
createdMembership => {
this.targetSite.joinRequested = true;
this.isJoinRequested.next(true);
if (
createdMembership.entry &&
createdMembership.entry.site &&
createdMembership.entry.site.role
) {
const info = {
shouldReload: true,
i18nKey: 'APP.MESSAGES.INFO.JOINED'
};
this.toggle.emit(info);
} else {
const info = {
shouldReload: false,
i18nKey: 'APP.MESSAGES.INFO.JOIN_REQUESTED'
};
this.toggle.emit(info);
}
},
error => {
const errWitMessage = {
error,
i18nKey: 'APP.MESSAGES.ERRORS.JOIN_REQUEST_FAILED'
};
this.error.emit(errWitMessage);
}
);
}
}
markMembershipRequest() {
if (!this.targetSite) {
return;
}
this.getMembershipRequest().subscribe(
data => {
if (data.entry.id === this.targetSite.id) {
this.targetSite.joinRequested = true;
this.isJoinRequested.next(true);
}
},
() => {
this.targetSite.joinRequested = false;
this.isJoinRequested.next(false);
}
);
}
private joinLibraryRequest() {
const memberBody = <SiteMembershipRequestBody>{
id: this.targetSite.id
};
return from(
this.alfrescoApiService.peopleApi.addSiteMembershipRequest(
'-me-',
memberBody
)
);
}
private cancelJoinRequest() {
return from(
this.alfrescoApiService.peopleApi.removeSiteMembershipRequest(
'-me-',
this.targetSite.id
)
);
}
private getMembershipRequest() {
return from(
this.alfrescoApiService.peopleApi.getSiteMembershipRequest(
'-me-',
this.targetSite.id
)
);
}
}

View File

@ -47,6 +47,7 @@ import { LibraryStatusColumnComponent } from '../components/common/library-statu
import { TrashcanNameColumnComponent } from '../components/common/trashcan-name-column/trashcan-name-column.component';
import { LocationLinkComponent } from '../components/common/location-link/location-link.component';
import { DocumentDisplayModeComponent } from '../components/toolbar/document-display-mode/document-display-mode.component';
import { ToggleJoinLibraryComponent } from '../components/toolbar/toggle-join-library/toggle-join-library.component';
export function setupExtensions(service: AppExtensionService): Function {
return () => service.load();
@ -85,6 +86,7 @@ export class CoreExtensionsModule {
'app.components.tabs.versions': VersionsTabComponent,
'app.toolbar.toggleInfoDrawer': ToggleInfoDrawerComponent,
'app.toolbar.toggleFavorite': ToggleFavoriteComponent,
'app.toolbar.toggleJoinLibrary': ToggleJoinLibraryComponent,
'app.toolbar.cardView': DocumentDisplayModeComponent,
'app.shared-link.toggleSharedLink': ToggleSharedComponent,
'app.columns.name': NameColumnComponent,
@ -110,6 +112,9 @@ export class CoreExtensionsModule {
'app.selection.file': app.hasFileSelected,
'app.selection.file.canShare': app.canShareFile,
'app.selection.library': app.hasLibrarySelected,
'app.selection.isPrivateLibrary': app.isPrivateLibrary,
'app.selection.hasLibraryRole': app.hasLibraryRole,
'app.selection.hasNoLibraryRole': app.hasNoLibraryRole,
'app.selection.folder': app.hasFolderSelected,
'app.selection.folder.canUpdate': app.canUpdateSelectedFolder,

View File

@ -178,6 +178,35 @@ export function hasLibrarySelected(
return library ? true : false;
}
export function isPrivateLibrary(
context: RuleContext,
...args: RuleParameter[]
): boolean {
const library = context.selection.library;
return library
? !!(
library.entry &&
library.entry.visibility &&
library.entry.visibility === 'PRIVATE'
)
: false;
}
export function hasLibraryRole(
context: RuleContext,
...args: RuleParameter[]
): boolean {
const library = context.selection.library;
return library ? !!(library.entry && library.entry.role) : false;
}
export function hasNoLibraryRole(
context: RuleContext,
...args: RuleParameter[]
): boolean {
return !hasLibraryRole(context, ...args);
}
export function hasFileSelected(
context: RuleContext,
...args: RuleParameter[]

View File

@ -75,7 +75,9 @@ export function isLibraries(
...args: RuleParameter[]
): boolean {
const { url } = context.navigation;
return url && url.endsWith('/libraries');
return (
url && (url.endsWith('/libraries') || url.startsWith('/search-libraries'))
);
}
export function isNotLibraries(

View File

@ -82,6 +82,8 @@ export class ContentManagementService {
libraryDeleted = new Subject<string>();
libraryCreated = new Subject<SiteEntry>();
libraryUpdated = new Subject<SiteEntry>();
libraryJoined = new Subject<string>();
joinLibraryToggle = new Subject<string>();
linksUnshared = new Subject<any>();
favoriteAdded = new Subject<Array<MinimalNodeEntity>>();
favoriteRemoved = new Subject<Array<MinimalNodeEntity>>();

View File

@ -45,6 +45,7 @@ export class NavigateLibraryAction implements Action {
readonly type = NAVIGATE_LIBRARY;
constructor(public payload?: string) {}
}
export class UpdateLibraryAction implements Action {
readonly type = UPDATE_LIBRARY;
constructor(public payload?: SiteBody) {}

View File

@ -33,8 +33,8 @@ import {
CREATE_LIBRARY,
NavigateLibraryAction,
NAVIGATE_LIBRARY,
UPDATE_LIBRARY,
UpdateLibraryAction
UpdateLibraryAction,
UPDATE_LIBRARY
} from '../actions';
import { ContentManagementService } from '../../services/content-management.service';
import { Store } from '@ngrx/store';

View File

@ -101,6 +101,20 @@
{ "type": "rule", "value": "app.navigation.isLibraries" }
]
},
{
"id": "app.libraries.toolbar.canToggleJoin",
"type": "core.every",
"parameters": [
{ "type": "rule", "value": "app.selection.library" },
{ "type": "rule",
"value": "core.not",
"parameters": [
{ "type": "rule", "value": "app.selection.isPrivateLibrary" }
]
},
{ "type": "rule", "value": "app.selection.hasNoLibraryRole" }
]
},
{
"id": "app.toolbar.canCopyNode",
"type": "core.every",
@ -387,6 +401,15 @@
"visible": "app.libraries.toolbar.info"
}
},
{
"id": "app.toolbar.joinLibrary",
"type": "custom",
"order": 704,
"component": "app.toolbar.toggleJoinLibrary",
"rules": {
"visible": "app.libraries.toolbar.canToggleJoin"
}
},
{
"id": "app.toolbar.more",
"type": "menu",
@ -1277,7 +1300,8 @@
"title": "APP.DOCUMENT_LIST.COLUMNS.ROLE",
"type": "text",
"sortable": true,
"desktopOnly": true
"template": "app.columns.libraryRole",
"desktopOnly": false
},
{
"id": "app.libraries.visibility",

View File

@ -162,7 +162,9 @@
"SHARE": "Share",
"SHARE_EDIT": "Shared link settings",
"PRINT": "Print",
"FULLSCREEN": "Activate full-screen mode"
"FULLSCREEN": "Activate full-screen mode",
"JOIN": "Join",
"CANCEL_JOIN": "Cancel join request"
},
"DIALOGS": {
"CONFIRM_PURGE": {
@ -227,7 +229,9 @@
"GENERIC": "There was a problem restoring {{ name }}"
}
},
"DELETE_LIBRARY_FAILED": "Cannot delete the library"
"DELETE_LIBRARY_FAILED": "Cannot delete the library",
"JOIN_REQUEST_FAILED": "Cannot join the library",
"JOIN_CANCEL_FAILED": "Cannot cancel the request to join the library"
},
"UPLOAD": {
"ERROR": {
@ -272,7 +276,10 @@
"FAIL": "{{ failed }} couldn't be moved."
}
},
"LIBRARY_DELETED": "Library deleted"
"LIBRARY_DELETED": "Library deleted",
"JOINED": "Library joined",
"JOIN_REQUESTED": "Join library request sent",
"JOIN_CANCELED": "Canceled the request to join the library"
}
},
"CONTENT_METADATA": {

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>join-library</title>
<desc>Created with Sketch.</desc>
<defs>
<polygon id="path-1" points="0.0003 0 16 0 16 16 0.0003 16"></polygon>
<polygon id="path-3" points="0 0 15.9998 0 15.9998 16 0 16"></polygon>
</defs>
<g id="join-library" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.54">
<g id="systemicon/join_library">
<g id="Group" transform="translate(2.000000, 2.000000)">
<g id="Group-2">
<g id="Group">
<g id="Group-3" transform="translate(4.000000, 0.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<g id="Clip-2"></g>
<path d="M14.0003,0 L2.0003,0 C0.9003,0 0.0003,0.9 0.0003,2 L0.0003,14 C0.0003,15.1 0.9003,16 2.0003,16 L14.0003,16 C15.1003,16 16.0003,15.1 16.0003,14 L16.0003,2 C16.0003,0.9 15.1003,0 14.0003,0" id="Fill-1" fill="#000000" mask="url(#mask-2)"></path>
</g>
<g id="Group-6" transform="translate(0.000000, 4.000000)">
<mask id="mask-4" fill="white">
<use xlink:href="#path-3"></use>
</mask>
<g id="Clip-5"></g>
<path d="M1.9998,0 L-0.0002,0 L-0.0002,14 C-0.0002,15.1 0.8998,16 1.9998,16 L15.9998,16 L15.9998,14 L1.9998,14 L1.9998,0 Z" id="Fill-4" fill="#000000" mask="url(#mask-4)"></path>
</g>
<polygon id="Fill-7" fill="#FEFEFE" points="11 12 11 10 7 10 7 6 11 6 11 4 15 8"></polygon>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB