Rename "wpPreviewModal" to a more generic "hoverCard" and use a turboFrame inside to be more flexible about the content

This commit is contained in:
Henriette Darge
2024-10-02 15:06:11 +02:00
parent b18304d7df
commit 348fdac823
20 changed files with 128 additions and 103 deletions
@@ -0,0 +1,37 @@
#-- 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.
#++
module WorkPackages
class HoverCardController < ApplicationController
before_action :load_and_authorize_in_optional_project
def show
@id = params[:id]
render layout: nil
end
end
end
+1 -1
View File
@@ -173,7 +173,7 @@ module WorkPackagesHelper
# Returns a string of css classes that apply to the issue
def work_package_css_classes(work_package)
s = "work_package preview-trigger".html_safe
s = "work_package op-hover-card--preview-trigger".html_safe
s << " status-#{work_package.status.position}" if work_package.status
s << " priority-#{work_package.priority.position}" if work_package.priority
s << " closed" if work_package.closed?
@@ -0,0 +1,3 @@
<turbo-frame id="op-hover-card-body">
Hallo WELT
</turbo-frame>
+2 -1
View File
@@ -217,7 +217,8 @@ Rails.application.reloader.to_prepare do
work_packages: %i[show index],
work_packages_api: [:get],
"work_packages/reports": %i[report report_details],
"work_packages/menus": %i[show]
"work_packages/menus": %i[show],
"work_packages/hover_card": %i[show]
},
permissible_on: %i[work_package project],
contract_actions: { work_packages: %i[read] }
+2
View File
@@ -571,6 +571,8 @@ Rails.application.routes.draw do
resources :work_packages, only: [:index] do
concerns :shareable
get "hover_card" => "work_packages/hover_card#show", on: :member
# move bulk of wps
get "move/new" => "work_packages/moves#new", on: :collection, as: "new_move"
post "move" => "work_packages/moves#create", on: :collection, as: "move"
+5 -5
View File
@@ -53,7 +53,7 @@ import { OpenprojectDashboardsModule } from 'core-app/features/dashboards/openpr
import {
OpenprojectWorkPackageGraphsModule,
} from 'core-app/shared/components/work-package-graphs/openproject-work-package-graphs.module';
import { PreviewTriggerService } from 'core-app/core/setup/globals/global-listeners/preview-trigger.service';
import { HoverCardTriggerService } from 'core-app/core/setup/globals/global-listeners/hover-card-trigger.service';
import { OpenprojectOverviewModule } from 'core-app/features/overview/openproject-overview.module';
import { OpenprojectMyPageModule } from 'core-app/features/my-page/openproject-my-page.module';
import { OpenprojectProjectsModule } from 'core-app/features/projects/openproject-projects.module';
@@ -77,8 +77,8 @@ import {
PasswordConfirmationModalComponent,
} from 'core-app/shared/components/modals/request-for-confirmation/password-confirmation.modal';
import {
WpPreviewModalComponent,
} from 'core-app/shared/components/modals/preview-modal/wp-preview-modal/wp-preview.modal';
HoverCardComponent,
} from 'core-app/shared/components/modals/preview-modal/hover-card-modal/hover-card.modal';
import {
OpHeaderProjectSelectComponent,
} from 'core-app/shared/components/header-project-select/header-project-select.component';
@@ -240,7 +240,7 @@ import { SpotSwitchComponent } from 'core-app/spot/components/switch/switch.comp
export function initializeServices(injector:Injector) {
return () => {
const PreviewTrigger = injector.get(PreviewTriggerService);
const PreviewTrigger = injector.get(HoverCardTriggerService);
const topMenuService = injector.get(TopMenuService);
const keyboardShortcuts = injector.get(KeyboardShortcutService);
// Conditionally add the Revit Add-In settings button
@@ -370,7 +370,7 @@ export function initializeServices(injector:Injector) {
ConfirmDialogModalComponent,
DynamicContentModalComponent,
PasswordConfirmationModalComponent,
WpPreviewModalComponent,
HoverCardComponent,
// Main menu
MainMenuResizerComponent,
@@ -28,10 +28,10 @@
import { Injectable, Injector, NgZone } from '@angular/core';
import { OpModalService } from 'core-app/shared/components/modal/modal.service';
import { WpPreviewModalComponent } from 'core-app/shared/components/modals/preview-modal/wp-preview-modal/wp-preview.modal';
import { HoverCardComponent } from 'core-app/shared/components/modals/preview-modal/hover-card-modal/hover-card.modal';
@Injectable({ providedIn: 'root' })
export class PreviewTriggerService {
export class HoverCardTriggerService {
private modalElement:HTMLElement;
private mouseInModal = false;
@@ -44,7 +44,7 @@ export class PreviewTriggerService {
}
setupListener() {
jQuery(document.body).on('mouseover', '.preview-trigger', (e) => {
jQuery(document.body).on('mouseover', '.op-hover-card--preview-trigger', (e) => {
e.preventDefault();
e.stopPropagation();
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
@@ -58,8 +58,9 @@ export class PreviewTriggerService {
}
this.opModalService.show(
WpPreviewModalComponent,
HoverCardComponent,
this.injector,
// TODO
{ workPackageLink: href, event: e },
true,
).subscribe((previewModal) => {
@@ -69,16 +70,16 @@ export class PreviewTriggerService {
}
});
jQuery(document.body).on('mouseleave', '.preview-trigger', () => {
jQuery(document.body).on('mouseleave', '.op-hover-card--preview-trigger', () => {
this.closeAfterTimeout();
});
jQuery(document.body).on('mouseleave', '.op-wp-preview-modal', () => {
jQuery(document.body).on('mouseleave', '.op-hover-card', () => {
this.mouseInModal = false;
this.closeAfterTimeout();
});
jQuery(document.body).on('mouseenter', '.op-wp-preview-modal', () => {
jQuery(document.body).on('mouseenter', '.op-hover-card', () => {
this.mouseInModal = true;
});
}
@@ -92,21 +93,4 @@ export class PreviewTriggerService {
}, 100);
});
}
private isMouseOverPreview(e:JQuery.MouseLeaveEvent) {
if (!this.modalElement) {
return false;
}
const previewElement = jQuery(this.modalElement.children[0]);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (previewElement && previewElement.offset()) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const horizontalHover = e.pageX >= Math.floor(previewElement.offset()!.left) && e.pageX < previewElement.offset()!.left + previewElement.width()!;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const verticalHover = e.pageY >= Math.floor(previewElement.offset()!.top) && e.pageY < previewElement.offset()!.top + previewElement.height()!;
return horizontalHover && verticalHover;
}
return false;
}
}
@@ -9,7 +9,7 @@
[displayFieldOptions]="{ writable: false }"
fieldName="type">
</display-field>
<a class="work-package--quickinfo preview-trigger"
<a class="op-hover-card--preview-trigger"
target="_top"
[href]="workPackageLink"
[attr.data-work-package-id]="workPackage.id">
@@ -0,0 +1,17 @@
<div
class="op-hover-card"
*ngIf="turboFrameSrc"
>
<turbo-frame
loading="lazy"
id="op-hover-card-body"
[src]="turboFrameSrc">
<op-content-loader
viewBox="0 0 180 80"
>
<svg:rect x="10" y="10" width="80%" height="16" rx="1" />
<svg:rect x="10" y="30" width="80%" height="16" rx="1" />
<svg:rect x="10" y="50" width="60%" height="16" rx="1" />
</op-content-loader>
</turbo-frame>
</div>
@@ -0,0 +1,10 @@
@import "helpers"
.op-hover-card
position: absolute
background-color: var(--body-background)
z-index: 5000
min-width: 350px
box-shadow: var(--shadow-floating-large)
pointer-events: all
padding: 1rem
@@ -32,17 +32,12 @@ import {
Component,
ElementRef,
Inject,
OnInit,
Input,
OnInit,
} from '@angular/core';
import { OpModalComponent } from 'core-app/shared/components/modal/modal.component';
import { OpModalLocalsToken, OpModalService } from 'core-app/shared/components/modal/modal.service';
import { OpModalLocalsToken } from 'core-app/shared/components/modal/modal.service';
import { OpModalLocalsMap } from 'core-app/shared/components/modal/modal.types';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource';
import idFromLink from 'core-app/features/hal/helpers/id-from-link';
import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service';
import { StateService } from '@uirouter/core';
import {
computePosition,
flip,
@@ -50,57 +45,57 @@ import {
Placement,
shift,
} from '@floating-ui/dom';
import { WorkPackageIsolatedQuerySpaceDirective } from 'core-app/features/work-packages/directives/query-space/wp-isolated-query-space.directive';
import { fromEvent } from 'rxjs';
import {
WorkPackageIsolatedQuerySpaceDirective,
} from 'core-app/features/work-packages/directives/query-space/wp-isolated-query-space.directive';
filter,
tap,
throttleTime,
} from 'rxjs/operators';
@Component({
templateUrl: './wp-preview.modal.html',
styleUrls: ['./wp-preview.modal.sass'],
templateUrl: './hover-card.modal.html',
styleUrls: ['./hover-card.modal.sass'],
changeDetection: ChangeDetectionStrategy.OnPush,
hostDirectives: [WorkPackageIsolatedQuerySpaceDirective],
})
export class WpPreviewModalComponent extends OpModalComponent implements OnInit {
public workPackage:WorkPackageResource;
public text = {
created_by: this.i18n.t('js.label_created_by'),
};
export class HoverCardComponent extends OpModalComponent implements OnInit {
@Input() public turboFrameSrc:string = "/work_packages/50/hover_card";
@Input() public alignment?:Placement = 'bottom-end';
@Input() public allowRepositioning? = true;
public test:string;
constructor(
readonly elementRef:ElementRef,
@Inject(OpModalLocalsToken) readonly locals:OpModalLocalsMap,
readonly cdRef:ChangeDetectorRef,
readonly i18n:I18nService,
readonly apiV3Service:ApiV3Service,
readonly opModalService:OpModalService,
readonly $state:StateService,
) {
super(locals, cdRef, elementRef);
}
ngOnInit() {
super.ngOnInit();
const { workPackageLink } = this.locals;
const workPackageId = idFromLink(workPackageLink as string|null);
this
.apiV3Service
.work_packages
.id(workPackageId)
.requireAndStream()
.subscribe((workPackage:WorkPackageResource) => {
this.workPackage = workPackage;
this.cdRef.detectChanges();
this.test = this.turboFrameSrc;
const modal = this.elementRef.nativeElement as HTMLElement;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-explicit-any
void this.reposition(modal, this.locals.event.target as HTMLElement);
});
// TODO
fromEvent(document, 'turbo:frame-load')
.pipe(
filter((event:CustomEvent) => {
return (event.target as HTMLElement).id?.includes('op-hover-card-body');
}),
throttleTime(100),
tap(() => {
this.cdRef.detectChanges();
const modal = this.elementRef.nativeElement as HTMLElement;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-explicit-any
void this.reposition(modal, this.locals.event.target as HTMLElement);
}),
);
}
public async reposition(element:HTMLElement, target:HTMLElement) {
@@ -125,9 +120,4 @@ export class WpPreviewModalComponent extends OpModalComponent implements OnInit
top: `${y}px`,
});
}
public openStateLink(event:{ workPackageId:string; requestedState:string }) {
const params = { workPackageId: event.workPackageId };
void this.$state.go(event.requestedState, params);
}
}
@@ -1,10 +0,0 @@
<div
class="op-wp-preview-modal"
*ngIf="workPackage"
>
<wp-single-card
[workPackage]="workPackage"
orientation="horizontal"
(stateLinkClicked)="openStateLink($event)"
></wp-single-card>
</div>
@@ -1,9 +0,0 @@
@import "helpers"
.op-wp-preview-modal
position: absolute
z-index: 5000
min-width: 350px
padding: 0px
box-shadow: 0px 0px 5px 2px rgba(0, 0, 0, 0.25)
pointer-events: all
@@ -75,7 +75,7 @@ module OpenProject::TextFormatting
def work_package_mention(work_package)
link_to("##{work_package.id}",
work_package_path_or_url(id: work_package.id, only_path: context[:only_path]),
class: "issue work_package preview-trigger")
class: "issue work_package op-hover-card--preview-trigger")
end
def class_from_mention(mention)
@@ -66,7 +66,7 @@ module OpenProject::TextFormatting::Matchers
def render_work_package_link(wp_id)
link_to("##{wp_id}",
work_package_path_or_url(id: wp_id, only_path: context[:only_path]),
class: "issue work_package preview-trigger")
class: "issue work_package op-hover-card--preview-trigger")
end
end
end
@@ -247,7 +247,7 @@ RSpec.describe "activity comments", :js do
wp_page.expect_comment text: "Single ##{work_package2.id}"
expect(page).to have_css(".user-comment opce-macro-wp-quickinfo", count: 2)
expect(page).to have_css(".user-comment .work-package--quickinfo.preview-trigger", count: 2)
expect(page).to have_css(".user-comment .op-hover-card--preview-trigger", count: 2)
end
end
@@ -55,7 +55,7 @@ RSpec.describe "Wysiwyg work package quicklink macros", :js do
# Expect output widget
within("#content") do
expect(page).to have_link("##{work_package.id}")
expect(page).to have_no_css(".work-package--quickinfo.preview-trigger")
expect(page).to have_no_css(".op-hover-card--preview-trigger")
end
# Edit page again
@@ -77,7 +77,7 @@ RSpec.describe "Wysiwyg work package quicklink macros", :js do
expected_macro_text = "#{work_package.type.name.upcase} ##{work_package.id}: My subject"
expect(page).to have_css("opce-macro-wp-quickinfo", text: expected_macro_text)
expect(page).to have_css("span", text: work_package.type.name.upcase)
expect(page).to have_css(".work-package--quickinfo.preview-trigger", text: "##{work_package.id}")
expect(page).to have_css(".op-hover-card--preview-trigger", text: "##{work_package.id}")
expect(page).to have_css("span", text: "My subject")
end
@@ -102,7 +102,7 @@ RSpec.describe "Wysiwyg work package quicklink macros", :js do
expect(page).to have_css("opce-macro-wp-quickinfo", text: expected_macro_text)
expect(page).to have_css("span", text: work_package.status.name)
expect(page).to have_css("span", text: work_package.type.name.upcase)
expect(page).to have_css(".work-package--quickinfo.preview-trigger", text: "##{work_package.id}")
expect(page).to have_css(".op-hover-card--preview-trigger", text: "##{work_package.id}")
expect(page).to have_css("span", text: "My subject")
# Dates are being rendered in two nested spans
expect(page).to have_css("span", text: "01/01/2020", count: 2)
@@ -95,7 +95,7 @@ RSpec.describe API::V3::Repositories::RevisionRepresenter do
id = work_package.id
str = "Totally references <a"
str << " class=\"issue work_package preview-trigger\""
str << " class=\"issue work_package op-hover-card--preview-trigger\""
str << " href=\"/work_packages/#{id}\">"
str << "##{id}</a>"
end
@@ -267,7 +267,7 @@ RSpec.describe OpenProject::TextFormatting,
let(:work_package_link) do
link_to("##{work_package.id}",
work_package_path(work_package),
class: "issue work_package preview-trigger op-uc-link",
class: "issue work_package op-hover-card--preview-trigger op-uc-link",
target: "_top")
end
@@ -337,7 +337,7 @@ RSpec.describe OpenProject::TextFormatting,
let(:work_package_link) do
link_to("##{work_package.id}",
work_package_path(work_package),
class: "issue work_package preview-trigger op-uc-link",
class: "issue work_package op-hover-card--preview-trigger op-uc-link",
target: "_top")
end
@@ -656,7 +656,7 @@ RSpec.describe OpenProject::TextFormatting,
let(:expected) do
<<~EXPECTED
<p class='op-uc-p'><a class="wiki-page op-uc-link" target="_top" href="/projects/#{project.identifier}/wiki/cookbook-documentation">CookBook documentation</a></p>
<p class='op-uc-p'><a class="issue work_package preview-trigger op-uc-link" target="_top" href="/work_packages/#{work_package.id}">##{work_package.id}</a></p>
<p class='op-uc-p'><a class="issue work_package op-hover-card--preview-trigger op-uc-link" target="_top" href="/work_packages/#{work_package.id}">##{work_package.id}</a></p>
<pre class="op-uc-code-block">
[[CookBook documentation]]
+2 -2
View File
@@ -90,7 +90,7 @@ RSpec.describe "API v3 Render resource" do
<<~HTML
<p class="op-uc-p">
Hello World! Have a look at
<a class="issue work_package preview-trigger op-uc-link"
<a class="issue work_package op-hover-card--preview-trigger op-uc-link"
target="_top"
href="#{href}">##{id}</a>
</p>
@@ -180,7 +180,7 @@ RSpec.describe "API v3 Render resource" do
it_behaves_like "valid response" do
let(:text) do
"<p>Hello *World*! Have a look at <a class=\"issue work_package preview-trigger\" href=\"/work_packages/1\">#1</a></p>\n\n<p>with two lines.</p>"
"<p>Hello *World*! Have a look at <a class=\"issue work_package op-hover-card--preview-trigger\" href=\"/work_packages/1\">#1</a></p>\n\n<p>with two lines.</p>"
end
end
end