-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add Advanced settings for offchain spaces. (#789)
* feat: add parent and sub-space settings * feat: add terms of services input * feat: add customDomain and isPrivate * feat: add DeleteSpace UI * feat: add support for deleteSpace action * feat: validate parent space * feat: add tooltips * fix: handle initial value validation * fix: validate parent or sub-space is not current space * fix: prevent duplicate sub-spaces * chore: fix typo actions -> action * fix: increase checkbox label gap * fix: use v2 external link icon * fix: remove danger border if button is disabled * chore: capitalize GitHub name * fix: add default values for computedAsync * feat: use inline validation * feat: allow parent and subspaces at the same time * feat: remove description from Advanced settings page * feat: inline Message icon * fix: show errors on customDomain field * feat: add custom message for self-parent * fix: use v-show for advanced To workaround #573 * feat: add basic domain validation * feat: add error when reaching maxmimum number of subspaces * fix: only show child limit error if it's not empty
- Loading branch information
Showing
24 changed files
with
718 additions
and
40 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@snapshot-labs/sx": patch | ||
--- | ||
|
||
add deleteSpace method to offchain ethSig client |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,245 @@ | ||
<script setup lang="ts"> | ||
import { getValidator } from '@/helpers/validation'; | ||
import { NetworkID } from '@/types'; | ||
const CHILDREN_LIMIT = 16; | ||
const PARENT_SPACE_DEFINITION = { | ||
type: 'string', | ||
title: 'Main space', | ||
examples: ['pistachiodao.eth'], | ||
tooltip: | ||
'The space that this space is a sub-space of will be displayed on the space page' | ||
}; | ||
const CHILD_DEFINITION = { | ||
type: 'string', | ||
title: 'Sub-space(s)', | ||
examples: ['pistachiodao.eth'], | ||
tooltip: 'Related Sub-spaces listed here will be displayed on the space page' | ||
}; | ||
const TERMS_OF_SERVICES_DEFINITION = { | ||
type: 'string', | ||
format: 'uri', | ||
title: 'Terms of services', | ||
maxLength: 256, | ||
examples: ['https://example.com/terms'], | ||
tooltip: | ||
'Users will be required to accept these terms once before they can create a proposal or cast a vote' | ||
}; | ||
const CUSTOM_DOMAIN_DEFINITION = { | ||
type: 'string', | ||
format: 'domain', | ||
title: 'Domain name', | ||
maxLength: 64, | ||
examples: ['vote.balancer.fi'] | ||
}; | ||
const parent = defineModel<string>('parent', { required: true }); | ||
const children = defineModel<string[]>('children', { required: true }); | ||
const termsOfServices = defineModel<string>('termsOfServices', { | ||
required: true | ||
}); | ||
const customDomain = defineModel<string>('customDomain', { required: true }); | ||
const isPrivate = defineModel<boolean>('isPrivate', { required: true }); | ||
const props = defineProps<{ | ||
networkId: NetworkID; | ||
spaceId: string; | ||
isController: boolean; | ||
}>(); | ||
const emit = defineEmits<{ | ||
(e: 'updateValidity', valid: boolean): void; | ||
(e: 'deleteSpace'); | ||
}>(); | ||
const { | ||
loading: isParentLoading, | ||
validationResult: parentSpaceValidationResult, | ||
error: parentValidationError | ||
} = useSpaceInputValidation(toRef(props, 'networkId'), parent); | ||
const child = ref(''); | ||
const { | ||
loading: isChildLoading, | ||
validationResult: childValidationResult, | ||
error: childValidationError | ||
} = useSpaceInputValidation(toRef(props, 'networkId'), child); | ||
const isDeleteSpaceModalOpen = ref(false); | ||
const addSubSpaceButtonEnabled = computed(() => { | ||
if (!child.value) return false; | ||
if (childValidationResult.value === null) return true; | ||
if (childValidationResult.value.value !== child.value) return false; | ||
if (!childValidationResult.value.valid) return false; | ||
return true; | ||
}); | ||
const formErrors = computed(() => { | ||
const validator = getValidator({ | ||
type: 'object', | ||
title: 'Advanced', | ||
additionalProperties: false, | ||
required: [], | ||
properties: { | ||
parent: PARENT_SPACE_DEFINITION, | ||
child: CHILD_DEFINITION, | ||
termsOfServices: TERMS_OF_SERVICES_DEFINITION, | ||
customDomain: CUSTOM_DOMAIN_DEFINITION | ||
} | ||
}); | ||
const errors = validator.validate( | ||
{ | ||
parent: parent.value, | ||
child: child.value, | ||
termsOfServices: termsOfServices.value, | ||
customDomain: customDomain.value | ||
}, | ||
{ | ||
skipEmptyOptionalFields: true | ||
} | ||
); | ||
if (parent.value === props.spaceId) { | ||
errors.parent = 'Space cannot be a parent of itself'; | ||
} | ||
if (child.value === props.spaceId) { | ||
errors.child = 'Space cannot be a sub-space of itself'; | ||
} | ||
if (children.value.includes(child.value)) { | ||
errors.child = 'Space already configured as sub-space'; | ||
} | ||
if (child.value && children.value.length >= CHILDREN_LIMIT) { | ||
errors.child = 'Maximum number of sub-spaces reached'; | ||
} | ||
return errors; | ||
}); | ||
function addChild() { | ||
children.value.push(child.value); | ||
child.value = ''; | ||
} | ||
function deleteChild(i: number) { | ||
children.value = children.value.filter((_, index) => index !== i); | ||
} | ||
watchEffect(() => { | ||
const valid = | ||
Object.keys(formErrors.value).length === 0 && | ||
parentSpaceValidationResult.value?.valid && | ||
parentSpaceValidationResult.value?.value === parent.value; | ||
emit('updateValidity', !!valid); | ||
}); | ||
</script> | ||
|
||
<template> | ||
<h4 class="eyebrow mb-2 font-medium">Sub-spaces</h4> | ||
<UiMessage | ||
type="info" | ||
:learn-more-link="'https://docs.snapshot.org/user-guides/spaces/sub-spaces'" | ||
> | ||
Add a sub-space to display its proposals within your space. If you want the | ||
current space to be displayed on the sub-space's page, the space need to be | ||
added as main space in the subs-space settings to make relation mutual. | ||
</UiMessage> | ||
<div class="s-box my-3"> | ||
<UiInputString | ||
v-model="parent" | ||
:loading="isParentLoading" | ||
:definition="PARENT_SPACE_DEFINITION" | ||
:error="formErrors.parent ?? parentValidationError" | ||
/> | ||
<UiInputString | ||
v-model="child" | ||
:loading="isChildLoading" | ||
:definition="CHILD_DEFINITION" | ||
:error="formErrors.child ?? childValidationError" | ||
/> | ||
<UiButton | ||
v-if="children.length < CHILDREN_LIMIT" | ||
:disabled="!!formErrors.child || !addSubSpaceButtonEnabled" | ||
class="w-full" | ||
@click="addChild" | ||
> | ||
Add space | ||
</UiButton> | ||
</div> | ||
<div class="flex flex-wrap gap-2"> | ||
<div | ||
v-for="(space, i) in children" | ||
:key="space" | ||
class="flex items-center gap-2 rounded-lg border px-3 py-2 w-fit" | ||
> | ||
<span>{{ space }}</span> | ||
<button type="button" @click="deleteChild(i)"> | ||
<IH-x-mark class="w-[16px]" /> | ||
</button> | ||
</div> | ||
</div> | ||
<h4 class="eyebrow mb-2 font-medium mt-4">Terms of services</h4> | ||
<div class="s-box"> | ||
<UiInputString | ||
v-model="termsOfServices" | ||
:definition="TERMS_OF_SERVICES_DEFINITION" | ||
:error="formErrors.termsOfServices" | ||
/> | ||
</div> | ||
<h4 class="eyebrow mb-2 font-medium mt-4">Custom domain</h4> | ||
<UiMessage | ||
type="info" | ||
:learn-more-link="'https://docs.snapshot.org/spaces/add-custom-domain'" | ||
> | ||
To setup a custom domain you additionally need to open a pull request on | ||
GitHub after you have created the space. | ||
</UiMessage> | ||
<div class="s-box mt-3"> | ||
<UiInputString | ||
v-model="customDomain" | ||
:definition="CUSTOM_DOMAIN_DEFINITION" | ||
:error="formErrors.customDomain" | ||
/> | ||
<UiSwitch v-model="isPrivate" title="Hide space from homepage" /> | ||
</div> | ||
<h4 class="eyebrow mb-2 font-medium mt-4">Danger zone</h4> | ||
<div | ||
class="flex flex-col md:flex-row gap-3 md:gap-1 items-center border rounded-md px-4 py-3" | ||
> | ||
<div class="flex flex-col"> | ||
<h4 class="text-base">Delete space</h4> | ||
<span class="leading-5"> | ||
Delete this space and all its content. This cannot be undone and you | ||
will not be able to create a new space with the same ENS domain name. | ||
</span> | ||
</div> | ||
<UiButton | ||
:disabled="!isController" | ||
class="w-full md:w-auto flex-shrink-0" | ||
:class="{ | ||
'border-skin-danger !text-skin-danger': isController | ||
}" | ||
@click="isDeleteSpaceModalOpen = true" | ||
> | ||
Delete space | ||
</UiButton> | ||
</div> | ||
<teleport to="#modal"> | ||
<ModalDeleteSpace | ||
:open="isDeleteSpaceModalOpen" | ||
:space-id="spaceId" | ||
@confirm="emit('deleteSpace')" | ||
@close="isDeleteSpaceModalOpen = false" | ||
/> | ||
</teleport> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
<script setup lang="ts"> | ||
import { clone } from '@/helpers/utils'; | ||
const DEFAULT_FORM_STATE = { | ||
id: '', | ||
confirmed: false | ||
}; | ||
const props = defineProps<{ | ||
open: boolean; | ||
spaceId: string; | ||
initialState?: any; | ||
}>(); | ||
const emit = defineEmits<{ | ||
(e: 'confirm'); | ||
(e: 'close'); | ||
}>(); | ||
const form: Ref<{ | ||
id: string; | ||
confirmed: boolean; | ||
}> = ref(clone(DEFAULT_FORM_STATE)); | ||
const isValid = computed( | ||
() => form.value.confirmed && form.value.id === props.spaceId | ||
); | ||
function handleSubmit() { | ||
emit('confirm'); | ||
emit('close'); | ||
} | ||
watch( | ||
() => props.open, | ||
() => { | ||
form.value = clone(DEFAULT_FORM_STATE); | ||
} | ||
); | ||
</script> | ||
|
||
<template> | ||
<UiModal :open="open" @close="$emit('close')"> | ||
<template #header> | ||
<h3>Confirm action</h3> | ||
</template> | ||
<div class="s-box p-4"> | ||
<UiMessage type="danger"> | ||
Do you really want to delete this space? This action cannot be undone | ||
and you will not be able to use {{ spaceId }} again to create another | ||
space. | ||
</UiMessage> | ||
<UiInputString | ||
v-model="form.id" | ||
class="my-3" | ||
:definition="{ | ||
type: 'string', | ||
title: `Enter ${spaceId} to continue`, | ||
minLength: 1 | ||
}" | ||
/> | ||
<UiCheckbox | ||
v-model="form.confirmed" | ||
:title="`I acknowledge that I will not be able to use ${spaceId} again to create a new space.`" | ||
/> | ||
</div> | ||
<template #footer> | ||
<UiButton class="w-full" :disabled="!isValid" @click="handleSubmit"> | ||
Confirm | ||
</UiButton> | ||
</template> | ||
</UiModal> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
<script setup lang="ts"> | ||
import { Switch, SwitchGroup, SwitchLabel } from '@headlessui/vue'; | ||
const enabled = defineModel<boolean>({ required: true }); | ||
defineProps<{ | ||
title: string; | ||
}>(); | ||
</script> | ||
|
||
<template> | ||
<SwitchGroup> | ||
<div class="flex items-top gap-2"> | ||
<Switch | ||
v-model="enabled" | ||
:class="enabled ? 'bg-skin-primary' : 'border bg-skin-input-bg'" | ||
class="flex items-center justify-center size-[20px] shrink-0 cursor-pointer rounded-md border-skin-border transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white/75" | ||
> | ||
<IC-switch-enabled v-if="enabled" class="text-skin-bg" /> | ||
</Switch> | ||
<SwitchLabel class="leading-[18px]"> | ||
{{ title }} | ||
</SwitchLabel> | ||
</div> | ||
</SwitchGroup> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.