Extract a single card as separate component

pull/7798/head
Henriette Dinger 5 years ago
parent 2fddef10a6
commit 7af56a66b5
  1. 6
      frontend/src/app/components/wp-card-view/styles/wp-card-view-vertical.sass
  2. 79
      frontend/src/app/components/wp-card-view/styles/wp-card-view.component.sass
  3. 80
      frontend/src/app/components/wp-card-view/wp-card-view.component.html
  4. 70
      frontend/src/app/components/wp-card-view/wp-card-view.component.ts
  5. 69
      frontend/src/app/components/wp-card-view/wp-single-card/wp-single-card.component.html
  6. 87
      frontend/src/app/components/wp-card-view/wp-single-card/wp-single-card.component.sass
  7. 110
      frontend/src/app/components/wp-card-view/wp-single-card/wp-single-card.component.ts
  8. 4
      frontend/src/app/modules/work_packages/openproject-work-packages.module.ts

@ -10,9 +10,3 @@
// independently of whether we scroll or not.
overflow-y: scroll
@include styled-scroll-bar
.wp-card
margin-top: 10px
// Take care that the shadow of the last element is still visible
&:last-of-type
margin-bottom: 3px

@ -1,86 +1,7 @@
@import 'helpers'
.wp-card
display: flex
flex-direction: column
user-select: none
width: 100%
border: 1px solid var(--widget-box-block-border-color)
border-radius: 2px
padding: 10px
position: relative
box-shadow: 1px 1px 3px 0px lightgrey
background: var(--body-background)
font-size: var(--card-font-size)
max-width: 400px
&:hover
box-shadow: 0px 0px 10px lightgrey
&.-new
padding-right: 25px
&.-checked
background-color: var(--table-row-highlighting-color)
.wp-card--content:not(.-new)
display: grid
grid-template-columns: auto 1fr auto
grid-template-rows: auto 1fr auto
grid-row-gap: 10px
grid-template-areas: "type type type" "subject subject subject" "attributeTag avatar idlink"
overflow: hidden
flex-grow: 1
.wp-card--type
grid-area: type
.wp-card--subject
grid-area: subject
max-width: 230px
@include text-shortener
white-space: normal
.wp-card--assignee
grid-area: avatar
place-self: center left
.wp-card--id
grid-area: idlink
place-self: center right
.wp-card--status
grid-area: attributeTag
max-width: 120px
margin-right: 5px
overflow: hidden
.wp-card--highlighting
width: 5px
height: 100%
position: absolute
top: 0
left: 0
border-radius: 2px 0 0 2px
.wp-inline-create-button
font-size: 0.9rem
padding-top: 1rem
text-align: center
.wp-card--inline-buttons
position: absolute
right: 0
top: 5px
opacity: 0
&.-show, .wp-card:hover &
opacity: 1
.wp-inline-create--reference-container
margin-bottom: 3rem
.wp-card--cover-image
display: block
margin: -10px -10px 10px
width: calc(100% + 10px + 10px)
max-width: calc(100% + 10px + 10px)
flex-basis: 200px
object-fit: cover

@ -8,76 +8,16 @@
</ndc-dynamic>
</div>
<div class="wp-card"
*ngFor="let wp of workPackages; trackBy:trackByHref"
[attr.data-is-new]="wp.isNew || undefined"
[attr.data-work-package-id]="wp.id"
[attr.data-class-identifier]="classIdentifier(wp)"
[ngClass]="cardClasses(wp)">
<div class="wp-card--highlighting"
[ngClass]="cardHighlightingClass(wp)">
</div>
<div class="wp-card--inline-buttons">
<a class="wp-card--inline-cancel-button -no-decoration"
*ngIf="wp.isNew || cardsRemovable"
[ngClass]="{ '-show': wp.isNew }"
[title]="text.removeCard"
(accessibleClick)="removeCard(wp)">
<op-icon icon-classes="icon icon-close"></op-icon>
</a>
<a class="-no-decoration"
*ngIf="!wp.isNew && showInfoButton"
[title]="text.detailsView"
(accessibleClick)="openSplitScreen(wp)">
<op-icon icon-classes="icon icon-info2"></op-icon>
</a>
</div>
<edit-form [resource]="wp"
[inEditMode]="wp.isNew"
*ngIf="wp.isNew">
<div class="wp-card--content -new">
<editable-attribute-field [resource]="wp"
[wrapperClasses]="'work-packages--type-selector'"
[fieldName]="'type'"
class="wp-card--type">
</editable-attribute-field>
<editable-attribute-field [resource]="wp"
fieldName="subject"
class="wp-card--subject -bold">
</editable-attribute-field>
</div>
</edit-form>
<img *ngIf="this.bcfSnapshotPath(wp)" [src]="this.bcfSnapshotPath(wp)" class="wp-card--cover-image">
<div *ngIf="!wp.isNew"
class="wp-card--content">
<span [textContent]="wpTypeAttribute(wp)"
class="wp-card--type"
[ngClass]="typeHighlightingClass(wp)"></span>
<a uiSref="work-packages.show"
[uiParams]="{workPackageId: wp.id}"
class="wp-card--id"
[ngClass]="uiStateLinkClass">
#{{wp.id}}
</a>
<span [textContent]="wpSubject(wp)"
class="wp-card--subject"></span>
<wp-status-button *ngIf="showStatusButton"
[workPackage]="wp"
class="wp-card--status">
</wp-status-button>
<user-avatar *ngIf="wp.assignee"
[user]="wp.assignee"
data-class-list="avatar-mini"
class="wp-card--assignee">
</user-avatar>
</div>
</div>
<wp-single-card *ngFor="let wp of workPackages; trackBy:trackByHref"
[workPackage]="wp"
[showInfoButton]="showInfoButton"
[showStatusButton]="showStatusButton"
[showRemoveButton]="cardsRemovable"
[highlightingMode]="highlightingMode"
[draggable]="this.canDragOutOf(wp)"
[orientation]="orientation"
(onRemove)="removeCard(wp)">
</wp-single-card>
</div>
<div *ngIf="showEmptyResultsBox && isResultEmpty">

@ -5,7 +5,6 @@ import {
Component,
ElementRef,
EventEmitter,
Inject,
Injector,
Input,
OnInit,
@ -21,8 +20,6 @@ import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {WorkPackageInlineCreateService} from "core-components/wp-inline-create/wp-inline-create.service";
import {WorkPackageCreateService} from "core-components/wp-new/wp-create.service";
import {AngularTrackingHelpers} from "core-components/angular/tracking-functions";
import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service";
import {Highlighting} from "core-components/wp-fast-table/builders/highlighting/highlighting.functions";
import {CardHighlightingMode} from "core-components/wp-fast-table/builders/highlighting/highlighting-mode.const";
import {AuthorisationService} from "core-app/modules/common/model-auth/model-auth.service";
import {StateService} from "@uirouter/core";
@ -35,7 +32,6 @@ import {WorkPackageViewSelectionService} from "core-app/modules/work_packages/ro
import {CardViewHandlerRegistry} from "core-components/wp-card-view/event-handler/card-view-handler-registry";
import {WorkPackageCardViewService} from "core-components/wp-card-view/services/wp-card-view.service";
import {WorkPackageCardDragAndDropService} from "core-components/wp-card-view/services/wp-card-drag-and-drop.service";
import {checkedClassName, uiStateLinkClass} from "core-components/wp-fast-table/builders/ui-state-link-builder";
import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service";
export type CardViewOrientation = 'horizontal'|'vertical';
@ -75,12 +71,8 @@ export class WorkPackageCardViewComponent implements OnInit, AfterViewInit {
title: this.I18n.t('js.work_packages.no_results.title'),
description: this.I18n.t('js.work_packages.no_results.description')
},
detailsView: this.I18n.t('js.button_open_details')
};
public uiStateLinkClass:string = uiStateLinkClass;
public checkedClassName:string = checkedClassName;
/** Inline create / reference properties */
public canAdd = false;
public canReference = false;
@ -162,68 +154,6 @@ export class WorkPackageCardViewComponent implements OnInit, AfterViewInit {
this.cardDragDrop.destroy();
}
public openSplitScreen(wp:WorkPackageResource) {
let classIdentifier = this.classIdentifier(wp);
this.wpTableSelection.setSelection(wp.id!, this.cardView.findRenderedCard(classIdentifier));
this.$state.go(
'work-packages.list.details',
{workPackageId: wp.id!}
);
}
public wpTypeAttribute(wp:WorkPackageResource) {
return wp.type.name;
}
public wpSubject(wp:WorkPackageResource) {
return wp.subject;
}
public isSelected(wp:WorkPackageResource):boolean {
return this.wpTableSelection.isSelected(wp.id!);
}
public classIdentifier(wp:WorkPackageResource) {
return this.cardView.classIdentifier(wp);
}
public bcfSnapshotPath(wp:WorkPackageResource) {
let vp = _.get(wp, 'bcf.viewpoints[0]');
if (vp) {
return this.pathHelper.attachmentDownloadPath(vp.id, vp.file_name);
} else {
return null;
}
}
public cardClasses(wp:WorkPackageResource) {
let classes = this.isSelected(wp) ? checkedClassName : '';
classes += this.canDragOutOf(wp) ? ' -draggable' : '';
classes += wp.isNew ? ' -new' : '';
classes += ' wp-card-' + wp.id;
return classes;
}
public cardHighlightingClass(wp:WorkPackageResource) {
return this.cardHighlighting(wp);
}
public typeHighlightingClass(wp:WorkPackageResource) {
return this.attributeHighlighting('type', wp);
}
private cardHighlighting(wp:WorkPackageResource) {
if (['status', 'priority', 'type'].includes(this.highlightingMode)) {
return Highlighting.backgroundClass(this.highlightingMode, wp[this.highlightingMode].id);
}
return '';
}
private attributeHighlighting(type:string, wp:WorkPackageResource) {
return Highlighting.inlineClass(type, wp.type.id!);
}
public get workPackages():WorkPackageResource[] {
return this.cardDragDrop.workPackages;
}

@ -0,0 +1,69 @@
<div class="wp-card"
[attr.data-is-new]="workPackage.isNew || undefined"
[attr.data-work-package-id]="workPackage.id"
[attr.data-class-identifier]="classIdentifier(workPackage)"
[ngClass]="cardClasses(workPackage)">
<div class="wp-card--highlighting"
[ngClass]="cardHighlightingClass(workPackage)">
</div>
<div class="wp-card--inline-buttons">
<a class="wp-card--inline-cancel-button -no-decoration"
*ngIf="workPackage.isNew || showRemoveButton"
[ngClass]="{ '-show': workPackage.isNew }"
[title]="text.removeCard"
(accessibleClick)="onRemoved(workPackage)">
<op-icon icon-classes="icon icon-close"></op-icon>
</a>
<a class="-no-decoration"
*ngIf="!workPackage.isNew && showInfoButton"
[title]="text.detailsView"
(accessibleClick)="openSplitScreen(workPackage)">
<op-icon icon-classes="icon icon-info2"></op-icon>
</a>
</div>
<edit-form [resource]="workPackage"
[inEditMode]="workPackage.isNew"
*ngIf="workPackage.isNew">
<div class="wp-card--content -new">
<editable-attribute-field [resource]="workPackage"
[wrapperClasses]="'work-packages--type-selector'"
[fieldName]="'type'"
class="wp-card--type">
</editable-attribute-field>
<editable-attribute-field [resource]="workPackage"
fieldName="subject"
class="wp-card--subject -bold">
</editable-attribute-field>
</div>
</edit-form>
<img *ngIf="this.bcfSnapshotPath(workPackage)" [src]="this.bcfSnapshotPath(workPackage)" class="wp-card--cover-image">
<div *ngIf="!workPackage.isNew"
class="wp-card--content">
<span [textContent]="wpTypeAttribute(workPackage)"
class="wp-card--type"
[ngClass]="typeHighlightingClass(workPackage)"></span>
<a uiSref="work-packages.show"
[uiParams]="{workPackageId: workPackage.id}"
class="wp-card--id"
[ngClass]="uiStateLinkClass">
#{{workPackage.id}}
</a>
<span [textContent]="wpSubject(workPackage)"
class="wp-card--subject"></span>
<wp-status-button *ngIf="showStatusButton"
[workPackage]="workPackage"
class="wp-card--status">
</wp-status-button>
<user-avatar *ngIf="workPackage.assignee"
[user]="workPackage.assignee"
data-class-list="avatar-mini"
class="wp-card--assignee">
</user-avatar>
</div>
</div>

@ -0,0 +1,87 @@
@import 'helpers'
.wp-card
display: flex
flex-direction: column
user-select: none
width: 100%
border: 1px solid var(--widget-box-block-border-color)
border-radius: 2px
padding: 10px
position: relative
box-shadow: 1px 1px 3px 0px lightgrey
background: var(--body-background)
font-size: var(--card-font-size)
max-width: 400px
&:hover
box-shadow: 0px 0px 10px lightgrey
&.-new
padding-right: 25px
&.-checked
background-color: var(--table-row-highlighting-color)
&.-horizontal
height: 100%
&.-vertical
margin-top: 10px
// Take care that the shadow of the last element is still visible
&:last-of-type
margin-bottom: 3px
.wp-card--content:not(.-new)
display: grid
grid-template-columns: auto 1fr auto
grid-template-rows: auto 1fr auto
grid-row-gap: 10px
grid-template-areas: "type type type" "subject subject subject" "attributeTag avatar idlink"
overflow: hidden
flex-grow: 1
.wp-card--type
grid-area: type
.wp-card--subject
grid-area: subject
max-width: 230px
@include text-shortener
white-space: normal
.wp-card--assignee
grid-area: avatar
place-self: center left
.wp-card--id
grid-area: idlink
place-self: center right
.wp-card--status
grid-area: attributeTag
max-width: 120px
margin-right: 5px
overflow: hidden
.wp-card--highlighting
width: 5px
height: 100%
position: absolute
top: 0
left: 0
border-radius: 2px 0 0 2px
.wp-card--inline-buttons
position: absolute
right: 0
top: 5px
opacity: 0
&.-show, .wp-card:hover &
opacity: 1
.wp-card--cover-image
display: block
margin: -10px -10px 10px
width: calc(100% + 10px + 10px)
max-width: calc(100% + 10px + 10px)
flex-basis: 200px
object-fit: cover

@ -0,0 +1,110 @@
import {ChangeDetectionStrategy, Component, EventEmitter, Input, Output} from "@angular/core";
import {WorkPackageResource} from "core-app/modules/hal/resources/work-package-resource";
import {checkedClassName, uiStateLinkClass} from "core-components/wp-fast-table/builders/ui-state-link-builder";
import {PathHelperService} from "core-app/modules/common/path-helper/path-helper.service";
import {Highlighting} from "core-components/wp-fast-table/builders/highlighting/highlighting.functions";
import {StateService} from "@uirouter/core";
import {WorkPackageViewSelectionService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-selection.service";
import {WorkPackageCardViewService} from "core-components/wp-card-view/services/wp-card-view.service";
import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {CardHighlightingMode} from "core-components/wp-fast-table/builders/highlighting/highlighting-mode.const";
import {CardViewOrientation} from "core-components/wp-card-view/wp-card-view.component";
@Component({
selector: 'wp-single-card',
styleUrls: ['./wp-single-card.component.sass'],
templateUrl: './wp-single-card.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class WorkPackageSingleCardComponent {
@Input() public workPackage:WorkPackageResource;
@Input() public showInfoButton:boolean = false;
@Input() public showStatusButton:boolean = true;
@Input() public showRemoveButton:boolean = false;
@Input() public highlightingMode:CardHighlightingMode = 'inline';
@Input() public draggable:boolean = false;
@Input() public orientation:CardViewOrientation = 'vertical';
@Output() public onRemove = new EventEmitter<WorkPackageResource>();
public uiStateLinkClass:string = uiStateLinkClass;
public text = {
removeCard: this.I18n.t('js.card.remove_from_list'),
detailsView: this.I18n.t('js.button_open_details')
};
constructor(readonly pathHelper:PathHelperService,
readonly I18n:I18nService,
readonly $state:StateService,
readonly wpTableSelection:WorkPackageViewSelectionService,
readonly cardView:WorkPackageCardViewService,) {
}
public classIdentifier(wp:WorkPackageResource) {
return this.cardView.classIdentifier(wp);
}
public openSplitScreen(wp:WorkPackageResource) {
let classIdentifier = this.classIdentifier(wp);
this.wpTableSelection.setSelection(wp.id!, this.cardView.findRenderedCard(classIdentifier));
this.$state.go(
'work-packages.list.details',
{workPackageId: wp.id!}
);
}
public cardClasses(wp:WorkPackageResource) {
let classes = this.isSelected(wp) ? checkedClassName : '';
classes += this.draggable ? ' -draggable' : '';
classes += wp.isNew ? ' -new' : '';
classes += ' wp-card-' + wp.id;
classes += ' -' + this.orientation;
return classes;
}
public isSelected(wp:WorkPackageResource):boolean {
return this.wpTableSelection.isSelected(wp.id!);
}
public wpTypeAttribute(wp:WorkPackageResource) {
return wp.type.name;
}
public wpSubject(wp:WorkPackageResource) {
return wp.subject;
}
public cardHighlightingClass(wp:WorkPackageResource) {
return this.cardHighlighting(wp);
}
public typeHighlightingClass(wp:WorkPackageResource) {
return this.attributeHighlighting('type', wp);
}
public onRemoved(wp:WorkPackageResource) {
this.onRemove.emit(wp);
}
public bcfSnapshotPath(wp:WorkPackageResource) {
let vp = _.get(wp, 'bcf.viewpoints[0]');
if (vp) {
return this.pathHelper.attachmentDownloadPath(vp.id, vp.file_name);
} else {
return null;
}
}
private cardHighlighting(wp:WorkPackageResource) {
if (['status', 'priority', 'type'].includes(this.highlightingMode)) {
return Highlighting.backgroundClass(this.highlightingMode, wp[this.highlightingMode].id);
}
return '';
}
private attributeHighlighting(type:string, wp:WorkPackageResource) {
return Highlighting.inlineClass(type, wp.type.id!);
}
}

@ -162,6 +162,7 @@ import {WorkPackageNotificationService} from "core-app/modules/work_packages/not
import {WorkPackageEditActionsBarComponent} from "core-app/modules/common/edit-actions-bar/wp-edit-actions-bar.component";
import {HalResource} from "core-app/modules/hal/resources/hal-resource";
import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset";
import {WorkPackageSingleCardComponent} from "core-components/wp-card-view/wp-single-card/wp-single-card.component";
@NgModule({
@ -375,6 +376,7 @@ import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changes
// Card view
WorkPackageCardViewComponent,
WorkPackageSingleCardComponent,
WorkPackageViewToggleButton,
],
entryComponents: [
@ -451,6 +453,7 @@ import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changes
// Card view
WorkPackageCardViewComponent,
WorkPackageSingleCardComponent,
CustomDateActionAdminComponent,
],
@ -460,6 +463,7 @@ import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changes
WorkPackageEmbeddedTableComponent,
WorkPackageEmbeddedTableEntryComponent,
WorkPackageCardViewComponent,
WorkPackageSingleCardComponent,
WorkPackageFilterButtonComponent,
WorkPackageFilterContainerComponent,
WorkPackageIsolatedQuerySpaceDirective,

Loading…
Cancel
Save