Skip to content

Commit b46436e

Browse files
authored
feat(cat-voices): adding read view for proposal page (#2537)
* feat: adding read view for proposal page * feat: range status * feat: read Only mode * feat: update campaign stage cubit to new implemantation * test: adding test * feat: self review * fix: spelling * feat: adding appbar * fix: format
1 parent 47fc157 commit b46436e

File tree

9 files changed

+208
-41
lines changed

9 files changed

+208
-41
lines changed

catalyst_voices/apps/voices/lib/pages/proposal/proposal_page.dart

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,32 @@ class ProposalPage extends StatefulWidget {
3636
State<ProposalPage> createState() => _ProposalPageState();
3737
}
3838

39+
class _AppBar extends StatelessWidget implements PreferredSizeWidget {
40+
const _AppBar();
41+
42+
@override
43+
Size get preferredSize => const VoicesAppBar().preferredSize;
44+
45+
@override
46+
Widget build(BuildContext context) {
47+
final readOnlyMode = context.select<ProposalCubit, bool>((cubit) => cubit.state.readOnlyMode);
48+
49+
return VoicesAppBar(
50+
automaticallyImplyLeading: false,
51+
actions: [
52+
Offstage(
53+
offstage: readOnlyMode,
54+
child: const SessionActionHeader(),
55+
),
56+
Offstage(
57+
offstage: readOnlyMode,
58+
child: const SessionStateHeader(),
59+
),
60+
],
61+
);
62+
}
63+
}
64+
3965
class _ProposalPageState extends State<ProposalPage>
4066
with
4167
ErrorHandlerStateMixin<ProposalCubit, ProposalPage>,
@@ -50,13 +76,7 @@ class _ProposalPageState extends State<ProposalPage>
5076
return SegmentsControllerScope(
5177
controller: _segmentsController,
5278
child: Scaffold(
53-
appBar: const VoicesAppBar(
54-
automaticallyImplyLeading: false,
55-
actions: [
56-
SessionActionHeader(),
57-
SessionStateHeader(),
58-
],
59-
),
79+
appBar: const _AppBar(),
6080
endDrawer: const OpportunitiesDrawer(),
6181
body: ProposalHeaderWrapper(
6282
child: ProposalSidebars(

catalyst_voices/apps/voices/lib/routes/guards/proposal_submission_guard.dart

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,17 @@ final class ProposalSubmissionGuard implements RouteGuard {
1212

1313
@override
1414
FutureOr<String?> redirect(BuildContext context, GoRouterState state) {
15+
final path = state.path;
1516
final campaignState = context.read<CampaignStageCubit>().state;
16-
if (campaignState is ProposalSubmissionStage) {
17-
if (state.path == const CampaignStageRoute().location) {
18-
return const DiscoveryRoute().location;
19-
}
20-
return null;
21-
} else {
22-
return const CampaignStageRoute().location;
23-
}
17+
18+
return switch (campaignState) {
19+
AfterProposalSubmissionStage() when path != null && ProposalRoute.isPath(path) => null,
20+
AfterProposalSubmissionStage() => const CampaignStageRoute().location,
21+
PreProposalSubmissionStage() when path != null && ProposalRoute.isPath(path) =>
22+
const CampaignStageRoute().location,
23+
ProposalSubmissionStage() when state.matchedLocation == const CampaignStageRoute().location =>
24+
const DiscoveryRoute().location,
25+
_ => null,
26+
};
2427
}
2528
}

catalyst_voices/apps/voices/lib/routes/routing/proposal_route.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ final class ProposalRoute extends GoRouteData
3939
version: version,
4040
isDraft: local,
4141
);
42-
4342
return ProposalPage(ref: ref);
4443
}
44+
45+
static bool isPath(String path) {
46+
return ($proposalRoute as GoRoute).path == path;
47+
}
4548
}

catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/campaign_stage/campaign_stage_cubit.dart

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -27,29 +27,20 @@ class CampaignStageCubit extends Cubit<CampaignStageState> {
2727
Future<void> getCampaignStage() async {
2828
try {
2929
emit(const LoadingCampaignStage());
30-
final campaignTimeline = await _campaignService.getCampaignTimeline();
31-
32-
final now = DateTime.now();
33-
final proposalSubmissionStage = campaignTimeline.firstWhere(
34-
(e) => e.stage == CampaignTimelineStage.proposalSubmission,
35-
orElse: () => throw const NotFoundException(
36-
message: 'Proposal submission stage not found',
37-
),
30+
final campaignTimeline = await _campaignService.getCampaignTimelineByStage(
31+
CampaignTimelineStage.proposalSubmission,
3832
);
33+
final dateRangeStatus = campaignTimeline.timeline.rangeStatusNow();
34+
final startDate = campaignTimeline.timeline.from;
3935

40-
if (proposalSubmissionStage.timeline.isInRange(now)) {
41-
emit(const ProposalSubmissionStage());
42-
_startCountdownTimer(proposalSubmissionStage.timeline.to);
43-
} else if (proposalSubmissionStage.timeline.isBeforeRange(now)) {
44-
emit(
45-
PreProposalSubmissionStage(
46-
startDate: proposalSubmissionStage.timeline.from,
47-
),
48-
);
49-
} else {
50-
emit(const AfterProposalSubmissionStage());
51-
}
52-
_logger.info(state.toString());
36+
return switch (dateRangeStatus) {
37+
DateRangeStatus.after => emit(const AfterProposalSubmissionStage()),
38+
DateRangeStatus.before => emit(PreProposalSubmissionStage(startDate: startDate)),
39+
DateRangeStatus.inRange => {
40+
_startCountdownTimer(startDate),
41+
emit(const ProposalSubmissionStage()),
42+
}
43+
};
5344
} catch (error, stackTrace) {
5445
_logger.severe('getCampaignStage error', error, stackTrace);
5546
emit(const ErrorSubmissionStage(LocalizedUnknownException()));

catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal/proposal_cubit.dart

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,8 @@ final class ProposalCubit extends Cubit<ProposalState>
3535
this._campaignService,
3636
this._documentMapper,
3737
) : super(const ProposalState()) {
38-
_cache = _cache.copyWith(
39-
activeAccountId: Optional(_userService.user.activeAccount?.catalystId),
40-
);
38+
_cache =
39+
_cache.copyWith(activeAccountId: Optional(_userService.user.activeAccount?.catalystId));
4140
_activeAccountIdSub = _userService.watchUser
4241
.map((event) => event.activeAccount?.catalystId)
4342
.distinct()
@@ -57,6 +56,7 @@ final class ProposalCubit extends Cubit<ProposalState>
5756

5857
Future<void> load({required DocumentRef ref}) async {
5958
try {
59+
final isReadOnlyMode = await _isReadOnlyMode();
6060
_logger.info('Loading $ref');
6161

6262
_cache = _cache.copyWith(ref: Optional.of(ref));
@@ -88,7 +88,7 @@ final class ProposalCubit extends Cubit<ProposalState>
8888
if (!isClosed) {
8989
final proposalState = _rebuildProposalState();
9090

91-
emit(ProposalState(data: proposalState));
91+
emit(ProposalState(data: proposalState, readOnlyMode: isReadOnlyMode));
9292

9393
if (proposalState.isCurrentVersionLatest == false) {
9494
emitSignal(const ViewingOlderVersionSignal());
@@ -379,6 +379,18 @@ final class ProposalCubit extends Cubit<ProposalState>
379379
emit(state.copyWith(data: _rebuildProposalState()));
380380
}
381381

382+
Future<bool> _isReadOnlyMode() async {
383+
final campaignTimeline = await _campaignService.getCampaignTimelineByStage(
384+
CampaignTimelineStage.proposalSubmission,
385+
);
386+
final dateRangeStatus = campaignTimeline.timeline.rangeStatusNow();
387+
388+
return switch (dateRangeStatus) {
389+
DateRangeStatus.after => true,
390+
_ => false,
391+
};
392+
}
393+
382394
ProposalViewData _rebuildProposalState() {
383395
final proposal = _cache.proposal;
384396
final category = _cache.category;

catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal/proposal_state.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ final class ProposalState extends Equatable {
88
final ProposalViewData data;
99
final CommentsState comments;
1010
final LocalizedException? error;
11+
final bool readOnlyMode;
1112

1213
const ProposalState({
1314
this.isLoading = false,
1415
this.data = const ProposalViewData(),
1516
this.comments = const CommentsState(),
1617
this.error,
18+
this.readOnlyMode = false,
1719
});
1820

1921
@override
@@ -22,6 +24,7 @@ final class ProposalState extends Equatable {
2224
data,
2325
comments,
2426
error,
27+
readOnlyMode,
2528
];
2629

2730
bool get showData => !showError;
@@ -33,12 +36,14 @@ final class ProposalState extends Equatable {
3336
ProposalViewData? data,
3437
CommentsState? comments,
3538
Optional<LocalizedException>? error,
39+
bool? readOnlyMode,
3640
}) {
3741
return ProposalState(
3842
isLoading: isLoading ?? this.isLoading,
3943
data: data ?? this.data,
4044
comments: comments ?? this.comments,
4145
error: error.dataOr(this.error),
46+
readOnlyMode: readOnlyMode ?? this.readOnlyMode,
4247
);
4348
}
4449

catalyst_voices/packages/internal/catalyst_voices_services/lib/src/campaign/campaign_service.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ abstract interface class CampaignService {
1818

1919
Future<List<CampaignTimeline>> getCampaignTimeline();
2020

21+
Future<CampaignTimeline> getCampaignTimelineByStage(CampaignTimelineStage stage);
22+
2123
Future<CampaignCategory> getCategory(SignedDocumentRef ref);
2224

2325
Future<CurrentCampaign> getCurrentCampaign();
@@ -74,6 +76,16 @@ final class CampaignServiceImpl implements CampaignService {
7476
return _campaignRepository.getCampaignTimeline();
7577
}
7678

79+
@override
80+
Future<CampaignTimeline> getCampaignTimelineByStage(CampaignTimelineStage stage) async {
81+
final timeline = await getCampaignTimeline();
82+
final timelineStage = timeline.firstWhere(
83+
(element) => element.stage == stage,
84+
orElse: () => throw (StateError('Stage $stage not found')),
85+
);
86+
return timelineStage;
87+
}
88+
7789
@override
7890
Future<CampaignCategory> getCategory(SignedDocumentRef ref) async {
7991
final category = await _campaignRepository.getCategory(ref);

catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/range/date_range.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,21 @@ class DateRange extends Equatable {
6464
bool isTodayInRange() {
6565
return isInRange(DateTime.now());
6666
}
67+
68+
DateRangeStatus rangeStatusNow() {
69+
final now = DateTime.now();
70+
if (isInRange(now)) {
71+
return DateRangeStatus.inRange;
72+
} else if (isBeforeRange(now)) {
73+
return DateRangeStatus.before;
74+
} else {
75+
return DateRangeStatus.after;
76+
}
77+
}
78+
}
79+
80+
enum DateRangeStatus {
81+
before,
82+
inRange,
83+
after,
6784
}

catalyst_voices/packages/internal/catalyst_voices_shared/test/src/range/date_range_test.dart

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ import 'package:test/test.dart';
33

44
void main() {
55
group(DateRange, () {
6+
late final DateTime now;
7+
8+
setUpAll(() {
9+
now = DateTimeExt.now();
10+
});
11+
612
test('is not in range', () {
713
// Given
814
final range = DateRange(from: DateTime(2025, 1, 20), to: DateTime(2025, 1, 22));
@@ -120,5 +126,103 @@ void main() {
120126
// Then
121127
expect(isInRange, isTrue);
122128
});
129+
130+
test('rangeStatusNow returns inRange when current date is within range', () {
131+
// Given
132+
final range = DateRange(
133+
from: now.subtract(const Duration(days: 1)),
134+
to: now.add(const Duration(days: 1)),
135+
);
136+
137+
// When
138+
final status = range.rangeStatusNow();
139+
140+
// Then
141+
expect(status, equals(DateRangeStatus.inRange));
142+
});
143+
144+
test('rangeStatusNow returns before when current date is before range', () {
145+
// Given
146+
final range = DateRange(
147+
from: now.add(const Duration(days: 1)),
148+
to: now.add(const Duration(days: 3)),
149+
);
150+
151+
// When
152+
final status = range.rangeStatusNow();
153+
154+
// Then
155+
expect(status, equals(DateRangeStatus.before));
156+
});
157+
158+
test('rangeStatusNow returns after when current date is after range', () {
159+
// Given
160+
final range = DateRange(
161+
from: now.subtract(const Duration(days: 3)),
162+
to: now.subtract(const Duration(days: 1)),
163+
);
164+
165+
// When
166+
final status = range.rangeStatusNow();
167+
168+
// Then
169+
expect(status, equals(DateRangeStatus.after));
170+
});
171+
172+
test('rangeStatusNow returns inRange when current date equals from date', () {
173+
// Given
174+
final range = DateRange(
175+
from: now,
176+
to: now.add(const Duration(days: 1)),
177+
);
178+
179+
// When
180+
final status = range.rangeStatusNow();
181+
182+
// Then
183+
expect(status, equals(DateRangeStatus.inRange));
184+
});
185+
186+
test('rangeStatusNow returns inRange when from is null and current date is before to', () {
187+
// Given
188+
final range = DateRange(
189+
from: null,
190+
to: now.add(const Duration(days: 1)),
191+
);
192+
193+
// When
194+
final status = range.rangeStatusNow();
195+
196+
// Then
197+
expect(status, equals(DateRangeStatus.inRange));
198+
});
199+
200+
test('rangeStatusNow returns inRange when to is null and current date is after from', () {
201+
// Given
202+
final range = DateRange(
203+
from: DateTime(2025, 1, 20),
204+
to: null,
205+
);
206+
207+
// When
208+
final status = range.rangeStatusNow();
209+
210+
// Then
211+
expect(status, equals(DateRangeStatus.inRange));
212+
});
213+
214+
test('rangeStatusNow returns inRange when both from and to are null', () {
215+
// Given
216+
const range = DateRange(
217+
from: null,
218+
to: null,
219+
);
220+
221+
// When
222+
final status = range.rangeStatusNow();
223+
224+
// Then
225+
expect(status, equals(DateRangeStatus.inRange));
226+
});
123227
});
124228
}

0 commit comments

Comments
 (0)