Skip to content

Commit

Permalink
feat: use inline validation
Browse files Browse the repository at this point in the history
  • Loading branch information
Sekhmet committed Sep 23, 2024
1 parent 6e8f55e commit 12e2a85
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 88 deletions.
114 changes: 43 additions & 71 deletions apps/ui/src/components/FormSpaceAdvanced.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<script setup lang="ts">
import { getValidator } from '@/helpers/validation';
import { getNetwork } from '@/networks';
import { NetworkID } from '@/types';
const CHILDREN_LIMIT = 16;
Expand All @@ -13,7 +12,7 @@ const PARENT_SPACE_DEFINITION = {
'The space that this space is a sub-space of will be displayed on the space page'
};
const SUB_SPACE_DEFINITION = {
const CHILD_DEFINITION = {
type: 'string',
title: 'Sub-space(s)',
examples: ['pistachiodao.eth'],
Expand Down Expand Up @@ -57,18 +56,32 @@ const emit = defineEmits<{
(e: 'deleteSpace');
}>();
const { addNotification } = useUiStore();
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 parentSpaceValidationResult = ref(
null as { value: string; valid: boolean } | null
);
const childInput = ref('');
const isAddingChild = ref(false);
const isDeleteSpaceModalOpen = ref(false);
const network = computed(() => getNetwork(props.networkId));
const canAddParentSpace = computed(() => children.value.length === 0);
const canAddChildSpace = computed(() => parent.value.length === 0);
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',
Expand All @@ -77,15 +90,15 @@ const formErrors = computed(() => {
required: [],
properties: {
parent: PARENT_SPACE_DEFINITION,
childInput: SUB_SPACE_DEFINITION,
child: CHILD_DEFINITION,
termsOfServices: TERMS_OF_SERVICES_DEFINITION
}
});
const errors = validator.validate(
{
parent: parent.value,
childInput: childInput.value,
child: child.value,
termsOfServices: termsOfServices.value
},
{
Expand All @@ -97,70 +110,26 @@ const formErrors = computed(() => {
errors.parent = 'Space cannot be a sub-space of itself';
}
if (childInput.value === props.spaceId) {
errors.childInput = 'Space cannot be a sub-space of itself';
if (child.value === props.spaceId) {
errors.child = 'Space cannot be a sub-space of itself';
}
if (children.value.includes(childInput.value)) {
errors.childInput = 'Space already configured as sub-space';
if (children.value.includes(child.value)) {
errors.child = 'Space already configured as sub-space';
}
return errors;
});
const parentSpaceError = computed(() => {
if (formErrors.value.parent) return formErrors.value.parent as string;
if (parentSpaceValidationResult.value === null) return null;
if (parentSpaceValidationResult.value.value !== parent.value) return null;
if (parentSpaceValidationResult.value.valid) return null;
return `Space ${parentSpaceValidationResult.value.value} not found`;
});
async function addChild() {
try {
isAddingChild.value = true;
const space = await network.value.api.loadSpace(childInput.value);
if (!space) {
throw new Error('Space not found');
}
children.value.push(childInput.value);
childInput.value = '';
} catch (e) {
addNotification('error', `Space ${childInput.value} not found`);
} finally {
isAddingChild.value = false;
}
function addChild() {
children.value.push(child.value);
child.value = '';
}
function deleteChild(i: number) {
children.value = children.value.filter((_, index) => index !== i);
}
watchDebounced(
parent,
async parent => {
if (!parent) {
parentSpaceValidationResult.value = {
value: '',
valid: true
};
return;
}
const space = await network.value.api.loadSpace(parent);
parentSpaceValidationResult.value = {
value: parent,
valid: !!space
};
},
{ debounce: 500, immediate: true }
);
watchEffect(() => {
const valid =
Object.keys(formErrors.value).length === 0 &&
Expand All @@ -185,25 +154,28 @@ watchEffect(() => {
<UiInputString
v-model="parent"
:disabled="!canAddParentSpace"
:loading="isParentLoading"
:class="{
'cursor-not-allowed': !canAddParentSpace
}"
:definition="PARENT_SPACE_DEFINITION"
:error="parentSpaceError ?? undefined"
:error="formErrors.parent ?? parentValidationError"
/>
<UiInputString
v-model="childInput"
v-model="child"
:disabled="!canAddChildSpace"
:loading="isChildLoading"
:class="{
'cursor-not-allowed': !canAddChildSpace
}"
:definition="SUB_SPACE_DEFINITION"
:error="formErrors.childInput"
:definition="CHILD_DEFINITION"
:error="formErrors.child ?? childValidationError"
/>
<UiButton
v-if="children.length < CHILDREN_LIMIT"
:disabled="!canAddChildSpace || !!formErrors.childInput"
:loading="isAddingChild"
:disabled="
!canAddChildSpace || !!formErrors.child || !addSubSpaceButtonEnabled
"
class="w-full"
@click="addChild"
>
Expand All @@ -212,11 +184,11 @@ watchEffect(() => {
</div>
<div class="flex flex-wrap gap-2">
<div
v-for="(child, i) in children"
:key="child"
v-for="(space, i) in children"
:key="space"
class="flex items-center gap-2 rounded-lg border px-3 py-2 w-fit"
>
<span>{{ child }}</span>
<span>{{ space }}</span>
<button type="button" @click="deleteChild(i)">
<IH-x-mark class="w-[16px]" />
</button>
Expand Down
2 changes: 2 additions & 0 deletions apps/ui/src/components/Ui/InputString.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default {
const model = defineModel<string>();
const props = defineProps<{
loading?: boolean;
error?: string;
definition: any;
}>();
Expand Down Expand Up @@ -37,6 +38,7 @@ watch(model, () => {
<UiWrapperInput
v-slot="{ id }"
:definition="definition"
:loading="loading"
:error="error"
:dirty="dirty"
:input-value-length="inputValue?.length"
Expand Down
38 changes: 21 additions & 17 deletions apps/ui/src/components/Ui/WrapperInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { _n } from '@/helpers/utils';
const props = withDefaults(
defineProps<{
loading?: boolean;
definition: any;
inputValueLength?: number;
error?: string;
Expand All @@ -18,25 +19,28 @@ const showError = computed(() => props.error && props.dirty);

<template>
<div class="s-base" :class="showError ? 's-error' : ''">
<div class="!flex s-label w-full gap-1">
<label
v-if="definition.title"
:for="id"
class="truncate flex items-center gap-1"
>
{{ definition.title }}
<UiTooltip v-if="definition.tooltip" :title="definition.tooltip">
<IH-question-mark-circle class="shrink-0" />
</UiTooltip>
</label>
<div
v-if="inputValueLength >= 0 && definition.maxLength"
class="text-sm hidden grow text-right s-label-char-count whitespace-nowrap"
>
{{ _n(inputValueLength) }} / {{ _n(definition.maxLength) }}
<div class="relative">
<div class="!flex justify-between s-label w-full gap-1">
<label
v-if="definition.title"
:for="id"
class="truncate flex items-center gap-1"
>
{{ definition.title }}
<UiTooltip v-if="definition.tooltip" :title="definition.tooltip">
<IH-question-mark-circle class="shrink-0" />
</UiTooltip>
</label>
<UiLoading v-if="loading" :size="16" />
<div
v-else-if="inputValueLength >= 0 && definition.maxLength"
class="text-sm hidden grow text-right s-label-char-count whitespace-nowrap"
>
{{ _n(inputValueLength) }} / {{ _n(definition.maxLength) }}
</div>
</div>
<slot :id="id" />
</div>
<slot :id="id" />
<span v-if="showError" class="s-input-error-message">{{ error }}</span>
<legend v-if="definition.description" v-text="definition.description" />
</div>
Expand Down
60 changes: 60 additions & 0 deletions apps/ui/src/composables/useSpaceInputValidation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { getNetwork } from '@/networks';
import { NetworkID } from '@/types';

type SpaceValidationResult = { value: string; valid: boolean };

export function useSpaceInputValidation(
networkId: Ref<NetworkID>,
value: Ref<string>
) {
const loading = ref(false);
const validationResult = ref(null as SpaceValidationResult | null);

const network = computed(() => getNetwork(networkId.value));
const error = computed(() => {
if (validationResult.value === null) return null;
if (validationResult.value.value !== value.value) return null;
if (validationResult.value.valid) return null;

return `Space ${validationResult.value.value} not found`;
});

watchDebounced(
value,
async value => {
loading.value = true;

try {
if (!value || value === validationResult.value?.value) {
validationResult.value = {
value,
valid: true
};
return;
}

const space = await network.value.api.loadSpace(value);
validationResult.value = {
value,
valid: !!space
};
} catch (e) {
console.error(e);

validationResult.value = {
value,
valid: false
};
} finally {
loading.value = false;
}
},
{ debounce: 500, immediate: true }
);

return {
loading,
validationResult,
error
};
}

0 comments on commit 12e2a85

Please sign in to comment.