Feature/41135 include all subprojects (#10413)

* Add new query attribute include_subprojects

* Set include_subprojects in factories

* Replace create_query helper with default endpoint

* Adapt spec to new query create service

* Add spec for changed subproject behavior

* Extend project filter with replaced values and add spec

* Fix spec trying to save default query

* Fix ordered_work_packages now that we're base service compatible

* Basic frontend for include all subprojects

* Fix disabled state, update button colors

* Add new query attribute include_subprojects

* Set include_subprojects in factories

* Replace create_query helper with default endpoint

* Adapt spec to new query create service

* Add spec for changed subproject behavior

* Extend project filter with replaced values and add spec

* Add a ParserStruct overriding Enumerable#group_by

* Fix ordered_work_packages now that we're base service compatible

* Add tooltip

* Add tooltips to include projects

* Add is parameter to query props for query space

* Fix specs, add i18n strings

* Correctly parse and update the query with includeSubprojects

* FIx most spec

* Fix badge count for team planner spec, fix duplicated where def in project filter

* Only load project list if opening drop modal

* Expect that sub_sub_bug is also present

* I18nify the strings

* Fix typo

* Fix specs

* Fix linting errors

* Fix specs

* Fix linting errors

* Fix linting errors

* Fix failing specs

Co-authored-by: Oliver Günther <mail@oliverguenther.de>
pull/10451/head
Benjamin Bädorf 3 years ago committed by GitHub
parent f8fbf1b5b4
commit b7f732ddcc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      app/services/api/v3/parse_query_params_service.rb
  2. 6
      app/services/update_query_from_params_service.rb
  3. 4
      config/locales/js-en.yml
  4. 4
      frontend/.eslintrc.js
  5. 2
      frontend/src/app/features/hal/resources/query-resource.ts
  6. 2
      frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.ts
  7. 6
      frontend/src/app/features/work-packages/components/wp-buttons/wp-view-toggle-button/work-package-view-toggle-button.component.ts
  8. 14
      frontend/src/app/features/work-packages/components/wp-list/wp-states-initialization.service.ts
  9. 2
      frontend/src/app/features/work-packages/components/wp-query/url-params-helper.spec.ts
  10. 12
      frontend/src/app/features/work-packages/components/wp-query/url-params-helper.ts
  11. 6
      frontend/src/app/features/work-packages/directives/query-space/isolated-query-space.ts
  12. 2
      frontend/src/app/features/work-packages/directives/query-space/wp-isolated-query-space.directive.ts
  13. 6
      frontend/src/app/features/work-packages/routing/wp-list-view/wp-list-view.component.ts
  14. 5
      frontend/src/app/features/work-packages/routing/wp-view-base/view-services/wp-view-base.service.ts
  15. 6
      frontend/src/app/features/work-packages/routing/wp-view-base/view-services/wp-view-display-representation.service.ts
  16. 66
      frontend/src/app/features/work-packages/routing/wp-view-base/view-services/wp-view-include-subprojects.service.ts
  17. 4
      frontend/src/app/features/work-packages/routing/wp-view-base/work-packages-view.base.ts
  18. 6
      frontend/src/app/shared/components/op-context-menu/handlers/wp-view-dropdown-menu.directive.ts
  19. 16
      frontend/src/app/shared/components/project-include/project-include.component.html
  20. 10
      frontend/src/app/shared/components/project-include/project-include.component.sass
  21. 105
      frontend/src/app/shared/components/project-include/project-include.component.ts
  22. 31
      frontend/src/app/shared/components/project-include/project-list.component.html
  23. 24
      frontend/src/app/shared/components/project-include/project-list.component.ts
  24. 12
      frontend/src/app/spot/components/drop-modal/drop-modal.component.ts
  25. 7
      frontend/src/app/spot/components/tooltip/tooltip.component.html
  26. 24
      frontend/src/app/spot/components/tooltip/tooltip.component.ts
  27. 19
      frontend/src/app/spot/drop-alignment-options.ts
  28. 67
      frontend/src/app/spot/spot-docs.component.html
  29. 13
      frontend/src/app/spot/spot-docs.component.ts
  30. 3
      frontend/src/app/spot/spot.module.ts
  31. 14
      frontend/src/app/spot/styles/sass/common/typography.sass
  32. 36
      frontend/src/app/spot/styles/sass/components/button.sass
  33. 42
      frontend/src/app/spot/styles/sass/components/drop-modal.sass
  34. 1
      frontend/src/app/spot/styles/sass/components/index.sass
  35. 13
      frontend/src/app/spot/styles/sass/components/list.sass
  36. 142
      frontend/src/app/spot/styles/sass/components/tooltip.sass
  37. 2
      frontend/src/app/spot/styles/sass/variables/zindex.sass
  38. 4
      frontend/src/global_styles/content/_badges.sass
  39. 4
      modules/calendar/spec/features/calendar_project_include_spec.rb
  40. 8
      modules/team_planner/spec/features/team_planner_add_existing_work_packages_spec.rb
  41. 6
      modules/team_planner/spec/features/team_planner_project_include_spec.rb
  42. 14
      modules/team_planner/spec/features/team_planner_spec.rb
  43. 2
      modules/team_planner/spec/features/team_planner_subproject_constraints_spec.rb
  44. 10
      modules/team_planner/spec/support/pages/team_planner.rb
  45. 2
      nix/shell.nix
  46. 212
      spec/features/work_packages/project_include/project_include_shared_examples.rb
  47. 4
      spec/features/work_packages/table/work_packages_table_project_include_spec.rb
  48. 12
      spec/services/api/v3/parse_query_params_service_spec.rb
  49. 28
      spec/services/update_query_from_params_service_spec.rb
  50. 10
      spec/support/components/project_include_component.rb

@ -69,7 +69,8 @@ module API
highlighting_mode: params[:highlightingMode],
highlighted_attributes: highlighted_attributes_from_params(params),
display_representation: params[:displayRepresentation],
show_hierarchies: boolearize(params[:showHierarchies])
show_hierarchies: boolearize(params[:showHierarchies]),
include_subprojects: boolearize(params[:includeSubprojects])
}
end

@ -51,6 +51,8 @@ class UpdateQueryFromParamsService
apply_display_representation(params)
apply_include_subprojects(params)
disable_hierarchy_when_only_grouped_by(params)
if valid_subset
@ -112,6 +114,10 @@ class UpdateQueryFromParamsService
query.display_representation = params[:display_representation] if params.key?(:display_representation)
end
def apply_include_subprojects(params)
query.include_subprojects = params[:include_subprojects] if params.key?(:include_subprojects)
end
def disable_hierarchy_when_only_grouped_by(params)
if params.key?(:group_by) && !params.key?(:show_hierarchies)
query.show_hierarchies = false

@ -1273,6 +1273,10 @@ en:
all: 'All projects'
selected: 'Only selected'
search_placeholder: 'Search project...'
include_subprojects: 'Include all sub-projects'
tooltip:
include_all_selected: 'Include all sub-projects is currently selected.'
current_project: 'This is the current project you are in.'
forms:
submit_success_message: 'The form was successfully submitted'

@ -42,11 +42,11 @@ module.exports = {
*/
"@angular-eslint/directive-selector": [
"error",
{ "type": "attribute", "prefix": "op", "style": "camelCase" }
{ "type": "attribute", "prefix": ["op", "spot"], "style": "camelCase" }
],
"@angular-eslint/component-selector": [
"error",
{ "type": "element", "prefix": "op", "style": "kebab-case" }
{ "type": "element", "prefix": ["op", "spot"], "style": "kebab-case" }
],
// Warn when new components are being created without OnPush

@ -92,6 +92,8 @@ export class QueryResource extends HalResource {
public project:ProjectResource;
public includeSubprojects:boolean;
public ordered_work_packages:QueryOrder;
public $initialize(source:any) {

@ -828,7 +828,7 @@ export class TeamPlannerComponent extends UntilDestroyedMixin implements OnInit,
principals.forEach((principal) => {
const resourceId = principal._links.self.href;
if (!assignable.includes(resourceId)) {
if (!assignable || !assignable.includes(resourceId)) {
api.addEvent({ ...eventBase, resourceId }, 'background');
}
});

@ -64,10 +64,12 @@ export class WorkPackageViewToggleButtonComponent extends UntilDestroyedMixin im
timeline: this.I18n.t('js.views.timeline'),
};
constructor(readonly I18n:I18nService,
constructor(
readonly I18n:I18nService,
readonly cdRef:ChangeDetectorRef,
readonly wpDisplayRepresentationService:WorkPackageViewDisplayRepresentationService,
readonly wpTableTimeline:WorkPackageViewTimelineService) {
readonly wpTableTimeline:WorkPackageViewTimelineService,
) {
super();
}

@ -7,6 +7,7 @@ import { WorkPackageViewHighlightingService } from 'core-app/features/work-packa
import { take } from 'rxjs/operators';
import { WorkPackageViewOrderService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-order.service';
import { WorkPackageViewDisplayRepresentationService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-display-representation.service';
import { WorkPackageViewIncludeSubprojectsService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-include-subprojects.service';
import { WorkPackageViewSumService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-sum.service';
import { WorkPackageViewColumnsService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-columns.service';
import { WorkPackageViewSortByService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-sort-by.service';
@ -27,7 +28,8 @@ import { WorkPackagesListChecksumService } from './wp-list-checksum.service';
@Injectable()
export class WorkPackageStatesInitializationService {
constructor(protected states:States,
constructor(
protected states:States,
protected querySpace:IsolatedQuerySpace,
protected wpTableColumns:WorkPackageViewColumnsService,
protected wpTableGroupBy:WorkPackageViewGroupByService,
@ -45,8 +47,9 @@ export class WorkPackageStatesInitializationService {
protected apiV3Service:ApiV3Service,
protected wpListChecksumService:WorkPackagesListChecksumService,
protected authorisationService:AuthorisationService,
protected wpDisplayRepresentation:WorkPackageViewDisplayRepresentationService) {
}
protected wpDisplayRepresentation:WorkPackageViewDisplayRepresentationService,
protected wpIncludeSubprojects:WorkPackageViewIncludeSubprojectsService,
) { }
/**
* Initialize the query and table states from the given query and results.
@ -128,6 +131,8 @@ export class WorkPackageStatesInitializationService {
this.wpDisplayRepresentation.initialize(query, results);
this.wpIncludeSubprojects.initialize(query, results);
this.querySpace.additionalRequiredWorkPackages
.values$()
.pipe(take(1))
@ -151,6 +156,7 @@ export class WorkPackageStatesInitializationService {
this.wpTableHierarchies.initialize(query, results);
this.wpTableHighlighting.initialize(query, results);
this.wpDisplayRepresentation.initialize(query, results);
this.wpIncludeSubprojects.initialize(query, results);
this.authorisationService.initModelAuth('query', query.$links);
this.authorisationService.initModelAuth('work_packages', results.$links);
@ -168,6 +174,7 @@ export class WorkPackageStatesInitializationService {
this.wpTableHierarchies.applyToQuery(query);
this.wpTableOrder.applyToQuery(query);
this.wpDisplayRepresentation.applyToQuery(query);
this.wpIncludeSubprojects.applyToQuery(query);
}
public clearStates() {
@ -186,6 +193,7 @@ export class WorkPackageStatesInitializationService {
this.wpTableGroupBy.clear(reason);
this.wpTableGroupFold.clear(reason);
this.wpDisplayRepresentation.clear(reason);
this.wpIncludeSubprojects.clear(reason);
this.wpTableSum.clear(reason);
// Clear rendered state

@ -238,6 +238,7 @@ describe('UrlParamsHelper', () => {
sortBy: JSON.stringify([['type', 'desc'], ['status', 'asc']]),
timelineVisible: false,
showHierarchies: false,
includeSubprojects: false,
highlightingMode: 'inline',
'highlightedAttributes[]': ['a', 'b'],
offset: 10,
@ -298,6 +299,7 @@ describe('UrlParamsHelper', () => {
timelineVisible: false,
showHierarchies: false,
highlightingMode: 'inline',
includeSubprojects: false,
sortBy: '[]',
};

@ -67,6 +67,8 @@ export interface QueryProps {
hla?:string[];
// Display representation
dr?:string;
// Inlude subprojects
is?:boolean;
// Pagination
pa?:string|number;
pp?:string|number;
@ -118,6 +120,7 @@ export class UrlParamsHelperService {
hi: !!query.showHierarchies,
g: _.get(query.groupBy, 'id', ''),
dr: query.displayRepresentation,
is: query.includeSubprojects,
...this.encodeSums(query),
...this.encodeTimelineVisible(query),
...this.encodeHighlightingMode(query),
@ -217,7 +220,7 @@ export class UrlParamsHelperService {
return queryData;
}
const properties = JSON.parse(updateJson);
const properties = JSON.parse(updateJson) as QueryProps;
if (properties.c) {
queryData['columns[]'] = properties.c.map((column:any) => column);
@ -242,6 +245,10 @@ export class UrlParamsHelperService {
queryData.displayRepresentation = properties.dr;
}
if (properties.is !== undefined) {
queryData.includeSubprojects = properties.is;
}
if (properties.hl) {
queryData.highlightingMode = properties.hl;
}
@ -250,7 +257,7 @@ export class UrlParamsHelperService {
queryData['highlightedAttributes[]'] = properties.hla.map((column:any) => column);
}
if (properties.hi === false || properties.hi === true) {
if (properties.hi !== undefined) {
queryData.showHierarchies = properties.hi;
}
@ -317,6 +324,7 @@ export class UrlParamsHelperService {
queryData.displayRepresentation = query.displayRepresentation;
}
queryData.includeSubprojects = !!query.includeSubprojects;
queryData.showHierarchies = !!query.showHierarchies;
queryData.groupBy = _.get(query.groupBy, 'id', '');

@ -1,5 +1,9 @@
import {
derive, input, InputState, State, StatesGroup,
derive,
input,
InputState,
State,
StatesGroup,
} from 'reactivestates';
import { Subject } from 'rxjs';
import { Injectable } from '@angular/core';

@ -59,6 +59,7 @@ import { WorkPackageViewOrderService } from 'core-app/features/work-packages/rou
import { CausedUpdatesService } from 'core-app/features/boards/board/caused-updates/caused-updates.service';
import { WorkPackageCardViewService } from 'core-app/features/work-packages/components/wp-card-view/services/wp-card-view.service';
import { WorkPackageViewDisplayRepresentationService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-display-representation.service';
import { WorkPackageViewIncludeSubprojectsService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-include-subprojects.service';
import { WorkPackageViewHierarchyIdentationService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-hierarchy-indentation.service';
import { HalResourceNotificationService } from 'core-app/features/hal/services/hal-resource-notification.service';
import { WorkPackageNotificationService } from 'core-app/features/work-packages/services/notifications/work-package-notification.service';
@ -101,6 +102,7 @@ import { WorkPackageService } from 'core-app/features/work-packages/services/wor
WorkPackageViewFocusService,
WorkPackageViewHighlightingService,
WorkPackageViewDisplayRepresentationService,
WorkPackageViewIncludeSubprojectsService,
WorkPackageViewOrderService,
WorkPackageViewHierarchyIdentationService,
CausedUpdatesService,

@ -91,7 +91,8 @@ export class WorkPackageListViewComponent extends UntilDestroyedMixin implements
dragAndDropEnabled: true,
};
constructor(readonly I18n:I18nService,
constructor(
readonly I18n:I18nService,
readonly injector:Injector,
readonly $state:StateService,
readonly keepTab:KeepTabService,
@ -102,7 +103,8 @@ export class WorkPackageListViewComponent extends UntilDestroyedMixin implements
readonly wpDisplayRepresentation:WorkPackageViewDisplayRepresentationService,
readonly cdRef:ChangeDetectorRef,
readonly elementRef:ElementRef,
private ngZone:NgZone) {
private ngZone:NgZone,
) {
super();
}

@ -45,8 +45,9 @@ export abstract class WorkPackageViewBaseService<T> {
/** Internal pristine state filled during +initialize+ only */
protected pristineState = input<T>();
constructor(protected readonly querySpace:IsolatedQuerySpace) {
}
constructor(
protected readonly querySpace:IsolatedQuerySpace,
) { }
/**
* Get the state value from the current query.

@ -38,8 +38,10 @@ export type WorkPackageDisplayRepresentationValue = 'list'|'card';
@Injectable()
export class WorkPackageViewDisplayRepresentationService extends WorkPackageQueryStateService<string|null> {
public constructor(readonly states:States,
readonly querySpace:IsolatedQuerySpace) {
public constructor(
readonly states:States,
readonly querySpace:IsolatedQuerySpace,
) {
super(querySpace);
}

@ -0,0 +1,66 @@
// -- 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.
//++
import { QueryResource } from 'core-app/features/hal/resources/query-resource';
import { States } from 'core-app/core/states/states.service';
import { IsolatedQuerySpace } from 'core-app/features/work-packages/directives/query-space/isolated-query-space';
import { Injectable } from '@angular/core';
import { WorkPackageQueryStateService } from './wp-view-base.service';
@Injectable()
export class WorkPackageViewIncludeSubprojectsService extends WorkPackageQueryStateService<boolean> {
public constructor(
readonly states:States,
readonly querySpace:IsolatedQuerySpace,
) {
super(querySpace);
}
public hasChanged(query:QueryResource):boolean {
return this.current !== query.includeSubprojects;
}
valueFromQuery(query:QueryResource):boolean {
return query.includeSubprojects || false;
}
public applyToQuery(query:QueryResource):boolean {
const { current } = this;
query.includeSubprojects = current; // eslint-disable-line no-param-reassign
return true;
}
public get current():boolean {
return this.lastUpdatedState.getValueOr(false);
}
public setIncludeSubprojects(include:boolean):void {
this.update(include);
}
}

@ -52,6 +52,7 @@ import { WorkPackageQueryStateService } from 'core-app/features/work-packages/ro
import { WorkPackageStatesInitializationService } from 'core-app/features/work-packages/components/wp-list/wp-states-initialization.service';
import { WorkPackageViewOrderService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-order.service';
import { WorkPackageViewDisplayRepresentationService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-display-representation.service';
import { WorkPackageViewIncludeSubprojectsService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-include-subprojects.service';
import { HalEvent, HalEventsService } from 'core-app/features/hal/services/hal-events.service';
import { DeviceService } from 'core-app/core/browser/device.service';
import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
@ -106,6 +107,8 @@ export abstract class WorkPackagesViewBase extends UntilDestroyedMixin implement
@InjectField() wpDisplayRepresentation:WorkPackageViewDisplayRepresentationService;
@InjectField() wpIncludeSubprojects:WorkPackageViewIncludeSubprojectsService;
@InjectField() halEvents:HalEventsService;
@InjectField() deviceService:DeviceService;
@ -156,6 +159,7 @@ export abstract class WorkPackagesViewBase extends UntilDestroyedMixin implement
this.setupChangeObserver(this.wpTableHighlighting);
this.setupChangeObserver(this.wpTableOrder);
this.setupChangeObserver(this.wpDisplayRepresentation);
this.setupChangeObserver(this.wpIncludeSubprojects);
}
/**

@ -41,11 +41,13 @@ import { WorkPackageViewTimelineService } from 'core-app/features/work-packages/
selector: '[wpViewDropdown]',
})
export class WorkPackageViewDropdownMenuDirective extends OpContextMenuTrigger {
constructor(readonly elementRef:ElementRef,
constructor(
readonly elementRef:ElementRef,
readonly opContextMenu:OPContextMenuService,
readonly I18n:I18nService,
readonly wpDisplayRepresentationService:WorkPackageViewDisplayRepresentationService,
readonly wpTableTimeline:WorkPackageViewTimelineService) {
readonly wpTableTimeline:WorkPackageViewTimelineService,
) {
super(elementRef, opContextMenu);
}

@ -35,7 +35,7 @@
[disabled]="!(areProjectsLoaded$ | async)"
[placeholder]="text.search_placeholder"
name="project-include-search"
[(ngModel)]="query"
[(ngModel)]="searchText"
[ngModelOptions]="{standalone: true}"
data-qa-selector="project-include-search"
>
@ -51,14 +51,24 @@
op-project-list
[projects]="projects$ | async"
[selected]="selectedProjects"
[query]="query"
[includeSubprojects]="includeSubprojects$ | async"
[searchText]="searchText"
(update)="selectedProjects = $event"
data-qa-selector="project-include-list"
></ul>
</ng-container>
<div class="spot-action-bar">
<div class="spot-action-bar--left"></div>
<div class="spot-action-bar--left">
<label class="op-project-include--include-all">
<spot-checkbox
[(ngModel)]="includeSubprojects"
[ngModelOptions]="{standalone: true}"
data-qa-project-include-all-subprojects="1"
></spot-checkbox>
<span class="op-project-include--include-all-text">{{ text.include_subprojects }}</span>
</label>
</div>
<div class="spot-action-bar--right">
<button
[disabled]="!(areProjectsLoaded$ | async)"

@ -32,3 +32,13 @@
&--loading
padding: 0 $spot-spacing-1 0 $spot-spacing-1
&--include-all
display: flex
align-items: center
cursor: pointer
&-text
// A true center aligned text does not look center aligned
padding-top: 1px
margin-left: $spot-spacing-0_5

@ -3,6 +3,7 @@ import {
ChangeDetectionStrategy,
Component,
HostBinding,
OnInit,
} from '@angular/core';
import { HttpClient } from '@angular/common/http';
import {
@ -14,8 +15,8 @@ import {
distinctUntilChanged,
map,
mergeMap,
skip,
take,
skip,
} from 'rxjs/operators';
import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service';
@ -26,6 +27,7 @@ import {
} from 'core-app/core/apiv3/paths/apiv3-list-resource.interface';
import { HalResource } from 'core-app/features/hal/resources/hal-resource';
import { WorkPackageViewFiltersService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-filters.service';
import { WorkPackageViewIncludeSubprojectsService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-include-subprojects.service';
import { QueryFilterInstanceResource } from 'core-app/features/hal/resources/query-filter-instance-resource';
import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin';
import { IHALCollection } from 'core-app/core/apiv3/types/hal-collection.type';
@ -44,7 +46,7 @@ import { getPaginatedResults } from 'core-app/core/apiv3/helpers/get-paginated-r
templateUrl: './project-include.component.html',
styleUrls: ['./project-include.component.sass'],
})
export class OpProjectIncludeComponent extends UntilDestroyedMixin {
export class OpProjectIncludeComponent extends UntilDestroyedMixin implements OnInit {
@HostBinding('class.op-project-include') className = true;
public text = {
@ -55,10 +57,13 @@ export class OpProjectIncludeComponent extends UntilDestroyedMixin {
search_placeholder: this.I18n.t('js.include_projects.search_placeholder'),
clear_selection: this.I18n.t('js.include_projects.clear_selection'),
apply: this.I18n.t('js.include_projects.apply'),
include_subprojects: this.I18n.t('js.include_projects.include_subprojects'),
};
public opened = false;
public query$ = this.wpTableFilters.querySpace.query.values$();
public displayModeOptions = [
{ value: 'all', title: this.text.filter_all },
{ value: 'selected', title: this.text.filter_selected },
@ -77,18 +82,31 @@ export class OpProjectIncludeComponent extends UntilDestroyedMixin {
public displayMode$ = new BehaviorSubject('all');
private _query = '';
private _searchText = '';
public get searchText():string {
return this._searchText;
}
public set searchText(val:string) {
this._searchText = val;
this.searchText$.next(val);
}
public searchText$ = new BehaviorSubject('');
private _includeSubprojects = true;
public get query():string {
return this._query;
public get includeSubprojects():boolean {
return this._includeSubprojects;
}
public set query(val:string) {
this._query = val;
this.query$.next(val);
public set includeSubprojects(val:boolean) {
this._includeSubprojects = val;
this.includeSubprojects$.next(val);
}
public query$ = new BehaviorSubject('');
public includeSubprojects$ = new BehaviorSubject(true);
private _selectedProjects:string[] = [];
@ -128,28 +146,46 @@ export class OpProjectIncludeComponent extends UntilDestroyedMixin {
public projects$ = combineLatest([
this.allProjects$,
this.query$.pipe(distinctUntilChanged()),
this.displayMode$.pipe(distinctUntilChanged()),
this.includeSubprojects$,
this.searchText$.pipe(debounceTime(200)),
])
.pipe(
debounceTime(20),
mergeMap(([projects, query, displayMode]) => this.selectedProjects$.pipe(
debounceTime(50),
mergeMap(([projects, displayMode, includeSubprojects, searchText]) => this.selectedProjects$.pipe(
take(1),
map((selected) => [projects, query, displayMode, selected]),
map((selected) => [projects, displayMode, includeSubprojects, searchText, selected]),
)),
map(
([projects, query, displayMode, selected]:[IProject[], string, string, string[]]) => projects
([projects, displayMode, includeSubprojects, searchText, selected]:[IProject[], string, boolean, string, string[]]) => projects
.filter(
(project) => {
if (displayMode === 'selected' && !selected.includes(project._links.self.href)) {
if (searchText.length) {
const matches = project.name.toLowerCase().includes(searchText.toLowerCase()) || project.identifier.toLowerCase().includes(searchText.toLowerCase());
if (!matches) {
return false;
}
}
if (query.length) {
return project.name.toLowerCase().includes(query.toLowerCase()) || project.identifier.toLowerCase().includes(query.toLowerCase());
if (displayMode !== 'selected') {
return true;
}
if (selected.includes(project._links.self.href)) {
return true;
}
const hasSelectedAncestor = project._links.ancestors.reduce(
(anySelected, ancestor) => anySelected || selected.includes(ancestor.href),
false,
);
if (includeSubprojects && hasSelectedAncestor) {
return true;
}
return false;
},
)
.sort((a, b) => a._links.ancestors.length - b._links.ancestors.length)
@ -165,7 +201,7 @@ export class OpProjectIncludeComponent extends UntilDestroyedMixin {
map((projects) => recursiveSort(projects)),
);
areProjectsLoaded$ = this
public areProjectsLoaded$ = this
.projects$
.pipe(
distinctUntilChanged(),
@ -178,21 +214,13 @@ export class OpProjectIncludeComponent extends UntilDestroyedMixin {
['active', '=', ['t']],
];
if (this.query) {
filters.push([
'name_and_identifier',
'~',
[this.query],
]);
}
return {
filters,
pageSize: -1,
select: [
'elements/id',
'elements/identifier',
'elements/name',
'elements/identifier',
'elements/self',
'elements/ancestors',
'total',
@ -207,23 +235,42 @@ export class OpProjectIncludeComponent extends UntilDestroyedMixin {
readonly I18n:I18nService,
readonly http:HttpClient,
readonly wpTableFilters:WorkPackageViewFiltersService,
readonly wpIncludeSubprojects:WorkPackageViewIncludeSubprojectsService,
readonly halResourceService:HalResourceService,
readonly currentProjectService:CurrentProjectService,
) {
super();
}
public ngOnInit():void {
this.query$
.pipe(
map((query) => query.includeSubprojects),
distinctUntilChanged(),
)
.subscribe((includeSubprojects) => {
this.includeSubprojects = includeSubprojects;
});
}
public toggleIncludeSubprojects():void {
this.wpIncludeSubprojects.setIncludeSubprojects(!this.wpIncludeSubprojects.current);
}
public toggleOpen():void {
this.opened = !this.opened;
if (this.opened) {
this.loadAllProjects();
this.projectsInFilter$
.pipe(take(1))
.subscribe((selectedProjects) => {
this.displayMode = 'all';
this.query = '';
this.searchText = '';
this.selectedProjects = selectedProjects as string[];
});
}
}
public loadAllProjects():void {
getPaginatedResults<IProject>(
@ -251,6 +298,8 @@ export class OpProjectIncludeComponent extends UntilDestroyedMixin {
projectFilter.values = projectHrefs.map((href:string) => this.halResourceService.createHalResource({ href }, true));
});
this.wpIncludeSubprojects.update(this.includeSubprojects);
this.close();
}

@ -4,20 +4,35 @@
>
<label
class="spot-list--item-action op-project-list--item-action"
[ngClass]="{ 'spot-list--item-action_disabled': !project.found }"
[ngClass]="{ 'spot-list--item-action_disabled': isDisabled(project) }"
>
<spot-tooltip
alignment="top-left"
[disabled]="!isDisabled(project)"
>
<ng-container slot="trigger">
<spot-checkbox
*ngFor="let checked of [isChecked(project.href)]"
[checked]="checked"
(change)="changeSelected(project.href)"
[disabled]="!project.found || project.href === currentProjectHref"
[checked]="checked || (includeSubprojects && parentChecked)"
(change)="changeSelected(project)"
[disabled]="isDisabled(project)"
[attr.data-qa-project-include-id]="project.id"
[attr.data-qa-project-include-checked]="checked ? 1 : 0"
[attr.data-qa-project-include-checked]="(checked || (includeSubprojects && parentChecked)) ? 1 : 0"
></spot-checkbox>
<div
class="spot-list--item-title"
[opSearchHighlight]="query"
[opSearchHighlight]="searchText"
>{{ project.name }}</div>
</ng-container>
<p
slot="body"
class="spot-body-small"
>
<span *ngIf="includeSubprojects && parentChecked">{{ text.include_all_selected }}</span>
<span *ngIf="project.href === currentProjectHref">{{ text.current_project }}</span>
</p>
</spot-tooltip>
</label>
<ul
@ -25,7 +40,9 @@
op-project-list
[projects]="project.children"
[selected]="selected"
[query]="query"
[includeSubprojects]="includeSubprojects"
[parentChecked]="parentChecked || isChecked(project.href)"
[searchText]="searchText"
(update)="updateSelected($event)"
></ul>
</li>

@ -27,17 +27,30 @@ export class OpProjectListComponent {
@Input() selected:string[] = [];
@Input() query = '';
@Input() searchText = '';
@Input() includeSubprojects = false;
@Input() parentChecked = false;
public get currentProjectHref():string|null {
return this.currentProjectService.apiv3Path;
}
public text = {
include_all_selected: this.I18n.t('js.include_projects.tooltip.include_all_selected'),
current_project: this.I18n.t('js.include_projects.tooltip.current_project'),
};
constructor(
readonly I18n:I18nService,
readonly currentProjectService:CurrentProjectService,
) { }
public isDisabled(project:IProjectData):boolean {
return project.href === this.currentProjectHref || (this.includeSubprojects && this.parentChecked);
}
public updateSelected(selected:string[]):void {
this.update.emit(selected);
}
@ -46,8 +59,15 @@ export class OpProjectListComponent {
return this.selected.includes(href);
}
public changeSelected(href:string):void {
public changeSelected(project:IProjectData):void {
const { href } = project;
const checked = this.isChecked(href);
const disabled = this.isDisabled(project);
if (disabled) {
return;
}
if (checked) {
this.updateSelected(this.selected.filter((selectedHref) => selectedHref !== href));
} else {

@ -8,15 +8,7 @@ import {
} from '@angular/core';
import { KeyCodes } from 'core-app/shared/helpers/keyCodes.enum';
import { I18nService } from 'core-app/core/i18n/i18n.service';
enum SpotDropModalAlignmentOption {
BottomCenter = 'bottom-center',
BottomLeft = 'bottom-left',
BottomRight = 'bottom-right',
TopCenter = 'top-center',
TopLeft = 'top-left',
TopRight = 'top-right',
}
import SpotDropAlignmentOption from '../../drop-alignment-options';
@Component({
selector: 'spot-drop-modal',
@ -29,7 +21,7 @@ export class SpotDropModalComponent implements OnDestroy {
@Output() closed = new EventEmitter<void>();
@Input() public alignment:SpotDropModalAlignmentOption = SpotDropModalAlignmentOption.BottomLeft;
@Input() public alignment:SpotDropAlignmentOption = SpotDropAlignmentOption.BottomLeft;
@Input('open')
set open(value:boolean) {

@ -0,0 +1,7 @@
<ng-content select="[slot=trigger]"></ng-content>
<div
*ngIf="!disabled"
[ngClass]="['spot-tooltip--body', 'spot-body', alignmentClass]"
>
<ng-content select="[slot=body]"></ng-content>
</div>

@ -0,0 +1,24 @@
import {
Component,
HostBinding,
Input,
ChangeDetectionStrategy,
} from '@angular/core';
import SpotDropAlignmentOption from '../../drop-alignment-options';
@Component({
selector: 'spot-tooltip',
templateUrl: './tooltip.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SpotTooltipComponent {
@HostBinding('class.spot-tooltip') public className = true;
@Input() disabled = false;
@Input() public alignment:SpotDropAlignmentOption = SpotDropAlignmentOption.BottomCenter;
get alignmentClass():string {
return `spot-tooltip--body_${this.alignment}`;
}
}

@ -0,0 +1,19 @@
enum SpotDropAlignmentOption {
TopLeft = 'top-left',
TopCenter = 'top-center',
TopRight = 'top-right',
RightTop = 'right-top',
RightCenter = 'right-center',
RightBottom = 'right-bottom',
BottomLeft = 'bottom-left',
BottomCenter = 'bottom-center',
BottomRight = 'bottom-right',
LeftTop = 'left-top',
LeftCenter = 'left-center',
LeftBottom = 'left-bottom',
}
export default SpotDropAlignmentOption;

@ -8,6 +8,7 @@
<button class="spot-button spot-button_main">Main</button>
<button class="spot-button spot-button_main">
<span class="spot-icon spot-icon_bell"></span>
<span>Main</span>
<span class="spot-icon spot-icon_bell"></span>
</button>
@ -15,6 +16,7 @@
<button class="spot-button spot-button_accent">Accent</button>
<button class="spot-button spot-button_accent">
<span class="spot-icon spot-icon_bell"></span>
<span>Accent</span>
<span class="spot-icon spot-icon_bell"></span>
</button>
@ -22,6 +24,7 @@
<button class="spot-button spot-button_danger">Danger</button>
<button class="spot-button spot-button_danger">
<span class="spot-icon spot-icon_bell"></span>
<span>Danger</span>
<span class="spot-icon spot-icon_bell"></span>
</button>
@ -29,6 +32,7 @@
<button class="spot-button" disabled>Disabled</button>
<button class="spot-button" disabled>
<span class="spot-icon spot-icon_bell"></span>
<span>Disabled</span>
<span class="spot-icon spot-icon_bell"></span>
</button>
@ -114,6 +118,14 @@ Currently selected value is {{ toggleValue }}
<spot-checkbox [(ngModel)]="listCheckboxValue"></spot-checkbox>
<div class="spot-list--item-title">Child checkbox item</div>
</label>
<ul class="spot-list">
<li class="spot-list--item">
<label class="spot-list--item-action">
<spot-checkbox [(ngModel)]="listCheckboxValue"></spot-checkbox>
<div class="spot-list--item-title">Secon level child</div>
</label>
</li>
</ul>
</li>
<li class="spot-list--item">
<label class="spot-list--item-action">
@ -123,7 +135,10 @@ Currently selected value is {{ toggleValue }}
</li>
<li class="spot-list--item">
<label class="spot-list--item-action spot-list--item-action_disabled">
<spot-checkbox [(ngModel)]="listCheckboxValue" disabled></spot-checkbox>
<spot-checkbox
[(ngModel)]="listCheckboxValue"
[disabled]="true"
></spot-checkbox>
<div class="spot-list--item-title">Disabled item</div>
</label>
</li>
@ -217,18 +232,24 @@ Currently selected value is {{ toggleValue }}
<h1>Drop Modal</h1>
<select [(ngModel)]="alignment">
<option>bottom-center</option>
<option>bottom-left</option>
<option>bottom-right</option>
<option>top-center</option>
<select [(ngModel)]="dropModalAlignment">
<option>left-top</option>
<option>left-center</option>
<option>left-bottom</option>
<option>top-left</option>
<option>top-center</option>
<option>top-right</option>
<option>right-top</option>
<option>right-center</option>
<option>right-bottom</option>
<option>bottom-left</option>
<option>bottom-center</option>
<option>bottom-right</option>
</select>
<spot-drop-modal
[open]="dropModalOpen"
[alignment]="alignment"
[alignment]="dropModalAlignment"
(closed)="dropModalOpen = false"
>
<button
@ -254,6 +275,38 @@ Currently selected value is {{ toggleValue }}
</ng-container>
</spot-drop-modal>
<h1>Tooltip</h1>
<select [(ngModel)]="tooltipAlignment">
<option>left-top</option>
<option>left-center</option>
<option>left-bottom</option>
<option>top-left</option>
<option>top-center</option>
<option>top-right</option>
<option>right-top</option>
<option>right-center</option>
<option>right-bottom</option>
<option>bottom-left</option>
<option>bottom-center</option>
<option>bottom-right</option>
</select>
<spot-tooltip [alignment]="tooltipAlignment">
<ng-container slot="trigger">Trigger</ng-container>
<p
slot="body"
class="spot-body-small"
>Body</p>
</spot-tooltip>
<spot-tooltip [alignment]="tooltipAlignment">
<ng-container slot="trigger">Trigger</ng-container>
<ng-container slot="body">
<p class="spot-body-small">Body with multiple paragraphs coming in here</p>
<p class="spot-body-small">This is the second paragraph</p>
</ng-container>
</spot-tooltip>
<h1>Action Bar</h1>
<div class="spot-action-bar">

@ -6,16 +6,25 @@ import { Component } from '@angular/core';
})
export class SpotDocsComponent {
indeterminateState = null;
checkboxValue = null;
listCheckboxValue = null;
textFieldValue = 'ngModel value';
dropModalOpen = false;
alignment = 'bottom-left';
dropModalAlignment = 'bottom-left';
tooltipAlignment = 'right-center';
toggleValue = null;
toggleOptions = [
{ value: 1, title: '1' },
{value: 2, title: '2'}
{ value: 2, title: '2' },
{ value: 3, title: '3' },
];
onRemoveChip() {

@ -10,6 +10,7 @@ import { SpotTextFieldComponent } from './components/text-field/text-field.compo
import { SpotFilterChipComponent } from './components/filter-chip/filter-chip.component';
import { SpotChipFieldComponent } from './components/chip-field/chip-field.component';
import { SpotDropModalComponent } from './components/drop-modal/drop-modal.component';
import { SpotTooltipComponent } from './components/tooltip/tooltip.component';
import { SpotDocsComponent } from './spot-docs.component';
@NgModule({
@ -29,6 +30,7 @@ import { SpotDocsComponent } from './spot-docs.component';
SpotFilterChipComponent,
SpotChipFieldComponent,
SpotDropModalComponent,
SpotTooltipComponent,
],
exports: [
SpotCheckboxComponent,
@ -37,6 +39,7 @@ import { SpotDocsComponent } from './spot-docs.component';
SpotFilterChipComponent,
SpotChipFieldComponent,
SpotDropModalComponent,
SpotTooltipComponent,
],
})
export class OpSpotModule { }

@ -1,20 +1,34 @@
.spot-header-big
@include spot-header-big
padding: 0
margin: 0
.spot-header-small
@include spot-header-small
padding: 0
margin: 0
.spot-subheader-big
@include spot-subheader-big
padding: 0
margin: 0
.spot-subheader-small
@include spot-subheader-small
padding: 0
margin: 0
.spot-body-big
@include spot-body-big
padding: 0
margin: 0
.spot-body-small
@include spot-body-small
padding: 0
margin: 0
.spot-caption
@include spot-caption
padding: 0
margin: 0

@ -6,7 +6,7 @@
align-items: center
justify-content: center
flex-wrap: nowrap
line-height: calc(#{$spot-spacing-2} - 2px) // The border needs to be removed from this
line-height: calc(#{$spot-spacing-1_25} - 2px) // The border needs to be removed from this
min-height: $spot-spacing-2
margin: 0
@ -19,16 +19,6 @@
color: $spot-color-main
cursor: pointer
.spot-icon
width: $spot-spacing-1_5
height: $spot-spacing-1_5
&:first-child
margin-left: calc(-1 * #{$spot-spacing-0_5} - 1px) // The border needs to be removed from this
&:last-child
margin-right: calc(-1 * #{$spot-spacing-0_5} - 1px) // The border needs to be removed from this
&, *
box-sizing: border-box
@ -194,3 +184,27 @@
&:active
color: $spot-color-danger
.spot-icon
width: $spot-spacing-1_5
height: $spot-spacing-1_5
&:first-child
margin-left: calc(-1 * #{$spot-spacing-0_25} - 1px) // The border needs to be removed from this
&:not(:last-child)
margin-right: $spot-spacing-0_25
&:last-child
margin-right: calc(-1 * #{$spot-spacing-0_25} - 1px) // The border needs to be removed from this
&:not(:first-child)
margin-left: $spot-spacing-0_25
&:first-child:last-child
margin: 0 calc(-1 * #{$spot-spacing-0_5} - 1px) // The border needs to be removed from this
.spot-checkbox
margin-right: $spot-spacing-0_75

@ -40,28 +40,54 @@
max-width: calc(100vw - (2 * #{$spot-spacing-1}))
max-height: calc(100vh - (2 * #{$spot-spacing-1}))
&_bottom-center
top: calc(100% + #{$spot-spacing-0_25})
left: 50%
transform: translateX(-50%)
&_left-top
right: calc(100% + #{$spot-spacing-0_25})
top: 0%
&_left-center
right: calc(100% + #{$spot-spacing-0_25})
top: 50%
transform: translateY(-50%)
&_left-bottom
right: calc(100% + #{$spot-spacing-0_25})
bottom: 0%
&_bottom-left
top: calc(100% + #{$spot-spacing-0_25})
left: 0%
&_bottom-center
top: calc(100% + #{$spot-spacing-0_25})
left: 50%
transform: translateX(-50%)
&_bottom-right
top: calc(100% + #{$spot-spacing-0_25})
right: 0%
&_top-center
bottom: calc(100% + #{$spot-spacing-0_25})
left: 50%
transform: translateX(-50%)
&_right-top
left: calc(100% + #{$spot-spacing-0_25})
top: 0%
&_right-center
left: calc(100% + #{$spot-spacing-0_25})
top: 50%
transform: translateY(-50%)
&_right-bottom
left: calc(100% + #{$spot-spacing-0_25})
bottom: 0%
&_top-left
bottom: calc(100% + #{$spot-spacing-0_25})
left: 0%
&_top-center
bottom: calc(100% + #{$spot-spacing-0_25})
left: 50%
transform: translateX(-50%)
&_top-right
bottom: calc(100% + #{$spot-spacing-0_25})
right: 0%

@ -9,3 +9,4 @@
@import './drop-modal'
@import './list'
@import './action-bar'
@import './tooltip'

@ -1,3 +1,5 @@
@use "sass:list"
.spot-list
display: flex
flex-direction: column
@ -18,6 +20,9 @@
border: 0
cursor: pointer
&:hover
background-color: $spot-color-main-light
&_disabled
color: $spot-color-basic-gray-3
cursor: default
@ -28,5 +33,9 @@
&:not(:last-child)
margin-left: $spot-spacing-0_5
.spot-list
margin-left: $spot-spacing-1_5
$item-list: ""
@for $i from 1 through 10
$item-list: list.append($item-list, ".spot-list--item")
#{$item-list} .spot-list--item-action
padding-left: $i * $spot-spacing-1_5

@ -0,0 +1,142 @@
.spot-tooltip
position: relative
display: inline-flex
$indicator-size: $spot-spacing-0_5
&--body
@include spot-z-index("tooltip", 1)
pointer-events: none
opacity: 0
position: absolute
min-width: $spot-spacing-10 + $spot-spacing-2
max-width: 90vw
height: auto
box-shadow: $spot-shadow-light-low
background: $spot-color-main-light
display: flex
flex-direction: column
&::before
display: block
content: ''
position: absolute
border: $indicator-size solid transparent
border-bottom: $indicator-size solid $spot-color-main-light
&_left-top
right: calc(100% + #{$indicator-size})
top: -$indicator-size
&::before
right: -2 * $indicator-size
transform: rotate(90deg)
top: $indicator-size
&_left-center
right: calc(100% + #{$indicator-size})
top: 50%
transform: translateY(-50%)
&::before
right: -2 * $indicator-size
top: 50%
transform: translateY(-50%) rotate(90deg)
&_left-bottom
right: calc(100% + #{$indicator-size})
bottom: -$indicator-size
&::before
right: -2 * $indicator-size
transform: rotate(90deg)
bottom: $indicator-size
&_bottom-left
top: calc(100% + #{$indicator-size})
left: 0%
&::before
top: -2 * $indicator-size
left: $indicator-size
&_bottom-center
top: calc(100% + #{$indicator-size})
left: 50%
transform: translateX(-50%)
&::before
top: -2 * $indicator-size
left: 50%
transform: translateX(-50%)
&_bottom-right
top: calc(100% + #{$indicator-size})
right: 0%
&::before
top: -2 * $indicator-size
right: $indicator-size
&_right-top
left: calc(100% + #{$indicator-size})
top: -$indicator-size
&::before
left: -2 * $indicator-size
transform: rotate(-90deg)
top: $indicator-size
&_right-center
left: calc(100% + #{$indicator-size})
top: 50%
transform: translateY(-50%)
&::before
left: -2 * $indicator-size
top: 50%
transform: translateY(-50%) rotate(-90deg)
&_right-bottom
left: calc(100% + #{$indicator-size})
bottom: -$indicator-size
&::before
left: -2 * $indicator-size
transform: rotate(-90deg)
bottom: $indicator-size
&_top-left
bottom: calc(100% + #{$indicator-size})
left: 0%
&::before
bottom: -2 * $indicator-size
left: $indicator-size
transform: rotate(180deg)
&_top-center
bottom: calc(100% + #{$indicator-size})
left: 50%
transform: translateX(-50%)
&::before
bottom: -2 * $indicator-size
left: 50%
transform: translateX(-50%) rotate(180deg)
&_top-right
bottom: calc(100% + #{$indicator-size})
right: 0%
&::before
bottom: -2 * $indicator-size
right: $indicator-size
transform: rotate(180deg)
&:hover &--body
pointer-events: all
opacity: 1

@ -1,6 +1,6 @@
@use "sass:map"
$spot-z-indexes: ( "header": 20, "drop-modal": 1000)
$spot-z-indexes: ( "header": 20, "tooltip": 500, "drop-modal": 1000)
@mixin spot-z-index($type, $addition: 0)
z-index: map.get($spot-z-indexes, $type) + $addition

@ -30,6 +30,10 @@ $badge-diameter: 1.25rem
.badge
@include badge
width: auto
min-width: 1.25rem
padding-left: 0.3rem
padding-right: 0.3rem
&.-secondary
@include badge-style($secondary-color, auto)

@ -58,7 +58,7 @@ describe 'Calendar project include', type: :feature, js: true do
dropdown.toggle!
dropdown.toggle_checkbox(sub_sub_project.id)
dropdown.click_button 'Apply'
dropdown.expect_count 2
dropdown.expect_count 1
work_package_view.expect_event sub_bug, present: true
work_package_view.expect_event sub_sub_bug
@ -66,7 +66,7 @@ describe 'Calendar project include', type: :feature, js: true do
dropdown.toggle!
dropdown.toggle_checkbox(other_project.id)
dropdown.click_button 'Apply'
dropdown.expect_count 3
dropdown.expect_count 2
work_package_view.expect_event other_task
work_package_view.expect_event other_other_task

@ -226,9 +226,17 @@ describe 'Team planner add existing work packages', type: :feature, js: true do
dropdown.toggle!
dropdown.expect_open
dropdown.toggle_include_all_subprojects
dropdown.expect_checkbox(project.id, true)
dropdown.expect_checkbox(sub_project.id, false)
dropdown.click_button 'Apply'
dropdown.expect_closed
dropdown.expect_count 1
dropdown.toggle!
dropdown.expect_open
dropdown.toggle_checkbox(sub_project.id)
dropdown.expect_checkbox(project.id, true)

@ -75,7 +75,7 @@ describe 'Team planner project include', type: :feature, js: true do
work_package_view.expect_assignee other_user
work_package_view.within_lane(user) do
work_package_view.expect_event task
work_package_view.expect_event task, present: true
work_package_view.expect_event sub_bug, present: true
work_package_view.expect_event sub_sub_bug, present: true
end
@ -88,7 +88,7 @@ describe 'Team planner project include', type: :feature, js: true do
dropdown.toggle!
dropdown.toggle_checkbox(sub_sub_project.id)
dropdown.click_button 'Apply'
dropdown.expect_count 2
dropdown.expect_count 1
work_package_view.within_lane(user) do
work_package_view.expect_event task
@ -99,7 +99,7 @@ describe 'Team planner project include', type: :feature, js: true do
dropdown.toggle!
dropdown.toggle_checkbox(other_project.id)
dropdown.click_button 'Apply'
dropdown.expect_count 3
dropdown.expect_count 2
work_package_view.within_lane(other_user) do
work_package_view.expect_event other_task

@ -91,11 +91,20 @@ describe 'Team planner', type: :feature, js: true do
project: project,
type: type_bug,
assigned_to: other_user,
status: closed_status,
start_date: Time.zone.today - 1.day,
due_date: Time.zone.today + 1.day,
subject: 'Another task for the other user'
end
let!(:closed_bug) do
create :work_package,
project: project,
type: type_bug,
assigned_to: other_user,
status: closed_status,
start_date: Time.zone.today - 1.day,
due_date: Time.zone.today + 1.day,
subject: 'Closed bug'
end
let!(:user_bug) do
create :work_package,
project: project,
@ -116,6 +125,7 @@ describe 'Team planner', type: :feature, js: true do
team_planner.title
team_planner.wait_for_loaded
team_planner.expect_empty_state
team_planner.expect_assignee(user, present: false)
team_planner.expect_assignee(other_user, present: false)
@ -144,6 +154,7 @@ describe 'Team planner', type: :feature, js: true do
team_planner.within_lane(other_user) do
team_planner.expect_event other_task
team_planner.expect_event other_bug
team_planner.expect_event closed_bug, present: false
end
# Add filter for type task
@ -160,6 +171,7 @@ describe 'Team planner', type: :feature, js: true do
team_planner.within_lane(other_user) do
team_planner.expect_event other_task
team_planner.expect_event other_bug, present: false
team_planner.expect_event closed_bug, present: false
end
# Open the split view for that task and change to bug

@ -70,7 +70,7 @@ describe 'Team planner constraints for a subproject', type: :feature, js: true d
project_include.toggle!
project_include.toggle_checkbox(subproject.id)
project_include.click_button 'Apply'
project_include.expect_count 2
project_include.expect_count 1
team_planner.within_lane(user) do
team_planner.expect_event work_package

@ -244,5 +244,15 @@ module Pages
def y_center(element)
element.native.location.y + (element.native.size.height / 2)
end
def wait_for_loaded
expect(page).to have_selector('.op-team-planner--wp-loading-skeleton')
retry_block do
raise "Should not be there" if page.has_selector?('.op-team-planner--wp-loading-skeleton')
sleep 5
end
end
end
end

@ -17,7 +17,7 @@ in
buildPackages.ruby_2_7
postgresql
nodejs
tightvnc
tigervnc
bundix
docker-compose
google-chrome

@ -36,18 +36,40 @@ shared_examples 'has a project include dropdown', type: :feature, js: true do
end
shared_let(:sub_project) do
create(:project, name: 'Child', parent: project, enabled_module_names: enabled_modules)
create(:project, name: 'Direct Child', parent: project, enabled_module_names: enabled_modules)
end
shared_let(:sub_sub_project) do
create(:project, name: 'Grandchild', parent: sub_project, enabled_module_names: enabled_modules)
create(:project, name: 'Direct Grandchild', parent: sub_project, enabled_module_names: enabled_modules)
end
shared_let(:other_project) do
create(:project, name: 'Other project', enabled_module_names: enabled_modules)
end
shared_let(:other_sub_project) do
create(:project, name: 'Other Child', parent: other_project, enabled_module_names: enabled_modules)
end
shared_let(:other_sub_sub_project) do
create(:project, name: 'First other sub sub child', parent: other_sub_project, enabled_module_names: enabled_modules)
end
shared_let(:another_sub_sub_project) do
create(:project, name: 'Second other sub sub child', parent: other_sub_project, enabled_module_names: enabled_modules)
end
shared_let(:user) do
create :user,
member_in_projects: [project, sub_project, sub_sub_project, other_project],
member_in_projects: [
project,
sub_project,
sub_sub_project,
other_project,
other_sub_project,
other_sub_sub_project,
another_sub_sub_project
],
member_with_permissions: permissions
end
@ -55,7 +77,15 @@ shared_examples 'has a project include dropdown', type: :feature, js: true do
create :user,
firstname: 'Other',
lastname: 'User',
member_in_projects: [project, other_project, sub_project, sub_sub_project],
member_in_projects: [
project,
sub_project,
sub_sub_project,
other_project,
other_sub_project,
other_sub_sub_project,
another_sub_sub_project
],
member_with_permissions: permissions
end
@ -122,8 +152,15 @@ shared_examples 'has a project include dropdown', type: :feature, js: true do
sub_project.types << type_task
sub_sub_project.types << type_bug
sub_sub_project.types << type_task
other_project.types << type_bug
other_project.types << type_task
other_sub_project.types << type_bug
other_sub_project.types << type_task
other_sub_sub_project.types << type_bug
other_sub_sub_project.types << type_task
another_sub_sub_project.types << type_bug
another_sub_sub_project.types << type_task
login_as current_user
work_package_view.visit!
@ -134,24 +171,42 @@ shared_examples 'has a project include dropdown', type: :feature, js: true do
dropdown.toggle!
dropdown.expect_open
dropdown.expect_checkbox(other_project.id)
dropdown.expect_checkbox(other_sub_project.id)
dropdown.expect_checkbox(other_sub_sub_project.id)
dropdown.expect_checkbox(another_sub_sub_project.id)
dropdown.expect_checkbox(project.id, true)
dropdown.expect_checkbox(sub_project.id, true)
dropdown.expect_checkbox(sub_sub_project.id, true)
dropdown.toggle_include_all_subprojects
dropdown.expect_checkbox(other_project.id)
dropdown.expect_checkbox(other_sub_project.id)
dropdown.expect_checkbox(other_sub_sub_project.id)
dropdown.expect_checkbox(another_sub_sub_project.id)
dropdown.expect_checkbox(project.id, true)
dropdown.expect_checkbox(sub_project.id)
dropdown.expect_checkbox(sub_sub_project.id)
dropdown.toggle_checkbox(project.id)
dropdown.toggle_checkbox(other_project.id)
dropdown.toggle_checkbox(other_sub_project.id)
dropdown.toggle_checkbox(sub_sub_project.id)
dropdown.expect_checkbox(other_project.id)
dropdown.expect_checkbox(other_sub_project.id, true)
dropdown.expect_checkbox(other_sub_sub_project.id)
dropdown.expect_checkbox(another_sub_sub_project.id)
dropdown.expect_checkbox(project.id, true)
dropdown.expect_checkbox(other_project.id, true)
dropdown.expect_checkbox(sub_project.id)
dropdown.expect_checkbox(sub_sub_project.id, true)
dropdown.toggle_checkbox(sub_sub_project.id)
dropdown.expect_checkbox(other_project.id)
dropdown.expect_checkbox(other_sub_project.id, true)
dropdown.expect_checkbox(other_sub_sub_project.id)
dropdown.expect_checkbox(another_sub_sub_project.id)
dropdown.expect_checkbox(project.id, true)
dropdown.expect_checkbox(other_project.id, true)
dropdown.expect_checkbox(sub_project.id)
dropdown.expect_checkbox(sub_sub_project.id)
@ -172,12 +227,28 @@ shared_examples 'has a project include dropdown', type: :feature, js: true do
dropdown.toggle!
dropdown.toggle_include_all_subprojects
dropdown.expect_checkbox(other_project.id)
dropdown.expect_checkbox(other_sub_project.id, true)
dropdown.expect_checkbox(other_sub_sub_project.id, true)
dropdown.expect_checkbox(another_sub_sub_project.id, true)
dropdown.expect_checkbox(project.id, true)
dropdown.expect_checkbox(sub_project.id, true)
dropdown.expect_checkbox(sub_sub_project.id, true)
dropdown.toggle_include_all_subprojects
dropdown.expect_checkbox(other_project.id)
dropdown.expect_checkbox(other_sub_project.id, true)
dropdown.expect_checkbox(other_sub_sub_project.id)
dropdown.expect_checkbox(another_sub_sub_project.id)
dropdown.expect_checkbox(project.id, true)
dropdown.expect_checkbox(other_project.id, true)
dropdown.expect_checkbox(sub_project.id)
dropdown.expect_checkbox(sub_sub_project.id, true)
dropdown.toggle_checkbox(sub_sub_project.id)
dropdown.click_button 'Apply'
dropdown.expect_closed
dropdown.expect_count 2
@ -188,12 +259,44 @@ shared_examples 'has a project include dropdown', type: :feature, js: true do
dropdown.toggle!
dropdown.expect_open
dropdown.toggle_checkbox(project.id)
dropdown.toggle_checkbox(other_project.id)
dropdown.toggle_checkbox(project.id)
dropdown.toggle_checkbox(sub_sub_project.id)
dropdown.expect_checkbox(project.id, true)
dropdown.expect_checkbox(other_project.id, true)
dropdown.expect_checkbox(other_sub_project.id, true)
dropdown.expect_checkbox(other_sub_sub_project.id, true)
dropdown.expect_checkbox(another_sub_sub_project.id, true)
dropdown.expect_checkbox(project.id, true)
dropdown.expect_checkbox(sub_project.id, true)
dropdown.expect_checkbox(sub_sub_project.id, true)
dropdown.click_button 'Apply'
dropdown.expect_closed
dropdown.expect_count 2
dropdown.toggle!
dropdown.click_button 'Clear selection'
dropdown.expect_checkbox(other_project.id)
dropdown.expect_checkbox(other_sub_project.id)
dropdown.expect_checkbox(other_sub_sub_project.id)
dropdown.expect_checkbox(another_sub_sub_project.id)
dropdown.expect_checkbox(project.id, true)
dropdown.expect_checkbox(sub_project.id, true)
dropdown.expect_checkbox(sub_sub_project.id, true)
dropdown.toggle_include_all_subprojects
dropdown.toggle_checkbox(other_sub_project.id)
dropdown.toggle_checkbox(sub_sub_project.id)
dropdown.expect_checkbox(other_project.id)
dropdown.expect_checkbox(other_sub_project.id, true)
dropdown.expect_checkbox(other_sub_sub_project.id)
dropdown.expect_checkbox(another_sub_sub_project.id)
dropdown.expect_checkbox(project.id, true)
dropdown.expect_checkbox(sub_project.id)
dropdown.expect_checkbox(sub_sub_project.id, true)
@ -205,8 +308,11 @@ shared_examples 'has a project include dropdown', type: :feature, js: true do
dropdown.click_button 'Clear selection'
dropdown.expect_checkbox(project.id, true)
dropdown.expect_checkbox(other_project.id)
dropdown.expect_checkbox(other_sub_project.id)
dropdown.expect_checkbox(other_sub_sub_project.id)
dropdown.expect_checkbox(another_sub_sub_project.id)
dropdown.expect_checkbox(project.id, true)
dropdown.expect_checkbox(sub_project.id)
dropdown.expect_checkbox(sub_sub_project.id)
@ -220,20 +326,25 @@ shared_examples 'has a project include dropdown', type: :feature, js: true do
dropdown.toggle!
dropdown.expect_open
dropdown.toggle_checkbox(other_project.id)
dropdown.toggle_checkbox(sub_sub_project.id)
retry_block do
dropdown.search sub_sub_project.name
dropdown.expect_checkbox(project.id, true)
dropdown.expect_no_checkbox(other_project.id)
dropdown.expect_checkbox(sub_project.id)
dropdown.expect_no_checkbox(other_sub_project.id)
dropdown.expect_no_checkbox(other_sub_sub_project.id)
dropdown.expect_no_checkbox(another_sub_sub_project.id)
dropdown.expect_checkbox(project.id, true)
dropdown.expect_checkbox(sub_project.id, true)
dropdown.expect_checkbox(sub_sub_project.id, true)
end
retry_block do
dropdown.search other_project.name
dropdown.expect_checkbox(other_project.id, true)
dropdown.expect_checkbox(other_project.id)
dropdown.expect_no_checkbox(other_sub_project.id)
dropdown.expect_no_checkbox(other_sub_sub_project.id)
dropdown.expect_no_checkbox(another_sub_sub_project.id)
dropdown.expect_no_checkbox(project.id)
dropdown.expect_no_checkbox(sub_project.id)
dropdown.expect_no_checkbox(sub_sub_project.id)
@ -241,55 +352,88 @@ shared_examples 'has a project include dropdown', type: :feature, js: true do
retry_block do
dropdown.search ''
dropdown.expect_checkbox(other_project.id)
dropdown.expect_checkbox(other_sub_project.id)
dropdown.expect_checkbox(other_sub_sub_project.id)
dropdown.expect_checkbox(another_sub_sub_project.id)
dropdown.expect_checkbox(project.id, true)
dropdown.expect_checkbox(other_project.id, true)
dropdown.expect_checkbox(sub_project.id)
dropdown.expect_checkbox(sub_project.id, true)
dropdown.expect_checkbox(sub_sub_project.id, true)
end
dropdown.toggle_checkbox(other_sub_sub_project.id)
retry_block do
dropdown.set_filter_selected true
dropdown.expect_checkbox(other_project.id)
dropdown.expect_checkbox(other_sub_project.id)
dropdown.expect_checkbox(other_sub_sub_project.id, true)
dropdown.expect_no_checkbox(another_sub_sub_project.id)
dropdown.expect_checkbox(project.id, true)
dropdown.expect_checkbox(other_project.id, true)
dropdown.expect_checkbox(sub_project.id)
dropdown.expect_checkbox(sub_project.id, true)
dropdown.expect_checkbox(sub_sub_project.id, true)
end
retry_block do
dropdown.set_filter_selected false
dropdown.toggle_checkbox(other_project.id)
dropdown.set_filter_selected true
retry_block do
dropdown.expect_checkbox(other_project.id, true)
dropdown.expect_checkbox(other_sub_project.id, true)
dropdown.expect_checkbox(other_sub_sub_project.id, true)
dropdown.expect_no_checkbox(another_sub_sub_project.id)
dropdown.expect_checkbox(project.id, true)
dropdown.expect_checkbox(sub_project.id)
dropdown.expect_checkbox(sub_project.id, true)
dropdown.expect_checkbox(sub_sub_project.id, true)
end
dropdown.expect_no_checkbox(other_project.id)
dropdown.toggle_include_all_subprojects
retry_block do
dropdown.expect_checkbox(other_project.id, true)
dropdown.expect_checkbox(other_sub_project.id)
dropdown.expect_checkbox(other_sub_sub_project.id, true)
dropdown.expect_no_checkbox(another_sub_sub_project.id)
dropdown.expect_checkbox(project.id, true)
dropdown.expect_no_checkbox(sub_project.id)
dropdown.expect_no_checkbox(sub_sub_project.id)
end
retry_block do
dropdown.search other_project.name
dropdown.expect_checkbox(other_project.id, true)
dropdown.expect_no_checkbox(other_sub_project.id)
dropdown.expect_no_checkbox(other_sub_sub_project.id)
dropdown.expect_no_checkbox(another_sub_sub_project.id)
dropdown.expect_no_checkbox(project.id)
dropdown.expect_no_checkbox(other_project.id)
dropdown.expect_no_checkbox(sub_project.id)
dropdown.expect_no_checkbox(sub_sub_project.id)
end
retry_block do
dropdown.search ''
dropdown.expect_checkbox(project.id, true)
dropdown.expect_checkbox(sub_project.id)
dropdown.expect_checkbox(sub_sub_project.id, true)
dropdown.expect_no_checkbox(other_project.id)
dropdown.expect_checkbox(other_project.id, true)
dropdown.expect_no_checkbox(other_sub_project.id)
dropdown.expect_no_checkbox(other_sub_sub_project.id)
dropdown.expect_no_checkbox(another_sub_sub_project.id)
dropdown.expect_checkbox(project.id, true)
dropdown.expect_no_checkbox(sub_project.id)
dropdown.expect_no_checkbox(sub_sub_project.id)
end
retry_block do
dropdown.set_filter_selected false
dropdown.expect_checkbox(other_project.id, true)
dropdown.expect_checkbox(other_sub_project.id)
dropdown.expect_checkbox(other_sub_sub_project.id, true)
dropdown.expect_checkbox(another_sub_sub_project.id)
dropdown.expect_checkbox(project.id, true)
dropdown.expect_checkbox(other_project.id)
dropdown.expect_checkbox(sub_project.id)
dropdown.expect_checkbox(sub_sub_project.id, true)
dropdown.expect_checkbox(sub_sub_project.id)
end
end
end

@ -51,7 +51,7 @@ describe 'Calendar project include', type: :feature, js: true do
dropdown.toggle!
dropdown.toggle_checkbox(sub_sub_project.id)
dropdown.click_button 'Apply'
dropdown.expect_count 2
dropdown.expect_count 1
work_package_view.expect_work_package_listed(task, other_task, sub_sub_bug, sub_bug)
work_package_view.ensure_work_package_not_listed!(other_other_task)
@ -59,7 +59,7 @@ describe 'Calendar project include', type: :feature, js: true do
dropdown.toggle!
dropdown.toggle_checkbox(other_project.id)
dropdown.click_button 'Apply'
dropdown.expect_count 3
dropdown.expect_count 2
work_package_view.expect_work_package_listed(task, other_task, sub_sub_bug, sub_bug, other_other_task)

@ -306,5 +306,17 @@ describe ::API::V3::ParseQueryParamsService,
let(:expected) { { timeline_labels: input.stringify_keys } }
end
end
context 'with includeSubprojects' do
it_behaves_like 'transforms' do
let(:params) { { includeSubprojects: 'true' } }
let(:expected) { { include_subprojects: true } }
end
it_behaves_like 'transforms' do
let(:params) { { includeSubprojects: 'false' } }
let(:expected) { { include_subprojects: false } }
end
end
end
end

@ -162,5 +162,33 @@ describe UpdateQueryFromParamsService,
.to eq(:none)
end
end
context 'when using include subprojects' do
let(:params) do
{ include_subprojects: include_subprojects }
end
context 'when true' do
let(:include_subprojects) { true }
it 'sets the display_representation' do
subject
expect(query.include_subprojects)
.to be true
end
end
context 'when false' do
let(:include_subprojects) { false }
it 'sets the display_representation' do
subject
expect(query.include_subprojects)
.to be false
end
end
end
end
end

@ -33,6 +33,11 @@ module Components
def initialize; end
def clear_tooltips
# Just hover anything else
page.find("[data-qa-selector='project-include-search']").hover
end
def toggle!
page.find("[data-qa-selector='project-include-button']").click
end
@ -45,8 +50,13 @@ module Components
expect(page).to have_selector("[data-qa-selector='project-include-button'] .badge", text: count)
end
def toggle_include_all_subprojects
page.find("[data-qa-project-include-all-subprojects]").click
end
def toggle_checkbox(project_id)
page.find("[data-qa-project-include-id='#{project_id}']").click
clear_tooltips
end
def set_filter_selected(filter)

Loading…
Cancel
Save