Browse Source

Better teams overview.

pull/925/head
Sebastian 3 years ago
parent
commit
44e9c14961
  1. 1
      backend/i18n/frontend_en.json
  2. 1
      backend/i18n/frontend_it.json
  3. 1
      backend/i18n/frontend_nl.json
  4. 1
      backend/i18n/frontend_zh.json
  5. 1
      backend/i18n/source/frontend_en.json
  6. 26
      frontend/src/app/features/apps/pages/apps-page.component.html
  7. 38
      frontend/src/app/features/apps/pages/apps-page.component.scss
  8. 38
      frontend/src/app/features/apps/pages/apps-page.component.ts
  9. 18
      frontend/src/app/features/apps/pages/team.component.html
  10. 26
      frontend/src/app/features/apps/pages/team.component.scss
  11. 8
      frontend/src/app/features/teams/pages/dashboard/cards/apps-card.component.html
  12. 6
      frontend/src/app/features/teams/pages/dashboard/cards/apps-card.component.ts
  13. 2
      frontend/src/app/theme/_vars.scss

1
backend/i18n/frontend_en.json

@ -992,6 +992,7 @@
"start.madeByCopyright": "Sebastian Stehle and Contributors, 2016-2021", "start.madeByCopyright": "Sebastian Stehle and Contributors, 2016-2021",
"teams.create": "Create", "teams.create": "Create",
"teams.createFailed": "Failed to create team. Please reload.", "teams.createFailed": "Failed to create team. Please reload.",
"teams.empty": "This team has no apps yet.",
"teams.leave": "Leave team", "teams.leave": "Leave team",
"teams.leaveConfirmText": "Do you really want to leave this team?", "teams.leaveConfirmText": "Do you really want to leave this team?",
"teams.leaveConfirmTitle": "Leave team.", "teams.leaveConfirmTitle": "Leave team.",

1
backend/i18n/frontend_it.json

@ -992,6 +992,7 @@
"start.madeByCopyright": "Sebastian Stehle e Collaboratori, 2016-2020", "start.madeByCopyright": "Sebastian Stehle e Collaboratori, 2016-2020",
"teams.create": "Create", "teams.create": "Create",
"teams.createFailed": "Failed to create team. Please reload.", "teams.createFailed": "Failed to create team. Please reload.",
"teams.empty": "This team has no apps yet.",
"teams.leave": "Leave team", "teams.leave": "Leave team",
"teams.leaveConfirmText": "Do you really want to leave this team?", "teams.leaveConfirmText": "Do you really want to leave this team?",
"teams.leaveConfirmTitle": "Leave team.", "teams.leaveConfirmTitle": "Leave team.",

1
backend/i18n/frontend_nl.json

@ -992,6 +992,7 @@
"start.madeByCopyright": "Sebastian Stehle en medewerkers, 2016-2020", "start.madeByCopyright": "Sebastian Stehle en medewerkers, 2016-2020",
"teams.create": "Create", "teams.create": "Create",
"teams.createFailed": "Failed to create team. Please reload.", "teams.createFailed": "Failed to create team. Please reload.",
"teams.empty": "This team has no apps yet.",
"teams.leave": "Leave team", "teams.leave": "Leave team",
"teams.leaveConfirmText": "Do you really want to leave this team?", "teams.leaveConfirmText": "Do you really want to leave this team?",
"teams.leaveConfirmTitle": "Leave team.", "teams.leaveConfirmTitle": "Leave team.",

1
backend/i18n/frontend_zh.json

@ -992,6 +992,7 @@
"start.madeByCopyright": "Sebastian Stehle 和贡献者,2016-2021", "start.madeByCopyright": "Sebastian Stehle 和贡献者,2016-2021",
"teams.create": "Create", "teams.create": "Create",
"teams.createFailed": "Failed to create team. Please reload.", "teams.createFailed": "Failed to create team. Please reload.",
"teams.empty": "This team has no apps yet.",
"teams.leave": "Leave team", "teams.leave": "Leave team",
"teams.leaveConfirmText": "Do you really want to leave this team?", "teams.leaveConfirmText": "Do you really want to leave this team?",
"teams.leaveConfirmTitle": "Leave team.", "teams.leaveConfirmTitle": "Leave team.",

1
backend/i18n/source/frontend_en.json

@ -992,6 +992,7 @@
"start.madeByCopyright": "Sebastian Stehle and Contributors, 2016-2021", "start.madeByCopyright": "Sebastian Stehle and Contributors, 2016-2021",
"teams.create": "Create", "teams.create": "Create",
"teams.createFailed": "Failed to create team. Please reload.", "teams.createFailed": "Failed to create team. Please reload.",
"teams.empty": "This team has no apps yet.",
"teams.leave": "Leave team", "teams.leave": "Leave team",
"teams.leaveConfirmText": "Do you really want to leave this team?", "teams.leaveConfirmText": "Do you really want to leave this team?",
"teams.leaveConfirmTitle": "Leave team.", "teams.leaveConfirmTitle": "Leave team.",

26
frontend/src/app/features/apps/pages/apps-page.component.html

@ -9,27 +9,25 @@
</div> </div>
</div> </div>
<ng-container *ngIf="appsState.apps | async; let apps"> <ng-container *ngIf="groupedApps | async; let groups">
<div class="apps-section"> <div class="apps-section">
<div class="empty" *ngIf="apps.length === 0"> <div class="empty" *ngIf="groups.length === 0">
<h3 class="empty-headline">{{ 'apps.empty' | sqxTranslate }}</h3> <h3 class="empty-headline">{{ 'apps.empty' | sqxTranslate }}</h3>
</div> </div>
<h3 *ngIf="apps.length > 0">{{ 'common.apps' | sqxTranslate }}</h3> <div class="team" *ngFor="let group of groups; trackByGroup">
<div class="team-header" *ngIf="group.team">
<sqx-app *ngFor="let app of apps; trackBy: trackByApp" <sqx-team [team]="group.team" (leave)="leaveTeam($event)"></sqx-team>
[app]="app" (leave)="leaveApp($event)">
</sqx-app>
</div> </div>
</ng-container>
<ng-container *ngIf="teamsState.teams | async; let teams"> <div class="team-body" [class.padded]="group.team">
<div class="teams-section"> <sqx-app *ngFor="let app of group.apps; trackBy: trackByApp" [app]="app" (leave)="leaveApp($event)"></sqx-app>
<h3 *ngIf="teams.length > 0">{{ 'common.teams' | sqxTranslate }}</h3>
<sqx-team *ngFor="let team of teams; trackBy: trackByTeam" <small class="team-empty" *ngIf="group.apps.length === 0">
[team]="team" (leave)="leaveTeam($event)"> {{ 'teams.empty' | sqxTranslate }}
</sqx-team> </small>
</div>
</div>
</div> </div>
</ng-container> </ng-container>

38
frontend/src/app/features/apps/pages/apps-page.component.scss

@ -2,12 +2,6 @@
@import 'vars'; @import 'vars';
.apps-section { .apps-section {
@include clearfix;
padding: 2rem 1.25rem 0 $size-sidebar-width + .25rem;
}
.teams-section {
@include clearfix;
padding: 2rem 1.25rem 0 $size-sidebar-width + .25rem; padding: 2rem 1.25rem 0 $size-sidebar-width + .25rem;
} }
@ -17,10 +11,6 @@
overflow-y: auto; overflow-y: auto;
} }
%no-decoration {
text-decoration: none;
}
:host ::ng-deep { :host ::ng-deep {
.card { .card {
@include hover-visible('.deeplinks', inline); @include hover-visible('.deeplinks', inline);
@ -83,19 +73,35 @@
&-href { &-href {
cursor: pointer; cursor: pointer;
&:hover { &:active,
@extend %no-decoration; &:focus {
@include box-shadow-outer(0, 3px, 16px, .2); text-decoration: none;
} }
&:focus { &:focus {
@extend %no-decoration;
outline: none; outline: none;
} }
&:active { &:hover {
@extend %no-decoration; @include box-shadow-outer(0, 3px, 16px, .2);
}
}
}
}
.team {
margin-top: 1rem;
&:first-child {
margin: 0;
} }
&-body {
@include clearfix;
&.padded {
padding-bottom: 1rem;
padding-top: 1rem;
} }
} }
} }

38
frontend/src/app/features/apps/pages/apps-page.component.ts

@ -6,11 +6,13 @@
*/ */
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs'; import { combineLatest } from 'rxjs';
import { map, take } from 'rxjs/operators'; import { map, take } from 'rxjs/operators';
import { AppDto, AppsState, AuthService, DialogModel, FeatureDto, LocalStoreService, NewsService, OnboardingService, TeamDto, TeamsState, TemplateDto, TemplatesState, UIOptions, UIState } from '@app/shared'; import { AppDto, AppsState, AuthService, DialogModel, FeatureDto, LocalStoreService, NewsService, OnboardingService, TeamDto, TeamsState, TemplateDto, TemplatesState, UIOptions, UIState } from '@app/shared';
import { Settings } from '@app/shared/state/settings'; import { Settings } from '@app/shared/state/settings';
type GroupedApps = { team?: TeamDto; apps: AppDto[] };
@Component({ @Component({
selector: 'sqx-apps-page', selector: 'sqx-apps-page',
styleUrls: ['./apps-page.component.scss'], styleUrls: ['./apps-page.component.scss'],
@ -27,18 +29,42 @@ export class AppsPageComponent implements OnInit {
public info = ''; public info = '';
public templates: Observable<TemplateDto[]> = public templates =
this.templatesState.templates.pipe( this.templatesState.templates.pipe(
map(x => x.filter(t => t.isStarter))); map(x => x.filter(t => t.isStarter)));
public groupedApps =
combineLatest([
this.appsState.apps,
this.teamsState.teams,
]).pipe(map(([apps, teams]) => {
const grouped: GroupedApps[] = [{ apps: [] }];
for (const team of teams) {
grouped.push({ team, apps: [] });
}
for (const app of apps) {
const group = grouped.find(x => x.team?.id === app.teamId) || grouped[0];
group.apps.push(app);
}
if (grouped[0].apps.length === 0) {
grouped.shift();
}
return grouped;
}));
constructor( constructor(
public readonly appsState: AppsState,
public readonly authState: AuthService, public readonly authState: AuthService,
public readonly uiState: UIState, public readonly uiState: UIState,
public readonly teamsState: TeamsState, private readonly appsState: AppsState,
private readonly localStore: LocalStoreService, private readonly localStore: LocalStoreService,
private readonly newsService: NewsService, private readonly newsService: NewsService,
private readonly onboardingService: OnboardingService, private readonly onboardingService: OnboardingService,
private readonly teamsState: TeamsState,
private readonly templatesState: TemplatesState, private readonly templatesState: TemplatesState,
private readonly uiOptions: UIOptions, private readonly uiOptions: UIOptions,
) { ) {
@ -92,7 +118,7 @@ export class AppsPageComponent implements OnInit {
return app.id; return app.id;
} }
public trackByTeam(_index: number, team: TeamDto) { public trackByGroup(_index: number, group: GroupedApps) {
return team.id; return group.team?.id || '0';
} }
} }

18
frontend/src/app/features/apps/pages/team.component.html

@ -1,15 +1,12 @@
<div class="card card-href card-team" [routerLink]="['/app/teams', team.id]"> <div class="team-header">
<div class="card-body"> <div class="row align-items-center">
<div class="row g-0"> <div class="col">
<div class="col col-12"> <h3>{{team.name}}</h3>
<h3 class="card-title">{{team.name}}</h3>
<div class="card-text card-links truncate">
<a [routerLink]="['/app/teams', team.id]" sqxStopClick>{{ 'common.edit' | sqxTranslate }}</a>
</div>
</div> </div>
<div class="col-auto">
<a class="link" [routerLink]="['/app/teams', team.id]" sqxStopClick>{{ 'common.edit' | sqxTranslate }}</a>
</div> </div>
<div class="col-auto">
<button type="button" class="btn btn-sm btn-text-secondary" (click)="dropdown.toggle()" sqxStopClick #buttonOptions> <button type="button" class="btn btn-sm btn-text-secondary" (click)="dropdown.toggle()" sqxStopClick #buttonOptions>
<i class="icon-dots"></i> <i class="icon-dots"></i>
</button> </button>
@ -26,4 +23,5 @@
</sqx-dropdown-menu> </sqx-dropdown-menu>
</ng-container> </ng-container>
</div> </div>
</div>
</div> </div>

26
frontend/src/app/features/apps/pages/team.component.scss

@ -1,14 +1,28 @@
@import 'mixins'; @import 'mixins';
@import 'vars'; @import 'vars';
.btn { h3 {
@include absolute(1rem, 1rem); @include truncate;
} }
.card-body { .row {
position: relative; flex-wrap: nowrap;
} }
.card-title { .col {
padding-right: 2rem; overflow: hidden;
}
.team-header {
border-bottom: 1px solid $color-border;
}
.link {
font-size: $font-small;
font-weight: normal;
text-decoration: none;
&:hover {
text-decoration: underline;
}
} }

8
frontend/src/app/features/teams/pages/dashboard/cards/apps-card.component.html

@ -1,7 +1,12 @@
<div class="card card"> <div class="card card">
<div class="card-header">{{ 'common.apps' | sqxTranslate }}</div> <div class="card-header">{{ 'common.apps' | sqxTranslate }}</div>
<div class="card-body"> <div class="card-body">
<div class="row" *ngFor="let app of snapshot.apps"> <ng-container *ngIf="snapshot.apps; let apps">
<small class="team-empty" *ngIf="apps.length === 0">
{{ 'teams.empty' | sqxTranslate }}
</small>
<div class="row" *ngFor="let app of apps">
<div class="col"> <div class="col">
{{app.displayName}} {{app.displayName}}
</div> </div>
@ -9,5 +14,6 @@
<a [routerLink]="['/app', app.name]" sqxStopClick>{{ 'common.edit' | sqxTranslate }}</a> <a [routerLink]="['/app', app.name]" sqxStopClick>{{ 'common.edit' | sqxTranslate }}</a>
</div> </div>
</div> </div>
</ng-container>
</div> </div>
</div> </div>

6
frontend/src/app/features/teams/pages/dashboard/cards/apps-card.component.ts

@ -10,7 +10,7 @@ import { AppDto, AppsService, StatefulComponent, TeamDto } from '@app/shared';
interface State { interface State {
// The apps for this team. // The apps for this team.
apps: ReadonlyArray<AppDto>; apps?: ReadonlyArray<AppDto>;
} }
@Component({ @Component({
@ -26,9 +26,7 @@ export class AppsCardComponent extends StatefulComponent<State> implements OnIni
constructor(changeDetector: ChangeDetectorRef, constructor(changeDetector: ChangeDetectorRef,
private readonly appsService: AppsService, private readonly appsService: AppsService,
) { ) {
super(changeDetector, { super(changeDetector, {});
apps: [],
});
} }
public ngOnInit() { public ngOnInit() {

2
frontend/src/app/theme/_vars.scss

@ -10,7 +10,7 @@ $color-border-darker: darken($color-border, 15%);
$color-title: #000; $color-title: #000;
$color-text: #373a3c; $color-text: #373a3c;
$color-text-decent: #6c707f; $color-text-decent: #545863;
$color-tooltip: #1a2129; $color-tooltip: #1a2129;
$color-code-background: #f5f7f9; $color-code-background: #f5f7f9;

Loading…
Cancel
Save