Prepare v0.1.0 release
This commit is contained in:
@@ -49,3 +49,7 @@ Frontend package:
|
|||||||
The campaign module can integrate with files when both modules are installed, for example for managed attachment selection and campaign file sharing.
|
The campaign module can integrate with files when both modules are installed, for example for managed attachment selection and campaign file sharing.
|
||||||
|
|
||||||
Platform RBAC and governance rules are documented in `govoplan-core/docs/`.
|
Platform RBAC and governance rules are documented in `govoplan-core/docs/`.
|
||||||
|
|
||||||
|
## Release packaging
|
||||||
|
|
||||||
|
The repository root includes a `package.json` for git-based WebUI installs. It exports the package `@govoplan/files-webui` from `webui/src` so release builds can depend on tagged git refs instead of local `file:` paths.
|
||||||
|
|||||||
31
package.json
Normal file
31
package.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "@govoplan/files-webui",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "webui/src/index.ts",
|
||||||
|
"module": "webui/src/index.ts",
|
||||||
|
"types": "webui/src/index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./webui/src/index.ts",
|
||||||
|
"import": "./webui/src/index.ts"
|
||||||
|
},
|
||||||
|
"./styles/file-manager.css": "./webui/src/styles/file-manager.css"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"webui/src",
|
||||||
|
"README.md",
|
||||||
|
"LICENSE"
|
||||||
|
],
|
||||||
|
"peerDependencies": {
|
||||||
|
"@govoplan/core-webui": "^0.1.0",
|
||||||
|
"lucide-react": "^0.555.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^7.1.1",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"typescript": "^5.7.2",
|
||||||
|
"vite": "^6.0.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from govoplan_files.backend.db.models import FileAsset
|
from govoplan_files.backend.db.models import FileAsset
|
||||||
from govoplan_files.backend.storage.common import FileConflictResolution, FileStorageError, UploadedStoredFile
|
from govoplan_files.backend.storage.common import FileConflictResolution, FileStorageError, UploadedStoredFile
|
||||||
from govoplan_files.backend.storage.backends import get_storage_backend
|
from govoplan_files.backend.storage.backends import StorageBackendError, get_storage_backend
|
||||||
from govoplan_files.backend.storage.files import create_file_asset, current_version_and_blob
|
from govoplan_files.backend.storage.files import create_file_asset, current_version_and_blob
|
||||||
from govoplan_files.backend.storage.paths import filename_from_path, normalize_folder, normalize_logical_path
|
from govoplan_files.backend.storage.paths import filename_from_path, normalize_folder, normalize_logical_path
|
||||||
|
|
||||||
@@ -51,9 +51,12 @@ def create_zip_file(session: Session, assets: Iterable[FileAsset], output_path:
|
|||||||
info = zipfile.ZipInfo(asset.display_path)
|
info = zipfile.ZipInfo(asset.display_path)
|
||||||
info.compress_type = zipfile.ZIP_DEFLATED
|
info.compress_type = zipfile.ZIP_DEFLATED
|
||||||
with archive.open(info, "w") as member:
|
with archive.open(info, "w") as member:
|
||||||
|
try:
|
||||||
for chunk in backend.iter_bytes(blob.storage_key):
|
for chunk in backend.iter_bytes(blob.storage_key):
|
||||||
if chunk:
|
if chunk:
|
||||||
member.write(chunk)
|
member.write(chunk)
|
||||||
|
except StorageBackendError as exc:
|
||||||
|
raise FileStorageError(str(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
def extract_zip_upload(
|
def extract_zip_upload(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Iterable, Protocol
|
from typing import Iterable, Protocol
|
||||||
|
|
||||||
@@ -26,33 +26,43 @@ class StorageBackend(Protocol):
|
|||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class LocalFilesystemStorageBackend:
|
class LocalFilesystemStorageBackend:
|
||||||
root: Path
|
root: Path
|
||||||
|
fallback_roots: tuple[Path, ...] = field(default_factory=tuple)
|
||||||
name: str = "local"
|
name: str = "local"
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
self.root = self.root.expanduser().resolve()
|
self.root = self.root.expanduser().resolve()
|
||||||
|
self.fallback_roots = tuple(root.expanduser().resolve() for root in self.fallback_roots if root)
|
||||||
self.root.mkdir(parents=True, exist_ok=True)
|
self.root.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
def _path(self, key: str) -> Path:
|
def _path_for_root(self, root: Path, key: str) -> Path:
|
||||||
path = (self.root / key).resolve()
|
path = (root / key).resolve()
|
||||||
if not path.is_relative_to(self.root):
|
if not path.is_relative_to(root):
|
||||||
raise StorageBackendError("Storage key escapes local storage root")
|
raise StorageBackendError("Storage key escapes local storage root")
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
def _path(self, key: str) -> Path:
|
||||||
|
return self._path_for_root(self.root, key)
|
||||||
|
|
||||||
|
def _readable_path(self, key: str) -> Path:
|
||||||
|
primary = self._path(key)
|
||||||
|
if primary.exists() and primary.is_file():
|
||||||
|
return primary
|
||||||
|
for root in self.fallback_roots:
|
||||||
|
candidate = self._path_for_root(root, key)
|
||||||
|
if candidate.exists() and candidate.is_file():
|
||||||
|
return candidate
|
||||||
|
raise StorageBackendError("Stored object does not exist")
|
||||||
|
|
||||||
def put_bytes(self, key: str, data: bytes, *, content_type: str | None = None) -> None:
|
def put_bytes(self, key: str, data: bytes, *, content_type: str | None = None) -> None:
|
||||||
path = self._path(key)
|
path = self._path(key)
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
path.write_bytes(data)
|
path.write_bytes(data)
|
||||||
|
|
||||||
def get_bytes(self, key: str) -> bytes:
|
def get_bytes(self, key: str) -> bytes:
|
||||||
path = self._path(key)
|
return self._readable_path(key).read_bytes()
|
||||||
if not path.exists() or not path.is_file():
|
|
||||||
raise StorageBackendError("Stored object does not exist")
|
|
||||||
return path.read_bytes()
|
|
||||||
|
|
||||||
def iter_bytes(self, key: str, *, chunk_size: int = 1024 * 1024) -> Iterable[bytes]:
|
def iter_bytes(self, key: str, *, chunk_size: int = 1024 * 1024) -> Iterable[bytes]:
|
||||||
path = self._path(key)
|
path = self._readable_path(key)
|
||||||
if not path.exists() or not path.is_file():
|
|
||||||
raise StorageBackendError("Stored object does not exist")
|
|
||||||
with path.open("rb") as handle:
|
with path.open("rb") as handle:
|
||||||
while True:
|
while True:
|
||||||
chunk = handle.read(chunk_size)
|
chunk = handle.read(chunk_size)
|
||||||
@@ -66,8 +76,11 @@ class LocalFilesystemStorageBackend:
|
|||||||
path.unlink()
|
path.unlink()
|
||||||
|
|
||||||
def exists(self, key: str) -> bool:
|
def exists(self, key: str) -> bool:
|
||||||
path = self._path(key)
|
try:
|
||||||
return path.exists() and path.is_file()
|
self._readable_path(key)
|
||||||
|
except StorageBackendError:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
@@ -125,10 +138,15 @@ class S3StorageBackend:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _fallback_roots() -> tuple[Path, ...]:
|
||||||
|
raw = getattr(settings, "file_storage_local_fallback_roots", "") or ""
|
||||||
|
return tuple(Path(item.strip()) for item in str(raw).split(",") if item.strip())
|
||||||
|
|
||||||
|
|
||||||
def get_storage_backend() -> StorageBackend:
|
def get_storage_backend() -> StorageBackend:
|
||||||
backend = settings.file_storage_backend.lower().strip()
|
backend = settings.file_storage_backend.lower().strip()
|
||||||
if backend in {"local", "filesystem", "fs"}:
|
if backend in {"local", "filesystem", "fs"}:
|
||||||
return LocalFilesystemStorageBackend(Path(settings.file_storage_local_root))
|
return LocalFilesystemStorageBackend(Path(settings.file_storage_local_root), fallback_roots=_fallback_roots())
|
||||||
if backend in {"s3", "garage"}:
|
if backend in {"s3", "garage"}:
|
||||||
return S3StorageBackend(
|
return S3StorageBackend(
|
||||||
bucket=settings.file_storage_s3_bucket or settings.s3_bucket,
|
bucket=settings.file_storage_s3_bucket or settings.s3_bucket,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from govoplan_campaign.backend.db.models import Campaign
|
|||||||
from govoplan_files.backend.db.models import CampaignAttachmentUse, FileAsset, FileBlob, FileShare, FileVersion
|
from govoplan_files.backend.db.models import CampaignAttachmentUse, FileAsset, FileBlob, FileShare, FileVersion
|
||||||
from govoplan_files.backend.runtime import settings
|
from govoplan_files.backend.runtime import settings
|
||||||
from govoplan_files.backend.storage.access import ensure_owner_access, user_group_ids
|
from govoplan_files.backend.storage.access import ensure_owner_access, user_group_ids
|
||||||
from govoplan_files.backend.storage.backends import get_storage_backend
|
from govoplan_files.backend.storage.backends import StorageBackendError, get_storage_backend
|
||||||
from govoplan_files.backend.storage.common import FileConflictResolution, FileStorageError, UploadedStoredFile, utcnow
|
from govoplan_files.backend.storage.common import FileConflictResolution, FileStorageError, UploadedStoredFile, utcnow
|
||||||
from govoplan_files.backend.storage.paths import filename_from_path, join_folder_filename, normalize_folder, normalize_logical_path, safe_storage_component
|
from govoplan_files.backend.storage.paths import filename_from_path, join_folder_filename, normalize_folder, normalize_logical_path, safe_storage_component
|
||||||
|
|
||||||
@@ -56,13 +56,22 @@ def _get_or_create_blob(
|
|||||||
.one_or_none()
|
.one_or_none()
|
||||||
)
|
)
|
||||||
if blob:
|
if blob:
|
||||||
|
backend = get_storage_backend()
|
||||||
|
if not backend.exists(blob.storage_key):
|
||||||
|
try:
|
||||||
|
backend.put_bytes(blob.storage_key, data, content_type=content_type)
|
||||||
|
except StorageBackendError as exc:
|
||||||
|
raise FileStorageError(str(exc)) from exc
|
||||||
blob.ref_count += 1
|
blob.ref_count += 1
|
||||||
session.add(blob)
|
session.add(blob)
|
||||||
return blob
|
return blob
|
||||||
|
|
||||||
storage_key = _storage_key(tenant_id=tenant_id, checksum=checksum, filename=filename)
|
storage_key = _storage_key(tenant_id=tenant_id, checksum=checksum, filename=filename)
|
||||||
backend = get_storage_backend()
|
backend = get_storage_backend()
|
||||||
|
try:
|
||||||
backend.put_bytes(storage_key, data, content_type=content_type)
|
backend.put_bytes(storage_key, data, content_type=content_type)
|
||||||
|
except StorageBackendError as exc:
|
||||||
|
raise FileStorageError(str(exc)) from exc
|
||||||
blob = FileBlob(
|
blob = FileBlob(
|
||||||
tenant_id=tenant_id,
|
tenant_id=tenant_id,
|
||||||
storage_backend=_storage_backend_name(),
|
storage_backend=_storage_backend_name(),
|
||||||
@@ -250,7 +259,10 @@ def current_version_and_blob(session: Session, asset: FileAsset) -> tuple[FileVe
|
|||||||
def read_asset_bytes(session: Session, asset: FileAsset) -> tuple[bytes, FileVersion, FileBlob]:
|
def read_asset_bytes(session: Session, asset: FileAsset) -> tuple[bytes, FileVersion, FileBlob]:
|
||||||
version, blob = current_version_and_blob(session, asset)
|
version, blob = current_version_and_blob(session, asset)
|
||||||
backend = get_storage_backend()
|
backend = get_storage_backend()
|
||||||
|
try:
|
||||||
return backend.get_bytes(blob.storage_key), version, blob
|
return backend.get_bytes(blob.storage_key), version, blob
|
||||||
|
except StorageBackendError as exc:
|
||||||
|
raise FileStorageError(str(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
def share_file(
|
def share_file(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { CSSProperties, DragEvent as ReactDragEvent, MouseEvent as ReactMouseEvent, ReactNode } from "react";
|
import type { CSSProperties, DragEvent as ReactDragEvent, MouseEvent as ReactMouseEvent, ReactNode } from "react";
|
||||||
import { Copy, Download, Folder, FolderOpen, Home, MoveRight, Plus, Trash2, UploadCloud } from "lucide-react";
|
import { Copy, Download, FolderOpen, Home, MoveRight, Plus, Trash2, UploadCloud } from "lucide-react";
|
||||||
import { Button, Dialog } from "@govoplan/core-webui";
|
import { Button, Dialog, ExplorerTree, type ExplorerTreeNodeContext } from "@govoplan/core-webui";
|
||||||
import type { ConflictAction, FileSpace, RenameResponse } from "../../../api/files";
|
import type { ConflictAction, FileSpace, RenameResponse } from "../../../api/files";
|
||||||
import type { ConflictDialogState, FileActionTarget, FileConflictItem, FolderNode, ContextMenuState } from "../types";
|
import type { ConflictDialogState, FileActionTarget, FileConflictItem, FolderNode, ContextMenuState } from "../types";
|
||||||
import { isPathUnderOrSame, normalizeFolder, treeNodeKey } from "../utils/fileManagerUtils";
|
import { isPathUnderOrSame, normalizeFolder, treeNodeKey } from "../utils/fileManagerUtils";
|
||||||
@@ -111,80 +111,35 @@ export function FolderTree({
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
depth?: number;
|
depth?: number;
|
||||||
}) {
|
}) {
|
||||||
if (nodes.length === 0) return null;
|
|
||||||
return (
|
return (
|
||||||
<div className="file-tree-children">
|
<ExplorerTree
|
||||||
{nodes.map((node) => {
|
nodes={nodes}
|
||||||
const isActive = activeSpaceId === spaceId && currentFolder === node.path;
|
getNodeId={(node) => treeNodeKey(spaceId, node.path)}
|
||||||
|
getNodeLabel={(node) => node.name}
|
||||||
|
getNodeChildren={(node) => node.children}
|
||||||
|
activeId={activeSpaceId === spaceId ? treeNodeKey(spaceId, currentFolder) : ""}
|
||||||
|
expandedIds={expandedKeys}
|
||||||
|
onOpen={(node) => onOpen(spaceId, node.path)}
|
||||||
|
onToggle={(node) => onToggle(spaceId, node.path)}
|
||||||
|
disabled={disabled}
|
||||||
|
depth={depth}
|
||||||
|
getNodeWrapClassName={(node) => {
|
||||||
const target = { spaceId, folderPath: node.path };
|
const target = { spaceId, folderPath: node.path };
|
||||||
const isDropTarget = dragDropEnabled && dropTargetKey === `${target.spaceId}:${normalizeFolder(target.folderPath)}`;
|
return dragDropEnabled && dropTargetKey === `${target.spaceId}:${normalizeFolder(target.folderPath)}` ? "is-drop-target" : undefined;
|
||||||
const hasChildren = node.children.length > 0;
|
|
||||||
const isExpanded = expandedKeys.has(treeNodeKey(spaceId, node.path));
|
|
||||||
return (
|
|
||||||
<div key={node.path}>
|
|
||||||
<div
|
|
||||||
className={`file-tree-node-wrap ${isActive ? "is-active" : ""} ${isDropTarget ? "is-drop-target" : ""}`}
|
|
||||||
style={{ paddingLeft: `${Math.min(depth * 14, 56)}px` }}
|
|
||||||
draggable={dragDropEnabled && !disabled}
|
|
||||||
onDragStart={dragDropEnabled && onDragStartFolder ? (event) => onDragStartFolder(spaceId, node.path, event) : undefined}
|
|
||||||
onDragEnd={dragDropEnabled ? onDragEndFolder : undefined}
|
|
||||||
onContextMenu={contextMenuEnabled && onContextMenu ? (event) => onContextMenu(event, spaceId, node.path) : undefined}
|
|
||||||
onDragOver={dragDropEnabled && onDragOverTarget ? (event) => {
|
|
||||||
onDragOverTarget(event, target);
|
|
||||||
if (hasChildren && !isExpanded) onRequestDragExpand?.(spaceId, node.path);
|
|
||||||
} : undefined}
|
|
||||||
onDragLeave={dragDropEnabled ? onClearDropState : undefined}
|
|
||||||
onDrop={dragDropEnabled && onDropOnTarget ? (event) => void onDropOnTarget(event, target) : undefined}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="file-tree-toggle"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
if (hasChildren) onToggle(spaceId, node.path);
|
|
||||||
}}
|
}}
|
||||||
disabled={disabled || !hasChildren}
|
getNodeWrapStyle={(_node, context: ExplorerTreeNodeContext) => ({ paddingLeft: `${Math.min(context.depth * 14, 56)}px` })}
|
||||||
aria-label={`${isExpanded ? "Collapse" : "Expand"} ${node.name}`}
|
getNodeDraggable={() => dragDropEnabled && !disabled}
|
||||||
aria-expanded={hasChildren ? isExpanded : undefined}
|
onContextMenu={contextMenuEnabled && onContextMenu ? (event, node) => onContextMenu(event, spaceId, node.path) : undefined}
|
||||||
>
|
onDragStart={dragDropEnabled && onDragStartFolder ? (event, node) => onDragStartFolder(spaceId, node.path, event) : undefined}
|
||||||
{isExpanded ? <FolderOpen size={15} aria-hidden="true" /> : <Folder size={15} aria-hidden="true" />}
|
onDragEnd={dragDropEnabled && onDragEndFolder ? () => onDragEndFolder() : undefined}
|
||||||
</button>
|
onDragOver={dragDropEnabled && onDragOverTarget ? (event, node, context) => {
|
||||||
<button
|
const target = { spaceId, folderPath: node.path };
|
||||||
type="button"
|
onDragOverTarget(event, target);
|
||||||
className="file-tree-node"
|
if (context.hasChildren && !context.expanded) onRequestDragExpand?.(spaceId, node.path);
|
||||||
onClick={() => onOpen(spaceId, node.path)}
|
} : undefined}
|
||||||
disabled={disabled}
|
onDragLeave={dragDropEnabled && onClearDropState ? () => onClearDropState() : undefined}
|
||||||
>
|
onDrop={dragDropEnabled && onDropOnTarget ? (event, node) => void onDropOnTarget(event, { spaceId, folderPath: node.path }) : undefined}
|
||||||
<span>{node.name}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{hasChildren && isExpanded && (
|
|
||||||
<FolderTree
|
|
||||||
nodes={node.children}
|
|
||||||
activeSpaceId={activeSpaceId}
|
|
||||||
spaceId={spaceId}
|
|
||||||
currentFolder={currentFolder}
|
|
||||||
dropTargetKey={dropTargetKey}
|
|
||||||
expandedKeys={expandedKeys}
|
|
||||||
onOpen={onOpen}
|
|
||||||
onToggle={onToggle}
|
|
||||||
onContextMenu={onContextMenu}
|
|
||||||
onDragOverTarget={onDragOverTarget}
|
|
||||||
onDropOnTarget={onDropOnTarget}
|
|
||||||
onClearDropState={onClearDropState}
|
|
||||||
onRequestDragExpand={onRequestDragExpand}
|
|
||||||
onDragStartFolder={onDragStartFolder}
|
|
||||||
onDragEndFolder={onDragEndFolder}
|
|
||||||
dragDropEnabled={dragDropEnabled}
|
|
||||||
contextMenuEnabled={contextMenuEnabled}
|
|
||||||
disabled={disabled}
|
|
||||||
depth={depth + 1}
|
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { formatDateTime as formatPlatformDateTime } from "@govoplan/core-webui";
|
||||||
import type { FileFolder, ManagedFile } from "../../../api/files";
|
import type { FileFolder, ManagedFile } from "../../../api/files";
|
||||||
import type { DragSelectionState, EntrySelectionKey, ExplorerEntry, FileEntry, FolderEntry, FolderNode, SortColumn, SortDirection } from "../types";
|
import type { DragSelectionState, EntrySelectionKey, ExplorerEntry, FileEntry, FolderEntry, FolderNode, SortColumn, SortDirection } from "../types";
|
||||||
|
|
||||||
@@ -297,7 +298,5 @@ export function formatBytes(value: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function formatDate(value: string): string {
|
export function formatDate(value: string): string {
|
||||||
const date = new Date(value);
|
return formatPlatformDateTime(value);
|
||||||
if (Number.isNaN(date.getTime())) return value;
|
|
||||||
return date.toLocaleString();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Folder } from "lucide-react";
|
|
||||||
import { createElement, lazy } from "react";
|
import { createElement, lazy } from "react";
|
||||||
import type { PlatformWebModule } from "@govoplan/core-webui";
|
import type { PlatformWebModule } from "@govoplan/core-webui";
|
||||||
const FilesPage = lazy(() => import("./features/files/FilesPage"));
|
const FilesPage = lazy(() => import("./features/files/FilesPage"));
|
||||||
@@ -10,7 +9,7 @@ export const filesModule: PlatformWebModule = {
|
|||||||
label: "Files",
|
label: "Files",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
dependencies: ["access"],
|
dependencies: ["access"],
|
||||||
navItems: [{ to: "/files", label: "Files", icon: Folder, anyOf: fileRead, order: 40 }],
|
navItems: [{ to: "/files", label: "Files", iconName: "folder", anyOf: fileRead, order: 40 }],
|
||||||
routes: [
|
routes: [
|
||||||
{ path: "/files", anyOf: fileRead, order: 40, render: ({ settings, auth }) => createElement(FilesPage, { settings, auth }) }
|
{ path: "/files", anyOf: fileRead, order: 40, render: ({ settings, auth }) => createElement(FilesPage, { settings, auth }) }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 10px 8px 16px;
|
padding: 0px 0px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-tree-space + .file-tree-space {
|
.file-tree-space + .file-tree-space {
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
grid-template-columns: 26px minmax(0, 1fr);
|
grid-template-columns: 26px minmax(0, 1fr);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
border-radius: 9px;
|
border-radius: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-tree-node,
|
.file-tree-node,
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
place-items: center;
|
place-items: center;
|
||||||
width: 26px;
|
width: 26px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border-radius: 9px;
|
border-radius: 0px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 8px 9px;
|
padding: 8px 9px;
|
||||||
border-radius: 9px;
|
border-radius: 0px;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@@ -137,7 +137,7 @@
|
|||||||
.file-tree-root:hover:not(:disabled),
|
.file-tree-root:hover:not(:disabled),
|
||||||
.file-tree-root:focus-visible,
|
.file-tree-root:focus-visible,
|
||||||
.file-tree-root.is-active {
|
.file-tree-root.is-active {
|
||||||
background: rgba(13, 110, 253, .08);
|
background: var(--line);
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,8 +149,8 @@
|
|||||||
|
|
||||||
.file-tree-node-wrap.is-drop-target,
|
.file-tree-node-wrap.is-drop-target,
|
||||||
.file-tree-root.is-drop-target {
|
.file-tree-root.is-drop-target {
|
||||||
background: rgba(13, 110, 253, .14);
|
background: var(--line);
|
||||||
box-shadow: inset 0 0 0 2px rgba(13, 110, 253, .28);
|
box-shadow: inset 0 0 0 2px var(--line-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-tree-node-wrap.is-selected .file-tree-select {
|
.file-tree-node-wrap.is-selected .file-tree-select {
|
||||||
@@ -240,8 +240,8 @@
|
|||||||
|
|
||||||
.file-list-drop-target.is-active,
|
.file-list-drop-target.is-active,
|
||||||
.file-list-drop-target.is-drop-target {
|
.file-list-drop-target.is-drop-target {
|
||||||
background: rgba(13, 110, 253, .05);
|
background: var(--line);
|
||||||
box-shadow: inset 0 0 0 2px rgba(13, 110, 253, .22);
|
box-shadow: inset 0 0 0 2px var(--line-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list-table {
|
.file-list-table {
|
||||||
@@ -295,18 +295,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.file-list-row:focus-visible {
|
.file-list-row:focus-visible {
|
||||||
outline: 2px solid rgba(13, 110, 253, .35);
|
outline: 2px solid var(--line-dark);
|
||||||
outline-offset: -2px;
|
outline-offset: -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list-row:hover,
|
.file-list-row:hover,
|
||||||
.file-list-row.is-selected {
|
.file-list-row.is-selected {
|
||||||
background: rgba(13, 110, 253, .07);
|
background: var(--line);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list-row.is-drop-target {
|
.file-list-row.is-drop-target {
|
||||||
background: rgba(13, 110, 253, .13);
|
background: var(--line);
|
||||||
box-shadow: inset 0 0 0 2px rgba(13, 110, 253, .28);
|
box-shadow: inset 0 0 0 2px var(--line-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-parent-row.is-disabled {
|
.file-parent-row.is-disabled {
|
||||||
|
|||||||
Reference in New Issue
Block a user