[Send] Groupings component (#1605)

* Initial commit of groupings component

* Update jslib ee164be

* Updated send-list item icons

* Requested changes

* Removed obsolete safari hacks
This commit is contained in:
Vincent Salucci 2021-02-10 10:16:34 -06:00 committed by GitHub
parent 1868b99d17
commit 0d6e212463
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 395 additions and 48 deletions

2
jslib

@ -1 +1 @@
Subproject commit 11249e34441ea747f53fcb0b6e38f690366b46b5 Subproject commit ee164bebc65aa56e41a122eb4ece8971eb23119b

View File

@ -1474,5 +1474,70 @@
"send": { "send": {
"message": "Send", "message": "Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"searchSends": {
"message": "Search Sends",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"addSend": {
"message": "Add Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendTypeText": {
"message": "Text"
},
"sendTypeFile": {
"message": "File"
},
"allSends": {
"message": "All Sends",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"maxAccessCountReached": {
"message": "Max access count reached"
},
"expired": {
"message": "Expired"
},
"pendingDeletion": {
"message": "Pending deletion"
},
"passwordProtected": {
"message": "Password protected"
},
"copySendLink": {
"message": "Copy Send Link",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"removePassword": {
"message": "Remove Password"
},
"delete": {
"message": "Delete"
},
"removedPassword": {
"message": "Removed Password"
},
"deletedSend": {
"message": "Deleted Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendLink": {
"message": "Send Link",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"disabled": {
"message": "Disabled"
},
"removePasswordConfirmation": {
"message": "Are you sure you want to remove the password?"
},
"deleteSend": {
"message": "Delete Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"deleteSendConfirmation": {
"message": "Are you sure you want to delete this Send?",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
} }
} }

View File

@ -46,7 +46,7 @@ import { PasswordHistoryComponent } from './vault/password-history.component';
import { ShareComponent } from './vault/share.component'; import { ShareComponent } from './vault/share.component';
import { ViewComponent } from './vault/view.component'; import { ViewComponent } from './vault/view.component';
import { SendComponent } from './send/send.component'; import { SendGroupingsComponent } from './send/send-groupings.component';
const routes: Routes = [ const routes: Routes = [
{ {
@ -273,7 +273,7 @@ const routes: Routes = [
}, },
{ {
path: 'send', path: 'send',
component: SendComponent, component: SendGroupingsComponent,
canActivate: [AuthGuardService], canActivate: [AuthGuardService],
data: { state: 'tabs_send' }, data: { state: 'tabs_send' },
}, },

View File

@ -52,7 +52,7 @@ import { PasswordHistoryComponent } from './vault/password-history.component';
import { ShareComponent } from './vault/share.component'; import { ShareComponent } from './vault/share.component';
import { ViewComponent } from './vault/view.component'; import { ViewComponent } from './vault/view.component';
import { SendComponent } from './send/send.component'; import { SendGroupingsComponent } from './send/send-groupings.component';
import { A11yTitleDirective } from 'jslib/angular/directives/a11y-title.directive'; import { A11yTitleDirective } from 'jslib/angular/directives/a11y-title.directive';
import { ApiActionDirective } from 'jslib/angular/directives/api-action.directive'; import { ApiActionDirective } from 'jslib/angular/directives/api-action.directive';
@ -73,6 +73,7 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
import { ActionButtonsComponent } from './components/action-buttons.component'; import { ActionButtonsComponent } from './components/action-buttons.component';
import { CiphersListComponent } from './components/ciphers-list.component'; import { CiphersListComponent } from './components/ciphers-list.component';
import { PopOutComponent } from './components/pop-out.component'; import { PopOutComponent } from './components/pop-out.component';
import { SendListComponent } from './components/send-list.component';
import { CalloutComponent } from 'jslib/angular/components/callout.component'; import { CalloutComponent } from 'jslib/angular/components/callout.component';
import { IconComponent } from 'jslib/angular/components/icon.component'; import { IconComponent } from 'jslib/angular/components/icon.component';
@ -211,7 +212,8 @@ registerLocaleData(localeZhTw, 'zh-TW');
RegisterComponent, RegisterComponent,
SearchCiphersPipe, SearchCiphersPipe,
SelectCopyDirective, SelectCopyDirective,
SendComponent, SendGroupingsComponent,
SendListComponent,
SettingsComponent, SettingsComponent,
ShareComponent, ShareComponent,
StopClickDirective, StopClickDirective,

View File

@ -0,0 +1,51 @@
<a *ngFor="let s of sends" (click)="selectSend(s)" href="#" appStopClick title="{{title}} - {{s.name}}"
class="box-content-row box-content-row-flex">
<div class="row-main">
<div class="app-vault-icon">
<div class="icon" aria-hidden="true">
<i class="fa fa-fw fa-lg fa-file-text-o" *ngIf="s.type === sendType.Text"></i>
<i class="fa fa-fw fa-lg fa-file-o" *ngIf="s.type === sendType.File"></i>
</div>
</div>
<div class="row-main-content">
<span class="text">
{{s.name}}
<ng-container *ngIf="s.disabled">
<i class="fa fa-ban text-muted" title="{{'disabled' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'disabled' | i18n}}</span>
</ng-container>
<ng-container *ngIf="s.password">
<i class="fa fa-key text-muted" title="{{'passwordProtected' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'passwordProtected' | i18n}}</span>
</ng-container>
<ng-container *ngIf="s.maxAccessCountReached">
<i class="fa fa-warning text-muted" title="{{'maxAccessCountReached' | i18n}}"
aria-hidden="true"></i>
<span class="sr-only">{{'maxAccessCountReached' | i18n}}</span>
</ng-container>
<ng-container *ngIf="s.expired">
<i class="fa fa-clock-o text-muted" title="{{'expired' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'expired' | i18n}}</span>
</ng-container>
<ng-container *ngIf="s.pendingDelete">
<i class="fa fa-trash text-muted" title="{{'pendingDeletion' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'pendingDeletion' | i18n}}</span>
</ng-container>
</span>
<span class="detail">{{s.deletionDate | date:'medium'}}</span>
</div>
</div>
<div class="action-buttons">
<span class="row-btn" appStopClick appStopProp appA11yTitle="{{'copySendLink' | i18n}}"
(click)="copySendLink(s)">
<i class="fa fa-lg fa-copy" aria-hidden="true"></i>
</span>
<span class="row-btn" appStopClick appStopProp appA11yTitle="{{'removePassword' | i18n}}"
(click)="removePassword(s)" *ngIf="s.password">
<i class="fa fa-lg fa-undo" aria-hidden="true"></i>
</span>
<span class="row-btn" appStopClick appStopProp appA11yTitle="{{'delete' | i18n}}" (click)="delete(s)">
<i class="fa fa-lg fa-trash-o" aria-hidden="true"></i>
</span>
</div>
</a>

View File

@ -0,0 +1,41 @@
import {
Component,
EventEmitter,
Input,
Output,
} from '@angular/core';
import { SendView } from 'jslib/models/view/sendView';
import { SendType } from 'jslib/enums/sendType';
@Component({
selector: 'app-send-list',
templateUrl: 'send-list.component.html',
})
export class SendListComponent {
@Input() sends: SendView[];
@Input() title: string;
@Output() onSelected = new EventEmitter<SendView>();
@Output() onCopySendLink = new EventEmitter<SendView>();
@Output() onRemovePassword = new EventEmitter<SendView>();
@Output() onDeleteSend = new EventEmitter<SendView>();
sendType = SendType;
selectSend(s: SendView) {
this.onSelected.emit(s);
}
copySendLink(s: SendView) {
this.onCopySendLink.emit(s);
}
removePassword(s: SendView) {
this.onRemovePassword.emit(s);
}
delete(s: SendView) {
this.onDeleteSend.emit(s);
}
}

View File

@ -0,0 +1,74 @@
<header>
<div class="left" *ngIf="showLeftHeader">
<app-pop-out></app-pop-out>
</div>
<div class="search">
<input type="search" placeholder="{{'searchSends' | i18n}}" id="search" [(ngModel)]="searchText"
(input)="search(200)" autocomplete="off" appAutofocus>
<i class="fa fa-search"></i>
</div>
<div class="right">
<button appBlurClick (click)="addSend()" appA11yTitle="{{'addSend' | i18n}}">
<i class="fa fa-plus fa-lg fa-fw" aria-hidden="true"></i>
</button>
</div>
</header>
<content>
<div class="no-items" *ngIf="(!sends || !sends.length) && !showSearching()">
<i class="fa fa-spinner fa-spin fa-3x" *ngIf="!loaded"></i>
<ng-container *ngIf="loaded">
<i class="fa fa-frown-o fa-4x"></i>
<p>{{'noItemsInList' | i18n}}</p>
<button (click)="addSend()" class="btn block primary link">{{'addSend' | i18n}}</button>
</ng-container>
</div>
<ng-container *ngIf="sends && sends.length && !showSearching()">
<div class="box list">
<div class="box-header">
{{'types' | i18n}}
</div>
<div class="box-content single-line">
<a href="#" class="box-content-row" appStopClick appBlurClick (click)="selectType(sendType.Text)">
<div class="row-main">
<div class="icon"><i class="fa fa-fw fa-lg fa-file-text-o"></i></div>
<span class="text">{{'sendTypeText' | i18n}}</span>
</div>
<span class="row-sub-label">{{typeCounts.get(sendType.Text) || 0}}</span>
<span><i class="fa fa-chevron-right fa-lg row-sub-icon"></i></span>
</a>
<a href="#" class="box-content-row" appStopClick appBlurClick (click)="selectType(sendType.File)">
<div class="row-main">
<div class="icon"><i class="fa fa-fw fa-lg fa-file-o"></i></div>
<span class="text">{{'sendTypeFile' | i18n}}</span>
</div>
<span class="row-sub-label">{{typeCounts.get(sendType.File) || 0}}</span>
<span><i class="fa fa-chevron-right fa-lg row-sub-icon"></i></span>
</a>
</div>
</div>
<div class="box list">
<div class="box-header">
{{'allSends' | i18n}}
<div class="flex-right">{{sends.length}}</div>
</div>
<div class="box-content">
<app-send-list [sends]="sends" title="{{'editItem' | i18n}}" (onSelected)="selectSend($event)"
(onCopySendLink)="copy($event)" (onRemovePassword)="removePassword($event)"
(onDeleteSend)="delete($event)"></app-send-list>
</div>
</div>
</ng-container>
<ng-container *ngIf="showSearching()">
<div class="no-items" *ngIf="!filteredSends || !filteredSends.length">
<p>{{'noItemsInList' | i18n}}</p>
</div>
<div class="box list full-list" *ngIf="filteredSends && filteredSends.length > 0">
<div class="box-content">
<app-send-list [sends]="filteredSends" title="{{'editItem' | i18n}}" (onSelected)="selectSend($event)"
(onCopySendLink)="copy($event)" (onRemovePassword)="removePassword($event)"
(onDeleteSend)="delete($event)">
</app-send-list>
</div>
</div>
</ng-container>
</content>

View File

@ -0,0 +1,157 @@
import {
Component,
NgZone,
} from '@angular/core';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { SendView } from 'jslib/models/view/sendView';
import { SendComponent as BaseSendComponent } from 'jslib/angular/components/send/send.component';
import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { PolicyService } from 'jslib/abstractions/policy.service';
import { SearchService } from 'jslib/abstractions/search.service';
import { SendService } from 'jslib/abstractions/send.service';
import { StateService } from 'jslib/abstractions/state.service';
import { SyncService } from 'jslib/abstractions/sync.service';
import { UserService } from 'jslib/abstractions/user.service';
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
import { PopupUtilsService } from '../services/popup-utils.service';
import { SendType } from 'jslib/enums/sendType';
const ComponentId = 'SendComponent';
const ScopeStateId = ComponentId + 'Scope';
@Component({
selector: 'app-send-groupings',
templateUrl: 'send-groupings.component.html',
})
export class SendGroupingsComponent extends BaseSendComponent {
// Header
showLeftHeader = true;
// Send Type Calculations
typeCounts = new Map<SendType, number>();
// State Handling
state: any;
scopeState: any;
private loadedTimeout: number;
constructor(sendService: SendService, i18nService: I18nService,
platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
broadcasterService: BroadcasterService, ngZone: NgZone, policyService: PolicyService,
userService: UserService, searchService: SearchService,
private popupUtils: PopupUtilsService, private stateService: StateService,
private route: ActivatedRoute, private router: Router, private syncService: SyncService) {
super(sendService, i18nService, platformUtilsService, environmentService, broadcasterService, ngZone,
searchService, policyService, userService);
super.onSuccessfulLoad = async () => {
this.calculateTypeCounts();
};
}
async ngOnInit() {
// Determine Header details
this.showLeftHeader = !(this.popupUtils.inSidebar(window) && this.platformUtilsService.isFirefox());
// Let super class finish
await super.ngOnInit();
// Handle State Restore if necessary
const restoredScopeState = await this.restoreState();
this.state = (await this.stateService.get<any>(ComponentId)) || {};
if (this.state.searchText != null) {
this.searchText = this.state.searchText;
}
if (!this.syncService.syncInProgress) {
this.load();
} else {
this.loadedTimeout = window.setTimeout(() => {
if (!this.loaded) {
this.load();
}
}, 5000);
}
if (!this.syncService.syncInProgress || restoredScopeState) {
window.setTimeout(() => this.popupUtils.setContentScrollY(window, this.state.scrollY), 0);
}
}
ngOnDestroy() {
// Remove timeout
if (this.loadedTimeout != null) {
window.clearTimeout(this.loadedTimeout);
}
// Save state
this.saveState();
// Allow super to finish
super.ngOnDestroy();
}
async selectType(type: SendType) {
// TODO this.router.navigate(['/send-type-list'], { queryParams: { type: type } });
}
async selectSend(s: SendView) {
// TODO -> Route to edit send
}
async addSend() {
// TODO -> Route to create send
}
showSearching() {
return this.hasSearched || (!this.searchPending && this.searchService.isSearchable(this.searchText));
}
private calculateTypeCounts() {
// Create type counts
const typeCounts = new Map<SendType, number>();
this.sends.forEach((s) => {
if (typeCounts.has(s.type)) {
typeCounts.set(s.type, typeCounts.get(s.type) + 1);
} else {
typeCounts.set(s.type, 1);
}
});
this.typeCounts = typeCounts;
}
private async saveState() {
this.state = {
scrollY: this.popupUtils.getContentScrollY(window),
searchText: this.searchText,
};
await this.stateService.save(ComponentId, this.state);
this.scopeState = {
sends: this.sends,
typeCounts: this.typeCounts,
};
await this.stateService.save(ScopeStateId, this.scopeState);
}
private async restoreState(): Promise<boolean> {
this.scopeState = await this.stateService.get<any>(ScopeStateId);
if (this.scopeState == null) {
return false;
}
if (this.scopeState.sends != null) {
this.sends = this.scopeState.sends;
}
if (this.scopeState.typeCounts != null) {
this.typeCounts = this.scopeState.typeCounts;
}
return true;
}
}

View File

@ -1,8 +0,0 @@
<header>
</header>
<content>
<div class="no-items">
<i class="fa fa-smile-o fa-4x"></i>
<p>Coming soon...</p>
</div>
</content>

View File

@ -1,35 +0,0 @@
import {
Component,
NgZone,
} from '@angular/core';
import { SendView } from 'jslib/models/view/sendView';
import { SendComponent as BaseSendComponent } from 'jslib/angular/components/send/send.component';
import { EnvironmentService } from 'jslib/abstractions/environment.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { SendService } from 'jslib/abstractions/send.service';
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
@Component({
selector: 'app-send',
templateUrl: 'send.component.html',
})
export class SendComponent extends BaseSendComponent {
constructor(sendService: SendService, i18nService: I18nService,
platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
broadcasterService: BroadcasterService, ngZone: NgZone) {
super(sendService, i18nService, platformUtilsService, environmentService, broadcasterService, ngZone);
}
addSend() {
// TODO
}
editSend(send: SendView) {
// TODO
}
}