FE-1062: new section for claim n new conversation for them (#144)

* FE-1062: new section for claim n new conversation for them
This commit is contained in:
Aleksandra Usacheva 2020-07-02 12:01:55 +03:00 committed by GitHub
parent ed7a8a9533
commit 3b472fe8fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
90 changed files with 892 additions and 70 deletions

2
Jenkinsfile vendored
View File

@ -39,5 +39,5 @@ build('control-center', 'docker-host') {
}
}
}
pipeDefault(pipeline, 'dr2.rbkmoney.com', 'jenkins_harbor')
pipeDefault(pipeline)
}

View File

@ -32,7 +32,14 @@
"src/favicon.ico",
"src/assets"
],
"styles": ["src/styles.scss"],
"styles": [
"src/app/styles/core.scss",
{
"input": "src/app/styles/themes/light.scss",
"bundleName": "themes/light",
"lazy": true
}
],
"scripts": ["./node_modules/keycloak-js/dist/keycloak.js"]
},
"configurations": {

View File

@ -4,7 +4,7 @@ import { KeycloakService } from 'keycloak-angular';
@Component({
selector: 'cc-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
username: string;

View File

@ -31,6 +31,8 @@ import { PayoutsModule } from './payouts/payouts.module';
import { RepairingModule } from './repairing/repairing.module';
import { PartyModule } from './sections/party/party.module';
import { SearchClaimsModule } from './sections/search-claims/search-claims.module';
import { SettingsModule } from './settings';
import { ThemeManager, ThemeManagerModule, ThemeName } from './theme-manager';
/**
* For use in specific locations (for example, questionary PDF document)
@ -59,6 +61,8 @@ moment.locale('en');
PartyModule,
DomainModule,
RepairingModule,
ThemeManagerModule,
SettingsModule,
DepositsModule,
ClaimMgtModule,
PartyModule,
@ -73,4 +77,8 @@ moment.locale('en');
],
bootstrap: [AppComponent],
})
export class AppModule {}
export class AppModule {
constructor(private themeManager: ThemeManager) {
this.themeManager.change(ThemeName.light);
}
}

View File

@ -8,7 +8,7 @@ import { RecreateClaimService } from './recreate-claim';
@Component({
templateUrl: 'claim.component.html',
styleUrls: ['claim.component.css'],
styleUrls: ['claim.component.scss'],
providers: [ClaimManagementService, ClaimService, RecreateClaimService],
})
export class ClaimComponent implements OnInit {

View File

@ -5,7 +5,7 @@ import { FileContainerService } from './file-container.service';
@Component({
selector: 'cc-file-container',
templateUrl: 'file-container.component.html',
styleUrls: ['file-container.component.css'],
styleUrls: ['file-container.component.scss'],
providers: [FileContainerService],
})
export class FileContainerComponent implements OnInit {

View File

@ -4,13 +4,12 @@ import { FlexModule } from '@angular/flex-layout';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { FileStorageService } from '../../../../thrift-services/file-storage/file-storage.service';
import { FileStorageModule } from '../../../../thrift-services/file-storage';
import { FileContainerComponent } from './file-container.component';
@NgModule({
imports: [CommonModule, MatCardModule, FlexModule, MatIconModule],
imports: [CommonModule, MatCardModule, FlexModule, MatIconModule, FileStorageModule],
exports: [FileContainerComponent],
declarations: [FileContainerComponent],
providers: [FileStorageService],
})
export class FileContainerModule {}

View File

@ -1,3 +0,0 @@
.dsh-file-uploader:hover {
cursor: pointer;
}

View File

@ -1,4 +1,4 @@
<div ngfSelect class="dsh-file-uploader" (filesChange)="startUploading($event)">
<div ngfSelect class="cc-file-uploader" (filesChange)="startUploading($event)">
<button mat-icon-button [disabled]="inProgress$ | async">
<mat-icon>attach_file</mat-icon>
</button>

View File

@ -0,0 +1,3 @@
.cc-file-uploader:hover {
cursor: pointer;
}

View File

@ -6,7 +6,7 @@ import { FileUploaderService } from './file-uploader.service';
@Component({
selector: 'cc-file-uploader',
templateUrl: 'file-uploader.component.html',
styleUrls: ['file-uploader.component.css'],
styleUrls: ['file-uploader.component.scss'],
})
export class FileUploaderComponent {
@Output()

View File

@ -13,7 +13,7 @@ import { RemoveConfirmComponent } from './remove-confirm/remove-confirm.componen
@Component({
selector: 'cc-party-modification-container',
templateUrl: 'party-modification-container.component.html',
styleUrls: ['./party-modification-container.component.css'],
styleUrls: ['./party-modification-container.component.scss'],
})
export class PartyModificationContainerComponent implements OnInit {
@Input()

View File

@ -8,7 +8,7 @@ import { ModificationGroupType, PartyModificationUnit } from '../model';
@Component({
selector: 'cc-party-modification-units',
templateUrl: 'party-modification-units.component.html',
styleUrls: ['./party-modification-units.component.css'],
styleUrls: ['./party-modification-units.component.scss'],
})
export class PartyModificationUnitsComponent {
@Input()

View File

@ -7,7 +7,7 @@ import { ClaimInfo } from '../../papi/model';
@Component({
selector: 'cc-claims-table',
templateUrl: 'claims-table.component.html',
styleUrls: ['./claims-table.component.css'],
styleUrls: ['./claims-table.component.scss'],
})
export class ClaimsTableComponent {
@Input()

View File

@ -8,6 +8,6 @@
fxLayoutGap="20px"
fxLayoutGap.xs="10px"
>
<button mat-button fxFlex fxFlex.xs="none" dsh-button (click)="cancel()">CANCEL</button>
<button mat-button fxFlex fxFlex.xs="none" dsh-button (click)="confirm()">CONFIRM</button>
<button mat-button fxFlex fxFlex.xs="none" (click)="cancel()">CANCEL</button>
<button mat-button fxFlex fxFlex.xs="none" (click)="confirm()">CONFIRM</button>
</div>

View File

@ -5,7 +5,7 @@ import { StatDeposit } from '../../thrift-services/fistful/gen-model/fistful_sta
@Component({
selector: 'cc-deposits-table',
templateUrl: 'deposits-table.component.html',
styleUrls: ['deposits-table.component.css'],
styleUrls: ['deposits-table.component.scss'],
})
export class DepositsTableComponent {
@Input()

View File

@ -8,7 +8,7 @@ import { Shop } from '../../../thrift-services/damsel/gen-model/domain';
@Component({
selector: 'cc-shops-table',
templateUrl: 'shops-table.component.html',
styleUrls: ['shops-table.component.css'],
styleUrls: ['shops-table.component.scss'],
})
export class ShopsTableComponent implements OnChanges {
@Input() shops: Shop[];

View File

@ -18,7 +18,7 @@ import { StatPayment } from '../../thrift-services/damsel/gen-model/merch_stat';
@Component({
selector: 'cc-payment-adjustment-table',
templateUrl: './table.component.html',
styleUrls: ['./table.component.css'],
styleUrls: ['./table.component.scss'],
})
export class TableComponent implements OnInit, OnChanges {
@Input()

View File

@ -20,7 +20,7 @@ import { CancelPayoutComponent } from '../cancel-payout/cancel-payout.component'
@Component({
selector: 'cc-payouts-table',
templateUrl: 'payouts-table.component.html',
styleUrls: ['./payouts-table.component.css'],
styleUrls: ['./payouts-table.component.scss'],
})
export class PayoutsTableComponent implements OnInit, OnChanges {
@Output()

View File

@ -8,7 +8,7 @@ import { SearchFormService } from './search-form/search-form.service';
@Component({
templateUrl: 'payouts.component.html',
styleUrls: ['./payouts.component.css'],
styleUrls: ['./payouts.component.scss'],
providers: [SearchFormService],
})
export class PayoutsComponent {

View File

@ -31,7 +31,7 @@ interface Element {
@Component({
selector: 'cc-repair-with-scenario',
templateUrl: 'repair-with-scenario.component.html',
styleUrls: ['../repairing.component.css'],
styleUrls: ['../repairing.component.scss'],
providers: [],
})
export class RepairWithScenarioComponent {

View File

@ -26,7 +26,7 @@ interface Element {
@Component({
selector: 'cc-repair',
templateUrl: 'repair.component.html',
styleUrls: ['../repairing.component.css'],
styleUrls: ['../repairing.component.scss'],
providers: [],
})
export class RepairComponent {

View File

@ -6,7 +6,7 @@ import { RepairingService } from './repairing.service';
@Component({
templateUrl: 'repairing.component.html',
styleUrls: ['repairing.component.css'],
styleUrls: ['repairing.component.scss'],
providers: [],
})
export class RepairingComponent {

View File

@ -31,7 +31,7 @@ interface Element {
@Component({
selector: 'cc-simple-repair',
templateUrl: 'simple-repair.component.html',
styleUrls: ['../repairing.component.css'],
styleUrls: ['../repairing.component.scss'],
providers: [],
})
export class SimpleRepairComponent {

View File

@ -1,4 +1,4 @@
<form fxLayout="row" fxLayout.xs="column" fxLayoutGap="20px" [formGroup]="form">
<form fxLayout="row" fxLayout.xs="column" fxLayoutGap="16px" [formGroup]="form">
<mat-form-field fxFlex="25">
<mat-select placeholder="Claim statuses" formControlName="statuses" multiple>
<mat-option *ngFor="let status of claimStatuses" [value]="status">{{

View File

@ -0,0 +1,9 @@
@import '~@angular/material/theming';
@mixin cc-party-claim-theme($theme) {
$foreground: map-get($theme, foreground);
.cc-party-claim-header {
color: mat-color($foreground, secondary-text);
}
}

View File

@ -0,0 +1,5 @@
<div ngfSelect class="cc-file-uploader" (filesChange)="startUploading($event)">
<button mat-icon-button [disabled]="inProgress$ | async">
<mat-icon>cloud_upload</mat-icon>
</button>
</div>

View File

@ -0,0 +1,3 @@
.cc-file-uploader:hover {
cursor: pointer;
}

View File

@ -0,0 +1,31 @@
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
import { Modification } from '../../../thrift-services/damsel/gen-model/claim_management';
import { FileUploaderService } from './file-uploader.service';
@Component({
selector: 'cc-file-uploader',
templateUrl: 'file-uploader.component.html',
styleUrls: ['file-uploader.component.scss'],
})
export class FileUploaderComponent implements OnInit {
@Output()
filesUploaded: EventEmitter<Modification[]> = new EventEmitter();
startUploading$ = this.fileUploaderService.startUploading$;
inProgress$ = this.fileUploaderService.inProgress$;
constructor(private fileUploaderService: FileUploaderService) {}
ngOnInit(): void {
this.fileUploaderService.filesUploaded$.subscribe((values) =>
this.filesUploaded.emit(
values.map((v) => this.fileUploaderService.createModification(v))
)
);
}
startUploading(files: File[]) {
this.startUploading$.next(files);
}
}

View File

@ -0,0 +1,25 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexModule } from '@angular/flex-layout';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { ngfModule } from 'angular-file';
import { FileStorageModule } from '../../../thrift-services/file-storage';
import { FileUploaderComponent } from './file-uploader.component';
import { FileUploaderService } from './file-uploader.service';
@NgModule({
imports: [
FlexModule,
ngfModule,
CommonModule,
MatIconModule,
MatButtonModule,
FileStorageModule,
],
exports: [FileUploaderComponent],
declarations: [FileUploaderComponent],
providers: [FileUploaderService],
})
export class FileUploaderModule {}

View File

@ -0,0 +1,90 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { progress } from '@rbkmoney/partial-fetcher/dist/progress';
import * as moment from 'moment';
import { forkJoin, merge, Observable, of, Subject } from 'rxjs';
import { catchError, filter, map, shareReplay, switchMap } from 'rxjs/operators';
import { Modification } from '../../../thrift-services/damsel/gen-model/claim_management';
import { FileStorageService } from '../../../thrift-services/file-storage/file-storage.service';
import { NewFileResult } from '../../../thrift-services/file-storage/gen-model/file_storage';
import { Value } from '../../../thrift-services/file-storage/gen-model/msgpack';
@Injectable()
export class FileUploaderService {
startUploading$ = new Subject<File[]>();
filesUploadingError$ = new Subject<null>();
filesUploaded$: Observable<string[]> = this.startUploading$.pipe(
switchMap((files) =>
this.uploadFiles(files).pipe(
catchError(() => {
this.filesUploadingError$.next(null);
return of([]);
})
)
),
filter((v) => !!v.length),
shareReplay(1)
);
inProgress$: Observable<boolean> = progress(
this.startUploading$,
merge(this.filesUploaded$, this.filesUploadingError$)
);
constructor(
private fileStorageService: FileStorageService,
private snackBar: MatSnackBar,
private http: HttpClient
) {
this.filesUploadingError$.subscribe(() => this.snackBar.open('File uploading error', 'OK'));
}
uploadFiles(files: File[]): Observable<string[]> {
return forkJoin(
files.map((file) =>
this.getUploadLink().pipe(
switchMap((uploadData) =>
forkJoin([
of(uploadData.file_data_id),
this.uploadFileToUrl(file, uploadData.upload_url),
])
),
map(([fileId]) => fileId)
)
)
);
}
createModification(id: string): Modification {
return {
claim_modification: {
file_modification: {
id,
modification: {
creation: {},
},
},
},
};
}
private getUploadLink(): Observable<NewFileResult> {
return this.fileStorageService.createNewFile(
new Map<string, Value>(),
moment().add(1, 'h').toISOString()
);
}
private uploadFileToUrl(file: File, url: string): Observable<any> {
return this.http.put(url, file, {
headers: {
'Content-Disposition': `attachment;filename=${encodeURI(file.name)}`,
'Content-Type': '',
},
});
}
}

View File

@ -0,0 +1 @@
export * from './party-claim.module';

View File

@ -0,0 +1,21 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { AppAuthGuardService } from '../../app-auth-guard.service';
import { PartyClaimComponent } from './party-claim.component';
@NgModule({
imports: [
RouterModule.forChild([
{
path: '',
component: PartyClaimComponent,
canActivate: [AppAuthGuardService],
data: {
roles: ['get_claims'],
},
},
]),
],
})
export class PartyClaimRoutingModule {}

View File

@ -0,0 +1,29 @@
<div fxLayout="column" fxLayoutAlign="space-around stretch" fxLayoutGap="32px">
<div fxFlex fxLayout="row" fxLayoutAlign="space-between center">
<h3 class="cc-headline">
Claim <span class="cc-party-claim-header">#{{ claimID$ | async }}</span>
</h3>
<h3 class="cc-headline">{{ 'Review' }}</h3>
</div>
<div fxFlex fxLayout="row" fxLayoutAlign="space-between center" fxLayoutGap="16px">
<h3 class="cc-title">Changeset</h3>
<mat-form-field fxFlex="0 1 266px">
<mat-label>Changeset filters</mat-label>
<mat-select>
<mat-option>
eekekekkekek
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<mat-card fxLayout="column" fxLayoutGap="24px">
<mat-card-content fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="space-between center">
<cc-send-comment fxFlex="100"></cc-send-comment>
<cc-file-uploader></cc-file-uploader>
</mat-card-content>
<mat-card-actions fxLayout="row" fxLayoutAlign="space-between center">
<button mat-button>CHANGE STATUS</button>
<button mat-button>ADD PARTY MODIFICATION</button>
</mat-card-actions>
</mat-card>

View File

@ -0,0 +1,14 @@
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { pluck, shareReplay } from 'rxjs/operators';
import { SHARE_REPLAY_CONF } from '../../shared/share-replay-conf';
@Component({
templateUrl: 'party-claim.component.html',
})
export class PartyClaimComponent {
claimID$ = this.route.params.pipe(pluck('claimID'), shareReplay(SHARE_REPLAY_CONF));
constructor(private route: ActivatedRoute) {}
}

View File

@ -0,0 +1,32 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexModule } from '@angular/flex-layout';
import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { FileUploaderModule } from './file-uploader/file-uploader.module';
import { PartyClaimRoutingModule } from './party-claim-routing.module';
import { PartyClaimComponent } from './party-claim.component';
import { SendCommentModule } from './send-comment/send-comment.module';
@NgModule({
imports: [
PartyClaimRoutingModule,
FlexModule,
MatSelectModule,
MatCardModule,
FileUploaderModule,
CommonModule,
MatButtonModule,
MatIconModule,
ReactiveFormsModule,
MatInputModule,
SendCommentModule,
],
declarations: [PartyClaimComponent],
})
export class PartyClaimModule {}

View File

@ -0,0 +1,9 @@
@import '~@angular/material/theming';
@mixin cc-send-comment-theme($theme) {
$warn: map-get($theme, warn);
.cc-send-comment-error {
color: mat-color($warn, 500) !important;
}
}

View File

@ -0,0 +1 @@
export * from './send-comment.component';

View File

@ -0,0 +1,22 @@
<form [formGroup]="form" fxLayout="row" fxLayoutGap="16px" fxLayoutAlign="end start">
<mat-form-field fxFlex>
<mat-label>Leave a comment...</mat-label>
<input formControlName="comment" matInput type="text" autocomplete="off" />
<mat-hint
[ngClass]="{
'cc-caption': true,
'cc-send-comment-error': form.controls.comment.value?.length > 1000
}"
>{{ form.controls.comment.value?.length || 0 }}/1000</mat-hint
>
</mat-form-field>
<div class="send-comment-action">
<button
mat-icon-button
(click)="sendComment()"
[disabled]="(inProgress$ | async) || !form.valid"
>
<mat-icon>send</mat-icon>
</button>
</div>
</form>

View File

@ -0,0 +1,7 @@
@import '~@angular/material/theming';
$cc-send-comment-action-padding: 8px 0 0 0;
.send-comment-action {
padding: $cc-send-comment-action-padding;
}

View File

@ -0,0 +1,36 @@
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { Modification } from '../../../thrift-services/damsel/gen-model/claim_management';
import { SendCommentService } from './send-comment.service';
@Component({
selector: 'cc-send-comment',
templateUrl: 'send-comment.component.html',
styleUrls: ['send-comment.component.scss'],
})
export class SendCommentComponent implements OnInit {
@Output() conversationSaved: EventEmitter<Modification[]> = new EventEmitter();
form: FormGroup = this.sendCommentService.form;
inProgress$ = this.sendCommentService.inProgress$;
constructor(private sendCommentService: SendCommentService) {}
ngOnInit(): void {
this.sendCommentService.conversationSaved$.subscribe((id) =>
this.conversationSaved.emit([this.sendCommentService.createModification(id)])
);
this.inProgress$.subscribe((inProgress) => {
if (inProgress) {
this.form.controls.comment.disable();
} else {
this.form.controls.comment.enable();
}
});
}
sendComment() {
this.sendCommentService.sendComment();
}
}

View File

@ -0,0 +1,29 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexModule } from '@angular/flex-layout';
import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MessagesModule } from '../../../thrift-services/messages';
import { SendCommentComponent } from './send-comment.component';
import { SendCommentService } from './send-comment.service';
@NgModule({
declarations: [SendCommentComponent],
providers: [SendCommentService],
imports: [
MessagesModule,
MatIconModule,
MatButtonModule,
ReactiveFormsModule,
MatFormFieldModule,
FlexModule,
MatInputModule,
CommonModule,
],
exports: [SendCommentComponent],
})
export class SendCommentModule {}

View File

@ -0,0 +1,94 @@
import { Injectable } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { MatSnackBar } from '@angular/material/snack-bar';
import { progress } from '@rbkmoney/partial-fetcher/dist/progress';
import get from 'lodash-es/get';
import { BehaviorSubject, forkJoin, merge, Observable, of, Subject } from 'rxjs';
import { catchError, filter, pluck, switchMap, tap } from 'rxjs/operators';
import * as uuid from 'uuid/v4';
import { KeycloakTokenInfoService } from '../../../keycloak-token-info.service';
import { Modification } from '../../../thrift-services/damsel/gen-model/claim_management';
import { ConversationId, User } from '../../../thrift-services/messages/gen-model/messages';
import { MessagesService } from '../../../thrift-services/messages/messages.service';
import { createSingleMessageConversationParams } from '../../../thrift-services/messages/utils';
@Injectable()
export class SendCommentService {
private conversationId$: BehaviorSubject<ConversationId | null> = new BehaviorSubject(null);
private error$: BehaviorSubject<any> = new BehaviorSubject({ hasError: false });
private sendComment$: Subject<string> = new Subject();
form = this.fb.group({
comment: ['', [Validators.maxLength(1000), Validators.required]],
});
conversationSaved$: Observable<ConversationId> = this.conversationId$.pipe(
filter((id) => !!id)
);
errorCode$: Observable<string> = this.error$.pipe(pluck('code'));
inProgress$: Observable<boolean> = progress(
this.sendComment$,
merge(this.conversationId$, this.error$)
);
constructor(
private fb: FormBuilder,
private messagesService: MessagesService,
private keycloakTokenInfoService: KeycloakTokenInfoService,
private snackBar: MatSnackBar
) {
this.sendComment$
.pipe(
tap(() => this.error$.next({ hasError: false })),
switchMap((text) => {
const { name, email, sub } = this.keycloakTokenInfoService.decodedUserToken;
const user: User = { fullname: name, email, user_id: sub };
const conversation_id = uuid();
const conversation = createSingleMessageConversationParams(
conversation_id,
text,
sub
);
return forkJoin([
of(conversation_id),
this.messagesService.saveConversations([conversation], user).pipe(
catchError((ex) => {
console.error(ex);
this.snackBar.open(
`There was an error sending a comment: ${ex}`,
'OK',
{ duration: 5000 }
);
const error = { hasError: true, code: 'saveConversationsFailed' };
this.error$.next(error);
return of(error);
})
),
]);
}),
filter(([, res]) => get(res, ['hasError']) !== true)
)
.subscribe(([conversation_id]) => {
this.conversationId$.next(conversation_id);
this.form.reset();
});
}
sendComment() {
const { comment } = this.form.value;
this.sendComment$.next(comment);
}
createModification(id: ConversationId): Modification {
return {
claim_modification: {
comment_modification: {
id,
modification: {
creation: {},
},
},
},
};
}
}

View File

@ -3,5 +3,5 @@
}
.action-cell {
width: 10px;
width: 8px;
}

View File

@ -1,9 +1,10 @@
<div fxLayout="column">
<div fxLayout="column" fxLayoutGap="24px">
<div fxFlex fxLayout="row" fxLayoutAlign="space-between center">
<div class="mat-headline">Party claims</div>
<div class="сс-headline">Party claims</div>
<button mat-button color="primary" (click)="createClaim()">CREATE</button>
</div>
<div fxLayout="column" fxLayoutGap="20px">
<div class="cc-headline">Party claims</div>
<div fxLayout="column" fxLayoutGap="24px">
<mat-card>
<mat-card-content>
<cc-claim-search-form
@ -17,7 +18,7 @@
</mat-card>
<ng-container *ngIf="claims$ | async as claims">
<cc-empty-search-result *ngIf="claims.length === 0"></cc-empty-search-result>
<mat-card *ngIf="claims.length > 0" fxLayout="column" fxLayoutGap="20px">
<mat-card *ngIf="claims.length > 0" fxLayout="column" fxLayoutGap="16px">
<cc-claims-table [claims]="claims"></cc-claims-table>
<button
fxFlex="100"

View File

@ -1 +1 @@
<div class="mat-headline">Party shops</div>
<div class="cc-headline">Party shops</div>

View File

@ -0,0 +1,9 @@
@import '~@angular/material/theming';
@mixin cc-party-theme($theme) {
$foreground: map-get($theme, foreground);
.cc-party-header {
color: mat-color($foreground, secondary-text);
}
}

View File

@ -24,6 +24,15 @@ import { PartyComponent } from './party.component';
roles: ['get_claims'],
},
},
{
path: 'claim/:claimID',
loadChildren: () =>
import('../party-claim').then((m) => m.PartyClaimModule),
canActivate: [AppAuthGuardService],
data: {
roles: ['get_claims'],
},
},
{
path: 'shops',
loadChildren: () =>

View File

@ -1,7 +1,7 @@
<div class="party-container">
<h3 class="mat-subheading-1">Party #{{ partyID }}</h3>
<div class="party-container" fxLayout="column" fxLayoutGap="24px">
<h3 class="cc-subheading-1 cc-party-header">Party #{{ partyID$ | async }}</h3>
<div fxLayout="column" fxLayoutGap="10px">
<div fxLayout="column" fxLayoutGap="24px">
<nav mat-tab-nav-bar>
<a
mat-tab-link
@ -9,7 +9,7 @@
[routerLink]="link.url"
routerLinkActive
#rla="routerLinkActive"
[active]="rla.isActive"
[active]="rla.isActive || (hasClaimID$ | async)"
>
{{ link.name }}
</a>

View File

@ -1,8 +1,8 @@
.party-container {
max-width: 900px;
margin: 20px auto;
max-width: 936px;
margin: 24px auto;
}
router-outlet {
margin-bottom: 0;
margin-bottom: 0 !important;
}

View File

@ -1,21 +1,25 @@
import { Component, OnInit } from '@angular/core';
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { map, pluck, shareReplay } from 'rxjs/operators';
import { SHARE_REPLAY_CONF } from '../../shared/share-replay-conf';
@Component({
templateUrl: 'party.component.html',
styleUrls: ['party.component.scss'],
})
export class PartyComponent implements OnInit {
export class PartyComponent {
links = [
{ name: 'Claims', url: 'claims' },
{ name: 'Shops', url: 'shops' },
];
partyID: string;
partyID$ = this.route.params.pipe(pluck('partyID'), shareReplay(SHARE_REPLAY_CONF));
hasClaimID$ = this.route.firstChild.params.pipe(
pluck('claimID'),
map((claimID) => !!claimID),
shareReplay(SHARE_REPLAY_CONF)
);
constructor(private route: ActivatedRoute) {}
ngOnInit(): void {
this.route.params.subscribe(({ partyID }) => (this.partyID = partyID));
}
}

View File

@ -1,6 +1,6 @@
<div class="search-claims-container" fxLayout="column">
<div class="mat-headline">Claims</div>
<div fxLayout="column" fxLayoutGap="20px">
<div class="search-claims-container" fxLayout="column" fxLayoutGap="24px">
<h1 class="cc-headline">Claims</h1>
<div fxLayout="column" fxLayoutGap="24px">
<mat-card>
<mat-card-content>
<cc-claim-search-form (valueChanges)="search($event)"></cc-claim-search-form>
@ -11,7 +11,7 @@
</mat-card>
<ng-container *ngIf="claims$ | async as claims">
<cc-empty-search-result *ngIf="claims.length === 0"></cc-empty-search-result>
<mat-card *ngIf="claims.length > 0" fxLayout="column" fxLayoutGap="20px">
<mat-card *ngIf="claims.length > 0" fxLayout="column" fxLayoutGap="18px">
<cc-search-table [claims]="claims"></cc-search-table>
<button
fxFlex="100"

View File

@ -1,4 +1,4 @@
.search-claims-container {
max-width: 890px;
margin: 20px auto;
margin: 24px auto;
}

View File

@ -3,7 +3,7 @@ table {
}
.action-cell {
width: 10px;
width: 8px;
}
.party-id {

View File

@ -0,0 +1,2 @@
export * from './settings.module';
export * from './settings.service';

View File

@ -0,0 +1,8 @@
import { NgModule } from '@angular/core';
import { SettingsService } from './settings.service';
@NgModule({
providers: [SettingsService],
})
export class SettingsModule {}

View File

@ -0,0 +1,22 @@
import { Injectable } from '@angular/core';
@Injectable()
export class SettingsService {
set(key: string, value: string) {
localStorage.setItem(this.getKeyName(key), value);
}
setAll(keyValue: { [name: string]: string }) {
for (const [k, v] of Object.entries(keyValue)) {
this.set(k, v);
}
}
get(key: string): string {
return localStorage.getItem(this.getKeyName(key));
}
private getKeyName(name: string) {
return `cc-${name}`;
}
}

View File

@ -9,10 +9,10 @@
flex-direction: column;
}
/deep/.card-container > * {
::ng-deep.card-container > * {
margin-bottom: 20px;
}
/deep/.card-container > *:last-child {
::ng-deep.card-container > *:last-child {
margin-bottom: 0;
}

View File

@ -3,6 +3,6 @@ import { Component } from '@angular/core';
@Component({
selector: 'cc-card-container',
templateUrl: 'card-container.component.html',
styleUrls: ['card-container.component.css'],
styleUrls: ['card-container.component.scss'],
})
export class CardContainerComponent {}

View File

@ -0,0 +1 @@
export * from './card-container.component';

View File

@ -0,0 +1 @@
export * from './empty-search-result.component';

View File

@ -0,0 +1,5 @@
export * from './card-container';
export * from './details-item';
export * from './empty-search-result';
export * from './pretty-json';
export * from './timeline';

View File

@ -0,0 +1 @@
export * from './pretty-json.component';

View File

@ -0,0 +1,4 @@
import { ShareReplayConfig } from 'rxjs/internal/operators/shareReplay';
// Default share replay config
export const SHARE_REPLAY_CONF: ShareReplayConfig = { bufferSize: 1, refCount: true };

View File

@ -4,9 +4,11 @@ import { FlexModule } from '@angular/flex-layout';
import { MatCardModule } from '@angular/material/card';
import { PrettyJsonModule } from 'angular2-prettyjson';
import { CardContainerComponent } from './components/card-container/card-container.component';
import { EmptySearchResultComponent } from './components/empty-search-result/empty-search-result.component';
import { PrettyJsonComponent } from './components/pretty-json/pretty-json.component';
import {
CardContainerComponent,
EmptySearchResultComponent,
PrettyJsonComponent,
} from './components';
import {
ClaimSourcePipe,
ClaimStatusPipe,

18
src/app/styles/core.scss Normal file
View File

@ -0,0 +1,18 @@
@import '~@angular/material/prebuilt-themes/indigo-pink.css';
@import '~@angular/material/theming';
@import './overrides/body';
@import './utils/typography';
@mixin cc-override() {
@include cc-body-override();
}
@mixin cc-typography($config) {
@include mat-core($config);
@include cc-base-typography($config);
}
@include cc-override();
@include cc-typography(cc-typography-config());

View File

@ -0,0 +1,5 @@
@mixin cc-body-override() {
body {
margin: 0;
}
}

View File

@ -0,0 +1,14 @@
@import '~@angular/material/theming';
@import '../../sections/party-claim/party-claim-theme';
@import '../../sections/party/party-theme';
@import '../../sections/party-claim/send-comment/send-comment-theme';
@mixin cc-theme($theme) {
body.#{map-get($theme, name)} {
@include angular-material-theme($theme);
@include cc-party-claim-theme($theme);
@include cc-party-theme($theme);
@include cc-send-comment-theme($theme);
}
}

View File

@ -0,0 +1,22 @@
@import '~@angular/material/theming';
@import './theme';
body.light {
background-color: mat-color($mat-gray, 50);
}
$theme: (
primary: mat-palette($mat-indigo),
accent: mat-palette($mat-pink),
warn: mat-palette($mat-red),
is-dark: false,
foreground: $mat-light-theme-foreground,
background: $mat-light-theme-background,
/*
* Custom
*
*/ name: 'light',
gray: mat-palette($mat-grey),
);
@include cc-theme($theme);

View File

@ -0,0 +1,136 @@
@import '~@angular/material/theming';
@function cc-typography-config(
$font-family: 'Roboto, "Helvetica Neue", sans-serif',
$display-4: mat-typography-level(112px, 112px, 300, $letter-spacing: -0.05em),
$display-3: mat-typography-level(56px, 56px, 400, $letter-spacing: -0.02em),
$display-2: mat-typography-level(45px, 48px, 400, $letter-spacing: -0.005em),
$display-1: mat-typography-level(34px, 40px, 400),
$headline: mat-typography-level(24px, 32px, 400),
$title: mat-typography-level(20px, 32px, 500),
$subheading-2: mat-typography-level(16px, 28px, 400),
$subheading-1: mat-typography-level(15px, 24px, 400),
$body-2: mat-typography-level(14px, 24px, 500),
$body-1: mat-typography-level(14px, 20px, 400),
$caption: mat-typography-level(12px, 20px, 400),
$button: mat-typography-level(14px, 14px, 500),
$input: mat-typography-level(14px, 1.15, 400)
) {
// Declare an initial map with all of the levels.
$config: (
display-4: $display-4,
display-3: $display-3,
display-2: $display-2,
display-1: $display-1,
headline: $headline,
title: $title,
subheading-2: $subheading-2,
subheading-1: $subheading-1,
body-2: $body-2,
body-1: $body-1,
caption: $caption,
button: $button,
input: $input,
);
// Loop through the levels and set the `font-family` of the ones that don't have one to the base.
// Note that Sass can't modify maps in place, which means that we need to merge and re-assign.
@each $key, $level in $config {
@if map-get($level, font-family) == null {
$new-level: map-merge(
$level,
(
font-family: $font-family,
)
);
$config: map-merge(
$config,
(
$key: $new-level,
)
);
}
}
// Add the base font family to the config.
@return map-merge(
$config,
(
font-family: $font-family,
)
);
}
@mixin typo-truncate($width, $max-width) {
width: $width;
max-width: $max-width;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
@mixin cc-base-typography($config, $selector: '.cc-typography') {
.cc-headline,
#{$selector} h1 {
@include mat-typography-level-to-styles($config, headline);
margin: 0;
}
.cc-title,
#{$selector} h2 {
@include mat-typography-level-to-styles($config, title);
margin: 0;
}
.cc-subheading-2,
#{$selector} h3 {
@include mat-typography-level-to-styles($config, subheading-2);
margin: 0;
}
.cc-subheading-1,
#{$selector} h4 {
@include mat-typography-level-to-styles($config, subheading-1);
margin: 0;
}
.cc-body-2 {
@include mat-typography-level-to-styles($config, body-2);
}
.cc-body-1,
#{$selector} {
@include mat-typography-level-to-styles($config, body-1);
p {
margin: 0;
}
}
.cc-caption {
@include mat-typography-level-to-styles($config, caption);
}
.cc-display-4,
#{$selector} .cc-display-4 {
@include mat-typography-level-to-styles($config, display-4);
margin: 0;
}
#{$selector} .cc-display-3 {
@include mat-typography-level-to-styles($config, display-3);
margin: 0;
}
.cc-display-2,
#{$selector} .cc-display-2 {
@include mat-typography-level-to-styles($config, display-2);
margin: 0;
}
.cc-display-1,
#{$selector} .cc-display-1 {
@include mat-typography-level-to-styles($config, display-1);
margin: 0;
}
}

View File

@ -0,0 +1,3 @@
export * from './theme-manager.service';
export * from './theme-manager.module';
export * from './theme-name';

View File

@ -0,0 +1,10 @@
import { NgModule } from '@angular/core';
import { SettingsModule } from '../settings';
import { ThemeManager } from './theme-manager.service';
@NgModule({
imports: [SettingsModule],
providers: [ThemeManager],
})
export class ThemeManagerModule {}

View File

@ -0,0 +1,72 @@
import { DOCUMENT } from '@angular/common';
import { Inject, Injectable } from '@angular/core';
import { environment } from '../../environments/environment';
import { SettingsService } from '../settings';
import { ThemeName } from './theme-name';
enum Type {
JS = 'js',
CSS = 'css',
}
@Injectable()
export class ThemeManager {
private static readonly KEY = 'theme';
current: ThemeName;
private element: HTMLScriptElement | HTMLLinkElement;
constructor(private settingsService: SettingsService, @Inject(DOCUMENT) private doc: Document) {
const name = this.settingsService.get(ThemeManager.KEY);
const correctedName = this.getCorrectName(name);
this.change(correctedName);
}
change(name: ThemeName) {
this.removeCurrent();
this.set(name);
}
private getCorrectName(theme: ThemeName | string): ThemeName {
if (!Object.values<string>(ThemeName).includes(theme)) {
return this.current || ThemeName.light;
}
return theme as ThemeName;
}
private set(name: ThemeName) {
this.element = this.createElement(name);
this.doc.head.appendChild(this.element);
this.doc.body.classList.add(name);
this.settingsService.set(ThemeManager.KEY, name);
this.current = name;
}
private removeCurrent() {
if (this.doc.head.contains(this.element)) {
this.doc.head.removeChild(this.element);
}
this.doc.body.classList.remove(this.current);
}
private createElement(name: ThemeName): HTMLLinkElement | HTMLScriptElement {
const fileType: Type = environment.production ? Type.CSS : Type.JS;
const url = `themes/${name}.${fileType}`;
return fileType === Type.JS ? this.createScriptElement(url) : this.createStyleElement(url);
}
private createStyleElement(url: string): HTMLLinkElement {
const styleElement = document.createElement('link');
styleElement.href = url;
styleElement.rel = 'stylesheet';
return styleElement;
}
private createScriptElement(url: string): HTMLScriptElement {
const scriptElement = document.createElement('script');
scriptElement.src = url;
return scriptElement;
}
}

View File

@ -0,0 +1,3 @@
export enum ThemeName {
light = 'light',
}

View File

@ -1,4 +1,8 @@
import { NgModule } from '@angular/core';
@NgModule({})
import { FileStorageService } from './file-storage.service';
@NgModule({
providers: [FileStorageService],
})
export class FileStorageModule {}

View File

@ -1,6 +0,0 @@
@import '~@angular/material/prebuilt-themes/indigo-pink.css';
body {
margin: 0;
background: #fafafa;
}

View File

@ -20,8 +20,8 @@
{
"grouped-imports": true,
"groups": [
{ "name": "node_modules", "match": "^(?![.]|@dsh/)", "order": 10 },
{ "name": "project", "match": "^@dsh/", "order": 20 },
{ "name": "node_modules", "match": "^(?![.]|@cc/)", "order": 10 },
{ "name": "project", "match": "^@cc/", "order": 20 },
{ "name": "current", "match": "^[.]", "order": 30 }
]
}