Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b32c807
feat: first version of models
LynxLynxx Dec 9, 2025
0e1e3ad
feat: first version of query
LynxLynxx Dec 9, 2025
5c4411a
feat: extract common subqueries
LynxLynxx Dec 9, 2025
0e17eb9
feat: add tests
LynxLynxx Dec 9, 2025
b501960
feat: mapping collaborators
LynxLynxx Dec 10, 2025
88df5f8
chore: remove comments
LynxLynxx Dec 10, 2025
08e3c16
Merge branch 'feat/co-proposers-3677' into feat/proposal-viewer-v2
LynxLynxx Dec 10, 2025
b20fe6f
chore: add tests
LynxLynxx Dec 10, 2025
703826f
Merge branch 'feat/proposal-viewer-v2' of github.com:input-output-hk/…
LynxLynxx Dec 10, 2025
1438281
chore: update getPreviousOf
LynxLynxx Dec 11, 2025
c36f47c
chore: adding test for setting collaborators statuses
LynxLynxx Dec 11, 2025
d86fa5e
chore: find actions for proposal id
LynxLynxx Dec 11, 2025
bf9aa43
Merge branch 'feat/co-proposers-3677' into feat/proposal-viewer-v2
LynxLynxx Dec 11, 2025
e5c31d3
chore: fix spelling
LynxLynxx Dec 11, 2025
dbaa150
Merge branch 'feat/proposal-viewer-v2' of github.com:input-output-hk/…
LynxLynxx Dec 11, 2025
121cfb0
Merge branch 'feat/co-proposers-3677' into feat/proposal-viewer-v2
LynxLynxx Dec 11, 2025
78e2330
chore: no setup for actions in raw proposal
LynxLynxx Dec 11, 2025
4ac77c2
Merge branch 'feat/co-proposers-3677' into feat/proposal-viewer-v2
LynxLynxx Dec 11, 2025
6270aab
feat: upate action getter
LynxLynxx Dec 11, 2025
99a180f
merge
LynxLynxx Dec 11, 2025
d8c9734
fix: tests
LynxLynxx Dec 11, 2025
cc9180c
chore: add unit test for proposals_v2_dao
LynxLynxx Dec 11, 2025
e80e0e2
chore: adding test
LynxLynxx Dec 12, 2025
1a6170e
fix: format
LynxLynxx Dec 12, 2025
d5a8cdf
Merge branch 'feat/co-proposers-3677' into feat/proposal-viewer-v2
LynxLynxx Dec 12, 2025
c7b84c7
chore: refactor review
LynxLynxx Dec 12, 2025
b8bd667
chore: clean up todo
LynxLynxx Dec 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,10 @@ export 'pagination/page_request.dart';
export 'permissions/exceptions/permission_exceptions.dart';
export 'proposal/core_proposal.dart';
export 'proposal/data/proposal_brief_data.dart';
export 'proposal/data/proposal_data_collaborator.dart';
export 'proposal/data/proposal_data_v2.dart';
export 'proposal/data/proposals_total_ask.dart';
export 'proposal/data/raw_proposal.dart';
export 'proposal/data/raw_proposal_brief.dart';
export 'proposal/data/raw_proposal_collaborators_actions.dart';
export 'proposal/detail_proposal.dart';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ final class ProposalBriefData extends Equatable {
final bool isFavorite;
final ProposalBriefDataVotes? votes;
final List<ProposalBriefDataVersion>? versions;
final List<ProposalBriefDataCollaborator>? collaborators;
final List<ProposalDataCollaborator>? collaborators;

const ProposalBriefData({
required this.id,
Expand Down Expand Up @@ -58,19 +58,10 @@ final class ProposalBriefData extends Equatable {
// Proposal Brief do not support "removed" or "left" status.
final collaborators = data.proposal.metadata.collaborators?.map(
(id) {
final action = collaboratorsActions[id.toSignificant()]?.action;
final status = switch (action) {
null => ProposalsCollaborationStatus.pending,
ProposalSubmissionAction.aFinal => ProposalsCollaborationStatus.accepted,
// When proposal is final, draft action do not mean it's accepted
ProposalSubmissionAction.draft when isFinal => ProposalsCollaborationStatus.pending,
ProposalSubmissionAction.draft => ProposalsCollaborationStatus.accepted,
ProposalSubmissionAction.hide => ProposalsCollaborationStatus.rejected,
};

return ProposalBriefDataCollaborator(
return ProposalDataCollaborator.fromAction(
id: id,
status: status,
action: collaboratorsActions[id.toSignificant()]?.action,
isProposalFinal: isFinal,
);
},
).toList();
Expand Down Expand Up @@ -116,19 +107,6 @@ final class ProposalBriefData extends Equatable {
];
}

final class ProposalBriefDataCollaborator extends Equatable {
final CatalystId id;
final ProposalsCollaborationStatus status;

const ProposalBriefDataCollaborator({
required this.id,
required this.status,
});

@override
List<Object?> get props => [id, status];
}

final class ProposalBriefDataVersion extends Equatable {
final DocumentRef ref;
final String? title;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import 'package:catalyst_voices_models/catalyst_voices_models.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';

class ProposalDataCollaborator extends Equatable {
final CatalystId id;
final ProposalsCollaborationStatus status;

const ProposalDataCollaborator({required this.id, required this.status});

/// Creates a collaborator with status derived from the submission action.
///
/// Status mapping:
/// - `null` action → [ProposalsCollaborationStatus.pending]
/// - [ProposalSubmissionAction.aFinal][ProposalsCollaborationStatus.accepted]
/// - [ProposalSubmissionAction.draft] when proposal is final → [ProposalsCollaborationStatus.pending]
/// - [ProposalSubmissionAction.draft] when proposal is draft → [ProposalsCollaborationStatus.accepted]
/// - [ProposalSubmissionAction.hide][ProposalsCollaborationStatus.rejected]
factory ProposalDataCollaborator.fromAction({
required CatalystId id,
required ProposalSubmissionAction? action,
required bool isProposalFinal,
}) {
final status = switch (action) {
null => ProposalsCollaborationStatus.pending,
ProposalSubmissionAction.aFinal => ProposalsCollaborationStatus.accepted,
// When proposal is final, draft action does not mean it's accepted
ProposalSubmissionAction.draft when isProposalFinal => ProposalsCollaborationStatus.pending,
ProposalSubmissionAction.draft => ProposalsCollaborationStatus.accepted,
ProposalSubmissionAction.hide => ProposalsCollaborationStatus.rejected,
};

return ProposalDataCollaborator(id: id, status: status);
}

@override
List<Object?> get props => [id, status];

static List<ProposalDataCollaborator> resolveCollaboratorStatuses({
required bool isProposalFinal,
Map<CatalystId, RawCollaboratorAction> collaboratorsActions = const {},
List<CatalystId> originalAuthor = const [],
List<CatalystId> prevCollaborators = const [],
List<CatalystId> prevAuthors = const [],
}) {
final significantPrevCollaborators = prevCollaborators.toSignificant();
final significantOriginalAuthor = originalAuthor.toSignificant();
final significantPrevAuthors = prevAuthors.toSignificant();

final collaboratorsStatuses = <ProposalDataCollaborator>[];
for (final collaborator in collaboratorsActions.keys) {
final significantCollaborator = collaborator.toSignificant();
// collaborator was removed from list and original authors are the same
if (!significantPrevCollaborators.contains(significantCollaborator) &&
listEquals(significantOriginalAuthor, significantPrevAuthors)) {
collaboratorsStatuses.add(
ProposalDataCollaborator(id: collaborator, status: ProposalsCollaborationStatus.removed),
);
// collaborator was removed from the list and original author is not the same as prev Author
} else if (!significantPrevCollaborators.contains(significantCollaborator) &&
!listEquals(significantOriginalAuthor, significantPrevAuthors)) {
collaboratorsStatuses.add(
ProposalDataCollaborator(id: collaborator, status: ProposalsCollaborationStatus.left),
);
} else {
collaboratorsStatuses.add(
ProposalDataCollaborator.fromAction(
id: collaborator,
action: collaboratorsActions[significantCollaborator]?.action,
isProposalFinal: isProposalFinal,
),
);
}
}
return collaboratorsStatuses;
}
}

extension on List<CatalystId> {
List<CatalystId> toSignificant() => map((e) => e.toSignificant()).toList();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import 'package:catalyst_voices_models/catalyst_voices_models.dart';
import 'package:equatable/equatable.dart';

final class ProposalDataV2 extends Equatable {
final DocumentRef id;

/// The parsed proposal document with template schema.
///
/// This is `null` when the template couldn't be retrieved.
/// The UI should show an error message in this case.
final ProposalDocument? document;
final bool isFavorite;
final String categoryName;
final ProposalBriefDataVotes? votes;
final List<DocumentRef>? versions;
final List<ProposalDataCollaborator>? collaborators;

const ProposalDataV2({
required this.id,
required this.document,
required this.isFavorite,
required this.categoryName,
this.votes,
this.versions,
this.collaborators,
});

/// Builds a [ProposalDataV2] from raw data.
///
/// [data] - Raw proposal data from database query.
/// [proposal] - Provides extracted data (categoryName, etc.) from proposal.
/// Works both with and without template loaded.
/// [proposalDocument] - Optional parsed proposal document. If null,
/// the UI should show an error that template couldn't be retrieved.
/// The caller (typically in the repository layer) should build this using
/// `ProposalDocumentFactory.create()` when `data.template` is available.
factory ProposalDataV2.build({
required RawProposal data,
required ProposalOrDocument proposal,
ProposalDocument? proposalDocument,
Vote? draftVote,
Vote? castedVote,
Map<CatalystId, RawCollaboratorAction> collaboratorsActions = const {},
List<CatalystId> prevCollaborators = const [],
List<CatalystId> prevAuthors = const [],
}) {
final id = data.proposal.id;
final isFinal = data.isFinal;

final versions = data.versionIds.map((e) => id.copyWith(ver: Optional(e))).toList();

final collaborators = ProposalDataCollaborator.resolveCollaboratorStatuses(
isProposalFinal: isFinal,
prevAuthors: prevAuthors,
prevCollaborators: prevCollaborators,
collaboratorsActions: collaboratorsActions,
originalAuthor: data.originalAuthors,
);

return ProposalDataV2(
id: id,
document: proposalDocument,
isFavorite: data.isFavorite,
categoryName: proposal.categoryName ?? '',
collaborators: collaborators,
versions: versions,
votes: isFinal ? ProposalBriefDataVotes(draft: draftVote, casted: castedVote) : null,
);
}

@override
List<Object?> get props => [
id,
document,
isFavorite,
categoryName,
votes,
versions,
collaborators,
];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'package:catalyst_voices_models/catalyst_voices_models.dart';
import 'package:equatable/equatable.dart';

class RawProposal extends Equatable {
final DocumentData proposal;
final DocumentData? template;
final ProposalSubmissionAction? actionType;
final List<String> versionIds;
final int commentsCount;
final bool isFavorite;
final List<CatalystId> originalAuthors;

const RawProposal({
required this.proposal,
required this.template,
required this.actionType,
required this.versionIds,
required this.commentsCount,
required this.isFavorite,
required this.originalAuthors,
});

bool get isFinal => actionType == ProposalSubmissionAction.aFinal;

int get iteration => versionIds.indexOf(proposal.id.ver!) + 1;

@override
List<Object?> get props => [
proposal,
template,
actionType,
versionIds,
commentsCount,
isFavorite,
originalAuthors,
];
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,16 @@ abstract interface class DocumentsV2Dao {
/// Returns `null` if the document ID does not exist in the database.
Future<DocumentRef?> getLatestOf(DocumentRef id);

/// Returns a previous version of a document, if available.
///
/// Behavior depends on whether the reference is exact or loose:
/// - [DocumentRef.isExact]: Returns the immediate previous version
/// (the version with [DocumentEntityV2.createdAt] just before the specified version).
/// - [DocumentRef.isLoose]: Returns the first known version (where `id == ver`).
///
/// Returns `null` if no previous version exists or the document is not found.
Future<DocumentRef?> getPreviousOf({required DocumentRef id});

/// Saves a single document and its associated authors.
///
/// This is a convenience wrapper around [saveAll].
Expand Down Expand Up @@ -294,6 +304,31 @@ class DriftDocumentsV2Dao extends DatabaseAccessor<DriftCatalystDatabase>
.getSingleOrNull();
}

@override
Future<DocumentRef?> getPreviousOf({required DocumentRef id}) {
final query = selectOnly(documentsV2)
..addColumns([documentsV2.id, documentsV2.ver])
..where(documentsV2.id.equals(id.id));

if (id.isLoose) {
query.where(documentsV2.ver.equals(id.id));
} else {
query.where(documentsV2.ver.isSmallerThanValue(id.ver!));
}
query
..orderBy([OrderingTerm.desc(documentsV2.ver)])
..limit(1);

return query.map(
(row) {
return SignedDocumentRef.exact(
id: row.read(documentsV2.id)!,
ver: row.read(documentsV2.ver)!,
);
},
).getSingleOrNull();
}

@override
Future<void> save(DocumentCompositeEntity entity) => saveAll([entity]);

Expand Down
Loading
Loading