diff --git a/backend/main/models/proposal.go b/backend/main/models/proposal.go index c1347370b..3a2ff83e0 100644 --- a/backend/main/models/proposal.go +++ b/backend/main/models/proposal.go @@ -42,6 +42,7 @@ type Proposal struct { Voucher *shared.Voucher `json:"voucher,omitempty"` Achievements_done bool `json:"achievementsDone"` TallyMethod string `json:"voteType" validate:"required"` + Quorum *float64 `json:"quorum,omitempty"` } type UpdateProposalRequestPayload struct { @@ -146,9 +147,10 @@ func (p *Proposal) CreateProposal(db *s.Database) error { cid, composite_signatures, voucher, - tally_method + tally_method, + quorum ) - VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) + VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) RETURNING id, created_at `, p.Community_id, @@ -167,6 +169,7 @@ func (p *Proposal) CreateProposal(db *s.Database) error { p.Composite_signatures, p.Voucher, p.TallyMethod, + p.Quorum, ).Scan(&p.ID, &p.Created_at) return err @@ -210,17 +213,19 @@ func (p *Proposal) UpdateDraftProposal(db *s.Database) error { strategy = COALESCE($3, strategy), min_balance = COALESCE($4, min_balance), max_weight = COALESCE($5, max_weight), - start_time = COALESCE($6, start_time), - end_time = COALESCE($7, end_time), - body = COALESCE($8, body), - block_height = COALESCE($9, block_height), - cid = COALESCE($10, cid) - WHERE id = $11 + quorum = COALESCE($6, quorum), + start_time = COALESCE($7, start_time), + end_time = COALESCE($8, end_time), + body = COALESCE($9, body), + block_height = COALESCE($10, block_height), + cid = COALESCE($11, cid) + WHERE id = $12 `, p.Name, p.Choices, p.Strategy, p.Min_balance, p.Max_weight, + p.Quorum, p.Start_time, p.End_time, p.Body, diff --git a/backend/main/server/controllers.go b/backend/main/server/controllers.go index 750222315..52de1ea20 100644 --- a/backend/main/server/controllers.go +++ b/backend/main/server/controllers.go @@ -398,7 +398,6 @@ func (a *App) createProposal(w http.ResponseWriter, r *http.Request) { var p models.Proposal p.Community_id = communityId - if err := validatePayload(r.Body, &p); err != nil { log.Error().Err(err).Msg("Error validating payload") respondWithError(w, errIncompleteRequest) diff --git a/backend/main/server/helpers.go b/backend/main/server/helpers.go index 9e8cc4920..7832b0694 100644 --- a/backend/main/server/helpers.go +++ b/backend/main/server/helpers.go @@ -606,7 +606,7 @@ func (h *Helpers) createProposal(p models.Proposal) (models.Proposal, errorRespo canUserCreateProposal := community.CanUserCreateProposal(h.A.DB, h.A.FlowAdapter, p.Creator_addr) - if err := handlePermissionErrorr(canUserCreateProposal); err != nilErr { + if err := handlePermissionError(canUserCreateProposal); err != nilErr { return models.Proposal{}, err } @@ -652,7 +652,7 @@ func (h *Helpers) createDraftProposal(c models.Community, p models.Proposal) (mo p.Creator_addr, ) - if err := handlePermissionErrorr(canUserCreateProposal); err != nilErr { + if err := handlePermissionError(canUserCreateProposal); err != nilErr { return models.Proposal{}, err } @@ -729,7 +729,7 @@ func (h *Helpers) updateDraftProposal(p models.Proposal) (models.Proposal, error p.Creator_addr, ) - if err := handlePermissionErrorr(canUserCreateProposal); err != nilErr { + if err := handlePermissionError(canUserCreateProposal); err != nilErr { return models.Proposal{}, err } @@ -742,7 +742,7 @@ func (h *Helpers) updateDraftProposal(p models.Proposal) (models.Proposal, error return p, nilErr } -func handlePermissionErrorr(result models.CanUserCreateProposalResponse) errorResponse { +func handlePermissionError(result models.CanUserCreateProposalResponse) errorResponse { // If user doesn't have permission, populate errorResponse // with reason and error. if !result.HasPermission { diff --git a/backend/migrations/000044_add_quorum_column.down.sql b/backend/migrations/000044_add_quorum_column.down.sql new file mode 100644 index 000000000..086cab85d --- /dev/null +++ b/backend/migrations/000044_add_quorum_column.down.sql @@ -0,0 +1 @@ +ALTER TABLE proposals DROP COLUMN IF EXISTS quorum; diff --git a/backend/migrations/000044_add_quorum_column.up.sql b/backend/migrations/000044_add_quorum_column.up.sql new file mode 100644 index 000000000..1a51df8f8 --- /dev/null +++ b/backend/migrations/000044_add_quorum_column.up.sql @@ -0,0 +1 @@ +ALTER TABLE proposals ADD COLUMN quorum float; \ No newline at end of file diff --git a/frontend/packages/client/src/api/proposals.js b/frontend/packages/client/src/api/proposals.js index 86f0fad92..6461c62a0 100644 --- a/frontend/packages/client/src/api/proposals.js +++ b/frontend/packages/client/src/api/proposals.js @@ -1,7 +1,7 @@ import { API_BASE_URL, COMMUNITIES_URL, - IPFS_GETWAY, + IPFS_GATEWAY, PROPOSALS_URL, } from './constants'; import { checkResponse } from 'utils'; @@ -20,10 +20,25 @@ export const fetchProposal = async ({ proposalId }) => { const response = await fetch(url); const proposal = await checkResponse(response); - const sortedProposalChoices = + let sortedProposalChoices = proposal.choices?.sort((a, b) => (a.choiceText > b.choiceText ? 1 : -1)) ?? []; + if (proposal.voteType === 'basic') { + // Ensure choices are ordered as For/Against/Abstain for basic voting + const choices = [...sortedProposalChoices]; + sortedProposalChoices.forEach((choice) => { + let index = 0; + if (choice.choiceText === 'Against') { + index = 1; + } else if (choice.choiceText === 'Abstain') { + index = 2; + } + choices[index] = choice; + }); + sortedProposalChoices = choices; + } + const proposalData = { ...proposal, choices: sortedProposalChoices.map((choice) => ({ @@ -32,7 +47,7 @@ export const fetchProposal = async ({ proposalId }) => { choiceImgUrl: choice.choiceImgUrl, })), ipfs: proposal.cid, - ipfsUrl: `${IPFS_GETWAY}/${proposal.cid}`, + ipfsUrl: `${IPFS_GATEWAY}/${proposal.cid}`, totalVotes: proposal.total_votes, // this is coming as a string from db but there could be multiple based on design strategy: proposal.strategy || '-', @@ -48,6 +63,15 @@ export const createProposalApiReq = async ({ timestamp, } = {}) => { const { communityId, ...proposalData } = proposalPayload; + + if (proposalData.voteType === 'basic') { + proposalData.choices = [ + { choiceText: 'For', choiceImgUrl: null }, + { choiceText: 'Against', choiceImgUrl: null }, + { choiceText: 'Abstain', choiceImgUrl: null }, + ]; + } + const url = `${COMMUNITIES_URL}/${communityId}/proposals`; const fetchOptions = { method: 'POST', diff --git a/frontend/packages/client/src/components/Proposal/VoteHeader.js b/frontend/packages/client/src/components/Proposal/VoteHeader.js index 5fa205a5b..8953c34ea 100644 --- a/frontend/packages/client/src/components/Proposal/VoteHeader.js +++ b/frontend/packages/client/src/components/Proposal/VoteHeader.js @@ -9,7 +9,7 @@ const Wrapper = ({ children }) => ( ); -export default function VoteHeader({ status }) { +export default function VoteHeader({ status, voteType = 'single-choice' }) { // Status: user-voted, invite-to-vote, is-closed const message = { 'user-voted': ( @@ -20,7 +20,12 @@ export default function VoteHeader({ status }) { You successfully voted on this proposal! ), - 'invite-to-vote': <>Rank your vote ✨>, + 'invite-to-vote': ( + <> + {voteType === 'single-choice' ? 'Cast your vote' : 'Rank your vote'}{' '} + ✨ + > + ), 'is-closed': <>Voting has ended on this proposal.>, }; diff --git a/frontend/packages/client/src/components/ProposalCreate/FormConfig.js b/frontend/packages/client/src/components/ProposalCreate/FormConfig.js index 4936ef7f8..6550259b2 100644 --- a/frontend/packages/client/src/components/ProposalCreate/FormConfig.js +++ b/frontend/packages/client/src/components/ProposalCreate/FormConfig.js @@ -46,12 +46,21 @@ const StepTwoSchema = yup.object().shape({ }) ), }) - .when('voteType', (voteType, schema) => - voteType === 'single-choice' + .when('voteType', (voteType, schema) => { + if (voteType === 'basic') return; + return voteType === 'single-choice' ? schema.min(2, 'Please add a choice, minimum amount is two') - : schema.min(3, 'Please add a choice, minimum amount is three') - ) + : schema.min(3, 'Please add a choice, minimum amount is three'); + }) .unique('value', 'Invalid duplicated option'), + quorum: yup + .string() + .trim() + .matches(/\s+$|^$|(^[0-9]+$)/, 'Quorum threshold must be a valid number') + .when('voteType', (voteType, schema) => { + if (voteType !== 'basic') return; + return schema.required('Quorum threshold is required'); + }), maxWeight: yup .string() .trim() diff --git a/frontend/packages/client/src/components/ProposalCreate/Preview.js b/frontend/packages/client/src/components/ProposalCreate/Preview.js index 1b4938c88..f30e8ff14 100644 --- a/frontend/packages/client/src/components/ProposalCreate/Preview.js +++ b/frontend/packages/client/src/components/ProposalCreate/Preview.js @@ -33,6 +33,7 @@ const Preview = ({ stepsData }) => { })) : null, }; + console.log(proposal.choices); return (
+ Voters vote on one option and are given three options to vote + on. (For, Against or Abstain) +
+