Skip to content

Commit d233d01

Browse files
committed
Support for OCS UI endpoints, updated UI example
Signed-off-by: Alexander Piskun <bigcat88@icloud.com>
1 parent ded86f5 commit d233d01

File tree

12 files changed

+548
-60
lines changed

12 files changed

+548
-60
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22

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

5-
## [0.5.2 - 2023-11-xx]
5+
## [0.6.0 - 2023-12-0x]
6+
7+
### Added
8+
9+
- Ability to develop applications with `UI`, example of such app, support for all new stuff of `AppAPI 1.4`. #168
610

711
### Fixed
812

docs/NextcloudApp.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ an empty response (which will be a status of 200) and in the background already
195195
The last parameter is a structure describing the action and the file on which it needs to be performed,
196196
which is passed by the AppAPI when clicking on the drop-down context menu of the file.
197197

198-
We use the built method :py:meth:`~nc_py_api.ex_app.ui.files.UiActionFileInfo.to_fs_node` into the structure to convert it
198+
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
199199
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.
200200

201201
In the **convert_video_to_gif** function, a standard conversion using ``OpenCV`` from a video file to a GIF image occurs,

docs/reference/ExApp.rst

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,26 @@ UI methods should be accessed with the help of :class:`~nc_py_api.nextcloud.Next
3333
.. autoclass:: nc_py_api.ex_app.ui.ui.UiApi
3434
:members:
3535

36-
.. automodule:: nc_py_api.ex_app.ui.files
36+
.. automodule:: nc_py_api.ex_app.ui.files_actions
3737
:members:
3838

39-
.. autoclass:: nc_py_api.ex_app.ui.files._UiFilesActionsAPI
39+
.. autoclass:: nc_py_api.ex_app.ui.files_actions._UiFilesActionsAPI
40+
:members:
41+
42+
.. automodule:: nc_py_api.ex_app.ui.top_menu
43+
:members:
44+
45+
.. autoclass:: nc_py_api.ex_app.ui.top_menu._UiTopMenuAPI
46+
:members:
47+
48+
.. autoclass:: nc_py_api.ex_app.ui.resources._UiResources
49+
:members:
50+
51+
.. autoclass:: nc_py_api.ex_app.ui.resources.UiInitState
52+
:members:
53+
54+
.. autoclass:: nc_py_api.ex_app.ui.resources.UiScript
55+
:members:
56+
57+
.. autoclass:: nc_py_api.ex_app.ui.resources.UiStyle
4058
:members:

examples/as_app/ui_example/lib/main.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,14 @@ async def lifespan(_app: FastAPI):
2020
APP = FastAPI(lifespan=lifespan)
2121

2222

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

2733

nc_py_api/ex_app/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
from .defs import ApiScope, LogLvl
44
from .integration_fastapi import nc_app, set_handlers, talk_bot_app
55
from .misc import persistent_storage, verify_version
6-
from .ui.files import UiActionFileInfo, UiFileActionHandlerInfo
6+
from .ui.files_actions import UiActionFileInfo, UiFileActionHandlerInfo
77
from .uvicorn_fastapi import run_app

nc_py_api/ex_app/ui/files.py renamed to nc_py_api/ex_app/ui/files_actions.py

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
"""Nextcloud API for working with drop-down file's menu."""
22

3+
import dataclasses
4+
import datetime
35
import os
46
import typing
5-
from datetime import datetime, timezone
67

78
from pydantic import BaseModel
89

@@ -12,6 +13,57 @@
1213
from ...files import FsNode, permissions_to_str
1314

1415

16+
@dataclasses.dataclass
17+
class UiFileActionEntry:
18+
"""Files app, right click file action entry description."""
19+
20+
def __init__(self, raw_data: dict):
21+
self._raw_data = raw_data
22+
23+
@property
24+
def appid(self) -> str:
25+
"""App ID for which this entry is."""
26+
return self._raw_data["appid"]
27+
28+
@property
29+
def name(self) -> str:
30+
"""File action name, acts like ID."""
31+
return self._raw_data["name"]
32+
33+
@property
34+
def display_name(self) -> str:
35+
"""Display name of the entry."""
36+
return self._raw_data["display_name"]
37+
38+
@property
39+
def mime(self) -> str:
40+
"""For which file types this entry applies."""
41+
return self._raw_data["mime"]
42+
43+
@property
44+
def permissions(self) -> int:
45+
"""For which file permissions this entry applies."""
46+
return int(self._raw_data["permissions"])
47+
48+
@property
49+
def order(self) -> int:
50+
"""Order of the entry in the file action list."""
51+
return int(self._raw_data["order"])
52+
53+
@property
54+
def icon(self) -> str:
55+
"""-no description-."""
56+
return self._raw_data["icon"]
57+
58+
@property
59+
def action_handler(self) -> str:
60+
"""Relative ExApp url which will be called if user click on the entry."""
61+
return self._raw_data["action_handler"]
62+
63+
def __repr__(self):
64+
return f"<{self.__class__.__name__} name={self.name}, mime={self.mime}, handler={self.action_handler}>"
65+
66+
1567
class UiActionFileInfo(BaseModel):
1668
"""File Information Nextcloud sends to the External Application."""
1769

@@ -62,7 +114,7 @@ def to_fs_node(self) -> FsNode:
62114
favorite=bool(self.favorite.lower() == "true"),
63115
file_id=file_id + self.instanceId if self.instanceId else file_id,
64116
fileid=self.fileId,
65-
last_modified=datetime.utcfromtimestamp(self.mtime).replace(tzinfo=timezone.utc),
117+
last_modified=datetime.datetime.utcfromtimestamp(self.mtime).replace(tzinfo=datetime.timezone.utc),
66118
mimetype=self.mime,
67119
)
68120

@@ -112,3 +164,13 @@ def unregister(self, name: str, not_fail=True) -> None:
112164
except NextcloudExceptionNotFound as e:
113165
if not not_fail:
114166
raise e from None
167+
168+
def get_entry(self, name: str) -> typing.Optional[UiFileActionEntry]:
169+
"""Get information of the file action meny entry for current app."""
170+
require_capabilities("app_api", self._session.capabilities)
171+
try:
172+
return UiFileActionEntry(
173+
self._session.ocs(method="GET", path=f"{self._session.ae_url}/{self._ep_suffix}", params={"name": name})
174+
)
175+
except NextcloudExceptionNotFound:
176+
return None

nc_py_api/ex_app/ui/resources.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
"""API for adding scripts, styles, initial-states to the Nextcloud UI."""
2+
3+
import dataclasses
4+
import typing
5+
6+
from ..._exceptions import NextcloudExceptionNotFound
7+
from ..._misc import require_capabilities
8+
from ..._session import NcSessionApp
9+
10+
11+
@dataclasses.dataclass
12+
class UiBase:
13+
"""Basic class for InitialStates, Scripts, Styles."""
14+
15+
def __init__(self, raw_data: dict):
16+
self._raw_data = raw_data
17+
18+
@property
19+
def appid(self) -> str:
20+
"""The App ID of the owner of this UI."""
21+
return self._raw_data["appid"]
22+
23+
@property
24+
def ui_type(self) -> str:
25+
"""UI type. Possible values: 'top_menu'."""
26+
return self._raw_data["type"]
27+
28+
@property
29+
def name(self) -> str:
30+
"""UI page name, acts like ID."""
31+
return self._raw_data["name"]
32+
33+
34+
class UiInitState(UiBase):
35+
"""One Initial State description."""
36+
37+
@property
38+
def key(self) -> str:
39+
"""Name of the object."""
40+
return self._raw_data["key"]
41+
42+
@property
43+
def value(self) -> typing.Union[dict, list]:
44+
"""Object for the page(template)."""
45+
return self._raw_data["value"]
46+
47+
def __repr__(self):
48+
return f"<{self.__class__.__name__} type={self.ui_type}, name={self.name}, key={self.key}>"
49+
50+
51+
class UiScript(UiBase):
52+
"""One Script description."""
53+
54+
@property
55+
def path(self) -> str:
56+
"""Url to script relative to the ExApp."""
57+
return self._raw_data["path"]
58+
59+
@property
60+
def after_app_id(self) -> str:
61+
"""Optional AppID after which script should be injected."""
62+
return self._raw_data["after_app_id"] if self._raw_data["after_app_id"] else ""
63+
64+
def __repr__(self):
65+
return f"<{self.__class__.__name__} type={self.ui_type}, name={self.name}, path={self.path}>"
66+
67+
68+
class UiStyle(UiBase):
69+
"""One Style description."""
70+
71+
@property
72+
def path(self) -> str:
73+
"""Url to style relative to the ExApp."""
74+
return self._raw_data["path"]
75+
76+
def __repr__(self):
77+
return f"<{self.__class__.__name__} type={self.ui_type}, name={self.name}, path={self.path}>"
78+
79+
80+
class _UiResources:
81+
"""API for adding scripts, styles, initial-states to the TopMenu pages."""
82+
83+
_ep_suffix_init_state: str = "ui/initial-state"
84+
_ep_suffix_js: str = "ui/script"
85+
_ep_suffix_css: str = "ui/style"
86+
87+
def __init__(self, session: NcSessionApp):
88+
self._session = session
89+
90+
def set_initial_state(self, ui_type: str, name: str, key: str, value: typing.Union[dict, list]) -> None:
91+
"""Add or update initial state for the page(template)."""
92+
require_capabilities("app_api", self._session.capabilities)
93+
params = {
94+
"type": ui_type,
95+
"name": name,
96+
"key": key,
97+
"value": value,
98+
}
99+
self._session.ocs(method="POST", path=f"{self._session.ae_url}/{self._ep_suffix_init_state}", json=params)
100+
101+
def delete_initial_state(self, ui_type: str, name: str, key: str, not_fail=True) -> None:
102+
"""Removes initial state for the page(template) by object name."""
103+
require_capabilities("app_api", self._session.capabilities)
104+
try:
105+
self._session.ocs(
106+
method="DELETE",
107+
path=f"{self._session.ae_url}/{self._ep_suffix_init_state}",
108+
params={"type": ui_type, "name": name, "key": key},
109+
)
110+
except NextcloudExceptionNotFound as e:
111+
if not not_fail:
112+
raise e from None
113+
114+
def get_initial_state(self, ui_type: str, name: str, key: str) -> typing.Optional[UiInitState]:
115+
"""Get information about initial state for the page(template) by object name."""
116+
require_capabilities("app_api", self._session.capabilities)
117+
try:
118+
return UiInitState(
119+
self._session.ocs(
120+
method="GET",
121+
path=f"{self._session.ae_url}/{self._ep_suffix_init_state}",
122+
params={"type": ui_type, "name": name, "key": key},
123+
)
124+
)
125+
except NextcloudExceptionNotFound:
126+
return None
127+
128+
def set_script(self, ui_type: str, name: str, path: str, after_app_id: str = "") -> None:
129+
"""Add or update script for the page(template)."""
130+
require_capabilities("app_api", self._session.capabilities)
131+
params = {
132+
"type": ui_type,
133+
"name": name,
134+
"path": path,
135+
"afterAppId": after_app_id,
136+
}
137+
self._session.ocs(method="POST", path=f"{self._session.ae_url}/{self._ep_suffix_js}", json=params)
138+
139+
def delete_script(self, ui_type: str, name: str, path: str, not_fail=True) -> None:
140+
"""Removes script for the page(template) by object name."""
141+
require_capabilities("app_api", self._session.capabilities)
142+
try:
143+
self._session.ocs(
144+
method="DELETE",
145+
path=f"{self._session.ae_url}/{self._ep_suffix_js}",
146+
params={"type": ui_type, "name": name, "path": path},
147+
)
148+
except NextcloudExceptionNotFound as e:
149+
if not not_fail:
150+
raise e from None
151+
152+
def get_script(self, ui_type: str, name: str, path: str) -> typing.Optional[UiScript]:
153+
"""Get information about script for the page(template) by object name."""
154+
require_capabilities("app_api", self._session.capabilities)
155+
try:
156+
return UiScript(
157+
self._session.ocs(
158+
method="GET",
159+
path=f"{self._session.ae_url}/{self._ep_suffix_js}",
160+
params={"type": ui_type, "name": name, "path": path},
161+
)
162+
)
163+
except NextcloudExceptionNotFound:
164+
return None
165+
166+
def set_style(self, ui_type: str, name: str, path: str) -> None:
167+
"""Add or update style(css) for the page(template)."""
168+
require_capabilities("app_api", self._session.capabilities)
169+
params = {
170+
"type": ui_type,
171+
"name": name,
172+
"path": path,
173+
}
174+
self._session.ocs(method="POST", path=f"{self._session.ae_url}/{self._ep_suffix_css}", json=params)
175+
176+
def delete_style(self, ui_type: str, name: str, path: str, not_fail=True) -> None:
177+
"""Removes style(css) for the page(template) by object name."""
178+
require_capabilities("app_api", self._session.capabilities)
179+
try:
180+
self._session.ocs(
181+
method="DELETE",
182+
path=f"{self._session.ae_url}/{self._ep_suffix_css}",
183+
params={"type": ui_type, "name": name, "path": path},
184+
)
185+
except NextcloudExceptionNotFound as e:
186+
if not not_fail:
187+
raise e from None
188+
189+
def get_style(self, ui_type: str, name: str, path: str) -> typing.Optional[UiStyle]:
190+
"""Get information about style(css) for the page(template) by object name."""
191+
require_capabilities("app_api", self._session.capabilities)
192+
try:
193+
return UiStyle(
194+
self._session.ocs(
195+
method="GET",
196+
path=f"{self._session.ae_url}/{self._ep_suffix_css}",
197+
params={"type": ui_type, "name": name, "path": path},
198+
)
199+
)
200+
except NextcloudExceptionNotFound:
201+
return None

0 commit comments

Comments
 (0)