Skip to content

Commit b501960

Browse files
committed
feat: mapping collaborators
1 parent 0e17eb9 commit b501960

File tree

11 files changed

+331
-38
lines changed

11 files changed

+331
-38
lines changed

catalyst_voices/packages/internal/catalyst_voices_models/lib/src/catalyst_voices_models.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export 'pagination/page_request.dart';
9696
export 'permissions/exceptions/permission_exceptions.dart';
9797
export 'proposal/core_proposal.dart';
9898
export 'proposal/data/proposal_brief_data.dart';
99+
export 'proposal/data/proposal_data_collaborator.dart';
99100
export 'proposal/data/proposal_data_v2.dart';
100101
export 'proposal/data/proposals_total_ask.dart';
101102
export 'proposal/data/raw_proposal.dart';

catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/data/proposal_brief_data.dart

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ final class ProposalBriefData extends Equatable {
1919
final bool isFavorite;
2020
final ProposalBriefDataVotes? votes;
2121
final List<ProposalBriefDataVersion>? versions;
22-
final List<ProposalBriefDataCollaborator>? collaborators;
22+
final List<ProposalDataCollaborator>? collaborators;
2323

2424
const ProposalBriefData({
2525
required this.id,
@@ -58,19 +58,10 @@ final class ProposalBriefData extends Equatable {
5858
// Proposal Brief do not support "removed" or "left" status.
5959
final collaborators = data.proposal.metadata.collaborators?.map(
6060
(id) {
61-
final action = collaboratorsActions[id.toSignificant()]?.action;
62-
final status = switch (action) {
63-
null => ProposalsCollaborationStatus.pending,
64-
ProposalSubmissionAction.aFinal => ProposalsCollaborationStatus.accepted,
65-
// When proposal is final, draft action do not mean it's accepted
66-
ProposalSubmissionAction.draft when isFinal => ProposalsCollaborationStatus.pending,
67-
ProposalSubmissionAction.draft => ProposalsCollaborationStatus.accepted,
68-
ProposalSubmissionAction.hide => ProposalsCollaborationStatus.rejected,
69-
};
70-
71-
return ProposalBriefDataCollaborator(
61+
return ProposalDataCollaborator.fromAction(
7262
id: id,
73-
status: status,
63+
action: collaboratorsActions[id.toSignificant()]?.action,
64+
isProposalFinal: isFinal,
7465
);
7566
},
7667
).toList();
@@ -116,19 +107,6 @@ final class ProposalBriefData extends Equatable {
116107
];
117108
}
118109

119-
final class ProposalBriefDataCollaborator extends Equatable {
120-
final CatalystId id;
121-
final ProposalsCollaborationStatus status;
122-
123-
const ProposalBriefDataCollaborator({
124-
required this.id,
125-
required this.status,
126-
});
127-
128-
@override
129-
List<Object?> get props => [id, status];
130-
}
131-
132110
final class ProposalBriefDataVersion extends Equatable {
133111
final DocumentRef ref;
134112
final String? title;
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import 'package:catalyst_voices_models/catalyst_voices_models.dart';
2+
import 'package:equatable/equatable.dart';
3+
import 'package:flutter/foundation.dart';
4+
5+
class ProposalDataCollaborator extends Equatable {
6+
final CatalystId id;
7+
final ProposalsCollaborationStatus status;
8+
9+
const ProposalDataCollaborator({required this.id, required this.status});
10+
11+
/// Creates a collaborator with status derived from the submission action.
12+
///
13+
/// Status mapping:
14+
/// - `null` action → [ProposalsCollaborationStatus.pending]
15+
/// - [ProposalSubmissionAction.aFinal][ProposalsCollaborationStatus.accepted]
16+
/// - [ProposalSubmissionAction.draft] when proposal is final → [ProposalsCollaborationStatus.pending]
17+
/// - [ProposalSubmissionAction.draft] when proposal is draft → [ProposalsCollaborationStatus.accepted]
18+
/// - [ProposalSubmissionAction.hide][ProposalsCollaborationStatus.rejected]
19+
factory ProposalDataCollaborator.fromAction({
20+
required CatalystId id,
21+
required ProposalSubmissionAction? action,
22+
required bool isProposalFinal,
23+
}) {
24+
final status = switch (action) {
25+
null => ProposalsCollaborationStatus.pending,
26+
ProposalSubmissionAction.aFinal => ProposalsCollaborationStatus.accepted,
27+
// When proposal is final, draft action does not mean it's accepted
28+
ProposalSubmissionAction.draft when isProposalFinal => ProposalsCollaborationStatus.pending,
29+
ProposalSubmissionAction.draft => ProposalsCollaborationStatus.accepted,
30+
ProposalSubmissionAction.hide => ProposalsCollaborationStatus.rejected,
31+
};
32+
33+
return ProposalDataCollaborator(id: id, status: status);
34+
}
35+
36+
@override
37+
List<Object?> get props => [id, status];
38+
39+
static List<ProposalDataCollaborator> resolveCollaboratorStatuses({
40+
required bool isProposalFinal,
41+
Map<CatalystId, RawCollaboratorAction> collaboratorsActions = const {},
42+
List<CatalystId> originalAuthor = const [],
43+
List<CatalystId> prevCollaborators = const [],
44+
List<CatalystId> prevAuthors = const [],
45+
}) {
46+
final significantPrevCollaborators = prevCollaborators.toSignificant();
47+
final significantOriginalAuthor = originalAuthor.toSignificant();
48+
final significantPrevAuthors = prevAuthors.toSignificant();
49+
50+
final collaboratorsStatuses = <ProposalDataCollaborator>[];
51+
for (final collaborator in collaboratorsActions.keys) {
52+
final significantCollaborator = collaborator.toSignificant();
53+
// collaborator was removed from list and original authors are the same
54+
if (!significantPrevCollaborators.contains(significantCollaborator) &&
55+
listEquals(significantOriginalAuthor, significantPrevAuthors)) {
56+
collaboratorsStatuses.add(
57+
ProposalDataCollaborator(id: collaborator, status: ProposalsCollaborationStatus.removed),
58+
);
59+
// collaborator was removed from the list and original author is not the same as prev Author
60+
} else if (!significantPrevCollaborators.contains(significantCollaborator) &&
61+
!listEquals(significantOriginalAuthor, significantPrevAuthors)) {
62+
collaboratorsStatuses.add(
63+
ProposalDataCollaborator(id: collaborator, status: ProposalsCollaborationStatus.left),
64+
);
65+
} else {
66+
collaboratorsStatuses.add(
67+
ProposalDataCollaborator.fromAction(
68+
id: collaborator,
69+
action: collaboratorsActions[significantCollaborator]?.action,
70+
isProposalFinal: isProposalFinal,
71+
),
72+
);
73+
}
74+
}
75+
return collaboratorsStatuses;
76+
}
77+
}
78+
79+
extension on List<CatalystId> {
80+
List<CatalystId> toSignificant() => map((e) => e.toSignificant()).toList();
81+
}

catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/data/proposal_data_v2.dart

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@ import 'package:equatable/equatable.dart';
33

44
final class ProposalDataV2 extends Equatable {
55
final DocumentRef id;
6-
// This can be retrive from ProposalOrDocument
7-
final ProposalDocument document;
8-
// Maybe at here nullable template
6+
7+
/// The parsed proposal document with template schema.
8+
///
9+
/// This is `null` when the template couldn't be retrieved.
10+
/// The UI should show an error message in this case.
11+
final ProposalDocument? document;
912
final bool isFavorite;
1013
final String categoryName;
1114
final ProposalBriefDataVotes? votes;
12-
final List<ProposalBriefDataVersion>? versions;
13-
final List<ProposalBriefDataCollaborator>? collaborators;
14-
// Consider adding more campaign or category data here
15+
final List<DocumentRef>? versions;
16+
final List<ProposalDataCollaborator>? collaborators;
1517

1618
const ProposalDataV2({
1719
required this.id,
@@ -23,7 +25,57 @@ final class ProposalDataV2 extends Equatable {
2325
this.collaborators,
2426
});
2527

28+
/// Builds a [ProposalDataV2] from raw data.
29+
///
30+
/// [data] - Raw proposal data from database query.
31+
/// [proposal] - Provides extracted data (categoryName, etc.) from proposal.
32+
/// Works both with and without template loaded.
33+
/// [proposalDocument] - Optional parsed proposal document. If null,
34+
/// the UI should show an error that template couldn't be retrieved.
35+
/// The caller (typically in the repository layer) should build this using
36+
/// `ProposalDocumentFactory.create()` when `data.template` is available.
37+
factory ProposalDataV2.build({
38+
required RawProposal data,
39+
required ProposalOrDocument proposal,
40+
ProposalDocument? proposalDocument,
41+
Vote? draftVote,
42+
Vote? castedVote,
43+
Map<CatalystId, RawCollaboratorAction> collaboratorsActions = const {},
44+
List<CatalystId> prevCollaborators = const [],
45+
List<CatalystId> prevAuthors = const [],
46+
}) {
47+
final id = data.proposal.id;
48+
final isFinal = data.isFinal;
49+
50+
final versions = data.versionIds.map((e) => id.copyWith(ver: Optional(e))).toList();
51+
52+
final collaborators = ProposalDataCollaborator.resolveCollaboratorStatuses(
53+
isProposalFinal: isFinal,
54+
prevAuthors: prevAuthors,
55+
prevCollaborators: prevCollaborators,
56+
collaboratorsActions: collaboratorsActions,
57+
originalAuthor: data.originalAuthors,
58+
);
59+
60+
return ProposalDataV2(
61+
id: id,
62+
document: proposalDocument,
63+
isFavorite: data.isFavorite,
64+
categoryName: proposal.categoryName ?? '',
65+
collaborators: collaborators,
66+
versions: versions,
67+
votes: isFinal ? ProposalBriefDataVotes(draft: draftVote, casted: castedVote) : null,
68+
);
69+
}
70+
2671
@override
27-
// TODO: implement props
28-
List<Object?> get props => [];
72+
List<Object?> get props => [
73+
id,
74+
document,
75+
isFavorite,
76+
categoryName,
77+
votes,
78+
versions,
79+
collaborators,
80+
];
2981
}

catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/documents_v2_dao.dart

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,16 @@ abstract interface class DocumentsV2Dao {
9797
/// Returns `null` if the document ID does not exist in the database.
9898
Future<DocumentRef?> getLatestOf(DocumentRef id);
9999

100+
/// Returns a previous version of a document, if available.
101+
///
102+
/// Behavior depends on whether the reference is exact or loose:
103+
/// - [DocumentRef.isExact]: Returns the immediate previous version
104+
/// (the version with [DocumentEntityV2.createdAt] just before the specified version).
105+
/// - [DocumentRef.isLoose]: Returns the first known version (where `id == ver`).
106+
///
107+
/// Returns `null` if no previous version exists or the document is not found.
108+
Future<DocumentRef?> getPreviousOf({required DocumentRef id});
109+
100110
/// Saves a single document and its associated authors.
101111
///
102112
/// This is a convenience wrapper around [saveAll].
@@ -294,6 +304,50 @@ class DriftDocumentsV2Dao extends DatabaseAccessor<DriftCatalystDatabase>
294304
.getSingleOrNull();
295305
}
296306

307+
@override
308+
Future<DocumentRef?> getPreviousOf({required DocumentRef id}) {
309+
if (id.isLoose) {
310+
final query = selectOnly(documentsV2)
311+
..addColumns([documentsV2.id, documentsV2.ver])
312+
..where(documentsV2.id.equals(id.id))
313+
..where(documentsV2.ver.equals(id.id))
314+
..limit(1);
315+
316+
return query
317+
.map(
318+
(row) => SignedDocumentRef.exact(
319+
id: row.read(documentsV2.id)!,
320+
ver: row.read(documentsV2.ver)!,
321+
),
322+
)
323+
.getSingleOrNull();
324+
}
325+
326+
final inner = alias(documentsV2, 'inner');
327+
final targetCreatedAt = subqueryExpression<DateTime>(
328+
selectOnly(inner)
329+
..addColumns([inner.createdAt])
330+
..where(inner.id.equals(id.id))
331+
..where(inner.ver.equals(id.ver!)),
332+
);
333+
334+
final query = selectOnly(documentsV2)
335+
..addColumns([documentsV2.id, documentsV2.ver])
336+
..where(documentsV2.id.equals(id.id))
337+
..where(documentsV2.createdAt.isSmallerThan(targetCreatedAt))
338+
..orderBy([OrderingTerm.desc(documentsV2.createdAt)])
339+
..limit(1);
340+
341+
return query
342+
.map(
343+
(row) => SignedDocumentRef.exact(
344+
id: row.read(documentsV2.id)!,
345+
ver: row.read(documentsV2.ver)!,
346+
),
347+
)
348+
.getSingleOrNull();
349+
}
350+
297351
@override
298352
Future<void> save(DocumentCompositeEntity entity) => saveAll([entity]);
299353

catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/database_documents_data_source.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,11 @@ final class DatabaseDocumentsDataSource
245245
.distinct()
246246
.map((proposal) => proposal?.toModel());
247247
}
248+
249+
@override
250+
Future<DocumentRef?> getPreviousOf({required DocumentRef id}) {
251+
return _database.documentsV2Dao.getPreviousOf(id: id);
252+
}
248253
}
249254

250255
extension on DocumentData {

catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/proposal_document_data_local_source.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,6 @@ abstract interface class ProposalDocumentDataLocalSource {
5252
ProposalsOrder order,
5353
ProposalsFiltersV2 filters,
5454
});
55+
56+
Future<DocumentRef?> getPreviousOf({required DocumentRef id});
5557
}

0 commit comments

Comments
 (0)