Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 24 additions & 23 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"@types/dragula": "^3.7.5",
"@types/flot": "^0.0.36",
"@types/hammerjs": "^2.0.36",
"@types/hotwired__turbo": "^8.0.5",
"@types/hotwired__turbo": "^8.0.10",
"@types/jquery": "^3.5.33",
"@types/jqueryui": "^1.12.24",
Comment thread
myabc marked this conversation as resolved.
"@types/lodash": "^4.17.23",
Expand Down Expand Up @@ -97,8 +97,8 @@
"@github/webauthn-json": "^2.1.1",
"@hocuspocus/provider": "^3.4.4",
"@hotwired/stimulus": "^3.2.2",
"@hotwired/turbo": "^8.0.20",
"@hotwired/turbo-rails": "^8.0.20",
"@hotwired/turbo": "^8.0.23",
"@hotwired/turbo-rails": "^8.0.23",
Comment thread
myabc marked this conversation as resolved.
"@knowledgecode/delegate": "^0.10.3",
"@kolkov/ngx-gallery": "^2.0.1",
"@mantine/core": "^9.0.1",
Expand Down
15 changes: 7 additions & 8 deletions frontend/src/app/core/setup/globals/openproject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,11 @@ export class OpenProject {
return this.pageState === 'submitted';
}

/** 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 get pageHasUnsavedChanges():boolean {
return this.pageWasEdited || this.editFormsContainUnsavedChanges();
}

public editFormsContainUnsavedChanges:() => boolean = () => false;

public getPluginContext():Promise<OpenProjectPluginContext> {
return firstValueFrom(this.pluginContext.values$());
Expand Down Expand Up @@ -103,9 +102,9 @@ export class OpenProject {
window.localStorage.setItem(key, newValue);
} else {
const value = window.localStorage.getItem(key);
return value === null ? undefined : value;
return value ?? undefined;
}
} catch (e) {
} catch {
console.error('Failed to access your browsers local storage. Is your local database corrupted?');
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service';
import { WorkPackageRelationsService } from './wp-relations.service';
import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
import { TurboRequestsService } from 'core-app/core/turbo/turbo-requests.service';
import { renderStreamMessage } from '@hotwired/turbo';
import { type FrameElement, renderStreamMessage, type TurboSubmitEndEvent } from '@hotwired/turbo';
import { HalEventsService } from 'core-app/features/hal/services/hal-events.service';

@Component({
Expand All @@ -52,7 +52,7 @@ export class WorkPackageRelationsComponent extends UntilDestroyedMixin implement

@Input() public workPackage:WorkPackageResource;

@ViewChild('frameElement') readonly relationTurboFrame:ElementRef<HTMLIFrameElement>;
@ViewChild('frameElement') readonly relationTurboFrame:ElementRef<FrameElement>;

turboFrameSrc:string;

Expand Down Expand Up @@ -96,12 +96,9 @@ export class WorkPackageRelationsComponent extends UntilDestroyedMixin implement
document.addEventListener('turbo:submit-end', this.turboFrameListener);
}

private async updateFrontendData(event:CustomEvent) {
private async updateFrontendData(event:TurboSubmitEndEvent) {
Comment thread
myabc marked this conversation as resolved.
if (event) {
// A turbo:submit-end event *has* a `formSubmission` property, but I do not
// know how to avoid the eslint type warning. Please if you know, fix it.
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const form = event.detail.formSubmission.formElement as HTMLFormElement;
const form = event.detail.formSubmission.formElement;
const updateWorkPackage = !!form.dataset?.updateWorkPackage;

if (updateWorkPackage) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service';
import { CollectionResource } from 'core-app/features/hal/resources/collection-resource';
import type { FrameElement, TurboSubmitEndEvent } from '@hotwired/turbo';

@Component({
templateUrl: './wp-reminder.modal.html',
Expand All @@ -23,7 +24,7 @@ export class WorkPackageReminderModalComponent extends OpModalComponent implemen
readonly actions$ = inject(ActionsService);
readonly apiV3Service = inject(ApiV3Service);

@ViewChild('frameElement') frameElement:ElementRef<HTMLIFrameElement>;
@ViewChild('frameElement') frameElement:ElementRef<FrameElement>;

// Hide close button so it's not duplicated in primer (WP#51699)
showCloseButton = false;
Expand Down Expand Up @@ -88,12 +89,10 @@ export class WorkPackageReminderModalComponent extends OpModalComponent implemen
this.frameSrc = url.toString();
}

private turboSubmitEndListener(event:CustomEvent) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
private turboSubmitEndListener(event:TurboSubmitEndEvent) {
const { fetchResponse } = event.detail;

// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (fetchResponse.succeeded) {
if (fetchResponse?.succeeded) {
this.closeMe();
this.onClose();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { WorkPackageResource } from 'core-app/features/hal/resources/work-packag
import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
import { ActionsService } from 'core-app/core/state/actions/actions.service';
import { shareModalUpdated } from 'core-app/features/work-packages/components/wp-share-modal/sharing.actions';
import { type FrameElement } from '@hotwired/turbo';

@Component({
templateUrl: './wp-share.modal.html',
Expand All @@ -17,7 +18,7 @@ export class WorkPackageShareModalComponent extends OpModalComponent implements
readonly pathHelper = inject(PathHelperService);
readonly actions$ = inject(ActionsService);

@ViewChild('frameElement') frameElement:ElementRef<HTMLIFrameElement>|undefined;
@ViewChild('frameElement') frameElement:ElementRef<FrameElement>|undefined;

// Hide close button so it's not duplicated in primer (WP#51699)
showCloseButton = false;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//-- copyright
// OpenProject is an open source project management software.
// Copyright (C) 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 { TestBed } from '@angular/core/testing';
import { StateService, Transition, TransitionService } from '@uirouter/core';
import * as Turbo from '@hotwired/turbo';
import { ConfigurationService } from 'core-app/core/config/configuration.service';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { EditingPortalService } from 'core-app/shared/components/fields/edit/editing-portal/editing-portal-service';
import { EditFieldHandler } from 'core-app/shared/components/fields/edit/editing-portal/edit-field-handler';
import { EditFormRoutingService } from 'core-app/shared/components/fields/edit/edit-form/edit-form-routing.service';
import { EditFormComponent } from 'core-app/shared/components/fields/edit/edit-form/edit-form.component';
import { GlobalEditFormChangesTrackerService } from 'core-app/shared/components/fields/edit/services/global-edit-form-changes-tracker/global-edit-form-changes-tracker.service';
import { vi } from 'vitest';

describe('EditFormComponent', () => {
let onBeforeCallback:(transition:Transition) => unknown;

afterEach(() => {
vi.restoreAllMocks();
});

beforeEach(async () => {
await TestBed
.configureTestingModule({
declarations: [
EditFormComponent,
],
providers: [
{
provide: TransitionService,
useValue: {
onBefore: vi.fn((_criteria:unknown, callback:(transition:Transition) => unknown) => {
onBeforeCallback = callback;
return vi.fn();
}),
},
},
{ provide: ConfigurationService, useValue: { warnOnLeavingUnsaved: vi.fn().mockReturnValue(true) } },
{ provide: EditingPortalService, useValue: {} },
{ provide: StateService, useValue: {} },
{ provide: I18nService, useValue: { t: vi.fn().mockReturnValue('Leave edit mode?') } },
{ provide: EditFormRoutingService, useValue: { blockedTransition: vi.fn().mockReturnValue(true) } },
{
provide: GlobalEditFormChangesTrackerService,
useValue: {
addToActiveForms: vi.fn(),
removeFromActiveForms: vi.fn(),
},
},
],
})
.compileComponents();
});

it('restores a canceled browser Back transition without navigating forward', () => {
const fixture = TestBed.createComponent(EditFormComponent);
const component = fixture.componentInstance;
const urlRouterUpdate = vi.fn();
const transition = {
options: vi.fn().mockReturnValue({ source: 'url' }),
from: vi.fn().mockReturnValue({ name: 'work-packages.partitioned.split' }),
params: vi.fn().mockReturnValue({ workPackageId: '46' }),
router: {
stateService: {
href: vi.fn().mockReturnValue('/work_packages/details/46/overview'),
},
urlRouter: {
update: urlRouterUpdate,
},
},
} as unknown as Transition;
const turboPush = vi.spyOn(
Turbo.session.history,
'push',
).mockImplementation(() => undefined);
const historyForward = vi.spyOn(window.history, 'forward');
const confirm = vi.spyOn(window, 'confirm').mockReturnValue(false);
const cancel = vi.spyOn(component, 'cancel');

component.activeFields = {
description: {} as EditFieldHandler,
};

expect(onBeforeCallback(transition)).toBe(false);
expect(confirm).toHaveBeenCalledWith('Leave edit mode?');
expect(cancel).not.toHaveBeenCalled();
expect(turboPush).toHaveBeenCalledOnce();
expect(turboPush.mock.calls[0][0].pathname).toBe('/work_packages/details/46/overview');
expect(urlRouterUpdate).toHaveBeenCalledWith(true);
expect(historyForward).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { EditFormRoutingService } from 'core-app/shared/components/fields/edit/e
import { ResourceChangesetCommit } from 'core-app/shared/components/fields/edit/services/hal-resource-editing.service';
import { GlobalEditFormChangesTrackerService } from 'core-app/shared/components/fields/edit/services/global-edit-form-changes-tracker/global-edit-form-changes-tracker.service';
import { firstValueFrom } from 'rxjs';
import * as Turbo from '@hotwired/turbo';

@Component({
selector: 'edit-form,[edit-form]',
Expand Down Expand Up @@ -103,6 +104,7 @@ export class EditFormComponent extends EditForm<HalResource> implements OnInit,
// that's not within the edit mode.
if (!this.editFormRouting || this.editFormRouting.blockedTransition(transition)) {
if (requiresConfirmation && !window.confirm(confirmText)) {
this.undoCanceledBrowserBackTransition(transition);
return false;
}

Expand All @@ -113,6 +115,31 @@ export class EditFormComponent extends EditForm<HalResource> implements OnInit,
});
}

private undoCanceledBrowserBackTransition(transition:Transition) {
if (transition.options().source !== 'url') {
return;
}

const fromUrl = transition
.router
.stateService
.href(transition.from(), transition.params('from'));

if (!fromUrl) {
return;
}

// Restore the canceled Back URL without firing a real forward navigation,
// which would make Turbo restore a stale snapshot of the split view.
Turbo.session
.history
.push(new URL(fromUrl, window.location.origin));

Comment thread
myabc marked this conversation as resolved.
// Keep UI-Router from replacing the restored browser history entry while
// it rolls back the aborted Back navigation.
transition.router.urlRouter.update(true);
}

ngOnInit() {
this.editMode = this.initializeEditMode;
this.globalEditFormChangesTrackerService.addToActiveForms(this);
Expand Down
Loading
Loading