feat: добавить workflow управления asset в admin
- добавлены сценарии создания source version и generation jobs\n- добавлены modal формы source version и variants generation\n- обновлена detail panel variants actions и ссылки на public URL
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { useAssetOverview } from "./hooks/use-asset-overview.hook"
|
||||
import { useAssetsDashboard } from "./hooks/use-assets-dashboard.hook"
|
||||
import { useCreateAsset } from "./hooks/use-create-asset.hook"
|
||||
import { useCreateAssetVersion } from "./hooks/use-create-asset-version.hook"
|
||||
import { useGenerateAssetVariants } from "./hooks/use-generate-asset-variants.hook"
|
||||
import type { AssetsFactory } from "./types/assets-factory.type"
|
||||
|
||||
/**
|
||||
@@ -11,5 +13,7 @@ export const assetsFactory: AssetsFactory = () => {
|
||||
useAssetOverview,
|
||||
useAssetsDashboard,
|
||||
useCreateAsset,
|
||||
useCreateAssetVersion,
|
||||
useGenerateAssetVariants,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useState } from "react"
|
||||
import { useSWRConfig } from "swr"
|
||||
import { backendApi, getAssetKey, getAssetVariantsKey, getAssetsListKey } from "infra/backend-api"
|
||||
|
||||
import { ASSETS_DASHBOARD_LIST_PARAMS } from "../config/assets.config"
|
||||
import { toError } from "../lib/to-error"
|
||||
import type { CreateAssetVersionAction, CreateAssetVersionInput } from "../types/assets-api.type"
|
||||
|
||||
/**
|
||||
* Сценарий создания новой source version.
|
||||
*/
|
||||
export const useCreateAssetVersion = (): CreateAssetVersionAction => {
|
||||
const { mutate } = useSWRConfig()
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
|
||||
const createAssetVersion = async (input: CreateAssetVersionInput) => {
|
||||
setError(null)
|
||||
setIsCreating(true)
|
||||
|
||||
try {
|
||||
const createdVersion = await backendApi.assets.createAssetVersion(
|
||||
{ publicId: input.publicId },
|
||||
{ sourceUrl: input.sourceUrl },
|
||||
)
|
||||
|
||||
await Promise.all([
|
||||
mutate(getAssetsListKey(ASSETS_DASHBOARD_LIST_PARAMS)),
|
||||
mutate(getAssetKey(input.publicId)),
|
||||
mutate(getAssetVariantsKey(input.publicId, String(createdVersion.version))),
|
||||
])
|
||||
|
||||
return createdVersion
|
||||
} catch (caughtError) {
|
||||
const nextError = toError(caughtError)
|
||||
setError(nextError)
|
||||
throw nextError
|
||||
} finally {
|
||||
setIsCreating(false)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
createAssetVersion,
|
||||
error,
|
||||
isCreating,
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,9 @@ import { useSWRConfig } from "swr"
|
||||
import { backendApi, getAssetsListKey } from "infra/backend-api"
|
||||
|
||||
import { ASSETS_DASHBOARD_LIST_PARAMS } from "../config/assets.config"
|
||||
import { toError } from "../lib/to-error"
|
||||
import type { CreateAssetAction, CreateAssetInput } from "../types/assets-api.type"
|
||||
|
||||
const toError = (error: unknown) => (error instanceof Error ? error : new Error("Неизвестная ошибка"))
|
||||
|
||||
/**
|
||||
* Сценарий создания asset с обновлением списка.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useState } from "react"
|
||||
import { useSWRConfig } from "swr"
|
||||
import {
|
||||
backendApi,
|
||||
getAssetKey,
|
||||
getAssetVariantsKey,
|
||||
getAssetsListKey,
|
||||
type CreateAssetVariantsRequestDto,
|
||||
} from "infra/backend-api"
|
||||
|
||||
import { ASSETS_DASHBOARD_LIST_PARAMS } from "../config/assets.config"
|
||||
import { toError } from "../lib/to-error"
|
||||
import type { GenerateAssetVariantsAction, GenerateAssetVariantsInput } from "../types/assets-api.type"
|
||||
|
||||
const toGeneratedRequest = (input: GenerateAssetVariantsInput): CreateAssetVariantsRequestDto => {
|
||||
return {
|
||||
preset: input.preset,
|
||||
format: input.format,
|
||||
formats: input.formats,
|
||||
height: input.height,
|
||||
mode: input.mode,
|
||||
quality: input.quality,
|
||||
resize: input.resize,
|
||||
version: input.version,
|
||||
width: input.width,
|
||||
} as CreateAssetVariantsRequestDto
|
||||
}
|
||||
|
||||
/**
|
||||
* Сценарий постановки generation jobs для variants.
|
||||
*/
|
||||
export const useGenerateAssetVariants = (): GenerateAssetVariantsAction => {
|
||||
const { mutate } = useSWRConfig()
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
const [isGenerating, setIsGenerating] = useState(false)
|
||||
|
||||
const generateAssetVariants = async (input: GenerateAssetVariantsInput) => {
|
||||
setError(null)
|
||||
setIsGenerating(true)
|
||||
|
||||
try {
|
||||
const response = await backendApi.assets.createAssetVariants(
|
||||
{ publicId: input.publicId },
|
||||
toGeneratedRequest(input),
|
||||
)
|
||||
|
||||
await Promise.all([
|
||||
mutate(getAssetsListKey(ASSETS_DASHBOARD_LIST_PARAMS)),
|
||||
mutate(getAssetKey(input.publicId)),
|
||||
mutate(getAssetVariantsKey(input.publicId, String(response.version))),
|
||||
])
|
||||
|
||||
return response
|
||||
} catch (caughtError) {
|
||||
const nextError = toError(caughtError)
|
||||
setError(nextError)
|
||||
throw nextError
|
||||
} finally {
|
||||
setIsGenerating(false)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
error,
|
||||
generateAssetVariants,
|
||||
isGenerating,
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,14 @@ export type {
|
||||
AssetOverview,
|
||||
AssetsApi,
|
||||
AssetsDashboard,
|
||||
AssetVariantFormat,
|
||||
AssetVariantMode,
|
||||
AssetVariantResize,
|
||||
CreateAssetAction,
|
||||
CreateAssetInput,
|
||||
CreateAssetVersionAction,
|
||||
CreateAssetVersionInput,
|
||||
GenerateAssetVariantsAction,
|
||||
GenerateAssetVariantsInput,
|
||||
} from "./types/assets-api.type"
|
||||
export type { AssetsFactory } from "./types/assets-factory.type"
|
||||
|
||||
2
apps/admin/src/business/assets/lib/to-error.ts
Normal file
2
apps/admin/src/business/assets/lib/to-error.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const toError = (error: unknown) =>
|
||||
error instanceof Error ? error : new Error("Неизвестная ошибка")
|
||||
@@ -3,10 +3,16 @@ import type {
|
||||
AssetVariantResponseDto,
|
||||
CreateAssetRequestDto,
|
||||
CreateAssetResponseDto,
|
||||
CreateAssetVersionResponseDto,
|
||||
CreateAssetVariantsResponseDto,
|
||||
PresetResponseDto,
|
||||
PresetsResponseDto,
|
||||
} from "infra/backend-api"
|
||||
|
||||
export type AssetVariantFormat = "avif" | "jpg" | "png" | "webp"
|
||||
export type AssetVariantMode = "family" | "single"
|
||||
export type AssetVariantResize = "fill" | "fit"
|
||||
|
||||
export type AssetOverview = {
|
||||
asset: AssetResponseDto | null
|
||||
error?: Error
|
||||
@@ -36,6 +42,36 @@ export type CreateAssetAction = {
|
||||
isCreating: boolean
|
||||
}
|
||||
|
||||
export type CreateAssetVersionInput = {
|
||||
publicId: string
|
||||
sourceUrl: string
|
||||
}
|
||||
|
||||
export type CreateAssetVersionAction = {
|
||||
createAssetVersion: (input: CreateAssetVersionInput) => Promise<CreateAssetVersionResponseDto>
|
||||
error: Error | null
|
||||
isCreating: boolean
|
||||
}
|
||||
|
||||
export type GenerateAssetVariantsInput = {
|
||||
format?: AssetVariantFormat
|
||||
formats?: AssetVariantFormat[]
|
||||
height?: number
|
||||
mode: AssetVariantMode
|
||||
preset: string
|
||||
publicId: string
|
||||
quality?: number
|
||||
resize?: AssetVariantResize
|
||||
version?: number
|
||||
width?: number
|
||||
}
|
||||
|
||||
export type GenerateAssetVariantsAction = {
|
||||
error: Error | null
|
||||
generateAssetVariants: (input: GenerateAssetVariantsInput) => Promise<CreateAssetVariantsResponseDto>
|
||||
isGenerating: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Публичный runtime API бизнес-модуля Assets.
|
||||
*/
|
||||
@@ -43,4 +79,6 @@ export type AssetsApi = {
|
||||
useAssetOverview: (publicId: string | null) => AssetOverview
|
||||
useAssetsDashboard: () => AssetsDashboard
|
||||
useCreateAsset: () => CreateAssetAction
|
||||
useCreateAssetVersion: () => CreateAssetVersionAction
|
||||
useGenerateAssetVariants: () => GenerateAssetVariantsAction
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@ export type {
|
||||
AssetsListResponseDto,
|
||||
CreateAssetRequestDto,
|
||||
CreateAssetResponseDto,
|
||||
CreateAssetVersionRequestDto,
|
||||
CreateAssetVersionResponseDto,
|
||||
CreateAssetVariantsRequestDto,
|
||||
CreateAssetVariantsResponseDto,
|
||||
CustomTransformConfigResponseDto,
|
||||
ListAssetsParams,
|
||||
PresetResponseDto,
|
||||
|
||||
@@ -8,6 +8,8 @@ import { DASHBOARD_PIPELINE } from "./config/dashboard.config"
|
||||
import { AssetDetailPanel } from "./parts/asset-detail-panel"
|
||||
import { AssetsTable } from "./parts/assets-table"
|
||||
import { CreateAssetModal } from "./parts/create-asset-modal"
|
||||
import { CreateSourceVersionModal } from "./parts/create-source-version-modal"
|
||||
import { GenerateVariantsModal } from "./parts/generate-variants-modal"
|
||||
import { PresetsPanel } from "./parts/presets-panel"
|
||||
import { SummaryCards } from "./parts/summary-cards"
|
||||
import styles from "./styles/dashboard.module.css"
|
||||
@@ -26,8 +28,12 @@ export const DashboardScreen = (props: DashboardScreenProps) => {
|
||||
const { className, ...rootAttrs } = props
|
||||
const [selectedPublicId, setSelectedPublicId] = useState<string | null>(null)
|
||||
const [isCreateAssetOpen, createAssetModal] = useDisclosure(false)
|
||||
const [isCreateVersionOpen, createVersionModal] = useDisclosure(false)
|
||||
const [isGenerateVariantsOpen, generateVariantsModal] = useDisclosure(false)
|
||||
const dashboard = assets.useAssetsDashboard()
|
||||
const createAsset = assets.useCreateAsset()
|
||||
const createAssetVersion = assets.useCreateAssetVersion()
|
||||
const generateAssetVariants = assets.useGenerateAssetVariants()
|
||||
const effectivePublicId = selectedPublicId ?? dashboard.assets[0]?.publicId ?? null
|
||||
const overview = assets.useAssetOverview(effectivePublicId)
|
||||
|
||||
@@ -74,7 +80,12 @@ export const DashboardScreen = (props: DashboardScreenProps) => {
|
||||
onSelect={setSelectedPublicId}
|
||||
selectedPublicId={effectivePublicId}
|
||||
/>
|
||||
<AssetDetailPanel overview={overview} publicId={effectivePublicId} />
|
||||
<AssetDetailPanel
|
||||
onCreateVersion={createVersionModal.open}
|
||||
onGenerateVariants={generateVariantsModal.open}
|
||||
overview={overview}
|
||||
publicId={effectivePublicId}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PresetsPanel
|
||||
@@ -91,6 +102,24 @@ export const DashboardScreen = (props: DashboardScreenProps) => {
|
||||
onCreated={setSelectedPublicId}
|
||||
opened={isCreateAssetOpen}
|
||||
/>
|
||||
|
||||
<CreateSourceVersionModal
|
||||
action={createAssetVersion}
|
||||
onClose={createVersionModal.close}
|
||||
onCreated={setSelectedPublicId}
|
||||
opened={isCreateVersionOpen}
|
||||
publicId={effectivePublicId}
|
||||
/>
|
||||
|
||||
<GenerateVariantsModal
|
||||
action={generateAssetVariants}
|
||||
asset={overview.asset}
|
||||
custom={dashboard.custom}
|
||||
onClose={generateVariantsModal.close}
|
||||
onGenerated={setSelectedPublicId}
|
||||
opened={isGenerateVariantsOpen}
|
||||
presets={dashboard.presets}
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Anchor, Badge, Code, Group, Paper, ScrollArea, Skeleton, Stack, Table, Text, Title } from "@mantine/core"
|
||||
import { Anchor, Badge, Button, Code, Group, Paper, ScrollArea, Skeleton, Stack, Table, Text, Title } from "@mantine/core"
|
||||
|
||||
import { ASSET_STATUS_COLORS, VARIANT_STATUS_COLORS } from "../../config/dashboard.config"
|
||||
import { formatDateTime } from "../../lib/format-date"
|
||||
@@ -12,7 +12,7 @@ import type { AssetDetailPanelProps } from "./types/asset-detail-panel-props.typ
|
||||
* - отображения статусов generated variants
|
||||
*/
|
||||
export const AssetDetailPanel = (props: AssetDetailPanelProps) => {
|
||||
const { overview, publicId } = props
|
||||
const { onCreateVersion, onGenerateVariants, overview, publicId } = props
|
||||
const { asset, variants } = overview
|
||||
|
||||
if (!publicId) {
|
||||
@@ -41,9 +41,17 @@ export const AssetDetailPanel = (props: AssetDetailPanelProps) => {
|
||||
</div>
|
||||
|
||||
{asset ? (
|
||||
<Group gap="xs">
|
||||
<Button onClick={onCreateVersion} radius="xl" size="xs" variant="light">
|
||||
New source version
|
||||
</Button>
|
||||
<Button onClick={onGenerateVariants} radius="xl" size="xs">
|
||||
Generate variants
|
||||
</Button>
|
||||
<Badge color={ASSET_STATUS_COLORS[asset.status] ?? "gray"} radius="xl" variant="light">
|
||||
{asset.status}
|
||||
</Badge>
|
||||
</Group>
|
||||
) : null}
|
||||
</Group>
|
||||
|
||||
@@ -91,6 +99,7 @@ export const AssetDetailPanel = (props: AssetDetailPanelProps) => {
|
||||
<Table.Th>Format</Table.Th>
|
||||
<Table.Th>Size</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>URL</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
@@ -108,6 +117,11 @@ export const AssetDetailPanel = (props: AssetDetailPanelProps) => {
|
||||
{variant.status}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Anchor href={variant.url} target="_blank">
|
||||
open
|
||||
</Anchor>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
|
||||
@@ -6,6 +6,10 @@ import type { AssetOverview } from "business/assets"
|
||||
export type AssetDetailPanelProps = {
|
||||
/** Данные выбранного asset. */
|
||||
overview: AssetOverview
|
||||
/** Callback открытия modal новой source version. */
|
||||
onCreateVersion: () => void
|
||||
/** Callback открытия modal генерации variants. */
|
||||
onGenerateVariants: () => void
|
||||
/** Выбранный publicId. */
|
||||
publicId: string | null
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { Button, Group, Modal, Stack, Text, TextInput } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { notifications } from "@mantine/notifications"
|
||||
|
||||
import type { CreateSourceVersionModalProps } from "./types/create-source-version-modal-props.type"
|
||||
|
||||
type CreateSourceVersionFormValues = {
|
||||
sourceUrl: string
|
||||
}
|
||||
|
||||
const SOURCE_URL_EXAMPLE = "https://storage.yandexcloud.net/shared1318/img/1.jpg"
|
||||
|
||||
const toErrorMessage = (error: unknown) => (error instanceof Error ? error.message : "Неизвестная ошибка")
|
||||
|
||||
/**
|
||||
* Modal создания новой source version.
|
||||
*
|
||||
* Используется для:
|
||||
* - регистрации нового source URL
|
||||
* - обновления immutable version выбранного asset
|
||||
*/
|
||||
export const CreateSourceVersionModal = (props: CreateSourceVersionModalProps) => {
|
||||
const { action, onClose, onCreated, opened, publicId } = props
|
||||
|
||||
const form = useForm<CreateSourceVersionFormValues>({
|
||||
initialValues: {
|
||||
sourceUrl: SOURCE_URL_EXAMPLE,
|
||||
},
|
||||
validate: {
|
||||
sourceUrl: (value) => {
|
||||
if (!value.trim()) {
|
||||
return "Укажите source URL"
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(value)
|
||||
return url.protocol === "http:" || url.protocol === "https:" ? null : "URL должен быть http/https"
|
||||
} catch {
|
||||
return "Некорректный URL"
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const handleClose = () => {
|
||||
if (!action.isCreating) {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = form.onSubmit(async (values) => {
|
||||
if (!publicId) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const createdVersion = await action.createAssetVersion({
|
||||
publicId,
|
||||
sourceUrl: values.sourceUrl.trim(),
|
||||
})
|
||||
notifications.show({
|
||||
color: "green",
|
||||
message: `Asset ${createdVersion.publicId} обновлён до v${createdVersion.version}`,
|
||||
title: "Source version created",
|
||||
})
|
||||
form.reset()
|
||||
onCreated(createdVersion.publicId)
|
||||
onClose()
|
||||
} catch (error) {
|
||||
notifications.show({
|
||||
color: "red",
|
||||
message: toErrorMessage(error),
|
||||
title: "Не удалось создать source version",
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Modal centered onClose={handleClose} opened={opened} radius="xl" title="New source version">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Stack gap="md">
|
||||
<Text c="dimmed" fz="sm">
|
||||
Новая source version изменит currentVersion asset. Старые public URLs останутся immutable.
|
||||
</Text>
|
||||
|
||||
<TextInput label="Asset" readOnly value={publicId ?? ""} />
|
||||
|
||||
<TextInput
|
||||
disabled={action.isCreating}
|
||||
label="New source URL"
|
||||
placeholder={SOURCE_URL_EXAMPLE}
|
||||
required
|
||||
{...form.getInputProps("sourceUrl")}
|
||||
/>
|
||||
|
||||
<Group justify="flex-end">
|
||||
<Button color="gray" disabled={action.isCreating} onClick={handleClose} variant="subtle">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button disabled={!publicId} loading={action.isCreating} type="submit">
|
||||
Create version
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { CreateSourceVersionModal } from "./create-source-version-modal"
|
||||
export type { CreateSourceVersionModalProps } from "./types/create-source-version-modal-props.type"
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { CreateAssetVersionAction } from "business/assets"
|
||||
|
||||
/**
|
||||
* Параметры CreateSourceVersionModal.
|
||||
*/
|
||||
export type CreateSourceVersionModalProps = {
|
||||
/** Сценарий создания source version. */
|
||||
action: CreateAssetVersionAction
|
||||
/** Callback закрытия modal. */
|
||||
onClose: () => void
|
||||
/** Callback успешного создания version. */
|
||||
onCreated: (publicId: string) => void
|
||||
/** Открыта ли modal. */
|
||||
opened: boolean
|
||||
/** Публичный идентификатор выбранного asset. */
|
||||
publicId: string | null
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
import { Button, Group, Modal, MultiSelect, NumberInput, Select, SegmentedControl, Stack, Text } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { notifications } from "@mantine/notifications"
|
||||
|
||||
import type {
|
||||
AssetVariantFormat,
|
||||
AssetVariantMode,
|
||||
AssetVariantResize,
|
||||
GenerateAssetVariantsInput,
|
||||
} from "business/assets"
|
||||
import type { GenerateVariantsModalProps } from "./types/generate-variants-modal-props.type"
|
||||
|
||||
type NumberFieldValue = number | string
|
||||
|
||||
type GenerateVariantsFormValues = {
|
||||
format: AssetVariantFormat
|
||||
formats: AssetVariantFormat[]
|
||||
height: NumberFieldValue
|
||||
mode: AssetVariantMode
|
||||
preset: string
|
||||
quality: NumberFieldValue
|
||||
resize: AssetVariantResize
|
||||
version: NumberFieldValue
|
||||
width: NumberFieldValue
|
||||
}
|
||||
|
||||
const DEFAULT_FORMAT: AssetVariantFormat = "webp"
|
||||
const DEFAULT_RESIZE: AssetVariantResize = "fit"
|
||||
|
||||
const FORMAT_OPTIONS: AssetVariantFormat[] = ["avif", "webp", "jpg", "png"]
|
||||
const RESIZE_OPTIONS: AssetVariantResize[] = ["fit", "fill"]
|
||||
|
||||
const toErrorMessage = (error: unknown) => (error instanceof Error ? error.message : "Неизвестная ошибка")
|
||||
|
||||
const toOptionalNumber = (value: NumberFieldValue) => {
|
||||
if (value === "") {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const parsedValue = Number(value)
|
||||
|
||||
return Number.isFinite(parsedValue) ? parsedValue : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal постановки generation jobs для variants.
|
||||
*
|
||||
* Используется для:
|
||||
* - генерации family static preset
|
||||
* - генерации single/custom variant
|
||||
*/
|
||||
export const GenerateVariantsModal = (props: GenerateVariantsModalProps) => {
|
||||
const { action, asset, custom, onClose, onGenerated, opened, presets } = props
|
||||
|
||||
const firstPreset = presets[0]?.name ?? ""
|
||||
const form = useForm<GenerateVariantsFormValues>({
|
||||
initialValues: {
|
||||
format: DEFAULT_FORMAT,
|
||||
formats: [],
|
||||
height: "",
|
||||
mode: "family",
|
||||
preset: firstPreset,
|
||||
quality: "",
|
||||
resize: DEFAULT_RESIZE,
|
||||
version: "",
|
||||
width: "",
|
||||
},
|
||||
validate: {
|
||||
preset: (value) => (value ? null : "Выберите preset"),
|
||||
width: (value, values) => {
|
||||
const selectedPreset = presets.find((preset) => preset.name === values.preset)
|
||||
const needsWidth = values.mode === "single" && (values.preset === "custom" || selectedPreset?.mode === "responsive")
|
||||
|
||||
return needsWidth && !toOptionalNumber(value) ? "Укажите width" : null
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const selectedPreset = presets.find((preset) => preset.name === form.values.preset)
|
||||
const isCustom = form.values.preset === "custom"
|
||||
const availableFormats = isCustom ? (custom?.formats ?? []) : (selectedPreset?.formats ?? FORMAT_OPTIONS)
|
||||
const presetOptions = [
|
||||
...presets.map((preset) => ({
|
||||
label: `${preset.name} (${preset.mode})`,
|
||||
value: preset.name,
|
||||
})),
|
||||
...(custom?.enabled ? [{ label: "custom", value: "custom" }] : []),
|
||||
]
|
||||
const formatOptions = availableFormats.map((format) => ({ label: format, value: format }))
|
||||
const widthHint = selectedPreset?.widths?.length ? `Allowed: ${selectedPreset.widths.join(", ")}` : undefined
|
||||
|
||||
const handleClose = () => {
|
||||
if (!action.isGenerating) {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = form.onSubmit(async (values) => {
|
||||
if (!asset) {
|
||||
return
|
||||
}
|
||||
|
||||
if (values.mode === "family" && isCustom) {
|
||||
notifications.show({
|
||||
color: "red",
|
||||
message: "Custom transform поддерживает только single generation.",
|
||||
title: "Некорректный режим",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const input: GenerateAssetVariantsInput = {
|
||||
mode: values.mode,
|
||||
preset: values.preset,
|
||||
publicId: asset.publicId,
|
||||
...(toOptionalNumber(values.version) ? { version: toOptionalNumber(values.version) } : {}),
|
||||
}
|
||||
|
||||
if (values.mode === "family") {
|
||||
input.formats = values.formats.length > 0 ? values.formats : undefined
|
||||
} else {
|
||||
input.format = values.format
|
||||
input.height = toOptionalNumber(values.height)
|
||||
input.quality = toOptionalNumber(values.quality)
|
||||
input.resize = isCustom ? values.resize : undefined
|
||||
input.width = toOptionalNumber(values.width)
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await action.generateAssetVariants(input)
|
||||
notifications.show({
|
||||
color: "green",
|
||||
message: `Поставлено variants: ${response.variants.length}`,
|
||||
title: "Generation jobs created",
|
||||
})
|
||||
onGenerated(response.publicId)
|
||||
onClose()
|
||||
} catch (error) {
|
||||
notifications.show({
|
||||
color: "red",
|
||||
message: toErrorMessage(error),
|
||||
title: "Не удалось поставить generation jobs",
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Modal centered onClose={handleClose} opened={opened} radius="xl" size="lg" title="Generate variants">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Stack gap="md">
|
||||
<Text c="dimmed" fz="sm">
|
||||
Endpoint создаёт rows variants и RabbitMQ jobs. Worker сгенерирует bytes асинхронно.
|
||||
</Text>
|
||||
|
||||
<Select
|
||||
data={presetOptions}
|
||||
disabled={action.isGenerating || presetOptions.length === 0}
|
||||
label="Preset"
|
||||
onChange={(value) => form.setFieldValue("preset", value ?? "")}
|
||||
required
|
||||
value={form.values.preset}
|
||||
/>
|
||||
|
||||
<SegmentedControl
|
||||
data={[
|
||||
{ label: "Family", value: "family" },
|
||||
{ label: "Single", value: "single" },
|
||||
]}
|
||||
disabled={action.isGenerating}
|
||||
onChange={(value) => form.setFieldValue("mode", value as AssetVariantMode)}
|
||||
value={form.values.mode}
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
disabled={action.isGenerating}
|
||||
label="Version"
|
||||
min={1}
|
||||
placeholder={`current v${asset?.currentVersion ?? ""}`}
|
||||
{...form.getInputProps("version")}
|
||||
/>
|
||||
|
||||
{form.values.mode === "family" ? (
|
||||
<MultiSelect
|
||||
data={formatOptions}
|
||||
disabled={action.isGenerating}
|
||||
label="Formats"
|
||||
placeholder="All preset formats"
|
||||
{...form.getInputProps("formats")}
|
||||
/>
|
||||
) : (
|
||||
<Stack gap="md">
|
||||
<Select
|
||||
data={formatOptions}
|
||||
disabled={action.isGenerating || formatOptions.length === 0}
|
||||
label="Format"
|
||||
onChange={(value) => form.setFieldValue("format", (value ?? DEFAULT_FORMAT) as AssetVariantFormat)}
|
||||
required
|
||||
value={form.values.format}
|
||||
/>
|
||||
|
||||
<Group grow>
|
||||
<NumberInput
|
||||
description={widthHint}
|
||||
disabled={action.isGenerating}
|
||||
label="Width"
|
||||
min={1}
|
||||
{...form.getInputProps("width")}
|
||||
/>
|
||||
<NumberInput
|
||||
disabled={action.isGenerating}
|
||||
label="Height"
|
||||
min={0}
|
||||
placeholder="auto"
|
||||
{...form.getInputProps("height")}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Group grow>
|
||||
<NumberInput
|
||||
disabled={action.isGenerating}
|
||||
label="Quality"
|
||||
max={100}
|
||||
min={1}
|
||||
placeholder={String(selectedPreset?.quality ?? custom?.quality ?? 80)}
|
||||
{...form.getInputProps("quality")}
|
||||
/>
|
||||
<Select
|
||||
data={RESIZE_OPTIONS.map((resize) => ({ label: resize, value: resize }))}
|
||||
disabled={action.isGenerating || !isCustom}
|
||||
label="Resize"
|
||||
onChange={(value) => form.setFieldValue("resize", (value ?? DEFAULT_RESIZE) as AssetVariantResize)}
|
||||
value={form.values.resize}
|
||||
/>
|
||||
</Group>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Group justify="flex-end">
|
||||
<Button color="gray" disabled={action.isGenerating} onClick={handleClose} variant="subtle">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button disabled={!asset || presetOptions.length === 0} loading={action.isGenerating} type="submit">
|
||||
Generate
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { GenerateVariantsModal } from "./generate-variants-modal"
|
||||
export type { GenerateVariantsModalProps } from "./types/generate-variants-modal-props.type"
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { AssetOverview, AssetsDashboard, GenerateAssetVariantsAction } from "business/assets"
|
||||
|
||||
/**
|
||||
* Параметры GenerateVariantsModal.
|
||||
*/
|
||||
export type GenerateVariantsModalProps = {
|
||||
/** Выбранный asset. */
|
||||
asset: AssetOverview["asset"]
|
||||
/** Custom transform config. */
|
||||
custom: AssetsDashboard["custom"]
|
||||
/** Сценарий генерации variants. */
|
||||
action: GenerateAssetVariantsAction
|
||||
/** Callback закрытия modal. */
|
||||
onClose: () => void
|
||||
/** Callback успешной генерации. */
|
||||
onGenerated: (publicId: string) => void
|
||||
/** Открыта ли modal. */
|
||||
opened: boolean
|
||||
/** Static presets. */
|
||||
presets: AssetsDashboard["presets"]
|
||||
}
|
||||
Reference in New Issue
Block a user