feat: require brand description in create and update operations, add validation messages
This commit is contained in:
@@ -17,7 +17,11 @@ import { useRouter } from 'vue-router'
|
||||
|
||||
import AiWaitingModal from '@/components/AiWaitingModal.vue'
|
||||
import { brandsApi } from '@/lib/api'
|
||||
import { formatError } from '@/lib/errors'
|
||||
import {
|
||||
formatError,
|
||||
getQuestionMaterializeErrorMessage,
|
||||
isDuplicateQuestionSkipReason,
|
||||
} from '@/lib/errors'
|
||||
import { useCompanyStore } from '@/stores/company'
|
||||
|
||||
const DEFAULT_SELECTED_QUESTION_COUNT = 5
|
||||
@@ -290,6 +294,10 @@ async function submitBrand(): Promise<void> {
|
||||
message.warning(t('onboardingBrand.messages.nameRequired'))
|
||||
return
|
||||
}
|
||||
if (!description) {
|
||||
message.warning(t('onboardingBrand.messages.descriptionRequired'))
|
||||
return
|
||||
}
|
||||
if (brandLimitReached.value) {
|
||||
message.warning(
|
||||
t('brands.messages.brandLimitReached', {
|
||||
@@ -303,7 +311,7 @@ async function submitBrand(): Promise<void> {
|
||||
const brand = await createBrandMutation.mutateAsync({
|
||||
name,
|
||||
website: website || null,
|
||||
description: description || null,
|
||||
description,
|
||||
})
|
||||
createdBrand.value = brand
|
||||
brandModalOpen.value = false
|
||||
@@ -365,7 +373,14 @@ async function saveSelectedQuestions(): Promise<void> {
|
||||
questions: selectedCandidates.value.map((item) => item.text.trim()).filter(Boolean),
|
||||
})
|
||||
if (result.created_questions <= 0) {
|
||||
message.warning(t('brands.questions.errors.noValidQuestions'))
|
||||
const skippedAllDuplicates =
|
||||
result.skipped_questions.length > 0 &&
|
||||
result.skipped_questions.every((item) => isDuplicateQuestionSkipReason(item.reason))
|
||||
message.warning(
|
||||
skippedAllDuplicates
|
||||
? t('brands.questions.errors.duplicateExisting')
|
||||
: t('brands.questions.errors.noValidQuestions'),
|
||||
)
|
||||
return
|
||||
}
|
||||
message.success(
|
||||
@@ -377,8 +392,12 @@ async function saveSelectedQuestions(): Promise<void> {
|
||||
questionModalOpen.value = false
|
||||
await enterWorkspace(brand)
|
||||
} catch (error) {
|
||||
if (error instanceof ApiClientError && error.message === 'no_valid_questions') {
|
||||
message.warning(t('brands.questions.errors.noValidQuestions'))
|
||||
const materializeMessage = getQuestionMaterializeErrorMessage(error, {
|
||||
duplicate: t('brands.questions.errors.duplicateExisting'),
|
||||
noValid: t('brands.questions.errors.noValidQuestions'),
|
||||
})
|
||||
if (materializeMessage) {
|
||||
message.warning(materializeMessage)
|
||||
return
|
||||
}
|
||||
message.error(formatError(error))
|
||||
@@ -463,7 +482,7 @@ async function saveSelectedQuestions(): Promise<void> {
|
||||
<template #prefix><GlobalOutlined /></template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('onboardingBrand.form.description')">
|
||||
<a-form-item :label="t('onboardingBrand.form.description')" required>
|
||||
<a-textarea
|
||||
v-model:value="brandForm.description"
|
||||
:rows="4"
|
||||
|
||||
@@ -17,7 +17,11 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import AiWaitingModal from '@/components/AiWaitingModal.vue'
|
||||
import { brandsApi } from '@/lib/api'
|
||||
import { formatError } from '@/lib/errors'
|
||||
import {
|
||||
formatError,
|
||||
getQuestionMaterializeErrorMessage,
|
||||
isDuplicateQuestionSkipReason,
|
||||
} from '@/lib/errors'
|
||||
|
||||
type CreateMode = 'combination' | 'ai' | 'batch'
|
||||
|
||||
@@ -155,6 +159,14 @@ const existingQuestionSet = computed(() => {
|
||||
})
|
||||
|
||||
const selectedCandidates = computed(() => candidateRows.value.filter((item) => item.selected))
|
||||
const selectedCandidatesAllExistInLibrary = computed(() => {
|
||||
if (!selectedCandidates.value.length) {
|
||||
return false
|
||||
}
|
||||
return selectedCandidates.value.every((item) =>
|
||||
existingQuestionSet.value.has(normalizeQuestionKey(item.text)),
|
||||
)
|
||||
})
|
||||
const candidateStats = computed(() => {
|
||||
const total = candidateRows.value.length
|
||||
const selected = selectedCandidates.value.length
|
||||
@@ -321,11 +333,19 @@ const materializeMutation = useMutation({
|
||||
})
|
||||
return
|
||||
}
|
||||
pageNotice.value = t('brands.questions.errors.noValidQuestions')
|
||||
pageNotice.value =
|
||||
result.skipped_questions.length > 0 &&
|
||||
result.skipped_questions.every((item) => isDuplicateQuestionSkipReason(item.reason))
|
||||
? t('brands.questions.errors.duplicateExisting')
|
||||
: t('brands.questions.errors.noValidQuestions')
|
||||
},
|
||||
onError: (error) => {
|
||||
if (error instanceof ApiClientError && error.message === 'no_valid_questions') {
|
||||
pageNotice.value = t('brands.questions.errors.noValidQuestions')
|
||||
const materializeMessage = getQuestionMaterializeErrorMessage(error, {
|
||||
duplicate: t('brands.questions.errors.duplicateExisting'),
|
||||
noValid: t('brands.questions.errors.noValidQuestions'),
|
||||
})
|
||||
if (materializeMessage) {
|
||||
pageNotice.value = materializeMessage
|
||||
return
|
||||
}
|
||||
pageNotice.value = formatError(error)
|
||||
@@ -786,6 +806,10 @@ async function saveCandidates(): Promise<void> {
|
||||
pageNotice.value = t('brands.questions.errors.noSelection')
|
||||
return
|
||||
}
|
||||
if (selectedCandidatesAllExistInLibrary.value) {
|
||||
pageNotice.value = t('brands.questions.errors.duplicateExisting')
|
||||
return
|
||||
}
|
||||
await materializeMutation.mutateAsync()
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
PlusOutlined,
|
||||
ThunderboltOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { ApiClientError } from '@geo/http-client'
|
||||
import type { Brand, BrandLibrarySummary, Competitor, Question } from '@geo/shared-types'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import type { TableColumnsType } from 'ant-design-vue'
|
||||
@@ -16,7 +15,11 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { brandsApi } from '@/lib/api'
|
||||
import { formatDateTime } from '@/lib/display'
|
||||
import { formatError } from '@/lib/errors'
|
||||
import {
|
||||
formatError,
|
||||
getQuestionMaterializeErrorMessage,
|
||||
isDuplicateQuestionSkipReason,
|
||||
} from '@/lib/errors'
|
||||
import { useCompanyStore } from '@/stores/company'
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
@@ -142,7 +145,7 @@ const brandMutations = {
|
||||
brandsApi.create({
|
||||
name: brandForm.name.trim(),
|
||||
website: brandForm.website.trim() || null,
|
||||
description: brandForm.description.trim() || null,
|
||||
description: brandForm.description.trim(),
|
||||
}),
|
||||
onSuccess: async (brand) => {
|
||||
message.success(t('brands.messages.createBrand'))
|
||||
@@ -158,7 +161,7 @@ const brandMutations = {
|
||||
brandsApi.update(editingBrandId.value as number, {
|
||||
name: brandForm.name.trim(),
|
||||
website: brandForm.website.trim() || null,
|
||||
description: brandForm.description.trim() || null,
|
||||
description: brandForm.description.trim(),
|
||||
}),
|
||||
onSuccess: async () => {
|
||||
message.success(t('brands.messages.updateBrand'))
|
||||
@@ -187,7 +190,14 @@ const questionMutations = {
|
||||
}),
|
||||
onSuccess: async (result) => {
|
||||
if (result.created_questions <= 0) {
|
||||
message.warning(t('brands.questions.errors.noValidQuestions'))
|
||||
const skippedAllDuplicates =
|
||||
result.skipped_questions.length > 0 &&
|
||||
result.skipped_questions.every((item) => isDuplicateQuestionSkipReason(item.reason))
|
||||
message.warning(
|
||||
skippedAllDuplicates
|
||||
? t('brands.questions.errors.duplicateExisting')
|
||||
: t('brands.questions.errors.noValidQuestions'),
|
||||
)
|
||||
return
|
||||
}
|
||||
message.success(t('brands.messages.createQuestion'))
|
||||
@@ -196,8 +206,12 @@ const questionMutations = {
|
||||
await invalidateBrandQueries()
|
||||
},
|
||||
onError: (error) => {
|
||||
if (error instanceof ApiClientError && error.message === 'no_valid_questions') {
|
||||
message.warning(t('brands.questions.errors.noValidQuestions'))
|
||||
const materializeMessage = getQuestionMaterializeErrorMessage(error, {
|
||||
duplicate: t('brands.questions.errors.duplicateExisting'),
|
||||
noValid: t('brands.questions.errors.noValidQuestions'),
|
||||
})
|
||||
if (materializeMessage) {
|
||||
message.warning(materializeMessage)
|
||||
return
|
||||
}
|
||||
message.error(formatError(error))
|
||||
@@ -390,6 +404,10 @@ async function submitBrand(): Promise<void> {
|
||||
if (!brandForm.name.trim()) {
|
||||
return
|
||||
}
|
||||
if (!brandForm.description.trim()) {
|
||||
message.warning(t('brands.messages.brandDescriptionRequired'))
|
||||
return
|
||||
}
|
||||
if (editingBrandId.value) {
|
||||
await brandMutations.update.mutateAsync()
|
||||
return
|
||||
@@ -628,13 +646,13 @@ async function invalidateBrandQueries(): Promise<void> {
|
||||
@ok="submitBrand"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item :label="t('brands.form.brandName')">
|
||||
<a-form-item :label="t('brands.form.brandName')" required>
|
||||
<a-input v-model:value="brandForm.name" />
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('brands.form.brandWebsite')">
|
||||
<a-input v-model:value="brandForm.website" />
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('brands.form.brandDescription')">
|
||||
<a-form-item :label="t('brands.form.brandDescription')" required>
|
||||
<a-textarea v-model:value="brandForm.description" :rows="4" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
Reference in New Issue
Block a user