feat(form-builder): form schema types and TanStack Vue Query composables

Adds apps/app/src/types/formSchema.ts with FormSchema, FormSchemaSummary,
FormSchemaPurpose, FormSubmissionMode, FormSchemaSnapshotMode, and the
payload/response shapes for schema CRUD plus lifecycle operations
(publish, unpublish, duplicate, rotate-public-token).

Adds apps/app/src/composables/api/useFormSchemas.ts mirroring the
useSections pattern: useFormSchemaList, useFormSchema, plus seven
mutations covering CRUD, duplicate, publish/unpublish and public-token
rotation. All queries and mutations invalidate the right cache keys.

Fields and sections on the full FormSchema are typed as unknown[] with
a TODO pointing to PR-b3 when the organizer field types land. No UI,
routes, or navigation — those come in PR-b2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-24 01:52:44 +02:00
parent 214a2debee
commit 7df37b8823
2 changed files with 368 additions and 0 deletions

View File

@@ -0,0 +1,177 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import type { Ref } from 'vue'
import { apiClient } from '@/lib/axios'
import type {
CreateFormSchemaPayload,
FormSchema,
FormSchemaSummary,
RotatePublicTokenPayload,
RotatePublicTokenResponse,
UpdateFormSchemaPayload,
} from '@/types/formSchema'
// Form schemas are organisation-scoped on the backend (see
// api/routes/api.php — /organisations/{organisation}/forms/schemas).
// There is no event in the path or controller signature; a single
// schema is reusable across all events of an organisation.
interface ApiResponse<T> {
success: boolean
data: T
message?: string
}
interface PaginatedResponse<T> {
data: T[]
}
export function useFormSchemaList(orgId: Ref<string>) {
return useQuery({
queryKey: ['form-schemas', orgId],
queryFn: async () => {
const { data } = await apiClient.get<PaginatedResponse<FormSchemaSummary>>(
`/organisations/${orgId.value}/forms/schemas`,
)
return data.data
},
enabled: () => !!orgId.value,
})
}
export function useFormSchema(orgId: Ref<string>, schemaId: Ref<string>) {
return useQuery({
queryKey: ['form-schemas', orgId, schemaId],
queryFn: async () => {
const { data } = await apiClient.get<ApiResponse<FormSchema>>(
`/organisations/${orgId.value}/forms/schemas/${schemaId.value}`,
)
return data.data
},
enabled: () => !!orgId.value && !!schemaId.value,
})
}
export function useCreateFormSchema(orgId: Ref<string>) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (payload: CreateFormSchemaPayload): Promise<FormSchema> => {
const { data } = await apiClient.post<ApiResponse<FormSchema>>(
`/organisations/${orgId.value}/forms/schemas`,
payload,
)
return data.data
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['form-schemas', orgId.value] })
},
})
}
export function useUpdateFormSchema(orgId: Ref<string>) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ id, ...payload }: UpdateFormSchemaPayload & { id: string }): Promise<FormSchema> => {
const { data } = await apiClient.put<ApiResponse<FormSchema>>(
`/organisations/${orgId.value}/forms/schemas/${id}`,
payload,
)
return data.data
},
onSuccess: (_updated, variables) => {
queryClient.invalidateQueries({ queryKey: ['form-schemas', orgId.value] })
queryClient.invalidateQueries({ queryKey: ['form-schemas', orgId.value, variables.id] })
},
})
}
export function useDeleteFormSchema(orgId: Ref<string>) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ id, confirmed_name }: { id: string; confirmed_name?: string }) => {
await apiClient.delete(`/organisations/${orgId.value}/forms/schemas/${id}`, {
params: confirmed_name ? { confirmed_name } : undefined,
})
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['form-schemas', orgId.value] })
},
})
}
export function useDuplicateFormSchema(orgId: Ref<string>) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (id: string): Promise<FormSchema> => {
const { data } = await apiClient.post<ApiResponse<FormSchema>>(
`/organisations/${orgId.value}/forms/schemas/${id}/duplicate`,
)
return data.data
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['form-schemas', orgId.value] })
},
})
}
export function usePublishFormSchema(orgId: Ref<string>) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (id: string): Promise<FormSchema> => {
const { data } = await apiClient.post<ApiResponse<FormSchema>>(
`/organisations/${orgId.value}/forms/schemas/${id}/publish`,
)
return data.data
},
onSuccess: (_updated, id) => {
queryClient.invalidateQueries({ queryKey: ['form-schemas', orgId.value] })
queryClient.invalidateQueries({ queryKey: ['form-schemas', orgId.value, id] })
},
})
}
export function useUnpublishFormSchema(orgId: Ref<string>) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (id: string): Promise<FormSchema> => {
const { data } = await apiClient.post<ApiResponse<FormSchema>>(
`/organisations/${orgId.value}/forms/schemas/${id}/unpublish`,
)
return data.data
},
onSuccess: (_updated, id) => {
queryClient.invalidateQueries({ queryKey: ['form-schemas', orgId.value] })
queryClient.invalidateQueries({ queryKey: ['form-schemas', orgId.value, id] })
},
})
}
export function useRotatePublicToken(orgId: Ref<string>) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ id, ...payload }: RotatePublicTokenPayload & { id: string }): Promise<RotatePublicTokenResponse> => {
const { data } = await apiClient.post<ApiResponse<FormSchema>>(
`/organisations/${orgId.value}/forms/schemas/${id}/rotate-public-token`,
payload,
)
return data.data
},
onSuccess: (_updated, variables) => {
queryClient.invalidateQueries({ queryKey: ['form-schemas', orgId.value, variables.id] })
},
})
}