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 @@
+
+
+
+
+
+
+
+ {{ text }} with
+ Turbo.
+
+
+ Get your space
+ verified
+ to increase the limit to {{ text }}.
+
+
{{ text }}
+
+
+
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"
/>
+
+
+
+