From 3ca6c40169407d6857f82a950bb3b987a18a9230 Mon Sep 17 00:00:00 2001 From: Jacob Rief Date: Fri, 11 Oct 2024 11:45:51 +0200 Subject: [PATCH] WiP on finder-browser component --- client/browser/FinderBrowser.tsx | 133 +++++++++++++++++++++++++++++++ client/browserbuild.config.mjs | 21 +++++ client/finder-browser.scss | 14 ++++ client/finder-browser.ts | 9 +++ client/icons/arrow-down.svg | 3 + client/icons/arrow-right.svg | 3 + client/icons/folder.svg | 3 + finder/api/__init__.py | 0 finder/api/urls.py | 27 +++++++ finder/api/views.py | 44 ++++++++++ package.json | 4 + testapp/templates/testapp.html | 7 +- testapp/urls.py | 6 +- 13 files changed, 268 insertions(+), 6 deletions(-) create mode 100644 client/browser/FinderBrowser.tsx create mode 100644 client/browserbuild.config.mjs create mode 100644 client/finder-browser.scss create mode 100644 client/finder-browser.ts create mode 100644 client/icons/arrow-down.svg create mode 100644 client/icons/arrow-right.svg create mode 100644 client/icons/folder.svg create mode 100644 finder/api/__init__.py create mode 100644 finder/api/urls.py create mode 100644 finder/api/views.py diff --git a/client/browser/FinderBrowser.tsx b/client/browser/FinderBrowser.tsx new file mode 100644 index 000000000..3117818da --- /dev/null +++ b/client/browser/FinderBrowser.tsx @@ -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 (
  • {gettext("Loading...")}
  • ); + + if (inodes.length === 0) { + if (searchQuery) + return (
  • {`No match while searching for “${searchQuery}”`}
  • ); + return (
  • {gettext("This folder is empty")}
  • ); + } + + return inodes.map(inode => inode.is_folder + ? + : + ); + } + + function cssClasses() { + const classes = ['inode-list']; + if (isLoading) + classes.push('loading'); + return classes.join(' '); + } + + return ( +
      + {renderInodes()} +
    + ); +} + + +function FolderStructure(props) { + const {folder} = props; + + return folder ? ( +
  • + {folder.is_root ? : <>{folder.name}} + {folder.children && (
      + {folder.children.map(child => ( + + ))} +
    )} +
  • + ) : 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 ( +
      + +
    + ); +} diff --git a/client/browserbuild.config.mjs b/client/browserbuild.config.mjs new file mode 100644 index 000000000..db9d5fd04 --- /dev/null +++ b/client/browserbuild.config.mjs @@ -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)); diff --git a/client/finder-browser.scss b/client/finder-browser.scss new file mode 100644 index 000000000..af5659cbf --- /dev/null +++ b/client/finder-browser.scss @@ -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; + } + } +} diff --git a/client/finder-browser.ts b/client/finder-browser.ts new file mode 100644 index 000000000..0c90430a8 --- /dev/null +++ b/client/finder-browser.ts @@ -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'}} + )); +}); diff --git a/client/icons/arrow-down.svg b/client/icons/arrow-down.svg new file mode 100644 index 000000000..25a9e132a --- /dev/null +++ b/client/icons/arrow-down.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/client/icons/arrow-right.svg b/client/icons/arrow-right.svg new file mode 100644 index 000000000..2dd539d23 --- /dev/null +++ b/client/icons/arrow-right.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/client/icons/folder.svg b/client/icons/folder.svg new file mode 100644 index 000000000..fdf8187ed --- /dev/null +++ b/client/icons/folder.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/finder/api/__init__.py b/finder/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/finder/api/urls.py b/finder/api/urls.py new file mode 100644 index 000000000..a4a36612a --- /dev/null +++ b/finder/api/urls.py @@ -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/', + BrowserView.as_view(action='structure'), + ), + path( + 'fetch/', + BrowserView.as_view(action='fetch'), + ), + path( + 'jsi18n/', + JavaScriptCatalog.as_view(packages=['finder']), + name="javascript-catalog", + ), + path( + '', + BrowserView.as_view(), + name="base-url", + ), +] diff --git a/finder/api/views.py b/finder/api/views.py new file mode 100644 index 000000000..fd04df994 --- /dev/null +++ b/finder/api/views.py @@ -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 +from django.views import View + +from finder.models.folder import FolderModel, RealmModel + + +class BrowserView(View): + """ + The view for web component . + """ + 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}'}) diff --git a/package.json b/package.json index eff6aca82..1c52432cb 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/testapp/templates/testapp.html b/testapp/templates/testapp.html index a90be71f7..d2ba73c17 100644 --- a/testapp/templates/testapp.html +++ b/testapp/templates/testapp.html @@ -8,8 +8,9 @@ Django-Filer - - + + + @@ -17,7 +18,7 @@

    Django Filer Demo

    -
    +
    diff --git a/testapp/urls.py b/testapp/urls.py index afb99eeda..78c8dc96d 100644 --- a/testapp/urls.py +++ b/testapp/urls.py @@ -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): @@ -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: