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 { useAssetOverview } from "./hooks/use-asset-overview.hook"
|
||||||
import { useAssetsDashboard } from "./hooks/use-assets-dashboard.hook"
|
import { useAssetsDashboard } from "./hooks/use-assets-dashboard.hook"
|
||||||
import { useCreateAsset } from "./hooks/use-create-asset.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"
|
import type { AssetsFactory } from "./types/assets-factory.type"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -11,5 +13,7 @@ export const assetsFactory: AssetsFactory = () => {
|
|||||||
useAssetOverview,
|
useAssetOverview,
|
||||||
useAssetsDashboard,
|
useAssetsDashboard,
|
||||||
useCreateAsset,
|
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 { backendApi, getAssetsListKey } from "infra/backend-api"
|
||||||
|
|
||||||
import { ASSETS_DASHBOARD_LIST_PARAMS } from "../config/assets.config"
|
import { ASSETS_DASHBOARD_LIST_PARAMS } from "../config/assets.config"
|
||||||
|
import { toError } from "../lib/to-error"
|
||||||
import type { CreateAssetAction, CreateAssetInput } from "../types/assets-api.type"
|
import type { CreateAssetAction, CreateAssetInput } from "../types/assets-api.type"
|
||||||
|
|
||||||
const toError = (error: unknown) => (error instanceof Error ? error : new Error("Неизвестная ошибка"))
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Сценарий создания asset с обновлением списка.
|
* Сценарий создания 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,
|
AssetOverview,
|
||||||
AssetsApi,
|
AssetsApi,
|
||||||
AssetsDashboard,
|
AssetsDashboard,
|
||||||
|
AssetVariantFormat,
|
||||||
|
AssetVariantMode,
|
||||||
|
AssetVariantResize,
|
||||||
CreateAssetAction,
|
CreateAssetAction,
|
||||||
CreateAssetInput,
|
CreateAssetInput,
|
||||||
|
CreateAssetVersionAction,
|
||||||
|
CreateAssetVersionInput,
|
||||||
|
GenerateAssetVariantsAction,
|
||||||
|
GenerateAssetVariantsInput,
|
||||||
} from "./types/assets-api.type"
|
} from "./types/assets-api.type"
|
||||||
export type { AssetsFactory } from "./types/assets-factory.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,
|
AssetVariantResponseDto,
|
||||||
CreateAssetRequestDto,
|
CreateAssetRequestDto,
|
||||||
CreateAssetResponseDto,
|
CreateAssetResponseDto,
|
||||||
|
CreateAssetVersionResponseDto,
|
||||||
|
CreateAssetVariantsResponseDto,
|
||||||
PresetResponseDto,
|
PresetResponseDto,
|
||||||
PresetsResponseDto,
|
PresetsResponseDto,
|
||||||
} from "infra/backend-api"
|
} from "infra/backend-api"
|
||||||
|
|
||||||
|
export type AssetVariantFormat = "avif" | "jpg" | "png" | "webp"
|
||||||
|
export type AssetVariantMode = "family" | "single"
|
||||||
|
export type AssetVariantResize = "fill" | "fit"
|
||||||
|
|
||||||
export type AssetOverview = {
|
export type AssetOverview = {
|
||||||
asset: AssetResponseDto | null
|
asset: AssetResponseDto | null
|
||||||
error?: Error
|
error?: Error
|
||||||
@@ -36,6 +42,36 @@ export type CreateAssetAction = {
|
|||||||
isCreating: boolean
|
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.
|
* Публичный runtime API бизнес-модуля Assets.
|
||||||
*/
|
*/
|
||||||
@@ -43,4 +79,6 @@ export type AssetsApi = {
|
|||||||
useAssetOverview: (publicId: string | null) => AssetOverview
|
useAssetOverview: (publicId: string | null) => AssetOverview
|
||||||
useAssetsDashboard: () => AssetsDashboard
|
useAssetsDashboard: () => AssetsDashboard
|
||||||
useCreateAsset: () => CreateAssetAction
|
useCreateAsset: () => CreateAssetAction
|
||||||
|
useCreateAssetVersion: () => CreateAssetVersionAction
|
||||||
|
useGenerateAssetVariants: () => GenerateAssetVariantsAction
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ export type {
|
|||||||
AssetsListResponseDto,
|
AssetsListResponseDto,
|
||||||
CreateAssetRequestDto,
|
CreateAssetRequestDto,
|
||||||
CreateAssetResponseDto,
|
CreateAssetResponseDto,
|
||||||
|
CreateAssetVersionRequestDto,
|
||||||
|
CreateAssetVersionResponseDto,
|
||||||
|
CreateAssetVariantsRequestDto,
|
||||||
|
CreateAssetVariantsResponseDto,
|
||||||
CustomTransformConfigResponseDto,
|
CustomTransformConfigResponseDto,
|
||||||
ListAssetsParams,
|
ListAssetsParams,
|
||||||
PresetResponseDto,
|
PresetResponseDto,
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { DASHBOARD_PIPELINE } from "./config/dashboard.config"
|
|||||||
import { AssetDetailPanel } from "./parts/asset-detail-panel"
|
import { AssetDetailPanel } from "./parts/asset-detail-panel"
|
||||||
import { AssetsTable } from "./parts/assets-table"
|
import { AssetsTable } from "./parts/assets-table"
|
||||||
import { CreateAssetModal } from "./parts/create-asset-modal"
|
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 { PresetsPanel } from "./parts/presets-panel"
|
||||||
import { SummaryCards } from "./parts/summary-cards"
|
import { SummaryCards } from "./parts/summary-cards"
|
||||||
import styles from "./styles/dashboard.module.css"
|
import styles from "./styles/dashboard.module.css"
|
||||||
@@ -26,8 +28,12 @@ export const DashboardScreen = (props: DashboardScreenProps) => {
|
|||||||
const { className, ...rootAttrs } = props
|
const { className, ...rootAttrs } = props
|
||||||
const [selectedPublicId, setSelectedPublicId] = useState<string | null>(null)
|
const [selectedPublicId, setSelectedPublicId] = useState<string | null>(null)
|
||||||
const [isCreateAssetOpen, createAssetModal] = useDisclosure(false)
|
const [isCreateAssetOpen, createAssetModal] = useDisclosure(false)
|
||||||
|
const [isCreateVersionOpen, createVersionModal] = useDisclosure(false)
|
||||||
|
const [isGenerateVariantsOpen, generateVariantsModal] = useDisclosure(false)
|
||||||
const dashboard = assets.useAssetsDashboard()
|
const dashboard = assets.useAssetsDashboard()
|
||||||
const createAsset = assets.useCreateAsset()
|
const createAsset = assets.useCreateAsset()
|
||||||
|
const createAssetVersion = assets.useCreateAssetVersion()
|
||||||
|
const generateAssetVariants = assets.useGenerateAssetVariants()
|
||||||
const effectivePublicId = selectedPublicId ?? dashboard.assets[0]?.publicId ?? null
|
const effectivePublicId = selectedPublicId ?? dashboard.assets[0]?.publicId ?? null
|
||||||
const overview = assets.useAssetOverview(effectivePublicId)
|
const overview = assets.useAssetOverview(effectivePublicId)
|
||||||
|
|
||||||
@@ -74,7 +80,12 @@ export const DashboardScreen = (props: DashboardScreenProps) => {
|
|||||||
onSelect={setSelectedPublicId}
|
onSelect={setSelectedPublicId}
|
||||||
selectedPublicId={effectivePublicId}
|
selectedPublicId={effectivePublicId}
|
||||||
/>
|
/>
|
||||||
<AssetDetailPanel overview={overview} publicId={effectivePublicId} />
|
<AssetDetailPanel
|
||||||
|
onCreateVersion={createVersionModal.open}
|
||||||
|
onGenerateVariants={generateVariantsModal.open}
|
||||||
|
overview={overview}
|
||||||
|
publicId={effectivePublicId}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PresetsPanel
|
<PresetsPanel
|
||||||
@@ -91,6 +102,24 @@ export const DashboardScreen = (props: DashboardScreenProps) => {
|
|||||||
onCreated={setSelectedPublicId}
|
onCreated={setSelectedPublicId}
|
||||||
opened={isCreateAssetOpen}
|
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>
|
</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 { ASSET_STATUS_COLORS, VARIANT_STATUS_COLORS } from "../../config/dashboard.config"
|
||||||
import { formatDateTime } from "../../lib/format-date"
|
import { formatDateTime } from "../../lib/format-date"
|
||||||
@@ -12,7 +12,7 @@ import type { AssetDetailPanelProps } from "./types/asset-detail-panel-props.typ
|
|||||||
* - отображения статусов generated variants
|
* - отображения статусов generated variants
|
||||||
*/
|
*/
|
||||||
export const AssetDetailPanel = (props: AssetDetailPanelProps) => {
|
export const AssetDetailPanel = (props: AssetDetailPanelProps) => {
|
||||||
const { overview, publicId } = props
|
const { onCreateVersion, onGenerateVariants, overview, publicId } = props
|
||||||
const { asset, variants } = overview
|
const { asset, variants } = overview
|
||||||
|
|
||||||
if (!publicId) {
|
if (!publicId) {
|
||||||
@@ -41,9 +41,17 @@ export const AssetDetailPanel = (props: AssetDetailPanelProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{asset ? (
|
{asset ? (
|
||||||
<Badge color={ASSET_STATUS_COLORS[asset.status] ?? "gray"} radius="xl" variant="light">
|
<Group gap="xs">
|
||||||
{asset.status}
|
<Button onClick={onCreateVersion} radius="xl" size="xs" variant="light">
|
||||||
</Badge>
|
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}
|
) : null}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
@@ -90,7 +98,8 @@ export const AssetDetailPanel = (props: AssetDetailPanelProps) => {
|
|||||||
<Table.Th>Preset</Table.Th>
|
<Table.Th>Preset</Table.Th>
|
||||||
<Table.Th>Format</Table.Th>
|
<Table.Th>Format</Table.Th>
|
||||||
<Table.Th>Size</Table.Th>
|
<Table.Th>Size</Table.Th>
|
||||||
<Table.Th>Status</Table.Th>
|
<Table.Th>Status</Table.Th>
|
||||||
|
<Table.Th>URL</Table.Th>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
</Table.Thead>
|
</Table.Thead>
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
@@ -108,6 +117,11 @@ export const AssetDetailPanel = (props: AssetDetailPanelProps) => {
|
|||||||
{variant.status}
|
{variant.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Anchor href={variant.url} target="_blank">
|
||||||
|
open
|
||||||
|
</Anchor>
|
||||||
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
))}
|
))}
|
||||||
</Table.Tbody>
|
</Table.Tbody>
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import type { AssetOverview } from "business/assets"
|
|||||||
export type AssetDetailPanelProps = {
|
export type AssetDetailPanelProps = {
|
||||||
/** Данные выбранного asset. */
|
/** Данные выбранного asset. */
|
||||||
overview: AssetOverview
|
overview: AssetOverview
|
||||||
|
/** Callback открытия modal новой source version. */
|
||||||
|
onCreateVersion: () => void
|
||||||
|
/** Callback открытия modal генерации variants. */
|
||||||
|
onGenerateVariants: () => void
|
||||||
/** Выбранный publicId. */
|
/** Выбранный publicId. */
|
||||||
publicId: string | null
|
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