Convert single view

pull/6179/head
Oliver Günther 7 years ago
parent a27212a424
commit df695672b6
No known key found for this signature in database
GPG Key ID: 88872239EB414F99
  1. 38
      frontend/app/angular4-modules.ts
  2. 1
      frontend/app/angular4-transition-utils.ts
  3. 79
      frontend/app/components/common/authoring/authoring.component.ts
  4. 18
      frontend/app/components/common/authoring/authoring.html
  5. 11
      frontend/app/components/common/date/op-date-time.directive.ts
  6. 43
      frontend/app/components/common/date/op-date-time.upgraded.directive.ts
  7. 73
      frontend/app/components/common/focus-within/focus-within.directive.ts
  8. 30
      frontend/app/components/common/help-texts/attribute-help-text-ng1-wrapper.ts
  9. 3
      frontend/app/components/routing/ui-router.config.ts
  10. 2
      frontend/app/components/routing/wp-split-view/wp-split-view.component.ts
  11. 2
      frontend/app/components/routing/wp-view-base/wp-view-base.controller.ts
  12. 29
      frontend/app/components/user/user-link/user-link.upgraded.component.ts
  13. 2
      frontend/app/components/work-packages/wp-attachments-formattable-field/wp-attachments-formattable.directive.ts
  14. 129
      frontend/app/components/work-packages/wp-single-view/wp-single-view.component.ts
  15. 170
      frontend/app/components/work-packages/wp-single-view/wp-single-view.directive.html
  16. 173
      frontend/app/components/work-packages/wp-single-view/wp-single-view.html
  17. 61
      frontend/app/components/wp-attachments/wp-attachment-list/wp-attachment-list-item.component.ts
  18. 23
      frontend/app/components/wp-attachments/wp-attachment-list/wp-attachment-list-item.html
  19. 32
      frontend/app/components/wp-attachments/wp-attachment-list/wp-attachment-list.component.ts
  20. 16
      frontend/app/components/wp-attachments/wp-attachment-list/wp-attachment-list.directive.html
  21. 14
      frontend/app/components/wp-attachments/wp-attachment-list/wp-attachment-list.html
  22. 56
      frontend/app/components/wp-attachments/wp-attachments-upload/wp-attachments-upload-ng1-wrapper.ts
  23. 21
      frontend/app/components/wp-attachments/wp-attachments-upload/wp-attachments-upload.directive.ts
  24. 21
      frontend/app/components/wp-attachments/wp-attachments-upload/wp-attachments-upload.html
  25. 2
      frontend/app/components/wp-buttons/wp-details-view-button/wp-details-view-button.component.ts
  26. 33
      frontend/app/components/wp-buttons/wp-status-button/wp-status-button.component.ts
  27. 14
      frontend/app/components/wp-buttons/wp-status-button/wp-status-button.directive.html
  28. 15
      frontend/app/components/wp-buttons/wp-status-button/wp-status-button.html
  29. 58
      frontend/app/components/wp-edit/wp-edit-field/wp-replacement-label.component.ts
  30. 4
      frontend/app/components/wp-edit/wp-edit-field/wp-replacement-label.directive.html
  31. 4
      frontend/app/components/wp-edit/wp-edit-field/wp-replacement-label.html
  32. 2
      frontend/app/components/wp-fast-table/builders/context-link-icon-builder.ts
  33. 2
      frontend/app/components/wp-fast-table/builders/ui-state-link-builder.ts
  34. 2
      frontend/app/components/wp-fast-table/handlers/row/click-handler.ts
  35. 2
      frontend/app/components/wp-fast-table/handlers/row/wp-state-links-handler.ts
  36. 11
      frontend/app/components/wp-panels/overview-panel/overview-panel.directive.html
  37. 0
      frontend/app/components/wp-single-view-tabs/activity-panel/activity-entry-info.ts
  38. 0
      frontend/app/components/wp-single-view-tabs/activity-panel/activity-panel-default.directive.html
  39. 0
      frontend/app/components/wp-single-view-tabs/activity-panel/activity-panel-overview.directive.html
  40. 0
      frontend/app/components/wp-single-view-tabs/activity-panel/activity-panel.directive.ts
  41. 0
      frontend/app/components/wp-single-view-tabs/activity-panel/wp-activity.service.ts
  42. 0
      frontend/app/components/wp-single-view-tabs/keep-tab/keep-tab.service.test.ts
  43. 0
      frontend/app/components/wp-single-view-tabs/keep-tab/keep-tab.service.ts
  44. 58
      frontend/app/components/wp-single-view-tabs/overview-tab/overview-tab.component.ts
  45. 13
      frontend/app/components/wp-single-view-tabs/overview-tab/overview-tab.html
  46. 0
      frontend/app/components/wp-single-view-tabs/relations-panel/relations-panel.directive.html
  47. 0
      frontend/app/components/wp-single-view-tabs/relations-panel/relations-panel.directive.ts
  48. 0
      frontend/app/components/wp-single-view-tabs/watchers-panel/watchers-panel.controller.ts
  49. 0
      frontend/app/components/wp-single-view-tabs/watchers-panel/watchers-panel.directive.html
  50. 0
      frontend/app/components/wp-single-view-tabs/watchers-panel/watchers-panel.directive.test.js
  51. 0
      frontend/app/components/wp-single-view-tabs/watchers-panel/watchers-panel.directive.ts
  52. 2
      frontend/app/components/wp-watchers/wp-watcher-entry.directive.ts
  53. 1
      frontend/app/templates/components/authoring.html
  54. 67
      frontend/app/ui_components/authoring-directive.js
  55. 3
      frontend/app/ui_components/index.js

@ -52,7 +52,7 @@ import {WorkPackageTableSelection} from 'core-components/wp-fast-table/state/wp-
import {WorkPackageTableSortByService} from 'core-components/wp-fast-table/state/wp-table-sort-by.service';
import {WorkPackageTableTimelineService} from 'core-components/wp-fast-table/state/wp-table-timeline.service';
import {WpInlineCreateDirectiveUpgraded} from 'core-components/wp-inline-create/wp-inline-create.directive';
import {KeepTabService} from 'core-components/wp-panels/keep-tab/keep-tab.service';
import {KeepTabService} from 'core-components/wp-single-view-tabs/keep-tab/keep-tab.service';
import {WorkPackageRelationsService} from 'core-components/wp-relations/wp-relations.service';
import {WpResizerDirectiveUpgraded} from 'core-components/wp-resizer/wp-resizer.directive';
import {SortHeaderDirective} from 'core-components/wp-table/sort-header/sort-header.directive';
@ -75,7 +75,7 @@ import {
halRequestToken,
I18nToken,
NotificationsServiceToken,
PathHelperToken,
PathHelperToken, TimezoneServiceToken,
upgradeService,
upgradeServiceWithToken, wpMoreMenuServiceToken
} from './angular4-transition-utils';
@ -114,6 +114,19 @@ import {WorkPackageEditFieldComponent} from 'core-components/wp-edit/wp-edit-fie
import {WorkPackageCreateService} from 'core-components/wp-create/wp-create.service';
import {WorkPackageWatcherButtonComponent} from 'core-components/work-packages/wp-watcher-button/wp-watcher-button.component';
import {WorkPackageSplitViewToolbarComponent} from 'core-components/wp-details/wp-details-toolbar.component';
import {WorkPackageOverviewTabComponent} from 'core-components/wp-single-view-tabs/overview-tab/overview-tab.component';
import {CurrentProjectService} from 'core-components/projects/current-project.service';
import {WorkPackageSingleViewComponent} from 'core-components/work-packages/wp-single-view/wp-single-view.component';
import {WorkPackageStatusButtonComponent} from 'core-components/wp-buttons/wp-status-button/wp-status-button.component';
import {Ng1AttributeHelpTextWrapper} from 'core-components/common/help-texts/attribute-help-text-ng1-wrapper';
import {WorkPackageReplacementLabelComponent} from 'core-components/wp-edit/wp-edit-field/wp-replacement-label.component';
import {FocusWithinDirective} from 'core-components/common/focus-within/focus-within.directive';
import {AuthoringComponent} from 'core-components/common/authoring/authoring.component';
import {Ng1WorkPackageAttachmentsUploadWrapper} from 'core-components/wp-attachments/wp-attachments-upload/wp-attachments-upload-ng1-wrapper';
import {WorkPackageAttachmentListComponent} from 'core-components/wp-attachments/wp-attachment-list/wp-attachment-list.component';
import {WorkPackageAttachmentListItemComponent} from 'core-components/wp-attachments/wp-attachment-list/wp-attachment-list-item.component';
import {OpDateTimeUpgradedDirective} from 'core-components/common/date/op-date-time.upgraded.directive';
import {UserLinkUpgradedComponent} from 'core-components/user/user-link/user-link.upgraded.component';
@NgModule({
imports: [
@ -136,6 +149,7 @@ import {WorkPackageSplitViewToolbarComponent} from 'core-components/wp-details/w
upgradeServiceWithToken('PathHelper', PathHelperToken),
upgradeServiceWithToken('halRequest', halRequestToken),
upgradeServiceWithToken('wpMoreMenuService', wpMoreMenuServiceToken),
upgradeServiceWithToken('TimezoneService', TimezoneServiceToken),
upgradeService('wpRelations', WorkPackageRelationsService),
upgradeService('wpCacheService', WorkPackageCacheService),
upgradeService('wpEditing', WorkPackageEditingService),
@ -164,6 +178,7 @@ import {WorkPackageSplitViewToolbarComponent} from 'core-components/wp-details/w
upgradeService('contextMenu', ContextMenuService),
upgradeService('authorisationService', AuthorisationService),
upgradeService('ConfigurationService', ConfigurationService),
upgradeService('currentProject', CurrentProjectService),
// Split view
upgradeService('wpCreate', WorkPackageCreateService),
upgradeService('firstRoute', FirstRouteService),
@ -208,6 +223,20 @@ import {WorkPackageSplitViewToolbarComponent} from 'core-components/wp-details/w
WorkPackageWatcherButtonComponent,
WorkPackageSubjectComponent,
// Single view
WorkPackageOverviewTabComponent,
WorkPackageSingleViewComponent,
WorkPackageStatusButtonComponent,
Ng1AttributeHelpTextWrapper,
WorkPackageReplacementLabelComponent,
FocusWithinDirective,
AuthoringComponent,
Ng1WorkPackageAttachmentsUploadWrapper,
WorkPackageAttachmentListComponent,
WorkPackageAttachmentListItemComponent,
OpDateTimeUpgradedDirective,
UserLinkUpgradedComponent,
// WP Edit Fields
WorkPackageEditFieldComponent,
],
@ -222,7 +251,10 @@ import {WorkPackageSplitViewToolbarComponent} from 'core-components/wp-details/w
AddSectionDropdownComponent,
// Split view
WorkPackageSplitViewComponent
WorkPackageSplitViewComponent,
// Single view
WorkPackageOverviewTabComponent,
]
})
export class OpenProjectModule {

@ -43,6 +43,7 @@ export const v3PathToken = new InjectionToken<any>('v3Path');
export const PathHelperToken = new InjectionToken<any>('PathHelper');
export const halRequestToken = new InjectionToken<any>('halRequest');
export const wpMoreMenuServiceToken = new InjectionToken<any>('wpMoreMenuService');
export const TimezoneServiceToken = new InjectionToken<any>('TimezoneService');
export const $httpToken = new InjectionToken<any>('$http');
export const halResourceFactoryToken = new InjectionToken<any>('halResourceFactory');

@ -0,0 +1,79 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
//
// 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-2017 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 docs/COPYRIGHT.rdoc for more details.
//++
import {Component, Inject, Input, OnInit} from '@angular/core';
import {I18nToken, TimezoneServiceToken} from '../../../angular4-transition-utils';
import {PathHelperService} from 'core-components/common/path-helper/path-helper.service';
import {HalResource} from 'core-components/api/api-v3/hal-resources/hal-resource.service';
import {opUiComponentsModule} from 'core-app/angular-modules';
import {downgradeComponent} from '@angular/upgrade/static';
@Component({
template: require('!!raw-loader!./authoring.html'),
selector: 'authoring',
})
export class AuthoringComponent implements OnInit {
// scope: { createdOn: '=', author: '=', showAuthorAsLink: '=', project: '=', activity: '=' },
@Input('createdOn') createdOn:string;
@Input('author') author:HalResource;
@Input('showAuthorAsLink') showAuthorAsLink:boolean;
@Input('project') project:any;
@Input('activity') activity:any;
public createdOnTime:any;
public timeago:any;
public time:any;
public userLink:string;
public constructor(readonly PathHelper:PathHelperService,
@Inject(I18nToken) readonly I18n:op.I18n,
@Inject(TimezoneServiceToken) readonly TimezoneService:any) {
}
ngOnInit() {
this.createdOnTime = this.TimezoneService.parseDatetime(this.createdOn);
this.timeago = this.createdOnTime.fromNow();
this.time = this.createdOnTime.format('LLL');
this.userLink = this.PathHelper.userPath(this.author.id);
}
public activityFromPath(from:any) {
var path = this.PathHelper.projectActivityPath(this.project);
if (from) {
path += '?from=' + from;
}
return path;
}
}
opUiComponentsModule.directive('authoring',
downgradeComponent({component: AuthoringComponent})
);

@ -0,0 +1,18 @@
<div class="authoring-entry">
<span *ngIf="showAuthorAsLink" [textContent]="author.name"></span>
<span *ngIf="!showAuthorAsLink">
<a [attr.href]="userLink"
[textContent]="author.name"></a>
</span>
<span *ngIf="activity">
<a [attr.title]="time"
[attr.href]="activityFromPath(createdOn.format('YYYY-MM-DD'))"
[textContent]="timeago"></a>
</span>
<span *ngIf="!activity"
class="timestamp"
[textContent]="timeago"
[attr.title]="time">
</span>
</div>

@ -26,18 +26,21 @@
// See docs/COPYRIGHT.rdoc for more details.
//++
module.exports = function($compile, TimezoneService) {
import {opUiComponentsModule} from '../../../angular-modules';
function opDateTimeDirective($compile:ng.ICompileService, TimezoneService:any) {
return {
restrict: 'EA',
replace: true,
scope: { dateTimeValue: '=' },
// Note: we cannot reuse op-date here as this does not apply the user's configured timezone
template: '<span title="{{ date }} {{ time }}"><span>{{date}}</span> <span>{{time}}</span></span>',
link: function(scope, element, attrs) {
link: function(scope:any, element:ng.IAugmentedJQuery) {
var c = TimezoneService.formattedDatetimeComponents(scope.dateTimeValue);
scope.date = c[0];
scope.time = c[1];
$compile(element.contents())(scope);
}
};
};
}
opUiComponentsModule.directive('opDateTime', opDateTimeDirective);

@ -0,0 +1,43 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
//
// 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-2017 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 doc/COPYRIGHT.rdoc for more details.
//++
import {Component, Directive, ElementRef, Injector, Input, Output} from '@angular/core';
import {UpgradeComponent} from '@angular/upgrade/static';
import {UserResource} from 'core-components/api/api-v3/hal-resources/user-resource.service';
@Directive({
selector: 'op-date-time'
})
export class OpDateTimeUpgradedDirective extends UpgradeComponent {
@Input('dateTimeValue') dateTimeValue:any;
constructor(elementRef:ElementRef, injector:Injector) {
super('opDateTime', elementRef, injector);
}
}

@ -29,46 +29,57 @@
import {wpDirectivesModule} from '../../../angular-modules';
import {scopedObservable} from '../../../helpers/angular-rx-utils';
import {BehaviorSubject} from 'rxjs';
import {Directive, ElementRef, Input, OnDestroy, OnInit} from '@angular/core';
import {componentDestroyed} from 'ng2-rx-componentdestroyed';
import {downgradeComponent} from '@angular/upgrade/static';
// with courtesy of http://stackoverflow.com/a/29722694/3206935
function focusWithinDirective($timeout:ng.ITimeoutService) {
return {
restrict: 'A',
@Directive({
selector: '[focus-within]'
})
export class FocusWithinDirective implements OnInit, OnDestroy {
@Input('focusWithinSelector') selector:string;
scope: {
selector: '=focusWithin'
},
private $element:ng.IAugmentedJQuery;
link: (scope:any, element:ng.IAugmentedJQuery) => {
let focusedObservable = new BehaviorSubject(false);
constructor(private elementRef:ElementRef) {
scopedObservable(
scope,
focusedObservable
)
.auditTime(50)
.subscribe(focused => {
element.toggleClass('-focus', focused);
});
}
ngOnInit() {
this.$element = jQuery(this.elementRef.nativeElement);
let focusedObservable = new BehaviorSubject(false);
let focusListener = function () {
focusedObservable.next(true);
};
element[0].addEventListener('focus', focusListener, true);
focusedObservable
.auditTime(50)
.takeUntil(componentDestroyed(this))
.subscribe(focused => {
this.$element.toggleClass('-focus', focused);
});
let blurListener = function () {
focusedObservable.next(false);
};
element[0].addEventListener('blur', blurListener, true);
$timeout(() => {
element.addClass('focus-within--trigger');
element.find(scope.selector).addClass('focus-within--depending');
}, 0);
}
};
let focusListener = function () {
focusedObservable.next(true);
};
this.$element[0].addEventListener('focus', focusListener, true);
let blurListener = function () {
focusedObservable.next(false);
};
this.$element[0].addEventListener('blur', blurListener, true);
setTimeout(() => {
this.$element.addClass('focus-within--trigger');
this.$element.find(this.selector).addClass('focus-within--depending');
}, 0);
}
ngOnDestroy() {
// NOthing to do
}
}
wpDirectivesModule.directive('focusWithin', focusWithinDirective);
wpDirectivesModule.directive('focusWithin',
downgradeComponent({component: FocusWithinDirective})
);

@ -0,0 +1,30 @@
// This Angular directive will act as an interface to the "upgraded" AngularJS component
// query-filters
import {
Directive, DoCheck, ElementRef, Inject, Injector, Input, OnChanges, OnDestroy,
OnInit, SimpleChanges
} from '@angular/core';
import {UpgradeComponent} from '@angular/upgrade/static';
@Directive({selector: 'ng1-attribute-help-text-wrapper'})
export class Ng1AttributeHelpTextWrapper extends UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
@Input('attribute') public attribute:string;
@Input('attributeScope') public attributeScope:string;
@Input('helpTextId') public helpTextId?:string;
@Input('additionalLabel') public additionalLabel?:string;
constructor(@Inject(ElementRef) elementRef:ElementRef, @Inject(Injector) injector:Injector) {
// We must pass the name of the directive as used by AngularJS to the super
super('attributeHelpText', elementRef, injector);
}
// For this class to work when compiled with AoT, we must implement these lifecycle hooks
// because the AoT compiler will not realise that the super class implements them
ngOnInit() { super.ngOnInit(); }
ngOnChanges(changes:SimpleChanges) { super.ngOnChanges(changes); }
ngDoCheck() { super.ngDoCheck(); }
ngOnDestroy() { super.ngOnDestroy(); }
}

@ -31,13 +31,14 @@ import {FirstRouteService} from 'app/components/routing/first-route-service';
import {Transition, TransitionService, UrlMatcherFactory, UrlService} from '@uirouter/core';
import {WorkPackageSplitViewComponent} from 'core-components/routing/wp-split-view/wp-split-view.component';
import {WorkPackagesListComponent} from 'core-components/routing/wp-list/wp-list.component';
import {WorkPackageOverviewTabComponent} from 'core-components/wp-single-view-tabs/overview-tab/overview-tab.component';
const panels = {
get overview() {
return {
url: '/overview',
reloadOnSearch: false,
template: '<overview-panel work-package="$ctrl.workPackage"></overview-panel>'
component: WorkPackageOverviewTabComponent
};
},

@ -28,7 +28,7 @@
import {States} from '../../states.service';
import {WorkPackageTableSelection} from '../../wp-fast-table/state/wp-table-selection.service';
import {KeepTabService} from '../../wp-panels/keep-tab/keep-tab.service';
import {KeepTabService} from '../../wp-single-view-tabs/keep-tab/keep-tab.service';
import {WorkPackageViewController} from '../wp-view-base/wp-view-base.controller';
import {FirstRouteService} from 'core-components/routing/first-route-service';
import {WorkPackageTableFocusService} from 'core-components/wp-fast-table/state/wp-table-focus.service';

@ -31,7 +31,7 @@ import {scopedObservable} from '../../../helpers/angular-rx-utils';
import {WorkPackageResourceInterface} from '../../api/api-v3/hal-resources/work-package-resource.service';
import {States} from '../../states.service';
import {WorkPackageCacheService} from '../../work-packages/work-package-cache.service';
import {KeepTabService} from '../../wp-panels/keep-tab/keep-tab.service';
import {KeepTabService} from '../../wp-single-view-tabs/keep-tab/keep-tab.service';
import {WorkPackageTableRefreshService} from '../../wp-table/wp-table-refresh-request.service';
import {$injectFields} from '../../angular/angular-injector-bridge.functions';
import {WorkPackageEditingService} from '../../wp-edit-form/work-package-editing-service';

@ -1,12 +1,12 @@
// -- copyright
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
// Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
//
// 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) 2006-2017 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
@ -24,15 +24,20 @@
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
// ++
//++
import {wpDirectivesModule} from "../../../angular-modules";
import {Component, Directive, ElementRef, Injector, Input, Output} from '@angular/core';
import {UpgradeComponent} from '@angular/upgrade/static';
import {UserResource} from 'core-components/api/api-v3/hal-resources/user-resource.service';
function overviewPanel(){
return {
restrict: 'E',
templateUrl: '/components/wp-panels/overview-panel/overview-panel.directive.html',
};
}
@Directive({
selector: 'user-link'
})
export class UserLinkUpgradedComponent extends UpgradeComponent {
@Input() user:UserResource;
wpDirectivesModule.directive('overviewPanel', overviewPanel);
constructor(elementRef:ElementRef, injector:Injector) {
super('userLink', elementRef, injector);
}
}

@ -1,6 +1,6 @@
import {InsertMode, ViewMode} from './wp-attachments-formattable.enums';
import {WorkPackageResourceInterface} from '../../api/api-v3/hal-resources/work-package-resource.service';
import {KeepTabService} from '../../wp-panels/keep-tab/keep-tab.service';
import {KeepTabService} from '../../wp-single-view-tabs/keep-tab/keep-tab.service';
import {openprojectModule} from '../../../angular-modules';
import {WorkPackageCacheService} from '../work-package-cache.service';
import {MarkupModel} from './models/markup-model';

@ -26,20 +26,19 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {opWorkPackagesModule} from '../../../angular-modules';
import {scopedObservable} from '../../../helpers/angular-rx-utils';
import {debugLog} from '../../../helpers/debug_output';
import {WorkPackageResourceInterface} from '../../api/api-v3/hal-resources/work-package-resource.service';
import {DisplayField} from '../../wp-display/wp-display-field/wp-display-field.module';
import {WorkPackageDisplayFieldService} from '../../wp-display/wp-display-field/wp-display-field.service';
import {WorkPackageCacheService} from '../work-package-cache.service';
import {WorkPackageEditFieldGroupController} from "../../wp-edit/wp-edit-field/wp-edit-field-group.directive";
import {
WorkPackageEditingService
} from '../../wp-edit-form/work-package-editing-service';
import {WorkPackageEditingService} from '../../wp-edit-form/work-package-editing-service';
import {States} from '../../states.service';
import {CurrentProjectService} from '../../projects/current-project.service';
import {StateParams} from '@uirouter/core';
import {Component, Inject, Input, OnDestroy, OnInit} from '@angular/core';
import {PathHelperService} from 'core-components/common/path-helper/path-helper.service';
import {I18nToken} from 'core-app/angular4-transition-utils';
import {componentDestroyed} from 'ng2-rx-componentdestroyed';
import {WorkPackageEditFieldGroupDirective} from 'core-components/wp-edit/wp-edit-field/wp-edit-field-group.directive';
interface FieldDescriptor {
name:string;
@ -55,9 +54,12 @@ interface GroupDescriptor {
members:FieldDescriptor[];
}
export class WorkPackageSingleViewController {
public wpEditFieldGroup:WorkPackageEditFieldGroupController;
public workPackage:WorkPackageResourceInterface;
@Component({
template: require('!!raw-loader!./wp-single-view.html'),
selector: 'wp-single-view',
})
export class WorkPackageSingleViewComponent implements OnInit, OnDestroy {
@Input('workPackage') public workPackage:WorkPackageResourceInterface;
// Grouped fields returned from API
public groupedFields:GroupDescriptor[] = [];
@ -66,32 +68,52 @@ export class WorkPackageSingleViewController {
href:string|null,
field?:FieldDescriptor[]
};
public text:any;
public scope:any;
public text = {
attachments: {
label: this.I18n.t('js.label_attachments')
},
project: {
required: this.I18n.t('js.project.required_outside_context'),
context: this.I18n.t('js.project.context'),
switchTo: this.I18n.t('js.project.click_to_switch_context'),
},
fields: {
description: this.I18n.t('js.work_packages.properties.description'),
},
description: {
placeholder: this.I18n.t('js.work_packages.placeholders.description')
},
date: {
startDate: this.I18n.t('js.label_no_start_date'),
dueDate: this.I18n.t('js.label_no_due_date')
},
infoRow: {
createdBy: this.I18n.t('js.label_created_by'),
lastUpdatedOn: this.I18n.t('js.label_last_updated_on')
},
};
protected firstTimeFocused:boolean = false;
constructor(protected $scope:ng.IScope,
protected $rootScope:ng.IRootScopeService,
protected $stateParams:StateParams,
protected I18n:op.I18n,
constructor(@Inject(I18nToken) readonly I18n:op.I18n,
public wpEditFieldGroup:WorkPackageEditFieldGroupDirective,
protected currentProject:CurrentProjectService,
protected PathHelper:any,
protected PathHelper:PathHelperService,
protected states:States,
protected wpEditing:WorkPackageEditingService,
protected wpDisplayField:WorkPackageDisplayFieldService,
protected wpCacheService:WorkPackageCacheService) {
}
public initialize() {
// Create I18n texts
this.setupI18nTexts();
public ngOnInit() {
if (this.workPackage.attachments) {
this.workPackage.attachments.updateElements();
}
scopedObservable(this.$scope, this.wpEditing.temporaryEditResource(this.workPackage.id).values$())
this.wpEditing.temporaryEditResource(this.workPackage.id)
.values$()
.takeUntil(componentDestroyed(this))
.subscribe((resource:WorkPackageResourceInterface) => {
// Prepare the fields that are required always
const isNew = this.workPackage.isNew;
@ -126,6 +148,10 @@ export class WorkPackageSingleViewController {
});
}
ngOnDestroy() {
// Nothing to do
}
/**
* Returns whether a group should be hidden due to being empty
* (e.g., consists only of CFs and none of them are active in this project.
@ -144,6 +170,16 @@ export class WorkPackageSingleViewController {
return this.wpEditFieldGroup.inEditMode && !field.writable;
}
/**
* angular 2 doesn't support track by property any more but requires a custom function
* https://github.com/angular/angular/issues/12969
* @param index
* @param elem
*/
public trackByName(_index:number, elem:{ name: string }) {
return elem.name;
}
/*
* Returns the work package label
*/
@ -158,29 +194,6 @@ export class WorkPackageSingleViewController {
return this.I18n.t('js.project.work_package_belongs_to', { projectname: project });
}
private setupI18nTexts() {
this.text = {
project: {
required: this.I18n.t('js.project.required_outside_context'),
context: this.I18n.t('js.project.context'),
switchTo: this.I18n.t('js.project.click_to_switch_context'),
},
dropFiles: this.I18n.t('js.label_drop_files'),
dropFilesHint: this.I18n.t('js.label_drop_files_hint'),
fields: {
description: this.I18n.t('js.work_packages.properties.description'),
},
date: {
startDate: this.I18n.t('js.label_no_start_date'),
dueDate: this.I18n.t('js.label_no_due_date')
},
infoRow: {
createdBy: this.I18n.t('js.label_created_by'),
lastUpdatedOn: this.I18n.t('js.label_last_updated_on')
},
};
}
/**
* Maps the grouped fields into their display fields.
* May return multiple fields (for the date virtual field).
@ -247,29 +260,3 @@ export class WorkPackageSingleViewController {
}
}
function wpSingleViewDirective():any {
return {
restrict: 'E',
templateUrl: '/components/work-packages/wp-single-view/wp-single-view.directive.html',
scope: {
workPackage: '=?'
},
require: ['^wpEditFieldGroup'],
link: (scope:any,
element:ng.IAugmentedJQuery,
attrs:any,
controllers: [WorkPackageEditFieldGroupController]) => {
scope.$ctrl.wpEditFieldGroup = controllers[0];
scope.$ctrl.initialize();
},
bindToController: true,
controller: WorkPackageSingleViewController,
controllerAs: '$ctrl'
};
}
opWorkPackagesModule.directive('wpSingleView', wpSingleViewDirective);

@ -1,170 +0,0 @@
<div ng-if="$ctrl.workPackage" class="work-package--single-view">
<div class="wp-new--subject-wrapper" ng-if="$ctrl.workPackage.isNew">
<wp-edit-field work-package-id="$ctrl.workPackage.id"
wrapper-classes="'-no-label'"
field-name="'subject'"></wp-edit-field>
</div>
<div class="wp-info-wrapper">
<wp-status-button ng-if="!$ctrl.workPackage.isNew"
work-package="$ctrl.workPackage"
allowed="$ctrl.workPackage.isEditable"></wp-status-button>
<attribute-help-text attribute="'status'" attribute-scope="WorkPackage" ng-if="!$ctrl.workPackage.isNew"></attribute-help-text>
<div class="work-packages--info-row" ng-if="!$ctrl.workPackage.isNew">
<span ng-bind="$ctrl.idLabel"/>:
<span ng-bind="$ctrl.text.infoRow.createdBy"/>
<user-link class="user-link" user="$ctrl.workPackage.author"></user-link>.
<span ng-bind="$ctrl.text.infoRow.lastUpdatedOn"/>
<op-date-time date-time-value="$ctrl.workPackage.updatedAt"></op-date-time>.
</div>
<wp-custom-actions [work-package]="$ctrl.workPackage" class="custom-actions"></wp-custom-actions>
</div>
<div class="attributes-group -project-context" ng-if="$ctrl.projectContext.field">
<div class="attributes-group--header">
<div class="attributes-group--header-container"></div>
</div>
<div class="">
<p ng-hide="$ctrl.projectContext.href" ng-bind="::$ctrl.text.project.required"></p>
<div class="attributes-key-value"
ng-class="{'-span-all-columns': descriptor.spanAll }"
ng-repeat="descriptor in $ctrl.projectContext.field track by descriptor.name">
<div class="attributes-key-value--key"
wp-replacement-label="descriptor.name">
{{ descriptor.label }}
<span class="required" ng-if="descriptor.field.required && descriptor.field.writable"> *</span>
<attribute-help-text title-text="descriptor.label" attribute="descriptor.name" attribute-scope="WorkPackage"></attribute-help-text>
</div>
<div class="attributes-key-value--value-container">
<wp-edit-field work-package-id="$ctrl.workPackage.id" field-name="descriptor.name"></wp-edit-field>
</div>
</div>
</div>
</div>
<div class="attributes-group -project-context hide-when-print" ng-if="!$ctrl.workPackage.isNew && !$ctrl.projectContext.matches">
<div>
<p>
<span ng-bind-html="$ctrl.projectContextText"></span>
<br/>
<a ng-href="{{ $ctrl.projectContext.href }}"
class="project-context--switch-link"
ng-bind="::$ctrl.text.project.switchTo">
</a>
</p>
</div>
</div>
<div class="attributes-group description-group">
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text"
ng-bind="$ctrl.text.fields.description"></h3>
</div>
</div>
<div class="single-attribute wiki work-packages--details--description">
<wp-edit-field field-name="'description'"
work-package-id="$ctrl.workPackage.id"
wrapper-classes="'-no-label'"
display-placeholder="$ctrl.I18n.t('js.work_packages.placeholders.description')"
wp-attachments-formattable>
</wp-edit-field>
</div>
</div>
<div ng-repeat="group in $ctrl.groupedFields track by group.name"
ng-hide="$ctrl.shouldHideGroup(group)"
data-group-name="{{ group.name }}"
class="attributes-group">
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text"
ng-bind="group.name"></h3>
</div>
</div>
<div class="-columns-2">
<div class="attributes-key-value"
ng-class="{'-span-all-columns': descriptor.spanAll }"
ng-repeat="descriptor in group.members track by descriptor.name"
ng-if="!$ctrl.shouldHideField(descriptor)">
<div
class="attributes-key-value--key"
ng-if="!descriptor.multiple"
wp-replacement-label="descriptor.name">
{{ descriptor.label }}
<span class="required" ng-if="descriptor.field.required && descriptor.field.writable"> *</span>
<attribute-help-text attribute="descriptor.name" attribute-scope="WorkPackage"></attribute-help-text>
</div>
<div ng-if="!descriptor.multiple"
class="attributes-key-value--value-container">
<wp-edit-field ng-if="descriptor.field.isFormattable"
class="wp-edit-formattable-field"
work-package-id="$ctrl.workPackage.id"
field-name="descriptor.name"
wp-attachments-formattable>
</wp-edit-field>
<wp-edit-field ng-if="!descriptor.field.isFormattable"
work-package-id="$ctrl.workPackage.id"
field-name="descriptor.name">
</wp-edit-field>
</div>
<div
class="attributes-key-value--key"
ng-if="descriptor.multiple"
wp-replacement-label="descriptor.label">
{{ descriptor.label }}
<attribute-help-text attribute="descriptor.name" attribute-scope="WorkPackage"></attribute-help-text>
</div>
<div
ng-if="descriptor.multiple"
class="attributes-key-value--value-container -minimal">
<wp-edit-field field-name="descriptor.fields[0].name"
work-package-id="$ctrl.workPackage.id"
wrapper-classes="'-small -shrink'"
display-placeholder="::$ctrl.text[descriptor.name][descriptor.fields[0].name]">
</wp-edit-field>
<span class="attributes-key-value--value-separator"></span>
<wp-edit-field field-name="descriptor.fields[1].name"
work-package-id="$ctrl.workPackage.id"
wrapper-classes="'-small -shrink'"
display-placeholder="::$ctrl.text[descriptor.name][descriptor.fields[1].name]">
</wp-edit-field>
</div>
</div>
</div>
</div>
</div>
<div class="work-packages--attachments attributes-group">
<div class="work-packages--atachments-container">
<div class="attributes-group--header"
ng-if="$ctrl.workPackage.attachments.elements.length">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text">
{{ ::$ctrl.I18n.t('js.label_attachments') }}
</h3>
</div>
</div>
<wp-attachment-list work-package="$ctrl.workPackage"></wp-attachment-list>
<wp-attachments-upload work-package="$ctrl.workPackage" class="hide-when-print">
<div class="work-package--attachments--drop-box">
<div class="work-package--attachments--label">
<op-icon icon-classes="icon-attachment"></op-icon>
<p>
{{ ::$ctrl.text.dropFiles }} <br>
{{ ::$ctrl.text.dropFilesHint }}
</p>
</div>
</div>
</wp-attachments-upload>
</div>
</div>

@ -0,0 +1,173 @@
<div *ngIf="workPackage"
class="work-package--single-view">
<div class="wp-new--subject-wrapper"
*ngIf="workPackage.isNew">
<wp-edit-field [workPackageId]="workPackage.id"
[wrapperClasses]="'-no-label'"
[fieldName]="'subject'"></wp-edit-field>
</div>
<div class="wp-info-wrapper">
<wp-status-button *ngIf="!workPackage.isNew"
[workPackage]="workPackage"
[allowed]="workPackage.isEditable"></wp-status-button>
<ng1-attribute-help-text-wrapper [attribute]="'status'"
[attributeScope]="'WorkPackage'"
*ngIf="!workPackage.isNew"></ng1-attribute-help-text-wrapper>
<div class="work-packages--info-row"
*ngIf="!workPackage.isNew">
<span [textContent]="idLabel"></span>:
<span [textContent]="text.infoRow.createdBy"></span>
<user-link class="user-link"
[user]="workPackage.author"></user-link>.
<span [textContent]="text.infoRow.lastUpdatedOn"></span>
<op-date-time [dateTimeValue]="workPackage.updatedAt"></op-date-time>.
</div>
<wp-custom-actions [workPackage]="workPackage"class="custom-actions"></wp-custom-actions>
</div>
<div class="attributes-group -project-context"
*ngIf="projectContext.field">
<div class="attributes-group--header">
<div class="attributes-group--header-container"></div>
</div>
<div>
<p [hidden]="projectContext.href" [textContent]="text.project.required"></p>
<div class="attributes-key-value"
[ngClass]="{'-span-all-columns': descriptor.spanAll }"
*ngFor="let descriptor of projectContext.field; trackBy:trackByName">
<div class="attributes-key-value--key">
<wp-replacement-label [fieldName]="descriptor.name">
{{ descriptor.label }}
<span class="required"
*ngIf="descriptor.field.required && descriptor.field.writable">*</span>
<ng1-attribute-help-text-wrapper [attribute]="descriptor.name"
[attributeScope]="'WorkPackage'"></ng1-attribute-help-text-wrapper>
</wp-replacement-label>
</div>
<div class="attributes-key-value--value-container">
<wp-edit-field [workPackageId]="workPackage.id"
[fieldName]="descriptor.name"></wp-edit-field>
</div>
</div>
</div>
</div>
<div class="attributes-group -project-context hide-when-print" *ngIf="!workPackage.isNew && !projectContext.matches">
<div>
<p>
<span [innerHTML]="projectContextText"></span>
<br/>
<a [attr.href]="projectContext.href"
class="project-context--switch-link"
[textContent]="text.project.switchTo">
</a>
</p>
</div>
</div>
<div class="attributes-group description-group">
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text"
[textContent]="text.fields.description"></h3>
</div>
</div>
<div class="single-attribute wiki work-packages--details--description">
<wp-edit-field [fieldName]="'description'"
[workPackageId]="workPackage.id"
[wrapperClasses]="'-no-label'"
[displayPlaceholder]="text.description.placeholder"
wp-attachments-formattable>
</wp-edit-field>
</div>
</div>
<div *ngFor="let group of groupedFields; trackBy:trackByName"
[hidden]="shouldHideGroup(group)"
[attr.data-group-name]="group.name"
class="attributes-group">
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text"
[textContent]="group.name"></h3>
</div>
</div>
<div class="-columns-2">
<div class="attributes-key-value"
[ngClass]="{'-span-all-columns': descriptor.spanAll }"
*ngFor="let descriptor of group.members; trackBy:trackByName">
<ng-template [ngIf]="!shouldHideField(descriptor)">
<div
class="attributes-key-value--key"
*ngIf="!descriptor.multiple">
<wp-replacement-label [fieldName]="descriptor.name">
{{ descriptor.label }}
<span class="required"
*ngIf="descriptor.field.required && descriptor.field.writable"> *</span>
<ng1-attribute-help-text-wrapper attribute="descriptor.name" attribute-scope="WorkPackage"></ng1-attribute-help-text-wrapper>
</wp-replacement-label>
</div>
<div *ngIf="!descriptor.multiple"
class="attributes-key-value--value-container">
<wp-edit-field *ngIf="descriptor.field.isFormattable"
class="wp-edit-formattable-field"
[workPackageId]="workPackage.id"
[fieldName]="descriptor.name"
wp-attachments-formattable>
</wp-edit-field>
<wp-edit-field *ngIf="!descriptor.field.isFormattable"
[workPackageId]="workPackage.id"
[fieldName]="descriptor.name">
</wp-edit-field>
</div>
<div
class="attributes-key-value--key"
*ngIf="descriptor.multiple">
<wp-replacement-label [fieldName]="descriptor.name">
{{ descriptor.label }}
<ng1-attribute-help-text-wrapper attribute="descriptor.name" attribute-scope="WorkPackage"></ng1-attribute-help-text-wrapper>
</wp-replacement-label>
</div>
<div
*ngIf="descriptor.multiple"
class="attributes-key-value--value-container -minimal">
<wp-edit-field [fieldName]="descriptor.fields[0].name"
[workPackageId]="workPackage.id"
[wrapperClasses]="'-small -shrink'"
[displayPlaceholder]="text[descriptor.name][descriptor.fields[0].name]">
</wp-edit-field>
<span class="attributes-key-value--value-separator"></span>
<wp-edit-field [fieldName]="descriptor.fields[1].name"
[workPackageId]="workPackage.id"
[wrapperClasses]="'-small -shrink'"
[displayPlaceholder]="text[descriptor.name][descriptor.fields[1].name]">
</wp-edit-field>
</div>
</ng-template>
</div>
</div>
</div>
</div>
<div class="work-packages--attachments attributes-group">
<div class="work-packages--atachments-container">
<div class="attributes-group--header"
*ngIf="workPackage.attachments">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text" [textContent]="text.attachments.label"></h3>
</div>
</div>
<wp-attachment-list [workPackage]="workPackage"></wp-attachment-list>
<ng1-wp-attachments-upload-wrapper [workPackage]="workPackage" class="hide-when-print">
</ng1-wp-attachments-upload-wrapper>
</div>
</div>

@ -26,44 +26,37 @@
// See doc/COPYRIGHT.rdoc for more details.
//++
import {wpDirectivesModule} from '../../../angular-modules';
import {WorkPackageResourceInterface} from '../../api/api-v3/hal-resources/work-package-resource.service';
import {UploadFile} from '../../api/op-file-upload/op-file-upload.service';
import {HalResource} from '../../api/api-v3/hal-resources/hal-resource.service';
import {WorkPackageNotificationService} from '../../wp-edit/wp-notification.service';
export class WorkPackageAttachmentListController {
public workPackage: WorkPackageResourceInterface;
public text: any = {};
public itemTemplateUrl =
'/components/wp-attachments/wp-attachment-list/wp-attachment-list-item.html';
constructor(protected wpNotificationsService:WorkPackageNotificationService, I18n:op.I18n) {
this.text = {
destroyConfirmation: I18n.t('js.text_attachment_destroy_confirmation'),
removeFile: (arg:any) => I18n.t('js.label_remove_file', arg)
};
if (this.workPackage.attachments) {
this.workPackage.attachments.updateElements();
}
import {Component, Inject, Input} from '@angular/core';
import {I18nToken} from 'core-app/angular4-transition-utils';
import {HalResource} from 'core-components/api/api-v3/hal-resources/hal-resource.service';
@Component({
template: require('!!raw-loader!./wp-attachment-list-item.html'),
selector: 'wp-attachment-list-item',
})
export class WorkPackageAttachmentListItemComponent {
@Input('workPackage') public workPackage:WorkPackageResourceInterface;
@Input('attachment') public attachment:HalResource;
public text = {
destroyConfirmation: this.I18n.t('js.text_attachment_destroy_confirmation'),
removeFile: (arg:any) => this.I18n.t('js.label_remove_file', arg)
}
}
function wpAttachmentListDirective():any {
return {
restrict: 'E',
templateUrl: '/components/wp-attachments/wp-attachment-list/wp-attachment-list.directive.html',
constructor(protected wpNotificationsService:WorkPackageNotificationService,
@Inject(I18nToken) readonly I18n:op.I18n) {
}
scope: {
workPackage: '='
},
public confirmRemoveAttachment($event:JQueryEventObject) {
if (!window.confirm(this.text.destroyConfirmation)) {
$event.stopImmediatePropagation();
$event.preventDefault();
return false;
}
controller: WorkPackageAttachmentListController,
controllerAs: '$ctrl',
bindToController: true
};
this.workPackage.removeAttachment(this.attachment);
return false;
}
}
wpDirectivesModule.directive('wpAttachmentList', wpAttachmentListDirective);

@ -1,29 +1,30 @@
<span class="inplace-editing--trigger-container">
<span class="inplace-editing--trigger-link"
focus-within="'.inplace-edit--icon-wrapper'">
focus-within
focusWithinSelector=".inplace-edit--icon-wrapper">
<span class="inplace-editing--container">
<span class="inplace-edit--read-value">
<op-icon icon-classes="icon-context icon-attachment"></op-icon>
<a
class="work-package--attachments--filename"
ng-href="{{ ::attachment.downloadLocation.href || '#' }}"
[attr.href]="attachment.downloadLocation.href || '#'"
download>
{{ ::attachment.fileName || attachment.customName || attachment.name }}
{{ attachment.fileName || attachment.customName || attachment.name }}
<authoring class="work-package--attachments--info"
created-on="attachment.createdAt"
author="attachment.author"
show-author-as-link="false"
ng-if="!$ctrl.workPackage.isNew"></authoring>
[createdOn]="attachment.createdAt"
[author]="attachment.author"
[showAuthorAsLink]="false"
*ngIf="!workPackage.isNew"></authoring>
</a>
</span>
<a
href=""
class="inplace-edit--icon-wrapper work-package--atachments--delete-button"
ng-if="!!attachment.$links.delete || $ctrl.workPackage.isNew"
ng-click="$ctrl.workPackage.removeAttachment(attachment)"
confirm-popup="{{ ::$ctrl.text.destroyConfirmation }}">
<op-icon icon-classes="icon-delete" icon-title="{{ ::$ctrl.text.removeFile({fileName: attachment.fileName}) }}"></op-icon>
*ngIf="!!attachment.$links.delete || workPackage.isNew"
(click)="confirmRemoveAttachment($event)">
<op-icon icon-classes="icon-delete"
[icon-title]="text.removeFile({fileName: attachment.fileName})"></op-icon>
</a>
</span>
</span>

@ -1,4 +1,4 @@
// -- copyright
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
@ -24,24 +24,22 @@
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
// ++
//++
import {wpButtonsModule} from '../../../angular-modules';
import {WorkPackageResourceInterface} from '../../api/api-v3/hal-resources/work-package-resource.service';
import {Component, Input, OnInit} from '@angular/core';
import {HalResource} from 'core-components/api/api-v3/hal-resources/hal-resource.service';
function wpCreateButton() {
return {
restrict: 'E',
templateUrl: '/components/wp-buttons/wp-status-button/wp-status-button.directive.html',
@Component({
template: require('!!raw-loader!./wp-attachment-list.html'),
selector: 'wp-attachment-list',
})
export class WorkPackageAttachmentListComponent implements OnInit {
@Input('workPackage') public workPackage:WorkPackageResourceInterface;
scope: {
allowed: '=',
workPackage: '='
},
bindToController: true,
controllerAs: '$ctrl',
controller: 'WorkPackageStatusButtonController'
ngOnInit() {
if (this.workPackage.attachments) {
this.workPackage.attachments.updateElements();
}
}
}
wpButtonsModule.directive('wpStatusButton', wpCreateButton);

@ -1,16 +0,0 @@
<div class="work-package--attachments--files">
<div class="work-package--details--long-field">
<span
class="inplace-edit--read"
ng-include="::$ctrl.itemTemplateUrl"
ng-if="$ctrl.workPackage.pendingAttachments"
ng-repeat="attachment in $ctrl.workPackage.pendingAttachments track by attachment.name+$index">
</span>
<span
class="inplace-edit--read"
ng-include="::$ctrl.itemTemplateUrl"
ng-if="$ctrl.workPackage.attachments.elements"
ng-repeat="attachment in $ctrl.workPackage.attachments.elements track by attachment.name+$index">
</span>
</div>
</div>

@ -0,0 +1,14 @@
<div class="work-package--attachments--files">
<div class="work-package--details--long-field">
<span
class="inplace-edit--read"
*ngFor="let attachment of workPackage.pendingAttachments">
<wp-attachment-list-item [attachment]="attachment" [workPackage]="workPackage"></wp-attachment-list-item>
</span>
<span
class="inplace-edit--read"
*ngFor="let attachment of workPackage.attachments.elements">
<wp-attachment-list-item [attachment]="attachment" [workPackage]="workPackage"></wp-attachment-list-item>
</span>
</div>
</div>

@ -0,0 +1,56 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
// 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 doc/COPYRIGHT.rdoc for more details.
//++
// This Angular directive will act as an interface to the "upgraded" AngularJS component
import {
Directive, DoCheck, ElementRef, Inject, Injector, Input, OnChanges, OnDestroy,
OnInit, SimpleChanges
} from '@angular/core';
import {UpgradeComponent} from '@angular/upgrade/static';
import {WorkPackageResourceInterface} from 'core-components/api/api-v3/hal-resources/work-package-resource.service';
@Directive({selector: 'ng1-wp-attachments-upload-wrapper'})
export class Ng1WorkPackageAttachmentsUploadWrapper extends UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
@Input('workPackage') workPackage:WorkPackageResourceInterface;
constructor(@Inject(ElementRef) elementRef:ElementRef, @Inject(Injector) injector:Injector) {
// We must pass the name of the directive as used by AngularJS to the super
super('wpAttachmentsUpload', elementRef, injector);
}
// For this class to work when compiled with AoT, we must implement these lifecycle hooks
// because the AoT compiler will not realise that the super class implements them
ngOnInit() { super.ngOnInit(); }
ngOnChanges(changes:SimpleChanges) { super.ngOnChanges(changes); }
ngDoCheck() { super.ngDoCheck(); }
ngOnDestroy() { super.ngOnDestroy(); }
}

@ -38,7 +38,9 @@ export class WorkPackageUploadDirectiveController {
constructor(protected $q:ng.IQService, ConfigurationService:any, protected I18n:op.I18n) {
this.text = {
uploadLabel: I18n.t('js.label_add_attachments')
uploadLabel: I18n.t('js.label_add_attachments'),
dropFiles: I18n.t('js.label_drop_files'),
dropFilesHint: I18n.t('js.label_drop_files_hint')
};
ConfigurationService.api().then((settings:any) => {
this.maxFileSize = settings.maximumAttachmentFileSize;
@ -66,22 +68,7 @@ function wpUploadDirective():any {
return {
restrict: 'AE',
template: `
<div
class="wp-attachment-upload hide-when-print"
ng-if="$ctrl.workPackage.canAddAttachments"
ngf-drop
ngf-select
ngf-change="$ctrl.uploadFiles($files)"
ngf-multiple="true"
ngf-validate="{ size: {max: ::$ctrl.maxFileSize} }"
tabindex="0"
aria-label="{{ ::$ctrl.text.uploadLabel }}"
click-on-keypress="[13, 32]"
ng-transclude>
</div>`,
transclude: true,
templateUrl: '/components/wp-attachments/wp-attachments-upload/wp-attachments-upload.html',
scope: {
workPackage: '='

@ -0,0 +1,21 @@
<div
class="wp-attachment-upload hide-when-print"
ng-if="$ctrl.workPackage.canAddAttachments"
ngf-drop
ngf-select
ngf-change="$ctrl.uploadFiles($files)"
ngf-multiple="true"
ngf-validate="{ size: {max: ::$ctrl.maxFileSize} }"
tabindex="0"
aria-label="{{ ::$ctrl.text.uploadLabel }}"
click-on-keypress="[13, 32]">
<div class="work-package--attachments--drop-box">
<div class="work-package--attachments--label">
<op-icon icon-classes="icon-attachment"></op-icon>
<p>
{{ ::text.dropFiles }} <br>
{{ ::text.dropFilesHint }}
</p>
</div>
</div>
</div>

@ -27,7 +27,7 @@
// ++
import {wpButtonsModule} from '../../../angular-modules';
import {KeepTabService} from '../../wp-panels/keep-tab/keep-tab.service';
import {KeepTabService} from '../../wp-single-view-tabs/keep-tab/keep-tab.service';
import {States} from '../../states.service';
import {WorkPackageTableFocusService} from 'core-components/wp-fast-table/state/wp-table-focus.service';
import {StateService} from '@uirouter/core';

@ -26,24 +26,25 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {wpButtonsModule} from '../../../angular-modules';
import {WorkPackageEditingService} from '../../wp-edit-form/work-package-editing-service';
import {WorkPackageResourceInterface} from '../../api/api-v3/hal-resources/work-package-resource.service';
import {WorkPackageResourceInterface} from 'core-components/api/api-v3/hal-resources/work-package-resource.service';
import {WorkPackageEditingService} from 'core-components/wp-edit-form/work-package-editing-service';
import {Component, Inject, Input} from '@angular/core';
import {I18nToken} from 'core-app/angular4-transition-utils';
export default class WorkPackageStatusButtonController {
public workPackage:WorkPackageResourceInterface;
public text:any;
public allowed:boolean;
@Component({
template: require('!!raw-loader!./wp-status-button.html'),
selector: 'wp-status-button',
})
export class WorkPackageStatusButtonComponent {
@Input('workPackage') public workPackage:WorkPackageResourceInterface;
@Input('allowed') public allowed:boolean;
constructor(protected I18n:op.I18n,
protected wpEditing:WorkPackageEditingService) {
this.text = {
explanation: I18n.t('js.label_edit_status')
};
}
public text = {
explanation: this.I18n.t('js.label_edit_status')
};
public $onInit() {
// Created for interface compliance
constructor(@Inject(I18nToken) readonly I18n:op.I18n,
protected wpEditing:WorkPackageEditingService) {
}
public isDisabled() {
@ -56,5 +57,3 @@ export default class WorkPackageStatusButtonController {
return changeset.value('status');
}
}
wpButtonsModule.controller('WorkPackageStatusButtonController', WorkPackageStatusButtonController);

@ -1,14 +0,0 @@
<div class="wp-status-button">
<button class="button"
has-dropdown-menu
target="WpStatusContextMenu"
ng-init="workPackage = $ctrl.workPackage"
locals="workPackage"
ng-disabled="$ctrl.isDisabled()"
aria-label="{{ ::$ctrl.text.explanation }}">
<span class="button--text"
aria-hidden="true">{{$ctrl.getStatus.name}}</span>
<op-icon icon-classes="button--icon icon-small icon-pulldown"></op-icon>
</button>
</div>

@ -0,0 +1,15 @@
<div class="wp-status-button">
<button class="button"
[disabled]="isDisabled()"
[attr.aria-label]="text.explanation"
hasDropdownMenu
[hasDropdownMenu-locals]="{'workPackage': workPackage }"
[hasDropdownMenu-target]="'WpStatusContextMenu'">
<span class="button--text"
aria-hidden="true"
[textContent]="getStatus.name"></span>
<op-icon icon-classes="button--icon icon-small icon-pulldown"></op-icon>
</button>
</div>

@ -26,13 +26,24 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {WorkPackageEditFieldGroupController} from './wp-edit-field-group.directive';
export class WorkPackageReplacementLabelController {
public wpEditFieldGroup:WorkPackageEditFieldGroupController;
public fieldName:string;
constructor(protected $scope:ng.IScope,
protected $element:ng.IAugmentedJQuery) {
import {WorkPackageEditFieldGroupDirective} from 'core-components/wp-edit/wp-edit-field/wp-edit-field-group.directive';
import {Component, ElementRef, Input, OnInit} from '@angular/core';
@Component({
template: require('!!raw-loader!./wp-replacement-label.html'),
selector: 'wp-replacement-label',
})
export class WorkPackageReplacementLabelComponent implements OnInit {
@Input('fieldName') public fieldName:string;
private $element:ng.IAugmentedJQuery;
constructor(protected wpEditFieldGroup:WorkPackageEditFieldGroupDirective,
protected elementRef:ElementRef) {
}
ngOnInit() {
this.$element = angular.element(this.elementRef.nativeElement);
}
public activate(evt:JQueryEventObject) {
@ -42,38 +53,9 @@ export class WorkPackageReplacementLabelController {
return true;
}
this.wpEditFieldGroup.fields[this.fieldName].handleUserActivate(null);
const field = this.wpEditFieldGroup.fields[this.fieldName];
field && field.handleUserActivate(null);
return false;
}
}
function wpReplacementLabelLink(scope:ng.IScope,
element:ng.IAugmentedJQuery,
attrs:ng.IAttributes,
controllers:[WorkPackageEditFieldGroupController, WorkPackageReplacementLabelController]) {
controllers[1].wpEditFieldGroup = controllers[0];
}
function wpReplacementLabel():any {
return {
restrict: 'A',
templateUrl: '/components/wp-edit/wp-edit-field/wp-replacement-label.directive.html',
transclude: true,
scope: {
fieldName: '=wpReplacementLabel',
},
require: ['^wpEditFieldGroup', 'wpReplacementLabel'],
link: wpReplacementLabelLink,
controller: WorkPackageReplacementLabelController,
controllerAs: 'vm',
bindToController: true
};
}
angular
.module('openproject')
.directive('wpReplacementLabel', wpReplacementLabel);

@ -1,4 +0,0 @@
<ng-transclude
ng-click="vm.activate($event)"
tabindex="-1">
</ng-transclude>

@ -0,0 +1,4 @@
<ng-content
(click)="activate($event)"
tabindex="-1">
</ng-content>

@ -1,6 +1,6 @@
import {Injector} from '@angular/core';
import {$stateToken, I18nToken} from 'core-app/angular4-transition-utils';
import {KeepTabService} from 'core-components/wp-panels/keep-tab/keep-tab.service';
import {KeepTabService} from 'core-components/wp-single-view-tabs/keep-tab/keep-tab.service';
import {opIconElement} from '../../../helpers/op-icon-builder';
import {WorkPackageResourceInterface} from '../../api/api-v3/hal-resources/work-package-resource.service';
import {wpCellTdClassName} from './cell-builder';

@ -1,4 +1,4 @@
import {KeepTabService} from 'core-components/wp-panels/keep-tab/keep-tab.service';
import {KeepTabService} from 'core-components/wp-single-view-tabs/keep-tab/keep-tab.service';
import {StateService} from '@uirouter/core';
export const uiStateLinkClass = '__ui-state-link';

@ -4,7 +4,7 @@ import {$stateToken} from 'core-app/angular4-transition-utils';
import {WorkPackageTableFocusService} from 'core-components/wp-fast-table/state/wp-table-focus.service';
import {debugLog} from '../../../../helpers/debug_output';
import {States} from '../../../states.service';
import {KeepTabService} from '../../../wp-panels/keep-tab/keep-tab.service';
import {KeepTabService} from '../../../wp-single-view-tabs/keep-tab/keep-tab.service';
import {tdClassName} from '../../builders/cell-builder';
import {tableRowClassName} from '../../builders/rows/single-row-builder';
import {WorkPackageTableSelection} from '../../state/wp-table-selection.service';

@ -3,7 +3,7 @@ import {$stateToken} from 'core-app/angular4-transition-utils';
import {WorkPackageTableFocusService} from 'core-components/wp-fast-table/state/wp-table-focus.service';
import {WorkPackageResource} from '../../../api/api-v3/hal-resources/work-package-resource.service';
import {States} from '../../../states.service';
import {KeepTabService} from '../../../wp-panels/keep-tab/keep-tab.service';
import {KeepTabService} from '../../../wp-single-view-tabs/keep-tab/keep-tab.service';
import {tableRowClassName} from '../../builders/rows/single-row-builder';
import {uiStateLinkClass} from '../../builders/ui-state-link-builder';
import {WorkPackageTableSelection} from '../../state/wp-table-selection.service';

@ -1,11 +0,0 @@
<wp-single-view work-package="$ctrl.workPackage" ng-if="$ctrl.workPackage"></wp-single-view>
<div class="attributes-group" ng-if="$ctrl.workPackage">
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text">{{ I18n.t('js.label_latest_activity') }}</h3>
</div>
</div>
<activity-panel template="overview" work-package="$ctrl.workPackage"></activity-panel>
</div>

@ -0,0 +1,58 @@
// -- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
// 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 doc/COPYRIGHT.rdoc for more details.
// ++
import {Component, Inject, Input, OnDestroy} from '@angular/core';
import {Transition} from '@uirouter/core';
import {I18nToken} from '../../../angular4-transition-utils';
import {WorkPackageCacheService} from 'core-components/work-packages/work-package-cache.service';
import {WorkPackageResourceInterface} from 'core-components/api/api-v3/hal-resources/work-package-resource.service';
import {componentDestroyed} from 'ng2-rx-componentdestroyed';
@Component({
template: require('!!raw-loader!./overview-tab.html'),
selector: 'wp-overview-tab',
})
export class WorkPackageOverviewTabComponent implements OnDestroy {
public workPackageId:string;
public workPackage:WorkPackageResourceInterface;
public tabName = this.I18n.t('js.label_latest_activity');
public constructor(@Inject(I18nToken) readonly I18n:op.I18n,
readonly $transition:Transition,
readonly wpCacheService:WorkPackageCacheService) {
this.workPackageId = this.$transition.params('to').workPackageId;
wpCacheService.loadWorkPackage(this.workPackageId)
.values$()
.takeUntil(componentDestroyed(this))
.subscribe((wp) => this.workPackage = wp);
}
ngOnDestroy() {
// Nothing to do
}
}

@ -0,0 +1,13 @@
<wp-single-view [workPackage]="workPackage"
*ngIf="workPackage"></wp-single-view>
<div class="attributes-group" *ngIf="workPackage">
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text" [textContent]="tabName"></h3>
</div>
</div>
<!-- TODO -->
<!-- <activity-panel template="overview" work-package="workPackage"></activity-panel> -->
</div>

@ -26,7 +26,7 @@
// See doc/COPYRIGHT.rdoc for more details.
//++
import {WatchersPanelController} from '../wp-panels/watchers-panel/watchers-panel.controller';
import {WatchersPanelController} from '../wp-single-view-tabs/watchers-panel/watchers-panel.controller';
import {UserResource} from '../api/api-v3/hal-resources/user-resource.service';
import {wpDirectivesModule} from '../../angular-modules';

@ -1 +0,0 @@
<span ng-bind-html="I18n.t('js.label_added_time_by', { author: authorLink, age: timestamp })"></span>

@ -1,67 +0,0 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
//
// 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-2017 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 docs/COPYRIGHT.rdoc for more details.
//++
// TODO move to UI components
module.exports = function(I18n, PathHelper, TimezoneService) {
return {
restrict: 'E',
replace: true,
scope: { createdOn: '=', author: '=', showAuthorAsLink: '=', project: '=', activity: '=' },
templateUrl: '/templates/components/authoring.html',
link: function(scope, element, attrs) {
moment.locale(I18n.locale);
var createdOn = TimezoneService.parseDatetime(scope.createdOn);
var timeago = createdOn.fromNow();
var time = createdOn.format('LLL');
function activityFromPath(project, from) {
var path = PathHelper.projectActivityPath(project);
if (from) {
path += '?from=' + from;
}
return path;
}
scope.I18n = I18n;
if(scope.showAuthorAsLink === false) {
scope.authorLink = '<span>' + scope.author.name + '</span>';
} else {
scope.authorLink = '<a href="'+ PathHelper.userPath(scope.author.id) + '">' + scope.author.name + '</a>';
}
if (scope.activity) {
scope.timestamp = '<a title="' + time + '" href="' + activityFromPath(scope.project, createdOn.format('YYYY-MM-DD')) + '">' + timeago + '</a>';
} else {
scope.timestamp = '<span class="timestamp" title="' + time + '">' + timeago + '</span>';
}
}
};
};

@ -31,8 +31,6 @@ angular.module('openproject.uiComponents')
'./accessible-by-keyboard-directive')])
.directive('accessibleCheckbox', [require('./accessible-checkbox-directive')])
.directive('accessibleElement', [require('./accessible-element-directive')])
.directive('authoring', ['I18n', 'PathHelper', 'TimezoneService', require(
'./authoring-directive')])
.directive('copyToClipboard', [
'I18n',
'$timeout',
@ -42,7 +40,6 @@ angular.module('openproject.uiComponents')
])
.directive('opDate', ['TimezoneService', require('./date/date-directive')])
.directive('opTime', ['TimezoneService', require('./date/time-directive')])
.directive('opDateTime', ['$compile', 'TimezoneService', require('./date/date-time-directive')])
.directive('emptyElement', [require('./empty-element-directive')])
.constant('ENTER_KEY', 13)
.directive('expandableSearch', ['ENTER_KEY', require('./expandable-search')])

Loading…
Cancel
Save