Add file ID support to FileItem and update related components for consistency
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user