Skip to content

Commit

Permalink
WiP on finder-browser component
Browse files Browse the repository at this point in the history
  • Loading branch information
jrief committed Oct 11, 2024
1 parent aa41597 commit 3ca6c40
Show file tree
Hide file tree
Showing 13 changed files with 268 additions and 6 deletions.
133 changes: 133 additions & 0 deletions client/browser/FinderBrowser.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import React, {createRef, useEffect, useState} from 'react';
import {File, Folder} from "../finder/Item";
import ArrowDownIcon from '../icons/arrow-down.svg';
import ArrowRightIcon from '../icons/arrow-right.svg';
import FolderIcon from '../icons/folder.svg';
import RootIcon from '../icons/root.svg';


function InodeList(props) {
const {folderId} = props;
const [isLoading, setLoading] = useState(false);
const [inodes, setInodes] = useState([]);
const [searchQuery, setSearchQuery] = useState(() => {
const params = new URLSearchParams(window.location.search);
return params.get('q');
});

useEffect(() => {
fetchInodes();
}, [searchQuery]);

async function fetchInodes() {
const params = new URLSearchParams({q: searchQuery});
const fetchInodesUrl = `${props.baseurl}${folderId}/fetch${searchQuery ? `?${params.toString()}` : ''}`;
setLoading(true);
const response = await fetch(fetchInodesUrl);
if (response.ok) {
const body = await response.json();
setInodes(body.inodes.map(inode => {
const elementRef = createRef();
return {...inode, elementRef};
}));
} else {
console.error(response);
}
setLoading(false);
}

function selectInode(event: PointerEvent, inode) {
if (inode.disabled)
return;
let modifier, selectedIndex = -1;
// simple click
if (inode.selected) {
modifier = f => ({...f, selected: false});
} else {
if (!(event.target as HTMLElement)?.classList.contains('inode-name')) {
// prevent selecting the inode when clicking on the name field to edit it
modifier = f => ({...f, selected: f.id === inode.id});
selectedIndex = inodes.findIndex(f => f.id === inode.id); // remember for an upcoming shift-click
} else {
modifier = f => f;
}
}
const modifiedInodes = inodes.map((f, k) => ({...modifier(f, k), cutted: false, copied: false}));
setInodes(modifiedInodes);
}

function deselectInodes() {
if (inodes.find(inode => inode.selected || inode.dragged)) {
setInodes(inodes.map(inode => ({...inode, selected: false, dragged: false})));
}
}

function renderInodes() {
if (isLoading)
return (<li className="status">{gettext("Loading...")}</li>);

if (inodes.length === 0) {
if (searchQuery)
return (<li className="status">{`No match while searching for “${searchQuery}”`}</li>);
return (<li className="status">{gettext("This folder is empty")}</li>);
}

return inodes.map(inode => inode.is_folder
? <Folder key={inode.id} {...inode} {...props} />
: <File key={inode.id} {...inode} {...props} />
);
}

function cssClasses() {
const classes = ['inode-list'];
if (isLoading)
classes.push('loading');
return classes.join(' ');
}

return (
<ul className={cssClasses()}>
{renderInodes()}
</ul>
);
}


function FolderStructure(props) {
const {folder} = props;

return folder ? (
<li>
{folder.is_root ? <RootIcon/> : <><ArrowRightIcon/><span><FolderIcon/>{folder.name}</span></>}
{folder.children && (<ul>
{folder.children.map(child => (
<FolderStructure key={child.id} folder={child}/>
))}
</ul>)}
</li>
) : null;
}


export function FinderBrowser(props) {
const [structure, setStructure] = useState({root_folder: null});

useEffect(() => {
getStructure();
}, []);

async function getStructure() {
const response = await fetch(`${props.baseurl}structure/${props.realm}`);
if (response.ok) {
setStructure(await response.json());
} else {
console.error(response);
}
}

return (
<ul className="folder-structure">
<FolderStructure folder={structure.root_folder}/>
</ul>
);
}
21 changes: 21 additions & 0 deletions client/browserbuild.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {build} from 'esbuild';
import svgr from 'esbuild-plugin-svgr';
import parser from 'yargs-parser';
const buildOptions = parser(process.argv.slice(2), {
boolean: ['debug', 'minify'],
});

await build({
entryPoints: [
'client/finder-browser.ts',
],
bundle: true,
minify: buildOptions.minify,
sourcemap: buildOptions.debug,
outfile: 'finder/static/finder/js/browser.js',
format: 'esm',
jsx: 'automatic',
plugins: [svgr()],
loader: {'.svg': 'text', '.jsx': 'jsx' },
target: ['es2020', 'chrome84', 'firefox84', 'safari14', 'edge84']
}).catch(() => process.exit(1));
14 changes: 14 additions & 0 deletions client/finder-browser.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.folder-structure {
&, ul {
padding-left: 0;
list-style: none;
}

li {
svg {
height: 20px;
vertical-align: text-bottom;
margin-right: 0.5em;
}
}
}
9 changes: 9 additions & 0 deletions client/finder-browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import r2wc from '@r2wc/react-to-web-component';
import {FinderBrowser} from 'browser/FinderBrowser';


window.addEventListener('DOMContentLoaded', (event) => {
window.customElements.define('finder-browser', r2wc(FinderBrowser, {
props: {baseurl: 'string', realm: 'string'}}
));
});
3 changes: 3 additions & 0 deletions client/icons/arrow-down.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions client/icons/arrow-right.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions client/icons/folder.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file added finder/api/__init__.py
Empty file.
27 changes: 27 additions & 0 deletions finder/api/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from django.urls import path
from django.views.i18n import JavaScriptCatalog

from finder.api.views import BrowserView


app_name = 'finder-api'
urlpatterns = [
path(
'structure/<slug:realm>',
BrowserView.as_view(action='structure'),
),
path(
'fetch/<uuid:folder_id>',
BrowserView.as_view(action='fetch'),
),
path(
'jsi18n/',
JavaScriptCatalog.as_view(packages=['finder']),
name="javascript-catalog",
),
path(
'',
BrowserView.as_view(),
name="base-url",
),
]
44 changes: 44 additions & 0 deletions finder/api/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from django.contrib.sites.shortcuts import get_current_site
from django.http import JsonResponse, HttpResponseBadRequest, HttpResponseNotFound
from django.utils.translation import gettext

Check failure on line 3 in finder/api/views.py

View workflow job for this annotation

GitHub Actions / flake8

'django.utils.translation.gettext' imported but unused
from django.views import View

from finder.models.folder import FolderModel, RealmModel


class BrowserView(View):
"""
The view for web component <finder-browser>.
"""
action = None

def get(self, request, *args, **kwargs):
action = getattr(self, self.action, None)
if not callable(action):
return HttpResponseBadRequest(f"Action {self.action} not allowed.")
return action(request, *args, **kwargs)

def structure(self, request, realm):
site = get_current_site(request)
try:
realm = RealmModel.objects.get(site=site, slug=realm)
except RealmModel.DoesNotExist:
return HttpResponseNotFound(f"Realm {realm} not found for {site.domain}.")
root_folder = FolderModel.objects.get_root_folder(realm)
children = FolderModel.objects.filter(parent=root_folder)
return JsonResponse({
'root_folder': {
'id': str(root_folder.id),
'name': None, # The root folder has no name.
'is_root': True,
'children': [{
'id': str(child.id),
'name': child.name,
'num_children': child.num_children
} for child in children],
'num_children': root_folder.num_children,
}
})

def fetch(self, request, folder_id):
return JsonResponse({'message': f'Fetching folder {folder_id}'})
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@
"private": true,
"scripts": {
"adminbuild": "node client/adminbuild.config.mjs",
"browserbuild": "node client/browserbuild.config.mjs",
"compilescss": "sass --load-path=. client:finder/static/finder/css/",
"buildall": "concurrently \"npm run adminbuild -- --debug \" \"npm run browserbuild -- --debug \" \"npm run compilescss\""
},
"devDependencies": {
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/modifiers": "^6.0.1",
"@r2wc/react-to-web-component": "^2.0.3",
"@types/react-dom": "^18.3.0",
"@wavesurfer/react": "^1.0.7",
"bootstrap": "^5.3.3",
"concurrently": "^8.2.0",
"esbuild": "^0.19.12",
"esbuild-plugin-svgr": "^2.1.0",
Expand Down
7 changes: 4 additions & 3 deletions testapp/templates/testapp.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,17 @@
<title>Django-Filer</title>
<link rel="icon" href="data:,">
<link href="{% static 'node_modules/bootstrap/dist/css/bootstrap.min.css' %}" rel="stylesheet">
<link href="{% static 'filer/admin/css/FilerAdmin.css' %}" rel="stylesheet">
<script type="module" src="{% static 'filer/admin/js/filer.js' %}"></script>
<link href="{% static 'finder/css/finder-browser.css' %}" rel="stylesheet">
<script src="{% url 'finder-api:javascript-catalog' %}"></script>
<script type="module" src="{% static 'finder/js/browser.js' %}"></script>
</head>

<body>
<div class="container">
<div class="row">
<div class="col col-md-8 mx-auto my-5">
<h1>Django Filer Demo</h1>
<div id="filer-admin"></div>
<finder-browser baseurl="{% url 'finder-api:base-url' %}" realm="admin"></finder-browser>
</div>
</div>
</div>
Expand Down
6 changes: 3 additions & 3 deletions testapp/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
from django.contrib import admin
from django.http import HttpResponse
from django.template.loader import get_template
from django.urls import path
from django.urls import include, path

from testapp.admin_site import admin_site
from finder.api import urls as finder_urls


def render_landing(request):
Expand All @@ -31,7 +31,7 @@ def render_landing(request):

urlpatterns = [
path('admin/', admin.site.urls),
path('testapp/admin/', admin_site.urls),
path('finder-api/', include(finder_urls)),
path('testapp/', render_landing),
]
if settings.DEBUG:
Expand Down

0 comments on commit 3ca6c40

Please sign in to comment.