mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
Remove uiRouter from Calendars
This commit is contained in:
@@ -147,6 +147,7 @@ import {
|
||||
import {
|
||||
BoardEntryComponent,
|
||||
} from 'core-app/features/boards/board/board-partitioned-page/board-entry.component';
|
||||
import { CalendarEntryComponent } from 'core-app/features/calendar/calendar-entry.component';
|
||||
import {
|
||||
StorageLoginButtonComponent,
|
||||
} from 'core-app/shared/components/storages/storage-login-button/storage-login-button.component';
|
||||
@@ -393,6 +394,7 @@ export class OpenProjectModule implements DoBootstrap {
|
||||
registerCustomElement('opce-notification-center', InAppNotificationCenterComponent, { injector });
|
||||
registerCustomElement('opce-wp-split-view', WorkPackageSplitViewEntryComponent, { injector });
|
||||
registerCustomElement('opce-board-view', BoardEntryComponent, { injector });
|
||||
registerCustomElement('opce-calendar-view', CalendarEntryComponent, { injector });
|
||||
registerCustomElement('opce-wp-full-view', WorkPackageFullViewEntryComponent, { injector });
|
||||
registerCustomElement('opce-wp-full-create', WorkPackageFullCreateEntryComponent, { injector });
|
||||
registerCustomElement('opce-wp-full-copy', WorkPackageFullCopyEntryComponent, { injector });
|
||||
|
||||
@@ -9,31 +9,31 @@ export class SubmenuService {
|
||||
reloadSubmenu(selectedQueryId:string|null, sidemenuId?:string):void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment
|
||||
const menuIdentifier:string|undefined = sidemenuId ?? this.$state.current.data?.sideMenuOptions?.sidemenuId;
|
||||
if (!menuIdentifier) { return; }
|
||||
|
||||
if (menuIdentifier) {
|
||||
const menu = document.getElementById(menuIdentifier) as FrameElement;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const sideMenuOptions = this.$state.$current.data?.sideMenuOptions as { hardReloadOnBaseRoute?:boolean, defaultQuery?:string };
|
||||
const currentSrc = menu.getAttribute('src');
|
||||
const menu = document.getElementById(menuIdentifier) as FrameElement|null;
|
||||
const currentSrc = menu?.getAttribute('src');
|
||||
if (!currentSrc || !menu) { return; }
|
||||
|
||||
if (currentSrc && menu) {
|
||||
const frameUrl = new URL(currentSrc, window.location.origin);
|
||||
const defaultQuery = sideMenuOptions?.defaultQuery;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const sideMenuOptions = this.$state.$current.data?.sideMenuOptions as { hardReloadOnBaseRoute?:boolean, defaultQuery?:string };
|
||||
const frameUrl = new URL(currentSrc, window.location.origin);
|
||||
|
||||
if (selectedQueryId) {
|
||||
// If there is a default query passed in the route definition, it means that id passed as argument and not as parameter,
|
||||
// e.g. calendars/:id, team_planner/:id, ...
|
||||
// Otherwise, we will just replace the params
|
||||
if (defaultQuery) {
|
||||
frameUrl.search = `?id=${selectedQueryId}`;
|
||||
} else {
|
||||
frameUrl.search = `?query_id=${selectedQueryId}`;
|
||||
}
|
||||
}
|
||||
if (selectedQueryId) {
|
||||
// Prefer the data attribute on the frame, then fall back to route sideMenuOptions,
|
||||
// then default to 'query_id'. Modules with path-based IDs (e.g. calendars/:id)
|
||||
// set data-query-param="id" on the frame.
|
||||
const queryParam = menu.getAttribute('data-query-param')
|
||||
?? (sideMenuOptions?.defaultQuery ? 'id' : 'query_id');
|
||||
|
||||
// Override the frame src to enforce a reload
|
||||
menu.setAttribute('src', frameUrl.href);
|
||||
}
|
||||
frameUrl.search = `?${queryParam}=${selectedQueryId}`;
|
||||
}
|
||||
|
||||
const newSrc = frameUrl.href;
|
||||
if (menu.getAttribute('src') !== newSrc) {
|
||||
menu.setAttribute('src', newSrc);
|
||||
} else {
|
||||
void menu.reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@ import {
|
||||
redirectToMobileAlternative,
|
||||
} from 'core-app/shared/helpers/routing/mobile-guard.helper';
|
||||
import { TEAM_PLANNER_LAZY_ROUTES } from 'core-app/features/team-planner/team-planner/team-planner.lazy-routes';
|
||||
import { CALENDAR_LAZY_ROUTES } from 'core-app/features/calendar/calendar.lazy-routes';
|
||||
|
||||
export const OPENPROJECT_ROUTES:Ng2StateDeclaration[] = [
|
||||
{
|
||||
@@ -75,7 +74,6 @@ export const OPENPROJECT_ROUTES:Ng2StateDeclaration[] = [
|
||||
loadChildren: () => import('../../features/bim/ifc_models/openproject-ifc-models.module').then((m) => m.OpenprojectIFCModelsModule),
|
||||
},
|
||||
...TEAM_PLANNER_LAZY_ROUTES,
|
||||
...CALENDAR_LAZY_ROUTES,
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
+19
-9
@@ -26,13 +26,23 @@
|
||||
// See COPYRIGHT and LICENSE files for more details.
|
||||
//++
|
||||
|
||||
import { Ng2StateDeclaration } from '@uirouter/angular';
|
||||
import { ChangeDetectionStrategy, Component, ElementRef, Input } from '@angular/core';
|
||||
import { populateInputsFromDataset } from 'core-app/shared/components/dataset-inputs';
|
||||
import {
|
||||
WorkPackageIsolatedQuerySpaceDirective,
|
||||
} from 'core-app/features/work-packages/directives/query-space/wp-isolated-query-space.directive';
|
||||
|
||||
export const CALENDAR_LAZY_ROUTES:Ng2StateDeclaration[] = [
|
||||
{
|
||||
name: 'calendar.**',
|
||||
parent: 'optional_project',
|
||||
url: '/calendars',
|
||||
loadChildren: () => import('./openproject-calendar.module').then((m) => m.OpenprojectCalendarModule),
|
||||
},
|
||||
];
|
||||
@Component({
|
||||
hostDirectives: [WorkPackageIsolatedQuerySpaceDirective],
|
||||
template: '<op-wp-calendar-page [queryId]="queryId" />',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class CalendarEntryComponent {
|
||||
@Input() queryId:string;
|
||||
|
||||
constructor(readonly elementRef:ElementRef) {
|
||||
populateInputsFromDataset(this);
|
||||
document.body.classList.add('router--calendar');
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
//-- 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 { Ng2StateDeclaration } from '@uirouter/angular';
|
||||
import { makeSplitViewRoutes } from 'core-app/features/work-packages/routing/split-view-routes.template';
|
||||
import { WorkPackageSplitViewComponent } from 'core-app/features/work-packages/routing/wp-split-view/wp-split-view.component';
|
||||
import { WorkPackagesBaseComponent } from 'core-app/features/work-packages/routing/wp-base/wp--base.component';
|
||||
import { WorkPackagesCalendarComponent } from 'core-app/features/calendar/wp-calendar/wp-calendar.component';
|
||||
import { WorkPackagesCalendarPageComponent } from 'core-app/features/calendar/wp-calendar-page/wp-calendar-page.component';
|
||||
|
||||
export const sidemenuId = 'calendar_sidemenu';
|
||||
export const sideMenuOptions = {
|
||||
sidemenuId,
|
||||
hardReloadOnBaseRoute: true,
|
||||
defaultQuery: 'new',
|
||||
};
|
||||
|
||||
export const CALENDAR_ROUTES:Ng2StateDeclaration[] = [
|
||||
{
|
||||
name: 'calendar',
|
||||
parent: 'optional_project',
|
||||
url: '/calendars/:query_id?&query_props&cdate&cview',
|
||||
redirectTo: 'calendar.page',
|
||||
views: {
|
||||
'!$default': { component: WorkPackagesBaseComponent },
|
||||
},
|
||||
params: {
|
||||
query_id: { type: 'opQueryId', dynamic: true },
|
||||
cdate: { type: 'string', dynamic: true },
|
||||
cview: { type: 'string', dynamic: true },
|
||||
// Use custom encoder/decoder that ensures validity of URL string
|
||||
query_props: { type: 'opQueryString' },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'calendar.page',
|
||||
component: WorkPackagesCalendarPageComponent,
|
||||
redirectTo: 'calendar.page.show',
|
||||
data: {
|
||||
bodyClasses: 'router--calendar',
|
||||
sideMenuOptions,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'calendar.page.show',
|
||||
data: {
|
||||
baseRoute: 'calendar.page.show',
|
||||
sideMenuOptions,
|
||||
},
|
||||
views: {
|
||||
'content-left': { component: WorkPackagesCalendarComponent },
|
||||
},
|
||||
},
|
||||
...makeSplitViewRoutes(
|
||||
'calendar.page.show',
|
||||
undefined,
|
||||
WorkPackageSplitViewComponent,
|
||||
),
|
||||
];
|
||||
@@ -15,9 +15,8 @@ import { ConfigurationService } from 'core-app/core/config/configuration.service
|
||||
import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { SchemaCacheService } from 'core-app/core/schemas/schema-cache.service';
|
||||
import { splitViewRoute } from 'core-app/features/work-packages/routing/split-view-routes.helper';
|
||||
import { StateService } from '@uirouter/angular';
|
||||
import { WorkPackageCollectionResource } from 'core-app/features/hal/resources/wp-collection-resource';
|
||||
import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
|
||||
import { ToastService } from 'core-app/shared/components/toaster/toast.service';
|
||||
import { firstValueFrom, Observable } from 'rxjs';
|
||||
import {
|
||||
@@ -34,7 +33,6 @@ import {
|
||||
UrlParamsHelperService,
|
||||
} from 'core-app/features/work-packages/components/wp-query/url-params-helper';
|
||||
import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service';
|
||||
import { UIRouterGlobals } from '@uirouter/core';
|
||||
import { TimezoneService } from 'core-app/core/datetime/timezone.service';
|
||||
import {
|
||||
WorkPackagesListChecksumService,
|
||||
@@ -93,7 +91,6 @@ export class OpWorkPackagesCalendarService extends UntilDestroyedMixin {
|
||||
private I18n:I18nService,
|
||||
private configuration:ConfigurationService,
|
||||
private sanitizer:DomSanitizer,
|
||||
private $state:StateService,
|
||||
readonly injector:Injector,
|
||||
readonly schemaCache:SchemaCacheService,
|
||||
readonly toastService:ToastService,
|
||||
@@ -104,8 +101,8 @@ export class OpWorkPackagesCalendarService extends UntilDestroyedMixin {
|
||||
readonly querySpace:IsolatedQuerySpace,
|
||||
readonly apiV3Service:ApiV3Service,
|
||||
readonly halResourceService:HalResourceService,
|
||||
readonly uiRouterGlobals:UIRouterGlobals,
|
||||
readonly timezoneService:TimezoneService,
|
||||
readonly pathHelper:PathHelperService,
|
||||
readonly halEditing:HalResourceEditingService,
|
||||
readonly wpTableSelection:WorkPackageViewSelectionService,
|
||||
readonly contextMenuService:OPContextMenuService,
|
||||
@@ -283,23 +280,18 @@ export class OpWorkPackagesCalendarService extends UntilDestroyedMixin {
|
||||
this.wpTableSelection.setSelection(id, -1);
|
||||
|
||||
// Only open the split view if already open, otherwise only clicking the details opens
|
||||
if (onlyWhenOpen && !this.$state.includes('**.details.*')) {
|
||||
if (onlyWhenOpen && !window.location.pathname.includes('/details/')) {
|
||||
return;
|
||||
}
|
||||
|
||||
void this.$state.go(
|
||||
`${splitViewRoute(this.$state)}.tabs`,
|
||||
{ workPackageId: id, tabIdentifier: 'overview' },
|
||||
);
|
||||
const basePath = window.location.pathname.replace(/\/details\/.*$/, '');
|
||||
const link = `${basePath}/details/${id}${window.location.search}`;
|
||||
Turbo.visit(link, { frame: 'content-bodyRight', action: 'advance' });
|
||||
}
|
||||
|
||||
public openFullView(id:string):void {
|
||||
this.wpTableSelection.setSelection(id, -1);
|
||||
|
||||
void this.$state.go(
|
||||
'work-packages.show',
|
||||
{ workPackageId: id },
|
||||
);
|
||||
Turbo.visit(this.pathHelper.workPackagePath(id));
|
||||
}
|
||||
|
||||
public onCardClicked({ workPackageId, event }:{ workPackageId:string, event:MouseEvent }):void {
|
||||
@@ -399,8 +391,22 @@ export class OpWorkPackagesCalendarService extends UntilDestroyedMixin {
|
||||
&& !this.urlParams.query_props;
|
||||
}
|
||||
|
||||
public get urlParams() {
|
||||
return this.uiRouterGlobals.params;
|
||||
public get urlParams():{
|
||||
query_id?:string;
|
||||
query_props?:string;
|
||||
cdate?:string;
|
||||
cview?:string;
|
||||
} {
|
||||
const search = new URLSearchParams(window.location.search);
|
||||
// Extract query_id from path-based routing (e.g. /calendars/<id>, /team_planners/<id>).
|
||||
const match = /\/(?:calendars|team_planners)\/([^/]+)/.exec(window.location.pathname);
|
||||
const rawId = match?.[1];
|
||||
return {
|
||||
query_id: rawId === 'new' ? undefined : rawId,
|
||||
query_props: search.get('query_props') ?? undefined,
|
||||
cdate: search.get('cdate') ?? undefined,
|
||||
cview: search.get('cview') ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private get areFiltersEmpty():boolean {
|
||||
@@ -417,17 +423,10 @@ export class OpWorkPackagesCalendarService extends UntilDestroyedMixin {
|
||||
}
|
||||
|
||||
private updateDateParam(dates:DatesSetArg) {
|
||||
void this.$state.go(
|
||||
'.',
|
||||
{
|
||||
cdate: this.timezoneService.formattedISODate(dates.view.calendar.getDate()),
|
||||
// v6.beta3 fails to have type on the ViewAPI
|
||||
cview: (dates.view as unknown as { type:string }).type,
|
||||
},
|
||||
{
|
||||
custom: { notify: false },
|
||||
},
|
||||
);
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('cdate', this.timezoneService.formattedISODate(dates.view.calendar.getDate()));
|
||||
url.searchParams.set('cview', (dates.view as unknown as { type:string }).type);
|
||||
window.history.pushState({}, '', url);
|
||||
}
|
||||
|
||||
updateDates(resizeInfo:EventResizeDoneArg|EventDropArg|EventReceiveArg, dragged?:boolean):ResourceChangeset<WorkPackageResource> {
|
||||
|
||||
@@ -32,12 +32,11 @@ import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { FullCalendarModule } from '@fullcalendar/angular';
|
||||
import { WorkPackagesCalendarComponent } from 'core-app/features/calendar/wp-calendar/wp-calendar.component';
|
||||
import { OpenprojectWorkPackagesModule } from 'core-app/features/work-packages/openproject-work-packages.module';
|
||||
import { UIRouterModule } from '@uirouter/angular';
|
||||
import { TimeEntryCalendarComponent } from 'core-app/features/calendar/te-calendar/te-calendar.component';
|
||||
import { OpenprojectFieldsModule } from 'core-app/shared/components/fields/openproject-fields.module';
|
||||
import { OpenprojectTimeEntriesModule } from 'core-app/shared/components/time_entries/openproject-time-entries.module';
|
||||
import { WorkPackagesCalendarPageComponent } from 'core-app/features/calendar/wp-calendar-page/wp-calendar-page.component';
|
||||
import { CALENDAR_ROUTES } from 'core-app/features/calendar/calendar.routes';
|
||||
import { CalendarEntryComponent } from 'core-app/features/calendar/calendar-entry.component';
|
||||
import { QueryGetIcalUrlModalComponent } from 'core-app/shared/components/modals/get-ical-url-modal/query-get-ical-url.modal';
|
||||
|
||||
@NgModule({
|
||||
@@ -45,9 +44,6 @@ import { QueryGetIcalUrlModalComponent } from 'core-app/shared/components/modals
|
||||
// Commons
|
||||
OpSharedModule,
|
||||
|
||||
// Routes for /calendar
|
||||
UIRouterModule.forChild({ states: CALENDAR_ROUTES }),
|
||||
|
||||
// Work Package module
|
||||
OpenprojectWorkPackagesModule,
|
||||
|
||||
@@ -65,6 +61,7 @@ import { QueryGetIcalUrlModalComponent } from 'core-app/shared/components/modals
|
||||
],
|
||||
declarations: [
|
||||
// Work package calendars
|
||||
CalendarEntryComponent,
|
||||
WorkPackagesCalendarPageComponent,
|
||||
WorkPackagesCalendarComponent,
|
||||
TimeEntryCalendarComponent,
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<div class="work-packages-partitioned-query-space--container"
|
||||
[ngClass]="currentPartition">
|
||||
<op-breadcrumbs [items]="breadcrumbItems()"
|
||||
[lastItemSection]="currentMenuSectionHeader()"
|
||||
/>
|
||||
<div class="toolbar-container -editable">
|
||||
<div class="toolbar">
|
||||
<h2 class="toolbar-title--container">
|
||||
<editable-toolbar-title [title]="selectedTitle"
|
||||
[inFlight]="toolbarDisabled"
|
||||
[showSaveCondition]="showToolbarSaveButton"
|
||||
(onSave)="changeChangesFromTitle($event)"
|
||||
(onEmptySubmit)="updateTitleName('')"
|
||||
[editable]="titleEditingEnabled" />
|
||||
</h2>
|
||||
@if (showToolbar) {
|
||||
<ul class="toolbar-items hide-when-print">
|
||||
@for (definition of toolbarButtonComponents; track definition) {
|
||||
@if (!definition.show || definition.show()) {
|
||||
<li class="toolbar-item"
|
||||
[ngClass]="definition.containerClasses">
|
||||
<ndc-dynamic [ndcDynamicComponent]="definition.component"
|
||||
[ndcDynamicInputs]="definition.inputs"
|
||||
[ndcDynamicInjector]="injector"
|
||||
[ndcDynamicOutputs]="definition.outputs" />
|
||||
</li>
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="work-packages-partitioned-query-space--filter-area">
|
||||
@if (filterContainerDefinition) {
|
||||
<ndc-dynamic [ndcDynamicComponent]="filterContainerDefinition.component"
|
||||
[ndcDynamicInputs]="filterContainerDefinition.inputs"
|
||||
[ndcDynamicOutputs]="filterContainerDefinition.outputs"
|
||||
[ndcDynamicInjector]="injector" />
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="work-packages-partitioned-page--content-container">
|
||||
<div class="work-packages-partitioned-page--content-left loading-indicator--location"
|
||||
data-indicator-name="table">
|
||||
<op-wp-calendar />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -29,6 +29,8 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Input,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { WorkPackagesCalendarComponent } from 'core-app/features/calendar/wp-calendar/wp-calendar.component';
|
||||
@@ -50,7 +52,8 @@ import { ActionsService } from 'core-app/core/state/actions/actions.service';
|
||||
import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
|
||||
|
||||
@Component({
|
||||
templateUrl: '../../work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.html',
|
||||
selector: 'op-wp-calendar-page',
|
||||
templateUrl: './wp-calendar-page.component.html',
|
||||
styleUrls: [
|
||||
'../../work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.sass',
|
||||
],
|
||||
@@ -60,7 +63,9 @@ import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decora
|
||||
],
|
||||
standalone: false,
|
||||
})
|
||||
export class WorkPackagesCalendarPageComponent extends PartitionedQuerySpacePageComponent {
|
||||
export class WorkPackagesCalendarPageComponent extends PartitionedQuerySpacePageComponent implements OnInit {
|
||||
@Input() queryId:string;
|
||||
|
||||
@InjectField(ActionsService) actions$:ActionsService;
|
||||
|
||||
@ViewChild(WorkPackagesCalendarComponent, { static: true }) calendarElement:WorkPackagesCalendarComponent;
|
||||
@@ -121,6 +126,20 @@ export class WorkPackagesCalendarPageComponent extends PartitionedQuerySpacePage
|
||||
},
|
||||
];
|
||||
|
||||
override ngOnInit():void {
|
||||
super.ngOnInit();
|
||||
// Fix showToolbarSaveButton from actual URL params (not uiRouter state)
|
||||
this.showToolbarSaveButton = !!new URLSearchParams(window.location.search).get('query_props');
|
||||
|
||||
// Update save button reactively when query_props changes via pushState (non-uiRouter pages)
|
||||
this.wpListChecksumService.visibleChecksum$
|
||||
.pipe(this.untilDestroyed())
|
||||
.subscribe((checksum) => {
|
||||
this.showToolbarSaveButton = !!checksum;
|
||||
this.cdRef.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to set the current partition to the grid to ensure
|
||||
* either side gets expanded to full width if we're not in '-split' mode.
|
||||
|
||||
@@ -57,7 +57,6 @@ import {
|
||||
WorkPackageViewFiltersService,
|
||||
} from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-filters.service';
|
||||
import { WorkPackagesListService } from 'core-app/features/work-packages/components/wp-list/wp-list.service';
|
||||
import { StateService } from '@uirouter/core';
|
||||
import { I18nService } from 'core-app/core/i18n/i18n.service';
|
||||
import { ToastService } from 'core-app/shared/components/toaster/toast.service';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
@@ -70,7 +69,7 @@ import {
|
||||
HalResourceEditingService,
|
||||
} from 'core-app/shared/components/fields/edit/services/hal-resource-editing.service';
|
||||
import { HalResourceNotificationService } from 'core-app/features/hal/services/hal-resource-notification.service';
|
||||
import { splitViewRoute } from 'core-app/features/work-packages/routing/split-view-routes.helper';
|
||||
import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
|
||||
import {
|
||||
CalendarViewEvent,
|
||||
OpWorkPackagesCalendarService,
|
||||
@@ -88,7 +87,6 @@ import {
|
||||
import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service';
|
||||
import { ApiV3FilterBuilder } from 'core-app/shared/helpers/api-v3/api-v3-filter-builder';
|
||||
import allLocales from '@fullcalendar/core/locales-all';
|
||||
import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
|
||||
import { MeetingResource } from 'core-app/features/hal/resources/meeting-resource';
|
||||
import { TimezoneService } from 'core-app/core/datetime/timezone.service';
|
||||
|
||||
@@ -128,7 +126,6 @@ export class WorkPackagesCalendarComponent extends UntilDestroyedMixin implement
|
||||
constructor(
|
||||
readonly actions$:ActionsService,
|
||||
readonly states:States,
|
||||
readonly $state:StateService,
|
||||
readonly wpTableFilters:WorkPackageViewFiltersService,
|
||||
readonly wpListService:WorkPackagesListService,
|
||||
readonly querySpace:IsolatedQuerySpace,
|
||||
@@ -345,13 +342,10 @@ export class WorkPackagesCalendarComponent extends UntilDestroyedMixin implement
|
||||
const workPackageId = (evt.event.extendedProps.workPackage as WorkPackageResource).id!;
|
||||
// Currently the calendar widget is shown on multiple pages,
|
||||
// but only the calendar module itself is a partitioned query space which can deal with a split screen request
|
||||
if (this.$state.includes('calendar')) {
|
||||
if (window.location.pathname.includes('/calendars/')) {
|
||||
this.workPackagesCalendar.openSplitView(workPackageId);
|
||||
} else {
|
||||
void this.$state.go(
|
||||
'work-packages.show',
|
||||
{ workPackageId },
|
||||
);
|
||||
window.location.href = this.pathHelper.workPackagePath(workPackageId);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -414,7 +408,7 @@ export class WorkPackagesCalendarComponent extends UntilDestroyedMixin implement
|
||||
durationEditable: this.workPackagesCalendar.eventDurationEditable(workPackage),
|
||||
end: exclusiveEnd,
|
||||
allDay: true,
|
||||
className: `fc-event-clickable __hl_background_type_${workPackage.type.id || ''}`,
|
||||
className: `fc-event-clickable __hl_background_type_${workPackage.type.id ?? ''}`,
|
||||
workPackage,
|
||||
};
|
||||
});
|
||||
@@ -444,13 +438,17 @@ export class WorkPackagesCalendarComponent extends UntilDestroyedMixin implement
|
||||
ignoreNonWorkingDays: nonWorkingDays,
|
||||
};
|
||||
|
||||
void this.$state.go(
|
||||
splitViewRoute(this.$state, 'new'),
|
||||
{
|
||||
defaults,
|
||||
tabIdentifier: 'overview',
|
||||
},
|
||||
);
|
||||
if (window.location.pathname.includes('/calendars/')) {
|
||||
const basePath = window.location.pathname.replace(/\/details\/.*$/, '');
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.set('startDate', defaults.startDate);
|
||||
params.set('dueDate', defaults.dueDate);
|
||||
if (defaults.ignoreNonWorkingDays) {
|
||||
params.set('ignoreNonWorkingDays', 'true');
|
||||
}
|
||||
const link = `${basePath}/details/new?${params.toString()}`;
|
||||
Turbo.visit(link, { frame: 'content-bodyRight', action: 'advance' });
|
||||
}
|
||||
}
|
||||
|
||||
@EffectCallback(calendarRefreshRequest)
|
||||
|
||||
+1
-1
@@ -98,7 +98,7 @@
|
||||
[showStartDate]="!isWpStartDateInCurrentView(wp)"
|
||||
[showEndDate]="!isWpEndDateInCurrentView(wp)"
|
||||
(stateLinkClicked)="openStateLink($event)"
|
||||
(cardClicked)="workPackagesCalendar.onCardClicked($event)"
|
||||
(cardClicked)="onCardClicked($event)"
|
||||
(cardDblClicked)="workPackagesCalendar.onCardDblClicked($event)"
|
||||
(cardContextMenu)="workPackagesCalendar.showEventContextMenu($event)"
|
||||
/>
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
import {
|
||||
CalendarOptions,
|
||||
DateSelectArg,
|
||||
DatesSetArg,
|
||||
EventApi,
|
||||
EventDropArg,
|
||||
EventInput,
|
||||
@@ -78,6 +79,7 @@ import { WorkPackageViewFiltersService } from 'core-app/features/work-packages/r
|
||||
import { IsolatedQuerySpace } from 'core-app/features/work-packages/directives/query-space/isolated-query-space';
|
||||
import { CurrentProjectService } from 'core-app/core/current-project/current-project.service';
|
||||
import { splitViewRoute } from 'core-app/features/work-packages/routing/split-view-routes.helper';
|
||||
import { isClickedWithModifier } from 'core-app/shared/helpers/link-handling/link-handling';
|
||||
import { QueryFilterInstanceResource } from 'core-app/features/hal/resources/query-filter-instance-resource';
|
||||
import { PrincipalsResourceService } from 'core-app/core/state/principals/principals.service';
|
||||
import {
|
||||
@@ -460,6 +462,7 @@ export class TeamPlannerComponent extends UntilDestroyedMixin implements OnInit,
|
||||
.pipe(
|
||||
this.untilDestroyed(),
|
||||
debounceTime(0),
|
||||
filter(() => !!this.ucCalendar),
|
||||
)
|
||||
.subscribe(([principals, showAddAssignee]) => {
|
||||
const api = this.ucCalendar.getApi();
|
||||
@@ -511,6 +514,19 @@ export class TeamPlannerComponent extends UntilDestroyedMixin implements OnInit,
|
||||
.then(() => {
|
||||
this.calendarOptions$.next(
|
||||
this.workPackagesCalendar.calendarOptions({
|
||||
// Override datesSet to persist cdate/cview via uiRouter instead of pushState,
|
||||
// because uiRouter manages the TeamPlanner URL and would otherwise strip these params.
|
||||
// Remove once uiRouter is removed.
|
||||
datesSet: (dates:DatesSetArg) => {
|
||||
void this.$state.go(
|
||||
'.',
|
||||
{
|
||||
cdate: this.workPackagesCalendar.timezoneService.formattedISODate(dates.view.calendar.getDate()),
|
||||
cview: (dates.view as unknown as { type:string }).type,
|
||||
},
|
||||
{ custom: { notify: false } },
|
||||
);
|
||||
},
|
||||
locales: allLocales,
|
||||
locale: this.I18n.locale,
|
||||
schedulerLicenseKey: 'GPL-My-Project-Is-Open-Source',
|
||||
@@ -857,6 +873,20 @@ export class TeamPlannerComponent extends UntilDestroyedMixin implements OnInit,
|
||||
}
|
||||
}
|
||||
|
||||
onCardClicked({ workPackageId, event }:{ workPackageId:string, event:MouseEvent }):void {
|
||||
if (isClickedWithModifier(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only switch the split view if it is already open
|
||||
if (!window.location.pathname.includes('/details/')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.workPackagesCalendar.wpTableSelection.setSelection(workPackageId, -1);
|
||||
this.keepTab.goCurrentDetailsState({ workPackageId });
|
||||
}
|
||||
|
||||
shouldShowAsGhost(id:string, globalDraggingId:string|undefined):boolean {
|
||||
if (globalDraggingId === undefined) {
|
||||
return false;
|
||||
|
||||
+26
@@ -31,6 +31,7 @@ import { UrlParamsHelperService } from 'core-app/features/work-packages/componen
|
||||
import { Injectable } from '@angular/core';
|
||||
import { WorkPackageViewPagination } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-table-pagination';
|
||||
import { QueryResource } from 'core-app/features/hal/resources/query-resource';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Injectable()
|
||||
export class WorkPackagesListChecksumService {
|
||||
@@ -44,6 +45,9 @@ export class WorkPackagesListChecksumService {
|
||||
|
||||
public visibleChecksum:string|null;
|
||||
|
||||
/** Emits whenever visibleChecksum changes (useful for non-uiRouter pages to react to URL param changes) */
|
||||
public readonly visibleChecksum$ = new Subject<string|null>();
|
||||
|
||||
public updateIfDifferent(query:QueryResource,
|
||||
pagination:WorkPackageViewPagination):Promise<unknown> {
|
||||
const newQueryChecksum = this.getNewChecksum(query, pagination);
|
||||
@@ -153,6 +157,28 @@ export class WorkPackagesListChecksumService {
|
||||
|
||||
private maintainUrlQueryState(id:string|null, checksum:string|null):TransitionPromise {
|
||||
this.visibleChecksum = checksum;
|
||||
this.visibleChecksum$.next(checksum);
|
||||
|
||||
// When uiRouter is not managing the current page (e.g. calendar after Turbo migration),
|
||||
// $state.current.name is empty and state.go('.') does nothing. Fall back to pushState.
|
||||
if (!this.$state.current.name) {
|
||||
const url = new URL(window.location.href);
|
||||
|
||||
if (checksum) {
|
||||
url.searchParams.set('query_props', checksum);
|
||||
} else {
|
||||
url.searchParams.delete('query_props');
|
||||
}
|
||||
|
||||
if (id) {
|
||||
url.searchParams.set('query_id', id);
|
||||
} else {
|
||||
url.searchParams.delete('query_id');
|
||||
}
|
||||
|
||||
window.history.pushState({}, '', url.toString());
|
||||
return Promise.resolve() as unknown as TransitionPromise;
|
||||
}
|
||||
|
||||
return this.$state.go(
|
||||
'.',
|
||||
|
||||
@@ -261,6 +261,7 @@ export class WorkPackagesListService {
|
||||
|
||||
// Reload the query, and then reload the menu
|
||||
this.reloadQuery(createdQuery).subscribe(() => {
|
||||
this.navigateToQueryOnNonRouterPage(createdQuery.id);
|
||||
this.states.changes.queries.next(createdQuery.id);
|
||||
this.reloadSidemenu(createdQuery.id);
|
||||
});
|
||||
@@ -311,7 +312,11 @@ export class WorkPackagesListService {
|
||||
this.toastService.addSuccess(this.I18n.t('js.notice_successful_update'));
|
||||
const queryAccessibleByUser = query.public || query.user.id === this.currentUser.userId;
|
||||
if (queryAccessibleByUser) {
|
||||
void this.$state.go('.', { query_id: query.id, query_props: null }, { reload: true });
|
||||
if (!this.$state.current.name) {
|
||||
this.navigateToQueryOnNonRouterPage(query.id);
|
||||
} else {
|
||||
void this.$state.go('.', { query_id: query.id, query_props: null }, { reload: true });
|
||||
}
|
||||
this.states.changes.queries.next(query.id);
|
||||
this.reloadSidemenu(query.id);
|
||||
} else {
|
||||
@@ -463,7 +468,27 @@ export class WorkPackagesListService {
|
||||
}
|
||||
}
|
||||
|
||||
private navigateToQueryOnNonRouterPage(queryId:string|null):void {
|
||||
if (this.$state.current.name) { return; }
|
||||
|
||||
// update the URL path to reflect the saved query ID so subsequent refetches use the correct query_id.
|
||||
const url = new URL(window.location.href);
|
||||
url.pathname = url.pathname.replace(/\/[^/]+$/, `/${queryId}`);
|
||||
url.searchParams.delete('query_id');
|
||||
url.searchParams.delete('query_props');
|
||||
window.history.pushState({}, '', url.toString());
|
||||
}
|
||||
|
||||
private reloadSidemenu(selectedQueryId:string|null):void {
|
||||
this.submenuService.reloadSubmenu(selectedQueryId);
|
||||
const sidemenuId = !this.$state.current.name ? this.getNonRouterSidemenuId() : undefined;
|
||||
this.submenuService.reloadSubmenu(selectedQueryId, sidemenuId);
|
||||
}
|
||||
|
||||
private getNonRouterSidemenuId():string|undefined {
|
||||
const { pathname } = window.location;
|
||||
if (pathname.includes('/calendars')) return 'calendar_sidemenu';
|
||||
if (pathname.includes('/team_planners')) return 'team_planner_sidemenu';
|
||||
if (pathname.includes('/ifc_models')) return 'bim_sidemenu';
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
+14
-14
@@ -34,20 +34,20 @@ export class WorkPackagesQueryViewService {
|
||||
}
|
||||
|
||||
private get viewType() {
|
||||
if (this.$state.includes('work-packages')) {
|
||||
return 'work_packages_table';
|
||||
}
|
||||
if (this.$state.includes('team_planner')) {
|
||||
return 'team_planner';
|
||||
}
|
||||
if (this.$state.includes('bim')) {
|
||||
return 'bim';
|
||||
}
|
||||
if (this.$state.includes('calendar')) {
|
||||
return 'work_packages_calendar';
|
||||
}
|
||||
if (this.$state.includes('gantt')) {
|
||||
return 'gantt';
|
||||
if (this.$state.current.name) {
|
||||
if (this.$state.includes('work-packages')) { return 'work_packages_table'; }
|
||||
if (this.$state.includes('team_planner')) { return 'team_planner'; }
|
||||
if (this.$state.includes('bim')) { return 'bim'; }
|
||||
if (this.$state.includes('calendar')) { return 'work_packages_calendar'; }
|
||||
if (this.$state.includes('gantt')) { return 'gantt'; }
|
||||
} else {
|
||||
// Non-uiRouter page — derive view type from URL path
|
||||
const { pathname } = window.location;
|
||||
if (pathname.includes('/calendars')) { return 'work_packages_calendar'; }
|
||||
if (pathname.includes('/team_planners')) { return 'team_planner'; }
|
||||
if (pathname.includes('/ifc_models')) { return 'bim'; }
|
||||
if (pathname.includes('/gantt')) { return 'gantt'; }
|
||||
if (pathname.includes('/work_packages')) { return 'work_packages_table'; }
|
||||
}
|
||||
|
||||
throw new Error('Not on a path defined for query views');
|
||||
|
||||
@@ -41,7 +41,7 @@ module Calendar
|
||||
|
||||
def dynamic_path
|
||||
if current_project
|
||||
new_project_calendars_path(current_project)
|
||||
new_project_calendar_path(current_project)
|
||||
else
|
||||
new_calendar_path
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
@@ -31,14 +33,17 @@ module ::Calendar
|
||||
before_action :load_and_authorize_in_optional_project
|
||||
before_action :build_calendar_view, only: %i[new]
|
||||
before_action :authorize, except: %i[index new create]
|
||||
before_action :authorize_global, only: %i[index new create]
|
||||
before_action :authorize_global, only: %i[index create]
|
||||
before_action :authorize_new, only: %i[new]
|
||||
authorization_checked! :new
|
||||
|
||||
before_action :find_calendar, only: %i[destroy]
|
||||
before_action :find_calendar, only: %i[show split_view destroy]
|
||||
menu_item :calendar_view
|
||||
|
||||
include Layout
|
||||
include PaginationHelper
|
||||
include SortHelper
|
||||
include WorkPackages::WithSplitView
|
||||
|
||||
def index
|
||||
@views = visible_views
|
||||
@@ -46,10 +51,26 @@ module ::Calendar
|
||||
end
|
||||
|
||||
def show
|
||||
render layout: "angular/angular"
|
||||
render
|
||||
end
|
||||
|
||||
def new; end
|
||||
def split_view
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
if turbo_frame_request?
|
||||
render "work_packages/split_view", layout: false
|
||||
else
|
||||
render :show
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def new
|
||||
# In a project context, show the calendar view with an unsaved query.
|
||||
# In the global context (no project), show the form so the user can select a project.
|
||||
render :show if @project
|
||||
end
|
||||
|
||||
def create
|
||||
service_result = create_service_class.new(user: User.current)
|
||||
@@ -59,7 +80,7 @@ module ::Calendar
|
||||
|
||||
if service_result.success?
|
||||
flash[:notice] = I18n.t(:notice_successful_create)
|
||||
redirect_to project_calendar_path(@project, @view.query)
|
||||
redirect_to project_calendar_path(@project || @view.query.project, @view.query)
|
||||
else
|
||||
render action: :new, status: :unprocessable_entity
|
||||
end
|
||||
@@ -77,6 +98,16 @@ module ::Calendar
|
||||
|
||||
private
|
||||
|
||||
# In project context, `new` renders the calendar view and needs the same project-level
|
||||
# permission as `show`. In global context (no project), it shows the creation form.
|
||||
def authorize_new
|
||||
@project ? authorize : authorize_global
|
||||
end
|
||||
|
||||
def split_view_base_route
|
||||
project_calendar_path(@project, @view, request.query_parameters)
|
||||
end
|
||||
|
||||
def build_calendar_view
|
||||
@view = Query.new
|
||||
end
|
||||
@@ -86,7 +117,7 @@ module ::Calendar
|
||||
end
|
||||
|
||||
def calendar_view_params
|
||||
params.require(:query).permit(:name, :public, :starred).merge(project_id: @project&.id)
|
||||
params.expect(query: %i[name public starred project_id])
|
||||
end
|
||||
|
||||
def visible_views
|
||||
|
||||
@@ -33,7 +33,7 @@ module ::Calendar
|
||||
def show
|
||||
@submenu_menu_items = ::Calendar::Menu.new(project: @project, params:).menu_items
|
||||
@create_btn_options = if User.current.allowed_in_project?(:manage_calendars, @project)
|
||||
{ href: new_project_calendars_path(@project), module_key: "calendar" }
|
||||
{ href: new_project_calendar_path(@project), module_key: "calendar" }
|
||||
end
|
||||
|
||||
render layout: nil
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
<%#-- 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.
|
||||
|
||||
++#%>
|
||||
|
||||
<%= error_messages_for @view %>
|
||||
|
||||
<section class="form--section">
|
||||
|
||||
<div class="form--field -required">
|
||||
<%= f.text_field :name,
|
||||
label: t(:label_title),
|
||||
required: true,
|
||||
size: 60,
|
||||
container_class: "-wide" %>
|
||||
</div>
|
||||
|
||||
<div class="form--field -required">
|
||||
<label class="form--label" for="project_id"><%= Query.human_attribute_name(:project) %>:</label>
|
||||
<div class="form--field-container">
|
||||
<%= angular_component_tag "opce-project-autocompleter",
|
||||
inputs: {
|
||||
filters: [{ name: :user_action, operator: "=", values: ["calendars/create"] }],
|
||||
inputName: "project_id",
|
||||
inputValue: @project&.id
|
||||
},
|
||||
id: "project_id",
|
||||
class: "form--select-container -wide remote-field--input",
|
||||
data: {
|
||||
"test-selector": "project_id"
|
||||
} %>
|
||||
</div>
|
||||
<div class="form--field-instructions">
|
||||
<p><%= t "help_texts.views.project",
|
||||
singular: t(:label_calendar).downcase,
|
||||
plural: t(:label_calendar_plural) %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form--field">
|
||||
<%= styled_label_tag "query[public]",
|
||||
Query.human_attribute_name(:public) %>
|
||||
<div class="form--field-container">
|
||||
<%= styled_check_box_tag "query[public]",
|
||||
"1",
|
||||
@view.public %>
|
||||
</div>
|
||||
<div class="form--field-instructions">
|
||||
<p><%= t "help_texts.views.public" %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form--field">
|
||||
<%= styled_label_tag "query[starred]",
|
||||
t(:label_favorite) %>
|
||||
<div class="form--field-container">
|
||||
<%= styled_check_box_tag "query[starred]",
|
||||
"1",
|
||||
@view.starred %>
|
||||
</div>
|
||||
<div class="form--field-instructions">
|
||||
<p><%= t "help_texts.views.favoured" %></p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -39,9 +39,48 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
end
|
||||
%>
|
||||
|
||||
<%= labelled_tabular_form_for @view, url: { controller: "/calendar/calendars", action: "create" }, html: { id: "calendar-form" } do |f| -%>
|
||||
<%= render partial: "form", locals: { f: f } %>
|
||||
<%= styled_button_tag t(:button_create), class: "-primary" %>
|
||||
<%= link_to t(:button_cancel), { controller: "calendar/calendars", action: "index" },
|
||||
class: "button" %>
|
||||
<%= primer_form_with(model: @view, scope: :query, url: calendars_path) do |f| %>
|
||||
<%= render_inline_form(f) do |form|
|
||||
project_id_value = @project&.id
|
||||
form.text_field(
|
||||
name: :name,
|
||||
label: helpers.t(:label_title),
|
||||
required: true,
|
||||
input_width: :large
|
||||
)
|
||||
|
||||
form.project_autocompleter(
|
||||
name: :project_id,
|
||||
label: Query.human_attribute_name(:project),
|
||||
required: true,
|
||||
caption: helpers.t("help_texts.views.project",
|
||||
singular: helpers.t(:label_calendar).downcase,
|
||||
plural: helpers.t(:label_calendar_plural)),
|
||||
input_width: :large,
|
||||
autocomplete_options: {
|
||||
focusDirectly: false,
|
||||
dropdownPosition: "bottom",
|
||||
inputValue: project_id_value,
|
||||
filters: [{ name: "user_action", operator: "=", values: ["calendars/create"] }],
|
||||
data: { "test-selector": "project_id" }
|
||||
}
|
||||
)
|
||||
|
||||
form.check_box(
|
||||
name: :public,
|
||||
label: Query.human_attribute_name(:public),
|
||||
caption: helpers.t("help_texts.views.public")
|
||||
)
|
||||
|
||||
form.check_box(
|
||||
name: :starred,
|
||||
label: helpers.t(:label_favorite),
|
||||
caption: helpers.t("help_texts.views.favoured")
|
||||
)
|
||||
|
||||
form.group(layout: :horizontal) do |button_group|
|
||||
button_group.submit(label: helpers.t(:button_create), name: :submit, scheme: :primary)
|
||||
button_group.button(tag: :a, href: helpers.calendars_path, label: helpers.t(:button_cancel), name: :cancel)
|
||||
end
|
||||
end %>
|
||||
<% end %>
|
||||
|
||||
@@ -27,4 +27,13 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
++#%>
|
||||
|
||||
<% html_title(t(:label_calendar_plural)) -%>
|
||||
<% html_title(@view.name.presence || t(:label_calendar_plural)) -%>
|
||||
|
||||
<% content_for :content_body do %>
|
||||
<%= angular_component_tag "opce-calendar-view", inputs: { queryId: @view.id.to_s } %>
|
||||
<% end %>
|
||||
|
||||
<% content_for :content_body_right do %>
|
||||
<%= turbo_stream.set_title(title: page_title(*html_title_parts)) if turbo_frame_request? %>
|
||||
<%= render(split_view_instance) if render_work_package_split_view? %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<%= turbo_frame_tag "calendar_sidemenu",
|
||||
src: menu_project_calendars_path(@project, **params.permit(:id)),
|
||||
target: "_top",
|
||||
data: { turbo: false },
|
||||
data: { turbo: false, query_param: "id" },
|
||||
loading: :lazy %>
|
||||
|
||||
@@ -2,14 +2,19 @@ Rails.application.routes.draw do
|
||||
scope "projects/:project_id", as: "project" do
|
||||
resources :calendars,
|
||||
controller: "calendar/calendars",
|
||||
only: %i[index destroy],
|
||||
only: %i[index show new create destroy],
|
||||
as: :calendars do
|
||||
collection do
|
||||
get "menu" => "calendar/menus#show"
|
||||
end
|
||||
get "/new" => "calendar/calendars#show", on: :collection, as: "new"
|
||||
get "/ical" => "calendar/ical#show", on: :member, as: "ical"
|
||||
get "(/*state)" => "calendar/calendars#show", on: :member, as: ""
|
||||
member do
|
||||
get "details/:work_package_id(/:tab)",
|
||||
action: :split_view,
|
||||
defaults: { tab: :overview },
|
||||
as: :details,
|
||||
work_package_split_view: true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ module OpenProject::Calendar
|
||||
settings: {} do
|
||||
project_module :calendar_view, dependencies: :work_package_tracking do
|
||||
permission :view_calendar,
|
||||
{ "calendar/calendars": %i[index show],
|
||||
{ "calendar/calendars": %i[index show split_view new],
|
||||
"calendar/menus": %i[show] },
|
||||
permissible_on: :project,
|
||||
dependencies: %i[view_work_packages],
|
||||
|
||||
@@ -241,6 +241,7 @@ RSpec.describe "Team planner",
|
||||
it "can add and remove assignees" do
|
||||
team_planner.visit!
|
||||
|
||||
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)
|
||||
|
||||
@@ -38,6 +38,7 @@ RSpec.describe "Team planner",
|
||||
it "allows switching of view modes", with_settings: { working_days: [1, 2, 3, 4, 5] } do
|
||||
team_planner.visit!
|
||||
|
||||
team_planner.wait_for_loaded
|
||||
team_planner.expect_empty_state
|
||||
team_planner.add_assignee user.name
|
||||
|
||||
|
||||
Reference in New Issue
Block a user