From 617fce237fed4973bd00c85ff5d1e25f935ecd2b Mon Sep 17 00:00:00 2001 From: George Phillips Date: Fri, 22 Aug 2025 10:51:17 +1200 Subject: [PATCH 1/2] Added collection key and added collection calls to example --- examples/file-browser/src/App.tsx | 22 +-- .../src/components/CodeEditor.tsx | 5 + .../src/components/CollectionBrowser.tsx | 63 +++++++ .../src/components/FileBrowser.tsx | 178 +++--------------- .../file-browser/src/components/FileTree.tsx | 174 +++++++++++++++++ .../src/hooks/useCloudCannonAPI.ts | 10 + src/index.d.ts | 5 + 7 files changed, 282 insertions(+), 175 deletions(-) create mode 100644 examples/file-browser/src/components/CollectionBrowser.tsx create mode 100644 examples/file-browser/src/components/FileTree.tsx diff --git a/examples/file-browser/src/App.tsx b/examples/file-browser/src/App.tsx index 60d452b..448cd4a 100644 --- a/examples/file-browser/src/App.tsx +++ b/examples/file-browser/src/App.tsx @@ -8,7 +8,7 @@ import { useCloudCannonAPI } from './hooks/useCloudCannonAPI'; function App() { const [selectedFile, setSelectedFile] = useState(null); - const { api, isLoading, error, files, refreshFiles } = useCloudCannonAPI(); + const { api, isLoading, error, files, refreshFiles, collections } = useCloudCannonAPI(); const handleFileSelect = (file: CloudCannonJavaScriptV1APIFile) => { setSelectedFile(file); @@ -69,27 +69,12 @@ function App() { return (
- {/* Header */} -
-
-

CloudCannon File Browser

-
- {files.length} files - {selectedFile && ( - <> - - Editing: {selectedFile.path} - - )} -
-
-
- {/* Main content */}
{/* File browser sidebar */}
+
CloudCannon API v1 {api && ( @@ -116,8 +102,6 @@ function App() { Connected )} -
-
{selectedFile && ( Language: {selectedFile.path.split('.').pop()?.toUpperCase() || 'PLAIN'} )} diff --git a/examples/file-browser/src/components/CodeEditor.tsx b/examples/file-browser/src/components/CodeEditor.tsx index 82f765f..5af1690 100644 --- a/examples/file-browser/src/components/CodeEditor.tsx +++ b/examples/file-browser/src/components/CodeEditor.tsx @@ -92,6 +92,11 @@ export function CodeEditor({ file, onSave }: CodeEditorProps) { }; loadFileContent(); + + file.addEventListener('change', async () => { + const fileContent = await file.get(); + setOriginalContent(fileContent); + }); }, [file]); useEffect(() => { diff --git a/examples/file-browser/src/components/CollectionBrowser.tsx b/examples/file-browser/src/components/CollectionBrowser.tsx new file mode 100644 index 0000000..e96eb89 --- /dev/null +++ b/examples/file-browser/src/components/CollectionBrowser.tsx @@ -0,0 +1,63 @@ +import type { + CloudCannonJavaScriptV1APICollection, + CloudCannonJavaScriptV1APIFile, +} from '@cloudcannon/javascript-api'; +import { RefreshCw } from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { FileTree } from './FileTree'; + +interface CollectionBrowserProps { + collection: CloudCannonJavaScriptV1APICollection; + selectedFile: CloudCannonJavaScriptV1APIFile | null; + onFileSelect: (file: CloudCannonJavaScriptV1APIFile) => void; + isLoading: boolean; + onRefresh: () => void; +} + +export function CollectionBrowser({ + collection, + selectedFile, + onFileSelect, + isLoading, + onRefresh, +}: CollectionBrowserProps) { + const [items, setItems] = useState(undefined); + + useEffect(() => { + const handleFileChange = async () => { + const items = await collection.items(); + setItems(items); + }; + + collection.addEventListener('change', handleFileChange); + collection.addEventListener('create', handleFileChange); + collection.addEventListener('delete', handleFileChange); + handleFileChange(); + + return () => { + collection.removeEventListener('change', handleFileChange); + collection.removeEventListener('create', handleFileChange); + collection.removeEventListener('delete', handleFileChange); + }; + }, [collection]); + + return ( +
+ {/* Header */} +
+

{collection.collectionKey}

+ +
+ + +
+ ); +} diff --git a/examples/file-browser/src/components/FileBrowser.tsx b/examples/file-browser/src/components/FileBrowser.tsx index 9f5ccdb..132f4a2 100644 --- a/examples/file-browser/src/components/FileBrowser.tsx +++ b/examples/file-browser/src/components/FileBrowser.tsx @@ -1,9 +1,14 @@ -import type { CloudCannonJavaScriptV1APIFile } from '@cloudcannon/javascript-api'; -import { AlertCircle, ChevronDown, ChevronRight, File, Folder, RefreshCw } from 'lucide-react'; -import { useMemo, useState } from 'react'; +import type { + CloudCannonJavaScriptV1APICollection, + CloudCannonJavaScriptV1APIFile, +} from '@cloudcannon/javascript-api'; +import { AlertCircle, RefreshCw } from 'lucide-react'; +import { CollectionBrowser } from './CollectionBrowser'; +import { FileTree } from './FileTree'; interface FileBrowserProps { files: CloudCannonJavaScriptV1APIFile[]; + collections: CloudCannonJavaScriptV1APICollection[]; selectedFile: CloudCannonJavaScriptV1APIFile | null; onFileSelect: (file: CloudCannonJavaScriptV1APIFile) => void; isLoading: boolean; @@ -11,142 +16,15 @@ interface FileBrowserProps { onRefresh: () => void; } -interface FileTreeNode { - name: string; - path: string; - type: 'file' | 'directory'; - children?: Record; - file?: CloudCannonJavaScriptV1APIFile; -} - -function buildFileTree(files: CloudCannonJavaScriptV1APIFile[]): FileTreeNode[] { - const root: Record = {}; - - files.forEach((file) => { - const parts = file.path.split('/').filter(Boolean); - let current = root; - - parts.forEach((part, index) => { - const isFile = index === parts.length - 1; - const currentPath = parts.slice(0, index + 1).join('/'); - - if (!current[part]) { - current[part] = { - name: part, - path: currentPath, - type: isFile ? 'file' : 'directory', - children: isFile ? undefined : {}, - file: isFile ? file : undefined, - }; - } - - if (!isFile) { - current = current[part].children as Record; - } - }); - }); - - // Convert to array and sort - const sortNodes = (nodes: Record): FileTreeNode[] => { - return Object.values(nodes) - .map((node) => ({ - ...node, - children: node.children ? sortNodes(node.children) : {}, - })) - .sort((a, b) => { - // Directories first, then files - if (a.type !== b.type) { - return a.type === 'directory' ? -1 : 1; - } - return a.name.localeCompare(b.name); - }); - }; - - return sortNodes(root); -} - -interface FileTreeItemProps { - node: FileTreeNode; - selectedFile: CloudCannonJavaScriptV1APIFile | null; - onFileSelect: (file: CloudCannonJavaScriptV1APIFile) => void; - level: number; -} - -function FileTreeItem({ node, selectedFile, onFileSelect, level }: FileTreeItemProps) { - const [isExpanded, setIsExpanded] = useState(level < 2); // Auto-expand first two levels - const isSelected = selectedFile?.path === node.path; - - const handleClick = () => { - if (node.type === 'directory') { - setIsExpanded(!isExpanded); - } else if (node.file) { - onFileSelect(node.file); - } - }; - - const getFileIcon = (_filename: string) => { - // const ext = filename.split('.').pop()?.toLowerCase(); - // You could expand this with more specific file type icons - return ; - }; - - return ( -
-
{ - if (e.key === 'Enter' || e.key === ' ') { - handleClick(); - } - }} - > - {node.type === 'directory' && ( - - {isExpanded ? : } - - )} - - - {node.type === 'directory' ? : getFileIcon(node.name)} - - - - {node.name} - -
- - {node.type === 'directory' && isExpanded && node.children && ( -
- {Object.values(node.children).map((child) => ( - - ))} -
- )} -
- ); -} - export function FileBrowser({ files, + collections, selectedFile, onFileSelect, isLoading, error, onRefresh, }: FileBrowserProps) { - const fileTree = useMemo(() => buildFileTree(files), [files]); - if (error) { return (
@@ -168,8 +46,19 @@ export function FileBrowser({ return (
+ {collections.map((collection) => ( + + ))} + {/* Header */} -
+

Files

); } diff --git a/examples/file-browser/src/components/FileTree.tsx b/examples/file-browser/src/components/FileTree.tsx new file mode 100644 index 0000000..c4ad5f3 --- /dev/null +++ b/examples/file-browser/src/components/FileTree.tsx @@ -0,0 +1,174 @@ +import type { + CloudCannonJavaScriptV1APIFile, +} from '@cloudcannon/javascript-api'; +import { ChevronDown, ChevronRight, File, Folder, RefreshCw } from 'lucide-react'; +import { useMemo, useState } from 'react'; + +interface FileTreeProps { + files: CloudCannonJavaScriptV1APIFile[]; + selectedFile: CloudCannonJavaScriptV1APIFile | null; + onFileSelect: (file: CloudCannonJavaScriptV1APIFile) => void; + isLoading: boolean; +} + +interface FileTreeNode { + name: string; + path: string; + type: 'file' | 'directory'; + children?: Record; + file?: CloudCannonJavaScriptV1APIFile; +} + +function buildFileTree(files: CloudCannonJavaScriptV1APIFile[]): FileTreeNode[] { + const root: Record = {}; + + files.forEach((file) => { + const parts = file.path.split('/').filter(Boolean); + let current = root; + + parts.forEach((part, index) => { + const isFile = index === parts.length - 1; + const currentPath = parts.slice(0, index + 1).join('/'); + + if (!current[part]) { + current[part] = { + name: part, + path: currentPath, + type: isFile ? 'file' : 'directory', + children: isFile ? undefined : {}, + file: isFile ? file : undefined, + }; + } + + if (!isFile) { + current = current[part].children as Record; + } + }); + }); + + // Convert to array and sort + const sortNodes = (nodes: Record): FileTreeNode[] => { + return Object.values(nodes) + .map((node) => ({ + ...node, + children: node.children ? sortNodes(node.children) : {}, + })) + .sort((a, b) => { + // Directories first, then files + if (a.type !== b.type) { + return a.type === 'directory' ? -1 : 1; + } + return a.name.localeCompare(b.name); + }); + }; + + return sortNodes(root); +} + +interface FileTreeItemProps { + node: FileTreeNode; + selectedFile: CloudCannonJavaScriptV1APIFile | null; + onFileSelect: (file: CloudCannonJavaScriptV1APIFile) => void; + level: number; +} + +function FileTreeItem({ node, selectedFile, onFileSelect, level }: FileTreeItemProps) { + const [isExpanded, setIsExpanded] = useState(level < 2); // Auto-expand first two levels + const isSelected = selectedFile?.path === node.path; + + const handleClick = () => { + if (node.type === 'directory') { + setIsExpanded(!isExpanded); + } else if (node.file) { + onFileSelect(node.file); + } + }; + + const getFileIcon = (_filename: string) => { + // const ext = filename.split('.').pop()?.toLowerCase(); + // You could expand this with more specific file type icons + return ; + }; + + return ( +
+
{ + if (e.key === 'Enter' || e.key === ' ') { + handleClick(); + } + }} + > + {node.type === 'directory' && ( + + {isExpanded ? : } + + )} + + + {node.type === 'directory' ? : getFileIcon(node.name)} + + + + {node.name} + +
+ + {node.type === 'directory' && isExpanded && node.children && ( +
+ {Object.values(node.children).map((child) => ( + + ))} +
+ )} +
+ ); +} + +export function FileTree({ + files, + selectedFile, + onFileSelect, + isLoading, +}: FileTreeProps) { + const fileTree = useMemo(() => buildFileTree(files), [files]); + + return ( +
+ {isLoading ? ( +
+ +

Loading files...

+
+ ) : fileTree.length === 0 ? ( +
+

No files found

+
+ ) : ( +
+ {fileTree.map((node) => ( + + ))} +
+ )} +
+ ); +} diff --git a/examples/file-browser/src/hooks/useCloudCannonAPI.ts b/examples/file-browser/src/hooks/useCloudCannonAPI.ts index 899a288..4d5849d 100644 --- a/examples/file-browser/src/hooks/useCloudCannonAPI.ts +++ b/examples/file-browser/src/hooks/useCloudCannonAPI.ts @@ -2,6 +2,7 @@ import type { CloudCannonApiEventDetails, CloudCannonEditorWindow, CloudCannonJavaScriptV1API, + CloudCannonJavaScriptV1APICollection, CloudCannonJavaScriptV1APIFile, CloudCannonJavascriptApiRouter, } from '@cloudcannon/javascript-api'; @@ -13,6 +14,7 @@ export interface UseCloudCannonAPIReturn { isLoading: boolean; error: string | null; files: CloudCannonJavaScriptV1APIFile[]; + collections: CloudCannonJavaScriptV1APICollection[]; refreshFiles: () => Promise; } @@ -21,6 +23,7 @@ export function useCloudCannonAPI(): UseCloudCannonAPIReturn { const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [files, setFiles] = useState([]); + const [collections, setCollections] = useState([]); const [CloudCannonAPI, setCloudCannonApi] = useState( undefined ); @@ -88,10 +91,16 @@ export function useCloudCannonAPI(): UseCloudCannonAPIReturn { const fileList = await v1API.files(); setFiles(fileList); + const collections = await v1API.collections(); + setCollections(collections); + // Set up event listeners for file changes const handleFileChange = async () => { const fileList = await v1API.files(); setFiles(fileList); + + const collections = await v1API.collections(); + setCollections(collections); }; v1API.addEventListener('change', handleFileChange); @@ -129,6 +138,7 @@ export function useCloudCannonAPI(): UseCloudCannonAPIReturn { isLoading, error, files, + collections, refreshFiles, }; } diff --git a/src/index.d.ts b/src/index.d.ts index 5dbdf87..042a5b3 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -479,6 +479,11 @@ export interface CloudCannonJavaScriptV1APIFile { } export interface CloudCannonJavaScriptV1APICollection { + /** + * The key of the collection + */ + collectionKey: string; + /** * Gets the items in a collection * @throws {CollectionNotFoundError} If the collection is not found From 18299eceb02a3286a0710e16de47d8afdc88b46c Mon Sep 17 00:00:00 2001 From: George Phillips Date: Fri, 22 Aug 2025 10:55:04 +1200 Subject: [PATCH 2/2] Fix linting --- examples/file-browser/src/App.tsx | 2 +- .../src/components/CollectionBrowser.tsx | 7 ++- .../src/components/FileBrowser.tsx | 7 ++- .../file-browser/src/components/FileTree.tsx | 57 ++++++++----------- 4 files changed, 38 insertions(+), 35 deletions(-) diff --git a/examples/file-browser/src/App.tsx b/examples/file-browser/src/App.tsx index 448cd4a..66a935f 100644 --- a/examples/file-browser/src/App.tsx +++ b/examples/file-browser/src/App.tsx @@ -93,7 +93,7 @@ function App() { {/* Status bar */}
-
+
CloudCannon API v1 {api && ( diff --git a/examples/file-browser/src/components/CollectionBrowser.tsx b/examples/file-browser/src/components/CollectionBrowser.tsx index e96eb89..af11f6f 100644 --- a/examples/file-browser/src/components/CollectionBrowser.tsx +++ b/examples/file-browser/src/components/CollectionBrowser.tsx @@ -57,7 +57,12 @@ export function CollectionBrowser({
- +
); } diff --git a/examples/file-browser/src/components/FileBrowser.tsx b/examples/file-browser/src/components/FileBrowser.tsx index 132f4a2..f2bdf9b 100644 --- a/examples/file-browser/src/components/FileBrowser.tsx +++ b/examples/file-browser/src/components/FileBrowser.tsx @@ -72,7 +72,12 @@ export function FileBrowser({
{/* File tree */} - +
); } diff --git a/examples/file-browser/src/components/FileTree.tsx b/examples/file-browser/src/components/FileTree.tsx index c4ad5f3..3a9cb19 100644 --- a/examples/file-browser/src/components/FileTree.tsx +++ b/examples/file-browser/src/components/FileTree.tsx @@ -1,6 +1,4 @@ -import type { - CloudCannonJavaScriptV1APIFile, -} from '@cloudcannon/javascript-api'; +import type { CloudCannonJavaScriptV1APIFile } from '@cloudcannon/javascript-api'; import { ChevronDown, ChevronRight, File, Folder, RefreshCw } from 'lucide-react'; import { useMemo, useState } from 'react'; @@ -137,38 +135,33 @@ function FileTreeItem({ node, selectedFile, onFileSelect, level }: FileTreeItemP ); } -export function FileTree({ - files, - selectedFile, - onFileSelect, - isLoading, -}: FileTreeProps) { +export function FileTree({ files, selectedFile, onFileSelect, isLoading }: FileTreeProps) { const fileTree = useMemo(() => buildFileTree(files), [files]); return ( -
- {isLoading ? ( -
- -

Loading files...

-
- ) : fileTree.length === 0 ? ( -
-

No files found

-
- ) : ( -
- {fileTree.map((node) => ( - - ))} -
- )} +
+ {isLoading ? ( +
+ +

Loading files...

+
+ ) : fileTree.length === 0 ? ( +
+

No files found

+
+ ) : ( +
+ {fileTree.map((node) => ( + + ))} +
+ )}
); }