From 4b8bf94ea51f1c816b390223b425905dd1d9d549 Mon Sep 17 00:00:00 2001 From: Illya Gerasymchuk Date: Mon, 26 Aug 2024 01:18:03 +0200 Subject: [PATCH] bug: fix 'Go Back' button --- .../screens/ManageProposalStatusesScreen.ts | 497 +++-- .../propose/screens/ProposalHomeScreen.ts | 1791 +++++++++-------- 2 files changed, 1176 insertions(+), 1112 deletions(-) diff --git a/src/channels/admin/screens/ManageProposalStatusesScreen.ts b/src/channels/admin/screens/ManageProposalStatusesScreen.ts index 4709a83..8def035 100644 --- a/src/channels/admin/screens/ManageProposalStatusesScreen.ts +++ b/src/channels/admin/screens/ManageProposalStatusesScreen.ts @@ -16,309 +16,304 @@ import { ManageProposalStatusesPaginator } from '../../../components/ProposalsPa import { ArgumentOracle } from '../../../CustomIDOracle'; export class ManageProposalStatusesScreen extends Screen { - public static readonly ID = 'manageProposalStatuses'; + public static readonly ID = 'manageProposalStatuses'; - protected permissions: Permission[] = []; // TODO: Implement proper admin permissions + protected permissions: Permission[] = []; // TODO: Implement proper admin permissions - public readonly selectFundingRoundAction: SelectFundingRoundAction; - public readonly selectProposalAction: SelectProposalAction; - public readonly updateProposalStatusAction: UpdateProposalStatusAction; + public readonly selectFundingRoundAction: SelectFundingRoundAction; + public readonly selectProposalAction: SelectProposalAction; + public readonly updateProposalStatusAction: UpdateProposalStatusAction; - constructor(dashboard: Dashboard, screenId: string) { - super(dashboard, screenId); - this.selectFundingRoundAction = new SelectFundingRoundAction(this); - this.selectProposalAction = new SelectProposalAction(this); - this.updateProposalStatusAction = new UpdateProposalStatusAction(this, UpdateProposalStatusAction.ID); - } + constructor(dashboard: Dashboard, screenId: string) { + super(dashboard, screenId); + this.selectFundingRoundAction = new SelectFundingRoundAction(this); + this.selectProposalAction = new SelectProposalAction(this); + this.updateProposalStatusAction = new UpdateProposalStatusAction(this, UpdateProposalStatusAction.ID); + } - protected allSubScreens(): Screen[] { - return []; - } + protected allSubScreens(): Screen[] { + return []; + } - protected allActions(): Action[] { - return [ - this.selectFundingRoundAction, - this.selectProposalAction, - this.updateProposalStatusAction, - ]; - } + protected allActions(): Action[] { + return [this.selectFundingRoundAction, this.selectProposalAction, this.updateProposalStatusAction]; + } - protected async getResponse(interaction: TrackedInteraction): Promise { - const embed = new EmbedBuilder() - .setColor('#0099ff') - .setTitle('Manage Proposal Statuses') - .setDescription('Select a Funding Round to manage proposal statuses:'); + protected async getResponse(interaction: TrackedInteraction): Promise { + const embed = new EmbedBuilder() + .setColor('#0099ff') + .setTitle('Manage Proposal Statuses') + .setDescription('Select a Funding Round to manage proposal statuses:'); - const selectFundingRoundButton = this.selectFundingRoundAction.getComponent(); + const selectFundingRoundButton = this.selectFundingRoundAction.getComponent(); - const row = new ActionRowBuilder().addComponents(selectFundingRoundButton); + const row = new ActionRowBuilder().addComponents(selectFundingRoundButton); - return { - embeds: [embed], - components: [row], - ephemeral: true - }; - } + return { + embeds: [embed], + components: [row], + ephemeral: true, + }; + } } export class SelectFundingRoundAction extends Action { - public static readonly ID = 'selectFundingRound'; - - private activeFundingRoundPaginator: ActiveFundingRoundPaginator; - - constructor(screen: ManageProposalStatusesScreen) { - super(screen, SelectFundingRoundAction.ID); - this.activeFundingRoundPaginator = new ActiveFundingRoundPaginator( - this.screen, - this, - 'selectFundingRound', - ActiveFundingRoundPaginator.ID - ); + public static readonly ID = 'selectFundingRound'; + + private activeFundingRoundPaginator: ActiveFundingRoundPaginator; + + constructor(screen: ManageProposalStatusesScreen) { + super(screen, SelectFundingRoundAction.ID); + this.activeFundingRoundPaginator = new ActiveFundingRoundPaginator(this.screen, this, 'selectFundingRound', ActiveFundingRoundPaginator.ID); + } + + protected async handleOperation(interaction: TrackedInteraction, operationId: string): Promise { + switch (operationId) { + case 'showFundingRounds': + case PaginationComponent.PAGINATION_ARG: + await this.handleShowFundingRounds(interaction); + break; + case 'selectFundingRound': + await this.handleSelectFundingRound(interaction); + break; + default: + await this.handleInvalidOperation(interaction, operationId); } + } - protected async handleOperation(interaction: TrackedInteraction, operationId: string): Promise { - switch (operationId) { - case 'showFundingRounds': - case PaginationComponent.PAGINATION_ARG: - await this.handleShowFundingRounds(interaction); - break; - case 'selectFundingRound': - await this.handleSelectFundingRound(interaction); - break; - default: - await this.handleInvalidOperation(interaction, operationId); - } - } + private async handleShowFundingRounds(interaction: TrackedInteraction): Promise { + await this.activeFundingRoundPaginator.handlePagination(interaction); + } - private async handleShowFundingRounds(interaction: TrackedInteraction): Promise { - await this.activeFundingRoundPaginator.handlePagination(interaction); - } + private async handleSelectFundingRound(interaction: TrackedInteraction): Promise { + const parsedInteraction: AnyInteractionWithValues | undefined = InteractionProperties.toInteractionWithValuesOrUndefined(interaction.interaction); - private async handleSelectFundingRound(interaction: TrackedInteraction): Promise { - const parsedInteraction: AnyInteractionWithValues | undefined = InteractionProperties.toInteractionWithValuesOrUndefined(interaction.interaction); + if (!parsedInteraction) { + await DiscordStatus.Error.error(interaction, 'Interaction does not have values'); + throw new EndUserError('Interaction does not have values'); + } - if (!parsedInteraction) { - await DiscordStatus.Error.error(interaction, 'Interaction does not have values'); - throw new EndUserError('Interaction does not have values'); - } + const fundingRoundId = parsedInteraction.values[0]; - const fundingRoundId = parsedInteraction.values[0]; + await (this.screen as ManageProposalStatusesScreen).selectProposalAction.renderHandleShowProposals(interaction, fundingRoundId); + } - await (this.screen as ManageProposalStatusesScreen).selectProposalAction.renderHandleShowProposals(interaction, fundingRoundId); - } + public allSubActions(): Action[] { + return []; + } - public allSubActions(): Action[] { - return []; - } - - getComponent(): ButtonBuilder { - return new ButtonBuilder() - .setCustomId(CustomIDOracle.addArgumentsToAction(this, 'showFundingRounds')) - .setLabel('Select Funding Round') - .setStyle(ButtonStyle.Primary); - } + getComponent(): ButtonBuilder { + return new ButtonBuilder() + .setCustomId(CustomIDOracle.addArgumentsToAction(this, 'showFundingRounds')) + .setLabel('Select Funding Round') + .setStyle(ButtonStyle.Primary); + } } export class SelectProposalAction extends Action { - public static readonly ID = 'selectProposal'; - - - public static readonly OPERATIONS = { - showProposals: 'showProposals', - selectProposal: 'selectProposal' - } - - private manageProposalStatusesPaginator: ManageProposalStatusesPaginator; - - constructor(screen: ManageProposalStatusesScreen) { - super(screen, SelectProposalAction.ID); - this.manageProposalStatusesPaginator = new ManageProposalStatusesPaginator( - this.screen, - this, - 'selectProposal', - ManageProposalStatusesPaginator.ID - ); + public static readonly ID = 'selectProposal'; + + public static readonly OPERATIONS = { + showProposals: 'showProposals', + selectProposal: 'selectProposal', + }; + + private manageProposalStatusesPaginator: ManageProposalStatusesPaginator; + + constructor(screen: ManageProposalStatusesScreen) { + super(screen, SelectProposalAction.ID); + this.manageProposalStatusesPaginator = new ManageProposalStatusesPaginator( + this.screen, + this, + 'selectProposal', + ManageProposalStatusesPaginator.ID, + ); + } + + protected async handleOperation(interaction: TrackedInteraction, operationId: string): Promise { + switch (operationId) { + case 'showProposals': + case PaginationComponent.PAGINATION_ARG: + await this.handleShowProposals(interaction); + break; + case 'selectProposal': + await this.handleSelectProposal(interaction); + break; + default: + await this.handleInvalidOperation(interaction, operationId); } + } - protected async handleOperation(interaction: TrackedInteraction, operationId: string): Promise { - switch (operationId) { - case 'showProposals': - case PaginationComponent.PAGINATION_ARG: - await this.handleShowProposals(interaction); - break; - case 'selectProposal': - await this.handleSelectProposal(interaction); - break; - default: - await this.handleInvalidOperation(interaction, operationId); - } - } + private async handleShowProposals(interaction: TrackedInteraction): Promise { + return await this.renderHandleShowProposals(interaction); + } + public async renderHandleShowProposals(interaction: TrackedInteraction, frId?: string): Promise { + const fundingRoundId = frId || CustomIDOracle.getNamedArgument(interaction.customId, 'frId'); - private async handleShowProposals(interaction: TrackedInteraction): Promise { - return await this.renderHandleShowProposals(interaction); + if (!fundingRoundId) { + await DiscordStatus.Error.error(interaction, 'Funding Round ID not found'); + throw new EndUserError('Funding Round ID not found'); } - public async renderHandleShowProposals(interaction: TrackedInteraction, frId?: string): Promise { - const fundingRoundId = frId || CustomIDOracle.getNamedArgument(interaction.customId, 'frId'); - - if (!fundingRoundId) { - await DiscordStatus.Error.error(interaction, 'Funding Round ID not found'); - throw new EndUserError('Funding Round ID not found'); - } + interaction.Context.set(ArgumentOracle.COMMON_ARGS.FUNDING_ROUND_ID, fundingRoundId); + await this.manageProposalStatusesPaginator.handlePagination(interaction); + } - interaction.Context.set(ArgumentOracle.COMMON_ARGS.FUNDING_ROUND_ID, fundingRoundId); - await this.manageProposalStatusesPaginator.handlePagination(interaction); + private async handleSelectProposal(interaction: TrackedInteraction): Promise { + const parsedInteraction = InteractionProperties.toInteractionWithValuesOrUndefined(interaction.interaction); + if (!parsedInteraction) { + throw new EndUserError('Invalid interaction type.'); } - private async handleSelectProposal(interaction: TrackedInteraction): Promise { - const parsedInteraction = InteractionProperties.toInteractionWithValuesOrUndefined(interaction.interaction); - if (!parsedInteraction) { - throw new EndUserError('Invalid interaction type.'); - } + const proposalId = parsedInteraction.values[0]; + interaction.Context.set('prId', proposalId); - const proposalId = parsedInteraction.values[0]; - interaction.Context.set('proposalId', proposalId); + await (this.screen as ManageProposalStatusesScreen).updateProposalStatusAction.renderShowStatusOptions(interaction, proposalId); + } - await (this.screen as ManageProposalStatusesScreen).updateProposalStatusAction.renderShowStatusOptions(interaction, proposalId); - } - - public allSubActions(): Action[] { - return []; - } + public allSubActions(): Action[] { + return []; + } - getComponent(fundingRoundId: string): ButtonBuilder { - return new ButtonBuilder() - .setCustomId(CustomIDOracle.addArgumentsToAction(this, 'showProposals', 'frId', fundingRoundId)) - .setLabel('Select Proposal') - .setStyle(ButtonStyle.Primary); - } + getComponent(fundingRoundId: string): ButtonBuilder { + return new ButtonBuilder() + .setCustomId(CustomIDOracle.addArgumentsToAction(this, 'showProposals', 'frId', fundingRoundId)) + .setLabel('Select Proposal') + .setStyle(ButtonStyle.Primary); + } } export class UpdateProposalStatusAction extends Action { - public static readonly ID = 'updateProposalStatus'; - - protected async handleOperation(interaction: TrackedInteraction, operationId: string): Promise { - switch (operationId) { - case 'showStatusOptions': - await this.handleShowStatusOptions(interaction); - break; - case 'updateStatus': - await this.handleUpdateStatus(interaction); - break; - default: - await this.handleInvalidOperation(interaction, operationId); - } + public static readonly ID = 'updateProposalStatus'; + + protected async handleOperation(interaction: TrackedInteraction, operationId: string): Promise { + switch (operationId) { + case 'showStatusOptions': + await this.handleShowStatusOptions(interaction); + break; + case 'updateStatus': + await this.handleUpdateStatus(interaction); + break; + default: + await this.handleInvalidOperation(interaction, operationId); } + } - public async renderShowStatusOptions(interaction: TrackedInteraction, pId?: string): Promise { - - const proposalIdFromCntx: string | undefined = interaction.Context.get('proposalId'); - const proposalIdFromCustomId = CustomIDOracle.getNamedArgument(interaction.customId, 'proposalId'); - - const proposalId: string | undefined = pId || proposalIdFromCntx || proposalIdFromCustomId; - - if (!proposalId) { - await DiscordStatus.Error.error(interaction, 'Proposal ID not found in customId, context or arg.'); - return; - } - - const proposal = await AdminProposalLogic.getProposalById(parseInt(proposalId)); - if (!proposal) { - await DiscordStatus.Error.error(interaction, 'Proposal not found'); - return; - } + public async renderShowStatusOptions(interaction: TrackedInteraction, pId?: string): Promise { + const proposalIdFromCntx: string | undefined = interaction.Context.get('prId'); + const proposalIdFromCustomId = CustomIDOracle.getNamedArgument(interaction.customId, 'prId'); - const statusOptions = Object.values(ProposalStatus).filter(status => status !== proposal.status); - - const selectMenu = new StringSelectMenuBuilder() - .setCustomId(CustomIDOracle.addArgumentsToAction(this, 'updateStatus', 'proposalId', proposalId)) - .setPlaceholder('Select new status') - .addOptions(statusOptions.map(status => ({ - label: status, - value: status, - description: `Change status to ${status}` - }))); - - const embed = new EmbedBuilder() - .setColor('#0099ff') - .setTitle(`Update Proposal Status: ${proposal.name}`) - .setDescription(`Current status: ${proposal.status}`) - .addFields( - { name: 'ID', value: proposal.id.toString(), inline: true }, - { name: 'URL', value: proposal.uri, inline: true }, - { name: 'Budget', value: proposal.budget.toString(), inline: true }, - { name: 'Proposer', value: proposal.proposerDuid, inline: true } - ); - - const row = new ActionRowBuilder().addComponents(selectMenu); - - await interaction.update({ embeds: [embed], components: [row] }); + const proposalId: string | undefined = pId || proposalIdFromCntx || proposalIdFromCustomId; + if (!proposalId) { + await DiscordStatus.Error.error(interaction, 'Proposal ID not found in customId, context or arg.'); + return; } - private async handleShowStatusOptions(interaction: TrackedInteraction): Promise { - await this.renderShowStatusOptions(interaction); + const proposal = await AdminProposalLogic.getProposalById(parseInt(proposalId)); + if (!proposal) { + await DiscordStatus.Error.error(interaction, 'Proposal not found'); + return; } - private async handleUpdateStatus(interaction: TrackedInteraction): Promise { - const parsedInteraction: AnyInteractionWithValues | undefined = InteractionProperties.toInteractionWithValuesOrUndefined(interaction.interaction); - - if (!parsedInteraction) { - await DiscordStatus.Error.error(interaction, 'Interaction does not have values'); - throw new EndUserError('Interaction does not have values'); - } - - const proposalIdFromCustomId: string | undefined = CustomIDOracle.getNamedArgument(interaction.customId, 'proposalId'); - const proposalIdFromContext: string | undefined = interaction.Context.get('proposalId'); - - const proposalId: string | undefined = proposalIdFromCustomId || proposalIdFromContext; - if (!proposalId) { - await DiscordStatus.Error.error(interaction, 'Proposal ID not found in customId or context.'); - return; - } - - const newStatus = parsedInteraction.values[0] as ProposalStatus; - - const updatedProposal = await AdminProposalLogic.updateProposalStatus(parseInt(proposalId), newStatus, this.screen); - - const embed = new EmbedBuilder() - .setColor('#00FF00') - .setTitle(`Proposal Status Updated: ${updatedProposal.name}`) - .setDescription(`New status: ${updatedProposal.status}`) - .addFields( - { name: 'ID', value: updatedProposal.id.toString(), inline: true }, - { name: 'URL', value: updatedProposal.uri, inline: true }, - { name: 'Budget', value: updatedProposal.budget.toString(), inline: true }, - { name: 'Proposer', value: updatedProposal.proposerDuid, inline: true } - ); + const statusOptions = Object.values(ProposalStatus).filter((status) => status !== proposal.status); + + const selectMenu = new StringSelectMenuBuilder() + .setCustomId(CustomIDOracle.addArgumentsToAction(this, 'updateStatus', 'prId', proposalId)) + .setPlaceholder('Select new status') + .addOptions( + statusOptions.map((status) => ({ + label: status, + value: status, + description: `Change status to ${status}`, + })), + ); + + const embed = new EmbedBuilder() + .setColor('#0099ff') + .setTitle(`Update Proposal Status: ${proposal.name}`) + .setDescription(`Current status: ${proposal.status}`) + .addFields( + { name: 'ID', value: proposal.id.toString(), inline: true }, + { name: 'URL', value: proposal.uri, inline: true }, + { name: 'Budget', value: proposal.budget.toString(), inline: true }, + { name: 'Proposer', value: proposal.proposerDuid, inline: true }, + ); + + const row = new ActionRowBuilder().addComponents(selectMenu); + + await interaction.update({ embeds: [embed], components: [row] }); + } + + private async handleShowStatusOptions(interaction: TrackedInteraction): Promise { + await this.renderShowStatusOptions(interaction); + } + + private async handleUpdateStatus(interaction: TrackedInteraction): Promise { + const parsedInteraction: AnyInteractionWithValues | undefined = InteractionProperties.toInteractionWithValuesOrUndefined(interaction.interaction); + + if (!parsedInteraction) { + await DiscordStatus.Error.error(interaction, 'Interaction does not have values'); + throw new EndUserError('Interaction does not have values'); + } - const selectPropAction: SelectProposalAction = (this.screen as ManageProposalStatusesScreen).selectProposalAction; + const proposalIdFromCustomId: string | undefined = CustomIDOracle.getNamedArgument(interaction.customId, 'prId'); + const proposalIdFromContext: string | undefined = interaction.Context.get('prId'); - if (!updatedProposal.fundingRoundId) { - await DiscordStatus.Warning.warning(interaction, `Proposal does not have a Funding Round associated`); - throw new EndUserError(`Proposal ${updatedProposal.id} does not have a Funding Round associated`); - } + const proposalId: string | undefined = proposalIdFromCustomId || proposalIdFromContext; + if (!proposalId) { + await DiscordStatus.Error.error(interaction, 'Proposal ID not found in customId or context.'); + return; + } - const backButton = new ButtonBuilder() - .setCustomId(CustomIDOracle.addArgumentsToAction(selectPropAction, SelectProposalAction.OPERATIONS.showProposals, 'frId', updatedProposal.fundingRoundId.toString())) - .setLabel('Update Status Again') - .setStyle(ButtonStyle.Primary); + const newStatus = parsedInteraction.values[0] as ProposalStatus; - const row = new ActionRowBuilder().addComponents(backButton); + const updatedProposal = await AdminProposalLogic.updateProposalStatus(parseInt(proposalId), newStatus, this.screen); - await interaction.update({ embeds: [embed], components: [row] }); + const embed = new EmbedBuilder() + .setColor('#00FF00') + .setTitle(`Proposal Status Updated: ${updatedProposal.name}`) + .setDescription(`New status: ${updatedProposal.status}`) + .addFields( + { name: 'ID', value: updatedProposal.id.toString(), inline: true }, + { name: 'URL', value: updatedProposal.uri, inline: true }, + { name: 'Budget', value: updatedProposal.budget.toString(), inline: true }, + { name: 'Proposer', value: updatedProposal.proposerDuid, inline: true }, + ); - } + const selectPropAction: SelectProposalAction = (this.screen as ManageProposalStatusesScreen).selectProposalAction; - public allSubActions(): Action[] { - return []; + if (!updatedProposal.fundingRoundId) { + await DiscordStatus.Warning.warning(interaction, `Proposal does not have a Funding Round associated`); + throw new EndUserError(`Proposal ${updatedProposal.id} does not have a Funding Round associated`); } - getComponent(): ButtonBuilder { - return new ButtonBuilder() - .setCustomId(CustomIDOracle.addArgumentsToAction(this, 'showStatusOptions')) - .setLabel('Update Proposal Status') - .setStyle(ButtonStyle.Primary); - } -} \ No newline at end of file + const backButton = new ButtonBuilder() + .setCustomId( + CustomIDOracle.addArgumentsToAction( + selectPropAction, + SelectProposalAction.OPERATIONS.showProposals, + 'frId', + updatedProposal.fundingRoundId.toString(), + ), + ) + .setLabel('Update Status Again') + .setStyle(ButtonStyle.Primary); + + const row = new ActionRowBuilder().addComponents(backButton); + + await interaction.update({ embeds: [embed], components: [row] }); + } + + public allSubActions(): Action[] { + return []; + } + + getComponent(): ButtonBuilder { + return new ButtonBuilder() + .setCustomId(CustomIDOracle.addArgumentsToAction(this, 'showStatusOptions')) + .setLabel('Update Proposal Status') + .setStyle(ButtonStyle.Primary); + } +} diff --git a/src/channels/propose/screens/ProposalHomeScreen.ts b/src/channels/propose/screens/ProposalHomeScreen.ts index 355ef55..41a4ab6 100644 --- a/src/channels/propose/screens/ProposalHomeScreen.ts +++ b/src/channels/propose/screens/ProposalHomeScreen.ts @@ -1,5 +1,17 @@ import { Screen, Action, Dashboard, Permission, TrackedInteraction, RenderArgs } from '../../../core/BaseClasses'; -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, MessageActionRowComponentBuilder, MessageCreateOptions, ModalBuilder, StringSelectMenuBuilder, TextChannel, TextInputBuilder, TextInputStyle } from 'discord.js'; +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + EmbedBuilder, + MessageActionRowComponentBuilder, + MessageCreateOptions, + ModalBuilder, + StringSelectMenuBuilder, + TextChannel, + TextInputBuilder, + TextInputStyle, +} from 'discord.js'; import { ArgumentOracle, CustomIDOracle } from '../../../CustomIDOracle'; import { AnyInteractionWithShowModal, AnyInteractionWithValues, IHomeScreen } from '../../../types/common'; import { ProposalLogic } from '../../../logic/ProposalLogic'; @@ -14,963 +26,1020 @@ import logger from '../../../logging'; import { EditMySubmittedProposalsPaginator } from '../../../components/ProposalsPaginator'; export class ProposalHomeScreen extends Screen implements IHomeScreen { - - public static readonly ID = 'proposalHome'; - - protected permissions: Permission[] = []; // Add appropriate permissions if needed - - public readonly manageSubmittedProposalsAction: ManageSubmittedProposalsAction; - public readonly manageDraftsAction: ManageDraftsAction; - public readonly createNewProposalAction: CreateNewProposalAction; - public readonly submitProposalToFundingRoundAction: SubmitProposalToFundingRoundAction; - - constructor(dashboard: Dashboard, screenId: string) { - super(dashboard, screenId); - this.manageSubmittedProposalsAction = new ManageSubmittedProposalsAction(this, ManageSubmittedProposalsAction.ID); - this.manageDraftsAction = new ManageDraftsAction(this, ManageDraftsAction.ID); - this.createNewProposalAction = new CreateNewProposalAction(this, CreateNewProposalAction.ID); - this.submitProposalToFundingRoundAction = new SubmitProposalToFundingRoundAction(this, SubmitProposalToFundingRoundAction.ID); + public static readonly ID = 'proposalHome'; + + protected permissions: Permission[] = []; // Add appropriate permissions if needed + + public readonly manageSubmittedProposalsAction: ManageSubmittedProposalsAction; + public readonly manageDraftsAction: ManageDraftsAction; + public readonly createNewProposalAction: CreateNewProposalAction; + public readonly submitProposalToFundingRoundAction: SubmitProposalToFundingRoundAction; + + constructor(dashboard: Dashboard, screenId: string) { + super(dashboard, screenId); + this.manageSubmittedProposalsAction = new ManageSubmittedProposalsAction(this, ManageSubmittedProposalsAction.ID); + this.manageDraftsAction = new ManageDraftsAction(this, ManageDraftsAction.ID); + this.createNewProposalAction = new CreateNewProposalAction(this, CreateNewProposalAction.ID); + this.submitProposalToFundingRoundAction = new SubmitProposalToFundingRoundAction(this, SubmitProposalToFundingRoundAction.ID); + } + + public async renderToTextChannel(channel: TextChannel): Promise { + const content: MessageCreateOptions = await this.getResponse(); + await channel.send(content); + } + + protected allSubScreens(): Screen[] { + return []; + } + + protected allActions(): Action[] { + return [this.manageSubmittedProposalsAction, this.manageDraftsAction, this.createNewProposalAction, this.submitProposalToFundingRoundAction]; + } + + protected async getResponse(interaction?: TrackedInteraction, args?: RenderArgs): Promise { + const embed = new EmbedBuilder() + .setColor('#0099ff') + .setTitle('Proposal Management') + .setDescription('Welcome to the Proposal Management channel. Here you can create, manage, and submit proposals for funding rounds.'); + + const manageSubmittedButton = this.manageSubmittedProposalsAction.getComponent(); + const manageDraftsButton = this.manageDraftsAction.getComponent(); + const createNewButton = this.createNewProposalAction.getComponent(); + const submitToFundingRoundButton = this.submitProposalToFundingRoundAction.getComponent(); + + const row = new ActionRowBuilder().addComponents( + manageSubmittedButton, + manageDraftsButton, + createNewButton, + submitToFundingRoundButton, + ); + + const components = [row]; + + if (args?.successMessage) { + const successEmbed = new EmbedBuilder().setColor('#28a745').setDescription(args.successMessage); + return { + embeds: [embed, successEmbed], + components, + ephemeral: true, + }; + } else if (args?.errorMessage) { + const errorEmbed = new EmbedBuilder().setColor('#dc3545').setDescription(args.errorMessage); + return { + embeds: [embed, errorEmbed], + components, + ephemeral: true, + }; } - public async renderToTextChannel(channel: TextChannel): Promise { - const content: MessageCreateOptions = await this.getResponse(); - await channel.send(content); + return { + embeds: [embed], + components, + ephemeral: true, + }; + } +} +export class ManageSubmittedProposalsAction extends PaginationComponent { + public static readonly ID: string = 'manageSubmittedProposals'; + + public editSubmittedProposalsPaginator = new EditMySubmittedProposalsPaginator( + this.screen, + this, + ManageSubmittedProposalsAction.OPERATIONS.SHOW_PROPOSAL_DETAILS, + EditMySubmittedProposalsPaginator.ID, + ); + public static readonly OPERATIONS = { + SHOW_FUNDING_ROUNDS: 'showFundingRounds', + SELECT_FUNDING_ROUND: 'selectFundingRound', + SHOW_PROPOSALS: 'showProposals', + SHOW_PROPOSAL_DETAILS: 'showProposalDetails', + CONFIRM_CANCEL_PROPOSAL: 'confirmCancelProposal', + EXECUTE_CANCEL_PROPOSAL: 'executeCancelProposal', + }; + + protected async getTotalPages(interaction: TrackedInteraction): Promise { + const fundingRounds: FundingRound[] = await FundingRoundLogic.getFundingRoundsWithUserProposals(interaction.interaction.user.id); + return Math.ceil(fundingRounds.length / 25); + } + + protected async getItemsForPage(interaction: TrackedInteraction, page: number): Promise { + const fundingRounds: FundingRound[] = await FundingRoundLogic.getFundingRoundsWithUserProposals(interaction.interaction.user.id); + return fundingRounds.slice(page * 25, (page + 1) * 25); + } + + protected async handleOperation(interaction: TrackedInteraction, operationId: string): Promise { + switch (operationId) { + case ManageSubmittedProposalsAction.OPERATIONS.SHOW_FUNDING_ROUNDS: + await this.handleShowFundingRounds(interaction); + break; + case ManageSubmittedProposalsAction.OPERATIONS.SELECT_FUNDING_ROUND: + await this.handleSelectFundingRound(interaction); + break; + case ManageSubmittedProposalsAction.OPERATIONS.SHOW_PROPOSALS: + await this.handleShowProposals(interaction); + break; + case ManageSubmittedProposalsAction.OPERATIONS.SHOW_PROPOSAL_DETAILS: + await this.handleShowProposalDetails(interaction); + break; + case ManageSubmittedProposalsAction.OPERATIONS.CONFIRM_CANCEL_PROPOSAL: + await this.handleConfirmCancelProposal(interaction); + break; + case ManageSubmittedProposalsAction.OPERATIONS.EXECUTE_CANCEL_PROPOSAL: + await this.handleExecuteCancelProposal(interaction); + break; + default: + await this.handleInvalidOperation(interaction, operationId); } - - protected allSubScreens(): Screen[] { - return []; + } + + private async handleShowFundingRounds(interaction: TrackedInteraction): Promise { + const currentPage: number = this.getCurrentPage(interaction); + const totalPages: number = await this.getTotalPages(interaction); + const fundingRounds: FundingRound[] = await this.getItemsForPage(interaction, currentPage); + + const embed = new EmbedBuilder() + .setColor('#0099ff') + .setTitle('Select A Funding Round') + .setDescription('To proceed, please select a funding round from the list below.'); + + const selectMenu: StringSelectMenuBuilder = new StringSelectMenuBuilder() + .setCustomId(CustomIDOracle.addArgumentsToAction(this, ManageSubmittedProposalsAction.OPERATIONS.SELECT_FUNDING_ROUND)) + .setPlaceholder('Select a Funding Round') + .addOptions( + fundingRounds.map((fr: FundingRound) => ({ + label: `${fr.name}${fr.endAt < new Date() ? ' (Ended)' : ''}`, + value: fr.id.toString(), + description: `Budget: ${fr.budget}, Status: ${fr.status}`, + })), + ); + + const row: ActionRowBuilder = new ActionRowBuilder().addComponents(selectMenu); + const components: ActionRowBuilder[] = [row]; + + if (totalPages > 1) { + const paginationRow: ActionRowBuilder = this.getPaginationRow(interaction, currentPage, totalPages); + components.push(paginationRow); } - protected allActions(): Action[] { - return [ - this.manageSubmittedProposalsAction, - this.manageDraftsAction, - this.createNewProposalAction, - this.submitProposalToFundingRoundAction, - ]; + await interaction.respond({ embeds: [embed], components }); + } + + private async handleSelectFundingRound(interaction: TrackedInteraction): Promise { + const interactionWithValues: AnyInteractionWithValues | undefined = InteractionProperties.toInteractionWithValuesOrUndefined( + interaction.interaction, + ); + if (!interactionWithValues) { + throw new EndUserError('Invalid interaction type.'); } - protected async getResponse(interaction?: TrackedInteraction, args?: RenderArgs): Promise { - const embed = new EmbedBuilder() - .setColor('#0099ff') - .setTitle('Proposal Management') - .setDescription('Welcome to the Proposal Management channel. Here you can create, manage, and submit proposals for funding rounds.'); + const fundingRoundId: number = parseInt(interactionWithValues.values[0]); + interaction.Context.set(ArgumentOracle.COMMON_ARGS.FUNDING_ROUND_ID, fundingRoundId.toString()); + await this.handleShowProposals(interaction); + } - const manageSubmittedButton = this.manageSubmittedProposalsAction.getComponent(); - const manageDraftsButton = this.manageDraftsAction.getComponent(); - const createNewButton = this.createNewProposalAction.getComponent(); - const submitToFundingRoundButton = this.submitProposalToFundingRoundAction.getComponent(); + private async handleShowProposals(interaction: TrackedInteraction): Promise { + await this.editSubmittedProposalsPaginator.handlePagination(interaction); + } - const row = new ActionRowBuilder() - .addComponents(manageSubmittedButton, manageDraftsButton, createNewButton, submitToFundingRoundButton); + private async handleShowProposalDetails(interaction: TrackedInteraction): Promise { + const proposalId: number = parseInt(ArgumentOracle.getNamedArgument(interaction, 'prId', 0)); + const fundingRoundId: number = parseInt(ArgumentOracle.getNamedArgument(interaction, 'frId')); - const components = [row]; + const proposal: Proposal | null = await ProposalLogic.getProposalById(proposalId); + const fundingRound: FundingRound | null = await FundingRoundLogic.getFundingRoundById(fundingRoundId); - if (args?.successMessage) { - const successEmbed = new EmbedBuilder() - .setColor('#28a745') - .setDescription(args.successMessage); - return { - embeds: [embed, successEmbed], - components, - ephemeral: true - }; - } else if (args?.errorMessage) { - const errorEmbed = new EmbedBuilder() - .setColor('#dc3545') - .setDescription(args.errorMessage); - return { - embeds: [embed, errorEmbed], - components, - ephemeral: true - }; - } + if (!proposal || !fundingRound) { + throw new EndUserError('Proposal or Funding Round not found.'); + } - return { - embeds: [embed], - components, - ephemeral: true - }; + const embed: EmbedBuilder = new EmbedBuilder() + .setColor('#0099ff') + .setTitle(`Proposal: ${proposal.name}`) + .addFields( + { name: 'Proposal Details', value: `Budget: ${proposal.budget}\nStatus: ${proposal.status}\nURI: ${proposal.uri}` }, + { name: 'Funding Round', value: `Name: ${fundingRound.name}\nBudget: ${fundingRound.budget}\nStatus: ${fundingRound.status}` }, + ); + + const cancelButton: ButtonBuilder = new ButtonBuilder() + .setCustomId( + CustomIDOracle.addArgumentsToAction( + this, + ManageSubmittedProposalsAction.OPERATIONS.CONFIRM_CANCEL_PROPOSAL, + 'prId', + proposalId.toString(), + 'frId', + fundingRoundId.toString(), + ), + ) + .setLabel('Cancel My Proposal') + .setStyle(ButtonStyle.Danger); + + if (proposal.status === ProposalStatus.CANCELLED) { + cancelButton.setDisabled(true); } -} -export class ManageSubmittedProposalsAction extends PaginationComponent { - public static readonly ID: string = 'manageSubmittedProposals'; + const row: ActionRowBuilder = new ActionRowBuilder().addComponents(cancelButton); + await interaction.respond({ embeds: [embed], components: [row], ephemeral: true }); + } - public editSubmittedProposalsPaginator = new EditMySubmittedProposalsPaginator(this.screen, this, ManageSubmittedProposalsAction.OPERATIONS.SHOW_PROPOSAL_DETAILS, EditMySubmittedProposalsPaginator.ID) - public static readonly OPERATIONS = { - SHOW_FUNDING_ROUNDS: 'showFundingRounds', - SELECT_FUNDING_ROUND: 'selectFundingRound', - SHOW_PROPOSALS: 'showProposals', - SHOW_PROPOSAL_DETAILS: 'showProposalDetails', - CONFIRM_CANCEL_PROPOSAL: 'confirmCancelProposal', - EXECUTE_CANCEL_PROPOSAL: 'executeCancelProposal', - }; + private async handleConfirmCancelProposal(interaction: TrackedInteraction): Promise { + const proposalId: string | undefined = CustomIDOracle.getNamedArgument(interaction.customId, 'prId'); + const fundingRoundId: string | undefined = CustomIDOracle.getNamedArgument(interaction.customId, 'frId'); + if (!proposalId || !fundingRoundId) { + throw new EndUserError('Invalid proposal or funding round ID.'); + } + const proposal: Proposal | null = await ProposalLogic.getProposalById(parseInt(proposalId)); + const fundingRound: FundingRound | null = await FundingRoundLogic.getFundingRoundById(parseInt(fundingRoundId)); - protected async getTotalPages(interaction: TrackedInteraction): Promise { - const fundingRounds: FundingRound[] = await FundingRoundLogic.getFundingRoundsWithUserProposals(interaction.interaction.user.id); - return Math.ceil(fundingRounds.length / 25); - } - - protected async getItemsForPage(interaction: TrackedInteraction, page: number): Promise { - const fundingRounds: FundingRound[] = await FundingRoundLogic.getFundingRoundsWithUserProposals(interaction.interaction.user.id); - return fundingRounds.slice(page * 25, (page + 1) * 25); - } - - protected async handleOperation(interaction: TrackedInteraction, operationId: string): Promise { - switch (operationId) { - case ManageSubmittedProposalsAction.OPERATIONS.SHOW_FUNDING_ROUNDS: - await this.handleShowFundingRounds(interaction); - break; - case ManageSubmittedProposalsAction.OPERATIONS.SELECT_FUNDING_ROUND: - await this.handleSelectFundingRound(interaction); - break; - case ManageSubmittedProposalsAction.OPERATIONS.SHOW_PROPOSALS: - await this.handleShowProposals(interaction); - break; - case ManageSubmittedProposalsAction.OPERATIONS.SHOW_PROPOSAL_DETAILS: - await this.handleShowProposalDetails(interaction); - break; - case ManageSubmittedProposalsAction.OPERATIONS.CONFIRM_CANCEL_PROPOSAL: - await this.handleConfirmCancelProposal(interaction); - break; - case ManageSubmittedProposalsAction.OPERATIONS.EXECUTE_CANCEL_PROPOSAL: - await this.handleExecuteCancelProposal(interaction); - break; - default: - await this.handleInvalidOperation(interaction, operationId); - } - } - - private async handleShowFundingRounds(interaction: TrackedInteraction): Promise { - const currentPage: number = this.getCurrentPage(interaction); - const totalPages: number = await this.getTotalPages(interaction); - const fundingRounds: FundingRound[] = await this.getItemsForPage(interaction, currentPage); - - const embed = new EmbedBuilder() - .setColor('#0099ff') - .setTitle('Select A Funding Round') - .setDescription('To proceed, please select a funding round from the list below.'); - - const selectMenu: StringSelectMenuBuilder = new StringSelectMenuBuilder() - .setCustomId(CustomIDOracle.addArgumentsToAction(this, ManageSubmittedProposalsAction.OPERATIONS.SELECT_FUNDING_ROUND)) - .setPlaceholder('Select a Funding Round') - .addOptions(fundingRounds.map((fr: FundingRound) => ({ - label: `${fr.name}${fr.endAt < new Date() ? ' (Ended)' : ''}`, - value: fr.id.toString(), - description: `Budget: ${fr.budget}, Status: ${fr.status}` - }))); - - const row: ActionRowBuilder = new ActionRowBuilder().addComponents(selectMenu); - const components: ActionRowBuilder[] = [row]; - - if (totalPages > 1) { - const paginationRow: ActionRowBuilder = this.getPaginationRow(interaction, currentPage, totalPages); - components.push(paginationRow); - } - - await interaction.respond({ embeds: [embed], components }); - } - - private async handleSelectFundingRound(interaction: TrackedInteraction): Promise { - const interactionWithValues: AnyInteractionWithValues | undefined = InteractionProperties.toInteractionWithValuesOrUndefined(interaction.interaction); - if (!interactionWithValues) { - throw new EndUserError('Invalid interaction type.'); - } - - const fundingRoundId: number = parseInt(interactionWithValues.values[0]); - interaction.Context.set(ArgumentOracle.COMMON_ARGS.FUNDING_ROUND_ID, fundingRoundId.toString()); - await this.handleShowProposals(interaction); + if (!proposal || !fundingRound) { + throw new EndUserError('Proposal or Funding Round not found.'); } - private async handleShowProposals(interaction: TrackedInteraction): Promise { - await this.editSubmittedProposalsPaginator.handlePagination(interaction) + const embed: EmbedBuilder = new EmbedBuilder() + .setColor('#FF0000') + .setTitle('Confirm Proposal Cancellation') + .setDescription('Are you sure you want to cancel this proposal? This action cannot be undone.') + .addFields( + { name: 'Proposal Details', value: `Name: ${proposal.name}\nBudget: ${proposal.budget}\nStatus: ${proposal.status}\nURI: ${proposal.uri}` }, + { name: 'Funding Round', value: `Name: ${fundingRound.name}\nBudget: ${fundingRound.budget}\nStatus: ${fundingRound.status}` }, + ) + .setFooter({ + text: 'Once cancelled, this proposal cannot be re-submitted to the funding round. You will need to create a new proposal if you want to submit again.', + }); + + const confirmButton: ButtonBuilder = new ButtonBuilder() + .setCustomId( + CustomIDOracle.addArgumentsToAction( + this, + ManageSubmittedProposalsAction.OPERATIONS.EXECUTE_CANCEL_PROPOSAL, + 'prId', + proposalId, + 'frId', + fundingRoundId, + ), + ) + .setLabel('Confirm Cancellation') + .setStyle(ButtonStyle.Danger); + + const cancelButton: ButtonBuilder = new ButtonBuilder() + .setCustomId( + CustomIDOracle.addArgumentsToAction( + this, + ManageSubmittedProposalsAction.OPERATIONS.SHOW_PROPOSAL_DETAILS, + 'prId', + proposalId, + 'frId', + fundingRoundId, + ), + ) + .setLabel('Go Back') + .setStyle(ButtonStyle.Secondary); + + const row: ActionRowBuilder = new ActionRowBuilder().addComponents(confirmButton, cancelButton); + + await interaction.update({ embeds: [embed], components: [row] }); + } + + private async handleExecuteCancelProposal(interaction: TrackedInteraction): Promise { + const proposalId: string | undefined = CustomIDOracle.getNamedArgument(interaction.customId, 'prId'); + const fundingRoundId: string | undefined = CustomIDOracle.getNamedArgument(interaction.customId, 'frId'); + + if (!proposalId || !fundingRoundId) { + throw new EndUserError('Invalid proposal or funding round ID.'); } - private async handleShowProposalDetails(interaction: TrackedInteraction): Promise { - const interactionWithValues: AnyInteractionWithValues | undefined = InteractionProperties.toInteractionWithValuesOrUndefined(interaction.interaction); - if (!interactionWithValues) { - throw new EndUserError('Invalid interaction type.'); - } - - const proposalId: number = parseInt(interactionWithValues.values[0]); - const fundingRoundId: number = parseInt(CustomIDOracle.getNamedArgument(interaction.customId, 'frId') || ''); - - const proposal: Proposal | null = await ProposalLogic.getProposalById(proposalId); - const fundingRound: FundingRound | null = await FundingRoundLogic.getFundingRoundById(fundingRoundId); - - if (!proposal || !fundingRound) { - throw new EndUserError('Proposal or Funding Round not found.'); - } + try { + await ProposalLogic.cancelProposal(parseInt(proposalId), this.screen); - const embed: EmbedBuilder = new EmbedBuilder() - .setColor('#0099ff') - .setTitle(`Proposal: ${proposal.name}`) - .addFields( - { name: 'Proposal Details', value: `Budget: ${proposal.budget}\nStatus: ${proposal.status}\nURI: ${proposal.uri}` }, - { name: 'Funding Round', value: `Name: ${fundingRound.name}\nBudget: ${fundingRound.budget}\nStatus: ${fundingRound.status}` } - ); + const successEmbed: EmbedBuilder = new EmbedBuilder() + .setColor('#00FF00') + .setTitle('Proposal Cancelled Successfully') + .setDescription('Your proposal has been cancelled and cannot be re-submitted to this funding round.'); - const cancelButton: ButtonBuilder = new ButtonBuilder() - .setCustomId(CustomIDOracle.addArgumentsToAction(this, ManageSubmittedProposalsAction.OPERATIONS.CONFIRM_CANCEL_PROPOSAL, 'proposalId', proposalId.toString(), 'frId', fundingRoundId.toString())) - .setLabel('Cancel My Proposal') - .setStyle(ButtonStyle.Danger); + const backButton: ButtonBuilder = new ButtonBuilder() + .setCustomId(CustomIDOracle.addArgumentsToAction(this, ManageSubmittedProposalsAction.OPERATIONS.SHOW_PROPOSALS, 'frId', fundingRoundId)) + .setLabel('Back to My Proposals') + .setStyle(ButtonStyle.Primary); - if (proposal.status === ProposalStatus.CANCELLED) { - cancelButton.setDisabled(true); - } + const row: ActionRowBuilder = new ActionRowBuilder().addComponents(backButton); - const row: ActionRowBuilder = new ActionRowBuilder().addComponents(cancelButton); - - await interaction.respond({ embeds: [embed], components: [row], ephemeral: true }); + await interaction.update({ embeds: [successEmbed], components: [row] }); + } catch (error) { + throw new EndUserError('Error cancelling proposal', error); } + } + + public allSubActions(): Action[] { + return []; + } + + getComponent(): ButtonBuilder { + return new ButtonBuilder() + .setCustomId(CustomIDOracle.addArgumentsToAction(this, ManageSubmittedProposalsAction.OPERATIONS.SHOW_FUNDING_ROUNDS)) + .setLabel('Manage My Proposals') + .setStyle(ButtonStyle.Primary); + } +} - private async handleConfirmCancelProposal(interaction: TrackedInteraction): Promise { - const proposalId: string | undefined = CustomIDOracle.getNamedArgument(interaction.customId, 'proposalId'); - const fundingRoundId: string | undefined = CustomIDOracle.getNamedArgument(interaction.customId, 'frId'); - - if (!proposalId || !fundingRoundId) { - throw new EndUserError('Invalid proposal or funding round ID.'); - } - - const proposal: Proposal | null = await ProposalLogic.getProposalById(parseInt(proposalId)); - const fundingRound: FundingRound | null = await FundingRoundLogic.getFundingRoundById(parseInt(fundingRoundId)); - - if (!proposal || !fundingRound) { - throw new EndUserError('Proposal or Funding Round not found.'); - } +export class ManageDraftsAction extends PaginationComponent { + public static readonly ID = 'manageDrafts'; + + public static readonly OPERATIONS = { + SHOW_DRAFTS: 'showDrafts', + SELECT_DRAFT: 'selectDraft', + EDIT_DRAFT: 'editDraft', + SUBMIT_EDIT: 'submitEdit', + DELETE_DRAFT: 'deleteDraft', + CONFIRM_DELETE: 'confirmDelete', + SUBMIT_TO_FUNDING_ROUND: 'submitToFundingRound', + }; + + public static readonly INPUT_IDS = { + NAME: 'name', + DESCRIPTION: 'description', + BUDGET: 'budget', + URI: 'uri', + }; + + protected async getTotalPages(interaction: TrackedInteraction): Promise { + const drafts = await ProposalLogic.getUserDraftProposals(interaction.interaction.user.id); + return Math.ceil(drafts.length / 25); + } + + protected async getItemsForPage(interaction: TrackedInteraction, page: number): Promise { + const drafts = await ProposalLogic.getUserDraftProposals(interaction.interaction.user.id); + return drafts.slice(page * 25, (page + 1) * 25); + } + + protected async handleOperation(interaction: TrackedInteraction, operationId: string): Promise { + switch (operationId) { + case ManageDraftsAction.OPERATIONS.SHOW_DRAFTS: + await this.handleShowDrafts(interaction); + break; + case ManageDraftsAction.OPERATIONS.SELECT_DRAFT: + await this.handleSelectDraft(interaction); + break; + case ManageDraftsAction.OPERATIONS.EDIT_DRAFT: + await this.handleEditDraft(interaction); + break; + case ManageDraftsAction.OPERATIONS.SUBMIT_EDIT: + await this.handleSubmitEdit(interaction); + break; + case ManageDraftsAction.OPERATIONS.DELETE_DRAFT: + await this.handleDeleteDraft(interaction); + break; + case ManageDraftsAction.OPERATIONS.CONFIRM_DELETE: + await this.handleConfirmDelete(interaction); + break; + case ManageDraftsAction.OPERATIONS.SUBMIT_TO_FUNDING_ROUND: + await this.handleSubmitToFundingRound(interaction); + break; + default: + await this.handleInvalidOperation(interaction, operationId); + } + } - const embed: EmbedBuilder = new EmbedBuilder() - .setColor('#FF0000') - .setTitle('Confirm Proposal Cancellation') - .setDescription('Are you sure you want to cancel this proposal? This action cannot be undone.') - .addFields( - { name: 'Proposal Details', value: `Name: ${proposal.name}\nBudget: ${proposal.budget}\nStatus: ${proposal.status}\nURI: ${proposal.uri}` }, - { name: 'Funding Round', value: `Name: ${fundingRound.name}\nBudget: ${fundingRound.budget}\nStatus: ${fundingRound.status}` } - ) - .setFooter({ text: 'Once cancelled, this proposal cannot be re-submitted to the funding round. You will need to create a new proposal if you want to submit again.' }); + private async handleShowDrafts(interaction: TrackedInteraction): Promise { + const currentPage = this.getCurrentPage(interaction); + const totalPages = await this.getTotalPages(interaction); + const drafts: Proposal[] = await this.getItemsForPage(interaction, currentPage); - const confirmButton: ButtonBuilder = new ButtonBuilder() - .setCustomId(CustomIDOracle.addArgumentsToAction(this, ManageSubmittedProposalsAction.OPERATIONS.EXECUTE_CANCEL_PROPOSAL, 'proposalId', proposalId, 'frId', fundingRoundId)) - .setLabel('Confirm Cancellation') - .setStyle(ButtonStyle.Danger); + if (drafts.length === 0) { + throw new EndUserInfo('You have no draft proposals.'); + } - const cancelButton: ButtonBuilder = new ButtonBuilder() - .setCustomId(CustomIDOracle.addArgumentsToAction(this, ManageSubmittedProposalsAction.OPERATIONS.SHOW_PROPOSAL_DETAILS, 'proposalId', proposalId, 'frId', fundingRoundId)) - .setLabel('Go Back') - .setStyle(ButtonStyle.Secondary); + const embed = new EmbedBuilder() + .setColor('#0099ff') + .setTitle('Select a Draft Proposal') + .setDescription('To proceed, please select a draft proposal from the list below.'); + + const selectMenu = new StringSelectMenuBuilder() + .setCustomId(CustomIDOracle.addArgumentsToAction(this, ManageDraftsAction.OPERATIONS.SELECT_DRAFT)) + .setPlaceholder('Select a Draft Proposal') + .addOptions( + drafts.map((draft) => ({ + label: draft.name, + value: draft.id.toString(), + description: `Budget: ${draft.budget}`, + })), + ); + + const row = new ActionRowBuilder().addComponents(selectMenu); + const components = [row]; + + if (totalPages > 1) { + const paginationRow = this.getPaginationRow(interaction, currentPage, totalPages); + components.push(paginationRow); + } - const row: ActionRowBuilder = new ActionRowBuilder().addComponents(confirmButton, cancelButton); + const asUpdate = CustomIDOracle.getNamedArgument(interaction.customId, 'udt') === '1'; - await interaction.update({ embeds: [embed], components: [row] }); + if (asUpdate) { + await interaction.update({ embeds: [embed], components }); + } else { + await interaction.respond({ embeds: [embed], components }); + } + } + + private async handleSelectDraft(interaction: TrackedInteraction, asUpdate: boolean = false, successMessage?: string): Promise { + let proposalId: number; + + const interactionWithValues: AnyInteractionWithValues | undefined = InteractionProperties.toInteractionWithValuesOrUndefined( + interaction.interaction, + ); + if (interactionWithValues) { + proposalId = parseInt(interactionWithValues.values[0]); + } else { + const proposalIdFromCustomId: string | undefined = CustomIDOracle.getNamedArgument(interaction.customId, 'prId'); + if (!proposalIdFromCustomId) { + throw new EndUserError('Interaction neither has proposalId in values, nor in customId.'); + } else { + proposalId = parseInt(proposalIdFromCustomId); + } } - private async handleExecuteCancelProposal(interaction: TrackedInteraction): Promise { - const proposalId: string | undefined = CustomIDOracle.getNamedArgument(interaction.customId, 'proposalId'); - const fundingRoundId: string | undefined = CustomIDOracle.getNamedArgument(interaction.customId, 'frId'); + const proposal: Proposal | null = await ProposalLogic.getProposalById(proposalId); - if (!proposalId || !fundingRoundId) { - throw new EndUserError('Invalid proposal or funding round ID.'); - } + if (!proposal) { + throw new EndUserError('Proposal not found.'); + } - try { - await ProposalLogic.cancelProposal(parseInt(proposalId), this.screen); + let successEmbed: EmbedBuilder | undefined; + if (successMessage) { + successEmbed = new EmbedBuilder().setColor('#28a745').setDescription(successMessage); + } - const successEmbed: EmbedBuilder = new EmbedBuilder() - .setColor('#00FF00') - .setTitle('Proposal Cancelled Successfully') - .setDescription('Your proposal has been cancelled and cannot be re-submitted to this funding round.'); + const embed: EmbedBuilder = new EmbedBuilder() + .setColor('#0099ff') + .setTitle(`Draft Proposal: ${proposal.name}`) + .setDescription(`Proposal URI: ${proposal.uri}`) + .addFields( + { name: 'ID (auto-assigned)', value: proposal.id.toString(), inline: true }, + { name: 'Budget', value: proposal.budget.toString(), inline: true }, + { name: 'URL', value: proposal.uri, inline: true }, + { name: 'Status', value: proposal.status, inline: true }, + ); + + const editButton = new ButtonBuilder() + .setCustomId(CustomIDOracle.addArgumentsToAction(this, ManageDraftsAction.OPERATIONS.EDIT_DRAFT, 'prId', proposalId.toString())) + .setLabel('Edit Proposal') + .setStyle(ButtonStyle.Primary); + + const deleteButton = new ButtonBuilder() + .setCustomId(CustomIDOracle.addArgumentsToAction(this, ManageDraftsAction.OPERATIONS.DELETE_DRAFT, 'prId', proposalId.toString())) + .setLabel('Delete Proposal') + .setStyle(ButtonStyle.Danger); + + const submitButton = new ButtonBuilder() + .setCustomId(CustomIDOracle.addArgumentsToAction(this, ManageDraftsAction.OPERATIONS.SUBMIT_TO_FUNDING_ROUND, 'prId', proposalId.toString())) + .setLabel('Submit to Funding Round') + .setStyle(ButtonStyle.Success); + + const row = new ActionRowBuilder().addComponents(editButton, deleteButton, submitButton); + + let embeds: EmbedBuilder[]; + if (successEmbed) { + embeds = [successEmbed, embed]; + } else { + embeds = [embed]; + } - const backButton: ButtonBuilder = new ButtonBuilder() - .setCustomId(CustomIDOracle.addArgumentsToAction(this, ManageSubmittedProposalsAction.OPERATIONS.SHOW_PROPOSALS, 'frId', fundingRoundId)) - .setLabel('Back to My Proposals') - .setStyle(ButtonStyle.Primary); + const asUpdateFromCustomId = CustomIDOracle.getNamedArgument(interaction.customId, 'udt'); - const row: ActionRowBuilder = new ActionRowBuilder().addComponents(backButton); + let doUpdate: boolean = asUpdate; - await interaction.update({ embeds: [successEmbed], components: [row] }); - } catch (error) { - throw new EndUserError('Error cancelling proposal', error); - } + if (asUpdateFromCustomId) { + if (asUpdateFromCustomId === '1') { + doUpdate = true; + } } - public allSubActions(): Action[] { - return []; + if (doUpdate) { + await interaction.update({ embeds, components: [row] }); + } else { + await interaction.respond({ embeds, components: [row] }); } + } - getComponent(): ButtonBuilder { - return new ButtonBuilder() - .setCustomId(CustomIDOracle.addArgumentsToAction(this, ManageSubmittedProposalsAction.OPERATIONS.SHOW_FUNDING_ROUNDS)) - .setLabel('Manage My Proposals') - .setStyle(ButtonStyle.Primary); + private async handleEditDraft(interaction: TrackedInteraction): Promise { + const proposalId: string | undefined = CustomIDOracle.getNamedArgument(interaction.customId, 'prId'); + if (!proposalId) { + throw new EndUserError('Invalid proposal ID.'); } -} - - -export class ManageDraftsAction extends PaginationComponent { - public static readonly ID = 'manageDrafts'; - - public static readonly OPERATIONS = { - SHOW_DRAFTS: 'showDrafts', - SELECT_DRAFT: 'selectDraft', - EDIT_DRAFT: 'editDraft', - SUBMIT_EDIT: 'submitEdit', - DELETE_DRAFT: 'deleteDraft', - CONFIRM_DELETE: 'confirmDelete', - SUBMIT_TO_FUNDING_ROUND: 'submitToFundingRound', - }; - public static readonly INPUT_IDS = { - NAME: 'name', - DESCRIPTION: 'description', - BUDGET: 'budget', - URI: 'uri', - }; - - protected async getTotalPages(interaction: TrackedInteraction): Promise { - const drafts = await ProposalLogic.getUserDraftProposals(interaction.interaction.user.id); - return Math.ceil(drafts.length / 25); - } - - protected async getItemsForPage(interaction: TrackedInteraction, page: number): Promise { - const drafts = await ProposalLogic.getUserDraftProposals(interaction.interaction.user.id); - return drafts.slice(page * 25, (page + 1) * 25); - } - - protected async handleOperation(interaction: TrackedInteraction, operationId: string): Promise { - switch (operationId) { - case ManageDraftsAction.OPERATIONS.SHOW_DRAFTS: - await this.handleShowDrafts(interaction); - break; - case ManageDraftsAction.OPERATIONS.SELECT_DRAFT: - await this.handleSelectDraft(interaction); - break; - case ManageDraftsAction.OPERATIONS.EDIT_DRAFT: - await this.handleEditDraft(interaction); - break; - case ManageDraftsAction.OPERATIONS.SUBMIT_EDIT: - await this.handleSubmitEdit(interaction); - break; - case ManageDraftsAction.OPERATIONS.DELETE_DRAFT: - await this.handleDeleteDraft(interaction); - break; - case ManageDraftsAction.OPERATIONS.CONFIRM_DELETE: - await this.handleConfirmDelete(interaction); - break; - case ManageDraftsAction.OPERATIONS.SUBMIT_TO_FUNDING_ROUND: - await this.handleSubmitToFundingRound(interaction); - break; - default: - await this.handleInvalidOperation(interaction, operationId); - } - } - - private async handleShowDrafts(interaction: TrackedInteraction): Promise { - const currentPage = this.getCurrentPage(interaction); - const totalPages = await this.getTotalPages(interaction); - const drafts: Proposal[] = await this.getItemsForPage(interaction, currentPage); - - if (drafts.length === 0) { - throw new EndUserInfo('You have no draft proposals.'); - } - - const embed = new EmbedBuilder() - .setColor('#0099ff') - .setTitle('Select a Draft Proposal') - .setDescription('To proceed, please select a draft proposal from the list below.'); - - const selectMenu = new StringSelectMenuBuilder() - .setCustomId(CustomIDOracle.addArgumentsToAction(this, ManageDraftsAction.OPERATIONS.SELECT_DRAFT)) - .setPlaceholder('Select a Draft Proposal') - .addOptions(drafts.map(draft => ({ - label: draft.name, - value: draft.id.toString(), - description: `Budget: ${draft.budget}` - }))); - - const row = new ActionRowBuilder().addComponents(selectMenu); - const components = [row]; - - if (totalPages > 1) { - const paginationRow = this.getPaginationRow(interaction, currentPage, totalPages); - components.push(paginationRow); - } - - const asUpdate = CustomIDOracle.getNamedArgument(interaction.customId, 'udt') === '1'; - - if (asUpdate) { - await interaction.update({ embeds: [embed], components }); - } else { - await interaction.respond({ embeds: [embed], components }); - } - - - } - - private async handleSelectDraft(interaction: TrackedInteraction, asUpdate: boolean = false, successMessage?: string): Promise { - let proposalId: number; - - const interactionWithValues: AnyInteractionWithValues | undefined = InteractionProperties.toInteractionWithValuesOrUndefined(interaction.interaction); - if (interactionWithValues) { - proposalId = parseInt(interactionWithValues.values[0]); - } else { - const proposalIdFromCustomId: string | undefined = CustomIDOracle.getNamedArgument(interaction.customId, 'proposalId'); - if (!proposalIdFromCustomId) { - throw new EndUserError('Interaction neither has proposalId in values, nor in customId.'); - } else { - proposalId = parseInt(proposalIdFromCustomId); - } - } - - const proposal: Proposal | null = await ProposalLogic.getProposalById(proposalId); - - if (!proposal) { - throw new EndUserError('Proposal not found.'); - } - - let successEmbed: EmbedBuilder | undefined; - if (successMessage) { - successEmbed = new EmbedBuilder() - .setColor('#28a745') - .setDescription(successMessage); - } - - const embed: EmbedBuilder = new EmbedBuilder() - .setColor('#0099ff') - .setTitle(`Draft Proposal: ${proposal.name}`) - .setDescription(`Proposal URI: ${proposal.uri}`) - .addFields( - { name: 'ID (auto-assigned)', value: proposal.id.toString(), inline: true }, - { name: 'Budget', value: proposal.budget.toString(), inline: true }, - { name: 'URL', value: proposal.uri, inline: true }, - { name: 'Status', value: proposal.status, inline: true } - ); - - - const editButton = new ButtonBuilder() - .setCustomId(CustomIDOracle.addArgumentsToAction(this, ManageDraftsAction.OPERATIONS.EDIT_DRAFT, 'proposalId', proposalId.toString())) - .setLabel('Edit Proposal') - .setStyle(ButtonStyle.Primary); - - const deleteButton = new ButtonBuilder() - .setCustomId(CustomIDOracle.addArgumentsToAction(this, ManageDraftsAction.OPERATIONS.DELETE_DRAFT, 'proposalId', proposalId.toString())) - .setLabel('Delete Proposal') - .setStyle(ButtonStyle.Danger); - - const submitButton = new ButtonBuilder() - .setCustomId(CustomIDOracle.addArgumentsToAction(this, ManageDraftsAction.OPERATIONS.SUBMIT_TO_FUNDING_ROUND, 'proposalId', proposalId.toString())) - .setLabel('Submit to Funding Round') - .setStyle(ButtonStyle.Success); - - const row = new ActionRowBuilder().addComponents(editButton, deleteButton, submitButton); - - let embeds: EmbedBuilder[]; - if (successEmbed) { - embeds = [successEmbed, embed]; - } else { - embeds = [embed]; - } - - const asUpdateFromCustomId = CustomIDOracle.getNamedArgument(interaction.customId, 'udt'); - - let doUpdate: boolean = asUpdate; - - if (asUpdateFromCustomId) { - if (asUpdateFromCustomId === '1') { - doUpdate = true; - } - } - - if (doUpdate) { - await interaction.update({ embeds, components: [row] }); - } else { - await interaction.respond({ embeds, components: [row] }); - } - } - - private async handleEditDraft(interaction: TrackedInteraction): Promise { - const proposalId: string | undefined = CustomIDOracle.getNamedArgument(interaction.customId, 'proposalId'); - if (!proposalId) { - throw new EndUserError('Invalid proposal ID.'); - } - - const proposal: Proposal | null = await ProposalLogic.getProposalById(parseInt(proposalId)); - if (!proposal) { - throw new EndUserError('Proposal not found.'); - } - - const modalInteraction: AnyInteractionWithShowModal | undefined = InteractionProperties.toShowModalOrUndefined(interaction.interaction); - if (!modalInteraction) { - throw new EndUserError('This interaction does not support modals.'); - } - - const modal: ModalBuilder = new ModalBuilder() - .setCustomId(CustomIDOracle.addArgumentsToAction(this, ManageDraftsAction.OPERATIONS.SUBMIT_EDIT, 'proposalId', proposalId)) - .setTitle('Edit Proposal'); - - const nameInput = new TextInputBuilder() - .setCustomId(ManageDraftsAction.INPUT_IDS.NAME) - .setLabel('Proposal Name') - .setStyle(TextInputStyle.Short) - .setValue(proposal.name) - .setRequired(true); - - const budgetInput = new TextInputBuilder() - .setCustomId(ManageDraftsAction.INPUT_IDS.BUDGET) - .setLabel('Budget') - .setStyle(TextInputStyle.Short) - .setValue(proposal.budget.toString()) - .setRequired(true); - - const uriInput = new TextInputBuilder() - .setCustomId(ManageDraftsAction.INPUT_IDS.URI) - .setLabel('URI (https://forums.minaprotocol.com)') - .setStyle(TextInputStyle.Short) - .setValue(proposal.uri) - .setRequired(true); - - modal.addComponents( - new ActionRowBuilder().addComponents(nameInput), - new ActionRowBuilder().addComponents(budgetInput), - new ActionRowBuilder().addComponents(uriInput) - ); - - await modalInteraction.showModal(modal); + const proposal: Proposal | null = await ProposalLogic.getProposalById(parseInt(proposalId)); + if (!proposal) { + throw new EndUserError('Proposal not found.'); } - private async handleSubmitEdit(interaction: TrackedInteraction): Promise { - const modalInteraction = InteractionProperties.toModalSubmitInteractionOrUndefined(interaction.interaction); - if (!modalInteraction) { - throw new EndUserError('Invalid interaction type.'); - } - - const proposalId = CustomIDOracle.getNamedArgument(interaction.customId, 'proposalId'); - if (!proposalId) { - throw new EndUserError('Invalid proposal ID.'); - } - - const name = modalInteraction.fields.getTextInputValue(ManageDraftsAction.INPUT_IDS.NAME); - const budget = parseFloat(modalInteraction.fields.getTextInputValue(ManageDraftsAction.INPUT_IDS.BUDGET)); - const uri = modalInteraction.fields.getTextInputValue(ManageDraftsAction.INPUT_IDS.URI); + const modalInteraction: AnyInteractionWithShowModal | undefined = InteractionProperties.toShowModalOrUndefined(interaction.interaction); + if (!modalInteraction) { + throw new EndUserError('This interaction does not support modals.'); + } - if (isNaN(budget)) { - throw new EndUserError('Invalid budget value.'); - } + const modal: ModalBuilder = new ModalBuilder() + .setCustomId(CustomIDOracle.addArgumentsToAction(this, ManageDraftsAction.OPERATIONS.SUBMIT_EDIT, 'prId', proposalId)) + .setTitle('Edit Proposal'); + + const nameInput = new TextInputBuilder() + .setCustomId(ManageDraftsAction.INPUT_IDS.NAME) + .setLabel('Proposal Name') + .setStyle(TextInputStyle.Short) + .setValue(proposal.name) + .setRequired(true); + + const budgetInput = new TextInputBuilder() + .setCustomId(ManageDraftsAction.INPUT_IDS.BUDGET) + .setLabel('Budget') + .setStyle(TextInputStyle.Short) + .setValue(proposal.budget.toString()) + .setRequired(true); + + const uriInput = new TextInputBuilder() + .setCustomId(ManageDraftsAction.INPUT_IDS.URI) + .setLabel('URI (https://forums.minaprotocol.com)') + .setStyle(TextInputStyle.Short) + .setValue(proposal.uri) + .setRequired(true); + + modal.addComponents( + new ActionRowBuilder().addComponents(nameInput), + new ActionRowBuilder().addComponents(budgetInput), + new ActionRowBuilder().addComponents(uriInput), + ); + + await modalInteraction.showModal(modal); + } + + private async handleSubmitEdit(interaction: TrackedInteraction): Promise { + const modalInteraction = InteractionProperties.toModalSubmitInteractionOrUndefined(interaction.interaction); + if (!modalInteraction) { + throw new EndUserError('Invalid interaction type.'); + } - if (!uri.startsWith('https://forums.minaprotocol.com')) { - throw new EndUserError('Invalid URI. Please use a link from https://forums.minaprotocol.com'); - } + const proposalId = CustomIDOracle.getNamedArgument(interaction.customId, 'prId'); + if (!proposalId) { + throw new EndUserError('Invalid proposal ID.'); + } - try { + const name = modalInteraction.fields.getTextInputValue(ManageDraftsAction.INPUT_IDS.NAME); + const budget = parseFloat(modalInteraction.fields.getTextInputValue(ManageDraftsAction.INPUT_IDS.BUDGET)); + const uri = modalInteraction.fields.getTextInputValue(ManageDraftsAction.INPUT_IDS.URI); - await ProposalLogic.updateProposal(parseInt(proposalId), { name, budget, uri }, this._screen); - const successMessage = `✅ '${name}' details updated successfully`; - await this.handleSelectDraft(interaction, true, successMessage); - } catch (error) { - throw new EndUserError('Error updating proposal', error); - } + if (isNaN(budget)) { + throw new EndUserError('Invalid budget value.'); } - private async handleDeleteDraft(interaction: TrackedInteraction): Promise { - const proposalId = CustomIDOracle.getNamedArgument(interaction.customId, 'proposalId'); - if (!proposalId) { - throw new EndUserError('Invalid proposal ID.'); - } + if (!uri.startsWith('https://forums.minaprotocol.com')) { + throw new EndUserError('Invalid URI. Please use a link from https://forums.minaprotocol.com'); + } - const proposal = await ProposalLogic.getProposalById(parseInt(proposalId)); - if (!proposal) { - throw new EndUserError('Proposal not found.'); - } + try { + await ProposalLogic.updateProposal(parseInt(proposalId), { name, budget, uri }, this._screen); + const successMessage = `✅ '${name}' details updated successfully`; + await this.handleSelectDraft(interaction, true, successMessage); + } catch (error) { + throw new EndUserError('Error updating proposal', error); + } + } - const embed = new EmbedBuilder() - .setColor('#ff0000') - .setTitle(`Confirm Delete: ${proposal.name}`) - .setDescription('Are you sure you want to delete this proposal? This action cannot be undone.') - .addFields( - { name: 'Name', value: proposal.name }, - { name: 'ID', value: proposal.id.toString() }, - { name: 'Budget', value: proposal.budget.toString() }, - { name: 'URL', value: proposal.uri } - ); + private async handleDeleteDraft(interaction: TrackedInteraction): Promise { + const proposalId = CustomIDOracle.getNamedArgument(interaction.customId, 'prId'); + if (!proposalId) { + throw new EndUserError('Invalid proposal ID.'); + } - const confirmButton = new ButtonBuilder() - .setCustomId(CustomIDOracle.addArgumentsToAction(this, ManageDraftsAction.OPERATIONS.CONFIRM_DELETE, 'proposalId', proposalId)) - .setLabel('Confirm Delete') - .setStyle(ButtonStyle.Danger); + const proposal = await ProposalLogic.getProposalById(parseInt(proposalId)); + if (!proposal) { + throw new EndUserError('Proposal not found.'); + } - const cancelButton = new ButtonBuilder() - .setCustomId(CustomIDOracle.addArgumentsToAction(this, ManageDraftsAction.OPERATIONS.SELECT_DRAFT, 'proposalId', proposalId, 'udt', '1')) - .setLabel('Cancel') - .setStyle(ButtonStyle.Secondary); + const embed = new EmbedBuilder() + .setColor('#ff0000') + .setTitle(`Confirm Delete: ${proposal.name}`) + .setDescription('Are you sure you want to delete this proposal? This action cannot be undone.') + .addFields( + { name: 'Name', value: proposal.name }, + { name: 'ID', value: proposal.id.toString() }, + { name: 'Budget', value: proposal.budget.toString() }, + { name: 'URL', value: proposal.uri }, + ); + + const confirmButton = new ButtonBuilder() + .setCustomId(CustomIDOracle.addArgumentsToAction(this, ManageDraftsAction.OPERATIONS.CONFIRM_DELETE, 'prId', proposalId)) + .setLabel('Confirm Delete') + .setStyle(ButtonStyle.Danger); + + const cancelButton = new ButtonBuilder() + .setCustomId(CustomIDOracle.addArgumentsToAction(this, ManageDraftsAction.OPERATIONS.SELECT_DRAFT, 'prId', proposalId, 'udt', '1')) + .setLabel('Cancel') + .setStyle(ButtonStyle.Secondary); + + const row = new ActionRowBuilder().addComponents(confirmButton, cancelButton); + + await interaction.update({ embeds: [embed], components: [row] }); + } + + private async handleConfirmDelete(interaction: TrackedInteraction): Promise { + const proposalId = CustomIDOracle.getNamedArgument(interaction.customId, 'prId'); + if (!proposalId) { + throw new EndUserError('Invalid proposal ID.'); + } - const row = new ActionRowBuilder().addComponents(confirmButton, cancelButton); + try { + await ProposalLogic.deleteProposal(parseInt(proposalId)); + await this.handleShowDrafts(interaction); + DiscordStatus.Success.success(interaction, `Proposal ${proposalId} deleted successfully.`); + } catch (error) { + throw new EndUserError('Error deleting proposal', error); + } + } - await interaction.update({ embeds: [embed], components: [row] }); + private async handleSubmitToFundingRound(interaction: TrackedInteraction): Promise { + const proposalId = CustomIDOracle.getNamedArgument(interaction.customId, 'prId'); + if (!proposalId) { + throw new EndUserError('Invalid proposal ID.'); } - private async handleConfirmDelete(interaction: TrackedInteraction): Promise { - const proposalId = CustomIDOracle.getNamedArgument(interaction.customId, 'proposalId'); - if (!proposalId) { - throw new EndUserError('Invalid proposal ID.'); - } + const eligibleFundingRounds: FundingRound[] = await FundingRoundLogic.getEligibleFundingRoundsForProposal( + parseInt(proposalId), + interaction.interaction.user.id, + ); - try { - await ProposalLogic.deleteProposal(parseInt(proposalId)); - await this.handleShowDrafts(interaction); - DiscordStatus.Success.success(interaction, `Proposal ${proposalId} deleted successfully.`) - } catch (error) { - throw new EndUserError('Error deleting proposal', error); - } + if (eligibleFundingRounds.length === 0) { + throw new EndUserInfo('There are no eligible funding rounds for this proposal at the moment.'); } - private async handleSubmitToFundingRound(interaction: TrackedInteraction): Promise { - const proposalId = CustomIDOracle.getNamedArgument(interaction.customId, 'proposalId'); - if (!proposalId) { - throw new EndUserError('Invalid proposal ID.'); - } + const selectMenu = new StringSelectMenuBuilder() + .setCustomId( + CustomIDOracle.addArgumentsToAction( + (this.screen as ProposalHomeScreen).submitProposalToFundingRoundAction, + SubmitProposalToFundingRoundAction.OPERATIONS.CONFIRM_SUBMISSION, + 'prId', + proposalId.toString(), + ), + ) + .setPlaceholder('Select a Funding Round') + .addOptions( + eligibleFundingRounds.map((fr) => ({ + label: fr.name, + value: fr.id.toString(), + description: `Budget: ${fr.budget}, Ends: ${fr.endAt.toLocaleDateString()}`, + })), + ); + + const row = new ActionRowBuilder().addComponents(selectMenu); + + await interaction.respond({ content: 'Please select a funding round to submit your proposal to:', components: [row] }); + } + + public allSubActions(): Action[] { + return []; + } + + getComponent(): ButtonBuilder { + return new ButtonBuilder() + .setCustomId(CustomIDOracle.addArgumentsToAction(this, ManageDraftsAction.OPERATIONS.SHOW_DRAFTS)) + .setLabel('Manage Drafts') + .setStyle(ButtonStyle.Primary); + } +} - const eligibleFundingRounds: FundingRound[] = await FundingRoundLogic.getEligibleFundingRoundsForProposal(parseInt(proposalId), interaction.interaction.user.id); +export class CreateNewProposalAction extends Action { + public static readonly ID = 'createNewProposal'; + + public static readonly OPERATIONS = { + SHOW_CREATE_FORM: 'showCreateForm', + SUBMIT_CREATE_FORM: 'submitCreateForm', + }; + + public static readonly INPUT_IDS = { + NAME: 'name', + BUDGET: 'budget', + URI: 'uri', + }; + + protected async handleOperation(interaction: TrackedInteraction, operationId: string): Promise { + switch (operationId) { + case CreateNewProposalAction.OPERATIONS.SHOW_CREATE_FORM: + await this.handleShowCreateForm(interaction); + break; + case CreateNewProposalAction.OPERATIONS.SUBMIT_CREATE_FORM: + await this.handleSubmitCreateForm(interaction); + break; + default: + await this.handleInvalidOperation(interaction, operationId); + } + } - if (eligibleFundingRounds.length === 0) { - throw new EndUserInfo('There are no eligible funding rounds for this proposal at the moment.'); - } + private async handleShowCreateForm(interaction: TrackedInteraction): Promise { + const modalInteraction: AnyInteractionWithShowModal | undefined = InteractionProperties.toShowModalOrUndefined(interaction.interaction); + if (!modalInteraction) { + throw new EndUserError('This interaction does not support modals.'); + } - const selectMenu = new StringSelectMenuBuilder() - .setCustomId(CustomIDOracle.addArgumentsToAction((this.screen as ProposalHomeScreen).submitProposalToFundingRoundAction, SubmitProposalToFundingRoundAction.OPERATIONS.CONFIRM_SUBMISSION, 'proposalId', proposalId.toString())) - .setPlaceholder('Select a Funding Round') - .addOptions(eligibleFundingRounds.map(fr => ({ - label: fr.name, - value: fr.id.toString(), - description: `Budget: ${fr.budget}, Ends: ${fr.endAt.toLocaleDateString()}` - }))); + const modal = new ModalBuilder() + .setCustomId(CustomIDOracle.addArgumentsToAction(this, CreateNewProposalAction.OPERATIONS.SUBMIT_CREATE_FORM)) + .setTitle('Create New Proposal'); + + const nameInput = new TextInputBuilder() + .setCustomId(CreateNewProposalAction.INPUT_IDS.NAME) + .setLabel('Proposal Name') + .setStyle(TextInputStyle.Short) + .setRequired(true); + + const budgetInput = new TextInputBuilder() + .setCustomId(CreateNewProposalAction.INPUT_IDS.BUDGET) + .setLabel('Budget') + .setStyle(TextInputStyle.Short) + .setRequired(true); + + const uriInput = new TextInputBuilder() + .setCustomId(CreateNewProposalAction.INPUT_IDS.URI) + .setLabel('URL (https://forums.minaprotocol.com/....)') + .setStyle(TextInputStyle.Short) + .setRequired(true); + + modal.addComponents( + new ActionRowBuilder().addComponents(nameInput), + new ActionRowBuilder().addComponents(budgetInput), + new ActionRowBuilder().addComponents(uriInput), + ); + + await modalInteraction.showModal(modal); + } + + private async handleSubmitCreateForm(interaction: TrackedInteraction): Promise { + const modalInteraction = InteractionProperties.toModalSubmitInteractionOrUndefined(interaction.interaction); + if (!modalInteraction) { + throw new EndUserError('Invalid interaction type.'); + } - const row = new ActionRowBuilder().addComponents(selectMenu); + const name = modalInteraction.fields.getTextInputValue(CreateNewProposalAction.INPUT_IDS.NAME); + const budget = parseFloat(modalInteraction.fields.getTextInputValue(CreateNewProposalAction.INPUT_IDS.BUDGET)); + let uri = modalInteraction.fields.getTextInputValue(CreateNewProposalAction.INPUT_IDS.URI); - await interaction.respond({ content: 'Please select a funding round to submit your proposal to:', components: [row] }); + if (uri.startsWith('http://')) { + uri = uri.replace('http://', 'https://'); + } else if (uri.startsWith('forums.minaprotocol.com/')) { + uri = 'https://' + uri; } - public allSubActions(): Action[] { - return []; + if (isNaN(budget)) { + throw new EndUserError('Invalid budget value.'); } - getComponent(): ButtonBuilder { - return new ButtonBuilder() - .setCustomId(CustomIDOracle.addArgumentsToAction(this, ManageDraftsAction.OPERATIONS.SHOW_DRAFTS)) - .setLabel('Manage Drafts') - .setStyle(ButtonStyle.Primary); + if (!uri.startsWith('https://forums.minaprotocol.com')) { + throw new EndUserError('Invalid URL. Please use a link from https://forums.minaprotocol.com'); } -} - -export class CreateNewProposalAction extends Action { - public static readonly ID = 'createNewProposal'; - - public static readonly OPERATIONS = { - SHOW_CREATE_FORM: 'showCreateForm', - SUBMIT_CREATE_FORM: 'submitCreateForm', - }; - public static readonly INPUT_IDS = { - NAME: 'name', - BUDGET: 'budget', - URI: 'uri', - }; - - protected async handleOperation(interaction: TrackedInteraction, operationId: string): Promise { - switch (operationId) { - case CreateNewProposalAction.OPERATIONS.SHOW_CREATE_FORM: - await this.handleShowCreateForm(interaction); - break; - case CreateNewProposalAction.OPERATIONS.SUBMIT_CREATE_FORM: - await this.handleSubmitCreateForm(interaction); - break; - default: - await this.handleInvalidOperation(interaction, operationId); - } - } - - private async handleShowCreateForm(interaction: TrackedInteraction): Promise { - const modalInteraction: AnyInteractionWithShowModal | undefined = InteractionProperties.toShowModalOrUndefined(interaction.interaction); - if (!modalInteraction) { - throw new EndUserError('This interaction does not support modals.'); - } - - const modal = new ModalBuilder() - .setCustomId(CustomIDOracle.addArgumentsToAction(this, CreateNewProposalAction.OPERATIONS.SUBMIT_CREATE_FORM)) - .setTitle('Create New Proposal'); - - const nameInput = new TextInputBuilder() - .setCustomId(CreateNewProposalAction.INPUT_IDS.NAME) - .setLabel('Proposal Name') - .setStyle(TextInputStyle.Short) - .setRequired(true); - - const budgetInput = new TextInputBuilder() - .setCustomId(CreateNewProposalAction.INPUT_IDS.BUDGET) - .setLabel('Budget') - .setStyle(TextInputStyle.Short) - .setRequired(true); - - const uriInput = new TextInputBuilder() - .setCustomId(CreateNewProposalAction.INPUT_IDS.URI) - .setLabel('URL (https://forums.minaprotocol.com/....)') - .setStyle(TextInputStyle.Short) - .setRequired(true); - - modal.addComponents( - new ActionRowBuilder().addComponents(nameInput), - new ActionRowBuilder().addComponents(budgetInput), - new ActionRowBuilder().addComponents(uriInput) + try { + const newProposal = await ProposalLogic.createProposal({ + name, + budget, + uri, + proposerDuid: interaction.interaction.user.id, + status: ProposalStatus.DRAFT, + fundingRoundId: null, + forumThreadId: null, + }); + + const embed = new EmbedBuilder() + .setColor('#00FF00') + .setTitle('Proposal Created Successfully') + .setDescription('Your new proposal draft has been created.') + .addFields( + { name: 'ID (auto-assigned)', value: newProposal.id.toString() }, + { name: 'Name', value: newProposal.name }, + { name: 'Budget', value: newProposal.budget.toString() }, + { name: 'URL', value: newProposal.uri }, ); - await modalInteraction.showModal(modal); - } - - private async handleSubmitCreateForm(interaction: TrackedInteraction): Promise { - const modalInteraction = InteractionProperties.toModalSubmitInteractionOrUndefined(interaction.interaction); - if (!modalInteraction) { - throw new EndUserError('Invalid interaction type.'); - } - - const name = modalInteraction.fields.getTextInputValue(CreateNewProposalAction.INPUT_IDS.NAME); - const budget = parseFloat(modalInteraction.fields.getTextInputValue(CreateNewProposalAction.INPUT_IDS.BUDGET)); - let uri = modalInteraction.fields.getTextInputValue(CreateNewProposalAction.INPUT_IDS.URI); - - if (uri.startsWith('http://')) { - uri = uri.replace('http://', 'https://'); - } else if (uri.startsWith('forums.minaprotocol.com/')) { - uri = 'https://' + uri; - } - - if (isNaN(budget)) { - throw new EndUserError('Invalid budget value.'); - } - - if (!uri.startsWith('https://forums.minaprotocol.com')) { - throw new EndUserError('Invalid URL. Please use a link from https://forums.minaprotocol.com'); - } - - try { - const newProposal = await ProposalLogic.createProposal({ - name, - budget, - uri, - proposerDuid: interaction.interaction.user.id, - status: ProposalStatus.DRAFT, - fundingRoundId: null, - forumThreadId: null, - }); - - const embed = new EmbedBuilder() - .setColor('#00FF00') - .setTitle('Proposal Created Successfully') - .setDescription('Your new proposal draft has been created.') - .addFields( - { name: 'ID (auto-assigned)', value: newProposal.id.toString() }, - { name: 'Name', value: newProposal.name }, - { name: 'Budget', value: newProposal.budget.toString() }, - { name: 'URL', value: newProposal.uri } - ); - - const manageButton = new ButtonBuilder() - .setCustomId(CustomIDOracle.addArgumentsToAction((this.screen as ProposalHomeScreen).manageDraftsAction, ManageDraftsAction.OPERATIONS.SHOW_DRAFTS, "udt", "1")) - .setLabel('Manage Drafts') - .setStyle(ButtonStyle.Primary); - - const row = new ActionRowBuilder().addComponents(manageButton); - - await interaction.respond({ embeds: [embed], components: [row] }); - } catch (error) { - throw new EndUserError('Error creating proposal', error); - } - } - - public allSubActions(): Action[] { - return []; - } - - getComponent(): ButtonBuilder { - return new ButtonBuilder() - .setCustomId(CustomIDOracle.addArgumentsToAction(this, CreateNewProposalAction.OPERATIONS.SHOW_CREATE_FORM)) - .setLabel('Create New Proposal') - .setStyle(ButtonStyle.Success); + const manageButton = new ButtonBuilder() + .setCustomId( + CustomIDOracle.addArgumentsToAction( + (this.screen as ProposalHomeScreen).manageDraftsAction, + ManageDraftsAction.OPERATIONS.SHOW_DRAFTS, + 'udt', + '1', + ), + ) + .setLabel('Manage Drafts') + .setStyle(ButtonStyle.Primary); + + const row = new ActionRowBuilder().addComponents(manageButton); + + await interaction.respond({ embeds: [embed], components: [row] }); + } catch (error) { + throw new EndUserError('Error creating proposal', error); } + } + + public allSubActions(): Action[] { + return []; + } + + getComponent(): ButtonBuilder { + return new ButtonBuilder() + .setCustomId(CustomIDOracle.addArgumentsToAction(this, CreateNewProposalAction.OPERATIONS.SHOW_CREATE_FORM)) + .setLabel('Create New Proposal') + .setStyle(ButtonStyle.Success); + } } export class SubmitProposalToFundingRoundAction extends Action { - public static readonly ID = 'submitProposalToFundingRound'; + public static readonly ID = 'submitProposalToFundingRound'; + + public static readonly OPERATIONS = { + SHOW_DRAFT_PROPOSALS: 'shDrP', + SELECT_FUNDING_ROUND: 'slFr', + CONFIRM_SUBMISSION: 'cnSb', + EXECUTE_SUBMISSION: 'exSb', + }; + + protected async handleOperation(interaction: TrackedInteraction, operationId: string): Promise { + switch (operationId) { + case SubmitProposalToFundingRoundAction.OPERATIONS.SHOW_DRAFT_PROPOSALS: + await this.handleShowDraftProposals(interaction); + break; + case SubmitProposalToFundingRoundAction.OPERATIONS.SELECT_FUNDING_ROUND: + await this.handleSelectFundingRound(interaction); + break; + case SubmitProposalToFundingRoundAction.OPERATIONS.CONFIRM_SUBMISSION: + await this.handleConfirmSubmission(interaction); + break; + case SubmitProposalToFundingRoundAction.OPERATIONS.EXECUTE_SUBMISSION: + await this.handleExecuteSubmission(interaction); + break; + default: + await this.handleInvalidOperation(interaction, operationId); + } + } - public static readonly OPERATIONS = { - SHOW_DRAFT_PROPOSALS: 'shDrP', - SELECT_FUNDING_ROUND: 'slFr', - CONFIRM_SUBMISSION: 'cnSb', - EXECUTE_SUBMISSION: 'exSb', - }; + private async handleShowDraftProposals(interaction: TrackedInteraction): Promise { + const draftProposals = await ProposalLogic.getUserDraftProposals(interaction.interaction.user.id); - protected async handleOperation(interaction: TrackedInteraction, operationId: string): Promise { - switch (operationId) { - case SubmitProposalToFundingRoundAction.OPERATIONS.SHOW_DRAFT_PROPOSALS: - await this.handleShowDraftProposals(interaction); - break; - case SubmitProposalToFundingRoundAction.OPERATIONS.SELECT_FUNDING_ROUND: - await this.handleSelectFundingRound(interaction); - break; - case SubmitProposalToFundingRoundAction.OPERATIONS.CONFIRM_SUBMISSION: - await this.handleConfirmSubmission(interaction); - break; - case SubmitProposalToFundingRoundAction.OPERATIONS.EXECUTE_SUBMISSION: - await this.handleExecuteSubmission(interaction); - break; - default: - await this.handleInvalidOperation(interaction, operationId); - } - } - - private async handleShowDraftProposals(interaction: TrackedInteraction): Promise { - const draftProposals = await ProposalLogic.getUserDraftProposals(interaction.interaction.user.id); - - if (draftProposals.length === 0) { - throw new EndUserInfo('ℹ️ You have no draft proposals to submit.'); - } - - const embed = new EmbedBuilder() - .setColor('#0099ff') - .setTitle('Select Draft Proposal') - .setDescription('Please select a draft proposal to submit to a funding round.'); - - const selectMenu = new StringSelectMenuBuilder() - .setCustomId(CustomIDOracle.addArgumentsToAction(this, SubmitProposalToFundingRoundAction.OPERATIONS.SELECT_FUNDING_ROUND)) - .setPlaceholder('Select a Draft Proposal') - .addOptions(draftProposals.map(proposal => ({ - label: proposal.name, - value: proposal.id.toString(), - description: `Budget: ${proposal.budget}` - }))); - - const row = new ActionRowBuilder().addComponents(selectMenu); - - const asUpdate = CustomIDOracle.getNamedArgument(interaction.customId, 'udt'); - - const isUpdate: boolean = asUpdate === '1'; - - if (isUpdate) { - await interaction.update({ embeds: [embed], components: [row] }); - } else { - - await interaction.respond({ embeds: [embed], components: [row] }); - } - } - - private async handleSelectFundingRound(interaction: TrackedInteraction): Promise { - const interactionWithValues = InteractionProperties.toInteractionWithValuesOrUndefined(interaction.interaction); - if (!interactionWithValues) { - throw new EndUserError('Invalid interaction type.'); - } - - const proposalId = parseInt(interactionWithValues.values[0]); - const eligibleFundingRounds: FundingRound[] = await FundingRoundLogic.getEligibleFundingRoundsForProposal(proposalId, interaction.interaction.user.id); - - if (eligibleFundingRounds.length === 0) { - throw new EndUserError('There are no eligible funding rounds for this proposal at the moment.'); - } - - const selectMenu = new StringSelectMenuBuilder() - .setCustomId(CustomIDOracle.addArgumentsToAction(this, SubmitProposalToFundingRoundAction.OPERATIONS.CONFIRM_SUBMISSION, 'proposalId', proposalId.toString())) - .setPlaceholder('Select a Funding Round') - .addOptions(eligibleFundingRounds.map(fr => ({ - label: fr.name, - value: fr.id.toString(), - description: `Budget: ${fr.budget}, Ends: ${fr.endAt.toLocaleDateString()}` - }))); - - const row = new ActionRowBuilder().addComponents(selectMenu); - - await interaction.respond({ content: 'Please select a funding round to submit your proposal to:', components: [row] }); - } - - private async handleConfirmSubmission(interaction: TrackedInteraction): Promise { - const interactionWithValues = InteractionProperties.toInteractionWithValuesOrUndefined(interaction.interaction); - if (!interactionWithValues) { - throw new EndUserError('Invalid interaction type.'); - } - - const fundingRoundId = parseInt(interactionWithValues.values[0]); - const proposalId = parseInt(CustomIDOracle.getNamedArgument(interaction.customId, 'proposalId') || ''); - - const proposal = await ProposalLogic.getProposalById(proposalId); - const fundingRound = await FundingRoundLogic.getFundingRoundById(fundingRoundId); - - if (!proposal || !fundingRound) { - throw new EndUserError('Proposal or Funding Round not found.'); - } - - const embed = new EmbedBuilder() - .setColor('#FFA500') - .setTitle('Confirm Proposal Submission') - .setDescription('Please review the details below and confirm your submission.') - .addFields( - { name: 'Proposal ID (auto-assigned)', value: proposal.id.toString() }, - { name: 'Proposal Name', value: proposal.name }, - { name: 'Proposal Budget', value: proposal.budget.toString() }, - { name: 'Proposal URI', value: proposal.uri }, - { name: 'Funding Round', value: fundingRound.name }, - { name: 'Funding Round Budget', value: fundingRound.budget.toString() }, - { name: 'Funding Round End Date', value: fundingRound.endAt.toLocaleDateString() } - ) - .setFooter({ text: 'Note: Once submitted, you cannot remove or reassign this proposal to another funding round. You can cancel your proposal later.' }); + if (draftProposals.length === 0) { + throw new EndUserInfo('ℹ️ You have no draft proposals to submit.'); + } + + const embed = new EmbedBuilder() + .setColor('#0099ff') + .setTitle('Select Draft Proposal') + .setDescription('Please select a draft proposal to submit to a funding round.'); + + const selectMenu = new StringSelectMenuBuilder() + .setCustomId(CustomIDOracle.addArgumentsToAction(this, SubmitProposalToFundingRoundAction.OPERATIONS.SELECT_FUNDING_ROUND)) + .setPlaceholder('Select a Draft Proposal') + .addOptions( + draftProposals.map((proposal) => ({ + label: proposal.name, + value: proposal.id.toString(), + description: `Budget: ${proposal.budget}`, + })), + ); - const confirmButton = new ButtonBuilder() - .setCustomId(CustomIDOracle.addArgumentsToAction(this, SubmitProposalToFundingRoundAction.OPERATIONS.EXECUTE_SUBMISSION, 'proposalId', proposalId.toString(), 'frId', fundingRoundId.toString())) - .setLabel('Confirm Submission') - .setStyle(ButtonStyle.Success); + const row = new ActionRowBuilder().addComponents(selectMenu); - const cancelButton = new ButtonBuilder() - .setCustomId(CustomIDOracle.addArgumentsToAction(this, SubmitProposalToFundingRoundAction.OPERATIONS.SHOW_DRAFT_PROPOSALS, "udt", "1")) - .setLabel('Cancel') - .setStyle(ButtonStyle.Danger); + const asUpdate = CustomIDOracle.getNamedArgument(interaction.customId, 'udt'); - const row = new ActionRowBuilder().addComponents(confirmButton, cancelButton); + const isUpdate: boolean = asUpdate === '1'; - await interaction.update({ embeds: [embed], components: [row] }); + if (isUpdate) { + await interaction.update({ embeds: [embed], components: [row] }); + } else { + await interaction.respond({ embeds: [embed], components: [row] }); } + } - private async handleExecuteSubmission(interaction: TrackedInteraction): Promise { - const proposalId = parseInt(CustomIDOracle.getNamedArgument(interaction.customId, 'proposalId') || ''); - const fundingRoundId = parseInt(CustomIDOracle.getNamedArgument(interaction.customId, 'frId') || ''); + private async handleSelectFundingRound(interaction: TrackedInteraction): Promise { + const interactionWithValues = InteractionProperties.toInteractionWithValuesOrUndefined(interaction.interaction); + if (!interactionWithValues) { + throw new EndUserError('Invalid interaction type.'); + } - try { - const embed = new EmbedBuilder() - .setColor('#00FF00') - .setTitle('Proposal Submitted Successfully') - .setDescription('Your proposal has been submitted to the funding round.') - .setFooter({ text: 'You can view and manage your submitted proposals in the "Manage Submitted Proposals" section.' }); + const proposalId = parseInt(interactionWithValues.values[0]); + const eligibleFundingRounds: FundingRound[] = await FundingRoundLogic.getEligibleFundingRoundsForProposal( + proposalId, + interaction.interaction.user.id, + ); - const manageButton = new ButtonBuilder() - .setCustomId(CustomIDOracle.addArgumentsToAction((this.screen as ProposalHomeScreen).manageSubmittedProposalsAction, 'showFundingRounds')) - .setLabel('Manage Submitted Proposals') - .setStyle(ButtonStyle.Primary); + if (eligibleFundingRounds.length === 0) { + throw new EndUserError('There are no eligible funding rounds for this proposal at the moment.'); + } - const row = new ActionRowBuilder().addComponents(manageButton); - await interaction.update({ embeds: [embed], components: [row] }); - await ProposalLogic.submitProposalToFundingRound(proposalId, fundingRoundId, this.screen); - } catch (error) { - logger.error(error); - throw new EndUserError('Failed to submit proposal', error); - } + const selectMenu = new StringSelectMenuBuilder() + .setCustomId( + CustomIDOracle.addArgumentsToAction(this, SubmitProposalToFundingRoundAction.OPERATIONS.CONFIRM_SUBMISSION, 'prId', proposalId.toString()), + ) + .setPlaceholder('Select a Funding Round') + .addOptions( + eligibleFundingRounds.map((fr) => ({ + label: fr.name, + value: fr.id.toString(), + description: `Budget: ${fr.budget}, Ends: ${fr.endAt.toLocaleDateString()}`, + })), + ); + + const row = new ActionRowBuilder().addComponents(selectMenu); + + await interaction.respond({ content: 'Please select a funding round to submit your proposal to:', components: [row] }); + } + + private async handleConfirmSubmission(interaction: TrackedInteraction): Promise { + const interactionWithValues = InteractionProperties.toInteractionWithValuesOrUndefined(interaction.interaction); + if (!interactionWithValues) { + throw new EndUserError('Invalid interaction type.'); + } + + const fundingRoundId = parseInt(interactionWithValues.values[0]); + const proposalId = parseInt(CustomIDOracle.getNamedArgument(interaction.customId, 'prId') || ''); + const proposal = await ProposalLogic.getProposalById(proposalId); + const fundingRound = await FundingRoundLogic.getFundingRoundById(fundingRoundId); + + if (!proposal || !fundingRound) { + throw new EndUserError('Proposal or Funding Round not found.'); } - public allSubActions(): Action[] { - return []; - } - - getComponent(): ButtonBuilder { - return new ButtonBuilder() - .setCustomId(CustomIDOracle.addArgumentsToAction(this, SubmitProposalToFundingRoundAction.OPERATIONS.SHOW_DRAFT_PROPOSALS)) - .setLabel('Submit Proposal To Funding Round') - .setStyle(ButtonStyle.Primary); + const embed = new EmbedBuilder() + .setColor('#FFA500') + .setTitle('Confirm Proposal Submission') + .setDescription('Please review the details below and confirm your submission.') + .addFields( + { name: 'Proposal ID (auto-assigned)', value: proposal.id.toString() }, + { name: 'Proposal Name', value: proposal.name }, + { name: 'Proposal Budget', value: proposal.budget.toString() }, + { name: 'Proposal URI', value: proposal.uri }, + { name: 'Funding Round', value: fundingRound.name }, + { name: 'Funding Round Budget', value: fundingRound.budget.toString() }, + { name: 'Funding Round End Date', value: fundingRound.endAt.toLocaleDateString() }, + ) + .setFooter({ + text: 'Note: Once submitted, you cannot remove or reassign this proposal to another funding round. You can cancel your proposal later.', + }); + + const confirmButton = new ButtonBuilder() + .setCustomId( + CustomIDOracle.addArgumentsToAction( + this, + SubmitProposalToFundingRoundAction.OPERATIONS.EXECUTE_SUBMISSION, + 'prId', + proposalId.toString(), + 'frId', + fundingRoundId.toString(), + ), + ) + .setLabel('Confirm Submission') + .setStyle(ButtonStyle.Success); + + const cancelButton = new ButtonBuilder() + .setCustomId(CustomIDOracle.addArgumentsToAction(this, SubmitProposalToFundingRoundAction.OPERATIONS.SHOW_DRAFT_PROPOSALS, 'udt', '1')) + .setLabel('Cancel') + .setStyle(ButtonStyle.Danger); + + const row = new ActionRowBuilder().addComponents(confirmButton, cancelButton); + + await interaction.update({ embeds: [embed], components: [row] }); + } + + private async handleExecuteSubmission(interaction: TrackedInteraction): Promise { + const proposalId = parseInt(CustomIDOracle.getNamedArgument(interaction.customId, 'prId') || ''); + const fundingRoundId = parseInt(CustomIDOracle.getNamedArgument(interaction.customId, 'frId') || ''); + + try { + const embed = new EmbedBuilder() + .setColor('#00FF00') + .setTitle('Proposal Submitted Successfully') + .setDescription('Your proposal has been submitted to the funding round.') + .setFooter({ text: 'You can view and manage your submitted proposals in the "Manage Submitted Proposals" section.' }); + + const manageButton = new ButtonBuilder() + .setCustomId(CustomIDOracle.addArgumentsToAction((this.screen as ProposalHomeScreen).manageSubmittedProposalsAction, 'showFundingRounds')) + .setLabel('Manage Submitted Proposals') + .setStyle(ButtonStyle.Primary); + + const row = new ActionRowBuilder().addComponents(manageButton); + await interaction.update({ embeds: [embed], components: [row] }); + await ProposalLogic.submitProposalToFundingRound(proposalId, fundingRoundId, this.screen); + } catch (error) { + logger.error(error); + throw new EndUserError('Failed to submit proposal', error); } -} \ No newline at end of file + } + + public allSubActions(): Action[] { + return []; + } + + getComponent(): ButtonBuilder { + return new ButtonBuilder() + .setCustomId(CustomIDOracle.addArgumentsToAction(this, SubmitProposalToFundingRoundAction.OPERATIONS.SHOW_DRAFT_PROPOSALS)) + .setLabel('Submit Proposal To Funding Round') + .setStyle(ButtonStyle.Primary); + } +}