Skip to content

Commit

Permalink
feat: add Advanced settings for offchain spaces. (#789)
Browse files Browse the repository at this point in the history
* 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
Sekhmet authored Sep 24, 2024
1 parent 78b961c commit aa87e0c
Show file tree
Hide file tree
Showing 24 changed files with 718 additions and 40 deletions.
5 changes: 5 additions & 0 deletions .changeset/sweet-papayas-provide.md
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
3 changes: 3 additions & 0 deletions apps/ui/src/assets/icons/switch-disabled.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions apps/ui/src/assets/icons/switch-enabled.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
245 changes: 245 additions & 0 deletions apps/ui/src/components/FormSpaceAdvanced.vue
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>
73 changes: 73 additions & 0 deletions apps/ui/src/components/Modal/DeleteSpace.vue
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>
26 changes: 26 additions & 0 deletions apps/ui/src/components/Ui/Checkbox.vue
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>
13 changes: 10 additions & 3 deletions apps/ui/src/components/Ui/ContainerSettings.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
<script setup lang="ts">
defineProps<{
title: string;
description: string;
description?: string;
}>();
</script>

<template>
<div class="mb-4">
<h3 class="text-md leading-6">{{ title }}</h3>
<span class="mb-4 inline-block">
<h3
class="text-md leading-6"
:class="{
'mb-4': !description
}"
>
{{ title }}
</h3>
<span v-if="description" class="mb-4 inline-block">
{{ description }}
</span>
<slot />
Expand Down
Loading

0 comments on commit aa87e0c

Please sign in to comment.