Merge pull request #10364 from opf/feature/boards-ce

[41511] Make basic boards a CE feature
pull/10369/head
Henriette Darge 3 years ago committed by GitHub
commit 83afed93f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 17
      frontend/src/app/features/boards/board/board-actions/board-actions-registry.service.ts
  2. 8
      frontend/src/app/features/boards/board/board-partitioned-page/board-list-container.component.html
  3. 10
      frontend/src/app/features/boards/board/board-partitioned-page/board-list-container.component.ts
  4. 5
      frontend/src/app/features/boards/boards-sidebar/boards-menu.component.ts
  5. 33
      frontend/src/app/features/boards/index-page/boards-index-page.component.html
  6. 37
      frontend/src/app/features/boards/index-page/boards-index-page.component.ts
  7. 40
      frontend/src/app/features/boards/new-board-modal/new-board-modal.component.ts
  8. 19
      frontend/src/app/features/boards/new-board-modal/new-board-modal.html
  9. 16
      frontend/src/app/features/boards/tile-view/tile-view.component.html
  10. 75
      frontend/src/app/features/boards/tile-view/tile-view.component.sass
  11. 6
      frontend/src/app/features/boards/tile-view/tile-view.component.spec.ts
  12. 5
      frontend/src/app/features/boards/tile-view/tile-view.component.ts
  13. 16
      frontend/src/app/shared/components/enterprise-banner/enterprise-banner.component.ts
  14. 4
      modules/boards/config/locales/js-en.yml
  15. 84
      modules/boards/spec/features/board_enterprise_spec.rb
  16. 7
      modules/boards/spec/features/support/board_index_page.rb

@ -1,17 +1,28 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { BoardActionService } from 'core-app/features/boards/board/board-actions/board-action.service'; import { BoardActionService } from 'core-app/features/boards/board/board-actions/board-action.service';
import { ITileViewEntry } from 'core-app/features/boards/tile-view/tile-view.component';
import { BannersService } from 'core-app/core/enterprise/banners.service';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class BoardActionsRegistryService { export class BoardActionsRegistryService {
constructor(
private bannersService:BannersService,
) {}
private mapping:{ [attribute:string]:BoardActionService } = {}; private mapping:{ [attribute:string]:BoardActionService } = {};
public add(attribute:string, service:BoardActionService) { public add(attribute:string, service:BoardActionService):void {
this.mapping[attribute] = service; this.mapping[attribute] = service;
} }
public available() { public available():ITileViewEntry[] {
return _.map(this.mapping, (service:BoardActionService, attribute:string) => ({ return _.map(this.mapping, (service:BoardActionService, attribute:string) => ({
attribute, text: service.localizedName, icon: '', description: '', image: '', attribute,
text: service.localizedName,
icon: '',
description: '',
image: '',
disabled: this.bannersService.eeShowBanners,
})); }));
} }

@ -2,7 +2,6 @@
<div class="boards-list--container" <div class="boards-list--container"
[ngClass]="{ '-free' : board.isFree }" [ngClass]="{ '-free' : board.isFree }"
#container #container
*ngIf="showBoardListView()"
cdkDropList cdkDropList
[cdkDropListDisabled]="!board.editable" [cdkDropListDisabled]="!board.editable"
cdkDropListOrientation="horizontal" cdkDropListOrientation="horizontal"
@ -39,11 +38,4 @@
</div> </div>
</div> </div>
</div> </div>
<enterprise-banner *ngIf="!showBoardListView()"
[leftMargin]="true"
[linkMessage]="text.upsaleCheckOutLink"
[textMessage]="text.upsaleBoards"
[opReferrer]="opReferrer(board)">
</enterprise-banner>
</ng-container> </ng-container>

@ -39,8 +39,6 @@ export class BoardListContainerComponent extends UntilDestroyedMixin implements
updateSuccessful: this.I18n.t('js.notice_successful_update'), updateSuccessful: this.I18n.t('js.notice_successful_update'),
loadingError: 'No such board found', loadingError: 'No such board found',
addList: this.I18n.t('js.boards.add_list'), addList: this.I18n.t('js.boards.add_list'),
upsaleBoards: this.I18n.t('js.boards.upsale.teaser_text'),
upsaleCheckOutLink: this.I18n.t('js.work_packages.table_configuration.upsale.check_out_link'),
unnamedList: this.I18n.t('js.boards.label_unnamed_list'), unnamedList: this.I18n.t('js.boards.label_unnamed_list'),
hiddenListWarning: this.I18n.t('js.boards.text_hidden_list_warning'), hiddenListWarning: this.I18n.t('js.boards.text_hidden_list_warning'),
}; };
@ -150,14 +148,6 @@ export class BoardListContainerComponent extends UntilDestroyedMixin implements
} }
} }
showBoardListView() {
return !this.Banner.eeShowBanners;
}
opReferrer(board:Board) {
return board.isFree ? 'boards#free' : 'boards#status';
}
saveBoard(board:Board):void { saveBoard(board:Board):void {
this.boardComponent.boardSaver.request(board); this.boardComponent.boardSaver.request(board);
} }

@ -31,14 +31,11 @@ export const boardsMenuSelector = 'boards-menu';
export class BoardsMenuComponent extends UntilDestroyedMixin implements OnInit { export class BoardsMenuComponent extends UntilDestroyedMixin implements OnInit {
@HostBinding('class.op-sidebar') className = true; @HostBinding('class.op-sidebar') className = true;
selectedBoardId:string;
boardOptions$:Observable<IOpSidemenuItem[]> = this boardOptions$:Observable<IOpSidemenuItem[]> = this
.apiV3Service .apiV3Service
.boards .boards
.observeAll() .observeAll()
.pipe( .pipe(
skip(1),
map((boards:Board[]) => { map((boards:Board[]) => {
const menuItems:IOpSidemenuItem[] = boards.map((board) => ({ const menuItems:IOpSidemenuItem[] = boards.map((board) => ({
title: board.name, title: board.name,
@ -88,7 +85,7 @@ export class BoardsMenuComponent extends UntilDestroyedMixin implements OnInit {
.onActivate('board_view') .onActivate('board_view')
.subscribe(() => { .subscribe(() => {
this.focusBackArrow(); this.focusBackArrow();
this.boardService.loadAllBoards(); void this.boardService.loadAllBoards();
}); });
} }

@ -3,8 +3,7 @@
<div class="title-container"> <div class="title-container">
<h2 [textContent]="text.boards"></h2> <h2 [textContent]="text.boards"></h2>
</div> </div>
<ul class="toolbar-items" <ul class="toolbar-items">
*ngIf="showBoardIndexView()">
<li *ngIf="canAdd" <li *ngIf="canAdd"
class="toolbar-item"> class="toolbar-item">
<a class="button -alt-highlight" <a class="button -alt-highlight"
@ -22,7 +21,7 @@
<div class="boards--listing-group loading-indicator--location" <div class="boards--listing-group loading-indicator--location"
data-indicator-name="boards-module"> data-indicator-name="boards-module">
<div *ngIf="showBoardIndexView() && (boards$ | async) as boards" <div *ngIf="(boards$ | async) as boards"
class="generic-table--container"> class="generic-table--container">
<div class="generic-table--results-container"> <div class="generic-table--results-container">
<table class="generic-table"> <table class="generic-table">
@ -108,31 +107,3 @@
</div> </div>
</div> </div>
</div> </div>
<div *ngIf="!showBoardIndexView()"
class="boards--teaser-container">
<p>{{ text.teaser_text }}</p>
<p>{{ text.enterprise }}</p>
<a class="button -alt-highlight -with-icon icon-checkmark"
[href]="eeLink()"
target='blank'>
{{ text.upgrade }}
</a>
<a class="button -highlight-inverted"
[href]="demoLink()"
[textContent]="text.personal_demo"
target="_blank"
>
</a>
<div class="boards--teaser-video-container">
<iframe [src]="teaserVideoURL"
class="boards--teaser-video"
frameborder="0"
allow="autoplay; fullscreen"
allowfullscreen>
</iframe>
</div>
</div>

@ -1,5 +1,8 @@
import { import {
AfterViewInit, Component, Injector, OnInit, AfterViewInit,
Component,
Injector,
OnInit,
} from '@angular/core'; } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { I18nService } from 'core-app/core/i18n/i18n.service'; import { I18nService } from 'core-app/core/i18n/i18n.service';
@ -8,12 +11,9 @@ import { Board } from 'core-app/features/boards/board/board';
import { ToastService } from 'core-app/shared/components/toaster/toast.service'; import { ToastService } from 'core-app/shared/components/toaster/toast.service';
import { OpModalService } from 'core-app/shared/components/modal/modal.service'; import { OpModalService } from 'core-app/shared/components/modal/modal.service';
import { NewBoardModalComponent } from 'core-app/features/boards/new-board-modal/new-board-modal.component'; import { NewBoardModalComponent } from 'core-app/features/boards/new-board-modal/new-board-modal.component';
import { BannersService } from 'core-app/core/enterprise/banners.service';
import { LoadingIndicatorService } from 'core-app/core/loading-indicator/loading-indicator.service'; import { LoadingIndicatorService } from 'core-app/core/loading-indicator/loading-indicator.service';
import { AuthorisationService } from 'core-app/core/model-auth/model-auth.service'; import { AuthorisationService } from 'core-app/core/model-auth/model-auth.service';
import { contactUrl } from 'core-app/core/setup/globals/constants.const'; import { contactUrl } from 'core-app/core/setup/globals/constants.const';
import { DomSanitizer } from '@angular/platform-browser';
import { boardTeaserVideoURL } from 'core-app/features/boards/board-constants.const';
import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin'; import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin';
import { componentDestroyed } from '@w11k/ngx-componentdestroyed'; import { componentDestroyed } from '@w11k/ngx-componentdestroyed';
import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service'; import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service';
@ -38,11 +38,6 @@ export class BoardsIndexPageComponent extends UntilDestroyedMixin implements OnI
areYouSure: this.I18n.t('js.text_are_you_sure'), areYouSure: this.I18n.t('js.text_are_you_sure'),
deleteSuccessful: this.I18n.t('js.notice_successful_delete'), deleteSuccessful: this.I18n.t('js.notice_successful_delete'),
noResults: this.I18n.t('js.notice_no_results_to_display'), noResults: this.I18n.t('js.notice_no_results_to_display'),
teaser_text: this.I18n.t('js.boards.upsale.teaser_text'),
enterprise: this.I18n.t('js.boards.upsale.upgrade_to_ee_text'),
upgrade: this.I18n.t('js.boards.upsale.upgrade'),
personal_demo: this.I18n.t('js.boards.upsale.personal_demo'),
}; };
public canAdd = false; public canAdd = false;
@ -55,9 +50,8 @@ export class BoardsIndexPageComponent extends UntilDestroyedMixin implements OnI
map((boards:Board[]) => boards.sort((a, b) => a.name.localeCompare(b.name))), map((boards:Board[]) => boards.sort((a, b) => a.name.localeCompare(b.name))),
); );
teaserVideoURL = this.domSanitizer.bypassSecurityTrustResourceUrl(boardTeaserVideoURL); constructor(
private readonly boardService:BoardService,
constructor(private readonly boardService:BoardService,
private readonly apiV3Service:ApiV3Service, private readonly apiV3Service:ApiV3Service,
private readonly I18n:I18nService, private readonly I18n:I18nService,
private readonly toastService:ToastService, private readonly toastService:ToastService,
@ -65,8 +59,7 @@ export class BoardsIndexPageComponent extends UntilDestroyedMixin implements OnI
private readonly loadingIndicatorService:LoadingIndicatorService, private readonly loadingIndicatorService:LoadingIndicatorService,
private readonly authorisationService:AuthorisationService, private readonly authorisationService:AuthorisationService,
private readonly injector:Injector, private readonly injector:Injector,
private readonly bannerService:BannersService, ) {
private readonly domSanitizer:DomSanitizer) {
super(); super();
} }
@ -83,11 +76,11 @@ export class BoardsIndexPageComponent extends UntilDestroyedMixin implements OnI
loadingIndicator.promise = this.boardService.loadAllBoards(); loadingIndicator.promise = this.boardService.loadAllBoards();
} }
newBoard() { newBoard():void {
this.opModalService.show(NewBoardModalComponent, this.injector); this.opModalService.show(NewBoardModalComponent, this.injector);
} }
destroyBoard(board:Board) { destroyBoard(board:Board):void {
if (!window.confirm(this.text.areYouSure)) { if (!window.confirm(this.text.areYouSure)) {
return; return;
} }
@ -99,16 +92,4 @@ export class BoardsIndexPageComponent extends UntilDestroyedMixin implements OnI
}) })
.catch((error) => this.toastService.addError(`Deletion failed: ${error}`)); .catch((error) => this.toastService.addError(`Deletion failed: ${error}`));
} }
public showBoardIndexView() {
return !this.bannerService.eeShowBanners;
}
public eeLink() {
return this.bannerService.getEnterPriseEditionUrl({ referrer: 'boards' });
}
public demoLink():string {
return contactUrl[this.I18n.locale] || contactUrl.en;
}
} }

@ -27,7 +27,12 @@
//++ //++
import { import {
ChangeDetectorRef, Component, ElementRef, Inject, ViewChild, ChangeDetectorRef,
Component,
ElementRef,
Inject,
OnInit,
ViewChild,
} from '@angular/core'; } from '@angular/core';
import { OpModalComponent } from 'core-app/shared/components/modal/modal.component'; import { OpModalComponent } from 'core-app/shared/components/modal/modal.component';
import { OpModalLocalsToken } from 'core-app/shared/components/modal/modal.service'; import { OpModalLocalsToken } from 'core-app/shared/components/modal/modal.service';
@ -41,11 +46,13 @@ import { LoadingIndicatorService } from 'core-app/core/loading-indicator/loading
import { HalResourceNotificationService } from 'core-app/features/hal/services/hal-resource-notification.service'; import { HalResourceNotificationService } from 'core-app/features/hal/services/hal-resource-notification.service';
import { imagePath } from 'core-app/shared/helpers/images/path-helper'; import { imagePath } from 'core-app/shared/helpers/images/path-helper';
import { ITileViewEntry } from '../tile-view/tile-view.component'; import { ITileViewEntry } from '../tile-view/tile-view.component';
import { BannersService } from 'core-app/core/enterprise/banners.service';
import { ToastService } from 'core-app/shared/components/toaster/toast.service';
@Component({ @Component({
templateUrl: './new-board-modal.html', templateUrl: './new-board-modal.html',
}) })
export class NewBoardModalComponent extends OpModalComponent { export class NewBoardModalComponent extends OpModalComponent implements OnInit {
@ViewChild('actionAttributeSelect', { static: true }) actionAttributeSelect:ElementRef; @ViewChild('actionAttributeSelect', { static: true }) actionAttributeSelect:ElementRef;
public showClose = true; public showClose = true;
@ -56,7 +63,9 @@ export class NewBoardModalComponent extends OpModalComponent {
public inFlight = false; public inFlight = false;
public text:any = { public eeShowBanners = false;
public text = {
close_popup: this.I18n.t('js.close_popup_title'), close_popup: this.I18n.t('js.close_popup_title'),
free_board: this.I18n.t('js.boards.board_type.free'), free_board: this.I18n.t('js.boards.board_type.free'),
@ -69,10 +78,14 @@ export class NewBoardModalComponent extends OpModalComponent {
select_attribute: this.I18n.t('js.boards.board_type.select_attribute'), select_attribute: this.I18n.t('js.boards.board_type.select_attribute'),
select_board_type: this.I18n.t('js.boards.board_type.select_board_type'), select_board_type: this.I18n.t('js.boards.board_type.select_board_type'),
placeholder: this.I18n.t('js.placeholders.selection'), placeholder: this.I18n.t('js.placeholders.selection'),
teaser_text: this.I18n.t('js.boards.upsale.teaser_text'),
upgrade_to_ee_text: this.I18n.t('js.boards.upsale.upgrade'),
}; };
constructor(readonly elementRef:ElementRef, constructor(
@Inject(OpModalLocalsToken) public locals:OpModalLocalsMap, @Inject(OpModalLocalsToken) public locals:OpModalLocalsMap,
readonly elementRef:ElementRef,
readonly cdRef:ChangeDetectorRef, readonly cdRef:ChangeDetectorRef,
readonly state:StateService, readonly state:StateService,
readonly boardService:BoardService, readonly boardService:BoardService,
@ -80,12 +93,20 @@ export class NewBoardModalComponent extends OpModalComponent {
readonly halNotification:HalResourceNotificationService, readonly halNotification:HalResourceNotificationService,
readonly loadingIndicatorService:LoadingIndicatorService, readonly loadingIndicatorService:LoadingIndicatorService,
readonly I18n:I18nService, readonly I18n:I18nService,
readonly boardActionRegistry:BoardActionsRegistryService) { readonly boardActionRegistry:BoardActionsRegistryService,
readonly bannersService:BannersService,
readonly toastService:ToastService,
) {
super(locals, cdRef, elementRef); super(locals, cdRef, elementRef);
this.initiateTiles(); this.initiateTiles();
} }
public createBoard(attribute:string) { ngOnInit():void {
super.ngOnInit();
this.eeShowBanners = this.bannersService.eeShowBanners;
}
public createBoard(attribute:string):void {
if (attribute === 'basic') { if (attribute === 'basic') {
this.createFree(); this.createFree();
} else { } else {
@ -111,7 +132,12 @@ export class NewBoardModalComponent extends OpModalComponent {
this.create({ type: 'free' }); this.create({ type: 'free' });
} }
private createAction(attribute:string) { private createAction(attribute:string):void {
if (this.eeShowBanners) {
this.toastService.addError(this.I18n.t('js.upsale.ee_only'));
return;
}
this.create({ type: 'action', attribute }); this.create({ type: 'action', attribute });
} }

@ -1,16 +1,25 @@
<div <div
class="op-modal op-modal_wide confirm-form-submit--modal loading-indicator--location" class="op-modal op-modal_wide confirm-form-submit--modal loading-indicator--location"
data-indicator-name="modal" data-indicator-name="modal"
> >
<op-modal-header (close)="closeMe($event)">{{text.board_type}}</op-modal-header> <op-modal-header (close)="closeMe($event)">{{text.board_type}}</op-modal-header>
<div class="op-modal--body"> <div class="op-modal--body">
<p [textContent]="text.select_board_type"></p> <enterprise-banner
*ngIf="eeShowBanners"
[linkMessage]="text.upgrade_to_ee_text"
[textMessage]="text.teaser_text"
opReferrer="boards"
></enterprise-banner>
<p
*ngIf="!eeShowBanners"
[textContent]="text.select_board_type"></p>
<section class="new-board--section"> <section class="new-board--section">
<tile-view <tile-view
[tiles]="available" [tiles]="available"
[disable]="inFlight" [disable]="inFlight"
(create)="createBoard($event)"> (create)="createBoard($event)">
</tile-view> </tile-view>
</section> </section>
</div> </div>

@ -1,15 +1,19 @@
<div class="tile-blocks--container"> <div class="op-tile-block">
<button <button
*ngFor="let tile of tiles" *ngFor="let tile of tiles"
class="tile-block button" class="op-tile-block--tile button"
data-qa-selector="op-tile-block"
type="button" type="button"
[disabled]="disabled()" [disabled]="tile.disabled || disable"
(click)="created(tile.attribute)" (click)="created(tile.attribute)"
> >
<img [src]="tile.image" class="tile-block-image"/> <img [src]="tile.image" class="op-tile-block--image"/>
<div> <div>
<span class="tile-block-title">{{ tile.text }}</span> <span
<p class="tile-block-description" [textContent]="tile.description"></p> data-qa-selector="op-tile-block-title"
class="op-tile-block--title"
>{{ tile.text }}</span>
<p class="op-tile-block--description" [textContent]="tile.description"></p>
</div> </div>
</button> </button>
</div> </div>

@ -1,44 +1,47 @@
.tile-blocks--container .op-tile-block
$block: &
display: grid display: grid
grid-template-rows: repeat(minmax(200px, auto)) grid-template-rows: repeat(minmax(200px, auto))
grid-template-columns: auto auto grid-template-columns: auto auto
grid-column-gap: 10px grid-column-gap: 10px
grid-row-gap: 10px grid-row-gap: 10px
&--tile
.tile-block border-radius: 10px
border-radius: 10px display: grid
display: grid grid-template: 95px 1fr / 1fr
grid-template: 95px 1fr / 1fr grid-template-columns: auto 1fr auto
grid-template-columns: auto 1fr auto grid-template-rows: auto
grid-template-rows: auto grid-row-gap: 5px
grid-row-gap: 5px justify-items: left
justify-items: left background: #f7fafc
background: #f7fafc min-height: 150px
min-height: 150px
&:disabled
&:hover background: #fafafa
text-decoration: none
border: 1px solid grey &:hover
border-radius: 10px !important text-decoration: none
cursor: pointer border: 1px solid grey
border-radius: 10px !important
.tile-block-image cursor: pointer
display: block
margin-top: auto &--image
margin-bottom: auto display: block
margin-top: auto
.tile-block-title margin-bottom: auto
padding-top: 30px
padding-bottom: 5px &--title
color: var(--primary-color-dark) padding-top: 30px
display: block padding-bottom: 5px
text-align: left color: var(--primary-color-dark)
font-weight: bolder display: block
font-size: large text-align: left
font-weight: bolder
.tile-block p font-size: large
text-align: left
width: 90% &--description
text-align: left
width: 90%

@ -34,19 +34,19 @@ describe('shows tiles', () => {
it('should render the componenet successfully', () => { it('should render the componenet successfully', () => {
fixture.detectChanges(); fixture.detectChanges();
const tile = document.querySelector('.tile-blocks--container'); const tile = document.querySelector('.op-tile-block--title');
expect(document.contains(tile)).toBeTruthy(); expect(document.contains(tile)).toBeTruthy();
}); });
it('should show each tile', () => { it('should show each tile', () => {
fixture.detectChanges(); fixture.detectChanges();
const tile:HTMLElement = element.query(By.css('.tile-block')).nativeElement; const tile:HTMLElement = element.query(By.css('.op-tile-block--title')).nativeElement;
expect(tile.textContent).toContain('Basic'); expect(tile.textContent).toContain('Basic');
}); });
it('should show the image', () => { it('should show the image', () => {
fixture.detectChanges(); fixture.detectChanges();
const tile = document.querySelector('.tile-block-image'); const tile = document.querySelector('.op-tile-block--image');
expect(document.contains(tile)).toBeTruthy(); expect(document.contains(tile)).toBeTruthy();
}); });
}); });

@ -8,6 +8,7 @@ export interface ITileViewEntry {
icon:string; icon:string;
description:string; description:string;
image:string; image:string;
disabled?:boolean;
} }
@Component({ @Component({
@ -23,10 +24,6 @@ export class TileViewComponent {
@Output() public create = new EventEmitter<string>(); @Output() public create = new EventEmitter<string>();
public disabled() {
return this.disable;
}
public created(attribute:string) { public created(attribute:string) {
this.create.emit(attribute); this.create.emit(attribute);
} }

@ -1,4 +1,8 @@
import { Component, Input } from '@angular/core'; import {
Component,
Input,
OnInit,
} from '@angular/core';
import { BannersService } from 'core-app/core/enterprise/banners.service'; import { BannersService } from 'core-app/core/enterprise/banners.service';
import { I18nService } from 'core-app/core/i18n/i18n.service'; import { I18nService } from 'core-app/core/i18n/i18n.service';
@ -12,7 +16,7 @@ import { I18nService } from 'core-app/core/i18n/i18n.service';
<div class="op-toast--content"> <div class="op-toast--content">
<p class="-bold" [textContent]="text.enterpriseFeature"></p> <p class="-bold" [textContent]="text.enterpriseFeature"></p>
<p [textContent]="textMessage"></p> <p [textContent]="textMessage"></p>
<a [href]="eeLink()" <a [href]="link"
target='blank' target='blank'
[textContent]="linkMessage"></a> [textContent]="linkMessage"></a>
</div> </div>
@ -20,7 +24,7 @@ import { I18nService } from 'core-app/core/i18n/i18n.service';
</div> </div>
` `
}) })
export class EnterpriseBannerComponent { export class EnterpriseBannerComponent implements OnInit {
@Input() public leftMargin = false; @Input() public leftMargin = false;
@Input() public textMessage:string; @Input() public textMessage:string;
@ -29,6 +33,8 @@ export class EnterpriseBannerComponent {
@Input() public opReferrer:string; @Input() public opReferrer:string;
public link:string;
public text:any = { public text:any = {
enterpriseFeature: this.I18n.t('js.upsale.ee_only'), enterpriseFeature: this.I18n.t('js.upsale.ee_only'),
}; };
@ -38,7 +44,7 @@ export class EnterpriseBannerComponent {
protected bannersService:BannersService, protected bannersService:BannersService,
) {} ) {}
public eeLink() { ngOnInit():void {
this.bannersService.getEnterPriseEditionUrl({ referrer: this.opReferrer }); this.link = this.bannersService.getEnterPriseEditionUrl({ referrer: this.opReferrer });
} }
} }

@ -8,10 +8,8 @@ en:
label_board_type: 'Board type' label_board_type: 'Board type'
upsale: upsale:
teaser_text: 'Improve your agile project management with this flexible Boards view. Create as many boards as you like for anything you would like to keep track of.' teaser_text: 'Need a predefined board template? Advanced boards are Enterprise features. Please upgrade to a paid plan.'
upgrade_to_ee_text: 'Boards is an Enterprise feature. Please upgrade to a paid plan.'
upgrade: 'Upgrade now' upgrade: 'Upgrade now'
personal_demo: 'Contact us for a demo'
lists: lists:
delete: 'Delete list' delete: 'Delete list'

@ -0,0 +1,84 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
require 'spec_helper'
require_relative './support/board_index_page'
require_relative './support/board_page'
describe 'Boards enterprise spec', type: :feature, js: true do
shared_let(:admin) { create(:admin) }
shared_let(:project) { create(:project, enabled_module_names: %i[work_package_tracking board_view]) }
shared_let(:priority) { create :default_priority }
shared_let(:status) { create :default_status }
let(:board_index) { Pages::BoardIndex.new(project) }
shared_let(:manual_board) { create :board_grid_with_query, name: 'My board', project: project }
shared_let(:action_board) do
create(:subproject_board,
name: 'Subproject board',
project: project,
projects_columns: [])
end
context 'when EE inactive' do
before do
login_as(admin)
board_index.visit!
end
it 'disabled all action boards' do
# Expect both existing boards to show
expect(page).to have_content 'My board'
expect(page).to have_content 'Subproject board'
page.find('.toolbar-item a', text: I18n.t('js.button_create')).click
expect(page).to have_selector('[data-qa-selector="op-tile-block"]:not([disabled])', text: 'Basic')
expect(page).to have_selector('[data-qa-selector="op-tile-block"]:disabled', count: 5)
end
end
context 'when EE active' do
before do
with_enterprise_token :board_view
login_as(admin)
board_index.visit!
end
it 'enables all options' do
expect(page).to have_content 'My board'
expect(page).to have_content 'Subproject board'
page.find('.toolbar-item a', text: I18n.t('js.button_create')).click
expect(page).to have_selector('[data-qa-selector="op-tile-block"]:not([disabled])', count: 6)
end
end
end

@ -59,11 +59,8 @@ module Pages
def create_board(action: nil, expect_empty: false) def create_board(action: nil, expect_empty: false)
page.find('.toolbar-item a', text: I18n.t('js.button_create')).click page.find('.toolbar-item a', text: I18n.t('js.button_create')).click
if action == nil text = action == nil ? 'Basic' : action.to_s[0..5]
find('.tile-block-title', text: 'Basic').click find('[data-qa-selector="op-tile-block-title"]', text: text).click
else
find('.tile-block-title', text: action.to_s[0..5]).click
end
if expect_empty if expect_empty
expect(page).to have_selector('.boards-list--add-item-text', wait: 10) expect(page).to have_selector('.boards-list--add-item-text', wait: 10)

Loading…
Cancel
Save