diff --git a/CHANGELOG.md b/CHANGELOG.md
index 00a8c768..c10785fd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
+## [2.6.0 - 2024-05-xx]
+
+### Added
+
+- Added File Actions v2 version with redirect to the ExApp UI. #284
+
+### Changed
+
+- Reworked scopes for database/cache requests optimization, drop old ex_app_scopes table. #285
+
## [2.5.1 - 2024-05-02]
### Added
diff --git a/appinfo/info.xml b/appinfo/info.xml
index 236f15ad..d17f5921 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -43,7 +43,7 @@ to join us in shaping a more versatile, stable, and secure app landscape.
*Your insights, suggestions, and contributions are invaluable to us.*
]]>
- 2.5.1
+ 2.6.0
agpl
Andrey Borysenko
Alexander Piskun
diff --git a/appinfo/routes.php b/appinfo/routes.php
index bac06c48..9e08a6e2 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -99,6 +99,7 @@
// --- UI ---
// File Actions Menu
['name' => 'OCSUi#registerFileActionMenu', 'url' => '/api/v1/ui/files-actions-menu', 'verb' => 'POST'],
+ ['name' => 'OCSUi#registerFileActionMenuV2', 'url' => '/api/v2/ui/files-actions-menu', 'verb' => 'POST'],
['name' => 'OCSUi#unregisterFileActionMenu', 'url' => '/api/v1/ui/files-actions-menu', 'verb' => 'DELETE'],
['name' => 'OCSUi#getFileActionMenu', 'url' => '/api/v1/ui/files-actions-menu', 'verb' => 'GET'],
diff --git a/docs/tech_details/api/fileactionsmenu.rst b/docs/tech_details/api/fileactionsmenu.rst
index a3a70a51..dbc4a55c 100644
--- a/docs/tech_details/api/fileactionsmenu.rst
+++ b/docs/tech_details/api/fileactionsmenu.rst
@@ -14,8 +14,15 @@ AppAPI takes responsibility to register FileActionsMenu, ExApps needs only to re
Register
^^^^^^^^
+.. note::
+
+ With AppAPI 2.6.0 there is a new v2 OCS endpoint with redirect to ExApp UI support:
+ OCS endpoint: ``POST /apps/app_api/api/v2/ui/files-actions-menu``.
+ Old v1 is marked as deprecated.
+
OCS endpoint: ``POST /apps/app_api/api/v1/ui/files-actions-menu``
+
Params
******
@@ -35,7 +42,6 @@ Complete list of params (including optional):
.. note:: Urls ``icon`` and ``actionHandler`` are relative to the ExApp root, starting slash is not required.
-
Optional params
***************
@@ -90,6 +96,25 @@ The following data is sent to ExApp FileActionsMenu handler from the context of
"instanceId": "string",
}
+Redirect to ExApp UI page (top menu)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. note::
+ Supported only for Nextcloud 28+.
+
+If you want to open some files in ExApp UI, your FileActionsMenu have to be registered using OCS v2 version (``/apps/app_api/api/v2/ui/files-actions-menu``).
+
+After that, AppAPI will expect in the JSON response of the ExApp ``action_handler``
+the ``redirect_handler`` - a relative path on the ExApp Top Menu page,
+to which AppAPI will attach a ``fileIds`` query parameter with the selected file ids, for example:
+
+``/index.php/apps/app_api/embedded/ui_example/first_menu/second_page?fileIds=123,124,125``,
+
+where the ``first_menu`` is the name of the Top Menu ExApp UI page,
+and the ``second_page`` relative route handled on the frontend routing of the ExApp,
+the ``fileIds`` query parameter contains the selected file ids separated by commas.
+After that you can get the files info via webdav search request, see `ui_example `_.
+
Request flow
^^^^^^^^^^^^
@@ -126,5 +151,5 @@ Examples
Here is a list of simple example ExApps based on FileActionsMenu:
-* `video_to_gif `_ - ExApp based on FileActionsMenu to convert videos to gif in place
-* `upscaler_demo `_ - ExApp based on FileActionsMenu to upscale image in place
+* `to_gif `_ - ExApp based on FileActionsMenu to convert videos to gif in place
+* `upscaler_example `_ - ExApp based on FileActionsMenu to upscale image in place
diff --git a/lib/Controller/OCSUiController.php b/lib/Controller/OCSUiController.php
index 56a52c5b..ea564355 100644
--- a/lib/Controller/OCSUiController.php
+++ b/lib/Controller/OCSUiController.php
@@ -38,6 +38,8 @@ public function __construct(
/**
* @throws OCSBadRequestException
+ *
+ * @depreacted since AppAPI 2.6.0, use registerFileActionMenuV2 instead
*/
#[AppAPIAuth]
#[PublicPage]
@@ -46,7 +48,24 @@ public function registerFileActionMenu(string $name, string $displayName, string
string $icon = "", string $mime = "file", int $permissions = 31,
int $order = 0): DataResponse {
$result = $this->filesActionsMenuService->registerFileActionMenu(
- $this->request->getHeader('EX-APP-ID'), $name, $displayName, $actionHandler, $icon, $mime, $permissions, $order);
+ $this->request->getHeader('EX-APP-ID'), $name, $displayName, $actionHandler, $icon, $mime, $permissions, $order, '1.0');
+ if (!$result) {
+ throw new OCSBadRequestException("File Action Menu entry could not be registered");
+ }
+ return new DataResponse();
+ }
+
+ /**
+ * @throws OCSBadRequestException
+ */
+ #[AppAPIAuth]
+ #[PublicPage]
+ #[NoCSRFRequired]
+ public function registerFileActionMenuV2(string $name, string $displayName, string $actionHandler,
+ string $icon = "", string $mime = "file", int $permissions = 31,
+ int $order = 0): DataResponse {
+ $result = $this->filesActionsMenuService->registerFileActionMenu(
+ $this->request->getHeader('EX-APP-ID'), $name, $displayName, $actionHandler, $icon, $mime, $permissions, $order, '2.0');
if (!$result) {
throw new OCSBadRequestException("File Action Menu entry could not be registered");
}
diff --git a/lib/Db/UI/FilesActionsMenu.php b/lib/Db/UI/FilesActionsMenu.php
index 17d058aa..124c4a12 100644
--- a/lib/Db/UI/FilesActionsMenu.php
+++ b/lib/Db/UI/FilesActionsMenu.php
@@ -20,6 +20,7 @@
* @method int getOrder()
* @method string getIcon()
* @method string getActionHandler()
+ * @method string getVersion()
* @method void setAppid(string $appid)
* @method void setName(string $name)
* @method void setDisplayName(string $displayName)
@@ -28,6 +29,7 @@
* @method void setOrder(int $order)
* @method void setIcon(string $icon)
* @method void setActionHandler(string $actionHandler)
+ * @method void setVersion(string $version)
*/
class FilesActionsMenu extends Entity implements JsonSerializable {
protected $appid;
@@ -38,6 +40,7 @@ class FilesActionsMenu extends Entity implements JsonSerializable {
protected $order;
protected $icon;
protected $actionHandler;
+ protected $version;
/**
* @param array $params
@@ -51,6 +54,7 @@ public function __construct(array $params = []) {
$this->addType('order', 'int');
$this->addType('icon', 'string');
$this->addType('actionHandler', 'string');
+ $this->addType('version', 'string');
if (isset($params['id'])) {
$this->setId($params['id']);
@@ -79,6 +83,9 @@ public function __construct(array $params = []) {
if (isset($params['action_handler'])) {
$this->setActionHandler($params['action_handler']);
}
+ if (isset($params['version'])) {
+ $this->setVersion($params['version']);
+ }
}
public function jsonSerialize(): array {
@@ -92,6 +99,7 @@ public function jsonSerialize(): array {
'order' => $this->getOrder(),
'icon' => $this->getIcon(),
'action_handler' => $this->getActionHandler(),
+ 'version' => $this->getVersion(),
];
}
}
diff --git a/lib/Migration/Version2206Date20240502145029.php b/lib/Migration/Version2206Date20240502145029.php
new file mode 100644
index 00000000..fe8b948a
--- /dev/null
+++ b/lib/Migration/Version2206Date20240502145029.php
@@ -0,0 +1,37 @@
+hasTable('ex_ui_files_actions')) {
+ $table = $schema->getTable('ex_ui_files_actions');
+
+ $table->addColumn('version', Types::STRING, [
+ 'notnull' => true,
+ 'length' => 64,
+ 'default' => '1.0',
+ ]);
+ }
+
+ return $schema;
+ }
+}
diff --git a/lib/Service/ExAppApiScopeService.php b/lib/Service/ExAppApiScopeService.php
index 905e3b3d..302a5b59 100644
--- a/lib/Service/ExAppApiScopeService.php
+++ b/lib/Service/ExAppApiScopeService.php
@@ -15,9 +15,11 @@ class ExAppApiScopeService {
public function __construct(
) {
$aeApiV1Prefix = '/apps/' . Application::APP_ID . '/api/v1';
+ $aeApiV2Prefix = '/apps/' . Application::APP_ID . '/api/v2';
$this->apiScopes = [
// AppAPI scopes
['api_route' => $aeApiV1Prefix . '/ui/files-actions-menu', 'scope_group' => 1, 'name' => 'BASIC', 'user_check' => 0],
+ ['api_route' => $aeApiV2Prefix . '/ui/files-actions-menu', 'scope_group' => 1, 'name' => 'BASIC', 'user_check' => 0],
['api_route' => $aeApiV1Prefix . '/ui/top-menu', 'scope_group' => 1, 'name' => 'BASIC', 'user_check' => 0],
['api_route' => $aeApiV1Prefix . '/ui/initial-state', 'scope_group' => 1, 'name' => 'BASIC', 'user_check' => 0],
['api_route' => $aeApiV1Prefix . '/ui/script', 'scope_group' => 1, 'name' => 'BASIC', 'user_check' => 0],
diff --git a/lib/Service/UI/FilesActionsMenuService.php b/lib/Service/UI/FilesActionsMenuService.php
index 5e6eacdb..693f1f2a 100644
--- a/lib/Service/UI/FilesActionsMenuService.php
+++ b/lib/Service/UI/FilesActionsMenuService.php
@@ -36,10 +36,11 @@ public function __construct(
* @param string $mime
* @param int $permissions
* @param int $order
+ * @param string $version
* @return FilesActionsMenu|null
*/
public function registerFileActionMenu(string $appId, string $name, string $displayName, string $actionHandler,
- string $icon, string $mime, int $permissions, int $order): ?FilesActionsMenu {
+ string $icon, string $mime, int $permissions, int $order, string $version): ?FilesActionsMenu {
try {
$fileActionMenu = $this->mapper->findByAppidName($appId, $name);
} catch (DoesNotExistException|MultipleObjectsReturnedException|Exception) {
@@ -55,6 +56,7 @@ public function registerFileActionMenu(string $appId, string $name, string $disp
'mime' => $mime,
'permissions' => $permissions,
'order' => $order,
+ 'version' => $version,
]);
if ($fileActionMenu !== null) {
$newFileActionMenu->setId($fileActionMenu->getId());
diff --git a/src/filesplugin28.js b/src/filesplugin28.js
index 7832f656..1f606322 100644
--- a/src/filesplugin28.js
+++ b/src/filesplugin28.js
@@ -29,6 +29,10 @@ function generateAppAPIProxyUrl(appId, route) {
return generateUrl(`/apps/app_api/proxy/${appId}/${route}`)
}
+function generateExAppUIPageUrl(appId, route) {
+ return generateUrl(`/apps/app_api/embedded/${appId}/${route}`)
+}
+
function registerFileAction28(fileAction, inlineSvgIcon) {
const action = new FileAction({
id: fileAction.name,
@@ -61,32 +65,48 @@ function registerFileAction28(fileAction, inlineSvgIcon) {
},
async exec(node, view, dir) {
const exAppFileActionHandler = generateAppAPIProxyUrl(fileAction.appid, fileAction.action_handler)
- return axios.post(exAppFileActionHandler, {
- fileId: node.fileid,
- name: node.basename,
- directory: node.dirname,
- etag: node.attributes.etag,
- mime: node.mime,
- favorite: Boolean(node.attributes.favorite).toString(),
- permissions: node.permissions,
- fileType: node.type,
- size: Number(node.size),
- mtime: new Date(node.mtime).getTime() / 1000, // convert ms to s
- shareTypes: node.attributes.shareTypes || null,
- shareAttributes: node.attributes.shareAttributes || null,
- sharePermissions: node.attributes.sharePermissions || null,
- shareOwner: node.attributes.ownerDisplayName || null,
- shareOwnerId: node.attributes.ownerId || null,
- userId: getCurrentUser().uid,
- instanceId: state.instanceId,
- }).then((response) => {
- return true
- }).catch((error) => {
- console.error('Failed to send FileAction request to ExApp', error)
- return false
- })
+ if ('version' in fileAction && fileAction.version === '2.0') {
+ return axios.post(exAppFileActionHandler, { files: [buildNodeInfo(node)] })
+ .then((response) => {
+ if (typeof response.data === 'object' && 'redirect_handler' in response.data) {
+ const redirectPage = generateExAppUIPageUrl(fileAction.appid, response.data.redirect_handler)
+ window.location.assign(`${redirectPage}?fileIds=${node.fileid}`)
+ return true
+ }
+ return true
+ }).catch((error) => {
+ console.error('Failed to send FileAction request to ExApp', error)
+ return false
+ })
+ }
+ return axios.post(exAppFileActionHandler, buildNodeInfo(node))
+ .then(() => {
+ return true
+ })
+ .catch((error) => {
+ console.error('Failed to send FileAction request to ExApp', error)
+ return false
+ })
},
async execBatch(nodes, view, dir) {
+ if ('version' in fileAction && fileAction.version === '2.0') {
+ const exAppFileActionHandler = generateAppAPIProxyUrl(fileAction.appid, fileAction.action_handler)
+ const nodesDataList = nodes.map(buildNodeInfo)
+ return axios.post(exAppFileActionHandler, { files: nodesDataList })
+ .then((response) => {
+ if (typeof response.data === 'object' && 'redirect_handler' in response.data) {
+ const redirectPage = generateExAppUIPageUrl(fileAction.appid, response.data.redirect_handler)
+ const fileIds = nodes.map((node) => node.fileid).join(',')
+ window.location.assign(`${redirectPage}?fileIds=${fileIds}`)
+ }
+ return nodes.map(_ => true)
+ })
+ .catch((error) => {
+ console.error('Failed to send FileAction request to ExApp', error)
+ return nodes.map(_ => false)
+ })
+ }
+ // for version 1.0 behavior is not changed
return Promise.all(nodes.map((node) => {
return this.exec(node, view, dir)
}))
@@ -95,6 +115,28 @@ function registerFileAction28(fileAction, inlineSvgIcon) {
registerFileAction(action)
}
+function buildNodeInfo(node) {
+ return {
+ fileId: node.fileid,
+ name: node.basename,
+ directory: node.dirname,
+ etag: node.attributes.etag,
+ mime: node.mime,
+ favorite: Boolean(node.attributes.favorite).toString(),
+ permissions: node.permissions,
+ fileType: node.type,
+ size: Number(node.size),
+ mtime: new Date(node.mtime).getTime() / 1000, // convert ms to s
+ shareTypes: node.attributes.shareTypes || null,
+ shareAttributes: node.attributes.shareAttributes || null,
+ sharePermissions: node.attributes.sharePermissions || null,
+ shareOwner: node.attributes.ownerDisplayName || null,
+ shareOwnerId: node.attributes.ownerId || null,
+ userId: getCurrentUser().uid,
+ instanceId: state.instanceId,
+ }
+}
+
document.addEventListener('DOMContentLoaded', () => {
state.fileActions.forEach(fileAction => {
if (fileAction.icon === '') {