Skip to content

Commit

Permalink
refactor: use useStepper composable to handle space creation steps
Browse files Browse the repository at this point in the history
  • Loading branch information
wa0x6e committed Jan 20, 2025
1 parent 80a9dd4 commit 1b6147b
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 114 deletions.
104 changes: 51 additions & 53 deletions apps/ui/src/components/SpaceCreateOnchain.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,46 @@ import { enabledReadWriteNetworks, getNetwork } from '@/networks';
import { StrategyConfig } from '@/networks/types';
import { NetworkID, SpaceMetadata, SpaceSettings } from '@/types';
const PAGES = [
{
id: 'profile',
title: 'Profile'
const STEPS = {
profile: {
title: 'Profile',
isValid: () => !stepsErrors.value['profile']
},
{
id: 'network',
title: 'Network'
network: {
title: 'Network',
isValid: () => true
},
{
id: 'strategies',
title: 'Strategies'
strategies: {
title: 'Strategies',
isValid: () => votingStrategies.value.length > 0
},
{
id: 'validations',
title: 'Proposal validation'
validations: {
title: 'Proposal validation',
isValid: () => {
if (!validationStrategy.value) return false;
return validationStrategy.value.validate
? validationStrategy.value.validate(validationStrategy.value.params)
: true;
}
},
{
id: 'executions',
title: 'Executions'
executions: {
title: 'Executions',
isValid: () => true
},
{
id: 'auths',
title: 'Auths'
auths: {
title: 'Auths',
isValid: () => authenticators.value.length > 0
},
{
id: 'voting',
title: 'Voting'
voting: {
title: 'Voting',
isValid: () => !stepsErrors.value['voting']
},
{
id: 'controller',
title: 'Controller'
controller: {
title: 'Controller',
isValid: () => !stepsErrors.value['controller']
}
] as const;
} as const;
const { predictSpaceAddress } = useActions();
const { web3 } = useWeb3();
Expand Down Expand Up @@ -75,21 +81,14 @@ const controller = ref(web3.value.account);
const confirming = ref(false);
const salt: Ref<string | null> = ref(null);
const predictedSpaceAddress: Ref<string | null> = ref(null);
const stepsErrors = ref<Record<string, boolean>>({
profile: false,
voting: false,
controller: false
});
const selectedNetwork = computed(() => getNetwork(selectedNetworkId.value));
function validatePage(page: string): boolean | undefined {
if (page === 'strategies') return votingStrategies.value.length > 0;
if (page === 'auths') return authenticators.value.length > 0;
if (page === 'validations') {
if (!validationStrategy.value) return false;
return validationStrategy.value.validate
? validationStrategy.value.validate(validationStrategy.value.params)
: true;
}
}
async function handleSubmit() {
salt.value = getSalt();
predictedSpaceAddress.value = await predictSpaceAddress(
Expand All @@ -99,6 +98,10 @@ async function handleSubmit() {
confirming.value = true;
}
function handleErrors(stepName: string, errors: any) {
stepsErrors.value[stepName] = Object.values(errors).length > 0;
}
watch(
() => web3.value.account,
value => {
Expand Down Expand Up @@ -132,14 +135,9 @@ watch(selectedNetworkId, () => {
:controller="controller"
@back="confirming = false"
/>
<UiStepper
v-else
:steps="PAGES"
:validate-page-fn="validatePage"
@submit="handleSubmit"
>
<template #content="{ currentPage, handleErrors }">
<template v-if="currentPage === 'profile'">
<UiStepper v-else :steps="STEPS" @submit="handleSubmit">
<template #content="{ currentStep }">
<template v-if="currentStep === 'profile'">
<h3 class="mb-4">Space profile</h3>
<FormSpaceProfile
:form="metadataForm"
Expand All @@ -158,11 +156,11 @@ watch(selectedNetworkId, () => {
</div>
</template>
<FormNetwork
v-else-if="currentPage === 'network'"
v-else-if="currentStep === 'network'"
v-model="selectedNetworkId"
/>
<FormStrategies
v-else-if="currentPage === 'strategies'"
v-else-if="currentStep === 'strategies'"
v-model="votingStrategies"
:network-id="selectedNetworkId"
:available-strategies="
Expand All @@ -172,7 +170,7 @@ watch(selectedNetworkId, () => {
description="Voting strategies are customizable contracts used to define how much voting power each user has when casting a vote."
/>
<FormStrategies
v-else-if="currentPage === 'auths'"
v-else-if="currentStep === 'auths'"
v-model="authenticators"
unique
:network-id="selectedNetworkId"
Expand All @@ -183,7 +181,7 @@ watch(selectedNetworkId, () => {
description="Authenticators are customizable contracts that verify user identity for proposing and voting using different methods."
/>
<FormValidation
v-else-if="currentPage === 'validations'"
v-else-if="currentStep === 'validations'"
v-model="validationStrategy"
:network-id="selectedNetworkId"
:available-strategies="
Expand All @@ -197,7 +195,7 @@ watch(selectedNetworkId, () => {
description="Proposal validation strategies are used to determine if a user is allowed to create a proposal."
/>
<FormStrategies
v-else-if="currentPage === 'executions'"
v-else-if="currentStep === 'executions'"
v-model="executionStrategies"
:network-id="selectedNetworkId"
:available-strategies="
Expand All @@ -208,13 +206,13 @@ watch(selectedNetworkId, () => {
description="Execution strategies are used to determine the status of a proposal and execute its payload if it's accepted."
/>
<FormVoting
v-else-if="currentPage === 'voting'"
v-else-if="currentStep === 'voting'"
:form="settingsForm"
:selected-network-id="selectedNetworkId"
@errors="v => handleErrors('voting', v)"
/>
<FormController
v-else-if="currentPage === 'controller'"
v-else-if="currentStep === 'controller'"
v-model="controller"
@errors="v => handleErrors('controller', v)"
/>
Expand Down
86 changes: 25 additions & 61 deletions apps/ui/src/components/Ui/Stepper.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
<script lang="ts" setup>
const props = withDefaults(
defineProps<{
steps: ReadonlyArray<{ id: string; title: string }>;
validatePageFn: (pageId: string) => boolean | undefined;
steps: Record<string, { title: string; isValid: () => boolean }>;
submitting?: boolean;
}>(),
{
Expand All @@ -12,56 +11,23 @@ const props = withDefaults(
const emit = defineEmits(['submit']);
const currentPage: Ref<string> = ref(props.steps[0].id);
const pagesRefs = ref([] as HTMLElement[]);
const pagesErrors: Ref<Record<string, Record<string, string>>> = ref(
props.steps.reduce(
(acc, page) => ({ ...acc, [page.id]: {} }),
{} as Record<string, Record<string, string>>
)
);
const stepper = useStepper(props.steps);
const accessiblePages = computed(() => {
const invalidPageIndex = props.steps.findIndex(
page => !validateStep(page.id)
);
const firstInvalidStepIndex = computed(() => {
const index = Object.values(props.steps).findIndex(step => !step.isValid());
return Object.fromEntries(
props.steps.map((page, i) => [
page.id,
invalidPageIndex === -1 ? true : i <= invalidPageIndex
])
);
return index >= 0 ? index : stepper.stepNames.value.length;
});
const showCreate = computed(
() =>
props.steps.findIndex(page => page.id === currentPage.value) ===
props.steps.length - 1
);
const nextDisabled = computed(() => !validateStep(currentPage.value));
const submitDisabled = computed(() =>
props.steps.some(page => !validateStep(page.id))
Object.values(props.steps).some(step => !step.isValid())
);
function validateStep(pageId: string) {
return (
props.validatePageFn(pageId) ??
Object.values(pagesErrors.value[pageId]).length === 0
);
}
function handleErrors(pageId: string, errors: any) {
pagesErrors.value[pageId] = errors;
}
function handleNextClick() {
const currentIndex = props.steps.findIndex(
page => page.id === currentPage.value
);
if (currentIndex === props.steps.length - 1) return;
currentPage.value = props.steps[currentIndex + 1].id;
pagesRefs.value[currentIndex + 1].scrollIntoView();
function goToStep(stepName: string) {
stepper.goTo(stepName);
window.scrollTo({
top: 0
});
}
</script>

Expand All @@ -71,32 +37,30 @@ function handleNextClick() {
class="flex fixed lg:sticky top-[72px] inset-x-0 p-3 border-b z-10 bg-skin-bg lg:top-auto lg:inset-x-auto lg:p-0 lg:pr-5 lg:border-0 lg:flex-col gap-1 min-w-[180px] overflow-auto"
>
<button
v-for="page in steps"
ref="pagesRefs"
:key="page.id"
v-for="(step, stepName, i) in steps"
:key="stepName"
type="button"
:disabled="!accessiblePages[page.id]"
:disabled="i > firstInvalidStepIndex"
class="px-3 py-1 block lg:w-full rounded text-left scroll-mr-3 first:ml-auto last:mr-auto whitespace-nowrap"
:class="{
'bg-skin-active-bg': page.id === currentPage,
'hover:bg-skin-hover-bg': page.id !== currentPage,
'text-skin-link': accessiblePages[page.id]
'bg-skin-active-bg': stepper.isCurrent(stepName),
'hover:bg-skin-hover-bg': !stepper.isCurrent(stepName),
'text-skin-link': i <= firstInvalidStepIndex
}"
@click="currentPage = page.id"
@click="goToStep(stepName)"
>
{{ page.title }}
{{ step.title }}
</button>
</div>
<div class="flex-1">
<div class="mt-8 lg:mt-0">
<slot
name="content"
:current-page="currentPage"
:handle-errors="handleErrors"
:current-step="stepper.stepNames.value[stepper.index.value]"
/>
</div>
<UiButton
v-if="showCreate"
v-if="stepper.isLast.value"
class="w-full"
:loading="submitting"
:disabled="submitDisabled"
Expand All @@ -105,10 +69,10 @@ function handleNextClick() {
<slot name="submit-text"> Submit </slot>
</UiButton>
<UiButton
v-else
v-else-if="stepper.next.value"
class="w-full"
:disabled="nextDisabled"
@click="handleNextClick"
:disabled="!stepper.current.value.isValid()"
@click="goToStep(stepper.next.value)"
>
Next
</UiButton>
Expand Down

0 comments on commit 1b6147b

Please sign in to comment.