From 1089315fb6d6eba246e7b969b34d9ccc1f89dc0a 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 WiP on finder-browser component WiP on finder-browser component WiP on finder-browser component --- client/browser/FinderFileSelect.tsx | 8 ++ client/browserbuild.config.mjs | 21 +++++ client/finder-browser.scss | 121 ++++++++++++++++++++++++++ client/finder-browser.ts | 2 +- client/icons/arrow-down.svg | 3 + client/icons/arrow-right.svg | 3 + client/icons/empty.svg | 2 + client/icons/folder.svg | 3 + finder/api/__init__.py | 0 finder/api/urls.py | 39 +++++++++ finder/api/views.py | 129 ++++++++++++++++++++++++++++ finder/models/folder.py | 4 + package.json | 2 + testapp/templates/testapp.html | 2 +- testapp/urls.py | 6 +- 15 files changed, 340 insertions(+), 5 deletions(-) create mode 100644 client/browserbuild.config.mjs create mode 100644 client/finder-browser.scss create mode 100644 client/icons/arrow-down.svg create mode 100644 client/icons/arrow-right.svg create mode 100644 client/icons/empty.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/FinderFileSelect.tsx b/client/browser/FinderFileSelect.tsx index 648243173..e48a72525 100644 --- a/client/browser/FinderFileSelect.tsx +++ b/client/browser/FinderFileSelect.tsx @@ -9,6 +9,7 @@ import React, { useState, } from 'react'; import FigureLabels from '../finder/FigureLabels'; +import FileUploader from '../finder/FileUploader'; import ArrowDownIcon from '../icons/arrow-down.svg'; import ArrowRightIcon from '../icons/arrow-right.svg'; import EmptyIcon from '../icons/empty.svg'; @@ -179,11 +180,18 @@ export default function FinderFileSelect(props) { } } + function handleUpload(folderId) { + folderListRef.current.fetchFiles + } + return structure.root_folder && (<> + + + ); } 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..7d57f6c74 --- /dev/null +++ b/client/finder-browser.scss @@ -0,0 +1,121 @@ +finder-file-select { + display: flex; + + .folder-structure { + flex-shrink: 0; + padding-right: 1rem; + + ul ul { + padding-left: 1.5rem; + } + + &, ul { + padding-left: 0; + list-style: none; + } + + li { + i { + display: inline-block; + cursor: pointer; + width: 1.5rem; + color: #808080; + &:hover { + color: inherit; + } + } + + svg { + height: 20px; + vertical-align: text-bottom; + margin-right: 0.5em; + } + + span { + cursor: pointer; + &:hover { + text-decoration: underline; + text-decoration-style: dashed; + text-decoration-color: #808080; + } + } + } + } + + .files-list { + &, ul { + list-style: none; + display: flex; + padding: 10px; + flex-wrap: wrap; + flex-direction: row; + align-content: flex-start; + justify-content: flex-start; + gap: 10px; + } + + li { + min-height: 150px; + width: 125px; + + &.status { + font-size: 18px; + font-weight: bold; + line-height: 50px; + color: #808080; + padding-left: 2em; + width: auto; + } + + .figure { + width: 100%; + height: 100%; + margin: 0; + cursor: pointer; + + div:has(> .figure-labels) { + position: relative; + } + + .figure-labels { + position: absolute; + line-height: 0; + text-align: right; + overflow: hidden; + inset: 4px; + + span { + width: 10px; + height: 10px; + display: inline-block; + border-radius: 50%; + margin-left: -4px; + } + + } + + img, video { + width: 100%; + border-radius: 0.25rem; + } + + img:not([src$=".svg"]), video { + box-shadow: 0 0 0.5rem #808080; + } + + figcaption { + text-align: center; + line-height: 1.5rem; + font-size: 0.8em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + } + } + + .file-uploader { + width: 100%; + } +} diff --git a/client/finder-browser.ts b/client/finder-browser.ts index cd7b5274b..de6deb6c2 100644 --- a/client/finder-browser.ts +++ b/client/finder-browser.ts @@ -4,6 +4,6 @@ import FinderFileSelect from 'browser/FinderFileSelect'; window.addEventListener('DOMContentLoaded', (event) => { window.customElements.define('finder-file-select', r2wc(FinderFileSelect, { - props: {'base-url': 'string', realm: 'string'}} + 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/empty.svg b/client/icons/empty.svg new file mode 100644 index 000000000..a7de8ec94 --- /dev/null +++ b/client/icons/empty.svg @@ -0,0 +1,2 @@ + + \ 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..3777bd359 --- /dev/null +++ b/finder/api/urls.py @@ -0,0 +1,39 @@ +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( + 'open/', + BrowserView.as_view(action='open'), + ), + path( + 'close/', + BrowserView.as_view(action='close'), + ), + path( + 'list/', + BrowserView.as_view(action='list'), + ), + 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..530ae9999 --- /dev/null +++ b/finder/api/views.py @@ -0,0 +1,129 @@ +from django.contrib.sites.shortcuts import get_current_site +from django.core.exceptions import ObjectDoesNotExist +from django.http import JsonResponse, HttpResponseBadRequest, HttpResponseNotFound +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.") + try: + return JsonResponse(action(request, *args, **kwargs)) + except Exception as e: + return HttpResponseBadRequest(str(e)) + + def _get_children(cls, open_folders, parent): + children = [] + for child in parent.subfolders: + child_id = str(child.id) + if child_id in open_folders: + grandchildren = cls._get_children(open_folders, child) + else: + grandchildren = None + children.append({ + 'id': child_id, + 'name': child.name, + 'children': grandchildren, + 'is_open': grandchildren is not None, + 'has_subfolders': child.subfolders.exists(), + }) + return children + + def structure(self, request, realm): + site = get_current_site(request) + try: + realm = RealmModel.objects.get(site=site, slug=realm) + except RealmModel.DoesNotExist: + raise ObjectDoesNotExist(f"Realm {realm} not found for {site.domain}.") + root_folder = FolderModel.objects.get_root_folder(realm) + root_folder_id = str(root_folder.id) + request.session.setdefault('finder.open_folders', []) + request.session.setdefault('finder.last_folder', root_folder_id) + if is_open := root_folder.subfolders.exists(): + # direct children of the root folder are open regardless of the `open_folders` session + children = self._get_children(request.session['finder.open_folders'], root_folder) + else: + children = None + return { + 'root_folder': { + 'id': root_folder_id, + 'name': None, # the root folder has no readable name + 'is_root': True, + 'is_open': is_open, + 'children': children, + 'has_subfolders': is_open, + }, + 'last_folder': request.session['finder.last_folder'], + **self.list(request, request.session['finder.last_folder']), + } + + def fetch(self, request, folder_id): + """ + Open the given folder and fetch children data for the folder. + """ + folder = FolderModel.objects.get(id=folder_id) + folder_id = str(folder_id) + request.session.setdefault('finder.open_folders', []) + if folder_id not in request.session['finder.open_folders']: + request.session['finder.open_folders'].append(folder_id) + request.session.modified = True + + return { + 'id': folder_id, + 'name': folder.name, + 'children': self._get_children(request.session['finder.open_folders'], folder), + 'is_open': True, + 'has_subfolders': folder.subfolders.exists(), + } + + def open(self, request, folder_id): + """ + Just open the folder. + """ + folder_id = str(folder_id) + request.session.setdefault('finder.open_folders', []) + if folder_id not in request.session['finder.open_folders']: + request.session['finder.open_folders'].append(folder_id) + request.session.modified = True + + def close(self, request, folder_id): + """ + Just close the folder. + """ + folder_id = str(folder_id) + try: + request.session['finder.open_folders'].remove(folder_id) + except (KeyError, ValueError): + pass + else: + request.session.modified = True + + def list(self, request, folder_id): + """ + List all the files of the given folder. + """ + folder = FolderModel.objects.get(id=folder_id) + request.session['finder.last_folder'] = str(folder_id) + + folder.listdir(is_folder=False) + + return { + 'files': [{ + 'id': str(file.id), + 'name': file.name, + 'mime_type': file.mime_type, + 'browser_component': file.casted.browser_component, + 'thumbnail_url': file.casted.get_thumbnail_url(), + 'sample_url': getattr(file.casted, 'get_sample_url', lambda: None)(), + 'labels': file.serializable_value('labels'), + } for file in folder.listdir(is_folder=False)] + } diff --git a/finder/models/folder.py b/finder/models/folder.py index 08718d84a..a2c3b9a00 100644 --- a/finder/models/folder.py +++ b/finder/models/folder.py @@ -79,6 +79,10 @@ def casted(self): def folder(self): return self + @property + def subfolders(self): + return FolderModel.objects.filter(parent=self) + @property def num_children(self): num_children = sum(inode_model.objects.filter(parent=self).count() for inode_model in InodeModel.real_models) diff --git a/package.json b/package.json index 0917e1379..9ca32aa58 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,10 @@ "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 b7d81371b..8ee177f5a 100644 --- a/testapp/templates/testapp.html +++ b/testapp/templates/testapp.html @@ -16,7 +16,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: