5941207955
Backend POST /api/v1/dictionaries/{name}/records/bulk-close принимает
businessKeys[] (1..500) + reason. BulkRecordService крутит цикл через
inject'ed DictionaryRecordService — каждый close в своей транзакции
(SERIALIZABLE), partial success возможен. Result: closed[] / skipped[]
(already_closed, not_found) / errors[].
Frontend: checkbox column + sticky toolbar когда selection.size > 0.
Header checkbox с indeterminate state для partial select. selectAll
включает все ВИДИМЫЕ ключи (не игнорирует фильтры). При изменении
фильтра — useEffect выкидывает невидимые ключи из selection (WYSIWYG).
Bulk close dialog: count + reason input → confirm → результат панель
"Закрыто N, пропущено M, ошибок K" с детальным списком skipped/errors.
После успеха selection остаётся для skipped/errors — юзер видит что
ещё не сделано.
Лимит 500: защита от случайного огромного селекта или бага UI.
Дедупликация внутри batch'а silent skip — не error.
252 lines
5.4 KiB
TypeScript
252 lines
5.4 KiB
TypeScript
import axios from 'axios'
|
|
|
|
export const apiClient = axios.create({
|
|
baseURL: import.meta.env.VITE_API_BASE ?? '/api/v1',
|
|
timeout: 10_000,
|
|
})
|
|
|
|
apiClient.interceptors.request.use((config) => {
|
|
const lang = (typeof navigator !== 'undefined' && navigator.language) || 'ru-RU'
|
|
config.headers['Accept-Language'] = `${lang},ru-RU;q=0.9,en-US;q=0.8`
|
|
const token = localStorage.getItem('ordinis.token')
|
|
if (token) config.headers['Authorization'] = `Bearer ${token}`
|
|
return config
|
|
})
|
|
|
|
apiClient.interceptors.response.use(
|
|
(resp) => resp,
|
|
(error) => {
|
|
if (error.response?.status === 401) {
|
|
localStorage.removeItem('ordinis.token')
|
|
}
|
|
return Promise.reject(error)
|
|
},
|
|
)
|
|
|
|
export type DataScope = 'PUBLIC' | 'INTERNAL' | 'RESTRICTED'
|
|
|
|
export type DictionaryDefinition = {
|
|
id: string
|
|
name: string
|
|
displayName?: string
|
|
description?: string
|
|
scope: DataScope
|
|
schemaVersion: string
|
|
bundle: string
|
|
supportedLocales: string[]
|
|
defaultLocale: string
|
|
recordCount?: number
|
|
createdAt: string
|
|
updatedAt: string
|
|
}
|
|
|
|
export type JsonSchema = {
|
|
$schema?: string
|
|
type?: string
|
|
title?: string
|
|
description?: string
|
|
properties?: Record<string, JsonSchema>
|
|
required?: string[]
|
|
enum?: unknown[]
|
|
format?: string
|
|
pattern?: string
|
|
minLength?: number
|
|
maxLength?: number
|
|
minimum?: number
|
|
maximum?: number
|
|
items?: JsonSchema
|
|
additionalProperties?: boolean | JsonSchema
|
|
['x-localized']?: boolean
|
|
['x-references']?: string
|
|
['x-id-source']?: string
|
|
['x-unique']?: boolean
|
|
default?: unknown
|
|
}
|
|
|
|
export type DictionaryDetail = DictionaryDefinition & {
|
|
schemaJson: JsonSchema
|
|
}
|
|
|
|
export type FlattenedRecord = {
|
|
id: string
|
|
businessKey: string
|
|
data: Record<string, unknown>
|
|
dataScope: DataScope
|
|
validFrom: string
|
|
validTo: string
|
|
_meta?: { locale?: string }
|
|
}
|
|
|
|
export type RecordResponse = {
|
|
id: string
|
|
dictionaryId: string
|
|
businessKey: string
|
|
data: Record<string, unknown>
|
|
geometryWkt?: string
|
|
dataScope: DataScope
|
|
validFrom: string
|
|
validTo: string
|
|
createdAt: string
|
|
updatedAt: string
|
|
createdBy?: string
|
|
updatedBy?: string
|
|
version: number
|
|
}
|
|
|
|
export type CreateDictionaryRequest = {
|
|
name: string
|
|
displayName?: string
|
|
description?: string
|
|
scope: DataScope
|
|
schemaJson: JsonSchema
|
|
schemaVersion?: string
|
|
bundle?: string
|
|
supportedLocales?: string[]
|
|
defaultLocale?: string
|
|
}
|
|
|
|
export type CreateRecordRequest = {
|
|
businessKey: string
|
|
data: Record<string, unknown>
|
|
geometryWkt?: string
|
|
validFrom?: string
|
|
validTo?: string
|
|
}
|
|
|
|
export type BulkCloseRequest = {
|
|
businessKeys: string[]
|
|
at?: string
|
|
reason?: string
|
|
}
|
|
|
|
export type BulkCloseResponse = {
|
|
closed: string[]
|
|
skipped: { businessKey: string; reason: string }[]
|
|
errors: { businessKey: string; reason: string }[]
|
|
}
|
|
|
|
export type AuditAction = 'CREATE' | 'UPDATE' | 'CLOSE'
|
|
|
|
export type AuditEntry = {
|
|
id: number
|
|
eventTime: string
|
|
eventType: string
|
|
action: AuditAction | string
|
|
userId?: string | null
|
|
userScope?: DataScope | null
|
|
dictionaryName?: string | null
|
|
dictionaryId?: string | null
|
|
recordId?: string | null
|
|
businessKey?: string | null
|
|
payloadBefore?: Record<string, unknown> | null
|
|
payloadAfter?: Record<string, unknown> | null
|
|
traceId?: string | null
|
|
requestId?: string | null
|
|
ipAddress?: string | null
|
|
userAgent?: string | null
|
|
}
|
|
|
|
export type AuditPage = {
|
|
content: AuditEntry[]
|
|
totalElements: number
|
|
totalPages: number
|
|
number: number
|
|
size: number
|
|
first: boolean
|
|
last: boolean
|
|
}
|
|
|
|
export type AuditFilters = {
|
|
dictionaryName?: string
|
|
userId?: string
|
|
action?: AuditAction | ''
|
|
businessKey?: string
|
|
from?: string
|
|
to?: string
|
|
page?: number
|
|
size?: number
|
|
}
|
|
|
|
export type OutboxStats = { pending: number; dlq: number }
|
|
|
|
export type DlqEntry = {
|
|
id: number
|
|
eventType: string
|
|
dictionaryName?: string | null
|
|
aggregateId: string
|
|
kafkaTopic: string
|
|
attemptCount: number
|
|
lastError?: string | null
|
|
createdAt: string
|
|
dlqAt: string
|
|
}
|
|
|
|
export type DlqPage = {
|
|
content: DlqEntry[]
|
|
totalElements: number
|
|
totalPages: number
|
|
number: number
|
|
size: number
|
|
}
|
|
|
|
// === Webhooks (v2) ===
|
|
|
|
export type WebhookSubscription = {
|
|
id: string
|
|
name: string
|
|
url: string
|
|
scopeFilter?: DataScope[] | null
|
|
dictionaryFilter?: string[] | null
|
|
eventTypeFilter?: string[] | null
|
|
/** Masked `sk_****<last4>` или plaintext только в response создания/rotate. */
|
|
hmacSecret: string
|
|
active: boolean
|
|
createdAt: string
|
|
createdBy: string
|
|
updatedAt: string
|
|
description?: string | null
|
|
}
|
|
|
|
export type CreateWebhookSubscriptionRequest = {
|
|
name: string
|
|
url: string
|
|
scopeFilter?: DataScope[]
|
|
dictionaryFilter?: string[]
|
|
eventTypeFilter?: string[]
|
|
active?: boolean
|
|
description?: string
|
|
}
|
|
|
|
export type WebhookDeliveryStatus = 'pending' | 'retrying' | 'delivered' | 'dlq'
|
|
|
|
export type WebhookDelivery = {
|
|
id: number
|
|
subscriptionId: string
|
|
outboxEventId: number
|
|
status: WebhookDeliveryStatus
|
|
attemptCount: number
|
|
createdAt: string
|
|
nextAttemptAt: string
|
|
lastAttemptAt?: string | null
|
|
deliveredAt?: string | null
|
|
lastError?: string | null
|
|
lastStatusCode?: number | null
|
|
dlqAt?: string | null
|
|
}
|
|
|
|
export type WebhookDeliveryPage = {
|
|
content: WebhookDelivery[]
|
|
totalElements: number
|
|
totalPages: number
|
|
number: number
|
|
size: number
|
|
}
|
|
|
|
export type WebhookTestPingResult = {
|
|
/** success | failure | rejected */
|
|
status: 'success' | 'failure' | 'rejected'
|
|
httpStatus?: number | null
|
|
latencyMs?: number | null
|
|
message?: string | null
|
|
}
|