Merge pull request #4708 from GabrielInTheWorld/tile-layout
Adds a new view with tiles to the motion-list
This commit is contained in:
commit
8bf1dad10e
@ -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>
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
@ -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>
|
@ -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;
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
@ -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">
|
||||
|
@ -0,0 +1,4 @@
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="tileContext"
|
||||
[ngTemplateOutletContext]="data">
|
||||
</ng-container>
|
94
client/src/app/shared/components/tile/tile.component.scss
Normal file
94
client/src/app/shared/components/tile/tile.component.scss
Normal 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%;
|
||||
}
|
||||
}
|
||||
}
|
24
client/src/app/shared/components/tile/tile.component.spec.ts
Normal file
24
client/src/app/shared/components/tile/tile.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
185
client/src/app/shared/components/tile/tile.component.ts
Normal file
185
client/src/app/shared/components/tile/tile.component.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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 },
|
||||
|
@ -23,8 +23,14 @@
|
||||
[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>
|
||||
|
||||
<div [ngSwitch]="selectedView">
|
||||
<span *ngSwitchCase="'list'">
|
||||
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
|
||||
<!-- Selector column -->
|
||||
<ng-container matColumnDef="selector">
|
||||
@ -90,7 +96,7 @@
|
||||
<!-- recommendation line -->
|
||||
<div
|
||||
*ngIf="motion.recommendation && motion.state.next_states_id.length > 0"
|
||||
class="ellipsis-overflow white spacer-top-3"
|
||||
class="ellipsis-overflow white"
|
||||
>
|
||||
<mat-basic-chip class="bluegrey" [disabled]="true">
|
||||
{{ getRecommendationLabel(motion) }}
|
||||
@ -153,6 +159,29 @@
|
||||
</mat-table>
|
||||
|
||||
<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">
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -93,4 +93,12 @@
|
||||
input[readonly] {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.stretch-to-fill-parent {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
51
client/src/assets/styles/media-queries.scss
Normal file
51
client/src/assets/styles/media-queries.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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 */
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user