Skip to content

Commit

Permalink
feat: add profile settings for offchain spaces (#718)
Browse files Browse the repository at this point in the history
* feat: add profile, delegations, treasuries, voting settings for offchain spaces

* feat: merge voting periods for offchain spaces

* fix: pass space as ref

* feat: only allow 1 delegation for offchain networks

* feat: increase cover height

* fix: do not mutate validation.params

* fix: keep same values in voting

* fix: add maxLength to name and about

* chore: add maxLength to treasury and delegation name

* feat: update offchain space cover

* fix: remove delegation-registry from settings

* feat: use same validation as v1, add coingecko

* feat: add offchain validation to voting settings

* feat: validate treasury uniqueness

* feat: add limit to treasuries

* feat: add updated space profile with categories

* chore: reduce margin

* chore: update coingecko entry

* chore: add cursor-pointer to Select items

* chore: remove old code
  • Loading branch information
Sekhmet authored Sep 14, 2024
1 parent 319925e commit 4627095
Show file tree
Hide file tree
Showing 30 changed files with 1,502 additions and 782 deletions.
5 changes: 5 additions & 0 deletions .changeset/smooth-moose-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@snapshot-labs/sx": patch
---

add updateSettings method to offchain EthereumSig client
1 change: 1 addition & 0 deletions apps/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"@walletconnect/utils": "^2.11.0",
"@walletconnect/web3wallet": "^1.10.0",
"ajv": "^8.12.0",
"ajv-errors": "^3.0.0",
"ajv-formats": "^2.1.1",
"autolinker": "^3.11.0",
"buffer": "^6.0.3",
Expand Down
16 changes: 13 additions & 3 deletions apps/ui/src/components/FormSpaceDelegations.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ const model = defineModel<SpaceMetadataDelegation[]>({
required: true
});
defineProps<{
limit?: number;
}>();
const modalOpen = ref(false);
const editedDelegation = ref<number | null>(null);
Expand Down Expand Up @@ -48,8 +52,8 @@ function deleteDelegation(index: number) {

<template>
<h4 v-bind="$attrs" class="eyebrow mb-2 font-medium">Delegations</h4>
<Draggable v-model="model" handle=".handle" :item-key="() => undefined"
><template #item="{ element: delegation, index: i }">
<Draggable v-model="model" handle=".handle" :item-key="() => undefined">
<template #item="{ element: delegation, index: i }">
<div
class="flex justify-between items-center rounded-lg border px-4 py-3 mb-3 text-skin-link"
>
Expand All @@ -75,7 +79,13 @@ function deleteDelegation(index: number) {
</div>
</template>
</Draggable>
<UiButton class="w-full" @click="addDelegation">Add delegation</UiButton>
<UiButton
v-if="limit ? model.length < limit : true"
class="w-full"
@click="addDelegation"
>
Add delegation
</UiButton>
<teleport to="#modal">
<ModalDelegationConfig
:open="modalOpen"
Expand Down
157 changes: 128 additions & 29 deletions apps/ui/src/components/FormSpaceProfile.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
<script setup lang="ts">
import { MAX_SYMBOL_LENGTH } from '@/helpers/constants';
import { validateForm } from '@/helpers/validation';
import { offchainNetworks } from '@/networks';
import { NetworkID } from '@/types';
const SPACE_CATEGORIES = [
{ id: 'protocol', name: 'Protocol' },
{ id: 'social', name: 'Social' },
{ id: 'investment', name: 'Investment' },
{ id: 'grant', name: 'Grant' },
{ id: 'service', name: 'Serivce' },
{ id: 'media', name: 'Media' },
{ id: 'creator', name: 'Creator' },
{ id: 'collector', name: 'Collector' }
];
const props = defineProps<{
form: any;
id?: string;
Expand All @@ -19,70 +31,129 @@ const emit = defineEmits<{
(e: 'pick', field: any);
}>();
const definition = computed(() => {
const isOffchainNetwork = computed(
() => props.space && offchainNetworks.includes(props.space.network)
);
const profileDefinition = computed(() => {
const offchainProperties = {
categories: {
type: 'array',
items: {
type: 'string',
enum: SPACE_CATEGORIES.map(c => c.id)
},
options: SPACE_CATEGORIES,
title: 'Categories',
examples: ['Select up to 2 categories'],
maxItems: 2
}
};
return {
type: 'object',
title: 'Space',
title: 'Profile',
additionalProperties: true,
required: ['name'],
properties: {
avatar: {
type: 'string',
format: 'stamp',
title: 'Avatar',
default:
props.id ||
'0x2121212121212121212121212121212121212121212121212121212121212121'
},
name: {
type: 'string',
title: 'Name',
minLength: 1,
maxLength: 32,
examples: ['Space name']
},
description: {
type: 'string',
format: 'long',
title: 'About',
maxLength: 160,
examples: ['Space description']
},
...(isOffchainNetwork.value ? offchainProperties : {})
}
};
});
const votingPowerDefinition = computed(() => ({
type: 'object',
title: 'Voting power',
additionalProperties: true,
required: [],
properties: {
symbol: {
type: 'string',
title: 'Voting power symbol',
examples: ['e.g. VP'],
maxLength: isOffchainNetwork ? 16 : MAX_SYMBOL_LENGTH,
minLength: isOffchainNetwork ? 1 : undefined
}
}
}));
const socialAccountsDefinition = computed(() => {
const onchainProperties = {
discord: {
type: 'string',
format: 'discord-handle',
title: 'Discord',
examples: ['Discord handle or invite code']
}
};
const offchainProperties = {
coingecko: {
type: 'string',
format: 'coingecko-handle',
title: 'CoinGecko handle',
examples: ['e.g. uniswap'],
maxLength: 32
}
};
return {
type: 'object',
title: 'Social accounts',
additionalProperties: true,
required: [],
properties: {
externalUrl: {
type: 'string',
format: 'uri',
title: 'Website',
examples: ['Website URL']
examples: ['Website URL'],
maxLength: 256
},
github: {
type: 'string',
format: 'github-handle',
title: 'GitHub',
examples: ['GitHub handle']
examples: ['GitHub handle'],
maxLength: 39
},
twitter: {
type: 'string',
format: 'twitter-handle',
title: 'X (Twitter)',
examples: ['X (Twitter) handle']
},
discord: {
type: 'string',
format: 'discord-handle',
title: 'Discord',
examples: ['Discord handle or invite code']
examples: ['X (Twitter) handle'],
maxLength: 15
},
votingPowerSymbol: {
type: 'string',
maxLength: MAX_SYMBOL_LENGTH,
title: 'Voting power symbol',
examples: ['e.g. VP']
}
...(isOffchainNetwork.value ? offchainProperties : onchainProperties)
}
};
});
const formErrors = computed(() =>
validateForm(definition.value, props.form, { skipEmptyOptionalFields: true })
);
const formErrors = computed(() => {
const validationOpts = {
skipEmptyOptionalFields: true
};
return {
...validateForm(profileDefinition.value, props.form, validationOpts),
...validateForm(votingPowerDefinition.value, props.form, validationOpts),
...validateForm(socialAccountsDefinition.value, props.form, validationOpts)
};
});
watch(formErrors, value => emit('errors', value));
Expand All @@ -94,6 +165,34 @@ onMounted(() => {
<template>
<UiInputStampCover v-model="(form as any).cover" :space="space" />
<div class="s-box p-4 mt-[-80px] max-w-[640px]">
<UiForm :model-value="form" :error="formErrors" :definition="definition" />
<UiInputStamp
:model-value="(form as any).avatar"
:definition="{
type: 'string',
format: 'stamp',
title: 'Avatar',
default:
props.id ||
'0x2121212121212121212121212121212121212121212121212121212121212121'
}"
/>
<h4 class="eyebrow mb-2 font-medium">Profile</h4>
<UiForm
:model-value="form"
:error="formErrors"
:definition="profileDefinition"
/>
<h4 class="eyebrow mt-4 mb-2 font-medium">Voting power</h4>
<UiForm
:model-value="form"
:error="formErrors"
:definition="votingPowerDefinition"
/>
<h4 class="eyebrow mt-4 mb-2 font-medium">Social accounts</h4>
<UiForm
:model-value="form"
:error="formErrors"
:definition="socialAccountsDefinition"
/>
</div>
</template>
27 changes: 26 additions & 1 deletion apps/ui/src/components/FormSpaceTreasuries.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,31 @@ const model = defineModel<SpaceMetadataTreasury[]>({
required: true
});
defineProps<{
limit?: number;
}>();
const { addNotification } = useUiStore();
const modalOpen = ref(false);
const editedTreasury = ref<number | null>(null);
const treasuryInitialState = ref<any | null>(null);
function addTreasuryConfig(config: SpaceMetadataTreasury) {
const existingTreasuries = new Map(
model.value.map(t => [`${t.name}-${t.network}-${t.address}`, true])
);
if (
existingTreasuries.has(`${config.name}-${config.network}-${config.address}`)
) {
return addNotification(
'error',
'Treasury with this name and wallet already exists'
);
}
const newValue = [...model.value];
if (editedTreasury.value !== null) {
Expand All @@ -22,6 +41,7 @@ function addTreasuryConfig(config: SpaceMetadataTreasury) {
}
model.value = newValue;
modalOpen.value = false;
}
function addTreasury() {
Expand Down Expand Up @@ -75,7 +95,12 @@ function deleteTreasury(index: number) {
</div>
</template>
</Draggable>
<UiButton class="w-full" @click="addTreasury">Add treasury</UiButton>
<UiButton
v-if="limit ? model.length < limit : true"
class="w-full"
@click="addTreasury"
>Add treasury</UiButton
>
<teleport to="#modal">
<ModalTreasuryConfig
:open="modalOpen"
Expand Down
1 change: 1 addition & 0 deletions apps/ui/src/components/Modal/DelegationConfig.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const definition = computed(() => {
type: 'string',
title: 'Name',
minLength: 1,
maxLength: 32,
examples: ['Delegation API name']
},
apiType: {
Expand Down
2 changes: 1 addition & 1 deletion apps/ui/src/components/Modal/TreasuryConfig.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const definition = computed(() => {
type: 'string',
title: 'Name',
minLength: 1,
maxLength: 32,
examples: ['Treasury name']
},
network: {
Expand Down Expand Up @@ -84,7 +85,6 @@ const formValid = computed(() => {
async function handleSubmit() {
emit('add', form.value);
emit('close');
}
watch(
Expand Down
6 changes: 5 additions & 1 deletion apps/ui/src/components/SpaceCover.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ const cb = computed(() => getCacheHash(props.space.cover));
:width="width"
:height="height"
:cb="cb"
type="space-cover-sx"
:type="
offchainNetworks.includes(space.network)
? 'space-cover'
: 'space-cover-sx'
"
class="object-cover !rounded-none size-full"
/>
<div
Expand Down
2 changes: 2 additions & 0 deletions apps/ui/src/components/Ui/Editable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ type Definition = {
type: 'string' | 'number' | 'integer';
format?: string;
examples?: string[];
maximum?: number;
errorMessage?: Record<string, string>;
};
const props = withDefaults(
Expand Down
Loading

0 comments on commit 4627095

Please sign in to comment.