Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: file actions redirect, v2 api version #284

Merged
merged 16 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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.*

]]></description>
<version>2.5.1</version>
<version>2.6.0</version>
<licence>agpl</licence>
<author mail="[email protected]" homepage="https://github.com/andrey18106">Andrey Borysenko</author>
<author mail="[email protected]" homepage="https://github.com/bigcat88">Alexander Piskun</author>
Expand Down
1 change: 1 addition & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
andrey18106 marked this conversation as resolved.
Show resolved Hide resolved
['name' => 'OCSUi#unregisterFileActionMenu', 'url' => '/api/v1/ui/files-actions-menu', 'verb' => 'DELETE'],
['name' => 'OCSUi#getFileActionMenu', 'url' => '/api/v1/ui/files-actions-menu', 'verb' => 'GET'],

Expand Down
31 changes: 28 additions & 3 deletions docs/tech_details/api/fileactionsmenu.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
******

Expand All @@ -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
***************

Expand Down Expand Up @@ -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 <https://github.com/cloud-py-api/ui_example>`_.


Request flow
^^^^^^^^^^^^
Expand Down Expand Up @@ -126,5 +151,5 @@ Examples

Here is a list of simple example ExApps based on FileActionsMenu:

* `video_to_gif <https://github.com/cloud-py-api/nc_py_api/tree/main/examples/as_app/to_gif>`_ - ExApp based on FileActionsMenu to convert videos to gif in place
* `upscaler_demo <https://github.com/cloud-py-api/upscaler_example.git>`_ - ExApp based on FileActionsMenu to upscale image in place
* `to_gif <https://github.com/cloud-py-api/nc_py_api/tree/main/examples/as_app/to_gif>`_ - ExApp based on FileActionsMenu to convert videos to gif in place
* `upscaler_example <https://github.com/cloud-py-api/upscaler_example.git>`_ - ExApp based on FileActionsMenu to upscale image in place
21 changes: 20 additions & 1 deletion lib/Controller/OCSUiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public function __construct(

/**
* @throws OCSBadRequestException
*
* @depreacted since AppAPI 2.6.0, use registerFileActionMenuV2 instead
*/
#[AppAPIAuth]
#[PublicPage]
Expand All @@ -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");
}
Expand Down
8 changes: 8 additions & 0 deletions lib/Db/UI/FilesActionsMenu.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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;
Expand All @@ -38,6 +40,7 @@ class FilesActionsMenu extends Entity implements JsonSerializable {
protected $order;
protected $icon;
protected $actionHandler;
protected $version;

/**
* @param array $params
Expand All @@ -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']);
Expand Down Expand Up @@ -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 {
Expand All @@ -92,6 +99,7 @@ public function jsonSerialize(): array {
'order' => $this->getOrder(),
'icon' => $this->getIcon(),
'action_handler' => $this->getActionHandler(),
'version' => $this->getVersion(),
];
}
}
37 changes: 37 additions & 0 deletions lib/Migration/Version2206Date20240502145029.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace OCA\AppAPI\Migration;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

class Version2206Date20240502145029 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
*
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();

if ($schema->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;
}
}
4 changes: 3 additions & 1 deletion lib/Service/UI/FilesActionsMenuService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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());
Expand Down
81 changes: 57 additions & 24 deletions src/filesplugin28.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -61,32 +65,39 @@ 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
})
return axios.post(exAppFileActionHandler, buildNodeInfo(node))
.then((response) => {
if ('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
})
},
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 ('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 true
}
return true
})
.catch((error) => {
console.error('Failed to send FileAction request to ExApp', error)
return false
})
}
// for version 1.0 behavior is not changed
return Promise.all(nodes.map((node) => {
return this.exec(node, view, dir)
}))
Expand All @@ -95,6 +106,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 === '') {
Expand Down
Loading