diff --git a/catalyst_voices/apps/voices/lib/app/view/app.dart b/catalyst_voices/apps/voices/lib/app/view/app.dart index 637ce8bf903..fb92b2b6029 100644 --- a/catalyst_voices/apps/voices/lib/app/view/app.dart +++ b/catalyst_voices/apps/voices/lib/app/view/app.dart @@ -56,6 +56,9 @@ class _AppState extends State { BlocProvider( create: (_) => Dependencies.instance.get(), ), + BlocProvider( + create: (context) => Dependencies.instance.get(), + ), ]; } } diff --git a/catalyst_voices/apps/voices/lib/common/codecs/markdown_codec.dart b/catalyst_voices/apps/voices/lib/common/codecs/markdown_codec.dart new file mode 100644 index 00000000000..08081267f70 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/common/codecs/markdown_codec.dart @@ -0,0 +1,60 @@ +import 'dart:convert'; + +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:flutter_quill/quill_delta.dart'; +import 'package:markdown/markdown.dart' as md; +import 'package:markdown_quill/markdown_quill.dart'; + +// Note. +// This codec is here because it depends on flutter_quill which is heavy +// package with lots different dependencies which we don't want to have in +// other packages. +// +// If we could have just Delta package it would be preferred to live in +// models/shared package +const markdown = MarkdownCodec(); + +final _mdDocument = md.Document(); +final _mdToDelta = MarkdownToDelta(markdownDocument: _mdDocument); +final _deltaToMd = DeltaToMarkdown( + customContentHandler: DeltaToMarkdown.escapeSpecialCharactersRelaxed, +); + +final class MarkdownCodec extends Codec { + const MarkdownCodec(); + + @override + Converter get decoder => const MarkdownEncoder(); + + @override + Converter get encoder => const MarkdownDecoder(); +} + +class MarkdownDecoder extends Converter { + const MarkdownDecoder(); + + @override + Delta convert(MarkdownData input) { + if (input.data.isEmpty) { + return Delta(); + } + + return _mdToDelta.convert(input.data); + } +} + +class MarkdownEncoder extends Converter { + const MarkdownEncoder(); + + @override + MarkdownData convert(Delta input) { + if (input.isEmpty) { + return const MarkdownData(''); + } + + final data = _deltaToMd.convert(input); + final trimmed = data.trim(); + + return MarkdownData(trimmed); + } +} diff --git a/catalyst_voices/apps/voices/lib/common/ext/guidance_ext.dart b/catalyst_voices/apps/voices/lib/common/ext/guidance_ext.dart index e4ab1f54554..1a59ca68baa 100644 --- a/catalyst_voices/apps/voices/lib/common/ext/guidance_ext.dart +++ b/catalyst_voices/apps/voices/lib/common/ext/guidance_ext.dart @@ -1,6 +1,6 @@ import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; import 'package:catalyst_voices_localization/generated/catalyst_voices_localizations.dart'; -import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; extension GuidanceExt on GuidanceType { String localizedType(VoicesLocalizations localizations) => switch (this) { diff --git a/catalyst_voices/apps/voices/lib/dependency/dependencies.dart b/catalyst_voices/apps/voices/lib/dependency/dependencies.dart index 97e31efa6de..417004543ab 100644 --- a/catalyst_voices/apps/voices/lib/dependency/dependencies.dart +++ b/catalyst_voices/apps/voices/lib/dependency/dependencies.dart @@ -68,7 +68,12 @@ final class Dependencies extends DependencyProvider { // TODO(ryszard-schossler): add repository for campaign management ..registerLazySingleton( CampaignBuilderCubit.new, - ); + ) + ..registerFactory(() { + return WorkspaceBloc( + get(), + ); + }); } void _registerRepositories() { @@ -116,8 +121,10 @@ final class Dependencies extends DependencyProvider { dispose: (service) => unawaited(service.dispose()), ); registerLazySingleton(AccessControl.new); - registerLazySingleton( - () => CampaignService(get()), - ); + registerLazySingleton(() { + return CampaignService( + get(), + ); + }); } } diff --git a/catalyst_voices/apps/voices/lib/pages/spaces/drawer/my_private_proposals.dart b/catalyst_voices/apps/voices/lib/pages/spaces/drawer/my_private_proposals.dart index 39813bf2fa3..f05f56257e0 100644 --- a/catalyst_voices/apps/voices/lib/pages/spaces/drawer/my_private_proposals.dart +++ b/catalyst_voices/apps/voices/lib/pages/spaces/drawer/my_private_proposals.dart @@ -8,32 +8,15 @@ class MyPrivateProposals extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( + return const Column( mainAxisSize: MainAxisSize.min, children: [ - const SpaceHeader(Space.workspace), - const SectionHeader( + SpaceHeader(Space.workspace), + SectionHeader( leading: SizedBox(width: 12), title: Text('My private proposals (3/5)'), ), - VoicesNavTile( - name: 'My first proposal', - status: ProposalStatus.draft, - trailing: const MoreOptionsButton(), - onTap: () => Scaffold.of(context).closeDrawer(), - ), - VoicesNavTile( - name: 'My second proposal', - status: ProposalStatus.inProgress, - trailing: const MoreOptionsButton(), - onTap: () => Scaffold.of(context).closeDrawer(), - ), - VoicesNavTile( - name: 'My third proposal', - status: ProposalStatus.inProgress, - trailing: const MoreOptionsButton(), - onTap: () => Scaffold.of(context).closeDrawer(), - ), + // TODO(damian-molinski): watch workspace bloc ], ); } diff --git a/catalyst_voices/apps/voices/lib/pages/treasury/treasury_page.dart b/catalyst_voices/apps/voices/lib/pages/treasury/treasury_page.dart index e5250fb8910..e554eb3eec0 100644 --- a/catalyst_voices/apps/voices/lib/pages/treasury/treasury_page.dart +++ b/catalyst_voices/apps/voices/lib/pages/treasury/treasury_page.dart @@ -8,16 +8,16 @@ import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; const sections = [ CampaignSetup( - id: 0, + id: '0', steps: [ DummyTopicStep( - id: 0, - sectionId: 0, + id: '0', + sectionId: '0', isEditable: false, ), - DummyTopicStep(id: 1, sectionId: 0), - DummyTopicStep(id: 2, sectionId: 0), - DummyTopicStep(id: 3, sectionId: 0), + DummyTopicStep(id: '1', sectionId: '0'), + DummyTopicStep(id: '2', sectionId: '0'), + DummyTopicStep(id: '3', sectionId: '0'), ], ), ]; diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/answer.dart b/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/answer.dart deleted file mode 100644 index ef05833b8a5..00000000000 --- a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/answer.dart +++ /dev/null @@ -1,5 +0,0 @@ -// ignore_for_file: prefer_single_quotes, require_trailing_commas, lines_longer_than_80_chars - -const answer = [ - {"insert": "Answer\n"} -]; diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/bonus_mark_up.dart b/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/bonus_mark_up.dart deleted file mode 100644 index 2accf562797..00000000000 --- a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/bonus_mark_up.dart +++ /dev/null @@ -1,78 +0,0 @@ -// ignore_for_file: prefer_single_quotes, require_trailing_commas, lines_longer_than_80_chars - -const bonusMarkUp = [ - { - 'insert': { - 'image': - 'https://upload.wikimedia.org/wikipedia/commons/b/b6/Image_created_with_a_mobile_phone.png', - }, - 'attributes': {'style': 'width: 181.764; height: 140; '}, - }, - {'insert': '\n\n'}, - { - 'insert': 'Legend Tells About Amazonian The Great Smith', - 'attributes': {'bold': true}, - }, - {'insert': '\n\nAn ancient legend confirms '}, - { - 'insert': 'Amazonian as the Father of the Samurai Sword. Amazonian', - 'attributes': {'bold': true}, - }, - {'insert': ' and his son, '}, - { - 'insert': 'Amateur', - 'attributes': {'italic': true}, - }, - { - 'insert': - ', were the prominent smiths who led a team of armorers, employed by ' - 'Emperor Mommy (683-707) to make swords for his army of warriors. ' - "Later his son, Amateur continued his father's ", - }, - { - 'insert': 'great work', - 'attributes': {'italic': true}, - }, - {'insert': '.\n\n'}, - { - 'insert': 'Amateur', - 'attributes': {'italic': true}, - }, - { - 'insert': '\n', - 'attributes': {'list': 'bullet'}, - }, - { - 'insert': 'Amauroses', - 'attributes': {'italic': true}, - }, - { - 'insert': '\n', - 'attributes': {'list': 'bullet'}, - }, - { - 'insert': 'Amateurism', - 'attributes': {'italic': true}, - }, - { - 'insert': '\n', - 'attributes': {'list': 'bullet'}, - }, - {'insert': '\n'}, - { - 'insert': 'Sword 1', - 'attributes': {'italic': true}, - }, - { - 'insert': '\n', - 'attributes': {'list': 'ordered'}, - }, - { - 'insert': 'Sword 2', - 'attributes': {'italic': true}, - }, - { - 'insert': '\n', - 'attributes': {'list': 'ordered'}, - } -]; diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/delivery_and_accountability.dart b/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/delivery_and_accountability.dart deleted file mode 100644 index db16aec5191..00000000000 --- a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/delivery_and_accountability.dart +++ /dev/null @@ -1,53 +0,0 @@ -// ignore_for_file: prefer_single_quotes, require_trailing_commas, lines_longer_than_80_chars - -const deliveryAndAccountability = [ - { - "insert": - "The Catalyst Team is committed to providing continuous, accessible updates to the Cardano community. Updates and outputs will also be published on the Catalyst public Gitbook. \n\nIn addition to monthly progress reports and completed milestone proof of achievement ceremonies, the Catalyst team will also promote outcomes, outputs, and general progress in the following ways:   \n\n" - }, - { - "insert": "Weekly newsletters:", - "attributes": {"bold": true} - }, - {"insert": "\nReach: 60,000 mailing list members"}, - { - "insert": "\n", - "attributes": {"list": "bullet"} - }, - { - "insert": - "Each week, an update email is sent to all mailing list members to provide a run down on progress and highlight key achievements.  " - }, - { - "insert": "\n", - "attributes": {"list": "bullet"} - }, - { - "insert": - "Fortnightly technical development updates: Reach: Averaging per week: 3000 report readers, 60,000 Twitter views, 100 retweets   " - }, - { - "insert": "\n", - "attributes": {"list": "bullet"} - }, - { - "insert": - "
The Catalyst team will provide technical development updates every two weeks as part of the overall Cardano technical development update communications. This will amount to at least 24 updates over the next 12 months, accounting for the seasonal Winter holiday period. \n
" - }, - { - "insert": "Weekly Town Halls", - "attributes": {"bold": true} - }, - { - "insert": - "\nReach: At least 1000 viewers, up to 10,000 periodically \n
Catalyst Town Hall is a mainstay platform for communicating key progress and achievements, and provides an opportunity to gather insights from attendees about new features or potential changes to Catalyst. Catalyst funded projects that have achieved project-completion status are highlighted weekly.\n
" - }, - { - "insert": "Catalyst Blogs: ", - "attributes": {"bold": true} - }, - { - "insert": - "\nReach: Averaging 5000 readers per blog based on the last 12 months\nRegular blogs published via ProjectCatalyst.io will help to amplify progress and updates to outputs that have been achieved.\n" - } -]; diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/feasibility_checks.dart b/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/feasibility_checks.dart deleted file mode 100644 index 075cf767141..00000000000 --- a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/feasibility_checks.dart +++ /dev/null @@ -1,12 +0,0 @@ -// ignore_for_file: prefer_single_quotes, require_trailing_commas, lines_longer_than_80_chars - -const feasibilityChecks = [ - { - "insert": "Approach and implementation", - "attributes": {"bold": true} - }, - { - "insert": - "\n\nEngineering and security best practices will be followed to implement the solution, in addition to consultation with both the Catalyst / Cardano community and internal IOG subject matter experts from cryptography, and game theory domains. Prior user research and community feedback informs our initial understanding of challenges to solve for. \n\nCatalyst Voices intends to develop iOS, Android, and Web applications from a single code base with near-native speed and performance. \n\nWe will approach the implementation of sets of features in terms of “modules”. Each module will correspond to a user role, segmenting the experience into sections aimed at completing specific actions. \n\nRole registrations, participation history, and saved preferences will unlock new aspects of the experience to help users engage at their own pace, on their own terms.\n\nLearnings acquired through developing and maintaining existing tools (such as Catalyst Mobile App, Voting Center, Snapshot Module) will be leveraged in order to rewrite the target development frameworks by building a single platform that is highly secure, extensible, and maintainable. While we anticipate unexpected challenges in integrating all features into a single platform, our plan to leverage battle-tested reference implementations should de-risk and accelerate development significantly. \n\nThe project will also benefit from maximizing the results of prior discovery and design research. Wireframes and mock-up designs created for and after user testing for the proposal submission module to replace Ideascale will continue to refine the UX with further user feedback gathered during the delivery of this project. This will include features and UX interactions for user profiles, cross-module navigation, and embedded guidance. \n\nFinally, the development of Catalyst Voices will follow the testing and deployment framework planned for the existing Catalyst stack. The latest experimental features will be deployed to a public devnet, and the latest stable features will be deployed to a public testnet. The community will have opportunities to engage with new features and provide feedback before promoting features to the production release candidate. \n\nRecurring voting events will run every 2-weeks on the Catalyst testnet to provide more frequent opportunities to engage with each of the phases of proposing, reviewing, and voting.\n" - } -]; diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/problem_statement.dart b/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/problem_statement.dart deleted file mode 100644 index 3c431c41fb4..00000000000 --- a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/problem_statement.dart +++ /dev/null @@ -1,8 +0,0 @@ -// ignore_for_file: prefer_single_quotes, require_trailing_commas, lines_longer_than_80_chars - -const problemStatement = [ - { - "insert": - "Catalyst's short participation windows with fragmented UX experience and complicated manual processes frustrate and limit community engagement.\n" - } -]; diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/public_description.dart b/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/public_description.dart deleted file mode 100644 index 9e70174bb1b..00000000000 --- a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/public_description.dart +++ /dev/null @@ -1,121 +0,0 @@ -// ignore_for_file: prefer_single_quotes, require_trailing_commas, lines_longer_than_80_chars - -const publicDescription = [ - { - "insert": "Introducing Catalyst Voices
", - "attributes": {"bold": true} - }, - { - "insert": - "Through this 12 month project, the Catalyst Team proposes to unleash the wisdom of the community by delivering a front-end web-browser based application that radically lowers the barriers to meaningful participation in collective decision-making for voters, representatives, and proposers. \n\nA unified front-end interface to meet the needs of the Catalyst community. Developed with continuous feedback from the community, the Catalyst Team will deliver a unified experience to replace the patchwork composed of wallets, Ideascale, standalone web apps, and the Catalyst mobile app today. \n\nThe proposed product design is informed by insights gleaned over nearly three years of operating Catalyst, ongoing discovery research, and feedback from the community. \n
Prior feedback and research indicate 3 significant opportunities to improve the Catalyst experience with a unified platform:\nStreamline the experience by designing modules to serve the needs of each role" - }, - { - "insert": "\n", - "attributes": {"list": "bullet"} - }, - { - "insert": - "Unlock continuous opportunities for ideation, feedback, building skills and reputation, as well as earning rewards" - }, - { - "insert": "\n", - "attributes": {"list": "bullet"} - }, - { - "insert": - "Offer better guidance by creating context based on identity, preferences and intent
" - }, - { - "insert": "\n", - "attributes": {"list": "bullet"} - }, - { - "insert": "The proposed outputs will: ", - "attributes": {"bold": true} - }, - { - "insert": - "\nreduce the time and steps required of Catalyst users to complete important actions eliminating the frustration of context switching " - }, - { - "insert": "\n", - "attributes": {"list": "bullet"} - }, - { - "insert": - "accelerate the onboarding and upskilling of both casual and committed Catalyst users" - }, - { - "insert": "\n", - "attributes": {"list": "bullet"} - }, - { - "insert": - "improve the quality of participation with just-in-time tips, and more data provided to voters about Catalyst proposals   " - }, - { - "insert": "\n", - "attributes": {"list": "bullet"} - }, - { - "insert": - "introduce participation history that maintains data and context over time, " - }, - { - "insert": "\n", - "attributes": {"list": "bullet"} - }, - { - "insert": - "
Simplified user processes mean more time spent on activities that matter, enabling new capabilities and co-building opportunities that push the boundaries of distributed decision-making. \n\nAll these benefits add up to productivity gains, a more collaborative and focused community, and better funding decisions that create more value for the Cardano ecosystem. \n" - }, - { - "insert": - "
Unlocking Incremental Value With Milestones & Continuous Testing", - "attributes": {"bold": true} - }, - { - "insert": - "\nThe proposed project will be delivered via a series of milestones, each unlocking new capabilities and creating value for Catalyst and the Cardano \necosystems. \n" - }, - { - "insert": "
Milestones include:", - "attributes": {"bold": true} - }, - {"insert": "\nOpen Source Setup"}, - { - "insert": "\n", - "attributes": {"list": "ordered"} - }, - { - "insert": "Architectural Updates to registrations to support multiple roles" - }, - { - "insert": "\n", - "attributes": {"list": "ordered"} - }, - {"insert": "Backend and Wallet Integration Updates"}, - { - "insert": "\n", - "attributes": {"list": "ordered"} - }, - {"insert": "Voting & Delegation"}, - { - "insert": "\n", - "attributes": {"list": "ordered"} - }, - {"insert": "Proposal Submission & Commentary"}, - { - "insert": "\n", - "attributes": {"list": "ordered"} - }, - {"insert": "\n"}, - { - "insert": "Continuous Testing & Learning", - "attributes": {"bold": true} - }, - { - "insert": - "\nAlong the way, continuous delivery to the Catalyst testnet will ensure that the community has meaningful feedback loops to help guide development - rather than waiting to give feedback. Voters, representatives, and proposers will have a chance to test drive the entire Catalyst process end-to-end every 2 weeks from inside Catalyst Voices once available.\n" - } -]; diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/solution_statement.dart b/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/solution_statement.dart deleted file mode 100644 index 731e2a145a6..00000000000 --- a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/solution_statement.dart +++ /dev/null @@ -1,8 +0,0 @@ -// ignore_for_file: prefer_single_quotes, require_trailing_commas, lines_longer_than_80_chars - -const solutionStatement = [ - { - "insert": - "Catalyst Voices provides a unified experience and platform including production-ready liquid democracy, meaningful collaboration opportunities & data-driven context for better onboarding&decisions.\n" - } -]; diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/title.dart b/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/title.dart deleted file mode 100644 index 8c4f522d7b2..00000000000 --- a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/title.dart +++ /dev/null @@ -1,8 +0,0 @@ -// ignore_for_file: prefer_single_quotes, require_trailing_commas, lines_longer_than_80_chars - -const title = [ - { - "insert": - "F10 / IOG Catalyst Team : Ideascale replacement and web-browser based Voting Centre with liquid democracy aka “Catalyst Voices”\n" - } -]; diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/value_for_money.dart b/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/value_for_money.dart deleted file mode 100644 index e18697d3b5c..00000000000 --- a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/value_for_money.dart +++ /dev/null @@ -1,16 +0,0 @@ -// ignore_for_file: prefer_single_quotes, require_trailing_commas, lines_longer_than_80_chars - -const valueForMoney = [ - { - "insert": - "The requested total budget for developing this first iteration of Catalyst Voices as a functional replacement for Ideascale, the existing mobile application, and wallet interface for registration is 840,000 ADA. This is a 12 Month project.\n    \nFund 11 Period : Open Source Activation             
₳75,000     \nFund 11 Period : Voices Architectural Changes,        
₳150,000\nFund 12 Period : Backend & Wallet Integration        
₳189,000    \nFund 13 Period : Voting & Delegation Implementation  
₳200,000    \nFund 13 Period : Voices First Release - Proposal Process Implementation     
₳226,000    
\n" - }, - { - "insert": "Total: ₳840,000", - "attributes": {"bold": true} - }, - { - "insert": - "\n
To deliver this work we require a small team of rust backend developers, QA engineers, front end developers, site reliability engineers and UI designers.\n
This is a moderately complex proposal due to the development of fully decentralized role based access control to replace the Web2 authorizations and user management required by Ideascale and other typical systems. It will also require throughout the entire project, Architectural Design to work on and refine the proposed CIPs and other technical aspects of the system, Engineering Management to keep the project on track, and Product Management that the final product delights and empowers users across the community.\n
This project will also require regular updates to the community and high levels of community engagement to properly respond to and evaluate feedback or queries we are receiving. Especially as it relates to refining and finalizing the CIPs necessary to underpin the operation of this Project, which will also be critical work underpinning future work both in Catalyst Voices and in future work envisioned for the “Athena” distributed Project Catalyst which we ultimately desire to have all the functionality proposed here.\n
We will also be building and deploying test versions of Catalyst Voices continuously to our internal test deployments, and these will be publicly accessible. This is to allow the community to easily see what state the development is in and the available functionality without needing to run the system themselves. We want to allow the greatest number of Project Catalyst community members to track our progress as possible.\n" - } -]; diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_guidance_view.dart b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_guidance_view.dart index fd580e3ac84..bb66b454a01 100644 --- a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_guidance_view.dart +++ b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_guidance_view.dart @@ -2,7 +2,7 @@ import 'package:catalyst_voices/common/ext/guidance_ext.dart'; import 'package:catalyst_voices/widgets/cards/guidance_card.dart'; import 'package:catalyst_voices/widgets/dropdown/voices_dropdown.dart'; import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; -import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:flutter/material.dart'; class GuidanceView extends StatefulWidget { diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_page.dart b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_page.dart index 5e06dc2e530..b2d0e3ea775 100644 --- a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_page.dart +++ b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_page.dart @@ -1,119 +1,17 @@ -import 'package:catalyst_voices/pages/workspace/rich_text/answer.dart'; -import 'package:catalyst_voices/pages/workspace/rich_text/bonus_mark_up.dart'; -import 'package:catalyst_voices/pages/workspace/rich_text/delivery_and_accountability.dart'; -import 'package:catalyst_voices/pages/workspace/rich_text/feasibility_checks.dart'; -import 'package:catalyst_voices/pages/workspace/rich_text/problem_statement.dart'; -import 'package:catalyst_voices/pages/workspace/rich_text/public_description.dart'; -import 'package:catalyst_voices/pages/workspace/rich_text/solution_statement.dart'; -import 'package:catalyst_voices/pages/workspace/rich_text/title.dart'; -import 'package:catalyst_voices/pages/workspace/rich_text/value_for_money.dart'; +import 'dart:async'; + import 'package:catalyst_voices/pages/workspace/workspace_body.dart'; import 'package:catalyst_voices/pages/workspace/workspace_navigation_panel.dart'; import 'package:catalyst_voices/pages/workspace/workspace_setup_panel.dart'; import 'package:catalyst_voices/widgets/containers/space_scaffold.dart'; import 'package:catalyst_voices/widgets/navigation/sections_controller.dart'; -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; -final sections = [ - const ProposalSetup( - id: 0, - steps: [ - TitleStep( - id: 0, - sectionId: 0, - data: DocumentJson(title), - guidances: mockGuidance, - ), - ], - ), - ProposalSummary( - id: 1, - steps: [ - ProblemStep( - id: 0, - sectionId: 1, - data: const DocumentJson(problemStatement), - charsLimit: 200, - guidances: [ - mockGuidance[0], - ], - ), - const SolutionStep( - id: 1, - sectionId: 1, - data: DocumentJson(solutionStatement), - charsLimit: 200, - guidances: mockGuidance, - ), - const PublicDescriptionStep( - id: 2, - sectionId: 1, - data: DocumentJson(publicDescription), - charsLimit: 3000, - guidances: mockGuidance, - ), - ], - ), - const ProposalSolution( - id: 2, - steps: [ - ProblemPerspectiveStep( - id: 0, - sectionId: 2, - data: DocumentJson(answer), - charsLimit: 200, - ), - PerspectiveRationaleStep( - id: 1, - sectionId: 2, - data: DocumentJson(answer), - charsLimit: 200, - ), - ProjectEngagementStep( - id: 2, - sectionId: 2, - data: DocumentJson(answer), - charsLimit: 200, - ), - ], - ), - const ProposalImpact( - id: 3, - steps: [ - BonusMarkUpStep( - id: 0, - sectionId: 3, - data: DocumentJson(bonusMarkUp), - charsLimit: 900, - ), - ValueForMoneyStep( - id: 1, - sectionId: 3, - data: DocumentJson(valueForMoney), - charsLimit: 2600, - ), - ], - ), - const CompatibilityAndFeasibility( - id: 4, - steps: [ - DeliveryAndAccountabilityStep( - id: 0, - sectionId: 4, - data: DocumentJson(deliveryAndAccountability), - ), - FeasibilityChecksStep( - id: 1, - sectionId: 4, - data: DocumentJson(feasibilityChecks), - ), - ], - ), -]; - class WorkspacePage extends StatefulWidget { const WorkspacePage({ super.key, @@ -127,20 +25,35 @@ class _WorkspacePageState extends State { late final SectionsController _sectionsController; late final ItemScrollController _bodyItemScrollController; + SectionStepId? _activeStepId; + StreamSubscription>? _sectionsSub; + @override void initState() { super.initState(); + final bloc = context.read(); + _sectionsController = SectionsController(); _bodyItemScrollController = ItemScrollController(); - _sectionsController.attachItemsScrollController(_bodyItemScrollController); + _sectionsController + ..addListener(_handleSectionsControllerChange) + ..attachItemsScrollController(_bodyItemScrollController); - _populateSections(); + _sectionsSub = bloc.stream + .map((event) => event.sections) + .distinct(listEquals) + .listen(_updateSections); + + bloc.add(const LoadCurrentProposalEvent()); } @override void dispose() { + unawaited(_sectionsSub?.cancel()); + _sectionsSub = null; + _sectionsController.dispose(); super.dispose(); } @@ -159,9 +72,24 @@ class _WorkspacePageState extends State { ); } - void _populateSections() { - _sectionsController.value = SectionsControllerState.initial( - sections: sections, - ); + void _updateSections(List
data) { + final state = _sectionsController.value; + + final newState = state.sections.isEmpty + ? SectionsControllerState.initial(sections: data) + : state.copyWith(sections: data); + + _sectionsController.value = newState; + } + + void _handleSectionsControllerChange() { + final activeStepId = _sectionsController.value.activeStepId; + + if (_activeStepId != activeStepId) { + _activeStepId = activeStepId; + + final event = ActiveStepChangedEvent(activeStepId); + context.read().add(event); + } } } diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_rich_text_step.dart b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_rich_text_step.dart index 9d5814abbc7..676190729c3 100644 --- a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_rich_text_step.dart +++ b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_rich_text_step.dart @@ -1,9 +1,13 @@ +import 'package:catalyst_voices/common/codecs/markdown_codec.dart'; import 'package:catalyst_voices/widgets/navigation/section_step_state_builder.dart'; import 'package:catalyst_voices/widgets/rich_text/voices_rich_text.dart'; import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_quill/flutter_quill.dart'; class WorkspaceRichTextStep extends StatefulWidget { @@ -26,7 +30,10 @@ class _WorkspaceRichTextStepState extends State { void initState() { super.initState(); - final document = Document.fromJson(widget.step.data.value); + final markdownString = widget.step.initialData ?? const MarkdownData(''); + final delta = markdown.encode(markdownString); + + final document = delta.isNotEmpty ? Document.fromDelta(delta) : Document(); final selectionOffset = document.length == 0 ? 0 : document.length - 1; _controller = VoicesRichTextController( @@ -55,16 +62,31 @@ class _WorkspaceRichTextStepState extends State { ); }, child: VoicesRichText( - title: widget.step.localizedDesc(context), + title: widget.step.description ?? widget.step.name, controller: _controller, editModeController: _editModeController, charsLimit: widget.step.charsLimit, canEditDocumentGetter: _canEditDocument, onEditBlocked: _showEditBlockedRationale, + onSaved: _saveDocument, ), ); } + void _saveDocument(Document document) { + final delta = document.toDelta(); + final markdownString = markdown.decode(delta); + + final sectionStepId = widget.step.sectionStepId; + + final event = UpdateStepAnswerEvent( + id: sectionStepId, + data: markdownString.data.isNotEmpty ? markdownString : null, + ); + + context.read().add(event); + } + bool _canEditDocument(Document document) { final sectionsController = SectionsControllerScope.of(context); diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_setup_panel.dart b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_setup_panel.dart index 279dc34265d..52129eb8480 100644 --- a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_setup_panel.dart +++ b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_setup_panel.dart @@ -1,39 +1,11 @@ import 'package:catalyst_voices/pages/workspace/workspace_guidance_view.dart'; import 'package:catalyst_voices/widgets/cards/comment_card.dart'; import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; import 'package:flutter/material.dart'; - -const List mockGuidance = [ - Guidance( - title: 'Use a Compelling Hook or Unique Angle', - description: - '''Adding an element of intrigue or a unique approach can make your title stand out. For example, “Revolutionizing Urban Mobility with Eco-Friendly Innovation” not only describes the proposal but also piques curiosity.''', - type: GuidanceType.tips, - weight: 1, - ), - Guidance( - title: 'Be Specific and Solution-Oriented', - description: - '''Use keywords that pinpoint the problem you’re solving or the opportunity you’re capitalizing on. A title like “Streamlining Supply Chains for Cost-Effective and Rapid Delivery” instantly tells the reader what the proposal aims to achieve.''', - type: GuidanceType.mandatory, - weight: 2, - ), - Guidance( - title: 'Highlight the Benefit or Outcome', - description: - '''Make sure the reader can immediately see the value or the end result of your proposal. A title like “Boosting Engagement and Growth through Targeted Digital Strategies” puts the focus on the positive outcomes.''', - type: GuidanceType.mandatory, - weight: 1, - ), - Guidance( - title: 'Education', - description: 'Use keywords that pinpoint the problem yo', - type: GuidanceType.education, - weight: 1, - ), -]; +import 'package:flutter_bloc/flutter_bloc.dart'; class WorkspaceSetupPanel extends StatelessWidget { const WorkspaceSetupPanel({super.key}); @@ -47,9 +19,7 @@ class WorkspaceSetupPanel extends StatelessWidget { tabs: [ SpaceSidePanelTab( name: 'Guidance', - body: SetupSectionListener( - SectionsControllerScope.of(context), - ), + body: const _GuidanceSelector(), ), SpaceSidePanelTab( name: 'Comments', @@ -71,28 +41,20 @@ class WorkspaceSetupPanel extends StatelessWidget { } } -class SetupSectionListener extends StatelessWidget { - final SectionsController _controller; - - const SetupSectionListener( - this._controller, { - super.key, - }); +class _GuidanceSelector extends StatelessWidget { + const _GuidanceSelector(); @override Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: _controller, - builder: (context, value, _) { - final activeStepId = value.activeStepId; - final activeStepGuidances = value.activeStepGuidances; - - if (activeStepId == null) { + return BlocSelector( + selector: (state) => state.guidance, + builder: (context, state) { + if (state.isNoneSelected) { return Text(context.l10n.selectASection); - } else if (activeStepGuidances == null || activeStepGuidances.isEmpty) { + } else if (state.showEmptyState) { return Text(context.l10n.noGuidanceForThisSection); } else { - return GuidanceView(activeStepGuidances); + return GuidanceView(state.guidances); } }, ); diff --git a/catalyst_voices/apps/voices/lib/widgets/cards/guidance_card.dart b/catalyst_voices/apps/voices/lib/widgets/cards/guidance_card.dart index 17e2aae73e0..3cf6253019d 100644 --- a/catalyst_voices/apps/voices/lib/widgets/cards/guidance_card.dart +++ b/catalyst_voices/apps/voices/lib/widgets/cards/guidance_card.dart @@ -2,7 +2,7 @@ import 'package:catalyst_voices/common/ext/guidance_ext.dart'; import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; -import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:flutter/material.dart'; class GuidanceCard extends StatelessWidget { @@ -57,6 +57,14 @@ class GuidanceCard extends StatelessWidget { ); } - String _buildTypeTitle(BuildContext context) => - '${guidance.type.localizedType(context.l10n)} ${guidance.weightText}'; + String _buildTypeTitle(BuildContext context) { + final weight = guidance.weight; + final localizedType = guidance.type.localizedType(context.l10n); + + if (weight == null) { + return localizedType; + } + + return '$localizedType $weight'; + } } diff --git a/catalyst_voices/apps/voices/lib/widgets/menu/voices_node_menu.dart b/catalyst_voices/apps/voices/lib/widgets/menu/voices_node_menu.dart index 8a365d2b104..9498840bb8b 100644 --- a/catalyst_voices/apps/voices/lib/widgets/menu/voices_node_menu.dart +++ b/catalyst_voices/apps/voices/lib/widgets/menu/voices_node_menu.dart @@ -6,7 +6,7 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; final class VoicesNodeMenuItem extends Equatable { - final int id; + final String id; final String label; final bool isEnabled; @@ -28,8 +28,8 @@ class VoicesNodeMenu extends StatelessWidget { final String name; final Widget? icon; final VoidCallback? onHeaderTap; - final int? selectedItemId; - final ValueChanged onItemTap; + final String? selectedItemId; + final ValueChanged onItemTap; final List items; final bool isExpandable; final bool isExpanded; diff --git a/catalyst_voices/apps/voices/lib/widgets/navigation/sections_controller.dart b/catalyst_voices/apps/voices/lib/widgets/navigation/sections_controller.dart index f3dc14e24be..85f31b34c6a 100644 --- a/catalyst_voices/apps/voices/lib/widgets/navigation/sections_controller.dart +++ b/catalyst_voices/apps/voices/lib/widgets/navigation/sections_controller.dart @@ -9,33 +9,20 @@ import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; final class SectionsControllerState extends Equatable { final List
sections; - final Set openedSections; + final Set openedSections; final SectionStepId? activeStepId; final Set editStepsIds; - final GuidanceType? activeGuidance; const SectionsControllerState({ this.sections = const [], this.openedSections = const {}, this.activeStepId, this.editStepsIds = const {}, - this.activeGuidance, }); - int? get activeSectionId => activeStepId?.sectionId; + String? get activeSectionId => activeStepId?.sectionId; - int? get activeStep => activeStepId?.stepId; - - List? get activeStepGuidances { - final activeStepId = this.activeStepId; - if (activeStepId == null) { - return null; - } else { - return sections[activeStepId.sectionId] - .steps[activeStepId.stepId] - .guidances; - } - } + String? get activeStep => activeStepId?.stepId; bool get allSegmentsClosed => openedSections.isEmpty; @@ -73,17 +60,15 @@ final class SectionsControllerState extends Equatable { SectionsControllerState copyWith({ List
? sections, - Set? openedSections, + Set? openedSections, Optional? activeStepId, Set? editStepsIds, - Optional? activeGuidance, }) { return SectionsControllerState( sections: sections ?? this.sections, openedSections: openedSections ?? this.openedSections, activeStepId: activeStepId.dataOr(this.activeStepId), editStepsIds: editStepsIds ?? this.editStepsIds, - activeGuidance: activeGuidance?.dataOr(this.activeGuidance), ); } @@ -93,7 +78,6 @@ final class SectionsControllerState extends Equatable { openedSections, activeStepId, editStepsIds, - activeGuidance, ]; } @@ -113,7 +97,7 @@ final class SectionsController extends ValueNotifier { _itemsScrollController = null; } - void toggleSection(int id) { + void toggleSection(String id) { final openedSections = {...value.openedSections}; final allSegmentsClosed = value.allSegmentsClosed; final shouldOpen = !openedSections.contains(id); @@ -159,7 +143,7 @@ final class SectionsController extends ValueNotifier { unawaited(_scrollToSectionStep(id)); } - void focusSection(int id) { + void focusSection(String id) { unawaited(_scrollToSection(id)); } @@ -183,17 +167,13 @@ final class SectionsController extends ValueNotifier { ); } - void setActiveGuidance(GuidanceType? type) { - value = value.copyWith(activeGuidance: Optional(type)); - } - @override void dispose() { detachItemsScrollController(); super.dispose(); } - Future _scrollToSection(int id) async { + Future _scrollToSection(String id) async { final index = value.listItems.indexWhere((e) => e is Section && e.id == id); if (index == -1) { return; diff --git a/catalyst_voices/apps/voices/lib/widgets/navigation/sections_menu.dart b/catalyst_voices/apps/voices/lib/widgets/navigation/sections_menu.dart index d0435ea1206..e8d23e6bb6f 100644 --- a/catalyst_voices/apps/voices/lib/widgets/navigation/sections_menu.dart +++ b/catalyst_voices/apps/voices/lib/widgets/navigation/sections_menu.dart @@ -31,9 +31,9 @@ class SectionsMenuListener extends StatelessWidget { class SectionsMenu extends StatelessWidget { final List
sections; - final Set openedSections; + final Set openedSections; final SectionStepId? selectedStep; - final ValueChanged onSectionTap; + final ValueChanged onSectionTap; final ValueChanged onStepSelected; const SectionsMenu({ diff --git a/catalyst_voices/apps/voices/lib/widgets/rich_text/voices_rich_text.dart b/catalyst_voices/apps/voices/lib/widgets/rich_text/voices_rich_text.dart index 848796f5364..a8b4aeab9cb 100644 --- a/catalyst_voices/apps/voices/lib/widgets/rich_text/voices_rich_text.dart +++ b/catalyst_voices/apps/voices/lib/widgets/rich_text/voices_rich_text.dart @@ -511,11 +511,13 @@ class _TopBar extends StatelessWidget { Widget build(BuildContext context) { return Row( children: [ - Text( - title, - style: Theme.of(context).textTheme.titleMedium, + Expanded( + child: Text( + title, + style: Theme.of(context).textTheme.titleMedium, + ), ), - const Spacer(), + const SizedBox(width: 16), VoicesTextButton( onTap: onToggleEditMode, child: Text( diff --git a/catalyst_voices/apps/voices/pubspec.yaml b/catalyst_voices/apps/voices/pubspec.yaml index 3b00fa0d7e6..01ae70d9b41 100644 --- a/catalyst_voices/apps/voices/pubspec.yaml +++ b/catalyst_voices/apps/voices/pubspec.yaml @@ -54,6 +54,8 @@ dependencies: go_router: ^14.0.2 google_fonts: ^6.2.1 intl: ^0.19.0 + markdown: ^7.2.2 + markdown_quill: ^4.2.0 mask_text_input_formatter: ^2.9.0 result_type: ^0.2.0 scrollable_positioned_list: ^0.3.8 diff --git a/catalyst_voices/apps/voices/test/common/codecs/markdown_codec_test.dart b/catalyst_voices/apps/voices/test/common/codecs/markdown_codec_test.dart new file mode 100644 index 00000000000..dd0961ed796 --- /dev/null +++ b/catalyst_voices/apps/voices/test/common/codecs/markdown_codec_test.dart @@ -0,0 +1,89 @@ +import 'package:catalyst_voices/common/codecs/markdown_codec.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:flutter_quill/quill_delta.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group(MarkdownCodec, () { + test('code and encode empty string', () { + // Given + const source = MarkdownData(''); + + // When + final delta = markdown.encode(source); + final md = markdown.decode(delta); + + // Then + expect(md, source); + }); + + test('code and encode plain text', () { + // Given + const source = MarkdownData('Hello Catalyst!'); + + // When + final delta = markdown.encode(source); + final md = markdown.decode(delta); + + // Then + expect(md, source); + }); + + group('decode', () { + test('empty delta file builds empty string', () { + // Given + final delta = Delta(); + + // When + final markdownString = markdown.decode(delta); + + // Then + expect(markdownString.data, isEmpty); + }); + + test('plan text delta file builds correct string', () { + // Given + const plainText = 'Hello Catalyst!'; + final delta = Delta() + ..insert(plainText) + ..insert('\n'); + + // When + final markdownString = markdown.decode(delta); + + // Then + expect(markdownString.data, plainText); + }); + }); + + group('encode', () { + test('empty markdown string builds valid empty delta', () { + // Given + const markdownString = MarkdownData(''); + + // When + final delta = markdown.encode(markdownString); + + // Then + expect(delta.isEmpty, isTrue); + }); + + test('plan text markdown builds correct delta', () { + // Given + const plainText = 'Hello Catalyst!'; + const markdownString = MarkdownData(plainText); + + // When + final delta = markdown.encode(markdownString); + + // Then + expect(delta.isNotEmpty, isTrue); + expect(delta.operations, hasLength(1)); + + final operation = delta.operations[0]; + expect(operation.key, 'insert'); + expect(operation.data, '$plainText\n'); + }); + }); + }); +} diff --git a/catalyst_voices/melos.yaml b/catalyst_voices/melos.yaml index 97a3f9ef63a..16b137e213d 100644 --- a/catalyst_voices/melos.yaml +++ b/catalyst_voices/melos.yaml @@ -112,6 +112,8 @@ command: formz: ^0.7.0 intl: ^0.19.0 logging: ^1.2.0 + markdown: ^7.2.2 + markdown_quill: ^4.2.0 meta: ^1.10.0 result_type: ^0.2.0 password_strength: ^0.2.0 diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/catalyst_voices_blocs.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/catalyst_voices_blocs.dart index 282367fe718..b364f054f88 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/catalyst_voices_blocs.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/catalyst_voices_blocs.dart @@ -8,3 +8,4 @@ export 'login/login.dart'; export 'proposals/proposals.dart'; export 'registration/registration.dart'; export 'session/session.dart'; +export 'workspace/workspace.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace.dart new file mode 100644 index 00000000000..e5b20e31001 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace.dart @@ -0,0 +1,3 @@ +export 'workspace_bloc.dart'; +export 'workspace_event.dart'; +export 'workspace_state.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace_bloc.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace_bloc.dart new file mode 100644 index 00000000000..89ca8bf2c09 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace_bloc.dart @@ -0,0 +1,115 @@ +import 'package:catalyst_voices_blocs/src/workspace/workspace_event.dart'; +import 'package:catalyst_voices_blocs/src/workspace/workspace_state.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_services/catalyst_voices_services.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +final class WorkspaceBloc extends Bloc { + final CampaignService _campaignService; + + final _answers = {}; + final _guidances = >{}; + + SectionStepId? _activeStepId; + + WorkspaceBloc( + this._campaignService, + ) : super(const WorkspaceState()) { + on(_loadCurrentProposal); + on(_updateStepAnswer); + on(_handleActiveStepEvent); + } + + Future _loadCurrentProposal( + LoadCurrentProposalEvent event, + Emitter emit, + ) async { + _answers.clear(); + _guidances.clear(); + + final activeCampaign = await _campaignService.getActiveCampaign(); + if (activeCampaign == null) { + emit( + state.copyWith( + sections: [], + guidance: const WorkspaceGuidance(isNoneSelected: true), + ), + ); + return; + } + + final template = activeCampaign.proposalTemplate; + + final sections = template.sections.map(_mapProposalSection).toList(); + + for (final section in template.sections) { + for (final step in section.steps) { + final id = (sectionId: section.id, stepId: step.id); + _guidances[id] = step.guidances; + } + } + + final activeStepId = _activeStepId; + final guidances = _guidances[activeStepId] ?? []; + final guidance = WorkspaceGuidance( + isNoneSelected: activeStepId == null, + guidances: guidances, + ); + + emit( + state.copyWith( + sections: sections, + guidance: guidance, + ), + ); + } + + void _updateStepAnswer( + UpdateStepAnswerEvent event, + Emitter emit, + ) { + final answer = event.data; + if (answer != null) { + _answers[event.id] = answer; + } else { + _answers.remove(event.id); + } + } + + void _handleActiveStepEvent( + ActiveStepChangedEvent event, + Emitter emit, + ) { + _activeStepId = event.id; + + final activeStepId = _activeStepId; + final guidances = _guidances[activeStepId] ?? []; + final guidance = WorkspaceGuidance( + isNoneSelected: activeStepId == null, + guidances: guidances, + ); + + emit(state.copyWith(guidance: guidance)); + } + + WorkspaceSection _mapProposalSection(ProposalSection section) { + return WorkspaceSection( + id: section.id, + name: section.name, + steps: section.steps.map( + (step) { + final id = (sectionId: section.id, stepId: step.id); + + return RichTextStep( + id: step.id, + sectionId: section.id, + name: step.name, + description: step.description, + initialData: _answers[id] ?? step.answer, + ); + }, + ).toList(), + ); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace_event.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace_event.dart new file mode 100644 index 00000000000..f51dff58017 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace_event.dart @@ -0,0 +1,36 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:equatable/equatable.dart'; + +sealed class WorkspaceEvent extends Equatable { + const WorkspaceEvent(); +} + +final class LoadCurrentProposalEvent extends WorkspaceEvent { + const LoadCurrentProposalEvent(); + + @override + List get props => []; +} + +final class UpdateStepAnswerEvent extends WorkspaceEvent { + final SectionStepId id; + final MarkdownData? data; + + const UpdateStepAnswerEvent({ + required this.id, + this.data, + }); + + @override + List get props => [id, data]; +} + +final class ActiveStepChangedEvent extends WorkspaceEvent { + final SectionStepId? id; + + const ActiveStepChangedEvent(this.id); + + @override + List get props => [id]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace_state.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace_state.dart new file mode 100644 index 00000000000..1e9429f486f --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace_state.dart @@ -0,0 +1,47 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:equatable/equatable.dart'; + +final class WorkspaceState extends Equatable { + final List
sections; + final WorkspaceGuidance guidance; + + const WorkspaceState({ + this.sections = const [], + this.guidance = const WorkspaceGuidance(), + }); + + WorkspaceState copyWith({ + List
? sections, + WorkspaceGuidance? guidance, + }) { + return WorkspaceState( + sections: sections ?? this.sections, + guidance: guidance ?? this.guidance, + ); + } + + @override + List get props => [ + sections, + guidance, + ]; +} + +final class WorkspaceGuidance extends Equatable { + final bool isNoneSelected; + final List guidances; + + const WorkspaceGuidance({ + this.isNoneSelected = false, + this.guidances = const [], + }); + + bool get showEmptyState => !isNoneSelected && guidances.isEmpty; + + @override + List get props => [ + isNoneSelected, + guidances, + ]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/test/proposals/proposals_cubit_test.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/test/proposals/proposals_cubit_test.dart index 42d48a846ba..57c89fc6677 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/test/proposals/proposals_cubit_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/test/proposals/proposals_cubit_test.dart @@ -19,8 +19,19 @@ void main() { publish: ProposalPublish.draft, access: ProposalAccess.private, commentsCount: 0, - completedSegments: 1, - totalSegments: 3, + sections: List.generate(3, (index) { + return ProposalSection( + id: 'f14/0_$index', + name: 'Section_$index', + steps: [ + ProposalSectionStep( + id: 'f14/0_${index}_1', + name: 'Topic 1', + answer: index < 1 ? const MarkdownData('Ans') : null, + ), + ], + ); + }), ); final pendingProposal = PendingProposal.fromProposal( diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/catalyst_voices_models.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/catalyst_voices_models.dart index ebfd61d8e0d..216f663bc30 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/catalyst_voices_models.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/catalyst_voices_models.dart @@ -8,12 +8,13 @@ export 'campaign/campaign_publish.dart'; export 'campaign/campaign_section.dart'; export 'crypto/keychain_metadata.dart'; export 'crypto/lock_factor.dart'; -export 'document/document_json.dart'; export 'errors/errors.dart'; export 'file/voices_file.dart'; +export 'markdown_data.dart'; export 'optional.dart'; +export 'proposal/guidance.dart'; export 'proposal/proposal.dart'; -export 'proposal/proposal_builder.dart'; +export 'proposal/proposal_section.dart'; export 'proposal/proposal_template.dart'; export 'registration/registration.dart'; export 'seed_phrase.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_json.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_json.dart deleted file mode 100644 index d8e1af38e29..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_json.dart +++ /dev/null @@ -1 +0,0 @@ -extension type const DocumentJson(List value) {} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/markdown_data.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/markdown_data.dart new file mode 100644 index 00000000000..6c5bebcfef8 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/markdown_data.dart @@ -0,0 +1 @@ +extension type const MarkdownData(String data) implements Object {} diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal/guidance/guidance.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/guidance.dart similarity index 71% rename from catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal/guidance/guidance.dart rename to catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/guidance.dart index 7cf394c461f..f51cc667d37 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal/guidance/guidance.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/guidance.dart @@ -1,22 +1,36 @@ -import 'package:catalyst_voices_view_models/src/proposal/guidance/guidance_type.dart'; +import 'dart:core'; + import 'package:equatable/equatable.dart'; +enum GuidanceType { + mandatory(priority: 0), + education(priority: 1), + tips(priority: 2); + + final int priority; + + const GuidanceType({ + required this.priority, + }); +} + final class Guidance extends Equatable implements Comparable { + final String id; final String title; final String description; final GuidanceType type; - final int? weight; // This represents how important the guidance is in - //specific [GuidanceType]. + + /// This represents how important the guidance is in specific [GuidanceType]. + final int? weight; const Guidance({ + required this.id, required this.title, required this.description, required this.type, this.weight, }); - String get weightText => weight?.toString() ?? ''; - @override int compareTo(Guidance other) { final typeComparison = type.priority.compareTo(other.type.priority); @@ -31,9 +45,11 @@ final class Guidance extends Equatable implements Comparable { @override List get props => [ + id, title, description, type, + weight, ]; } diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal.dart index d7ea28d2cba..68995d67524 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal.dart @@ -1,4 +1,5 @@ import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; +import 'package:catalyst_voices_models/src/proposal/proposal_section.dart'; import 'package:equatable/equatable.dart'; // Note. This enum may be deleted later. Its here for backwards compatibility. @@ -18,14 +19,13 @@ final class Proposal extends Equatable { final ProposalStatus status; final ProposalPublish publish; final ProposalAccess access; + final List sections; // This may be a reference to class final String category; // Those may be getters. final int commentsCount; - final int completedSegments; - final int totalSegments; const Proposal({ required this.id, @@ -37,12 +37,17 @@ final class Proposal extends Equatable { required this.status, required this.publish, required this.access, + required this.sections, required this.category, required this.commentsCount, - required this.completedSegments, - required this.totalSegments, }); + int get totalSegments => sections.length; + + int get completedSegments { + return sections.where((element) => element.isCompleted).length; + } + @override List get props => [ id, @@ -53,9 +58,8 @@ final class Proposal extends Equatable { fundsRequested.value, publish, access, + sections, category, commentsCount, - completedSegments, - totalSegments, ]; } diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_builder.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_builder.dart deleted file mode 100644 index 250bad0247d..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_builder.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:catalyst_voices_models/src/proposal/proposal.dart'; -import 'package:catalyst_voices_models/src/proposal/proposal_template.dart'; -import 'package:equatable/equatable.dart'; - -final class ProposalBuilder extends Equatable { - const ProposalBuilder(); - - // ignore: avoid_unused_constructor_parameters - const ProposalBuilder.fromTemplate(ProposalTemplate template) : this(); - - // ignore: avoid_unused_constructor_parameters - const ProposalBuilder.fromProposal(Proposal proposal) : this(); - - bool get isValid => false; - - Proposal build() { - throw UnimplementedError(); - } - - ProposalBuilder copyWith() { - return const ProposalBuilder(); - } - - @override - List get props => []; -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_section.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_section.dart new file mode 100644 index 00000000000..069fc44ebb9 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_section.dart @@ -0,0 +1,78 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:equatable/equatable.dart'; + +final class ProposalSection extends Equatable { + final String id; + final String name; + final List steps; + + const ProposalSection({ + required this.id, + required this.name, + required this.steps, + }); + + bool get isCompleted => steps.every((element) => element.hasAnswer); + + ProposalSection copyWith({ + String? id, + String? name, + List? steps, + }) { + return ProposalSection( + id: id ?? this.id, + name: name ?? this.name, + steps: steps ?? this.steps, + ); + } + + @override + List get props => [ + id, + name, + steps, + ]; +} + +final class ProposalSectionStep extends Equatable { + final String id; + final String name; + final String? description; + final List guidances; + final MarkdownData? answer; + + const ProposalSectionStep({ + required this.id, + required this.name, + this.description, + this.guidances = const [], + this.answer, + }); + + bool get hasAnswer => answer != null; + + ProposalSectionStep copyWith({ + String? id, + String? name, + Optional? description, + List? guidances, + Optional? answer, + }) { + return ProposalSectionStep( + id: id ?? this.id, + name: name ?? this.name, + description: description.dataOr(this.description), + guidances: guidances ?? this.guidances, + answer: answer.dataOr(this.answer), + ); + } + + @override + List get props => [ + id, + name, + description, + guidances, + answer, + ]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_template.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_template.dart index f7ffb480043..afd4aa586be 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_template.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_template.dart @@ -1,8 +1,15 @@ +import 'package:catalyst_voices_models/src/proposal/proposal_section.dart'; import 'package:equatable/equatable.dart'; final class ProposalTemplate extends Equatable { - const ProposalTemplate(); + final List sections; + + const ProposalTemplate({ + required this.sections, + }); @override - List get props => []; + List get props => [ + sections, + ]; } diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/campaign/campaign_repository.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/campaign/campaign_repository.dart index 8ffb5f1875a..67785d512cc 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/campaign/campaign_repository.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/campaign/campaign_repository.dart @@ -32,7 +32,63 @@ class CampaignRepository { proposalsCount: 0, sections: sections, publish: CampaignPublish.draft, - proposalTemplate: const ProposalTemplate(), + proposalTemplate: ProposalTemplate( + sections: [ + ProposalSection( + id: '${id}_1', + name: 'Proposal Setup', + steps: [ + ProposalSectionStep( + id: '${id}_1_1', + name: 'Title', + guidances: List.from(_mockGuidance), + ), + ], + ), + ProposalSection( + id: '${id}_2', + name: 'Proposal Summary', + steps: [ + ProposalSectionStep( + id: '${id}_2_1', + name: 'Problem Statement', + guidances: List.from(_mockGuidance), + ), + ProposalSectionStep( + id: '${id}_2_2', + name: 'Solution Statement', + guidances: List.from(_mockGuidance), + ), + ], + ), + ProposalSection( + id: '${id}_3', + name: 'Proposal Setup', + steps: [ + ProposalSectionStep( + id: '${id}_3_1', + name: 'Topic 1', + guidances: List.from(_mockGuidance), + ), + ProposalSectionStep( + id: '${id}_3_2', + name: 'Topic 2', + guidances: List.from(_mockGuidance), + ), + ProposalSectionStep( + id: '${id}_3_3', + name: 'Topic 3', + guidances: List.from(_mockGuidance), + ), + ProposalSectionStep( + id: '${id}_3_4', + name: 'Topic 4', + guidances: List.from(_mockGuidance), + ), + ], + ), + ], + ), ); } } @@ -61,3 +117,37 @@ As part of their deliverables, projects will also be required to submit open source, high quality documentation for their technology that can be used as a learning resource by the rest of the community.'''; + +const List _mockGuidance = [ + Guidance( + id: 'g_1', + title: 'Use a Compelling Hook or Unique Angle', + description: + '''Adding an element of intrigue or a unique approach can make your title stand out. For example, “Revolutionizing Urban Mobility with Eco-Friendly Innovation” not only describes the proposal but also piques curiosity.''', + type: GuidanceType.tips, + weight: 1, + ), + Guidance( + id: 'g_1', + title: 'Be Specific and Solution-Oriented', + description: + '''Use keywords that pinpoint the problem you’re solving or the opportunity you’re capitalizing on. A title like “Streamlining Supply Chains for Cost-Effective and Rapid Delivery” instantly tells the reader what the proposal aims to achieve.''', + type: GuidanceType.mandatory, + weight: 2, + ), + Guidance( + id: 'g_1', + title: 'Highlight the Benefit or Outcome', + description: + '''Make sure the reader can immediately see the value or the end result of your proposal. A title like “Boosting Engagement and Growth through Targeted Digital Strategies” puts the focus on the positive outcomes.''', + type: GuidanceType.mandatory, + weight: 1, + ), + Guidance( + id: 'g_1', + title: 'Education', + description: 'Use keywords that pinpoint the problem yo', + type: GuidanceType.education, + weight: 1, + ), +]; diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/proposal/proposal_repository.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/proposal/proposal_repository.dart index 92d96ff6644..67012f80e3e 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/proposal/proposal_repository.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/proposal/proposal_repository.dart @@ -36,8 +36,18 @@ final _proposals = [ access: ProposalAccess.private, commentsCount: 0, description: _proposalDescription, - completedSegments: 0, - totalSegments: 13, + sections: List.generate(13, (index) { + return ProposalSection( + id: 'f14/0_$index', + name: 'Section_$index', + steps: [ + ProposalSectionStep( + id: 'f14/0_${index}_1', + name: 'Topic 1', + ), + ], + ); + }), ), Proposal( id: 'f14/1', @@ -50,8 +60,19 @@ final _proposals = [ access: ProposalAccess.private, commentsCount: 0, description: _proposalDescription, - completedSegments: 7, - totalSegments: 13, + sections: List.generate(13, (index) { + return ProposalSection( + id: 'f14/0_$index', + name: 'Section_$index', + steps: [ + ProposalSectionStep( + id: 'f14/0_${index}_1', + name: 'Topic 1', + answer: index < 7 ? const MarkdownData('Ans') : null, + ), + ], + ); + }), ), Proposal( id: 'f14/2', @@ -64,7 +85,18 @@ final _proposals = [ access: ProposalAccess.private, commentsCount: 0, description: _proposalDescription, - completedSegments: 13, - totalSegments: 13, + sections: List.generate(13, (index) { + return ProposalSection( + id: 'f14/0_$index', + name: 'Section_$index', + steps: [ + ProposalSectionStep( + id: 'f14/0_${index}_1', + name: 'Topic 1', + answer: const MarkdownData('Ans'), + ), + ], + ); + }), ), ]; diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/campaign/campaign_service.dart b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/campaign/campaign_service.dart index f86f6844bb0..58e5e2388bb 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/campaign/campaign_service.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/campaign/campaign_service.dart @@ -1,13 +1,36 @@ import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart'; -class CampaignService { +abstract interface class CampaignService { + factory CampaignService( + CampaignRepository campaignRepository, + ) { + return CampaignServiceImpl( + campaignRepository, + ); + } + + Future isAnyCampaignActive(); + + Future getActiveCampaign(); +} + +final class CampaignServiceImpl implements CampaignService { final CampaignRepository _campaignRepository; - const CampaignService(this._campaignRepository); + const CampaignServiceImpl( + this._campaignRepository, + ); + + @override + Future isAnyCampaignActive() async { + return true; + } + + @override + Future getActiveCampaign() async { + final campaign = await _campaignRepository.getCampaign(id: 'F14'); - Future getActiveCampaign() { - // TODO(dtscalac): replace by api call - return _campaignRepository.getCampaign(id: 'F14'); + return campaign; } } diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/catalyst_voices_services.dart b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/catalyst_voices_services.dart index 5be0a1e4193..d478ef80cc4 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/catalyst_voices_services.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/catalyst_voices_services.dart @@ -1,4 +1,4 @@ -export 'campaign/campaign_service.dart'; +export 'campaign/campaign_service.dart' show CampaignService; export 'crypto/key_derivation.dart'; export 'downloader/downloader.dart'; export 'keychain/keychain.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/catalyst_voices_view_models.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/catalyst_voices_view_models.dart index 9b6f5744c1f..5b5004d26ad 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/catalyst_voices_view_models.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/catalyst_voices_view_models.dart @@ -11,8 +11,6 @@ export 'navigation/sections_list_view_item.dart'; export 'navigation/sections_navigation.dart'; export 'proposal/comment.dart'; export 'proposal/funded_proposal.dart'; -export 'proposal/guidance/guidance.dart'; -export 'proposal/guidance/guidance_type.dart'; export 'proposal/pending_proposal.dart'; export 'registration/exception/localized_registration_exception.dart'; export 'registration/registration.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/navigation/sections_navigation.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/navigation/sections_navigation.dart index 45a82fb464f..0eba0c4bc75 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/navigation/sections_navigation.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/navigation/sections_navigation.dart @@ -3,10 +3,10 @@ import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/widgets.dart'; -typedef SectionStepId = ({int sectionId, int stepId}); +typedef SectionStepId = ({String sectionId, String stepId}); abstract interface class Section implements SectionsListViewItem { - int get id; + String get id; SvgGenImage get icon; @@ -16,9 +16,9 @@ abstract interface class Section implements SectionsListViewItem { } abstract interface class SectionStep implements SectionsListViewItem { - int get id; + String get id; - int get sectionId; + String get sectionId; SectionStepId get sectionStepId; @@ -26,15 +26,13 @@ abstract interface class SectionStep implements SectionsListViewItem { bool get isEditable; - List get guidances; - String localizedName(BuildContext context); } abstract base class BaseSection extends Equatable implements Section { @override - final int id; + final String id; @override final List steps; @@ -58,22 +56,19 @@ abstract base class BaseSection extends Equatable abstract base class BaseSectionStep extends Equatable implements SectionStep { @override - final int id; + final String id; @override - final int sectionId; + final String sectionId; @override final bool isEnabled; @override final bool isEditable; - @override - final List guidances; const BaseSectionStep({ required this.id, required this.sectionId, this.isEnabled = true, this.isEditable = true, - this.guidances = const [], }); @override diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal/guidance/guidance_type.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal/guidance/guidance_type.dart deleted file mode 100644 index 7b6ccc6990e..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal/guidance/guidance_type.dart +++ /dev/null @@ -1,11 +0,0 @@ -enum GuidanceType { mandatory, education, tips } - -extension GuidanceTypeExt on GuidanceType { - int get priority { - return switch (this) { - GuidanceType.mandatory => 0, // Highest priority - GuidanceType.education => 1, - GuidanceType.tips => 2, // Lowest priority - }; - } -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/capability_and_feasibility.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/capability_and_feasibility.dart deleted file mode 100644 index c24b42a864c..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/capability_and_feasibility.dart +++ /dev/null @@ -1,51 +0,0 @@ -part of 'workspace_sections.dart'; - -final class CompatibilityAndFeasibility extends WorkspaceSection { - const CompatibilityAndFeasibility({ - required super.id, - required super.steps, - }); - - @override - String localizedName(BuildContext context) { - return 'Compatibility & Feasibility'; - } -} - -final class DeliveryAndAccountabilityStep extends RichTextStep { - const DeliveryAndAccountabilityStep({ - required super.id, - required super.sectionId, - required super.data, - super.charsLimit, - }); - - @override - String localizedName(BuildContext context) { - return 'Delivery & Accountability'; - } - - @override - String localizedDesc(BuildContext context) { - return 'How do you proof trust and accountability for your project?'; - } -} - -final class FeasibilityChecksStep extends RichTextStep { - const FeasibilityChecksStep({ - required super.id, - required super.sectionId, - required super.data, - super.charsLimit, - }); - - @override - String localizedName(BuildContext context) { - return 'Feasibility checks'; - } - - @override - String localizedDesc(BuildContext context) { - return 'How will you check if your approach will work?'; - } -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/proposal_impact.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/proposal_impact.dart deleted file mode 100644 index 92c55ac5280..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/proposal_impact.dart +++ /dev/null @@ -1,41 +0,0 @@ -part of 'workspace_sections.dart'; - -final class ProposalImpact extends WorkspaceSection { - const ProposalImpact({ - required super.id, - required super.steps, - }); - - @override - String localizedName(BuildContext context) { - return 'Proposal impact'; - } -} - -final class BonusMarkUpStep extends RichTextStep { - const BonusMarkUpStep({ - required super.id, - required super.sectionId, - required super.data, - super.charsLimit, - }); - - @override - String localizedName(BuildContext context) { - return 'Bonus mark-up'; - } -} - -final class ValueForMoneyStep extends RichTextStep { - const ValueForMoneyStep({ - required super.id, - required super.sectionId, - required super.data, - super.charsLimit, - }); - - @override - String localizedName(BuildContext context) { - return 'Value for Money'; - } -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/proposal_setup.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/proposal_setup.dart deleted file mode 100644 index 179ea21663a..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/proposal_setup.dart +++ /dev/null @@ -1,27 +0,0 @@ -part of 'workspace_sections.dart'; - -final class ProposalSetup extends WorkspaceSection { - const ProposalSetup({ - required super.id, - required super.steps, - }); - - @override - String localizedName(BuildContext context) { - return 'Proposal setup'; - } -} - -final class TitleStep extends RichTextStep { - const TitleStep({ - required super.id, - required super.sectionId, - required super.data, - super.guidances, - }); - - @override - String localizedName(BuildContext context) { - return 'Title'; - } -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/proposal_solution.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/proposal_solution.dart deleted file mode 100644 index 013e1ed37b2..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/proposal_solution.dart +++ /dev/null @@ -1,73 +0,0 @@ -part of 'workspace_sections.dart'; - -final class ProposalSolution extends WorkspaceSection { - const ProposalSolution({ - required super.id, - required super.steps, - }); - - @override - String localizedName(BuildContext context) { - return 'Proposal solution'; - } -} - -final class ProblemPerspectiveStep extends RichTextStep { - const ProblemPerspectiveStep({ - required super.id, - required super.sectionId, - required super.data, - super.charsLimit, - super.guidances, - }); - - @override - String localizedName(BuildContext context) { - return 'Problem perspective'; - } - - @override - String localizedDesc(BuildContext context) { - return "What is your perspective on the problem you're solving?"; - } -} - -final class PerspectiveRationaleStep extends RichTextStep { - const PerspectiveRationaleStep({ - required super.id, - required super.sectionId, - required super.data, - super.charsLimit, - super.guidances, - }); - - @override - String localizedName(BuildContext context) { - return 'Perspective rationale'; - } - - @override - String localizedDesc(BuildContext context) { - return 'Why did you choose this perspective?'; - } -} - -final class ProjectEngagementStep extends RichTextStep { - const ProjectEngagementStep({ - required super.id, - required super.sectionId, - required super.data, - super.charsLimit, - super.guidances, - }); - - @override - String localizedName(BuildContext context) { - return 'Project engagement'; - } - - @override - String localizedDesc(BuildContext context) { - return 'Who will your project engage?'; - } -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/proposal_summary.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/proposal_summary.dart deleted file mode 100644 index bed4f23ffcd..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/proposal_summary.dart +++ /dev/null @@ -1,58 +0,0 @@ -part of 'workspace_sections.dart'; - -final class ProposalSummary extends WorkspaceSection { - const ProposalSummary({ - required super.id, - required super.steps, - }); - - @override - String localizedName(BuildContext context) { - return 'Proposal summary'; - } -} - -final class ProblemStep extends RichTextStep { - const ProblemStep({ - required super.id, - required super.sectionId, - required super.data, - super.charsLimit, - super.guidances, - }); - - @override - String localizedName(BuildContext context) { - return 'Problem segment'; - } -} - -final class SolutionStep extends RichTextStep { - const SolutionStep({ - required super.id, - required super.sectionId, - required super.data, - super.charsLimit, - super.guidances, - }); - - @override - String localizedName(BuildContext context) { - return 'Solution segment'; - } -} - -final class PublicDescriptionStep extends RichTextStep { - const PublicDescriptionStep({ - required super.id, - required super.sectionId, - required super.data, - super.charsLimit, - super.guidances, - }); - - @override - String localizedName(BuildContext context) { - return 'Public description'; - } -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/workspace_sections.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/workspace_sections.dart index 658e273775f..4ad9e817cd7 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/workspace_sections.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/workspace_sections.dart @@ -2,17 +2,23 @@ import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; import 'package:flutter/widgets.dart'; -part 'capability_and_feasibility.dart'; -part 'proposal_impact.dart'; -part 'proposal_setup.dart'; -part 'proposal_solution.dart'; -part 'proposal_summary.dart'; +final class WorkspaceSection extends BaseSection { + final String name; -sealed class WorkspaceSection extends BaseSection { const WorkspaceSection({ required super.id, + required this.name, required super.steps, }); + + @override + String localizedName(BuildContext context) => name; + + @override + List get props => [ + ...super.props, + name, + ]; } sealed class WorkspaceSectionStep extends BaseSectionStep { @@ -21,22 +27,34 @@ sealed class WorkspaceSectionStep extends BaseSectionStep { required super.sectionId, super.isEnabled, super.isEditable, - super.guidances, }); } -abstract base class RichTextStep extends WorkspaceSectionStep { - final DocumentJson data; +final class RichTextStep extends WorkspaceSectionStep { + final String name; + final String? description; + final MarkdownData? initialData; final int? charsLimit; const RichTextStep({ required super.id, required super.sectionId, - required this.data, + required this.name, + this.description, + this.initialData, this.charsLimit, super.isEditable, - super.guidances, }); - String localizedDesc(BuildContext context) => localizedName(context); + @override + String localizedName(BuildContext context) => name; + + @override + List get props => [ + ...super.props, + name, + description, + initialData, + charsLimit, + ]; } diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/test/campaign/campaign_stage_test.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/test/campaign/campaign_stage_test.dart index c8bfc3b7e2b..6d10f5d3da6 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/test/campaign/campaign_stage_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/test/campaign/campaign_stage_test.dart @@ -15,7 +15,7 @@ void main() { proposalsCount: 0, sections: const [], publish: CampaignPublish.draft, - proposalTemplate: const ProposalTemplate(), + proposalTemplate: const ProposalTemplate(sections: []), ); test('draft campaign resolves to draft stage', () { diff --git a/catalyst_voices/utilities/uikit_example/lib/examples/voices_menu_example.dart b/catalyst_voices/utilities/uikit_example/lib/examples/voices_menu_example.dart index 3dcfa8fb0fa..647b15f6b38 100644 --- a/catalyst_voices/utilities/uikit_example/lib/examples/voices_menu_example.dart +++ b/catalyst_voices/utilities/uikit_example/lib/examples/voices_menu_example.dart @@ -13,7 +13,7 @@ class VoicesMenuExample extends StatefulWidget { } class _VoicesMenuExampleState extends State { - int? _selectedItemId; + String? _selectedItemId; @override void dispose() { @@ -45,9 +45,9 @@ class _VoicesMenuExampleState extends State { }, selectedItemId: _selectedItemId, items: const [ - VoicesNodeMenuItem(id: 0, label: 'Start'), - VoicesNodeMenuItem(id: 1, label: 'Vote'), - VoicesNodeMenuItem(id: 2, label: 'Results'), + VoicesNodeMenuItem(id: '0', label: 'Start'), + VoicesNodeMenuItem(id: '1', label: 'Vote'), + VoicesNodeMenuItem(id: '2', label: 'Results'), ], ), ].separatedBy(const SizedBox(height: 12)).toList(),