Skip to content

Commit 33697b3

Browse files
authored
Merge pull request #280 from cap-java/linkSupport
Link support
2 parents 9c1d1fc + 1605ec0 commit 33697b3

11 files changed

Lines changed: 1444 additions & 19 deletions

File tree

README.md

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ This plugin can be consumed by the CAP application deployed on BTP to store thei
1313
- Virus scanning : Provides the capability to support virus scan for virus scan enabled repositories.
1414
- Draft functionality : Provides the capability of working with draft attachments.
1515
- Display attachments specific to repository: Lists attachments contained in the repository that is configured with the CAP application.
16+
- Custom properties : Provides the capability to define custom properties for attachments.
1617
- Maximum allowed uploads: Provides the capability to define the maximum number of uploads allowed for the user.
1718
- Multiple attachment facets: Provides the capability to define multiple attachment facets/sections in the CAP Entity.
1819
- Technical user support: Provides the capability to consume the plugin using technical user.
1920
- Copy attachments: Provides the capability to copy attachments from one entity to another entity.
21+
- Link as attachments: Provides the capability to support link or URL as attachments.
2022

2123
## Table of Contents
2224

@@ -30,6 +32,7 @@ This plugin can be consumed by the CAP application deployed on BTP to store thei
3032
- [Support for Multiple attachment facets](#support-for-multiple-attachment-facets)
3133
- [Support for Technical user](#support-for-technical-user)
3234
- [Support for Copy attachments](#support-for-copy-attachments)
35+
- [Support for Link type attachments](#support-for-link-type-attachments)
3336
- [Known Restrictions](#known-restrictions)
3437
- [Support, Feedback, Contributing](#support-feedback-contributing)
3538
- [Code of Conduct](#code-of-conduct)
@@ -556,6 +559,189 @@ This plugin provides capability to copy attachments from one entity to another.
556559
}
557560
```
558561

562+
## Support for link type attachments
563+
564+
> **Note:** Row-press is the new recommended approach for handling actions on attachment rows. With row-press enabled, the Attachments column will no longer appear as a separate action button. Instead, clicking on a row will automatically perform the appropriate action — opening a link or downloading a file, based on the attachment type.
565+
566+
This plugin provides the capability to create, open, rename and delete attachments of link type.
567+
568+
### Steps to Enable Row-Press for Open Link
569+
570+
1. **Add the `openAttachment` action to application's service definition**
571+
572+
See this [example](https://github.com/cap-java/sdm/blob/90cfc716967d844e114457a710daebdd55431965/cap-notebook/demoapp/srv/admin-service.cds#L19) from a sample Bookshop app.
573+
574+
```cds
575+
action openAttachment() returns String;
576+
```
577+
578+
2. **Add a custom controller extension**
579+
580+
In webapp/controller/custom.controller.js, copy and paste below content.
581+
582+
See this [example](https://github.com/cap-java/sdm/blob/develop_deploy/cap-notebook/demoapp/app/admin-books/webapp/controller/custom.controller.js) from a sample Bookshop app.
583+
584+
```js
585+
sap.ui.define(
586+
[
587+
"sap/ui/core/mvc/ControllerExtension",
588+
"sap/m/library"
589+
],
590+
function (ControllerExtension,library) {
591+
"use strict";
592+
593+
return ControllerExtension.extend("books.controller.custom", {
594+
onRowPress: function(oContext) {
595+
this.base.editFlow
596+
.invokeAction("AdminService.openAttachment", {
597+
contexts: oContext.getParameter("bindingContext")
598+
})
599+
.then(function (res) {
600+
let odataurl = "";
601+
if(res.getObject().value == "None") {
602+
const lastSlashIndex = res.oModel.getServiceUrl().lastIndexOf('/');
603+
let str = res.oModel.getServiceUrl();
604+
if (lastSlashIndex !== -1) {
605+
str = str.substring(0, lastSlashIndex) + str.substring(lastSlashIndex + 1);
606+
}
607+
odataurl = str+res.oBinding.oContext.sPath+"/content";
608+
} else {
609+
odataurl = res.getObject().value;
610+
}
611+
library.URLHelper.redirect(odataurl, true);
612+
613+
});
614+
}
615+
});
616+
}
617+
);
618+
```
619+
620+
- Replace `books` in `ControllerExtension.extend` with the `SAPUI5.Component` name from your `app/appconfig/fioriSandboxConfig.json` file. See this [example](https://github.com/cap-java/sdm/blob/90cfc716967d844e114457a710daebdd55431965/cap-notebook/demoapp/app/appconfig/fioriSandboxConfig.json#L86).
621+
- Replace `AdminService` in `invokeAction("AdminService.openAttachment")` with the name of your service.
622+
623+
3. **Add controlConfiguration for Row Press**
624+
625+
In your `sap.ui5.routing.targets` section, under the relevant Object Page (e.g., `BooksDetails`), add or extend the `controlConfiguration` for the facet you want to enhance by copy and pasting below content. See this [example](https://github.com/cap-java/sdm/blob/90cfc716967d844e114457a710daebdd55431965/cap-notebook/demoapp/app/admin-books/webapp/manifest.json#L121)
626+
627+
```json
628+
"controlConfiguration": {
629+
"attachments/@com.sap.vocabularies.UI.v1.LineItem": {
630+
"tableSettings": {
631+
"type": "ResponsiveTable",
632+
"selectionMode": "Auto",
633+
"rowPress": ".extension.books.controller.custom.onRowPress"
634+
}
635+
}
636+
}
637+
```
638+
- Replace `attachments` with your entity’s facet name as needed.
639+
- Repeat for other facets's if required.
640+
- Replace `books` in `"rowPress": ".extension.books.controller.custom.onRowPress"` with the SAPUI5.Component name from your
641+
`app/appconfig/fioriSandboxConfig.json` file. Refer this [example](https://github.com/cap-java/sdm/blob/90cfc716967d844e114457a710daebdd55431965/cap-notebook/demoapp/app/appconfig/fioriSandboxConfig.json#L86) from a sample Bookshop app.
642+
643+
4. **Register the Custom Controller Extension**
644+
645+
In the root of your `sap.ui5` section, add or extend the `extends` property to register your custom controller by copy and pasting below content. See this [example](https://github.com/cap-java/sdm/blob/90cfc716967d844e114457a710daebdd55431965/cap-notebook/demoapp/app/admin-books/webapp/manifest.json#L159)
646+
647+
```json
648+
"extends": {
649+
"extensions": {
650+
"sap.ui.controllerExtensions": {
651+
"sap.fe.templates.ObjectPage.ObjectPageController#books::BooksDetailsList": {
652+
"controllerName": "books.controller.custom"
653+
}
654+
}
655+
}
656+
}
657+
```
658+
- Replace `books` in `"sap.fe.templates.ObjectPage.ObjectPageController#books::BooksDetailsList"` with the SAPUI5.Component name from your
659+
`app/appconfig/fioriSandboxConfig.json` file. Refer this [example](https://github.com/cap-java/sdm/blob/90cfc716967d844e114457a710daebdd55431965/cap-notebook/demoapp/app/appconfig/fioriSandboxConfig.json#L86) from a sample Bookshop app.
660+
- Replace `BooksDetailsList` in `"sap.fe.templates.ObjectPage.ObjectPageController#books::BooksDetailsList"` with id of the relevant Object Page (e.g., BooksDetails). Refer this [example](https://github.com/cap-java/sdm/blob/90cfc716967d844e114457a710daebdd55431965/cap-notebook/demoapp/app/admin-books/webapp/manifest.json#L109) from a sample Bookshop app.
661+
- Replace `books` in `"controllerName": "books.controller.custom"` with the SAPUI5.Component name from your
662+
`app/appconfig/fioriSandboxConfig.json` file. Refer this [example](https://github.com/cap-java/sdm/blob/90cfc716967d844e114457a710daebdd55431965/cap-notebook/demoapp/app/appconfig/fioriSandboxConfig.json#L86) from a sample Bookshop app.
663+
664+
### Steps to Enable Create Link Feature in CAP Application
665+
666+
> **Note:** Enabling row-press for open link (see steps above) is a prerequisite for link support.
667+
668+
1. **Add the `createLink` action to application's service definition**
669+
670+
See this [example](https://github.com/cap-java/sdm/blob/90cfc716967d844e114457a710daebdd55431965/cap-notebook/demoapp/srv/admin-service.cds#L12) from a sample Bookshop app:
671+
672+
```cds
673+
action createLink(
674+
in:many $self,
675+
@mandatory @Common.Label:'Name' name: String @UI.Placeholder: 'Enter a name for the link',
676+
@mandatory @assert.format:'^(https?:\/\/)(([a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,}|localhost)(:\d{2,5})?(\/[^\s]*)?$'
677+
@Common.Label:'URL' url: String @UI.Placeholder: 'Example: https://www.example.com'
678+
);
679+
```
680+
- Purpose: Enables users to create links with name and URL.
681+
- Validation: Ensures only valid HTTP(S) URLs are accepted.
682+
- UI Support: Provides labels and placeholders for better user experience.
683+
684+
### UI Annotation Setup
685+
686+
To enable the creation of links, you need to add a new button to the attachments table toolbar. This button will appear as a **menu button labeled "Create"** on the toolbar of the attachments table. When clicked, it will display a menu with the **"Link"** option, allowing users to create a new link-type attachment.
687+
688+
Add the following annotation block to your app/common.cds file. See this [example](https://github.com/cap-java/sdm/blob/4288ce6f58bc415a171a9e0340fa075aeac835ff/cap-notebook/demoapp/app/common.cds#L60)
689+
690+
```cds
691+
annotate my.Books.attachments with @UI: {
692+
HeaderInfo: {
693+
$Type : 'UI.HeaderInfoType',
694+
TypeName : '{i18n>Attachment}',
695+
TypeNamePlural: '{i18n>Attachments}',
696+
},
697+
LineItem : [
698+
{Value: type, @HTML5.CssDefaults: {width: '10%'}},
699+
{Value: fileName, @HTML5.CssDefaults: {width: '25%'}},
700+
{Value: content, @HTML5.CssDefaults: {width: '0%'}},
701+
{Value: createdAt, @HTML5.CssDefaults: {width: '20%'}},
702+
{Value: createdBy, @HTML5.CssDefaults: {width: '20%'}},
703+
{Value: note, @HTML5.CssDefaults: {width: '25%'}},
704+
{
705+
$Type : 'UI.DataFieldForActionGroup',
706+
ID : 'TableActionGroup',
707+
Label : 'Create',
708+
![@UI.Hidden]: {$edmJson: {$Eq: [ {$Path: 'IsActiveEntity'}, true ]}},
709+
Actions: [
710+
{
711+
$Type : 'UI.DataFieldForAction',
712+
Label : 'Link',
713+
Action: 'AdminService.createLink',
714+
}
715+
]
716+
},
717+
],
718+
}
719+
{
720+
note @(title: '{i18n>Note}');
721+
type @(title: '{i18n>Type}');
722+
linkUrl @(title: '{i18n>LinkURL}');
723+
fileName @(title: '{i18n>Filename}');
724+
modifiedAt @(odata.etag: null);
725+
content
726+
@Core.ContentDisposition: { Filename: fileName }
727+
@(title: '{i18n>Attachment}');
728+
folderId @UI.Hidden;
729+
repositoryId @UI.Hidden ;
730+
objectId @UI.Hidden ;
731+
mimeType @UI.Hidden;
732+
status @UI.Hidden;
733+
}
734+
annotate Attachments with @Common: {SideEffects #ContentChanged: {
735+
SourceProperties: [content],
736+
TargetProperties: ['status'],
737+
TargetEntities : [Books.attachments]
738+
}}{};
739+
```
740+
741+
- Replace `my.Books.attachments` with the correct path for your entity and element (for example, `my.YourEntity.yourElement`) where you have defined `composition of many Attachments`.
742+
- Replace `AdminService` in `Action: 'AdminService.createLink'` with the name of your service.
743+
- Repeat for other entities and elements if you have defined multiple `composition of many Attachments`.
744+
559745
## Known Restrictions
560746

561747
- UI5 Version 1.135.0: This version causes error in upload of attachments.

sdm/src/main/java/com/sap/cds/sdm/configuration/Registration.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,15 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) {
8787
documentService,
8888
tokenHandlerInstance,
8989
dbQueryInstance));
90-
configurer.eventHandler(new SDMServiceGenericHandler(attachmentService));
90+
configurer.eventHandler(
91+
new SDMServiceGenericHandler(
92+
attachmentService,
93+
persistenceService,
94+
sdmService,
95+
documentService,
96+
draftServiceList,
97+
dbQueryInstance,
98+
tokenHandlerInstance));
9199
configurer.eventHandler(
92100
new SDMCustomServiceHandler(sdmService, draftServiceList, tokenHandlerInstance));
93101
}

sdm/src/main/java/com/sap/cds/sdm/constants/SDMConstants.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ private SDMConstants() {
4545
public static final String SDM_CONNECTIONPOOL_PREFIX = "cds.attachments.sdm.http.%s";
4646
public static final String USER_NOT_AUTHORISED_ERROR =
4747
"You do not have the required permissions to upload attachments. Please contact your administrator for access.";
48+
public static final String USER_NOT_AUTHORISED_ERROR_LINK =
49+
"You do not have the required permissions to create links. Please contact your administrator for access.";
4850
public static final String FILE_NOT_FOUND_ERROR = "Object not found in repository";
4951
public static final Integer MAX_CONNECTIONS = 100;
5052
public static final int CONNECTION_TIMEOUT = 1200;
@@ -100,6 +102,26 @@ public static String nameConstraintMessage(
100102
return bulletPoints.toString();
101103
}
102104

105+
public static String linkNameConstraintMessage(
106+
List<String> fileNameWithRestrictedCharacters, String operation) {
107+
// Create the base message
108+
String prefixMessage =
109+
"Link could not be %s. The following name(s) contain unsupported characters (/, \\). \n\n";
110+
111+
// Create the formatted prefix message
112+
String formattedPrefixMessage = String.format(prefixMessage, operation);
113+
114+
// Initialize the StringBuilder with the formatted message prefix
115+
StringBuilder bulletPoints = new StringBuilder(formattedPrefixMessage);
116+
117+
// Append each unsupported file name to the StringBuilder
118+
for (String file : fileNameWithRestrictedCharacters) {
119+
bulletPoints.append(String.format("\t• %s%n", file));
120+
}
121+
bulletPoints.append("\nRename the link and try again.");
122+
return bulletPoints.toString();
123+
}
124+
103125
public static String fileNotFound(List<String> fileNameNotFound) {
104126
// Create the base message
105127
String prefixMessage =
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.sap.cds.sdm.model;
2+
3+
import com.sap.cds.services.EventContext;
4+
import com.sap.cds.services.EventName;
5+
6+
@EventName("openAttachment")
7+
public interface AttachmentReadContext extends EventContext {
8+
9+
static AttachmentReadContext create() {
10+
return (AttachmentReadContext) EventContext.create(AttachmentReadContext.class, null);
11+
}
12+
13+
void setResult(String res);
14+
15+
String getResult();
16+
}

sdm/src/main/java/com/sap/cds/sdm/model/CmisDocument.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,6 @@ public class CmisDocument {
2424
private String mimeType;
2525
private long contentLength;
2626
private String subdomain;
27+
private String url;
28+
private String contentId;
2729
}

sdm/src/main/java/com/sap/cds/sdm/persistence/DBQuery.java

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,33 @@ public Result getAttachmentsForUPID(
3535
String upIdKey) {
3636
CqnSelect q =
3737
Select.from(attachmentEntity)
38-
.columns("fileName", "ID", "IsActiveEntity", "folderId", "repositoryId")
38+
.columns("fileName", "ID", "IsActiveEntity", "folderId", "repositoryId", "mimeType")
3939
.where(doc -> doc.get(upIdKey).eq(upID));
4040
return persistenceService.run(q);
4141
}
4242

43+
public CmisDocument getObjectIdForAttachmentID(
44+
CdsEntity attachmentEntity, PersistenceService persistenceService, String id) {
45+
CqnSelect q =
46+
Select.from(attachmentEntity)
47+
.columns("objectId", "folderId", "fileName", "mimeType", "contentId", "linkUrl")
48+
.where(doc -> doc.get("ID").eq(id));
49+
Result result = persistenceService.run(q);
50+
Optional<Row> res = result.first();
51+
CmisDocument cmisDocument = new CmisDocument();
52+
if (res.isPresent()) {
53+
Row row = res.get();
54+
cmisDocument.setObjectId(row.get("objectId").toString());
55+
cmisDocument.setFileName(row.get("fileName").toString());
56+
cmisDocument.setFolderId(row.get("folderId").toString());
57+
cmisDocument.setMimeType(row.get("mimeType").toString());
58+
cmisDocument.setContentId(
59+
row.get("contentId") != null ? row.get("contentId").toString() : null);
60+
cmisDocument.setUrl(row.get("linkUrl") != null ? row.get("linkUrl").toString() : null);
61+
}
62+
return cmisDocument;
63+
}
64+
4365
public Result getAttachmentsForUPIDAndRepository(
4466
CdsEntity attachmentEntity,
4567
PersistenceService persistenceService,
@@ -74,6 +96,8 @@ public void addAttachmentToDraft(
7496
updatedFields.put("repositoryId", repositoryId);
7597
updatedFields.put("folderId", cmisDocument.getFolderId());
7698
updatedFields.put("status", "Clean");
99+
String icon = getIconForMimeType(cmisDocument.getMimeType());
100+
updatedFields.put("type", icon);
77101

78102
CqnUpdate updateQuery =
79103
Update.entity(attachmentEntity)
@@ -82,6 +106,66 @@ public void addAttachmentToDraft(
82106
persistenceService.run(updateQuery);
83107
}
84108

109+
private static String getIconForMimeType(String mimeType) {
110+
if (isExcel(mimeType)) {
111+
return "sap-icon://excel-attachment";
112+
} else if (isImage(mimeType)) {
113+
return "sap-icon://attachment-photo";
114+
} else if (isText(mimeType)) {
115+
return "sap-icon://attachment-text-file";
116+
} else if (isPdf(mimeType)) {
117+
return "sap-icon://pdf-attachment";
118+
} else if (isPowerPoint(mimeType)) {
119+
return "sap-icon://ppt-attachment";
120+
} else if (isVideo(mimeType)) {
121+
return "sap-icon://attachment-video";
122+
} else if (isAudio(mimeType)) {
123+
return "sap-icon://attachment-audio";
124+
} else if (isZip(mimeType)) {
125+
return "sap-icon://attachment-zip-file";
126+
} else if (isHtml(mimeType)) {
127+
return "sap-icon://attachment-html";
128+
}
129+
return "sap-icon://document";
130+
}
131+
132+
private static boolean isExcel(String mimeType) {
133+
return mimeType.contains("vnd.ms-excel")
134+
|| mimeType.contains("vnd.openxmlformats-officedocument.spreadsheetml.sheet");
135+
}
136+
137+
private static boolean isImage(String mimeType) {
138+
return mimeType.contains("image");
139+
}
140+
141+
private static boolean isText(String mimeType) {
142+
return mimeType.contains("text");
143+
}
144+
145+
private static boolean isPdf(String mimeType) {
146+
return mimeType.contains("pdf");
147+
}
148+
149+
private static boolean isPowerPoint(String mimeType) {
150+
return mimeType.contains("powerpoint") || mimeType.contains("presentation");
151+
}
152+
153+
private static boolean isVideo(String mimeType) {
154+
return mimeType.contains("video");
155+
}
156+
157+
private static boolean isAudio(String mimeType) {
158+
return mimeType.contains("audio");
159+
}
160+
161+
private static boolean isZip(String mimeType) {
162+
return mimeType.contains("zip");
163+
}
164+
165+
private static boolean isHtml(String mimeType) {
166+
return mimeType.contains("html");
167+
}
168+
85169
public List<CmisDocument> getAttachmentsForFolder(
86170
String entity,
87171
PersistenceService persistenceService,

0 commit comments

Comments
 (0)