Skip to content

Commit 45e11a5

Browse files
committed
Merge branch 'main' into 2091-map-proposal-elements-for-testing
2 parents 1292ab0 + 2b4cf67 commit 45e11a5

File tree

9 files changed

+202
-41
lines changed

9 files changed

+202
-41
lines changed

catalyst_voices/apps/voices/lib/pages/overall_spaces/space/user_proposal_selectors/workspace_overview_proposal_selector.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ class _WorkspaceDataProposalSelector extends StatelessWidget {
2828
BlocSelector<WorkspaceBloc, WorkspaceState,
2929
DataVisibilityState<List<Proposal>>>(
3030
selector: (state) {
31-
return (data: state.notPublished, show: state.showProposals);
31+
return (
32+
data: state.notPublished,
33+
show: state.showProposals && !state.isLoading
34+
);
3235
},
3336
builder: (context, state) {
3437
return Offstage(
Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,28 @@
1-
import 'package:catalyst_voices/widgets/indicators/voices_circular_progress_indicator.dart';
1+
import 'package:catalyst_voices/widgets/indicators/voices_loading_overlay.dart';
22
import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart';
33
import 'package:flutter/material.dart';
44
import 'package:flutter_bloc/flutter_bloc.dart';
55

66
class WorkspaceLoadingSelector extends StatelessWidget {
7-
const WorkspaceLoadingSelector({super.key});
7+
final Widget child;
8+
9+
const WorkspaceLoadingSelector({
10+
super.key,
11+
required this.child,
12+
});
813

914
@override
1015
Widget build(BuildContext context) {
1116
return BlocSelector<WorkspaceBloc, WorkspaceState, bool>(
1217
selector: (state) => state.isLoading,
13-
builder: (context, state) {
14-
return Offstage(
15-
offstage: !state,
16-
child: TickerMode(
17-
enabled: state,
18-
child: const _WorkspaceLoading(),
19-
),
18+
builder: (context, isLoading) {
19+
return Stack(
20+
children: [
21+
child,
22+
VoicesLoadingOverlay(show: isLoading),
23+
],
2024
);
2125
},
2226
);
2327
}
2428
}
25-
26-
class _WorkspaceLoading extends StatelessWidget {
27-
const _WorkspaceLoading();
28-
29-
@override
30-
Widget build(BuildContext context) {
31-
return const Align(
32-
alignment: Alignment.topCenter,
33-
child: Padding(
34-
padding: EdgeInsets.only(top: 32),
35-
child: VoicesCircularProgressIndicator(),
36-
),
37-
);
38-
}
39-
}

catalyst_voices/apps/voices/lib/pages/workspace/page/workspace_page.dart

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,20 @@ class _WorkspacePageState extends State<WorkspacePage>
3030
@override
3131
Widget build(BuildContext context) {
3232
return const Scaffold(
33-
body: SingleChildScrollView(
34-
child: Column(
35-
children: [
36-
WorkspaceHeader(),
37-
Stack(
38-
children: [
39-
WorkspaceErrorSelector(),
40-
WorkspaceLoadingSelector(),
41-
WorkspaceUserProposalsSelector(),
42-
],
43-
),
44-
SizedBox(height: 50),
45-
],
33+
body: WorkspaceLoadingSelector(
34+
child: SingleChildScrollView(
35+
child: Column(
36+
children: [
37+
WorkspaceHeader(),
38+
Stack(
39+
children: [
40+
WorkspaceErrorSelector(),
41+
WorkspaceUserProposalsSelector(),
42+
],
43+
),
44+
SizedBox(height: 50),
45+
],
46+
),
4647
),
4748
),
4849
);
@@ -72,6 +73,8 @@ class _WorkspacePageState extends State<WorkspacePage>
7273
);
7374
case SubmissionCloseDate():
7475
unawaited(_showSubmissionClosingWarningDialog(signal.date));
76+
case ForgetProposalSuccessWorkspaceSignal():
77+
_showForgetSuccessSnackBar();
7578
}
7679
}
7780

@@ -113,6 +116,17 @@ class _WorkspacePageState extends State<WorkspacePage>
113116
).show(context);
114117
}
115118

119+
void _showForgetSuccessSnackBar() {
120+
VoicesSnackBar.hideCurrent(context);
121+
122+
VoicesSnackBar(
123+
type: VoicesSnackBarType.success,
124+
behavior: SnackBarBehavior.floating,
125+
title: context.l10n.successProposalForgot,
126+
message: context.l10n.successProposalForgotDescription,
127+
).show(context);
128+
}
129+
116130
Future<void> _showSubmissionClosingWarningDialog([
117131
DateTime? submissionCloseDate,
118132
]) async {

catalyst_voices/apps/voices/lib/widgets/indicators/voices_loading_overlay.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ class _VoicesLoadingOverlayState extends State<VoicesLoadingOverlay>
114114
);
115115

116116
_showingSince = widget.show ? DateTimeExt.now() : null;
117+
118+
if (widget.show) {
119+
_fadeInAnimController.value = _fadeInAnimController.upperBound;
120+
}
117121
}
118122

119123
void _hideNow() {

catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace_bloc.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,15 @@ final class WorkspaceBloc extends Bloc<WorkspaceEvent, WorkspaceState>
7575
Emitter<WorkspaceState> emit,
7676
) async {
7777
try {
78+
emit(state.copyWith(isLoading: true));
7879
await _proposalService.deleteDraftProposal(event.ref);
80+
emit(state.copyWith(userProposals: _removeProposal(event.ref)));
7981
emitSignal(const DeletedDraftWorkspaceSignal());
8082
} catch (error, stackTrace) {
8183
_logger.severe('Delete proposal failed', error, stackTrace);
8284
emitError(const LocalizedProposalDeletionException());
85+
} finally {
86+
emit(state.copyWith(isLoading: false));
8387
}
8488
}
8589

@@ -138,12 +142,18 @@ final class WorkspaceBloc extends Bloc<WorkspaceEvent, WorkspaceState>
138142
return emitError(const LocalizedUnknownException());
139143
}
140144
try {
145+
emit(state.copyWith(isLoading: true));
141146
await _proposalService.forgetProposal(
142147
proposalRef: proposal.selfRef as SignedDocumentRef,
143148
categoryId: proposal.categoryId,
144149
);
150+
emit(state.copyWith(userProposals: _removeProposal(event.ref)));
151+
emitSignal(const ForgetProposalSuccessWorkspaceSignal());
145152
} catch (e, stackTrace) {
153+
emitError(LocalizedException.create(e));
146154
_logger.severe('Error forgetting proposal', e, stackTrace);
155+
} finally {
156+
emit(state.copyWith(isLoading: false));
147157
}
148158
}
149159

@@ -164,12 +174,16 @@ final class WorkspaceBloc extends Bloc<WorkspaceEvent, WorkspaceState>
164174
Emitter<WorkspaceState> emit,
165175
) async {
166176
try {
177+
emit(state.copyWith(isLoading: true));
167178
final ref = await _proposalService.importProposal(event.proposalData);
168179
emitSignal(ImportedProposalWorkspaceSignal(proposalRef: ref));
169180
} catch (error, stackTrace) {
170181
_logger.severe('Importing proposal failed', error, stackTrace);
182+
emit(state.copyWith(isLoading: false));
171183
emitError(LocalizedException.create(error));
172184
}
185+
// We don't need to emit isLoading false here because it will be emitted
186+
// in the stream subscription.
173187
}
174188

175189
Future<void> _loadProposals(
@@ -185,6 +199,13 @@ final class WorkspaceBloc extends Bloc<WorkspaceEvent, WorkspaceState>
185199
);
186200
}
187201

202+
List<Proposal> _removeProposal(
203+
DocumentRef proposalRef,
204+
) {
205+
return [...state.userProposals]
206+
..removeWhere((e) => e.selfRef.id == proposalRef.id);
207+
}
208+
188209
void _setupProposalsSubscription() {
189210
_proposalsSubscription = _proposalService.watchUserProposals().listen(
190211
(proposals) {

catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace_signal.dart

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import 'package:equatable/equatable.dart';
33

44
final class DeletedDraftWorkspaceSignal extends WorkspaceSignal {
55
const DeletedDraftWorkspaceSignal();
6+
}
67

7-
@override
8-
List<Object?> get props => [];
8+
final class ForgetProposalSuccessWorkspaceSignal extends WorkspaceSignal {
9+
const ForgetProposalSuccessWorkspaceSignal();
910
}
1011

1112
final class ImportedProposalWorkspaceSignal extends WorkspaceSignal {
@@ -37,4 +38,7 @@ final class SubmissionCloseDate extends WorkspaceSignal {
3738

3839
sealed class WorkspaceSignal extends Equatable {
3940
const WorkspaceSignal();
41+
42+
@override
43+
List<Object?> get props => [];
4044
}

catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace_state.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ final class WorkspaceState extends Equatable {
4040
.toList();
4141
bool get showError => error != null && !isLoading;
4242
bool get showLoading => isLoading;
43-
bool get showProposals => error == null && !isLoading;
43+
bool get showProposals => error == null;
4444
DateTime? get submissionCloseDate => timelineItems
4545
.firstWhereOrNull(
4646
(e) => e.stage == CampaignTimelineStage.proposalSubmission,
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import 'dart:typed_data';
2+
3+
import 'package:bloc_test/bloc_test.dart';
4+
import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart';
5+
import 'package:catalyst_voices_models/catalyst_voices_models.dart';
6+
import 'package:catalyst_voices_services/catalyst_voices_services.dart';
7+
import 'package:flutter_test/flutter_test.dart';
8+
import 'package:mocktail/mocktail.dart';
9+
10+
void main() {
11+
group(WorkspaceBloc, () {
12+
late MockCampaignService mockCampaignService;
13+
late MockProposalService mockProposalService;
14+
late MockDocumentMapper mockDocumentMapper;
15+
late MockDownloaderService mockDownloaderService;
16+
17+
late WorkspaceBloc workspaceBloc;
18+
19+
final proposalRef = SignedDocumentRef.generateFirstRef();
20+
final documentData = DocumentData(
21+
metadata: DocumentDataMetadata(
22+
type: DocumentType.proposalDocument,
23+
selfRef: proposalRef,
24+
template: SignedDocumentRef.generateFirstRef(),
25+
categoryId: SignedDocumentRef.generateFirstRef(),
26+
),
27+
content: const DocumentDataContent({}),
28+
);
29+
30+
setUpAll(() {
31+
registerFallbackValue(SignedDocumentRef.generateFirstRef());
32+
registerFallbackValue(documentData);
33+
registerFallbackValue(Uint8List(0));
34+
});
35+
36+
setUp(() async {
37+
mockCampaignService = MockCampaignService();
38+
mockProposalService = MockProposalService();
39+
mockDocumentMapper = MockDocumentMapper();
40+
mockDownloaderService = MockDownloaderService();
41+
42+
workspaceBloc = WorkspaceBloc(
43+
mockCampaignService,
44+
mockProposalService,
45+
mockDocumentMapper,
46+
mockDownloaderService,
47+
);
48+
});
49+
50+
tearDown(() async {
51+
await workspaceBloc.close();
52+
});
53+
test('initial state is correct', () {
54+
expect(workspaceBloc.state, const WorkspaceState());
55+
});
56+
57+
blocTest<WorkspaceBloc, WorkspaceState>(
58+
'emit loading state and loaded state when watching proposals succeeds',
59+
build: () {
60+
when(() => mockProposalService.watchUserProposals()).thenAnswer(
61+
(_) => Stream.value(
62+
[ProposalWithVersionX.dummy(ProposalPublish.localDraft)],
63+
),
64+
);
65+
return workspaceBloc;
66+
},
67+
act: (bloc) => bloc.add(const WatchUserProposalsEvent()),
68+
expect: () => [
69+
isA<WorkspaceState>().having((s) => s.isLoading, 'isLoading', true),
70+
isA<WorkspaceState>().having((s) => s.isLoading, 'isLoading', false),
71+
],
72+
);
73+
74+
blocTest<WorkspaceBloc, WorkspaceState>(
75+
'watch user proposals - success',
76+
build: () {
77+
when(() => mockProposalService.watchUserProposals()).thenAnswer(
78+
(_) => Stream.value([
79+
ProposalWithVersionX.dummy(ProposalPublish.localDraft),
80+
ProposalWithVersionX.dummy(ProposalPublish.localDraft),
81+
]),
82+
);
83+
return workspaceBloc;
84+
},
85+
act: (bloc) => bloc.add(const WatchUserProposalsEvent()),
86+
expect: () => [
87+
isA<WorkspaceState>().having((s) => s.isLoading, 'isLoading', true),
88+
isA<WorkspaceState>()
89+
.having((s) => s.isLoading, 'isLoading', false)
90+
.having((s) => s.userProposals.length, 'proposals count', 2),
91+
],
92+
);
93+
94+
blocTest<WorkspaceBloc, WorkspaceState>(
95+
'watch user proposals - failure',
96+
build: () {
97+
when(() => mockProposalService.watchUserProposals())
98+
.thenAnswer((_) => Stream.error(Exception('Failed to load')));
99+
return workspaceBloc;
100+
},
101+
act: (bloc) => bloc.add(const WatchUserProposalsEvent()),
102+
expect: () => [
103+
isA<WorkspaceState>().having((s) => s.isLoading, 'isLoading', true),
104+
isA<WorkspaceState>()
105+
.having((s) => s.isLoading, 'isLoading', false)
106+
.having((s) => s.error, 'has error', isNotNull),
107+
],
108+
);
109+
});
110+
}
111+
112+
class MockCampaignService extends Mock implements CampaignService {}
113+
114+
class MockDocumentMapper extends Mock implements DocumentMapper {}
115+
116+
class MockDownloaderService extends Mock implements DownloaderService {}
117+
118+
class MockProposalService extends Mock implements ProposalService {}

catalyst_voices/packages/internal/catalyst_voices_localization/lib/l10n/intl_en.arb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2124,6 +2124,14 @@
21242124
"@successProposalDeletedDescription": {
21252125
"description": "Description of success message when draft is deleted"
21262126
},
2127+
"successProposalForgot": "Proposal forgotten",
2128+
"@successProposalForgot": {
2129+
"description": "Success message when public draft/final proposal is forgotten"
2130+
},
2131+
"successProposalForgotDescription": "Your local proposal has been forgotten. No one can see it anymore.",
2132+
"@successProposalForgotDescription": {
2133+
"description": "Description of success message when public draft/final proposal is forgotten"
2134+
},
21272135
"errorProposalDeleted": "Error deleting proposal",
21282136
"@errorProposalDeleted": {
21292137
"description": "Error message when draft is not deleted"

0 commit comments

Comments
 (0)