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

View File

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

View File

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

View File

@@ -22,6 +22,7 @@ class FileService {
'/user/files', '/user/files',
queryParameters: pathParam, queryParameters: pathParam,
fromJson: (data) => FileItem( fromJson: (data) => FileItem(
id: data['id'],
name: data['name'], name: data['name'],
path: data['path'], path: data['path'],
type: data['type'] == 'file' ? FileType.file : FileType.folder, type: data['type'] == 'file' ? FileType.file : FileType.folder,
@@ -35,6 +36,7 @@ class FileService {
'/orgs/$orgId/files', '/orgs/$orgId/files',
queryParameters: pathParam, queryParameters: pathParam,
fromJson: (data) => FileItem( fromJson: (data) => FileItem(
id: data['id'],
name: data['name'], name: data['name'],
path: data['path'], path: data['path'],
type: data['type'] == 'file' ? FileType.file : FileType.folder, 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' // If bytes or localPath available, send multipart upload with field 'file'
final Map<String, dynamic> fields = {'path': file.path}; final Map<String, dynamic> fields = {'path': file.path};
FormData formData; 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) { if (file.bytes != null) {
print(
'[FileService] Using bytes for upload (${file.bytes!.length} bytes)',
);
formData = FormData.fromMap({ formData = FormData.fromMap({
...fields, ...fields,
'file': MultipartFile.fromBytes(file.bytes!, filename: file.name), 'file': MultipartFile.fromBytes(file.bytes!, filename: file.name),
}); });
} else if (file.localPath != null) { } else if (file.localPath != null) {
print('[FileService] Using localPath for upload: ${file.localPath}');
formData = FormData.fromMap({ formData = FormData.fromMap({
...fields, ...fields,
'file': MultipartFile.fromFile(file.localPath!, filename: file.name), 'file': MultipartFile.fromFile(file.localPath!, filename: file.name),
}); });
} else { } else {
// Fallback to metadata-only create (folders or client that can't send file content) // 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 = { final data = {
'name': file.name, 'name': file.name,
'path': file.path, 'path': file.path,
@@ -97,9 +86,7 @@ class FileService {
} }
final endpoint = orgId.isEmpty ? '/user/files' : '/orgs/$orgId/files'; final endpoint = orgId.isEmpty ? '/user/files' : '/orgs/$orgId/files';
print('[FileService] Uploading to endpoint: $endpoint');
await _apiClient.post(endpoint, data: formData, fromJson: (d) => null); await _apiClient.post(endpoint, data: formData, fromJson: (d) => null);
print('[FileService] Upload completed for ${file.name}');
} }
Future<void> deleteFile(String orgId, String path) async { 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)) out := make([]map[string]interface{}, 0, len(files))
for _, f := range files { for _, f := range files {
out = append(out, map[string]interface{}{ out = append(out, map[string]interface{}{
"id": f.ID.String(),
"name": f.Name, "name": f.Name,
"path": f.Path, "path": f.Path,
"type": f.Type, "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)) out := make([]map[string]interface{}, 0, len(files))
for _, f := range files { for _, f := range files {
out = append(out, map[string]interface{}{ out = append(out, map[string]interface{}{
"id": f.ID.String(),
"name": f.Name, "name": f.Name,
"path": f.Path, "path": f.Path,
"type": f.Type, "type": f.Type,