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

Introduced UIResourceComponent, clarified its API #118

Merged
Merged
Show file tree
Hide file tree
Changes from all 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
34 changes: 20 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ functions to render layouts from model configuration.

See also [JinjaX documentation](https://jinjax.scaletti.dev/).

Oarepo supports the use of components within Jinja templates using the JinjaX library.
Oarepo builds its static UI pages on top of the JinjaX library.
To load a Jinja application, a JinjaX component is expected on the input.
The relative path to the component is taken from the configuration

Expand All @@ -27,20 +27,14 @@ To work with parameters within components, you need to define them in the templa

Example of component specification in config:

```json
```python
templates = {
"detail": {
"layout": "docs_app/DetailRoot.html.jinja",
"blocks": {
"record_main_content": "Main",
"record_sidebar": "Sidebar",
},
},
"search": {"layout": "docs_app/search.html"},
"detail": "DetailPage",
"search": "SearchPage"
}
```

Example of possible contents of the DetailRoot component:
Example of possible contents of the DetailPage component, contained inside `templates/DetailPage.jinja`

```json
{#def metadata, ui, layout #}
Expand All @@ -53,18 +47,17 @@ Example of possible contents of the DetailRoot component:
{{ webpack['docs_app_components.css']}}

{%- endblock %}
```

Based on the definition from the config, the block content is then automatically added to the component content:
```json
{% block record_main_content %}
<Main metadata={{metadata}}></Main>
{% endblock %}

{% block record_sidebar %}
<Sidebar metadata={{metadata}}></Sidebar>
{% endblock %}
```


Sample of possible contents of Main component:
```json
{#def metadata, ui, layout #}
Expand All @@ -74,9 +67,22 @@ Sample of possible contents of Main component:

```

You can also namespace your ui components, by using dot notation:

```python
templates = {
"detail": "myrepo.DetailPage",
"search": "myrepo.SearchPage"
}
```

Then, the component will be loaded from the `templates/myrepo/DetailPage.jinja` file.


#### JinjaX components

Within the Oarepo-ui library, basic components are defined in the `templates` folder.

### React

To render a custom layout in a React app (e. g. records search result page), this package provides the `useLayout` hook and an entrypoint
Expand Down
13 changes: 13 additions & 0 deletions oarepo_ui/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,16 @@
THEME_HEADER_LOGIN_TEMPLATE = "oarepo_ui/header_login.html"

OAREPO_UI_JINJAX_FILTERS = {}

OAREPO_UI_RECORD_ACTIONS = [
"edit",
"new_version",
"manage",
"update_draft",
"read_files",
"review",
"view",
"delete_draft",
"manage_files",
"manage_record_access",
]
9 changes: 8 additions & 1 deletion oarepo_ui/ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,10 @@ def _catalog_config(self, catalog, env):
env.policies.setdefault("json.dumps_kwargs", {}).setdefault("default", str)
self.app.update_template_context(context)
catalog.jinja_env.loader = env.loader
catalog.jinja_env.autoescape = env.autoescape

# autoescape everything (this catalogue is used just for html jinjax components, so can do that) ...
catalog.jinja_env.autoescape = True

context.update(catalog.jinja_env.globals)
context.update(env.globals)
catalog.jinja_env.globals = context
Expand Down Expand Up @@ -144,6 +147,10 @@ def development_after_request(self, response: Response):

return add_vite_tags(response)

@property
def record_actions(self):
return self.app.config["OAREPO_UI_RECORD_ACTIONS"]


class OARepoUIExtension:
def __init__(self, app=None):
Expand Down
149 changes: 83 additions & 66 deletions oarepo_ui/resources/catalog.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import os
import re
from functools import cached_property
from pathlib import Path
from typing import Dict, Tuple

import jinja2
from jinjax import Catalog
Expand Down Expand Up @@ -29,52 +31,91 @@ def get_source(self, cname: str, file_ext: "TFileExt" = "") -> str:
_root_path, path = self._get_component_path(prefix, name, file_ext=file_ext)
return Path(path).read_text()

@cached_property
def component_paths(self) -> Dict[str, Tuple[Path, Path]]:
"""
Returns a cache of component-name => (root_path, component_path).
To invalidate the cache, call `del self.component_paths`.
"""
paths: Dict[str, Tuple[Path, Path, int]] = {}

# paths known by the current jinja environment
search_paths = {
Path(searchpath["component_file"])
for searchpath in self.jinja_env.loader.searchpath
}

for root_path, namespace, template_path in self._get_all_template_files():
# if the file is known to the current jinja environment,
# get the priority and add it to known components
if template_path in search_paths:
template_filename = os.path.basename(template_path)
template_filename, priority = self._extract_priority(template_filename)

if namespace:
relative_filepath = f"{namespace}/{template_filename}"
else:
relative_filepath = template_filename

# if the priority is greater, replace the path
if (
relative_filepath not in paths
or priority > paths[relative_filepath][2]
):
paths[relative_filepath] = (root_path, template_path, priority)

return {k: (v[0], v[1]) for k, v in paths.items()}

def _extract_priority(self, filename):
# check if there is a priority on the file, if not, take default 0
prefix_pattern = re.compile(r"^\d{3}-")
priority = 0
if prefix_pattern.match(filename):
# Remove the priority from the filename
priority = int(filename[:3])
filename = filename[4:]
return filename, priority

def _get_all_template_files(self):
for prefix in self.prefixes:
root_paths = self.prefixes[prefix].searchpath

for root_path_rec in root_paths:
component_path = root_path_rec["component_path"]
root_path = Path(root_path_rec["root_path"])

for file_absolute_folder, _folders, files in os.walk(
component_path, topdown=False, followlinks=True
):
namespace = os.path.relpath(
file_absolute_folder, component_path
).strip(".")
for filename in files:
yield root_path, namespace, Path(
file_absolute_folder
) / filename

def _get_component_path(
self, prefix: str, name: str, file_ext: "TFileExt" = ""
) -> "tuple[Path, Path]":
name = name.replace(DELIMITER, SLASH)
name_dot = f"{name}."
file_ext = file_ext or self.file_ext
root_paths = self.prefixes[prefix].searchpath

for root_path in root_paths:
component_path = root_path["component_path"]
for curr_folder, _folders, files in os.walk(
component_path, topdown=False, followlinks=True
):
relfolder = os.path.relpath(curr_folder, component_path).strip(".")
if relfolder and not name_dot.startswith(relfolder):
continue

for filename in files:
_filepath = curr_folder + "/" + filename
in_searchpath = False
for searchpath in self.jinja_env.loader.searchpath:
if _filepath == searchpath["component_file"]:
in_searchpath = True
break
if in_searchpath:
prefix_pattern = re.compile(r"^\d{3}-")
without_prefix_filename = filename
if prefix_pattern.match(filename):
# Remove the prefix
without_prefix_filename = prefix_pattern.sub("", filename)
if relfolder:
filepath = f"{relfolder}/{without_prefix_filename}"
else:
filepath = without_prefix_filename
if filepath.startswith(name_dot) and filepath.endswith(
file_ext
):
return (
Path(root_path["root_path"]),
Path(curr_folder) / filename,
)

raise ComponentNotFound(
f"Unable to find a file named {name}{file_ext} "
f"or one following the pattern {name_dot}*{file_ext}"
)
if not file_ext.startswith("."):
file_ext = "." + file_ext
name = name.replace(SLASH, DELIMITER) + file_ext

paths = self.component_paths
if name in paths:
return paths[name]

if self.jinja_env.auto_reload:
# clear cache
del self.component_paths

paths = self.component_paths
if name in paths:
return paths[name]

raise ComponentNotFound(f"Unable to find a file named {name}")

def _get_from_file(
self, *, prefix: str, name: str, url_prefix: str, file_ext: str
Expand All @@ -91,30 +132,6 @@ def _get_from_file(
return component


def get_jinja_template(_catalog, template_def, fields=None):
if fields is None:
fields = []
jinja_content = None
for component in _catalog.jinja_env.loader.searchpath:
if component["component_file"].endswith(template_def["layout"]):
with open(component["component_file"], "r") as file:
jinja_content = file.read()
if not jinja_content:
raise Exception("%s was not found" % (template_def["layout"]))
assembled_template = [jinja_content]
if "blocks" in template_def:
for blk_name, blk in template_def["blocks"].items():
component_content = ""
for field in fields:
component_content = component_content + "%s={%s} " % (field, field)
component_str = "<%s %s> </%s>" % (blk, component_content, blk)
assembled_template.append(
"{%% block %s %%}%s{%% endblock %%}" % (blk_name, component_str)
)
assembled_template = "\n".join(assembled_template)
return assembled_template


def lazy_string_encoder(obj):
if isinstance(obj, list):
return [lazy_string_encoder(item) for item in obj]
Expand Down
Loading