Merge remote-tracking branch 'origin/release/11.0' into dev

fix/34436-edit-backlog-date-focus-backlog-details-firefox-quirk
Wieland Lindenthal 4 years ago
commit a6c5c5385a
  1. 16
      app/views/layouts/base.html.erb
  2. 6
      docs/release-notes/11-0-0/README.md
  3. 2
      frontend/src/app/globals/global-listeners.ts
  4. 6
      frontend/src/app/globals/openproject.ts
  5. 27
      frontend/src/app/modules/common/drag-and-drop/reorder-delta-builder.spec.ts
  6. 3
      frontend/src/app/modules/common/drag-and-drop/reorder-delta-builder.ts
  7. 25
      frontend/src/app/modules/common/op-date-picker/datepicker.ts
  8. 14
      frontend/src/app/modules/fields/edit/edit-form/edit-form.component.ts
  9. 84
      frontend/src/app/modules/fields/edit/services/global-edit-form-changes-tracker/global-edit-form-changes-tracker.service.spec.ts
  10. 37
      frontend/src/app/modules/fields/edit/services/global-edit-form-changes-tracker/global-edit-form-changes-tracker.service.ts
  11. 6
      frontend/src/global_styles/layout/_main_menu.sass

@ -149,15 +149,13 @@ See docs/COPYRIGHT.rdoc for more details.
<h1 class="hidden-for-sighted"><%= t(:label_main_menu) %></h1>
<main-menu-resizer></main-menu-resizer>
<div id="menu-sidebar">
<div class="main-menu-wrapper">
<%= main_menu %>
<%= content_for :main_menu %>
<%= call_hook :view_layouts_base_main_menu %>
<!-- Sidebar -->
<div id="sidebar">
<%= content_for :sidebar %>
<%= call_hook :view_layouts_base_sidebar %>
</div>
<%= main_menu %>
<%= content_for :main_menu %>
<%= call_hook :view_layouts_base_main_menu %>
<!-- Sidebar -->
<div id="sidebar">
<%= content_for :sidebar %>
<%= call_hook :view_layouts_base_sidebar %>
</div>
</div>
</div>

@ -3,7 +3,7 @@ title: OpenProject 11.0.0
sidebar_navigation:
title: 11.0.0
release_version: 11.0.0
release_date: 2020-09-29
release_date: 2020-10-07
---
# OpenProject 11.0.0
@ -285,7 +285,7 @@ There are lots and lots of new things we packed into 11.0 to tell you about.
- Changed: Gantt chart: Make row's background translucent when hovered \[[#34028](https://community.openproject.com/wp/34028)\]
- Changed: change icons for manual scheduling mode \[[#34058](https://community.openproject.com/wp/34058)\]
- Changed: Improve board creation modal \[[#34070](https://community.openproject.com/wp/34070)\]
- Changed: Hide derived(Start|Finish)Date from work package forms \[[#34122](https://community.openproject.com/wp/34122)\]
- Changed: Hide derived (Start Finish) Date from work package forms \[[#34122](https://community.openproject.com/wp/34122)\]
- Changed: Extend search autocompleter with useful information \[[#34132](https://community.openproject.com/wp/34132)\]
- Changed: [all projects overview] (Add option to) show status-text in expanded view. \[[#34191](https://community.openproject.com/wp/34191)\]
- Changed: Map board subtasks columns when copying projects \[[#34238](https://community.openproject.com/wp/34238)\]
@ -332,4 +332,4 @@ Special thanks go to all OpenProject contributors without whom this release woul
## What’s next?
We are continuously developing new features and improvements for OpenProject. If you’d like to preview what’s coming in the next release, be sure to check out our [development roadmap](https://community.openproject.com/projects/openproject/work_packages?query_id=918).
We are continuously developing new features and improvements for OpenProject. If you’d like to preview what’s coming in the next release, be sure to check out our [development roadmap](https://community.openproject.com/projects/openproject/work_packages?query_id=918).

@ -96,7 +96,7 @@ import {detectOnboardingTour} from "core-app/globals/onboarding/onboarding_tour_
// Cancel the event
event.preventDefault();
// Chrome requires returnValue to be set
event.returnValue = '';
event.returnValue = I18n.t("js.work_packages.confirm_edit_cancel");
}
});

@ -45,6 +45,12 @@ export class OpenProject {
/** Globally setable variable whether the page form is submitted.
* Necessary to avoid a data loss warning on beforeunload */
public pageIsSubmitted:boolean = false;
/** Globally setable variable whether any of the EditFormComponent
* contain changes.
* Necessary to show a data loss warning on beforeunload when clicking
* on a link out of the Angular app (ie: main side menu)
* */
public editFormsContainModelChanges:boolean;
public getPluginContext():Promise<OpenProjectPluginContext> {
return this.pluginContext

@ -40,14 +40,17 @@ describe('ReorderDeltaBuilder', () => {
wpId:string,
positions:QueryOrder,
wps:string[] = work_packages,
fromIndex:number|null = null) {
fromIndex:number|null = null,
toIndex:number|null = null) {
// As work_packages is already the list with moved element, simply compute the index
let index:number = work_packages.indexOf(wpId);
if (!toIndex) {
toIndex = work_packages.indexOf(wpId);
if (index === -1) {
throw "Invalid wpId given for work_packages, must be contained.";
if (toIndex === -1) {
throw "Invalid wpId given for work_packages, must be contained.";
}
}
return new ReorderDeltaBuilder(wps, positions, wpId, index, fromIndex).buildDelta();
return new ReorderDeltaBuilder(wps, positions, wpId, toIndex, fromIndex).buildDelta();
}
it('Empty, inserting at beginning sets the delta for wpId 1 to the default value', () => {
@ -263,6 +266,20 @@ describe('ReorderDeltaBuilder', () => {
});
});
it('reorders on incomplete positions information and moving the first (0 positioned) work package', () => {
// This can happen if a query is saved and the filters on it changed later on so that
// additional work packages are now present.
let delta = buildDelta('1', { '1': 0, '5': 8196 }, ['2', '3', '4', '1', '5'], 0, 3);
expect(delta).toEqual({
'2': 0,
'3': 2049,
'4': 4098,
'1': 6147,
'5': 8196
});
});
// It will reassign default orders when not in ascending order and min/max not sufficient
});

@ -231,7 +231,8 @@ export class ReorderDeltaBuilder {
* @param wpId
*/
private livePosition(wpId:string):number|undefined {
return this.delta[wpId] || this.positions[wpId];
// Explicitly check for undefined here as the delta might be 0 which is falsey.
return this.delta[wpId] === undefined ? this.positions[wpId] : this.delta[wpId];
}
/**

@ -37,6 +37,7 @@ export class DatePicker {
private datepickerCont:JQuery = jQuery(this.datepickerElemIdentifier);
public datepickerInstance:Instance;
private reshowTimeout:any;
constructor(private datepickerElemIdentifier:string,
private date:any,
@ -77,6 +78,8 @@ export class DatePicker {
}
this.datepickerInstance = Array.isArray(datePickerInstances) ? datePickerInstances[0] : datePickerInstances;
document.addEventListener('scroll', this.hideDuringScroll, true);
}
public clear() {
@ -93,12 +96,12 @@ export class DatePicker {
this.datepickerInstance.close();
}
this.datepickerCont.scrollParent().off('scroll');
document.removeEventListener('scroll', this.hideDuringScroll, true);
}
public show() {
this.datepickerInstance.open();
this.hideDuringScroll();
document.addEventListener('scroll', this.hideDuringScroll, true);
}
public setDates(dates:DateOption|DateOption[]) {
@ -109,26 +112,22 @@ export class DatePicker {
return this.datepickerInstance.isOpen;
}
private hideDuringScroll() {
let reshowTimeout:any = null;
let scrollParent = this.datepickerCont.scrollParent();
scrollParent.scroll(() => {
private hideDuringScroll = () => {
this.datepickerInstance.close();
if (reshowTimeout) {
clearTimeout(reshowTimeout);
if (this.reshowTimeout) {
clearTimeout(this.reshowTimeout);
}
reshowTimeout = setTimeout(() => {
this.reshowTimeout = setTimeout(() => {
if (this.visibleAndActive()) {
this.datepickerInstance.open();
}
}, 50);
});
}
private visibleAndActive() {
var input = this.datepickerCont;
const input = this.datepickerCont;
try {
return document.elementFromPoint(input.offset()!.left, input.offset()!.top) === input[0] &&
@ -137,5 +136,5 @@ export class DatePicker {
console.error("Failed to test visibleAndActive " + e);
return false;
}
};
}
}

@ -44,6 +44,7 @@ import {EditFieldHandler} from "core-app/modules/fields/edit/editing-portal/edit
import {EditingPortalService} from "core-app/modules/fields/edit/editing-portal/editing-portal-service";
import {EditFormRoutingService} from "core-app/modules/fields/edit/edit-form/edit-form-routing.service";
import {ResourceChangesetCommit} from "core-app/modules/fields/edit/services/hal-resource-editing.service";
import {GlobalEditFormChangesTrackerService} from "core-app/modules/fields/edit/services/global-edit-form-changes-tracker/global-edit-form-changes-tracker.service";
@Component({
selector: 'edit-form,[edit-form]',
@ -67,7 +68,8 @@ export class EditFormComponent extends EditForm<HalResource> implements OnInit,
protected readonly editingPortalService:EditingPortalService,
protected readonly $state:StateService,
protected readonly I18n:I18nService,
@Optional() protected readonly editFormRouting:EditFormRoutingService) {
@Optional() protected readonly editFormRouting:EditFormRoutingService,
private globalEditFormChangesTrackerService:GlobalEditFormChangesTrackerService) {
super(injector);
const confirmText = I18n.t('js.work_packages.confirm_edit_cancel');
@ -92,18 +94,20 @@ export class EditFormComponent extends EditForm<HalResource> implements OnInit,
});
}
ngOnDestroy() {
this.unregisterListener();
}
ngOnInit() {
this.editMode = this.initializeEditMode;
this.globalEditFormChangesTrackerService.addToActiveForms(this);
if (this.initializeEditMode) {
this.start();
}
}
ngOnDestroy() {
this.unregisterListener();
this.globalEditFormChangesTrackerService.removeFromActiveForms(this);
}
public async activateField(form:EditForm, schema:IFieldSchema, fieldName:string, errors:string[]):Promise<EditFieldHandler> {
return this.waitForField(fieldName).then((ctrl) => {
ctrl.setActive(true);

@ -0,0 +1,84 @@
import { TestBed } from '@angular/core/testing';
import { GlobalEditFormChangesTrackerService } from './global-edit-form-changes-tracker.service';
import {EditFormComponent} from "core-app/modules/fields/edit/edit-form/edit-form.component";
describe('GlobalEditFormChangesTrackerService', () => {
let service:GlobalEditFormChangesTrackerService;
const createForm = (changed?:boolean) => {
return {
change: {
isEmpty: () => !changed
}
} as EditFormComponent;
};
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(GlobalEditFormChangesTrackerService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should report no changes when empty', () => {
expect(service.thereAreFormsWithUnsavedChanges).toBeFalse();
});
it('should report no changes when one form has no changes', () => {
const form = createForm();
service.addToActiveForms(form);
expect(service.thereAreFormsWithUnsavedChanges).toBeFalse();
});
it('should report no changes when multiple forms have no changes', () => {
const form = createForm();
const form2 = createForm();
const form3 = createForm();
service.addToActiveForms(form);
service.addToActiveForms(form2);
service.addToActiveForms(form3);
expect(service.thereAreFormsWithUnsavedChanges).toBeFalse();
});
it('should report no changes when the only form with changes is removed', () => {
const form = createForm(true);
service.addToActiveForms(form);
service.removeFromActiveForms(form);
expect(service.thereAreFormsWithUnsavedChanges).toBeFalse();
});
it('should report changes when one form has changes', () => {
const form = createForm(true);
service.addToActiveForms(form);
expect(service.thereAreFormsWithUnsavedChanges).toBeTrue();
});
it('should report forms with changes when multiple form have changes', () => {
const form = createForm(true);
const form2 = createForm(true);
const form3 = createForm();
service.addToActiveForms(form);
service.addToActiveForms(form2);
service.addToActiveForms(form3);
expect(service.thereAreFormsWithUnsavedChanges).toBeTrue();
});
it('should call thereAreFormsWithUnsavedChangesSpy on beforeunload', () => {
const thereAreFormsWithUnsavedChangesSpy = spyOnProperty(service, 'thereAreFormsWithUnsavedChanges', 'get');
window.dispatchEvent(new Event('beforeunload'));
expect(thereAreFormsWithUnsavedChangesSpy).toHaveBeenCalled();
});
});

@ -0,0 +1,37 @@
import { Injectable } from '@angular/core';
import {EditFormComponent} from "core-app/modules/fields/edit/edit-form/edit-form.component";
import {I18nService} from "core-app/modules/common/i18n/i18n.service";
@Injectable({
providedIn: 'root'
})
export class GlobalEditFormChangesTrackerService {
private activeForms = new Map<EditFormComponent, boolean>();
get thereAreFormsWithUnsavedChanges () {
return Array.from(this.activeForms.keys()).some(form => {
return !form.change.isEmpty();
});
}
constructor(
private i18nService:I18nService,
) {
// Global beforeunload hook to show a data loss warn
// when the user clicks on a link out of the Angular app
window.addEventListener('beforeunload', (event) => {
if (this.thereAreFormsWithUnsavedChanges) {
event.preventDefault();
event.returnValue = this.i18nService.t('js.work_packages.confirm_edit_cancel');
}
});
}
public addToActiveForms(form:EditFormComponent) {
this.activeForms.set(form, true);
}
public removeFromActiveForms(form:EditFormComponent) {
this.activeForms.delete(form);
}
}

@ -67,11 +67,6 @@ $menu-item-line-height: 30px
height: calc(100% - (var(--main-menu-item-height) + 10px)) // 10px spacing
overflow: auto
@include styled-scroll-bar
.main-menu-wrapper
display: flex
flex-direction: column
height: 100%
a:focus
color: var(--main-menu-font-color)
@ -79,7 +74,6 @@ $menu-item-line-height: 30px
ul
margin: 0
padding: 0
overflow-y: auto
// -------------------- ALL menu items ---------------------------
li

Loading…
Cancel
Save