feat(vps): визуальный редактор кастомных полей и колонки в таблице
Docker / build (push) Has been cancelled
Docker / build (push) Has been cancelled
Заменён JSON-редактор в настройках на UI с drag-reorder, добавлены динамические колонки VPS с picker видимости и типизированные контракты в @cfdm/shared. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const customFieldTypeSchema = z.enum(['text', 'number', 'bool'])
|
||||
|
||||
export const customFieldDefSchema = z.object({
|
||||
key: z
|
||||
.string()
|
||||
.min(1, 'Ключ обязателен')
|
||||
.regex(/^[a-z][a-z0-9_]*$/, 'Ключ: латиница, цифры, _, начинается с буквы'),
|
||||
label: z.string().min(1, 'Название обязательно').max(80),
|
||||
type: customFieldTypeSchema.default('text'),
|
||||
})
|
||||
|
||||
export const customFieldsSchema = z.array(customFieldDefSchema)
|
||||
|
||||
export type CustomFieldType = z.infer<typeof customFieldTypeSchema>
|
||||
export type CustomFieldDef = z.infer<typeof customFieldDefSchema>
|
||||
|
||||
export function parseCustomFieldDefs(raw: unknown): CustomFieldDef[] {
|
||||
if (!Array.isArray(raw)) return []
|
||||
return raw
|
||||
.filter((item): item is Record<string, unknown> => item != null && typeof item === 'object')
|
||||
.map((item) => ({
|
||||
key: String(item.key ?? '').trim(),
|
||||
label: String(item.label ?? item.key ?? '').trim(),
|
||||
type: (item.type as CustomFieldType) ?? 'text',
|
||||
}))
|
||||
.filter((f) => f.key.length > 0)
|
||||
}
|
||||
|
||||
export function parseCustomData(raw: unknown): Record<string, string | number | boolean> {
|
||||
if (typeof raw === 'string' && raw.trim()) {
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as unknown
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||
return parsed as Record<string, string | number | boolean>
|
||||
}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
||||
return raw as Record<string, string | number | boolean>
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
export function slugifyCustomFieldKey(label: string): string {
|
||||
const translitMap: Record<string, string> = {
|
||||
а: 'a', б: 'b', в: 'v', г: 'g', д: 'd', е: 'e', ё: 'e', ж: 'zh', з: 'z',
|
||||
и: 'i', й: 'y', к: 'k', л: 'l', м: 'm', н: 'n', о: 'o', п: 'p', р: 'r',
|
||||
с: 's', т: 't', у: 'u', ф: 'f', х: 'h', ц: 'ts', ч: 'ch', ш: 'sh', щ: 'sch',
|
||||
ъ: '', ы: 'y', ь: '', э: 'e', ю: 'yu', я: 'ya',
|
||||
}
|
||||
const lower = label.trim().toLowerCase()
|
||||
let slug = ''
|
||||
for (const ch of lower) {
|
||||
if (translitMap[ch] !== undefined) {
|
||||
slug += translitMap[ch]
|
||||
} else if (/[a-z0-9]/.test(ch)) {
|
||||
slug += ch
|
||||
} else if (/\s|[-_]/.test(ch)) {
|
||||
slug += '_'
|
||||
}
|
||||
}
|
||||
slug = slug.replace(/_+/g, '_').replace(/^_|_$/g, '')
|
||||
if (!slug) return 'field'
|
||||
if (!/^[a-z]/.test(slug)) slug = `field_${slug}`
|
||||
return slug.slice(0, 40)
|
||||
}
|
||||
|
||||
export function formatCustomFieldValue(
|
||||
def: CustomFieldDef,
|
||||
value: string | number | boolean | undefined,
|
||||
): string {
|
||||
if (value === undefined || value === null || value === '') return '—'
|
||||
if (def.type === 'bool') return value ? 'Да' : 'Нет'
|
||||
if (def.type === 'number') {
|
||||
const n = Number(value)
|
||||
return Number.isFinite(n) ? String(n) : '—'
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from 'zod'
|
||||
import { customFieldsSchema } from './custom-fields.js'
|
||||
|
||||
export const settingsSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
@@ -19,7 +20,7 @@ export const settingsSchema = z.object({
|
||||
notifyVpsDownEnabled: z.boolean().optional(),
|
||||
webhookUrl: z.string().url('Невалидный URL').or(z.literal('')).optional(),
|
||||
webhookEnabled: z.boolean().optional(),
|
||||
customFields: z.any().optional(),
|
||||
customFields: customFieldsSchema.optional(),
|
||||
})
|
||||
|
||||
export type Settings = z.infer<typeof settingsSchema>
|
||||
|
||||
@@ -5,4 +5,5 @@ export * from './contracts/vps.js'
|
||||
export * from './contracts/payment.js'
|
||||
export * from './contracts/balance-ledger.js'
|
||||
export * from './contracts/settings.js'
|
||||
export * from './contracts/custom-fields.js'
|
||||
export * from './contracts/project.js'
|
||||
|
||||
Reference in New Issue
Block a user