Merge pull request #4708 from GabrielInTheWorld/tile-layout

Adds a new view with tiles to the motion-list
This commit is contained in:
Emanuel Schütze 2019-05-27 22:47:24 +02:00 committed by GitHub
commit 8bf1dad10e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1019 additions and 119 deletions

View File

@ -0,0 +1,57 @@
<mat-card
class="block-tile"
[style.display]="orientation === 'horizontal' ? 'flex' : 'block'"
(click)="onClick($event)"
>
<div [ngSwitch]="blockType" class="block-node-container">
<div *ngSwitchCase="'text'" class="tile-text stretch-to-fill-parent" [style.border-radius]="orientation === 'horizontal' ? '4px 0 0 4px' : '4px 4px 0 0'">
<table>
<tbody>
<tr>
<td>
{{ block }}
</td>
</tr>
</tbody>
</table>
</div>
<div *ngSwitchCase="'image'">
<img mat-card-image [src]="block" alt="" />
</div>
<div *ngSwitchCase="'node'" class="tile-text stretch-to-fill-parent" [style.border-radius]="orientation === 'horizontal' ? '4px 0 0 4px' : '4px 4px 0 0'">
<ng-container
[ngTemplateOutlet]="blockNode"
[ngTemplateOutletContext]="data"></ng-container>
</div>
</div>
<div class="tile-content-node-container">
<mat-card-content class="tile-content">
<mat-card-title class="tile-content-title stretch-to-fill-parent" *ngIf="!only || only === 'title'">
{{ title }}
</mat-card-title>
<mat-card-subtitle class="tile-content-subtitle" *ngIf="subtitle">
{{ subtitle }}
</mat-card-subtitle>
<mat-divider *ngIf="!only"></mat-divider>
<div *ngIf="!only || only === 'content'" class="tile-content-extra">
<ng-container
[ngTemplateOutlet]="contentNode"
[ngTemplateOutletContext]="data"></ng-container>
</div>
<mat-card-actions *ngIf="showActions">
<ng-container
[ngTemplateOutlet]="actionNode"></ng-container>
</mat-card-actions>
</mat-card-content>
</div>
</mat-card>
<ng-template #blockNode>
<ng-content select=".block-node"></ng-content>
</ng-template>
<ng-template #contentNode>
<ng-content select=".block-content-node"></ng-content>
</ng-template>
<ng-template #actionNode>
<ng-content select=".block-action-node"></ng-content>
</ng-template>

View File

@ -0,0 +1,60 @@
@import '~@angular/material/theming';
@mixin os-block-tile-style($theme) {
$primary: map-get(
$map: $theme,
$key: primary
);
.block-tile {
padding: 0;
.block-node-container {
position: relative;
padding-bottom: 50%;
min-width: 30%;
.tile-text {
padding: 8px 16px;
background-color: mat-color($primary, lighter);
table {
height: 100%;
width: 100%;
text-align: center;
font-size: 24px;
font-weight: 500;
}
}
}
.tile-content-node-container {
position: relative;
width: 100%;
margin: 8px 16px !important;
.tile-content {
margin-bottom: 0;
height: 100%;
.tile-content-title {
font-size: 20px;
font-weight: unset;
margin-bottom: 0;
overflow: hidden;
}
}
.tile-content-extra {
padding-top: 8px;
}
}
&:hover {
cursor: pointer;
box-shadow: 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 5px 8px 0px rgba(0, 0, 0, 0.14),
0px 1px 14px 0px rgba(0, 0, 0, 0.12) !important;
}
}
}

View File

@ -0,0 +1,26 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { BlockTileComponent } from './block-tile.component';
import { E2EImportsModule } from 'e2e-imports.module';
describe('BlockTileComponent', () => {
let component: BlockTileComponent;
let fixture: ComponentFixture<BlockTileComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule]
// declarations: [BlockTileComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BlockTileComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,115 @@
import { Component, TemplateRef, ContentChild, Input } from '@angular/core';
import { TileComponent } from '../tile/tile.component';
/**
* Enumeration to define if the content is only text or a node.
*/
export enum ContentType {
text = 'text',
node = 'node'
}
/**
* Enumeration to define of which the big block is.
*/
export enum BlockType {
text = 'text',
node = 'node',
picture = 'picture'
}
/**
* Tells, whether to align the block and content next to each other or one below the other.
*/
export enum Orientation {
horizontal = 'horizontal',
vertical = 'vertical'
}
/**
* Tells, if the tile should only display the content or the title in the content part.
*/
export enum ShowOnly {
title = 'title',
content = 'content'
}
/**
* Class, that extends the `tile.component`.
* This class specifies a tile with two separated parts: the block and the content part.
* The block part is like a header, the content part contains further information.
*/
@Component({
selector: 'os-block-tile',
templateUrl: './block-tile.component.html',
styleUrls: ['./block-tile.component.scss']
})
export class BlockTileComponent extends TileComponent {
/**
* Reference to the content of the content part.
*/
@ContentChild(TemplateRef)
public contentNode: TemplateRef<any>;
/**
* Reference to the block part, if it is a node.
*/
@ContentChild(TemplateRef)
public blockNode: TemplateRef<any>;
/**
* Reference to the action buttons in the content part, if used.
*/
@ContentChild(TemplateRef)
public actionNode: TemplateRef<any>;
/**
* Defines the type of the primary block.
*/
@Input()
public blockType: BlockType;
/**
* Input for the primary block content.
* Only string for the source of a picture or text.
*/
@Input()
public block: string;
/**
* Defines the type of the content.
*/
@Input()
public contentType: ContentType;
/**
* The title in the content part.
*/
@Input()
public title: string;
/**
* The subtitle in the content part.
*/
@Input()
public subtitle: string;
/**
* Tells the orientation -
* whether the block part should be displayed above the content or next to it.
*/
@Input()
public orientation: Orientation;
/**
* Tells, whether the tile should display only one of `Title` or `Content` in the content part.
*/
@Input()
public only: ShowOnly;
/**
* Boolean, whether to show action buttons in the content part.
*/
@Input()
public showActions: boolean;
}

View File

@ -0,0 +1,15 @@
<div
*ngIf="title"
class="heading-container"
[ngClass]="{'no-space': noSpace}"
>
<h3>{{ title }}</h3>
<mat-divider></mat-divider>
</div>
<div
class="os-grid"
[ngClass]="{'no-space': noSpace}"
>
<ng-content></ng-content>
</div>

View File

@ -0,0 +1,17 @@
.heading-container {
padding: 0 16px;
}
.heading-container.no-space {
padding: 0;
}
.os-grid {
padding: 8px;
display: flex;
flex-flow: row wrap;
}
.os-grid.no-space {
padding: 0;
}

View File

@ -0,0 +1,26 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { GridLayoutComponent } from './grid-layout.component';
import { E2EImportsModule } from 'e2e-imports.module';
describe('GridLayoutComponent', () => {
let component: GridLayoutComponent;
let fixture: ComponentFixture<GridLayoutComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule]
// declarations: [GridLayoutComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(GridLayoutComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,25 @@
import { Component, Input } from '@angular/core';
/**
* Component to create a `grid-layout`.
* Aligns items in a flex display.
*/
@Component({
selector: 'os-grid-layout',
templateUrl: './grid-layout.component.html',
styleUrls: ['./grid-layout.component.scss']
})
export class GridLayoutComponent {
/**
* Property for an optional title.
*/
@Input()
public title: string;
/**
* If the grid layout should have no space.
* This contains the padding for the grid itself and the margin of the tiles.
*/
@Input()
public noSpace: boolean;
}

View File

@ -20,6 +20,10 @@
</div>
</div>
<div>
<span class="extra-controls-wrapper">
<ng-content select=".extra-controls-slot"></ng-content>
</span>
<button mat-button *ngIf="hasFilters" (click)="filterMenu.opened ? filterMenu.close() : filterMenu.open()">
<span *ngIf="!filterService.activeFilterCount" class="upper" translate> Filter </span>
<span *ngIf="filterService.activeFilterCount">

View File

@ -0,0 +1,4 @@
<ng-container
[ngTemplateOutlet]="tileContext"
[ngTemplateOutletContext]="data">
</ng-container>

View File

@ -0,0 +1,94 @@
@import '~@angular/material/theming';
@import '../../../../assets/styles/media-queries.scss';
@mixin os-tile-style($theme) {
$primary: map-get($theme, primary);
@include set-breakpoint-lower(xs) {
@for $i from 1 through 4 {
.os-tile--xs-#{$i} {
width: get-width('xs', $i, true);
}
.os-grid.no-space > .os-tile--xs-#{$i} {
width: get-width('xs', $i, false);
}
}
}
@include set-breakpoint-between(xs, sm) {
@for $i from 1 through 8 {
.os-tile--sm-#{$i} {
width: get-width('sm', $i, true);
}
.os-grid.no-space > .os-tile--sm-#{$i} {
width: get-width('sm', $i, false);
}
}
}
@include set-breakpoint-between(sm, lg) {
@for $i from 1 through 12 {
.os-tile--md-#{$i} {
width: get-width('md', $i, true);
}
.os-grid.no-space > .os-tile--md-#{$i} {
width: get-width('md', $i, false);
}
}
}
@include set-breakpoint-upper(lg) {
@for $i from 1 through 16 {
.os-tile--lg-#{$i} {
width: get-width('lg', $i, true);
}
.os-grid.no-space > .os-tile--lg-#{$i} {
width: get-width('md', $i, false);
}
}
}
.os-tile {
height: calc(100% - 16px);
margin: 8px;
}
.os-grid.no-space > .os-tile {
height: 100%;
margin: 0;
}
}
/* This function calculates the width to the given device-size */
@function get-width($property, $size, $space) {
@if $property == 'xs' {
@if $space == true {
@return calc(#{$size} / 4 * 100% - 16px);
} @else {
@return $size / 4 * 100%;
}
}
@if $property == 'sm' {
@if $space == true {
@return calc(#{$size} / 8 * 100% - 16px);
} @else {
@return $size / 8 * 100%;
}
}
@if $property == 'md' {
@if $space == true {
@return calc(#{$size} / 12 * 100% - 16px);
} @else {
@return $size / 12 * 100%;
}
}
@if $property == 'lg' {
@if $space == true {
@return calc(#{$size} / 16 * 100% - 16px);
} @else {
@return $size / 16 * 100%;
}
}
}

View File

@ -0,0 +1,24 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TileComponent } from './tile.component';
describe('TileComponent', () => {
let component: TileComponent;
let fixture: ComponentFixture<TileComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [TileComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TileComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,185 @@
import { Component, OnInit, Input, TemplateRef, ContentChild, Output, EventEmitter, HostBinding } from '@angular/core';
/**
* Interface, that defines the type of the `ClickEvent`.
*/
export interface EventObject {
data: Object;
source: MouseEvent;
}
/**
* Interface, that defines the possible shape of `@Input() preferredSize`
*/
export interface SizeObject {
mobile?: number;
tablet?: number;
medium?: number;
large?: number;
}
/**
* Component, that acts like a tile in a grid-layout.
* This component accepts several attributes, that define the size of it.
*/
@Component({
selector: 'os-tile',
templateUrl: './tile.component.html',
styleUrls: ['./tile.component.scss']
})
export class TileComponent implements OnInit {
/**
* HostBinding to add the necessary classes to the host element `os-tile`.
*/
@HostBinding('class')
public get classes(): string {
return (
'os-tile' +
' os-tile--xs-' +
this.mobileSize +
' os-tile--sm-' +
this.tabletSize +
' os-tile--md-' +
this.mediumSize +
' os-tile--lg-' +
this.largeSize
);
}
/**
* Reference to the dynamic content.
*/
@ContentChild(TemplateRef)
public tileContext: TemplateRef<any>;
/**
* Optional data, that can be passed to the component.
*/
@Input()
public data: Object;
/**
* Optional input to define the preferred size of the tile.
* This can be a number, which defines the size in every device resolution,
* or an object, which defines the size for each one resolution separately.
*/
@Input()
public preferredSize: number | SizeObject;
/**
* EventEmitter for the `ClickEvent`.
*/
@Output()
public clicked: EventEmitter<EventObject> = new EventEmitter<EventObject>();
/**
* Property, which defines the size of the tile @Mobile
*/
public mobileSize: number;
/**
* Property, which defines the size of the tile @Tablet
*/
public tabletSize: number;
/**
* Property, which defines the size of the tile @Medium devices
*/
public mediumSize: number;
/**
* Property, which defines the size of the tile @Large devices
*/
public largeSize: number;
/**
* OnInit method.
* The preferred size for the tile will calculated.
*/
public ngOnInit(): void {
if (!this.preferredSize) {
this.preferredSize = 4;
}
if (typeof this.preferredSize === 'number') {
this.setLargeSize(this.preferredSize);
this.setMediumSize(this.preferredSize);
this.setTabletSize(this.preferredSize);
this.setMobileSize(this.preferredSize);
} else {
const {
mobile,
tablet,
medium,
large
}: { mobile?: number; tablet?: number; medium?: number; large?: number } = this.preferredSize;
this.setMobileSize(mobile);
this.setTabletSize(tablet);
this.setMediumSize(medium);
this.setLargeSize(large);
}
}
/**
* Function, that fires when the user clicks on the tile.
*
* @param event The source event on click.
*/
public onClick(event: MouseEvent): void {
this.clicked.emit({
data: this.data,
source: event
});
}
/**
* Function to set the size @Mobile
*
* @param size how great the tile should be
*/
private setMobileSize(size: number): void {
if (size <= 4 && size >= 0) {
this.mobileSize = size;
} else {
this.mobileSize = 4;
}
}
/**
* Function to set the size @Tablet
*
* @param size how great the tile should be
*/
private setTabletSize(size: number): void {
if (size <= 8 && size >= 0) {
this.tabletSize = size;
} else {
this.tabletSize = 8;
}
}
/**
* Function to set the size of the tile @Medium devices
*
* @param size how great the tile should be
*/
private setMediumSize(size: number): void {
if (size <= 12 && size >= 0) {
this.mediumSize = size;
} else {
this.mediumSize = 12;
}
}
/**
* Function to set the size of the tile @Large devices
*
* @param size how great the tile should be
*/
private setLargeSize(size: number): void {
if (size <= 16 && size >= 0) {
this.largeSize = size;
} else {
this.largeSize = 16;
}
}
}

View File

@ -28,7 +28,8 @@ import {
MatStepperModule,
MatTabsModule,
MatBottomSheetModule,
MatSliderModule
MatSliderModule,
MatDividerModule
} from '@angular/material';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatChipsModule } from '@angular/material';
@ -84,6 +85,9 @@ import { CountdownTimeComponent } from './components/contdown-time/countdown-tim
import { MediaUploadContentComponent } from './components/media-upload-content/media-upload-content.component';
import { PrecisionPipe } from './pipes/precision.pipe';
import { SpeakerButtonComponent } from './components/speaker-button/speaker-button.component';
import { GridLayoutComponent } from './components/grid-layout/grid-layout.component';
import { TileComponent } from './components/tile/tile.component';
import { BlockTileComponent } from './components/block-tile/block-tile.component';
/**
* Share Module for all "dumb" components and pipes.
@ -133,6 +137,7 @@ import { SpeakerButtonComponent } from './components/speaker-button/speaker-butt
MatStepperModule,
MatTabsModule,
MatSliderModule,
MatDividerModule,
OwlDateTimeModule,
OwlNativeDateTimeModule,
DragDropModule,
@ -176,6 +181,7 @@ import { SpeakerButtonComponent } from './components/speaker-button/speaker-butt
MatButtonToggleModule,
MatStepperModule,
MatSliderModule,
MatDividerModule,
DragDropModule,
NgxMatSelectSearchModule,
FileDropModule,
@ -207,8 +213,11 @@ import { SpeakerButtonComponent } from './components/speaker-button/speaker-butt
CountdownTimeComponent,
MediaUploadContentComponent,
PrecisionPipe,
ScrollingModule,
SpeakerButtonComponent
SpeakerButtonComponent,
GridLayoutComponent,
TileComponent,
BlockTileComponent,
ScrollingModule
],
declarations: [
PermsDirective,
@ -237,7 +246,10 @@ import { SpeakerButtonComponent } from './components/speaker-button/speaker-butt
CountdownTimeComponent,
MediaUploadContentComponent,
PrecisionPipe,
SpeakerButtonComponent
SpeakerButtonComponent,
GridLayoutComponent,
TileComponent,
BlockTileComponent
],
providers: [
{ provide: DateAdapter, useClass: OpenSlidesDateAdapter },

View File

@ -23,116 +23,122 @@
[sortService]="sortService"
(searchFieldChange)="searchFilter($event)"
>
<mat-button-toggle-group *ngIf="isCategoryAvailable()" #group="matButtonToggleGroup" [value]="selectedView" (change)="onChangeView(group.value)" appearance="legacy" aria-label="Select view" class="extra-controls-slot select-view-wrapper">
<mat-button-toggle value="tiles" matTooltip="{{ 'Tile view' | translate }}"><mat-icon>view_module</mat-icon></mat-button-toggle>
<mat-button-toggle value="list" matTooltip="{{ 'List view' | translate }}"><mat-icon>view_headline</mat-icon></mat-button-toggle>
</mat-button-toggle-group>
</os-sort-filter-bar>
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
<!-- Selector column -->
<ng-container matColumnDef="selector">
<mat-header-cell *matHeaderCellDef mat-sort-header></mat-header-cell>
<mat-cell *matCellDef="let motion">
<mat-icon>{{ isSelected(motion) ? 'check_circle' : '' }}</mat-icon>
</mat-cell>
</ng-container>
<div [ngSwitch]="selectedView">
<span *ngSwitchCase="'list'">
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
<!-- Selector column -->
<ng-container matColumnDef="selector">
<mat-header-cell *matHeaderCellDef mat-sort-header></mat-header-cell>
<mat-cell *matCellDef="let motion">
<mat-icon>{{ isSelected(motion) ? 'check_circle' : '' }}</mat-icon>
</mat-cell>
</ng-container>
<!-- Projector column -->
<ng-container matColumnDef="projector">
<mat-header-cell *matHeaderCellDef mat-sort-header>Projector</mat-header-cell>
<mat-cell *matCellDef="let motion">
<os-projector-button [object]="motion"></os-projector-button>
</mat-cell>
</ng-container>
<!-- Projector column -->
<ng-container matColumnDef="projector">
<mat-header-cell *matHeaderCellDef mat-sort-header>Projector</mat-header-cell>
<mat-cell *matCellDef="let motion">
<os-projector-button [object]="motion"></os-projector-button>
</mat-cell>
</ng-container>
<!-- identifier column -->
<ng-container matColumnDef="identifier">
<mat-header-cell *matHeaderCellDef mat-sort-header>Identifier</mat-header-cell>
<mat-cell *matCellDef="let motion">
<div class="innerTable">
{{ motion.identifier }}
</div>
</mat-cell>
</ng-container>
<!-- title column -->
<ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef mat-sort-header>Title</mat-header-cell>
<mat-cell *matCellDef="let motion">
<div class="innerTable max-width">
<!-- title line -->
<div class="title-line ellipsis-overflow">
<!-- favorite icon -->
<span *ngIf="motion.star" class="favorite-star">
<mat-icon inline>star</mat-icon>
</span>
<!-- attachment icon -->
<span class="attached-files" *ngIf="motion.hasAttachments()">
<mat-icon>attach_file</mat-icon>
</span>
<!-- title -->
<span class="motion-list-title">
{{ motion.title }}
</span>
</div>
<!-- submitters line -->
<div class="submitters-line ellipsis-overflow" *ngIf="motion.submitters.length">
<span translate>by</span> {{ motion.submitters }}
<span *osPerms="'motions.can_manage'">
&middot;
<span translate>Sequential number</span>
{{ motion.id }}
</span>
</div>
<!-- state line-->
<div class="ellipsis-overflow white">
<mat-basic-chip *ngIf="motion.state" [ngClass]="motion.stateCssColor" [disabled]="true">
{{ getStateLabel(motion) }}
</mat-basic-chip>
</div>
<!-- recommendation line -->
<div
*ngIf="motion.recommendation && motion.state.next_states_id.length > 0"
class="ellipsis-overflow white spacer-top-3"
>
<mat-basic-chip class="bluegrey" [disabled]="true">
{{ getRecommendationLabel(motion) }}
</mat-basic-chip>
</div>
</div>
</mat-cell>
</ng-container>
<!-- state column -->
<ng-container matColumnDef="state">
<mat-header-cell *matHeaderCellDef mat-sort-header>State</mat-header-cell>
<mat-cell (click)="openEditInfo(motion, $event)" *matCellDef="let motion">
<div class="fill">
<div class="innerTable state-column">
<div class="small ellipsis-overflow" *ngIf="motion.category">
<mat-icon>device_hub</mat-icon>
{{ motion.category }}
<!-- identifier column -->
<ng-container matColumnDef="identifier">
<mat-header-cell *matHeaderCellDef mat-sort-header>Identifier</mat-header-cell>
<mat-cell *matCellDef="let motion">
<div class="innerTable">
{{ motion.identifier }}
</div>
<div class="small ellipsis-overflow" *ngIf="motion.motion_block">
<mat-icon>widgets</mat-icon>
{{ motion.motion_block.title }}
</div>
<div class="small ellipsis-overflow" *ngIf="motion.tags && motion.tags.length">
<mat-icon>local_offer</mat-icon>
<span *ngFor="let tag of motion.tags; let last = last">
{{ tag.getTitle() }}
<span *ngIf="!last">,&nbsp;</span>
</span>
</div>
</div>
</div>
</mat-cell>
</ng-container>
</mat-cell>
</ng-container>
<!-- Anchor column to open the separate tab -->
<ng-container matColumnDef="anchor">
<mat-header-cell *matHeaderCellDef></mat-header-cell>
<mat-cell *matCellDef="let motion">
<a [routerLink]="motion.id" *ngIf="!isMultiSelect"></a>
</mat-cell>
</ng-container>
<!-- title column -->
<ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef mat-sort-header>Title</mat-header-cell>
<mat-cell *matCellDef="let motion">
<div class="innerTable max-width">
<!-- title line -->
<div class="title-line ellipsis-overflow">
<!-- favorite icon -->
<span *ngIf="motion.star" class="favorite-star">
<mat-icon inline>star</mat-icon>
</span>
<!-- attachment icon -->
<span class="attached-files" *ngIf="motion.hasAttachments()">
<mat-icon>attach_file</mat-icon>
</span>
<!-- title -->
<span class="motion-list-title">
{{ motion.title }}
</span>
</div>
<!-- submitters line -->
<div class="submitters-line ellipsis-overflow" *ngIf="motion.submitters.length">
<span translate>by</span> {{ motion.submitters }}
<span *osPerms="'motions.can_manage'">
&middot;
<span translate>Sequential number</span>
{{ motion.id }}
</span>
</div>
<!-- state line-->
<div class="ellipsis-overflow white">
<mat-basic-chip *ngIf="motion.state" [ngClass]="motion.stateCssColor" [disabled]="true">
{{ getStateLabel(motion) }}
</mat-basic-chip>
</div>
<!-- recommendation line -->
<div
*ngIf="motion.recommendation && motion.state.next_states_id.length > 0"
class="ellipsis-overflow white"
>
<mat-basic-chip class="bluegrey" [disabled]="true">
{{ getRecommendationLabel(motion) }}
</mat-basic-chip>
</div>
</div>
</mat-cell>
</ng-container>
<!-- state column -->
<ng-container matColumnDef="state">
<mat-header-cell *matHeaderCellDef mat-sort-header>State</mat-header-cell>
<mat-cell (click)="openEditInfo(motion, $event)" *matCellDef="let motion">
<div class="fill">
<div class="innerTable state-column">
<div class="small ellipsis-overflow" *ngIf="motion.category">
<mat-icon>device_hub</mat-icon>
{{ motion.category }}
</div>
<div class="small ellipsis-overflow" *ngIf="motion.motion_block">
<mat-icon>widgets</mat-icon>
{{ motion.motion_block.title }}
</div>
<div class="small ellipsis-overflow" *ngIf="motion.tags && motion.tags.length">
<mat-icon>local_offer</mat-icon>
<span *ngFor="let tag of motion.tags; let last = last">
{{ tag.getTitle() }}
<span *ngIf="!last">,&nbsp;</span>
</span>
</div>
</div>
</div>
</mat-cell>
</ng-container>
<!-- Anchor column to open the separate tab -->
<ng-container matColumnDef="anchor">
<mat-header-cell *matHeaderCellDef></mat-header-cell>
<mat-cell *matCellDef="let motion">
<a [routerLink]="motion.id" *ngIf="!isMultiSelect"></a>
</mat-cell>
</ng-container>
<!-- Speakers column -->
<ng-container matColumnDef="speakers">
@ -142,17 +148,40 @@
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row>
<mat-row
[ngClass]="selectedRows.indexOf(row) >= 0 ? 'selected' : ''"
(click)="selectItem(row, $event)"
*matRowDef="let row; columns: getColumnDefinition()"
class="lg"
>
</mat-row>
</mat-table>
<mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row>
<mat-row
[ngClass]="selectedRows.indexOf(row) >= 0 ? 'selected' : ''"
(click)="selectItem(row, $event)"
*matRowDef="let row; columns: getColumnDefinition()"
class="lg"
>
</mat-row>
</mat-table>
<mat-paginator class="on-transition-fade" [pageSizeOptions]="pageSize"></mat-paginator>
<mat-paginator class="on-transition-fade" [pageSizeOptions]="pageSize"></mat-paginator>
</span>
<span *ngSwitchCase="'tiles'">
<os-grid-layout>
<os-block-tile *ngFor="let tileCategory of tileCategories" (clicked)="changeToViewWithTileCategory(tileCategory)" [orientation]="'horizontal'" [only]="'title'" [blockType]="'node'" [data]="tileCategory" title="{{ tileCategory.name | translate }}">
<ng-container class="block-node">
<table matTooltip="{{ tileCategory.amountOfMotions }} {{ 'Motions' | translate }} {{ tileCategory.name | translate }}">
<tbody>
<tr>
<td>
<span class="tile-block-title" [matBadge]="tileCategory.amountOfMotions" [matBadgeColor]="'accent'" [ngSwitch]="tileCategory.name">
<span *ngSwitchCase="'Favorites'"><mat-icon>star</mat-icon></span>
<span *ngSwitchCase="'No category'"><mat-icon>block</mat-icon></span>
<span *ngSwitchDefault>{{ tileCategory.prefix }}</span>
</span>
</td>
</tr>
</tbody>
</table>
</ng-container>
</os-block-tile>
</os-grid-layout>
</span>
</div>
</mat-drawer-container>
<mat-menu #motionListMenu="matMenu">

View File

@ -7,6 +7,17 @@
line-height: 150%;
}
.mat-button-toggle-group {
line-height: normal;
vertical-align: middle;
box-shadow: none;
mat-icon {
vertical-align: middle;
margin: 0;
}
}
.os-listview-table {
/** identifier */
.mat-column-identifier {
@ -72,3 +83,15 @@
.max-width {
width: 100%;
}
os-grid-layout {
.tile-block-title {
font-size: 36px;
.mat-icon {
font-size: 36px;
width: 36px;
height: 36px;
}
}
}

View File

@ -31,6 +31,16 @@ import { MotionXlsxExportService } from 'app/site/motions/services/motion-xlsx-e
import { LocalPermissionsService } from 'app/site/motions/services/local-permissions.service';
import { StorageService } from 'app/core/core-services/storage.service';
import { PdfError } from 'app/core/ui-services/pdf-document.service';
import { tap } from 'rxjs/operators';
import { Observable } from 'rxjs';
interface TileCategoryInformation {
filter: string;
name: string;
prefix?: string;
condition: number | boolean | null;
amountOfMotions: number;
}
/**
* Interface to describe possible values and changes for
@ -79,6 +89,11 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
*/
public infoDialog: InfoDialog;
/**
* String to define the current selected view.
*/
public selectedView: string;
/**
* Columns to display in table when desktop view is available
*/
@ -101,6 +116,17 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
public categories: ViewCategory[] = [];
public motionBlocks: ViewMotionBlock[] = [];
/**
* List of `TileCategoryInformation`.
* Necessary to not iterate over the values of the map below.
*/
public tileCategories: TileCategoryInformation[] = [];
/**
* Map of information about the categories relating to their id.
*/
public informationOfMotionsInTileCategories: { [id: number]: TileCategoryInformation } = {};
/**
* Constructor implements title and translation Module.
*
@ -161,9 +187,10 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
* Sets the title, inits the table, defines the filter/sorting options and
* subscribes to filter and sorting services
*/
public ngOnInit(): void {
public async ngOnInit(): Promise<void> {
super.setTitle('Motions');
this.initTable();
const storedView = await this.storage.get<string>('motionListView');
this.configService
.get<boolean>('motions_statutes_enabled')
.subscribe(enabled => (this.statutesEnabled = enabled));
@ -176,6 +203,11 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
});
this.categoryRepo.getViewModelListObservable().subscribe(cats => {
this.categories = cats;
if (cats.length > 0) {
this.selectedView = storedView || 'tiles';
} else {
this.selectedView = 'list';
}
this.updateStateColumnVisibility();
});
this.tagRepo.getViewModelListObservable().subscribe(tags => {
@ -194,6 +226,70 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
this.router.navigate(['./' + motion.id], { relativeTo: this.route });
}
/**
* Overwriting method of base-class.
* Every time this method is called, all motions are counted in their related categories.
*
* @returns {Observable<ViewMotion[]>} An observable containing the list of motions.
*/
protected getModelListObservable(): Observable<ViewMotion[]> {
return super.getModelListObservable().pipe(
tap(motions => {
this.informationOfMotionsInTileCategories = {};
for (const motion of motions) {
if (motion.star) {
this.countMotions(-1, true, 'star', 'Favorites');
}
if (motion.category_id) {
this.countMotions(
motion.category_id,
motion.category_id,
'category',
motion.category.name,
motion.category.prefix
);
} else {
this.countMotions(-2, null, 'category', 'No category');
}
}
this.tileCategories = Object.values(this.informationOfMotionsInTileCategories);
})
);
}
/**
* Function to count the motions in their related categories.
*
* @param id The key of TileCategory in `informationOfMotionsInTileCategories` object
* @param condition The condition, if the tile is selected
* @param filter The filter, if the tile is selected
* @param name The title of the tile
* @param prefix The prefix of the category
*/
private countMotions(
id: number,
condition: number | boolean | null,
filter: string,
name: string,
prefix?: string
): void {
let info = this.informationOfMotionsInTileCategories[id];
if (info) {
++info.amountOfMotions;
} else {
info = {
filter,
name,
condition,
prefix,
amountOfMotions: 1
};
}
this.informationOfMotionsInTileCategories[id] = info;
}
/**
* Get the icon to the corresponding Motion Status
* TODO Needs to be more accessible (Motion workflow needs adjustment on the server)
@ -388,6 +484,31 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion, Motio
};
}
/**
* This function saves the selected view by changes.
*
* @param value is the new view the user has selected.
*/
public onChangeView(value: string): void {
this.selectedView = value;
this.storage.set('motionListView', value);
}
/**
* This function changes the view to the list of motions where the selected category becomes the active filter.
*
* @param tileCategory information about filter and condition.
*/
public changeToViewWithTileCategory(tileCategory: TileCategoryInformation): void {
this.filterService.clearAllFilters();
this.filterService.toggleFilterOption(tileCategory.filter, {
label: tileCategory.name,
condition: tileCategory.condition,
isActive: false
});
this.onChangeView('list');
}
/**
* Opens a dialog to edit some meta information about a motion.
*

View File

@ -93,4 +93,12 @@
input[readonly] {
cursor: default;
}
.stretch-to-fill-parent {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
}

View File

@ -0,0 +1,51 @@
$breakpoints: (
xs: 599px,
sm: 959px,
md: 1199px,
lg: 1599px
);
@mixin set-breakpoint-lower($breakpoint) {
@if map-has-key($map: $breakpoints, $key: $breakpoint) {
// Get the value from breakpoint
$breakpoint-value: map-get($breakpoints, $breakpoint);
@media screen and (max-width: $breakpoint-value) {
@content;
}
} @else {
@warn 'Invalid breakpoint: #{$breakpoint}'
}
};
@mixin set-breakpoint-between($lower, $upper) {
@if map-has-key($breakpoints, $lower) and map-has-key($breakpoints, $upper) {
$lower-point: map-get($breakpoints, $lower);
$upper-point: map-get($breakpoints, $upper);
@media (min-width: $lower-point + 1) and (max-width: $upper-point) {
@content;
}
} @else {
@if (map-has-key($breakpoints, $lower) == false) {
@warn 'Invalid lower breakpoint: #{$lower}'
};
@if (map-has-key($breakpoints, $upper) == false) {
@warn 'Invalid upper breakpoint: #{$upper}'
};
}
};
@mixin set-breakpoint-upper($breakpoint) {
@if map-has-key($breakpoints, $breakpoint) {
$breakpoint-value: map-get($breakpoints, $breakpoint);
@media screen and (min-width: $breakpoint-value + 1) {
@content;
}
}
}

View File

@ -16,6 +16,8 @@
@import './app/site/agenda/components/list-of-speakers/list-of-speakers.component.scss-theme.scss';
@import './app/shared/components/sorting-tree/sorting-tree.component.scss';
@import './app/site/global-spinner/global-spinner.component.scss';
@import './app/shared/components/tile/tile.component.scss';
@import './app/shared/components/block-tile/block-tile.component.scss';
/** fonts */
@import './assets/styles/fonts.scss';
@ -29,6 +31,8 @@
@include os-list-of-speakers-style($theme);
@include os-sorting-tree-style($theme);
@include os-global-spinner-theme($theme);
@include os-tile-style($theme);
@include os-block-tile-style($theme);
/** More components are added here */
}