Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 @@ -97,7 +97,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 implements Comparable<ProposalBr
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 implements Comparable<ProposalBr
// 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 @@ -145,7 +136,7 @@ final class ProposalBriefData extends Equatable implements Comparable<ProposalBr
bool? isFavorite,
Optional<ProposalBriefDataVotes>? votes,
Optional<List<ProposalBriefDataVersion>>? versions,
Optional<List<ProposalBriefDataCollaborator>>? collaborators,
Optional<List<ProposalDataCollaborator>>? collaborators,
}) {
return ProposalBriefData(
id: id ?? this.id,
Expand All @@ -168,19 +159,6 @@ final class ProposalBriefData extends Equatable implements Comparable<ProposalBr
}
}

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,73 @@
import 'package:catalyst_voices_models/catalyst_voices_models.dart';
import 'package:collection/collection.dart';
import 'package:equatable/equatable.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,
List<CatalystId> currentCollaborators = const [],
Map<CatalystId, RawCollaboratorAction> collaboratorsActions = const {},
List<CatalystId> prevCollaborators = const [],
List<CatalystId> prevAuthors = const [],
}) {
final currentCollaboratorsStatuses = currentCollaborators.map((id) {
return ProposalDataCollaborator.fromAction(
id: id,
action: collaboratorsActions[id.toSignificant()]?.action,
isProposalFinal: isProposalFinal,
);
});

final missingCollaborators = prevCollaborators.where(
(prev) => currentCollaborators.none((current) => prev.isSameAs(current)),
);

final missingCollaboratorsStatuses = missingCollaborators.map((id) {
final didAuthorPrevVersion = prevAuthors.any((author) => author.isSameAs(id));
// If they authored the previous version, they left voluntarily.
// Otherwise, they were removed by someone else.
final status = didAuthorPrevVersion
? ProposalsCollaborationStatus.left
: ProposalsCollaborationStatus.removed;
return ProposalDataCollaborator(id: id, status: status);
});

return [
...missingCollaboratorsStatuses,
...currentCollaboratorsStatuses,
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
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 ProposalOrDocument proposalOrDocument;
final ProposalSubmissionAction? submissionAction;
final bool isFavorite;
final String categoryName;
final ProposalBriefDataVotes? votes;
final List<DocumentRef>? versions;
final List<ProposalDataCollaborator>? collaborators;

const ProposalDataV2({
required this.id,
required this.proposalOrDocument,
required this.submissionAction,
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.
/// [proposalOrDocument] - Provides extracted data (categoryName, etc.) from proposal.
/// Works both with and without template loaded.
factory ProposalDataV2.build({
required RawProposal data,
required ProposalOrDocument proposalOrDocument,
Vote? draftVote,
Vote? castedVote,
Map<CatalystId, RawCollaboratorAction> collaboratorsActions = const {},
List<CatalystId> prevCollaborators = const [],
List<CatalystId> prevAuthors = const [],
ProposalSubmissionAction? action,
}) {
final id = data.proposal.id;
final isFinal = data.isFinal;

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

final collaborators = ProposalDataCollaborator.resolveCollaboratorStatuses(
isProposalFinal: isFinal,
currentCollaborators: data.proposal.metadata.collaborators ?? [],
collaboratorsActions: collaboratorsActions,
prevCollaborators: prevCollaborators,
prevAuthors: prevAuthors,
);

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

ProposalPublish? get proposalPublish {
if (submissionAction == null && id is DraftRef) {
return ProposalPublish.localDraft;
} else if (submissionAction == ProposalSubmissionAction.aFinal) {
return ProposalPublish.submittedProposal;
} else if (submissionAction == ProposalSubmissionAction.draft) {
return ProposalPublish.publishedDraft;
}
return null;
}

@override
List<Object?> get props => [
id,
proposalOrDocument,
submissionAction,
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,
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 @@ -23,7 +23,13 @@ sealed class ProposalOrDocument extends Equatable {
/// Creates a [ProposalOrDocument] from a structured [ProposalDocument].
const factory ProposalOrDocument.proposal(ProposalDocument data) = _Proposal;

// TODO(damian-molinski): Category name should come from query but atm those are not documents.
/// Returns the underlying [ProposalDocument] if this is a proposal,
/// or null if it's just a document without a template.
ProposalDocument? get asProposalDocument => switch (this) {
_Proposal(:final data) => data,
_Document() => null,
};

/// The name of the proposal's category.
String? get categoryName {
return Campaign.all
Expand All @@ -36,10 +42,10 @@ sealed class ProposalOrDocument extends Equatable {
/// A brief description of the proposal.
String? get description;

// TODO(damian-molinski): Fund number should come from query but atm those are not documents.
/// The duration of the proposal in months.
int? get durationInMonths;

// TODO(damian-molinski): Fund number should come from query but atm those are not documents.
/// The number of fund this proposal was submitted for.
int? get fundNumber {
return Campaign.all
Expand Down
Loading
Loading