Skip to content

Commit

Permalink
feat: add Proposal settings (excl. validation) (#832)
Browse files Browse the repository at this point in the history
* feat: add Proposal settings (excl. validation)

* fix: allow empty guidelines/template when dirty

* feat: display all extra errors in bottom bar

* refactor: reorder function calls

* feat: display global error for FormSpaceVoting
  • Loading branch information
Sekhmet authored Oct 1, 2024
1 parent e9ba137 commit 626a8a5
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 15 deletions.
8 changes: 4 additions & 4 deletions apps/ui/src/components/FormSpaceAdvanced.vue
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
(e: 'updateValidity', valid: boolean): void;
(e: 'updateValidity', valid: boolean, resolved: boolean): void;
(e: 'deleteSpace');
}>();
Expand Down Expand Up @@ -135,12 +135,12 @@ function deleteChild(i: number) {
}
watchEffect(() => {
const resolved = parentSpaceValidationResult.value?.value === parent.value;
const valid =
Object.keys(formErrors.value).length === 0 &&
parentSpaceValidationResult.value?.valid &&
parentSpaceValidationResult.value?.value === parent.value;
parentSpaceValidationResult.value?.valid;
emit('updateValidity', !!valid);
emit('updateValidity', !!valid, resolved);
});
</script>

Expand Down
85 changes: 85 additions & 0 deletions apps/ui/src/components/FormSpaceProposal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<script setup lang="ts">
import { getValidator } from '@/helpers/validation';
const GUIDELINES_DEFINITION = {
type: 'string',
format: 'uri',
title: 'Guidelines',
maxLength: 256,
examples: ['https://example.com/guidelines'],
tooltip:
'Display a link to your guidelines on proposal creation to help users understand what constitutes a good/valid proposal'
};
const TEMPLATE_DEFINITION = {
type: 'string',
title: 'Template',
maxLength: 1024,
examples: ['## Intro\n## Body\n## Conclusion'],
tooltip:
'Start every proposal with a template to help users understand what information is required'
};
const onlyMembers = defineModel<boolean>('onlyMembers', {
required: true
});
const guidelines = defineModel<string>('guidelines', {
required: true
});
const template = defineModel<string>('template', {
required: true
});
const emit = defineEmits<{
(e: 'updateValidity', valid: boolean): void;
}>();
const errors = computed(() => {
const validator = getValidator({
type: 'object',
title: 'Proposal',
additionalProperties: false,
required: [],
properties: {
guidelines: GUIDELINES_DEFINITION,
template: TEMPLATE_DEFINITION
}
});
return validator.validate(
{
guidelines: guidelines.value,
template: template.value
},
{ skipEmptyOptionalFields: true }
);
});
watchEffect(() => {
emit('updateValidity', Object.values(errors.value).length === 0);
});
</script>

<template>
<h4 class="eyebrow mb-2 font-medium">Proposal Validation</h4>
<div class="s-box mb-4">
<UiSwitch
v-model="onlyMembers"
title="Allow only authors to submit a proposal"
/>
</div>
<h4 class="eyebrow mb-2 font-medium">Proposal</h4>
<div class="s-box mb-4">
<UiInputString
v-model="guidelines"
:definition="GUIDELINES_DEFINITION"
:error="errors.guidelines"
/>
<UiTextarea
v-model="template"
:definition="TEMPLATE_DEFINITION"
:error="errors.template"
class="!min-h-[140px]"
/>
</div>
</template>
8 changes: 8 additions & 0 deletions apps/ui/src/components/FormSpaceVoting.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ const props = defineProps<{
space: Space;
}>();
const emit = defineEmits<{
(e: 'updateValidity', valid: boolean): void;
}>();
const QUORUM_DEFINITION = {
type: 'number',
title: 'Quorum',
Expand Down Expand Up @@ -99,6 +103,10 @@ function getIsMaxVotingPeriodValid(value: number) {
props.space.min_voting_period
);
}
watchEffect(() => {
emit('updateValidity', Object.values(errors.value).length === 0);
});
</script>

<template>
Expand Down
44 changes: 40 additions & 4 deletions apps/ui/src/composables/useSpaceSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ export function useSpaceSettings(space: Ref<Space>) {
const initialValidationStrategyObjectHash = ref(null as string | null);

// Offchain properties
const onlyMembers = ref(false);
const guidelines = ref('');
const template = ref('');
const quorumType = ref(
'default' as NonNullable<OffchainApiSpace['voting']['quorumType']>
);
Expand Down Expand Up @@ -487,8 +490,8 @@ export function useSpaceSettings(space: Ref<Space>) {
private: isPrivate.value,
domain: customDomain.value,
skin: space.value.additionalRawData.skin,
guidelines: space.value.additionalRawData.guidelines,
template: space.value.additionalRawData.template,
guidelines: guidelines.value,
template: template.value,
strategies: strategies.value.map(strategy => ({
name: strategy.name,
network: strategy.chainId?.toString() ?? snapshotChainId.value,
Expand All @@ -512,7 +515,10 @@ export function useSpaceSettings(space: Ref<Space>) {
.map(member => member.address),
plugins: space.value.additionalRawData.plugins,
delegationPortal: delegationPortal,
filters: space.value.additionalRawData.filters,
filters: {
...space.value.additionalRawData.filters,
onlyMembers: onlyMembers.value
},
voting: {
...space.value.additionalRawData.voting,
delay:
Expand Down Expand Up @@ -645,8 +651,12 @@ export function useSpaceSettings(space: Ref<Space>) {
);

if (offchainNetworks.includes(space.value.network)) {
const initialVotingProperties = getInitialVotingProperties(space.value);
onlyMembers.value =
space.value.additionalRawData?.filters.onlyMembers ?? false;
guidelines.value = space.value.additionalRawData?.guidelines ?? '';
template.value = space.value.additionalRawData?.template ?? '';

const initialVotingProperties = getInitialVotingProperties(space.value);
quorumType.value = initialVotingProperties.quorumType;
quorum.value = initialVotingProperties.quorum;
voteType.value = initialVotingProperties.votingType;
Expand Down Expand Up @@ -681,6 +691,9 @@ export function useSpaceSettings(space: Ref<Space>) {
const validationStrategyValue = validationStrategy.value;
const initialValidationStrategyObjectHashValue =
initialValidationStrategyObjectHash.value;
const onlyMembersValue = onlyMembers.value;
const guidelinesValue = guidelines.value;
const templateValue = template.value;
const quorumTypeValue = quorumType.value;
const quorumValue = quorum.value;
const votingTypeValue = voteType.value;
Expand Down Expand Up @@ -734,6 +747,26 @@ export function useSpaceSettings(space: Ref<Space>) {
if (offchainNetworks.includes(space.value.network)) {
const ignoreOrderOpts = { unorderedArrays: true };

if (
onlyMembersValue !==
(space.value.additionalRawData?.filters.onlyMembers ?? false)
) {
isModified.value = true;
return;
}

if (
guidelinesValue !== (space.value.additionalRawData?.guidelines ?? '')
) {
isModified.value = true;
return;
}

if (templateValue !== (space.value.additionalRawData?.template ?? '')) {
isModified.value = true;
return;
}

const initialVotingProperties = getInitialVotingProperties(space.value);

if (quorumTypeValue !== initialVotingProperties.quorumType) {
Expand Down Expand Up @@ -872,6 +905,9 @@ export function useSpaceSettings(space: Ref<Space>) {
authenticators,
validationStrategy,
votingStrategies,
onlyMembers,
guidelines,
template,
quorumType,
quorum,
votingType: voteType,
Expand Down
52 changes: 45 additions & 7 deletions apps/ui/src/views/Space/Settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ const {
authenticators,
validationStrategy,
votingStrategies,
onlyMembers,
guidelines,
template,
quorumType,
quorum,
votingType,
Expand All @@ -44,8 +47,11 @@ const {
const spacesStore = useSpacesStore();
const { setTitle } = useTitle();
const hasAdvancedErrors = ref(false);
const isAdvancedFormResolved = ref(false);
const hasStrategiesErrors = ref(false);
const hasVotingErrors = ref(false);
const hasProposalErrors = ref(false);
const hasAdvancedErrors = ref(false);
const changeControllerModalOpen = ref(false);
const executeFn = ref(save);
const saving = ref(false);
Expand All @@ -59,6 +65,7 @@ type Tab = {
| 'authenticators'
| 'proposal-validation'
| 'voting-strategies'
| 'proposal'
| 'voting'
| 'members'
| 'execution'
Expand Down Expand Up @@ -110,6 +117,11 @@ const tabs = computed<Tab[]>(
name: 'Voting strategies',
visible: !isOffchainNetwork.value
},
{
id: 'proposal',
name: 'Proposal',
visible: isOffchainNetwork.value
},
{
id: 'voting',
name: 'Voting',
Expand Down Expand Up @@ -182,6 +194,18 @@ const error = computed(() => {
if (hasStrategiesErrors.value) {
return 'Strategies are invalid';
}
if (hasProposalErrors.value) {
return 'Proposal settings are invalid';
}
if (hasVotingErrors.value) {
return 'Voting settings are invalid';
}
if (hasAdvancedErrors.value && isAdvancedFormResolved.value) {
return 'Advanced settings are invalid';
}
}
return null;
Expand Down Expand Up @@ -349,6 +373,18 @@ watchEffect(() => setTitle(`Edit settings - ${props.space.name}`));
title="Voting strategies"
description="Voting strategies are customizable contracts used to define how much voting power each user has when casting a vote."
/>
<UiContainerSettings
v-else-if="activeTab === 'proposal'"
title="Proposal"
description="Set proposal validation to define who can create proposals and provide additional resources for proposal authors."
>
<FormSpaceProposal
v-model:only-members="onlyMembers"
v-model:guidelines="guidelines"
v-model:template="template"
@update-validity="v => (hasProposalErrors = !v)"
/>
</UiContainerSettings>
<UiContainerSettings
v-else-if="activeTab === 'voting'"
title="Voting"
Expand All @@ -364,6 +400,7 @@ watchEffect(() => setTitle(`Edit settings - ${props.space.name}`));
v-model:privacy="privacy"
v-model:ignore-abstain-votes="ignoreAbstainVotes"
:space="space"
@update-validity="v => (hasVotingErrors = !v)"
/>
</UiContainerSettings>
<UiContainerSettings
Expand Down Expand Up @@ -441,16 +478,17 @@ watchEffect(() => setTitle(`Edit settings - ${props.space.name}`));
:space-id="space.id"
:is-controller="isController"
@delete-space="handleSpaceDelete"
@update-validity="v => (hasAdvancedErrors = !v)"
@update-validity="
(valid, resolved) => {
hasAdvancedErrors = !valid;
isAdvancedFormResolved = resolved;
}
"
/>
</UiContainerSettings>
<UiToolbarBottom
v-if="
(isModified &&
canModifySettings &&
!hasAdvancedErrors &&
!hasStrategiesErrors) ||
error
(isModified && isAdvancedFormResolved && canModifySettings) || error
"
class="px-4 py-3 flex flex-col xs:flex-row justify-between items-center"
>
Expand Down

0 comments on commit 626a8a5

Please sign in to comment.