Merge pull request #10658 from opf/implementation/42158-add-direct-action-hover-menu

[#42158] Add direct action hover menu
pull/10800/head
Eric Schubert 2 years ago committed by GitHub
commit 213b025367
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      config/locales/js-en.yml
  2. 19
      frontend/src/app/core/state/attachments/attachments.service.ts
  3. 21
      frontend/src/app/core/state/collection-store.ts
  4. 2
      frontend/src/app/core/state/file-links/file-link.model.ts
  5. 21
      frontend/src/app/core/state/file-links/file-links.service.ts
  6. 26
      frontend/src/app/features/work-packages/components/wp-single-view-tabs/files-tab/op-files-tab.html
  7. 29
      frontend/src/app/shared/components/file-links/file-link-icons/file-link-list-item-icon.factory.ts
  8. 102
      frontend/src/app/shared/components/file-links/file-link-icons/icon-mappings.ts
  9. 59
      frontend/src/app/shared/components/file-links/file-link-list/file-link-list-item.component.ts
  10. 62
      frontend/src/app/shared/components/file-links/file-link-list/file-link-list-item.html
  11. 69
      frontend/src/app/shared/components/file-links/file-link-list/file-link-list.component.ts
  12. 45
      frontend/src/app/shared/components/file-links/file-link-list/file-link-list.html
  13. 43
      frontend/src/app/shared/components/file-links/file-link-list/storage-information.html
  14. 3
      frontend/src/app/spot/icon-font/src/add-link.svg
  15. 3
      frontend/src/app/spot/icon-font/src/nextcloud.svg
  16. 3
      frontend/src/app/spot/icon-font/src/remove-link.svg
  17. 1125
      frontend/src/app/spot/styles/sass/common/icon.sass
  18. 555
      frontend/src/assets/fonts/openproject_icon/openproject-icon-font.svg
  19. BIN
      frontend/src/assets/fonts/openproject_icon/openproject-icon-font.ttf
  20. BIN
      frontend/src/assets/fonts/openproject_icon/openproject-icon-font.woff
  21. BIN
      frontend/src/assets/fonts/openproject_icon/openproject-icon-font.woff2
  22. 124
      frontend/src/global_styles/content/work_packages/tabs/_files.sass
  23. 25
      frontend/src/global_styles/content/work_packages/tabs/_tab_content.sass
  24. 1113
      frontend/src/global_styles/fonts/_openproject_icon_definitions.sass
  25. 3
      frontend/src/global_styles/fonts/_openproject_icon_font.lsg
  26. 2
      modules/storages/spec/features/show_file_links_spec.rb

@ -421,6 +421,7 @@ en:
label_learn_more_link: "Learn more"
label_less_or_equal: "<="
label_less_than_ago: "less than days ago"
label_link_files_in_storage: "Link files in %{storageType}"
label_loading: "Loading..."
label_login_to_storage: "Login to %{storageType}"
label_mail_notification: "Email notifications"
@ -429,7 +430,6 @@ en:
label_meeting_minutes: "Minutes"
label_menu_collapse: "collapse"
label_menu_expand: "expand"
label_modified_at: "Modified: %{date}"
label_more_than_ago: "more than days ago"
label_next: "Next"
label_no_color: "No color"
@ -438,8 +438,7 @@ en:
label_no_start_date: "no start date"
label_no_storage_connection: "No %{storageType} connection"
label_no_value: "No value"
label_no_file_links_header: "Link files in %{storageType}"
label_no_file_links_content: "In order to link files to this work package please do it via %{storageType}."
label_no_file_links: "In order to link files to this work package please do it via %{storageType}."
label_none: "none"
label_not_contains: "doesn't contain"
label_not_equals: "is not"
@ -448,6 +447,8 @@ en:
label_open_context_menu: "Open context menu"
label_open_storage: "Open %{storageType}"
label_open_work_packages: "open"
label_open_file_link: 'Open file on storage'
label_open_file_link_location: 'Open file in location'
label_password: "Password"
label_previous: "Previous"
label_per_page: "Per page:"
@ -460,6 +461,7 @@ en:
label_remove: "Remove"
label_remove_column: "Remove column"
label_remove_columns: "Remove selected columns"
label_remove_file_link: "Remove file link"
label_remove_row: "Remove row"
label_report: "Report"
label_repository_plural: "Repositories"

@ -56,7 +56,7 @@ import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service';
import { HalLink } from 'core-app/features/hal/hal-link/hal-link';
import isNewResource, { HAL_NEW_RESOURCE_ID } from 'core-app/features/hal/helpers/is-new-resource';
import { ConfigurationService } from 'core-app/core/config/configuration.service';
import { insertCollectionIntoState } from 'core-app/core/state/collection-store';
import { insertCollectionIntoState, removeEntityFromCollectionAndState } from 'core-app/core/state/collection-store';
import {
CollectionStore,
ResourceCollectionService,
@ -119,22 +119,7 @@ export class AttachmentsResourceService extends ResourceCollectionService<IAttac
return this.http
.delete<void>(attachment._links.delete.href, { withCredentials: true, headers })
.pipe(
tap(() => {
applyTransaction(() => {
this.store.remove(attachment.id);
this.store.update(({ collections }) => (
{
collections: {
...collections,
[collectionKey]: {
...collections[collectionKey],
ids: (collections[collectionKey]?.ids || []).filter((id) => id !== attachment.id),
},
},
}
));
});
}),
tap(() => removeEntityFromCollectionAndState(this.store, attachment.id, collectionKey)),
catchError((error) => {
this.toastService.addError(error);
throw error;

@ -85,6 +85,27 @@ export function insertCollectionIntoState<T extends { id:ID }>(
});
}
export function removeEntityFromCollectionAndState<T extends { id:ID }>(
store:EntityStore<CollectionState<T>>,
entityId:ID,
collectionUrl:string,
):void {
applyTransaction(() => {
store.remove(entityId);
store.update(({ collections }) => (
{
collections: {
...collections,
[collectionUrl]: {
...collections[collectionUrl],
ids: (collections[collectionUrl]?.ids || []).filter((id) => id !== entityId),
},
},
}
));
});
}
export function collectionFrom<T>(elements:T[]):IHALCollection<T> {
const count = elements.length;

@ -44,7 +44,7 @@ export interface IFileLinkHalResourceLinks extends IHalResourceLinks {
staticOriginOpenLocation:IHalResourceLink;
}
interface IFileLinkOriginData {
export interface IFileLinkOriginData {
name:string;
mimeType?:string;
size?:number;

@ -27,7 +27,7 @@
//++
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { from } from 'rxjs';
import {
catchError,
@ -41,7 +41,7 @@ import { IFileLink } from 'core-app/core/state/file-links/file-link.model';
import { IHALCollection } from 'core-app/core/apiv3/types/hal-collection.type';
import { ToastService } from 'core-app/shared/components/toaster/toast.service';
import { FileLinksStore } from 'core-app/core/state/file-links/file-links.store';
import { insertCollectionIntoState } from 'core-app/core/state/collection-store';
import { insertCollectionIntoState, removeEntityFromCollectionAndState } from 'core-app/core/state/collection-store';
import idFromLink from 'core-app/features/hal/helpers/id-from-link';
import {
CollectionStore,
@ -90,4 +90,21 @@ export class FileLinksResourceService extends ResourceCollectionService<IFileLin
protected createStore():CollectionStore<IFileLink> {
return new FileLinksStore();
}
remove(collectionKey:string, fileLink:IFileLink):void {
if (!fileLink._links.delete) {
return;
}
const headers = new HttpHeaders({ 'Content-Type': 'application/json' });
this.http
.delete<void>(fileLink._links.delete.href, { withCredentials: true, headers })
.pipe(
catchError((error) => {
this.toastService.addError(error);
throw error;
}),
)
.subscribe(() => removeEntityFromCollectionAndState(this.store, fileLink.id, collectionKey));
}
}

@ -1,7 +1,15 @@
<div class="op-tab-content--container">
<section data-qa-selector="op-tab-content--tab-section">
<div *ngIf="(canViewFileLinks$ | async) === true" class="op-tab-content--header">
<h3 class="op-tab-content--header-text" [textContent]="text.attachments.label"></h3>
<div
class="op-tab-content--container op-files-tab"
>
<section
class="op-tab-content--tab-section"
data-qa-selector="op-tab-content--tab-section"
>
<div
*ngIf="canViewFileLinks$ | async"
class="op-tab-content--header"
>
<span class="op-tab-content--header-text" [textContent]="text.attachments.label"></span>
</div>
<op-attachment-list [resource]="workPackage"></op-attachment-list>
@ -9,10 +17,14 @@
<op-attachments-upload [resource]="workPackage"></op-attachments-upload>
</section>
<section *ngFor="let storage of storages$ | async" data-qa-selector="op-tab-content--tab-section">
<section
*ngFor="let storage of storages$ | async"
data-qa-selector="op-tab-content--tab-section"
class="op-tab-content--tab-section"
>
<div class=op-tab-content--header>
<op-icon icon-classes="icon3 icon-nextcloud-circle op-files-tab--color-blue-deep"></op-icon>
<h3 class="op-tab-content--header-text" [textContent]="storage.name"></h3>
<span class="spot-icon spot-icon_nextcloud-circle"></span>
<span class="op-tab-content--header-text" [textContent]="storage.name"></span>
</div>
<op-file-link-list

@ -26,30 +26,15 @@
// See COPYRIGHT and LICENSE files for more details.
//++
export interface IFileLinkListItemIcon {
icon:'image1'|'movie'|'file-text'|'export-pdf-descr'|'file-doc'|'file-sheet'|'file-presentation'|'folder'|'ticket'
color:'red'|'blue'|'blue-deep'|'blue-dark'|'turquoise'|'green'|'grey-dark'|'grey'|'orange'
}
const mimeTypeIconMap:{ [mimeType:string]:IFileLinkListItemIcon; } = {
'image/*': { icon: 'image1', color: 'blue-dark' },
'text/plain': { icon: 'file-text', color: 'grey-dark' },
'application/pdf': { icon: 'export-pdf-descr', color: 'red' },
'application/vnd.oasis.opendocument.text': { icon: 'file-doc', color: 'blue-deep' },
'application/vnd.oasis.opendocument.spreadsheet': { icon: 'file-sheet', color: 'green' },
'application/vnd.oasis.opendocument.presentation': { icon: 'file-presentation', color: 'turquoise' },
'application/x-op-directory': { icon: 'folder', color: 'blue' },
default: { icon: 'ticket', color: 'grey-dark' },
};
import {
fileIconMappings,
IFileLinkListItemIcon,
} from 'core-app/shared/components/file-links/file-link-icons/icon-mappings';
export function getIconForMimeType(mimeType?:string):IFileLinkListItemIcon {
if (mimeType?.startsWith('image/')) {
return mimeTypeIconMap['image/*'];
}
if (mimeType && mimeTypeIconMap[mimeType]) {
return mimeTypeIconMap[mimeType];
if (mimeType && fileIconMappings[mimeType]) {
return fileIconMappings[mimeType];
}
return mimeTypeIconMap.default;
return fileIconMappings.default;
}

@ -0,0 +1,102 @@
// -- copyright
// OpenProject is an open source project management software.
// Copyright (C) 2012-2022 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.
//++
export interface IFileLinkListItemIcon {
icon:'image1'|'movie'|'file-text'|'export-pdf-descr'|'file-doc'|'file-sheet'|'file-presentation'|'folder'|'ticket'
clazz:'pdf'|'img'|'txt'|'doc'|'sheet'|'presentation'|'form'|'dir'|'mov'|'default'
}
export const fileIconMappings:Record<string, IFileLinkListItemIcon> = {
'application/pdf': { icon: 'export-pdf-descr', clazz: 'pdf' },
'image/jpeg': { icon: 'image1', clazz: 'img' },
'image/png': { icon: 'image1', clazz: 'img' },
'image/gif': { icon: 'image1', clazz: 'img' },
'image/svg+xml': { icon: 'image1', clazz: 'img' },
'image/tiff': { icon: 'image1', clazz: 'img' },
'image/bmp': { icon: 'image1', clazz: 'img' },
'image/webp': { icon: 'image1', clazz: 'img' },
'image/heic': { icon: 'image1', clazz: 'img' },
'image/heif': { icon: 'image1', clazz: 'img' },
'image/avif': { icon: 'image1', clazz: 'img' },
'image/cgm': { icon: 'image1', clazz: 'img' },
'text/plain': { icon: 'file-text', clazz: 'txt' },
'text/html': { icon: 'file-text', clazz: 'txt' },
'application/rtf': { icon: 'file-text', clazz: 'txt' },
'application/xml': { icon: 'file-text', clazz: 'txt' },
'application/xhtml+xml': { icon: 'file-text', clazz: 'txt' },
'application/x-tex': { icon: 'file-text', clazz: 'txt' },
'application/vnd.oasis.opendocument.text': { icon: 'file-doc', clazz: 'doc' },
'application/vnd.oasis.opendocument.text-template': { icon: 'file-doc', clazz: 'doc' },
'application/msword': { icon: 'file-doc', clazz: 'doc' },
'application/vnd.apple.pages': { icon: 'file-doc', clazz: 'doc' },
'application/vnd.stardivision.writer': { icon: 'file-doc', clazz: 'doc' },
'application/x-abiword': { icon: 'file-doc', clazz: 'doc' },
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': { icon: 'file-doc', clazz: 'doc' },
'font/otf': { icon: 'file-doc', clazz: 'doc' },
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': { icon: 'file-sheet', clazz: 'sheet' },
'application/vnd.oasis.opendocument.spreadsheet': { icon: 'file-sheet', clazz: 'sheet' },
'application/vnd.oasis.opendocument.spreadsheet-template': { icon: 'file-sheet', clazz: 'sheet' },
'application/vnd.ms-excel': { icon: 'file-sheet', clazz: 'sheet' },
'application/vnd.stardivision.calc': { icon: 'file-sheet', clazz: 'sheet' },
'application/vnd.apple.numbers': { icon: 'file-sheet', clazz: 'sheet' },
'application/x-starcalc': { icon: 'file-sheet', clazz: 'sheet' },
'application/x-quattro-pro': { icon: 'file-sheet', clazz: 'sheet' },
'application/csv': { icon: 'file-sheet', clazz: 'sheet' },
'application/vnd.oasis.opendocument.presentation': { icon: 'file-presentation', clazz: 'presentation' },
'application/vnd.oasis.opendocument.presentation-template': { icon: 'file-presentation', clazz: 'presentation' },
'application/vnd.apple.keynote': { icon: 'file-presentation', clazz: 'presentation' },
'application/vnd.ms-powerpoint': { icon: 'file-presentation', clazz: 'presentation' },
'application/vnd.openxmlformats-officedocument.presentationml.presentation': {
icon: 'file-presentation',
clazz: 'presentation',
},
'application/vnd.stardivision.impress': { icon: 'file-presentation', clazz: 'presentation' },
'application/mathematica': { icon: 'file-presentation', clazz: 'presentation' },
'video/mp4': { icon: 'movie', clazz: 'mov' },
'video/x-m4v': { icon: 'movie', clazz: 'mov' },
'video/avi': { icon: 'movie', clazz: 'mov' },
'video/quicktime': { icon: 'movie', clazz: 'mov' },
'video/webm': { icon: 'movie', clazz: 'mov' },
'video/mpg': { icon: 'movie', clazz: 'mov' },
'video/x-matroska': { icon: 'movie', clazz: 'mov' },
'video/mp1s': { icon: 'movie', clazz: 'mov' },
'video/mp2p': { icon: 'movie', clazz: 'mov' },
'video/3gpp': { icon: 'movie', clazz: 'mov' },
'video/3gpp-tt': { icon: 'movie', clazz: 'mov' },
'video/3gpp-2': { icon: 'movie', clazz: 'mov' },
'application/x-op-directory': { icon: 'folder', clazz: 'dir' },
default: { icon: 'ticket', clazz: 'default' },
};

@ -27,44 +27,83 @@
//++
import {
ChangeDetectionStrategy, Component, Input, OnInit,
AfterViewInit,
ChangeDetectionStrategy,
Component,
ElementRef,
EventEmitter,
Input,
OnInit,
Output,
ViewChild,
} from '@angular/core';
import { HalResource } from 'core-app/features/hal/resources/hal-resource';
import { IFileLink } from 'core-app/core/state/file-links/file-link.model';
import { IFileLink, IFileLinkOriginData } from 'core-app/core/state/file-links/file-link.model';
import {
getIconForMimeType,
IFileLinkListItemIcon,
} from 'core-app/shared/components/file-links/file-link-list/file-link-list-item-icon.factory';
} from 'core-app/shared/components/file-links/file-link-icons/file-link-list-item-icon.factory';
import { TimezoneService } from 'core-app/core/datetime/timezone.service';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { PrincipalRendererService } from 'core-app/shared/components/principal/principal-renderer.service';
import { IFileLinkListItemIcon } from 'core-app/shared/components/file-links/file-link-icons/icon-mappings';
@Component({
selector: 'op-file-link-list-item',
// eslint-disable-next-line @angular-eslint/component-selector
selector: '[op-file-link-list-item]',
templateUrl: './file-link-list-item.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FileLinkListItemComponent implements OnInit {
export class FileLinkListItemComponent implements OnInit, AfterViewInit {
@Input() public resource:HalResource;
@Input() public fileLink:IFileLink;
@Input() public index:number;
@Input() public allowEditing = false;
@Output() public removeFileLink = new EventEmitter<void>();
@ViewChild('avatar') avatar:ElementRef;
public infoTimestampText:string;
public fileLinkIcon:IFileLinkListItemIcon;
public text = {
title: {
openFile: this.i18n.t('js.label_open_file_link'),
openFileLocation: this.i18n.t('js.label_open_file_link_location'),
removeFileLink: this.i18n.t('js.label_remove_file_link'),
},
};
constructor(
private readonly i18n:I18nService,
private readonly timezoneService:TimezoneService,
private readonly principalRendererService:PrincipalRendererService,
) {}
private get originData():IFileLinkOriginData {
return this.fileLink.originData;
}
ngOnInit():void {
if (this.fileLink.originData.lastModifiedAt) {
const date = this.timezoneService.formattedDate(this.fileLink.originData.lastModifiedAt);
this.infoTimestampText = this.i18n.t('js.label_modified_at', { date });
if (this.originData.lastModifiedAt) {
this.infoTimestampText = this.timezoneService.parseDatetime(this.originData.lastModifiedAt).fromNow();
}
this.fileLinkIcon = getIconForMimeType(this.fileLink.originData.mimeType);
this.fileLinkIcon = getIconForMimeType(this.originData.mimeType);
}
ngAfterViewInit():void {
if (this.originData.lastModifiedByName) {
this.principalRendererService.render(
this.avatar.nativeElement,
{ name: this.originData.lastModifiedByName, href: '/users/1' },
{ hide: true, link: false },
{ hide: false, size: 'mini' },
);
}
}
}

@ -1,25 +1,45 @@
<div
class="op-files-tab--file-list-item--content file-name"
>
<op-icon
icon-classes="icon4 icon-{{fileLinkIcon.icon}}"
class="op-files-tab--color-{{fileLinkIcon.color}}"
></op-icon>
<div class="spot-list--item-floating-wrapper">
<a
[textContent]="fileLink.originData.name"
></a>
</div>
class="spot-list--item-action op-files-tab--file-list-item-action"
[href]="fileLink._links.staticOriginOpen.href"
[title]="fileLink.originData.name"
target="_blank"
>
<span
class="spot-icon spot-icon_{{fileLinkIcon.icon}} op-files-tab--icon_{{fileLinkIcon.clazz}}"
></span>
<span
[textContent]="fileLink.originData.name"
class="spot-list--item-title op-files-tab--file-list-item-title"
></span>
<div
class="op-files-tab--file-list-item--content file-info"
>
<span
class="op-files-tab--file-list-item--file-info-timestamp op-files-tab--color-grey"
[textContent]="infoTimestampText"
></span>
<span
class="op-files-tab--file-list-item-text"
[textContent]="infoTimestampText"
></span>
<span
[textContent]="fileLink.originData.lastModifiedByName"
></span>
<div
#avatar
class="op-files-tab--file-list-item-avatar"
></div>
</a>
<div class="spot-list--item-floating-actions op-files-tab--file-list-item-floating-actions">
<a
class="spot-link"
[title]="text.title.openFileLocation"
[href]="fileLink._links.staticOriginOpenLocation.href"
target="_blank"
>
<span class="spot-icon spot-icon_folder-open"></span>
</a>
<button
*ngIf="allowEditing"
class="spot-link"
[title]="text.title.removeFileLink"
(click)="removeFileLink.emit()"
>
<span class="spot-icon spot-icon_remove-link"></span>
</button>
</div>
</div>

@ -29,11 +29,12 @@
import {
ChangeDetectionStrategy, Component, Input, OnInit,
} from '@angular/core';
import { Observable } from 'rxjs';
import { BehaviorSubject, Observable } from 'rxjs';
import { HalResource } from 'core-app/features/hal/resources/hal-resource';
import { IFileLink } from 'core-app/core/state/file-links/file-link.model';
import { IStorage } from 'core-app/core/state/storages/storage.model';
import { FileLinksResourceService } from 'core-app/core/state/file-links/file-links.service';
import { CurrentUserService } from 'core-app/core/current-user/current-user.service';
import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import isNewResource from 'core-app/features/hal/helpers/is-new-resource';
@ -58,15 +59,17 @@ export class FileLinkListComponent extends UntilDestroyedMixin implements OnInit
informationBoxIcon:string;
showInformationBox = false;
allowEditing = false;
showFileLinks = false;
showInformationBox$ = new BehaviorSubject<boolean>(false);
showFileLinks$ = new BehaviorSubject<boolean>(false);
private readonly storageTypeMap:{ [urn:string]:string; } = {
'urn:openproject-org:api:v3:storages:Nextcloud': 'Nextcloud',
};
private text:{
text:{
infoBox:{
emptyStorageHeader:string,
emptyStorageContent:string,
@ -76,12 +79,16 @@ export class FileLinkListComponent extends UntilDestroyedMixin implements OnInit
authenticationFailureHeader:string,
authenticationFailureContent:string,
loginButton:string,
},
actions:{
linkFile:string,
}
};
constructor(
private readonly i18n:I18nService,
private readonly fileLinkResourceService:FileLinksResourceService,
private readonly currentUserService:CurrentUserService,
) {
super();
}
@ -100,6 +107,21 @@ export class FileLinkListComponent extends UntilDestroyedMixin implements OnInit
this.deriveStorageInformation(fileLinks.length);
});
this.currentUserService
.hasCapabilities$('file_links/manage', (this.resource.project as unknown&{ id:string }).id)
.pipe(this.untilDestroyed())
.subscribe((value) => {
this.allowEditing = value;
});
}
public removeFileLink(fileLink:IFileLink):void {
this.fileLinkResourceService.remove(this.collectionKey, fileLink);
}
public openStorageLocation():void {
window.open(this.storage._links.origin.href);
}
private get collectionKey():string {
@ -112,9 +134,9 @@ export class FileLinkListComponent extends UntilDestroyedMixin implements OnInit
}
private deriveStorageInformation(fileLinkCount:number):void {
switch (this.storage._links?.connectionState.href) {
switch (this.storage._links.connectionState.href) {
case 'urn:openproject-org:api:v3:storages:connection:FailedAuthentication':
this.setAuthenticationFailureState();
this.setAuthenticationFailureState(fileLinkCount);
break;
case 'urn:openproject-org:api:v3:storages:connection:Error':
this.setConnectionErrorState();
@ -123,49 +145,49 @@ export class FileLinkListComponent extends UntilDestroyedMixin implements OnInit
if (fileLinkCount === 0) {
this.setEmptyFileLinkListState();
} else {
this.showInformationBox = false;
this.showFileLinks = true;
this.showInformationBox$.next(false);
this.showFileLinks$.next(true);
}
break;
default:
this.showInformationBox = false;
this.showFileLinks = false;
this.showInformationBox$.next(false);
this.showFileLinks$.next(false);
}
}
private setAuthenticationFailureState():void {
private setAuthenticationFailureState(fileLinkCount:number):void {
this.informationBoxHeader = this.text.infoBox.authenticationFailureHeader;
this.informationBoxContent = this.text.infoBox.authenticationFailureContent;
this.informationBoxButton = this.text.infoBox.loginButton;
this.informationBoxIcon = 'info1';
this.showInformationBox = true;
this.showFileLinks = true;
this.informationBoxIcon = 'import';
this.showInformationBox$.next(true);
this.showFileLinks$.next(fileLinkCount > 0);
}
private setConnectionErrorState():void {
this.informationBoxHeader = this.text.infoBox.connectionErrorHeader;
this.informationBoxContent = this.text.infoBox.connectionErrorContent;
this.informationBoxButton = this.text.infoBox.loginButton;
this.informationBoxIcon = 'info1';
this.showInformationBox = true;
this.showFileLinks = false;
this.informationBoxIcon = 'remove-link';
this.showInformationBox$.next(true);
this.showFileLinks$.next(false);
}
private setEmptyFileLinkListState():void {
this.informationBoxHeader = this.text.infoBox.emptyStorageHeader;
this.informationBoxContent = this.text.infoBox.emptyStorageContent;
this.informationBoxButton = this.text.infoBox.emptyStorageButton;
this.informationBoxIcon = 'info1';
this.showInformationBox = true;
this.showFileLinks = true;
this.informationBoxIcon = 'add-link';
this.showInformationBox$.next(true);
this.showFileLinks$.next(false);
}
private initializeLocales():void {
const storageType = this.storageTypeMap[this.storage._links.type.href];
this.text = {
infoBox: {
emptyStorageHeader: this.i18n.t('js.label_no_file_links_header', { storageType }),
emptyStorageContent: this.i18n.t('js.label_no_file_links_content', { storageType }),
emptyStorageHeader: this.i18n.t('js.label_link_files_in_storage', { storageType }),
emptyStorageContent: this.i18n.t('js.label_no_file_links', { storageType }),
emptyStorageButton: this.i18n.t('js.label_open_storage', { storageType }),
connectionErrorHeader: this.i18n.t('js.label_no_storage_connection', { storageType }),
connectionErrorContent: this.i18n.t('js.label_storage_connection_error', { storageType }),
@ -173,6 +195,9 @@ export class FileLinkListComponent extends UntilDestroyedMixin implements OnInit
authenticationFailureContent: this.i18n.t('js.label_storage_not_connected', { storageType }),
loginButton: this.i18n.t('js.label_storage_login', { storageType }),
},
actions: {
linkFile: this.i18n.t('js.label_link_files_in_storage', { storageType }),
},
};
}
}

@ -1,23 +1,44 @@
<div class="work-package--attachments--files" *ngIf="resource" data-qa-selector="op-files-tab--file-link-list">
<ng-container
*ngIf="resource"
>
<op-storage-information
*ngIf="showInformationBox"
[infoIcon]="'info1'"
*ngIf="showInformationBox$ | async"
class="op-files-tab--storage-info-box"
[infoIcon]="informationBoxIcon"
[infoTextHeader]="informationBoxHeader"
[infoTextContent]="informationBoxContent"
[buttonText]="informationBoxButton"
(buttonClickEventEmitter)="openStorageLocation()"
></op-storage-information>
<div
*ngIf="showFileLinks"
<ul
*ngIf="showFileLinks$ | async"
class="spot-list spot-list_compact op-files-tab--file-list"
data-qa-selector="op-files-tab--file-list"
>
<op-file-link-list-item
<li
*ngFor="let fileLink of fileLinks$ | async; let i = index;"
class="op-files-tab--file-list-item"
[ngClass]="{ 'inactive': showInformationBox || fileLink._links.permission.href === 'urn:openproject-org:api:v3:file-links:permission:NotAllowed' }"
class="spot-list--item op-files-tab--file-list-item"
data-qa-selector="op-files-tab--file-list-item"
op-file-link-list-item
[fileLink]="fileLink"
[resource]="resource"
[index]="i">
</op-file-link-list-item>
</div>
</div>
[index]="i"
[allowEditing]="allowEditing"
(removeFileLink)="removeFileLink(fileLink)"
></li>
</ul>
<p
*ngIf="(showInformationBox$ | async) === false"
class="spot-body-small"
>
<a
class="spot-link op-files-tab--file-list-action-button"
(click)="openStorageLocation()"
>
<span class="spot-icon spot-icon_external-link"></span>
<span [textContent]="text.actions.linkFile"></span>
</a>
</p>
</ng-container>

@ -1,36 +1,27 @@
<span class="spot-icon big spot-icon_{{infoIcon}}"></span>
<div
class="op-files-tab--storage-info-box"
class="text-box"
>
<div
class="icon-box"
class="text-box-header"
>
<i
class="icon icon-{{infoIcon}}"
></i>
<span [textContent]="infoTextHeader"></span>
</div>
<div
class="text-box"
class="text-box-content"
>
<div
class="text-box-header"
>
<span [textContent]="infoTextHeader"></span>
</div>
<div
class="text-box-content"
>
<span [textContent]="infoTextContent"></span>
</div>
<span [textContent]="infoTextContent"></span>
</div>
</div>
<div
class="button-box"
>
<button
class="action-button"
[textContent]="buttonText"
(click)="buttonClickEventEmitter.emit()"
></button>
</div>
<div
class="button-box"
>
<button class="button button_no-margin">
<op-icon icon-classes="button--icon spot-icon spot-icon_external-link" class="op-icon--wrapper">
<i class="button--icon spot-icon spot-icon_external-link"></i>
</op-icon>
<span class="button--text">Open Nextcloud</span>
</button>
</div>

@ -0,0 +1,3 @@
<svg width="16" height="10" viewBox="0 0 16 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.8 3.07692H11.2V4.61538H4.8V3.07692ZM14.48 3.84615H16C16 1.72308 14.208 0 12 0H8.8V1.46154H12C13.368 1.46154 14.48 2.53077 14.48 3.84615ZM1.52 3.84615C1.52 2.53077 2.632 1.46154 4 1.46154H7.2V0H4C1.792 0 0 1.72308 0 3.84615C0 5.96923 1.792 7.69231 4 7.69231H7.2V6.23077H4C2.632 6.23077 1.52 5.16154 1.52 3.84615ZM13.6 3.84615H12V6.15385H9.6V7.69231H12V10H13.6V7.69231H16V6.15385H13.6V3.84615Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 523 B

@ -0,0 +1,3 @@
<svg width="18" height="10" viewBox="0 0 18 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.01372 0.899994C7.14796 0.899994 5.56658 2.16485 5.07646 3.87859C4.65049 2.9696 3.7274 2.33277 2.66417 2.33277C1.20197 2.33277 0 3.53474 0 4.99694C0 6.45914 1.20197 7.66166 2.66417 7.66166C3.7274 7.66166 4.65049 7.02444 5.07646 6.11529C5.56658 7.82916 7.14796 9.09444 9.01372 9.09444C10.8657 9.09444 12.439 7.84824 12.9417 6.1537C13.3755 7.04221 14.2872 7.66166 15.3353 7.66166C16.7975 7.66166 18 6.45914 18 4.99694C18 3.53474 16.7975 2.33277 15.3353 2.33277C14.2872 2.33277 13.3755 2.95183 12.9417 3.84018C12.439 2.14577 10.8657 0.899994 9.01372 0.899994ZM9.01372 2.46392C10.4222 2.46392 11.5473 3.58851 11.5473 4.99694C11.5473 6.40537 10.4222 7.53051 9.01372 7.53051C7.60529 7.53051 6.48071 6.40537 6.48071 4.99694C6.48071 3.58851 7.60529 2.46392 9.01372 2.46392ZM2.66417 3.8967C3.28123 3.8967 3.76495 4.37987 3.76495 4.99694C3.76495 5.61401 3.28123 6.09773 2.66417 6.09773C2.0471 6.09773 1.56393 5.61401 1.56393 4.99694C1.56393 4.37987 2.0471 3.8967 2.66417 3.8967ZM15.3353 3.8967C15.9524 3.8967 16.4361 4.37987 16.4361 4.99694C16.4361 5.61401 15.9524 6.09773 15.3353 6.09773C14.7182 6.09773 14.235 5.61401 14.235 4.99694C14.235 4.37987 14.7182 3.8967 15.3353 3.8967Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.6343 16L7.67075 8.82415H4.0026V7.17585H6.07103L4.47132 5.52756H4.0026C3.36619 5.52756 2.75585 5.78805 2.30584 6.25172C1.85583 6.71539 1.60302 7.34427 1.60302 8C1.60302 8.65573 1.85583 9.28461 2.30584 9.74828C2.75585 10.212 3.36619 10.4724 4.0026 10.4724H6.40218V12.1207H4.0026C3.02898 12.1216 2.0885 11.7564 1.35753 11.0937C0.626559 10.4311 0.155306 9.51642 0.0321393 8.52129C-0.0910271 7.52616 0.142354 6.51891 0.688518 5.68843C1.23468 4.85795 2.05611 4.26128 2.99878 4.0103L0.236864 1.16534L1.36866 0L15.7661 14.8347L14.6351 15.9992V16H14.6343ZM14.5095 11.2084L13.3673 10.0323C13.7876 9.73232 14.1039 9.30181 14.2701 8.80357C14.4363 8.30534 14.4437 7.76544 14.2912 7.26258C14.1387 6.75972 13.8343 6.3202 13.4223 6.0081C13.0104 5.696 12.5125 5.52764 12.0012 5.52756H9.60161V3.87926H12.0012C12.8286 3.87936 13.6357 4.14391 14.3111 4.63643C14.9865 5.12894 15.497 5.82519 15.7722 6.6292C16.0475 7.43321 16.0739 8.30539 15.8479 9.12553C15.6219 9.94568 15.1546 10.6734 14.5103 11.2084H14.5095Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 179 KiB

@ -27,96 +27,104 @@
//++
.op-files-tab
&--file-list-item
display: grid
grid-template-columns: 1fr 1fr
height: 40px
.op-files-tab--file-list
.op-files-tab--file-list-item
.op-files-tab--file-list-item-action
padding-left: 0
padding-right: 0
&:hover .op-files-tab--file-list-item-title
text-decoration: underline
.op-files-tab--file-list-item-title
@include spot-body-small(bold)
&.inactive
opacity: 0.5
line-height: $spot-spacing-1-5
word-break: normal
overflow: hidden
text-overflow: ellipsis
&--content
align-self: center
width: 100%
white-space: nowrap
overflow: hidden
text-overflow: ellipsis
.op-files-tab--file-list-item-text
@include spot-caption()
&.file-name
font-size: 14px
color: #9A9A9A
line-height: $spot-spacing-1-5
flex-shrink: 0
&.file-info
text-align: right
font-size: 12px
&:not(:last-child)
margin-right: $spot-spacing-0-5
&--file-info-timestamp
margin-right: 8px
.spot-icon:first-child:not(:last-child)
width: $spot-spacing-1-5
height: $spot-spacing-1-5
.op-files-tab--file-list-item-avatar
flex-shrink: 0
display: inline-flex
justify-content: center
align-items: center
width: $spot-spacing-1-5
height: $spot-spacing-1-5
.op-files-tab--file-list-item-floating-actions
padding-right: 0
&--storage-info-box
margin-top: 14px
margin-top: 0.875rem
display: grid
grid-template: "icon text" "button button" / auto 1fr
.icon-box
height: 54px
width: 54px
.spot-icon.big
color: #9A9A9A
grid-area: icon
display: flex
justify-content: center
align-items: center
.icon
color: #878787
font-size: 36px
width: 3.375rem
height: 3.375rem
font-size: 1.25rem
.text-box
grid-area: text
.text-box-header
font-size: 16px
font-weight: 700
line-height: 24px
line-height: $spot-spacing-1-5
.text-box-content
font-size: 14px
font-weight: 400
line-height: 20px
color: #878787
@include spot-body-small()
color: #9A9A9A
.button-box
grid-area: button
display: flex
justify-content: end
.action-button
padding: 4px 12px
font-size: 14px
line-height: 20px
&--color
&-red
&--icon
&_pdf
color: #B93A33
&-blue
color: #00BDF3
&_img
color: #0081C7
&-blue-deep
color: #006E8F
&_mov
color: #7C006E
&-blue-dark
color: #0081C7
&_txt
color: #9A9A9A
&-turquoise
color: #87E2C1
&_doc
color: #006E8F
&-green
&_sheet
color: #007528
&-grey-dark
color: #9A9A9A
&_presentation
color: #EF9E56
&-grey
color: #878787
&_form
color: #87E2C1
&-orange
color: #EF9E56
&_dir
color: #00BDF3
&_default
color: #9A9A9A

@ -28,17 +28,30 @@
.op-tab-content
&--container
margin-right: 16px
margin-right: $spot-spacing-1
.spot-list:not(:last-child)
margin-bottom: $spot-spacing-0-75
.op-files-tab--storage-info-box:not(:last-child)
margin-bottom: $spot-spacing-1
&--tab-section
margin-bottom: $spot-spacing-2
&--header
border-bottom: 1px solid #ddd
padding-bottom: $spot-spacing-0-75
display: flex
align-items: center
&-text
font-size: 16px
line-height: 1.1875em
line-height: $spot-spacing-1_5
font-weight: 700
text-transform: uppercase
margin: 16px 0
padding: 0
border-bottom: none
.spot-icon
width: $spot-spacing-1_5
height: $spot-spacing-1_5
margin-right: $spot-spacing-0-5
color: #1A67A3

@ -5,6 +5,7 @@
<ul class="icon-list">
<li><span class="icon icon-accessibility"></span>accessibility</li>
<li><span class="icon icon-accountable"></span>accountable</li>
<li><span class="icon icon-add-link"></span>add-link</li>
<li><span class="icon icon-add"></span>add</li>
<li><span class="icon icon-align-center"></span>align-center</li>
<li><span class="icon icon-align-justify"></span>align-justify</li>
@ -167,6 +168,7 @@
<li><span class="icon icon-new-planning-element"></span>new-planning-element</li>
<li><span class="icon icon-news"></span>news</li>
<li><span class="icon icon-nextcloud-circle"></span>nextcloud-circle</li>
<li><span class="icon icon-nextcloud"></span>nextcloud</li>
<li><span class="icon icon-no-hierarchy"></span>no-hierarchy</li>
<li><span class="icon icon-no-zen-mode"></span>no-zen-mode</li>
<li><span class="icon icon-not-supported"></span>not-supported</li>
@ -203,6 +205,7 @@
<li><span class="icon icon-relations"></span>relations</li>
<li><span class="icon icon-reload"></span>reload</li>
<li><span class="icon icon-reminder"></span>reminder</li>
<li><span class="icon icon-remove-link"></span>remove-link</li>
<li><span class="icon icon-remove"></span>remove</li>
<li><span class="icon icon-rename"></span>rename</li>
<li><span class="icon icon-reported-by-me"></span>reported-by-me</li>

@ -51,7 +51,7 @@ describe 'Showing of file links in work package', with_flag: { storages_module_a
context 'if work package has associated file links' do
it "must show associated file links" do
expect(page).to have_selector('[data-qa-selector="op-tab-content--tab-section"]', count: 2)
expect(page.find('[data-qa-selector="op-files-tab--file-link-list"]'))
expect(page.find('[data-qa-selector="op-files-tab--file-list"]'))
.to have_selector('[data-qa-selector="op-files-tab--file-list-item"]', text: file_link.origin_name, count: 1)
end
end

Loading…
Cancel
Save