Skip to content

Commit

Permalink
feat(cat-voices): Import proposal (#1929)
Browse files Browse the repository at this point in the history
* feat: downloader service working with generic data, not only with Uri's

* feat: export proposal

* feat: export/import proposal

* fix: dialog export

* fix: confirm dialog buttons are opposite

* chore: cleanup

* fix: to lower case

* feat: import proposal

* chore: cleanup

* chore: logs

* style: reformat

* fix: route settings for upload file dialog

* chore: review adjustments

* chore: cleanup
  • Loading branch information
dtscalac authored Feb 28, 2025
1 parent 66aa3c8 commit 19d6ae6
Show file tree
Hide file tree
Showing 18 changed files with 294 additions and 133 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ final class Dependencies extends DependencyProvider {
..registerFactory<WorkspaceBloc>(() {
return WorkspaceBloc(
get<CampaignService>(),
get<ProposalService>(),
);
})
..registerFactory<ProposalBuilderBloc>(() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class UploadSeedPhraseDialog {
static Future<List<SeedPhraseWord>> show(BuildContext context) async {
final file = await VoicesUploadFileDialog.show(
context,
routeSettings: const RouteSettings(name: '/upload-seed-phrase'),
title: context.l10n.uploadKeychainTitle,
itemNameToUpload: context.l10n.key,
info: context.l10n.uploadKeychainInfo,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,40 @@
part of 'workspace_header.dart';

class _ImportProposalButton extends StatelessWidget {
class _ImportProposalButton extends StatefulWidget {
const _ImportProposalButton();

@override
State<_ImportProposalButton> createState() => _ImportProposalButtonState();
}

class _ImportProposalButtonState extends State<_ImportProposalButton> {
@override
Widget build(BuildContext context) {
return VoicesOutlinedButton(
onTap: () {
// TODO(dtscalac): import proposal
unawaited(const ProposalBuilderDraftRoute().push(context));
},
onTap: _importProposal,
child: Text(context.l10n.importProposal),
);
}

Future<void> _importProposal() async {
final proposal = await _ImportProposalDialog.show(context);
if (proposal != null && mounted) {
context.read<WorkspaceBloc>().add(ImportProposalEvent(proposal));
}
}
}

class _ImportProposalDialog {
static Future<Uint8List?> show(BuildContext context) async {
final file = await VoicesUploadFileDialog.show(
context,
routeSettings: const RouteSettings(name: '/import-proposal'),
title: context.l10n.proposalImportDialogTitle,
itemNameToUpload: context.l10n.proposal,
info: context.l10n.proposalImportDialogDescription,
allowedExtensions: [ProposalDocument.exportFileExt],
);

return file?.bytes;
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import 'dart:async';
import 'dart:typed_data';

import 'package:catalyst_voices/common/ext/space_ext.dart';
import 'package:catalyst_voices/routes/routing/proposal_builder_route.dart';
import 'package:catalyst_voices/widgets/campaign_timeline/campaign_timeline_card.dart';
import 'package:catalyst_voices/widgets/widgets.dart';
import 'package:catalyst_voices_assets/catalyst_voices_assets.dart';
import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart';
import 'package:catalyst_voices_localization/catalyst_voices_localization.dart';
import 'package:catalyst_voices_models/catalyst_voices_models.dart';
import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

part 'create_proposal_button.dart';
part 'import_proposal_button.dart';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import 'dart:async';

import 'package:catalyst_voices/common/error_handler.dart';
import 'package:catalyst_voices/pages/workspace/header/workspace_header.dart';
import 'package:catalyst_voices/pages/workspace/my_proposals/workspace_my_proposals_selector.dart';
import 'package:catalyst_voices/pages/workspace/page/workspace_empty.dart';
import 'package:catalyst_voices/pages/workspace/page/workspace_error.dart';
import 'package:catalyst_voices/pages/workspace/page/workspace_loading.dart';
import 'package:catalyst_voices/routes/routing/proposal_builder_route.dart';
import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart';
import 'package:catalyst_voices_models/catalyst_voices_models.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

Expand All @@ -14,7 +19,10 @@ class WorkspacePage extends StatefulWidget {
State<WorkspacePage> createState() => _WorkspacePageState();
}

class _WorkspacePageState extends State<WorkspacePage> {
class _WorkspacePageState extends State<WorkspacePage>
with ErrorHandlerStateMixin<WorkspaceBloc, WorkspacePage> {
StreamSubscription<dynamic>? _importedProposalSub;

@override
Widget build(BuildContext context) {
return const Scaffold(
Expand All @@ -37,9 +45,28 @@ class _WorkspacePageState extends State<WorkspacePage> {
);
}

@override
void dispose() {
unawaited(_importedProposalSub?.cancel());
_importedProposalSub = null;
super.dispose();
}

@override
void initState() {
super.initState();
context.read<WorkspaceBloc>().add(const LoadProposalsEvent());
final bloc = context.read<WorkspaceBloc>();

// ignore: cascade_invocations
bloc.add(const LoadProposalsEvent());

_importedProposalSub =
bloc.stream.map((e) => e.importedProposalRef).listen(_onImportProposal);
}

void _onImportProposal(DocumentRef? ref) {
if (ref != null) {
unawaited(ProposalBuilderRoute(proposalId: ref.id).push(context));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,12 @@ class VoicesUploadFileDialog extends StatefulWidget {
this.onUpload,
});

@override
State<VoicesUploadFileDialog> createState() => _VoicesUploadFileDialogState();

static Future<VoicesFile?> show(
BuildContext context, {
required RouteSettings routeSettings,
required String title,
String? itemNameToUpload,
String? info,
Expand All @@ -42,7 +46,7 @@ class VoicesUploadFileDialog extends StatefulWidget {
}) {
return VoicesDialog.show<VoicesFile?>(
context: context,
routeSettings: const RouteSettings(name: '/upload-file'),
routeSettings: routeSettings,
builder: (context) {
return VoicesUploadFileDialog(
title: title,
Expand All @@ -54,56 +58,6 @@ class VoicesUploadFileDialog extends StatefulWidget {
},
);
}

@override
State<VoicesUploadFileDialog> createState() => _VoicesUploadFileDialogState();
}

class _VoicesUploadFileDialogState extends State<VoicesUploadFileDialog> {
VoicesFile? _selectedFile;
bool _isUploading = false;

@override
Widget build(BuildContext context) {
return VoicesSinglePaneDialog(
constraints: const BoxConstraints(maxWidth: 600, maxHeight: 450),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_Title(widget.title),
const SizedBox(height: 24),
_UploadContainer(
itemNameToUpload: widget.itemNameToUpload,
info: widget.info,
allowedExtensions: widget.allowedExtensions,
onFileSelected: (file) {
setState(() {
_selectedFile = file;
});
},
),
if (_selectedFile != null)
_InfoContainer(
selectedFilename: _selectedFile!.name,
isUploading: _isUploading,
),
const SizedBox(height: 24),
_Buttons(
selectedFile: _selectedFile,
onUpload: (file) async {
setState(() {
_isUploading = true;
});
await widget.onUpload?.call(file);
},
),
],
),
),
);
}
}

class _Buttons extends StatefulWidget {
Expand Down Expand Up @@ -215,6 +169,20 @@ class _InfoContainer extends StatelessWidget {
}
}

class _Title extends StatelessWidget {
final String title;

const _Title(this.title);

@override
Widget build(BuildContext context) {
return Text(
title.toUpperCase(),
style: Theme.of(context).textTheme.titleLarge,
);
}
}

class _UploadContainer extends StatefulWidget {
final String itemNameToUpload;
final String? info;
Expand Down Expand Up @@ -350,16 +318,49 @@ class _UploadContainerState extends State<_UploadContainer> {
}
}

class _Title extends StatelessWidget {
final String title;

const _Title(this.title);
class _VoicesUploadFileDialogState extends State<VoicesUploadFileDialog> {
VoicesFile? _selectedFile;
bool _isUploading = false;

@override
Widget build(BuildContext context) {
return Text(
title.toUpperCase(),
style: Theme.of(context).textTheme.titleLarge,
return VoicesSinglePaneDialog(
constraints: const BoxConstraints(maxWidth: 600, maxHeight: 450),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_Title(widget.title),
const SizedBox(height: 24),
_UploadContainer(
itemNameToUpload: widget.itemNameToUpload,
info: widget.info,
allowedExtensions: widget.allowedExtensions,
onFileSelected: (file) {
setState(() {
_selectedFile = file;
});
},
),
if (_selectedFile != null)
_InfoContainer(
selectedFilename: _selectedFile!.name,
isUploading: _isUploading,
),
const SizedBox(height: 24),
_Buttons(
selectedFile: _selectedFile,
onUpload: (file) async {
setState(() {
_isUploading = true;
});
await widget.onUpload?.call(file);
},
),
],
),
),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,12 @@ final class ProposalBuilderBloc
document: _buildDocument(),
);

final filename = '${event.filePrefix}_$proposalId';
const extension = ProposalDocument.exportFileExt;

await _downloaderService.download(
data: encodedProposal,
filename: '${event.filePrefix}_$proposalId.json',
filename: '$filename.$extension',
);
} catch (error, stackTrace) {
_logger.severe('Exporting proposal failed', error, stackTrace);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import 'package:catalyst_voices_models/catalyst_voices_models.dart';
import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart';
import 'package:equatable/equatable.dart';
import 'package:uuid/uuid.dart';

final class ProposalBuilderMetadata extends Equatable {
final ProposalPublish publish;
Expand All @@ -19,10 +18,9 @@ final class ProposalBuilderMetadata extends Equatable {
});

factory ProposalBuilderMetadata.newDraft({required DocumentRef templateRef}) {
final id = const Uuid().v7();
return ProposalBuilderMetadata(
publish: ProposalPublish.localDraft,
documentRef: DraftRef(id: id, version: id),
documentRef: DraftRef.generateFirstRef(),
templateRef: templateRef,
currentIteration: 0,
);
Expand Down
Loading

0 comments on commit 19d6ae6

Please sign in to comment.