Add file ID support to FileItem and update related components for consistency

This commit is contained in:
Leon Bösche
2026-01-10 00:26:34 +01:00
parent 260b8b180e
commit ca39b3dee4
5 changed files with 49 additions and 21 deletions

View File

@@ -4,6 +4,7 @@ import 'dart:typed_data';
enum FileType { folder, file }
class FileItem extends Equatable {
final String? id;
final String name;
final String path;
final FileType type;
@@ -13,6 +14,7 @@ class FileItem extends Equatable {
final Uint8List? bytes; // optional file bytes for web/desktop uploads
const FileItem({
this.id,
required this.name,
required this.path,
required this.type,
@@ -23,9 +25,10 @@ class FileItem extends Equatable {
});
@override
List<Object?> get props => [name, path, type, size, lastModified];
List<Object?> get props => [id, name, path, type, size, lastModified];
FileItem copyWith({
String? id,
String? name,
String? path,
FileType? type,
@@ -33,6 +36,7 @@ class FileItem extends Equatable {
DateTime? lastModified,
}) {
return FileItem(
id: id ?? this.id,
name: name ?? this.name,
path: path ?? this.path,
type: type ?? this.type,

View File

@@ -560,10 +560,13 @@ class _FileExplorerState extends State<FileExplorer> {
if (file.type == FileType.folder) {
context.read<FileBrowserBloc>().add(NavigateToFolder(file.path));
} else {
final fileId = file.path.startsWith('/')
? file.path.substring(1)
: file.path;
_showDocumentViewer(widget.orgId, fileId);
if (file.id == null || file.id!.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Error: File ID is missing')),
);
return;
}
_showDocumentViewer(widget.orgId, file.id!);
}
},
child: Container(
@@ -728,9 +731,18 @@ class _FileExplorerState extends State<FileExplorer> {
children: [
ModernGlassButton(
onPressed: () async {
print(
'[FileExplorer-Empty] Upload clicked, currentPath=${state.currentPath}',
);
final result = await FilePicker.platform
.pickFiles(withData: true);
if (result != null && result.files.isNotEmpty) {
print(
'[FileExplorer-Empty] Selected ${result.files.length} files',
);
print(
'[FileExplorer-Empty] Will upload to: ${state.currentPath}',
);
final files = result.files
.map(
(file) => FileItem(

View File

@@ -6,9 +6,11 @@ import '../models/document_capabilities.dart';
import '../models/api_error.dart';
import '../repositories/file_repository.dart';
import 'package:path/path.dart' as p;
import 'package:uuid/uuid.dart';
class MockFileRepository implements FileRepository {
final Map<String, List<FileItem>> _orgFiles = {};
final _uuid = const Uuid();
List<FileItem> _getFilesForOrg(String orgId) {
if (!_orgFiles.containsKey(orgId)) {
@@ -16,18 +18,21 @@ class MockFileRepository implements FileRepository {
if (orgId == 'org1') {
_orgFiles[orgId] = [
FileItem(
id: _uuid.v4(),
name: 'Personal Documents',
path: '/Personal Documents',
type: FileType.folder,
lastModified: DateTime.now(),
),
FileItem(
id: _uuid.v4(),
name: 'Photos',
path: '/Photos',
type: FileType.folder,
lastModified: DateTime.now(),
),
FileItem(
id: _uuid.v4(),
name: 'resume.pdf',
path: '/resume.pdf',
type: FileType.file,
@@ -35,6 +40,7 @@ class MockFileRepository implements FileRepository {
lastModified: DateTime.now(),
),
FileItem(
id: _uuid.v4(),
name: 'notes.txt',
path: '/notes.txt',
type: FileType.file,
@@ -45,12 +51,14 @@ class MockFileRepository implements FileRepository {
} else if (orgId == 'org2') {
_orgFiles[orgId] = [
FileItem(
id: _uuid.v4(),
name: 'Company Reports',
path: '/Company Reports',
type: FileType.folder,
lastModified: DateTime.now(),
),
FileItem(
id: _uuid.v4(),
name: 'annual_report.pdf',
path: '/annual_report.pdf',
type: FileType.file,
@@ -58,6 +66,7 @@ class MockFileRepository implements FileRepository {
lastModified: DateTime.now(),
),
FileItem(
id: _uuid.v4(),
name: 'presentation.pptx',
path: '/presentation.pptx',
type: FileType.file,
@@ -68,12 +77,14 @@ class MockFileRepository implements FileRepository {
} else if (orgId == 'org3') {
_orgFiles[orgId] = [
FileItem(
id: _uuid.v4(),
name: 'Project Code',
path: '/Project Code',
type: FileType.folder,
lastModified: DateTime.now(),
),
FileItem(
id: _uuid.v4(),
name: 'side_project.dart',
path: '/side_project.dart',
type: FileType.file,
@@ -153,6 +164,7 @@ class MockFileRepository implements FileRepository {
final files = _getFilesForOrg(orgId);
files.add(
FileItem(
id: _uuid.v4(),
name: normalizedName,
path: newPath,
type: FileType.folder,
@@ -175,6 +187,7 @@ class MockFileRepository implements FileRepository {
final newName = file.path.split('/').last;
final newPath = targetPath == '/' ? '/$newName' : '$targetPath/$newName';
files[fileIndex] = FileItem(
id: file.id,
name: file.name,
path: newPath,
type: file.type,
@@ -194,6 +207,7 @@ class MockFileRepository implements FileRepository {
final parentPath = p.dirname(path);
final newPath = parentPath == '.' ? '/$newName' : '$parentPath/$newName';
files[fileIndex] = FileItem(
id: file.id,
name: newName,
path: newPath,
type: file.type,
@@ -216,7 +230,16 @@ class MockFileRepository implements FileRepository {
Future<void> uploadFile(String orgId, FileItem file) async {
await Future.delayed(const Duration(seconds: 1));
final files = _getFilesForOrg(orgId);
files.add(file);
files.add(
FileItem(
id: _uuid.v4(),
name: file.name,
path: file.path,
type: file.type,
size: file.size,
lastModified: file.lastModified,
),
);
}
@override

View File

@@ -22,6 +22,7 @@ class FileService {
'/user/files',
queryParameters: pathParam,
fromJson: (data) => FileItem(
id: data['id'],
name: data['name'],
path: data['path'],
type: data['type'] == 'file' ? FileType.file : FileType.folder,
@@ -35,6 +36,7 @@ class FileService {
'/orgs/$orgId/files',
queryParameters: pathParam,
fromJson: (data) => FileItem(
id: data['id'],
name: data['name'],
path: data['path'],
type: data['type'] == 'file' ? FileType.file : FileType.folder,
@@ -52,32 +54,19 @@ class FileService {
// If bytes or localPath available, send multipart upload with field 'file'
final Map<String, dynamic> fields = {'path': file.path};
FormData formData;
print(
'[FileService] uploadFile: file=${file.name}, path=${file.path}, orgId=$orgId',
);
print(
'[FileService] bytes=${file.bytes?.length ?? 0}, localPath=${file.localPath}',
);
if (file.bytes != null) {
print(
'[FileService] Using bytes for upload (${file.bytes!.length} bytes)',
);
formData = FormData.fromMap({
...fields,
'file': MultipartFile.fromBytes(file.bytes!, filename: file.name),
});
} else if (file.localPath != null) {
print('[FileService] Using localPath for upload: ${file.localPath}');
formData = FormData.fromMap({
...fields,
'file': MultipartFile.fromFile(file.localPath!, filename: file.name),
});
} else {
// Fallback to metadata-only create (folders or client that can't send file content)
print(
'[FileService] No bytes or localPath; falling back to metadata-only',
);
final data = {
'name': file.name,
'path': file.path,
@@ -97,9 +86,7 @@ class FileService {
}
final endpoint = orgId.isEmpty ? '/user/files' : '/orgs/$orgId/files';
print('[FileService] Uploading to endpoint: $endpoint');
await _apiClient.post(endpoint, data: formData, fromJson: (d) => null);
print('[FileService] Upload completed for ${file.name}');
}
Future<void> deleteFile(String orgId, String path) async {

View File

@@ -338,6 +338,7 @@ func listFilesHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
out := make([]map[string]interface{}, 0, len(files))
for _, f := range files {
out = append(out, map[string]interface{}{
"id": f.ID.String(),
"name": f.Name,
"path": f.Path,
"type": f.Type,
@@ -884,6 +885,7 @@ func userFilesHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
out := make([]map[string]interface{}, 0, len(files))
for _, f := range files {
out = append(out, map[string]interface{}{
"id": f.ID.String(),
"name": f.Name,
"path": f.Path,
"type": f.Type,