Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Advanced settings for offchain spaces. #789

Merged
merged 26 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
30abf9f
feat: add parent and sub-space settings
Sekhmet Sep 18, 2024
5cdae69
feat: add terms of services input
Sekhmet Sep 18, 2024
f8c2cb6
feat: add customDomain and isPrivate
Sekhmet Sep 18, 2024
5a80080
feat: add DeleteSpace UI
Sekhmet Sep 19, 2024
20356f7
feat: add support for deleteSpace action
Sekhmet Sep 19, 2024
0561109
feat: validate parent space
Sekhmet Sep 19, 2024
2c772d7
feat: add tooltips
Sekhmet Sep 19, 2024
2ead3b5
fix: handle initial value validation
Sekhmet Sep 23, 2024
545f214
fix: validate parent or sub-space is not current space
Sekhmet Sep 23, 2024
28b5e46
fix: prevent duplicate sub-spaces
Sekhmet Sep 23, 2024
ce1a5c0
chore: fix typo actions -> action
Sekhmet Sep 23, 2024
b57b05a
fix: increase checkbox label gap
Sekhmet Sep 23, 2024
51e7171
fix: use v2 external link icon
Sekhmet Sep 23, 2024
eb44f29
fix: remove danger border if button is disabled
Sekhmet Sep 23, 2024
3e2b41a
chore: capitalize GitHub name
Sekhmet Sep 23, 2024
6e8f55e
fix: add default values for computedAsync
Sekhmet Sep 23, 2024
12e2a85
feat: use inline validation
Sekhmet Sep 23, 2024
ac30ece
feat: allow parent and subspaces at the same time
Sekhmet Sep 23, 2024
2dcc465
feat: remove description from Advanced settings page
Sekhmet Sep 23, 2024
40ee852
feat: inline Message icon
Sekhmet Sep 23, 2024
138daa4
fix: show errors on customDomain field
Sekhmet Sep 23, 2024
62441c3
feat: add custom message for self-parent
Sekhmet Sep 23, 2024
c64d5a6
fix: use v-show for advanced
Sekhmet Sep 23, 2024
c088e06
feat: add basic domain validation
Sekhmet Sep 23, 2024
a0842ea
feat: add error when reaching maxmimum number of subspaces
Sekhmet Sep 23, 2024
f3c0791
fix: only show child limit error if it's not empty
Sekhmet Sep 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading