Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Support for OCS UI endpoints, updated UI example
Signed-off-by: Alexander Piskun <bigcat88@icloud.com>
  • Loading branch information
bigcat88 committed Dec 3, 2023
commit d233d018e9b1ef70798094476a69f93f4fd4266b
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

All notable changes to this project will be documented in this file.

## [0.5.2 - 2023-11-xx]
## [0.6.0 - 2023-12-0x]

### Added

- Ability to develop applications with `UI`, example of such app, support for all new stuff of `AppAPI 1.4`. #168

### Fixed

Expand Down
2 changes: 1 addition & 1 deletion docs/NextcloudApp.rst
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ an empty response (which will be a status of 200) and in the background already
The last parameter is a structure describing the action and the file on which it needs to be performed,
which is passed by the AppAPI when clicking on the drop-down context menu of the file.

We use the built method :py:meth:`~nc_py_api.ex_app.ui.files.UiActionFileInfo.to_fs_node` into the structure to convert it
We use the built method :py:meth:`~nc_py_api.ex_app.ui.files_actions.UiActionFileInfo.to_fs_node` into the structure to convert it
into a standard :py:class:`~nc_py_api.files.FsNode` class that describes the file and pass the FsNode class instance to the background task.

In the **convert_video_to_gif** function, a standard conversion using ``OpenCV`` from a video file to a GIF image occurs,
Expand Down
22 changes: 20 additions & 2 deletions docs/reference/ExApp.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,26 @@ UI methods should be accessed with the help of :class:`~nc_py_api.nextcloud.Next
.. autoclass:: nc_py_api.ex_app.ui.ui.UiApi
:members:

.. automodule:: nc_py_api.ex_app.ui.files
.. automodule:: nc_py_api.ex_app.ui.files_actions
:members:

.. autoclass:: nc_py_api.ex_app.ui.files._UiFilesActionsAPI
.. autoclass:: nc_py_api.ex_app.ui.files_actions._UiFilesActionsAPI
:members:

.. automodule:: nc_py_api.ex_app.ui.top_menu
:members:

.. autoclass:: nc_py_api.ex_app.ui.top_menu._UiTopMenuAPI
:members:

.. autoclass:: nc_py_api.ex_app.ui.resources._UiResources
:members:

.. autoclass:: nc_py_api.ex_app.ui.resources.UiInitState
:members:

.. autoclass:: nc_py_api.ex_app.ui.resources.UiScript
:members:

.. autoclass:: nc_py_api.ex_app.ui.resources.UiStyle
:members:
8 changes: 7 additions & 1 deletion examples/as_app/ui_example/lib/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,14 @@ async def lifespan(_app: FastAPI):
APP = FastAPI(lifespan=lifespan)


def enabled_handler(enabled: bool, _nc: NextcloudApp) -> str:
def enabled_handler(enabled: bool, nc: NextcloudApp) -> str:
print(f"enabled={enabled}")
if enabled:
nc.ui.resources.set_initial_state(
"top_menu", "first_menu", "ui_example_state", {"initial_value": "test init value"}
)
nc.ui.resources.set_script("top_menu", "first_menu", "js/ui_example-main")
nc.ui.top_menu.register("first_menu", "UI example", "img/icon.svg")
return ""


Expand Down
2 changes: 1 addition & 1 deletion nc_py_api/ex_app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
from .defs import ApiScope, LogLvl
from .integration_fastapi import nc_app, set_handlers, talk_bot_app
from .misc import persistent_storage, verify_version
from .ui.files import UiActionFileInfo, UiFileActionHandlerInfo
from .ui.files_actions import UiActionFileInfo, UiFileActionHandlerInfo
from .uvicorn_fastapi import run_app
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""Nextcloud API for working with drop-down file's menu."""

import dataclasses
import datetime
import os
import typing
from datetime import datetime, timezone

from pydantic import BaseModel

Expand All @@ -12,6 +13,57 @@
from ...files import FsNode, permissions_to_str


@dataclasses.dataclass
class UiFileActionEntry:
"""Files app, right click file action entry description."""

def __init__(self, raw_data: dict):
self._raw_data = raw_data

@property
def appid(self) -> str:
"""App ID for which this entry is."""
return self._raw_data["appid"]

@property
def name(self) -> str:
"""File action name, acts like ID."""
return self._raw_data["name"]

@property
def display_name(self) -> str:
"""Display name of the entry."""
return self._raw_data["display_name"]

@property
def mime(self) -> str:
"""For which file types this entry applies."""
return self._raw_data["mime"]

@property
def permissions(self) -> int:
"""For which file permissions this entry applies."""
return int(self._raw_data["permissions"])

@property
def order(self) -> int:
"""Order of the entry in the file action list."""
return int(self._raw_data["order"])

@property
def icon(self) -> str:
"""-no description-."""
return self._raw_data["icon"]

@property
def action_handler(self) -> str:
"""Relative ExApp url which will be called if user click on the entry."""
return self._raw_data["action_handler"]

def __repr__(self):
return f"<{self.__class__.__name__} name={self.name}, mime={self.mime}, handler={self.action_handler}>"


class UiActionFileInfo(BaseModel):
"""File Information Nextcloud sends to the External Application."""

Expand Down Expand Up @@ -62,7 +114,7 @@ def to_fs_node(self) -> FsNode:
favorite=bool(self.favorite.lower() == "true"),
file_id=file_id + self.instanceId if self.instanceId else file_id,
fileid=self.fileId,
last_modified=datetime.utcfromtimestamp(self.mtime).replace(tzinfo=timezone.utc),
last_modified=datetime.datetime.utcfromtimestamp(self.mtime).replace(tzinfo=datetime.timezone.utc),
mimetype=self.mime,
)

Expand Down Expand Up @@ -112,3 +164,13 @@ def unregister(self, name: str, not_fail=True) -> None:
except NextcloudExceptionNotFound as e:
if not not_fail:
raise e from None

def get_entry(self, name: str) -> typing.Optional[UiFileActionEntry]:
"""Get information of the file action meny entry for current app."""
require_capabilities("app_api", self._session.capabilities)
try:
return UiFileActionEntry(
self._session.ocs(method="GET", path=f"{self._session.ae_url}/{self._ep_suffix}", params={"name": name})
)
except NextcloudExceptionNotFound:
return None
201 changes: 201 additions & 0 deletions nc_py_api/ex_app/ui/resources.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
"""API for adding scripts, styles, initial-states to the Nextcloud UI."""

import dataclasses
import typing

from ..._exceptions import NextcloudExceptionNotFound
from ..._misc import require_capabilities
from ..._session import NcSessionApp


@dataclasses.dataclass
class UiBase:
"""Basic class for InitialStates, Scripts, Styles."""

def __init__(self, raw_data: dict):
self._raw_data = raw_data

@property
def appid(self) -> str:
"""The App ID of the owner of this UI."""
return self._raw_data["appid"]

@property
def ui_type(self) -> str:
"""UI type. Possible values: 'top_menu'."""
return self._raw_data["type"]

@property
def name(self) -> str:
"""UI page name, acts like ID."""
return self._raw_data["name"]


class UiInitState(UiBase):
"""One Initial State description."""

@property
def key(self) -> str:
"""Name of the object."""
return self._raw_data["key"]

@property
def value(self) -> typing.Union[dict, list]:
"""Object for the page(template)."""
return self._raw_data["value"]

def __repr__(self):
return f"<{self.__class__.__name__} type={self.ui_type}, name={self.name}, key={self.key}>"


class UiScript(UiBase):
"""One Script description."""

@property
def path(self) -> str:
"""Url to script relative to the ExApp."""
return self._raw_data["path"]

@property
def after_app_id(self) -> str:
"""Optional AppID after which script should be injected."""
return self._raw_data["after_app_id"] if self._raw_data["after_app_id"] else ""

def __repr__(self):
return f"<{self.__class__.__name__} type={self.ui_type}, name={self.name}, path={self.path}>"


class UiStyle(UiBase):
"""One Style description."""

@property
def path(self) -> str:
"""Url to style relative to the ExApp."""
return self._raw_data["path"]

def __repr__(self):
return f"<{self.__class__.__name__} type={self.ui_type}, name={self.name}, path={self.path}>"


class _UiResources:
"""API for adding scripts, styles, initial-states to the TopMenu pages."""

_ep_suffix_init_state: str = "ui/initial-state"
_ep_suffix_js: str = "ui/script"
_ep_suffix_css: str = "ui/style"

def __init__(self, session: NcSessionApp):
self._session = session

def set_initial_state(self, ui_type: str, name: str, key: str, value: typing.Union[dict, list]) -> None:
"""Add or update initial state for the page(template)."""
require_capabilities("app_api", self._session.capabilities)
params = {
"type": ui_type,
"name": name,
"key": key,
"value": value,
}
self._session.ocs(method="POST", path=f"{self._session.ae_url}/{self._ep_suffix_init_state}", json=params)

def delete_initial_state(self, ui_type: str, name: str, key: str, not_fail=True) -> None:
"""Removes initial state for the page(template) by object name."""
require_capabilities("app_api", self._session.capabilities)
try:
self._session.ocs(
method="DELETE",
path=f"{self._session.ae_url}/{self._ep_suffix_init_state}",
params={"type": ui_type, "name": name, "key": key},
)
except NextcloudExceptionNotFound as e:
if not not_fail:
raise e from None

def get_initial_state(self, ui_type: str, name: str, key: str) -> typing.Optional[UiInitState]:
"""Get information about initial state for the page(template) by object name."""
require_capabilities("app_api", self._session.capabilities)
try:
return UiInitState(
self._session.ocs(
method="GET",
path=f"{self._session.ae_url}/{self._ep_suffix_init_state}",
params={"type": ui_type, "name": name, "key": key},
)
)
except NextcloudExceptionNotFound:
return None

def set_script(self, ui_type: str, name: str, path: str, after_app_id: str = "") -> None:
"""Add or update script for the page(template)."""
require_capabilities("app_api", self._session.capabilities)
params = {
"type": ui_type,
"name": name,
"path": path,
"afterAppId": after_app_id,
}
self._session.ocs(method="POST", path=f"{self._session.ae_url}/{self._ep_suffix_js}", json=params)

def delete_script(self, ui_type: str, name: str, path: str, not_fail=True) -> None:
"""Removes script for the page(template) by object name."""
require_capabilities("app_api", self._session.capabilities)
try:
self._session.ocs(
method="DELETE",
path=f"{self._session.ae_url}/{self._ep_suffix_js}",
params={"type": ui_type, "name": name, "path": path},
)
except NextcloudExceptionNotFound as e:
if not not_fail:
raise e from None

def get_script(self, ui_type: str, name: str, path: str) -> typing.Optional[UiScript]:
"""Get information about script for the page(template) by object name."""
require_capabilities("app_api", self._session.capabilities)
try:
return UiScript(
self._session.ocs(
method="GET",
path=f"{self._session.ae_url}/{self._ep_suffix_js}",
params={"type": ui_type, "name": name, "path": path},
)
)
except NextcloudExceptionNotFound:
return None

def set_style(self, ui_type: str, name: str, path: str) -> None:
"""Add or update style(css) for the page(template)."""
require_capabilities("app_api", self._session.capabilities)
params = {
"type": ui_type,
"name": name,
"path": path,
}
self._session.ocs(method="POST", path=f"{self._session.ae_url}/{self._ep_suffix_css}", json=params)

def delete_style(self, ui_type: str, name: str, path: str, not_fail=True) -> None:
"""Removes style(css) for the page(template) by object name."""
require_capabilities("app_api", self._session.capabilities)
try:
self._session.ocs(
method="DELETE",
path=f"{self._session.ae_url}/{self._ep_suffix_css}",
params={"type": ui_type, "name": name, "path": path},
)
except NextcloudExceptionNotFound as e:
if not not_fail:
raise e from None

def get_style(self, ui_type: str, name: str, path: str) -> typing.Optional[UiStyle]:
"""Get information about style(css) for the page(template) by object name."""
require_capabilities("app_api", self._session.capabilities)
try:
return UiStyle(
self._session.ocs(
method="GET",
path=f"{self._session.ae_url}/{self._ep_suffix_css}",
params={"type": ui_type, "name": name, "path": path},
)
)
except NextcloudExceptionNotFound:
return None
Loading