diff --git a/apps/ui/src/components/EditorHint.vue b/apps/ui/src/components/EditorHint.vue new file mode 100644 index 000000000..51dab57da --- /dev/null +++ b/apps/ui/src/components/EditorHint.vue @@ -0,0 +1,38 @@ + + + diff --git a/apps/ui/src/helpers/turbo.ts b/apps/ui/src/helpers/turbo.ts new file mode 100644 index 000000000..b096b7f49 --- /dev/null +++ b/apps/ui/src/helpers/turbo.ts @@ -0,0 +1,26 @@ +export const MAX_BODY_LENGTH = { + default: 10000, + turbo: 40000 +}; + +export const MAX_CHOICES = { + default: 500, + turbo: 1000 +}; + +export const MAX_1D_PROPOSALS = { + default: 3, + verified: 20, + turbo: 40 +}; + +export const MAX_30D_PROPOSALS = { + default: 15, + verified: 100, + turbo: 200 +}; + +export const TURBO_URL = + 'https://docs.snapshot.org/user-guides/spaces/turbo-plan'; +export const VERIFIED_URL = + 'https://docs.snapshot.org/user-guides/spaces/get-verified'; diff --git a/apps/ui/src/helpers/validation.ts b/apps/ui/src/helpers/validation.ts index 858f20099..399c49a48 100644 --- a/apps/ui/src/helpers/validation.ts +++ b/apps/ui/src/helpers/validation.ts @@ -308,7 +308,9 @@ const getErrors = (errors: Partial[]) => { let current = output; for (let i = 0; i < path.length - 1; i++) { const subpath = path[i]; - if (!current[subpath]) current[subpath] = {}; + if (typeof current[subpath] !== 'object' || current[subpath] === null) { + current[subpath] = {}; + } current = current[subpath]; } diff --git a/apps/ui/src/networks/offchain/api/index.ts b/apps/ui/src/networks/offchain/api/index.ts index 2a9c6a655..4e7fb770b 100644 --- a/apps/ui/src/networks/offchain/api/index.ts +++ b/apps/ui/src/networks/offchain/api/index.ts @@ -179,6 +179,9 @@ function formatSpace( discord: '', coingecko: space.coingecko || '', proposal_count: space.proposalsCount, + proposal_count_1d: space.proposalsCount1d, + proposal_count_7d: space.proposalsCount7d, + proposal_count_30d: space.proposalsCount30d, vote_count: space.votesCount, follower_count: space.followersCount, voting_power_symbol: space.symbol, diff --git a/apps/ui/src/networks/offchain/api/queries.ts b/apps/ui/src/networks/offchain/api/queries.ts index 6943058a4..30a9d9915 100644 --- a/apps/ui/src/networks/offchain/api/queries.ts +++ b/apps/ui/src/networks/offchain/api/queries.ts @@ -51,6 +51,9 @@ const SPACE_FRAGMENT = gql` onlyMembers } proposalsCount + proposalsCount1d + proposalsCount7d + proposalsCount30d votesCount followersCount children { diff --git a/apps/ui/src/networks/offchain/api/types.ts b/apps/ui/src/networks/offchain/api/types.ts index c1da7aca3..d37bcae57 100644 --- a/apps/ui/src/networks/offchain/api/types.ts +++ b/apps/ui/src/networks/offchain/api/types.ts @@ -58,6 +58,9 @@ export type ApiSpace = { onlyMembers: boolean; }; proposalsCount: number; + proposalsCount1d: number; + proposalsCount7d: number; + proposalsCount30d: number; votesCount: number; followersCount: number; children: [ApiRelatedSpace]; diff --git a/apps/ui/src/types.ts b/apps/ui/src/types.ts index 074901164..2d8e7535e 100644 --- a/apps/ui/src/types.ts +++ b/apps/ui/src/types.ts @@ -189,6 +189,9 @@ export type Space = { treasury_chain: number | null; }[]; proposal_count: number; + proposal_count_1d?: number; + proposal_count_7d?: number; + proposal_count_30d?: number; vote_count: number; follower_count?: number; created: number; diff --git a/apps/ui/src/views/Editor.vue b/apps/ui/src/views/Editor.vue index 01301ee5c..ce003874c 100644 --- a/apps/ui/src/views/Editor.vue +++ b/apps/ui/src/views/Editor.vue @@ -2,16 +2,17 @@ import { NavigationGuard } from 'vue-router'; import { StrategyWithTreasury } from '@/composables/useTreasuries'; import { resolver } from '@/helpers/resolver'; -import { omit } from '@/helpers/utils'; +import { + MAX_1D_PROPOSALS, + MAX_30D_PROPOSALS, + MAX_BODY_LENGTH, + MAX_CHOICES +} from '@/helpers/turbo'; +import { _n, omit } from '@/helpers/utils'; import { validateForm } from '@/helpers/validation'; import { getNetwork, offchainNetworks } from '@/networks'; import { Contact, Transaction, VoteType } from '@/types'; -const MAX_BODY_LENGTH = { - default: 10000, - turbo: 40000 -} as const; - const TITLE_DEFINITION = { type: 'string', title: 'Title', @@ -27,15 +28,6 @@ const DISCUSSION_DEFINITION = { examples: ['e.g. https://forum.balancer.fi/t/proposal…'] }; -const CHOICES_DEFINITION = { - type: 'array', - title: 'Choices', - minItems: 1, - maxItems: 500, - items: [{ type: 'string', minLength: 1, maxLength: 32 }], - additionalItems: { type: 'string', maxLength: 32 } -}; - const { setTitle } = useTitle(); const { proposals, createDraft } = useEditor(); const { param } = useRouteParser('space'); @@ -134,6 +126,15 @@ const bodyDefinition = computed(() => ({ maxLength: MAX_BODY_LENGTH[space.value?.turbo ? 'turbo' : 'default'], examples: ['Propose something…'] })); + +const choicesDefinition = computed(() => ({ + type: 'array', + title: 'Choices', + minItems: 1, + maxItems: MAX_CHOICES[space.value?.turbo ? 'turbo' : 'default'], + items: [{ type: 'string', minLength: 1, maxLength: 32 }], + additionalItems: { type: 'string', maxLength: 32 } +})); const formErrors = computed(() => { if (!proposal.value) return {}; @@ -147,7 +148,7 @@ const formErrors = computed(() => { title: TITLE_DEFINITION, body: bodyDefinition.value, discussion: DISCUSSION_DEFINITION, - choices: CHOICES_DEFINITION + choices: choicesDefinition.value } }, { @@ -169,6 +170,23 @@ const canSubmit = computed(() => { : !web3.value.authLoading; }); +const spaceType = computed(() => { + if (!space.value) return 'default'; + return space.value.turbo + ? 'turbo' + : space.value.verified + ? 'verified' + : 'default'; +}); + +const proposalLimitReached = computed(() => { + if (!space.value) return false; + return ( + (space.value.proposal_count_1d || 0) >= MAX_1D_PROPOSALS[spaceType.value] || + (space.value.proposal_count_30d || 0) >= MAX_30D_PROPOSALS[spaceType.value] + ); +}); + async function handleProposeClick() { if (!space.value || !proposal.value) return; @@ -384,6 +402,16 @@ export default defineComponent({ action="propose" @fetch-voting-power="handleFetchVotingPower" /> + + + +
- + +