Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PE-6113: add a modal to filter success uploads #1736

Merged
merged 10 commits into from
Aug 19, 2024
4 changes: 3 additions & 1 deletion lib/blocs/upload/enums/conflicting_files_actions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@
/// `Skip` Will ignore the files and don't upload them.
///
/// `Replace` will upload the conflicting file and replace the existent.
enum UploadActions { skip, replace }
///
/// `SkipSuccessfulUploads` will skip the files that were successfully uploaded.
enum UploadActions { skip, skipSuccessfulUploads, replace }
73 changes: 66 additions & 7 deletions lib/blocs/upload/upload_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ class UploadCubit extends Cubit<UploadState> {
/// Map of conflicting file ids keyed by their file names.
final Map<String, String> conflictingFiles = {};
final List<String> conflictingFolders = [];
List<String> failedFiles = [];

UploadCubit({
required this.driveId,
Expand Down Expand Up @@ -306,32 +307,81 @@ class UploadCubit extends Cubit<UploadState> {
checkConflicts();
}

Future<void> checkConflictingFiles() async {
Future<void> checkConflictingFiles({
bool checkFailedFiles = true,
}) async {
emit(UploadPreparationInProgress());

_removeFilesWithFolderNameConflicts();

for (final file in files) {
final fileName = file.ioFile.name;
final existingFileId = await _driveDao
final existingFileIds = await _driveDao
.filesInFolderWithName(
driveId: _targetDrive.id,
parentFolderId: file.parentFolderId,
name: fileName,
)
.map((f) => f.id)
.getSingleOrNull();
.get();

if (existingFileIds.isNotEmpty) {
final existingFileId = existingFileIds.first;

if (existingFileId != null) {
logger.d('Found conflicting file. Existing file id: $existingFileId');
conflictingFiles[file.getIdentifier()] = existingFileId;
}
}

if (conflictingFiles.isNotEmpty) {
if (checkFailedFiles) {
failedFiles.clear();

conflictingFiles.forEach((key, value) {
logger.d('Checking if file $key has failed');
});

for (final fileNameKey in conflictingFiles.keys) {
final fileId = conflictingFiles[fileNameKey];

final fileRevision = await _driveDao
.latestFileRevisionByFileId(
driveId: driveId,
fileId: fileId!,
)
.getSingleOrNull();

final status = _driveDao.select(_driveDao.networkTransactions)
..where((tbl) => tbl.id.equals(fileRevision!.dataTxId));

final transaction = await status.getSingleOrNull();

logger.d('Transaction status: ${transaction?.status}');

if (transaction?.status == TransactionStatus.failed) {
failedFiles.add(fileNameKey);
}
}

logger.d('Failed files: $failedFiles');

if (failedFiles.isNotEmpty) {
emit(
UploadConflictWithFailedFiles(
areAllFilesConflicting: conflictingFiles.length == files.length,
conflictingFileNames: conflictingFiles.keys.toList(),
conflictingFileNamesForFailedFiles: failedFiles,
),
);
return;
}
}

emit(
UploadFileConflict(
areAllFilesConflicting: conflictingFiles.length == files.length,
conflictingFileNames: conflictingFiles.keys.toList(),
conflictingFileNamesForFailedFiles: const [],
),
);
} else {
Expand Down Expand Up @@ -363,19 +413,20 @@ class UploadCubit extends Cubit<UploadState> {
)
.map((f) => f.id)
.getSingleOrNull();
final existingFileId = await _driveDao
final existingFileIds = await _driveDao
.filesInFolderWithName(
driveId: driveId,
name: folder.name,
parentFolderId: folder.parentFolderId,
)
.map((f) => f.id)
.getSingleOrNull();
.get();

if (existingFolderId != null) {
folder.id = existingFolderId;
foldersToSkip.add(folder);
}
if (existingFileId != null) {
if (existingFileIds.isNotEmpty) {
conflictingFolders.add(folder.name);
}
}
Expand Down Expand Up @@ -413,6 +464,8 @@ class UploadCubit extends Cubit<UploadState> {

if (uploadAction == UploadActions.skip) {
_removeFilesWithFileNameConflicts();
} else if (uploadAction == UploadActions.skipSuccessfulUploads) {
_removeSuccessfullyUploadedFiles();
}

logger.d(
Expand Down Expand Up @@ -920,6 +973,12 @@ class UploadCubit extends Cubit<UploadState> {
);
}

void _removeSuccessfullyUploadedFiles() {
files.removeWhere(
(file) => !failedFiles.contains(file.getIdentifier()),
);
}

void _removeFilesWithFolderNameConflicts() {
files.removeWhere((file) => conflictingFolders.contains(file.ioFile.name));
}
Expand Down
12 changes: 12 additions & 0 deletions lib/blocs/upload/upload_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,33 @@ class UploadSigningInProgress extends UploadState {

class UploadFileConflict extends UploadState {
final List<String> conflictingFileNames;
final List<String> conflictingFileNamesForFailedFiles;

final bool areAllFilesConflicting;

UploadFileConflict({
required this.conflictingFileNames,
required this.areAllFilesConflicting,
required this.conflictingFileNamesForFailedFiles,
});

@override
List<Object> get props => [conflictingFileNames];
}

class UploadConflictWithFailedFiles extends UploadFileConflict {
UploadConflictWithFailedFiles({
required super.conflictingFileNames,
required super.areAllFilesConflicting,
super.conflictingFileNamesForFailedFiles = const <String>[],
});
}

class UploadFolderNameConflict extends UploadFileConflict {
UploadFolderNameConflict({
required super.conflictingFileNames,
required super.areAllFilesConflicting,
super.conflictingFileNamesForFailedFiles = const <String>[],
});
}

Expand Down
138 changes: 93 additions & 45 deletions lib/components/upload_form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -287,63 +287,111 @@ class _UploadFormState extends State<UploadForm> {
),
],
);
} else if (state is UploadFileConflict) {
} else if (state is UploadConflictWithFailedFiles) {
return ArDriveStandardModal(
title: appLocalizationsOf(context)
.duplicateFiles(state.conflictingFileNames.length),
content: SizedBox(
width: kMediumDialogWidth,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
appLocalizationsOf(context)
.filesWithTheSameNameAlreadyExists(
state.conflictingFileNames.length,
title: 'Retry Failed Uploads?',
content: SizedBox(
width: kMediumDialogWidth,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'There are ${state.conflictingFileNamesForFailedFiles.length} file(s) marked with a red dot, indicating they failed to upload. Would you like to retry uploading these files by replacing the failed versions? This action will only affect the failed uploads and will not alter any successfully uploaded files. Alternatively, you can choose to skip these files and proceed with the others.',
style: ArDriveTypography.body.buttonNormalRegular(),
),
const SizedBox(height: 16),
Text(
'Conflicting files',
style: ArDriveTypography.body.buttonNormalRegular(),
),
const SizedBox(height: 8),
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 320),
child: SingleChildScrollView(
child: Text(
state.conflictingFileNamesForFailedFiles
.join(', \n'),
style: ArDriveTypography.body.buttonNormalRegular(),
),
style: ArDriveTypography.body.buttonNormalRegular(),
),
const SizedBox(height: 16),
Text(
appLocalizationsOf(context).conflictingFiles,
style: ArDriveTypography.body.buttonNormalRegular(),
),
],
),
),
actions: [
ModalAction(
action: () => context
.read<UploadCubit>()
.checkConflictingFiles(checkFailedFiles: false),
title: appLocalizationsOf(context).skipEmphasized,
),
ModalAction(
action: () => context
.read<UploadCubit>()
.prepareUploadPlanAndCostEstimates(
uploadAction: UploadActions.skipSuccessfulUploads),
title: 'Replace failed uploads',
),
],
);
} else if (state is UploadFileConflict) {
return ArDriveStandardModal(
title: appLocalizationsOf(context)
.duplicateFiles(state.conflictingFileNames.length),
content: SizedBox(
width: kMediumDialogWidth,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
appLocalizationsOf(context)
.filesWithTheSameNameAlreadyExists(
state.conflictingFileNames.length,
),
const SizedBox(height: 8),
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 320),
child: SingleChildScrollView(
child: Text(
state.conflictingFileNames.join(', \n'),
style:
ArDriveTypography.body.buttonNormalRegular(),
),
style: ArDriveTypography.body.buttonNormalRegular(),
),
const SizedBox(height: 16),
Text(
appLocalizationsOf(context).conflictingFiles,
style: ArDriveTypography.body.buttonNormalRegular(),
),
const SizedBox(height: 8),
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 320),
child: SingleChildScrollView(
child: Text(
state.conflictingFileNames.join(', \n'),
style: ArDriveTypography.body.buttonNormalRegular(),
),
),
],
),
),
actions: [
if (!state.areAllFilesConflicting)
ModalAction(
action: () => context
.read<UploadCubit>()
.prepareUploadPlanAndCostEstimates(
uploadAction: UploadActions.skip),
title: appLocalizationsOf(context).skipEmphasized,
),
ModalAction(
action: () => Navigator.of(context).pop(false),
title: appLocalizationsOf(context).cancelEmphasized,
),
],
),
),
actions: [
if (!state.areAllFilesConflicting)
ModalAction(
action: () => context
.read<UploadCubit>()
.prepareUploadPlanAndCostEstimates(
uploadAction: UploadActions.replace),
title: appLocalizationsOf(context).replaceEmphasized,
uploadAction: UploadActions.skip),
title: appLocalizationsOf(context).skipEmphasized,
),
]);
ModalAction(
action: () => Navigator.of(context).pop(false),
title: appLocalizationsOf(context).cancelEmphasized,
),
ModalAction(
action: () => context
.read<UploadCubit>()
.prepareUploadPlanAndCostEstimates(
uploadAction: UploadActions.replace),
title: appLocalizationsOf(context).replaceEmphasized,
),
],
);
} else if (state is UploadFileTooLarge) {
return ArDriveStandardModal(
title: appLocalizationsOf(context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ query FirstDriveEntityWithIdOwner($driveId: String!, $after: String) {
tags: [
{ name: "Drive-Id", values: [$driveId] }
{ name: "Entity-Type", values: ["drive"] }
{ name: "Drive-Privacy", values: ["public", "private"]}
]
) {
edges {
Expand Down
13 changes: 10 additions & 3 deletions test/blocs/upload_cubit_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,12 @@ void main() {
const TypeMatcher<UploadPreparationInProgress>(),
UploadFileConflict(
areAllFilesConflicting: true,
conflictingFileNames: const ['${tRootFolderId}1']),
conflictingFileNames: const [
'${tRootFolderId}1'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious, why is there a 1 added here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using addTestFilesToDb to add files on the database it creates the files with $rootFolderId$i or $nestedFolderId$i where i is the index of the file inside that folder.

As we used this method, to create a conflicting file we just need to add to its name the parent folder id + the index 1.

   batch.insertAll(
      db.fileEntries,
      [
        ...List.generate(
          rootFolderFileCount,
          (i) {
            final fileId = '$rootFolderId$i';
            return FileEntriesCompanion.insert(
              id: fileId,
              driveId: driveId,
              parentFolderId: rootFolderId,
              name: fileId,
              dataTxId: '${fileId}Data',
              size: 500,
              dateCreated: Value(defaultDate),
              lastModifiedDate: defaultDate,
              dataContentType: const Value(''),
              isHidden: const Value(false),
              path: '',
            );
          },
        )..shuffle(Random(0)),
        ...List.generate(
          nestedFolderFileCount,
          (i) {
            final fileId = '$nestedFolderId$i';
            return FileEntriesCompanion.insert(
              id: fileId,
              driveId: driveId,
              parentFolderId: nestedFolderId,
              name: fileId,
              dataTxId: '${fileId}Data',
              size: 500,
              dateCreated: Value(defaultDate),
              lastModifiedDate: defaultDate,
              dataContentType: const Value(''),
              isHidden: const Value(false),
              path: '',
            );
          },
        )..shuffle(Random(0)),
      ],
    );

],
conflictingFileNamesForFailedFiles: const [
'${tRootFolderId}1'
]),
]);

blocTest<UploadCubit, UploadState>(
Expand All @@ -351,8 +356,10 @@ void main() {
const TypeMatcher<UploadPreparationInitialized>(),
const TypeMatcher<UploadPreparationInProgress>(),
UploadFileConflict(
areAllFilesConflicting: false,
conflictingFileNames: const ['${tRootFolderId}1'])
areAllFilesConflicting: false,
conflictingFileNames: const ['${tRootFolderId}1'],
conflictingFileNamesForFailedFiles: const ['${tRootFolderId}1'],
)
]);

blocTest<UploadCubit, UploadState>(
Expand Down
Loading