From d6ffdaea01f20b57822b8bc4d494e525632fa194 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Fri, 14 Jun 2024 14:50:26 -0400 Subject: [PATCH] asset image pannel can delete, toggle visible, name as main form (disabled), popover with name field --- .../templates/form/fetcher-image-select.tsx | 134 ++++++++++++------ .../templates/form/fetcher-text.tsx | 115 +++++++++++++++ .../dashboard-entity-panel.actions.delete.tsx | 10 ++ ...rd-entity-panel.actions.toggle-visible.tsx | 9 ++ .../panel/dashboard-entity-panel.tsx | 20 ++- ...hboard-entity-panel.values.asset.image.tsx | 58 ++++++++ .../panel/dashboard-entity-panel.values.tsx | 5 + app/components/ui/radio-group.tsx | 43 ++++++ app/models/asset/asset.get.server.ts | 1 + app/models/asset/asset.server.ts | 1 + .../image.delete.artwork-version.server.ts | 20 +++ app/models/asset/image/image.get.server.ts | 1 + .../image.update.artwork-version.server.ts | 73 ++++++++++ ...e.update.artwork-version.visible.server.ts | 45 ++++++ .../sidebars.panel.assets.artwork-version.tsx | 8 +- .../__components/sidebars.panel.assets.tsx | 1 + .../asset.image.artwork-version.clone.tsx | 115 +++++++++++++++ .../asset.image.artwork-version.create.tsx | 21 +-- .../asset.image.artwork-version.delete.tsx | 101 +++++++++++++ ...t.image.artwork-version.update.visible.tsx | 102 +++++++++++++ .../resources+/api.v1+/asset.update.name.tsx | 108 ++++++++++++++ app/schema/asset/__shared.ts | 7 + app/schema/asset/image.artwork-version.ts | 14 ++ app/schema/asset/image.ts | 19 +-- app/schema/entity.ts | 2 +- ...et.image.artwork-version.delete.service.ts | 31 ++++ ....artwork-version.update.visible.service.ts | 51 +++++++ .../dashboard-panel/delete-entity.strategy.ts | 8 ++ .../entity-action/entity-action.ts | 18 ++- .../update-entity-order.strategy.ts | 7 + .../update-entity-visible.strategy.ts | 8 ++ app/utils/routes.const.ts | 2 + package-lock.json | 51 +++++++ package.json | 1 + .../migration.sql | 30 ++++ prisma/schema.prisma | 1 + 36 files changed, 1156 insertions(+), 85 deletions(-) create mode 100644 app/components/templates/form/fetcher-text.tsx create mode 100644 app/components/templates/panel/dashboard-entity-panel.values.asset.image.tsx create mode 100644 app/components/ui/radio-group.tsx create mode 100644 app/models/asset/image/image.delete.artwork-version.server.ts create mode 100644 app/models/asset/image/image.update.artwork-version.server.ts create mode 100644 app/models/asset/image/image.update.artwork-version.visible.server.ts create mode 100644 app/routes/resources+/api.v1+/asset.image.artwork-version.clone.tsx create mode 100644 app/routes/resources+/api.v1+/asset.image.artwork-version.delete.tsx create mode 100644 app/routes/resources+/api.v1+/asset.image.artwork-version.update.visible.tsx create mode 100644 app/routes/resources+/api.v1+/asset.update.name.tsx create mode 100644 app/services/asset.image.artwork-version.delete.service.ts create mode 100644 app/services/asset.image.artwork-version.update.visible.service.ts create mode 100644 prisma/migrations/20240613070911_add_visible_to_asset/migration.sql diff --git a/app/components/templates/form/fetcher-image-select.tsx b/app/components/templates/form/fetcher-image-select.tsx index 2ac7494c..21c1dd8d 100644 --- a/app/components/templates/form/fetcher-image-select.tsx +++ b/app/components/templates/form/fetcher-image-select.tsx @@ -1,5 +1,5 @@ import { conform, useForm } from '@conform-to/react' -import { getFieldsetConstraint } from '@conform-to/zod' +import { getFieldsetConstraint, parse } from '@conform-to/zod' import { type FetcherWithComponents } from '@remix-run/react' import { useEffect, useState } from 'react' import { AuthenticityTokenInput } from 'remix-utils/csrf/react' @@ -9,9 +9,8 @@ import { ImagePreviewContainer, ImagePreviewLabel, ImagePreviewWrapper, - ImageUploadInput, } from '#app/components/image' -import { FlexColumn } from '#app/components/layout' +import { FlexColumn, FlexRow } from '#app/components/layout' import { DialogContentGrid, DialogFormsContainer, @@ -26,13 +25,14 @@ import { DialogTrigger, } from '#app/components/ui/dialog' import { type IconName } from '#app/components/ui/icon' -import { Label } from '#app/components/ui/label' +import { Input } from '#app/components/ui/input' import { PanelIconButton } from '#app/components/ui/panel-icon-button' import { StatusButton } from '#app/components/ui/status-button' import { type IAssetParent } from '#app/models/asset/asset.server' import { type IAssetImage } from '#app/models/asset/image/image.server' +import { sizeInMB } from '#app/models/asset/image/utils' import { type IAssetImageSrcStrategy } from '#app/strategies/asset.image.src.strategy' -import { useIsPending } from '#app/utils/misc' +import { cn, useIsPending } from '#app/utils/misc' import { TooltipHydrated } from '../tooltip' export const FetcherImageSelect = ({ @@ -67,13 +67,24 @@ export const FetcherImageSelect = ({ children: JSX.Element }) => { const [open, setOpen] = useState(false) + const [selectedImageId, setSelectedImageId] = useState< + IAssetImage['id'] | null + >(null) const lastSubmission = fetcher.data?.submission const isPending = useIsPending() - const [form, fields] = useForm({ + const [form, { assetImageId }] = useForm({ id: formId, constraint: getFieldsetConstraint(schema), lastSubmission, + shouldValidate: 'onInput', + onValidate({ formData }) { + const parsed = parse(formData, { schema }) + setSelectedImageId( + (parsed.payload.assetImageId as IAssetImage['id']) ?? null, + ) + return parsed + }, onSubmit: async (event, { formData }) => { event.preventDefault() fetcher.submit(formData, { @@ -114,48 +125,75 @@ export const FetcherImageSelect = ({ {children} - - {images.map(image => { - const { id, name, description, attributes } = image - const { altText } = attributes - const imgSrc = strategy.getAssetSrc({ - parentId: parent.id, - assetId: id, - }) +
+ + {conform + .collection(assetImageId, { + type: 'radio', + options: images.map(image => image.id), + }) + .map((props, index) => { + const image = images.find( + image => image.id === props.value, + ) as IAssetImage + const isSelectedImage = selectedImageId === image.id + + const { id, name, attributes } = image + const { altText, height, width, size } = attributes + const imgSrc = strategy.getAssetSrc({ + parentId: parent.id, + assetId: id, + }) - return ( - - - - -
- -
-
-
-
- - - -
- ) - })} + return ( + + + + +
+
+ +
+
+ +
+
+
+ + + + + {name} + + + {width}x{height} + + +
{sizeInMB(size)} MB
+
+
+
+
+
+ ) + })} +
+
@@ -163,6 +201,8 @@ export const FetcherImageSelect = ({ diff --git a/app/components/templates/form/fetcher-text.tsx b/app/components/templates/form/fetcher-text.tsx new file mode 100644 index 00000000..b958aa95 --- /dev/null +++ b/app/components/templates/form/fetcher-text.tsx @@ -0,0 +1,115 @@ +import { useForm, conform } from '@conform-to/react' +import { getFieldsetConstraint, parse } from '@conform-to/zod' +import { type FetcherWithComponents } from '@remix-run/react' +import { useRef } from 'react' +import { AuthenticityTokenInput } from 'remix-utils/csrf/react' +import { type z } from 'zod' +import { Icon, type IconName } from '#app/components/ui/icon' +import { Input } from '#app/components/ui/input' +import { Label } from '#app/components/ui/label' +import { useOptimisticValue } from '#app/utils/forms' +import { useDebounce, useIsPending } from '#app/utils/misc' +import { TooltipHydrated } from '../tooltip' + +export const FetcherText = ({ + fetcher, + fetcherKey, + route, + schema, + formId, + fieldName, + fieldValue, + placeholder, + tooltipText, + isHydrated, + disabled, + children, + icon, +}: { + fetcher: FetcherWithComponents + fetcherKey: string + route: string + schema: z.ZodSchema + formId: string + fieldName: string + fieldValue: string + placeholder: string + tooltipText: string + isHydrated: boolean + disabled?: boolean + children: JSX.Element + icon?: IconName +}) => { + const optimisticValue = useOptimisticValue(fetcherKey, schema, fieldName) + const value = optimisticValue ?? fieldValue ?? 0 + const lastSubmission = fetcher.data?.submission + const isPending = useIsPending() + const [form, fields] = useForm({ + id: formId, + constraint: getFieldsetConstraint(schema), + lastSubmission, + shouldValidate: 'onInput', + shouldRevalidate: 'onInput', + onValidate: ({ formData }) => { + return parse(formData, { schema: schema }) + }, + onSubmit: async (event, { formData }) => { + event.preventDefault() + fetcher.submit(formData, { + method: 'POST', + action: route, + }) + }, + defaultValue: { + [fieldName]: value, + }, + }) + + // hack to submit select form on change + // through conform-to and fetcher + const submitRef = useRef(null) + const handleChangeSubmit = useDebounce((f: HTMLFormElement) => { + submitRef.current?.click() + }, 400) + + return ( + handleChangeSubmit(e.currentTarget)} + {...form.props} + className="flex-1" + > + + + {/* hidden field values */} + {children} + + {/* need this div class for icon */} +
+ {/* icon might be for artwork height, width */} + {icon && ( + + )} + + + +
+ + +
+ ) +} diff --git a/app/components/templates/panel/dashboard-entity-panel.actions.delete.tsx b/app/components/templates/panel/dashboard-entity-panel.actions.delete.tsx index 3b8a0f41..854b31f8 100644 --- a/app/components/templates/panel/dashboard-entity-panel.actions.delete.tsx +++ b/app/components/templates/panel/dashboard-entity-panel.actions.delete.tsx @@ -1,6 +1,9 @@ import { memo, useCallback } from 'react' +import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server' +import { type IAssetImage } from '#app/models/asset/image/image.server' import { type IDesign } from '#app/models/design/design.server' import { ArtworkVersionDesignDelete } from '#app/routes/resources+/api.v1+/artwork-version.design.delete' +import { AssetImageArtworkVersionDelete } from '#app/routes/resources+/api.v1+/asset.image.artwork-version.delete' import { LayerDesignDelete } from '#app/routes/resources+/api.v1+/layer.design.delete' import { type entityParentTypeEnum, @@ -22,6 +25,13 @@ interface DeleteChildEntityFormProps { const ArtworkVersionDeleteChildEntityForm = memo( ({ entityType, entity, parent }: DeleteChildEntityFormProps) => { switch (entityType) { + case EntityType.ASSET: + return ( + + ) case EntityType.DESIGN: return ( { switch (entityType) { + case EntityType.ASSET: + return ( + + ) case EntityType.DESIGN: return ( { const entityCount = entities.length @@ -41,13 +43,17 @@ export const DashboardEntityPanel = ({ {entities.map((entity, index) => { return ( - + {skipReorder ? ( +
+ ) : ( + + )} { + // display color on popover trigger if fill is defined and solid + // const { fill } = entity as IDesignWithFill + // const { basis, value } = fill + // const displayColor = + // basis === FillBasisTypeEnum.DEFINED && FillStyleTypeEnum.SOLID + // const backgroundColor = displayColor ? value : undefined + + return ( + + + Name + + + + ) +}) +EntityPopover.displayName = 'EntityPopover' + +const EntityMainForm = memo(({ entity }: EntityProps) => { + return +}) +EntityMainForm.displayName = 'EntityMainForm' + +export const PanelEntityValuesAssetImage = ({ + entity, +}: { + entity: IAssetImage +}) => { + const entityPopover = useCallback( + () => , + [entity], + ) + + const entityMainForm = useCallback( + () => , + [entity], + ) + + return ( + + {entityPopover()} + {entityMainForm()} + + ) +} diff --git a/app/components/templates/panel/dashboard-entity-panel.values.tsx b/app/components/templates/panel/dashboard-entity-panel.values.tsx index 1d9e3432..71ccb669 100644 --- a/app/components/templates/panel/dashboard-entity-panel.values.tsx +++ b/app/components/templates/panel/dashboard-entity-panel.values.tsx @@ -1,9 +1,12 @@ +import { type IAssetImage } from '#app/models/asset/image/image.server' +import { AssetTypeEnum } from '#app/schema/asset' import { DesignTypeEnum } from '#app/schema/design' import { type IEntityParentType, type IEntity, type IEntityType, } from '#app/schema/entity' +import { PanelEntityValuesAssetImage } from './dashboard-entity-panel.values.asset.image' import { PanelEntityValuesDesignFill } from './dashboard-entity-panel.values.design.fill' import { PanelEntityValuesDesignLayout } from './dashboard-entity-panel.values.design.layout' import { PanelEntityValuesDesignLine } from './dashboard-entity-panel.values.design.line' @@ -28,6 +31,8 @@ export const PanelEntityValues = ({ } switch (type) { + case AssetTypeEnum.IMAGE: + return case DesignTypeEnum.FILL: return case DesignTypeEnum.LAYOUT: diff --git a/app/components/ui/radio-group.tsx b/app/components/ui/radio-group.tsx new file mode 100644 index 00000000..5e64ce03 --- /dev/null +++ b/app/components/ui/radio-group.tsx @@ -0,0 +1,43 @@ +import * as RadioGroupPrimitive from '@radix-ui/react-radio-group' +import { Circle } from 'lucide-react' +import * as React from 'react' + +import { cn } from '#app/utils/misc.tsx' + +const RadioGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ) +}) +RadioGroup.displayName = RadioGroupPrimitive.Root.displayName + +const RadioGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + +

yo

+ +
+
+ ) +}) +RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName + +export { RadioGroup, RadioGroupItem } diff --git a/app/models/asset/asset.get.server.ts b/app/models/asset/asset.get.server.ts index e66866b4..cbe46635 100644 --- a/app/models/asset/asset.get.server.ts +++ b/app/models/asset/asset.get.server.ts @@ -10,6 +10,7 @@ export const assetSelect = { description: true, type: true, attributes: true, + visible: true, // no blob, too much memory on query createdAt: true, updatedAt: true, diff --git a/app/models/asset/asset.server.ts b/app/models/asset/asset.server.ts index c7ed1666..4a99a6df 100644 --- a/app/models/asset/asset.server.ts +++ b/app/models/asset/asset.server.ts @@ -57,6 +57,7 @@ export type IAssetParent = interface IAssetData { name: string description?: string + visible: boolean } export interface IAssetSubmission extends IAssetData { diff --git a/app/models/asset/image/image.delete.artwork-version.server.ts b/app/models/asset/image/image.delete.artwork-version.server.ts new file mode 100644 index 00000000..d2131ce1 --- /dev/null +++ b/app/models/asset/image/image.delete.artwork-version.server.ts @@ -0,0 +1,20 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { DeleteAssetImageArtworkVersionSchema } from '#app/schema/asset/image.artwork-version' +import { ValidateAssetSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { validateEntitySubmission } from '#app/utils/conform-utils' + +export const validateDeleteAssetImageArtworkVersionSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateAssetSubmissionStrategy() + + // not validateEntityImageSubmission + // there is no image file to parse and transform + return await validateEntitySubmission({ + userId, + formData, + schema: DeleteAssetImageArtworkVersionSchema, + strategy, + }) +} diff --git a/app/models/asset/image/image.get.server.ts b/app/models/asset/image/image.get.server.ts index 04a53530..0a29554b 100644 --- a/app/models/asset/image/image.get.server.ts +++ b/app/models/asset/image/image.get.server.ts @@ -11,6 +11,7 @@ const whereArgs = z.object({ id: z.string().optional(), ownerId: z.string().optional(), artworkId: z.string().optional(), + artworkVersionId: z.string().optional(), }) // TODO: Add schemas for each type of query and parse with zod diff --git a/app/models/asset/image/image.update.artwork-version.server.ts b/app/models/asset/image/image.update.artwork-version.server.ts new file mode 100644 index 00000000..66d1ad8e --- /dev/null +++ b/app/models/asset/image/image.update.artwork-version.server.ts @@ -0,0 +1,73 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server' +import { + EditAssetImageArtworkVersionSchema, + EditVisibleAssetImageArtworkVersionSchema, +} from '#app/schema/asset/image.artwork-version' +import { ValidateAssetSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { + validateEntityImageSubmission, + validateEntitySubmission, +} from '#app/utils/conform-utils' +import { prisma } from '#app/utils/db.server' +import { type IAssetImage } from './image.server' +import { + type IAssetImageUpdateData, + type IAssetImageUpdateSubmission, +} from './image.update.server' +import { stringifyAssetImageAttributes } from './utils' + +export const validateEditAssetImageArtworkVersionSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateAssetSubmissionStrategy() + + return await validateEntityImageSubmission({ + userId, + formData, + schema: EditAssetImageArtworkVersionSchema, + strategy, + }) +} + +export const validateEditVisibleAssetImageArtworkVersionSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateAssetSubmissionStrategy() + + return await validateEntitySubmission({ + userId, + formData, + schema: EditVisibleAssetImageArtworkVersionSchema, + strategy, + }) +} + +export interface IAssetImageArtworkVersionUpdateSubmission + extends IAssetImageUpdateSubmission { + artworkVersionId: IArtworkVersion['id'] +} + +interface IAssetImageArtworkVersionUpdateData extends IAssetImageUpdateData { + artworkVersionId: IArtworkVersion['id'] +} + +export const updateAssetImageArtworkVersion = ({ + id, + data, +}: { + id: IAssetImage['id'] + data: IAssetImageArtworkVersionUpdateData +}) => { + const { attributes, ...rest } = data + const jsonAttributes = stringifyAssetImageAttributes(attributes) + return prisma.asset.update({ + where: { id }, + data: { + ...rest, + attributes: jsonAttributes, + }, + }) +} diff --git a/app/models/asset/image/image.update.artwork-version.visible.server.ts b/app/models/asset/image/image.update.artwork-version.visible.server.ts new file mode 100644 index 00000000..4d4a0e2e --- /dev/null +++ b/app/models/asset/image/image.update.artwork-version.visible.server.ts @@ -0,0 +1,45 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server' +import { type IUser } from '#app/models/user/user.server' +import { EditVisibleAssetImageArtworkVersionSchema } from '#app/schema/asset/image.artwork-version' +import { ValidateAssetSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { validateEntitySubmission } from '#app/utils/conform-utils' +import { prisma } from '#app/utils/db.server' +import { type IAssetImage } from './image.server' + +export const validateEditVisibleAssetImageArtworkVersionSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateAssetSubmissionStrategy() + + return await validateEntitySubmission({ + userId, + formData, + schema: EditVisibleAssetImageArtworkVersionSchema, + strategy, + }) +} + +export interface IAssetImageArtworkVersionUpdateVisibleSubmission { + id: IAssetImage['id'] + userId: IUser['id'] + artworkVersionId: IArtworkVersion['id'] +} + +interface IAssetImageArtworkVersionUpdateVisibleData { + visible: boolean +} + +export const updateAssetImageArtworkVersionVisible = ({ + id, + data, +}: { + id: IAssetImage['id'] + data: IAssetImageArtworkVersionUpdateVisibleData +}) => { + return prisma.asset.update({ + where: { id }, + data, + }) +} diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.artwork-version.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.artwork-version.tsx index 8d05848b..b62ec476 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.artwork-version.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.artwork-version.tsx @@ -1,7 +1,7 @@ import { type IArtworkVersionWithChildren } from '#app/models/artwork-version/artwork-version.server' import { DashboardPanelCreateArtworkVersionAssetTypeStrategy } from '#app/strategies/component/dashboard-panel/create-entity.strategy' -import { DashboardPanelArtworkVersionDesignActionStrategy } from '#app/strategies/component/dashboard-panel/entity-action/entity-action' -import { DashboardPanelUpdateArtworkVersionDesignTypeOrderStrategy } from '#app/strategies/component/dashboard-panel/update-entity-order.strategy' +import { DashboardPanelArtworkVersionAssetActionStrategy } from '#app/strategies/component/dashboard-panel/entity-action/entity-action' +import { DashboardPanelUpdateArtworkVersionAssetTypeOrderStrategy } from '#app/strategies/component/dashboard-panel/update-entity-order.strategy' import { PanelAssets } from './sidebars.panel.assets' export const PanelArtworkVersionAssets = ({ @@ -12,8 +12,8 @@ export const PanelArtworkVersionAssets = ({ const strategyEntityNew = new DashboardPanelCreateArtworkVersionAssetTypeStrategy() const strategyReorder = - new DashboardPanelUpdateArtworkVersionDesignTypeOrderStrategy() - const strategyActions = new DashboardPanelArtworkVersionDesignActionStrategy() + new DashboardPanelUpdateArtworkVersionAssetTypeOrderStrategy() + const strategyActions = new DashboardPanelArtworkVersionAssetActionStrategy() return ( ) } diff --git a/app/routes/resources+/api.v1+/asset.image.artwork-version.clone.tsx b/app/routes/resources+/api.v1+/asset.image.artwork-version.clone.tsx new file mode 100644 index 00000000..f318aef2 --- /dev/null +++ b/app/routes/resources+/api.v1+/asset.image.artwork-version.clone.tsx @@ -0,0 +1,115 @@ +import { + json, + type ActionFunctionArgs, + type LoaderFunctionArgs, + unstable_createMemoryUploadHandler as createMemoryUploadHandler, + unstable_parseMultipartFormData as parseMultipartFormData, +} from '@remix-run/node' +import { useFetcher } from '@remix-run/react' +import { redirectBack } from 'remix-utils/redirect-back' +import { useHydrated } from 'remix-utils/use-hydrated' +import { FetcherImageSelect } from '#app/components/templates/form/fetcher-image-select' +import { useArtworkFromVersion } from '#app/models/artwork/hooks' +import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server' +import { useAssetImagesArtwork } from '#app/models/asset/image/hooks' +import { validateNewAssetImageArtworkVersionSubmission } from '#app/models/asset/image/image.create.artwork-version.server' +import { MAX_UPLOAD_SIZE } from '#app/schema/asset/image' +import { CloneAssetImageArtworkToArtworkVersionSchema } from '#app/schema/asset/image.artwork-version' +import { validateNoJS } from '#app/schema/form-data' +import { assetImageArtworkVersionCreateService } from '#app/services/asset.image.artwork-version.create.service' +import { ArtworkAssetImageSrcStrategy } from '#app/strategies/asset.image.src.strategy' +import { requireUserId } from '#app/utils/auth.server' +import { Routes } from '#app/utils/routes.const' + +// technically this would be a clone of the artowrk asset image by id + +// https://www.epicweb.dev/full-stack-components + +const route = Routes.RESOURCES.API.V1.ASSET.IMAGE.ARTWORK_VERSION.CREATE +const schema = CloneAssetImageArtworkToArtworkVersionSchema + +// auth GET request to endpoint +export async function loader({ request }: LoaderFunctionArgs) { + await requireUserId(request) + return json({}) +} + +export async function action({ request }: ActionFunctionArgs) { + const userId = await requireUserId(request) + // consider intent if cloning or uploading new image + const formData = await parseMultipartFormData( + request, + createMemoryUploadHandler({ maxPartSize: MAX_UPLOAD_SIZE }), + ) + const noJS = validateNoJS({ formData }) + + let createSuccess = false + let errorMessage = '' + const { status, submission } = + await validateNewAssetImageArtworkVersionSubmission({ + userId, + formData, + }) + + if (status === 'success') { + const { success, message } = await assetImageArtworkVersionCreateService({ + userId, + ...submission.value, + }) + + createSuccess = success + errorMessage = message || '' + } + + if (noJS) { + throw redirectBack(request, { + fallback: '/', + }) + } + + return json( + { status, submission, message: errorMessage }, + { + status: status === 'error' || !createSuccess ? 422 : 201, + }, + ) +} + +export const AssetImageArtworkVersionClone = ({ + version, +}: { + version: IArtworkVersion +}) => { + const artworkVersionId = version.id + const images = useAssetImagesArtwork() + const artwork = useArtworkFromVersion() + const artworkId = artwork.id + const strategy = new ArtworkAssetImageSrcStrategy() + const formId = `asset-image-artwork-version-${artworkVersionId}-create` + + const fetcher = useFetcher() + let isHydrated = useHydrated() + + return ( + +
+ + +
+
+ ) +} diff --git a/app/routes/resources+/api.v1+/asset.image.artwork-version.create.tsx b/app/routes/resources+/api.v1+/asset.image.artwork-version.create.tsx index 02f0ea48..c4c407ae 100644 --- a/app/routes/resources+/api.v1+/asset.image.artwork-version.create.tsx +++ b/app/routes/resources+/api.v1+/asset.image.artwork-version.create.tsx @@ -8,16 +8,13 @@ import { import { useFetcher } from '@remix-run/react' import { redirectBack } from 'remix-utils/redirect-back' import { useHydrated } from 'remix-utils/use-hydrated' -import { FetcherImageSelect } from '#app/components/templates/form/fetcher-image-select' -import { useArtworkFromVersion } from '#app/models/artwork/hooks' +import { FetcherImageUpload } from '#app/components/templates/form/fetcher-image-upload' import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server' -import { useAssetImagesArtwork } from '#app/models/asset/image/hooks' import { validateNewAssetImageArtworkVersionSubmission } from '#app/models/asset/image/image.create.artwork-version.server' import { MAX_UPLOAD_SIZE } from '#app/schema/asset/image' import { NewAssetImageArtworkVersionSchema } from '#app/schema/asset/image.artwork-version' import { validateNoJS } from '#app/schema/form-data' import { assetImageArtworkVersionCreateService } from '#app/services/asset.image.artwork-version.create.service' -import { ArtworkAssetImageSrcStrategy } from '#app/strategies/asset.image.src.strategy' import { requireUserId } from '#app/utils/auth.server' import { Routes } from '#app/utils/routes.const' @@ -78,33 +75,27 @@ export const AssetImageArtworkVersionCreate = ({ version: IArtworkVersion }) => { const artworkVersionId = version.id - const images = useAssetImagesArtwork() - const artwork = useArtworkFromVersion() - const strategy = new ArtworkAssetImageSrcStrategy() - const formId = `asset-image-artwork-${artworkVersionId}-create` + const formId = `asset-image-artwork-version-${artworkVersionId}-create` const fetcher = useFetcher() let isHydrated = useHydrated() return ( -
-
+ ) } diff --git a/app/routes/resources+/api.v1+/asset.image.artwork-version.delete.tsx b/app/routes/resources+/api.v1+/asset.image.artwork-version.delete.tsx new file mode 100644 index 00000000..4bc9345d --- /dev/null +++ b/app/routes/resources+/api.v1+/asset.image.artwork-version.delete.tsx @@ -0,0 +1,101 @@ +import { + json, + type ActionFunctionArgs, + type LoaderFunctionArgs, +} from '@remix-run/node' +import { useFetcher } from '@remix-run/react' +import { redirectBack } from 'remix-utils/redirect-back' +import { useHydrated } from 'remix-utils/use-hydrated' +import { FetcherIconConfirm } from '#app/components/templates/form/fetcher-icon-confirm' +import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server' +import { validateDeleteAssetImageArtworkVersionSubmission } from '#app/models/asset/image/image.delete.artwork-version.server' +import { type IAssetImage } from '#app/models/asset/image/image.server' +import { DeleteAssetImageArtworkVersionSchema } from '#app/schema/asset/image.artwork-version' +import { validateNoJS } from '#app/schema/form-data' +import { assetImageArtworkVersionDeleteService } from '#app/services/asset.image.artwork-version.delete.service' +import { requireUserId } from '#app/utils/auth.server' +import { Routes } from '#app/utils/routes.const' + +// https://www.epicweb.dev/full-stack-components + +const route = Routes.RESOURCES.API.V1.ASSET.IMAGE.ARTWORK_VERSION.DELETE +const schema = DeleteAssetImageArtworkVersionSchema + +// auth GET request to endpoint +export async function loader({ request }: LoaderFunctionArgs) { + await requireUserId(request) + return json({}) +} + +export async function action({ request }: ActionFunctionArgs) { + const userId = await requireUserId(request) + const formData = await request.formData() + const noJS = validateNoJS({ formData }) + + let createSuccess = false + let errorMessage = '' + const { status, submission } = + await validateDeleteAssetImageArtworkVersionSubmission({ + userId, + formData, + }) + + if (status === 'success') { + const { success, message } = await assetImageArtworkVersionDeleteService({ + userId, + ...submission.value, + }) + + createSuccess = success + errorMessage = message || '' + } + + if (noJS) { + throw redirectBack(request, { + fallback: '/', + }) + } + + return json( + { status, submission, message: errorMessage }, + { + status: status === 'error' || !createSuccess ? 422 : 201, + }, + ) +} + +export const AssetImageArtworkVersionDelete = ({ + image, + artworkVersion, +}: { + image: IAssetImage + artworkVersion: IArtworkVersion +}) => { + const imageId = image.id + const artworkVersionId = artworkVersion.id + const iconText = `Delete Image...` + const formId = `asset-image--${imageId}-artworkVersion-${artworkVersionId}-delete` + + const fetcher = useFetcher() + let isHydrated = useHydrated() + + return ( + +
+ + +
+
+ ) +} diff --git a/app/routes/resources+/api.v1+/asset.image.artwork-version.update.visible.tsx b/app/routes/resources+/api.v1+/asset.image.artwork-version.update.visible.tsx new file mode 100644 index 00000000..1aab0ffd --- /dev/null +++ b/app/routes/resources+/api.v1+/asset.image.artwork-version.update.visible.tsx @@ -0,0 +1,102 @@ +import { + json, + type ActionFunctionArgs, + type LoaderFunctionArgs, +} from '@remix-run/node' +import { useFetcher } from '@remix-run/react' +import { redirectBack } from 'remix-utils/redirect-back' +import { useHydrated } from 'remix-utils/use-hydrated' +import { FetcherIconButton } from '#app/components/templates/form/fetcher-icon-button' +import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server' +import { type IAssetImage } from '#app/models/asset/image/image.server' +import { validateEditVisibleAssetImageArtworkVersionSubmission } from '#app/models/asset/image/image.update.artwork-version.server' +import { EditVisibleAssetImageArtworkVersionSchema } from '#app/schema/asset/image.artwork-version' +import { validateNoJS } from '#app/schema/form-data' +import { assetImageArtworkVersionUpdateVisibleService } from '#app/services/asset.image.artwork-version.update.visible.service' +import { requireUserId } from '#app/utils/auth.server' +import { Routes } from '#app/utils/routes.const' + +// https://www.epicweb.dev/full-stack-components + +const route = Routes.RESOURCES.API.V1.ASSET.IMAGE.ARTWORK_VERSION.UPDATE_VISIBLE +const schema = EditVisibleAssetImageArtworkVersionSchema + +// auth GET request to endpoint +export async function loader({ request }: LoaderFunctionArgs) { + await requireUserId(request) + return json({}) +} + +export async function action({ request }: ActionFunctionArgs) { + const userId = await requireUserId(request) + const formData = await request.formData() + const noJS = validateNoJS({ formData }) + + let createSuccess = false + let errorMessage = '' + const { status, submission } = + await validateEditVisibleAssetImageArtworkVersionSubmission({ + userId, + formData, + }) + + if (status === 'success') { + const { success, message } = + await assetImageArtworkVersionUpdateVisibleService({ + userId, + ...submission.value, + }) + + createSuccess = success + errorMessage = message || '' + } + + if (noJS) { + throw redirectBack(request, { + fallback: '/', + }) + } + + return json( + { status, submission, message: errorMessage }, + { + status: status === 'error' || !createSuccess ? 422 : 201, + }, + ) +} + +export const AssetImageArtworkVersionUpdateVisible = ({ + image, + artworkVersion, +}: { + image: IAssetImage + artworkVersion: IArtworkVersion +}) => { + const imageId = image.id + const artworkVersionId = artworkVersion.id + const isVisible = image.visible + const icon = isVisible ? 'eye-open' : 'eye-closed' + const iconText = `${isVisible ? 'Hide' : 'Show'} ${image.name}` + const formId = `asset-image--${imageId}-artworkVersion-${artworkVersionId}-update-visible` + + const fetcher = useFetcher() + let isHydrated = useHydrated() + + return ( + +
+ + +
+
+ ) +} diff --git a/app/routes/resources+/api.v1+/asset.update.name.tsx b/app/routes/resources+/api.v1+/asset.update.name.tsx new file mode 100644 index 00000000..3754ee6a --- /dev/null +++ b/app/routes/resources+/api.v1+/asset.update.name.tsx @@ -0,0 +1,108 @@ +import { + json, + type ActionFunctionArgs, + type LoaderFunctionArgs, +} from '@remix-run/node' +import { useFetcher } from '@remix-run/react' +import { redirectBack } from 'remix-utils/redirect-back' +import { useHydrated } from 'remix-utils/use-hydrated' +import { FetcherText } from '#app/components/templates/form/fetcher-text' +import { type IAssetType } from '#app/models/asset/asset.server' +import { + updateDesignTypeFillValue, + validateDesignTypeUpdateFillValueSubmission, +} from '#app/models/design-type/fill/fill.update.server' +import { EntityParentIdType } from '#app/schema/entity' +import { EditDesignFillValueSchema } from '#app/schema/fill' +import { validateNoJS } from '#app/schema/form-data' +import { requireUserId } from '#app/utils/auth.server' +import { Routes } from '#app/utils/routes.const' + +// https://www.epicweb.dev/full-stack-components + +const route = Routes.RESOURCES.API.V1.DESIGN.TYPE.FILL.UPDATE.VALUE +const schema = EditDesignFillValueSchema + +// auth GET request to endpoint +export async function loader({ request }: LoaderFunctionArgs) { + await requireUserId(request) + return json({}) +} + +export async function action({ request }: ActionFunctionArgs) { + const userId = await requireUserId(request) + const formData = await request.formData() + const noJS = validateNoJS({ formData }) + + let updateSuccess = false + const { status, submission } = + await validateDesignTypeUpdateFillValueSubmission({ + userId, + formData, + }) + + if (status === 'success') { + const { success } = await updateDesignTypeFillValue({ + userId, + ...submission.value, + }) + updateSuccess = success + } + + if (noJS) { + throw redirectBack(request, { + fallback: '/', + }) + } + + return json( + { status, submission }, + { + status: status === 'error' || !updateSuccess ? 404 : 200, + }, + ) +} + +export const AssetUpdateName = ({ + asset, + formLocation = '', +}: { + asset: IAssetType + formLocation?: string +}) => { + const assetId = asset.id + const field = 'name' + const fetcherKey = `asset-update-${field}-${assetId}` + const formId = `${fetcherKey}${formLocation ? `-${formLocation}` : ''}` + const value = asset[field] + + let isHydrated = useHydrated() + const fetcher = useFetcher({ + key: fetcherKey, + }) + + return ( + +
+ + +
+
+ ) +} diff --git a/app/schema/asset/__shared.ts b/app/schema/asset/__shared.ts index 14193c67..9dacf696 100644 --- a/app/schema/asset/__shared.ts +++ b/app/schema/asset/__shared.ts @@ -8,3 +8,10 @@ export const AssetDescriptionSchema = z .string() .max(MAX_DESCRIPTION_LENGTH) .optional() + +export const AssetDataSchema = z.object({ + name: AssetNameSchema, + description: AssetDescriptionSchema, + visible: z.boolean(), + ownerId: z.string(), +}) diff --git a/app/schema/asset/image.artwork-version.ts b/app/schema/asset/image.artwork-version.ts index 07f6a539..50d2dbcb 100644 --- a/app/schema/asset/image.artwork-version.ts +++ b/app/schema/asset/image.artwork-version.ts @@ -14,13 +14,27 @@ export const AssetImageArtworkVersionDataSchema = AssetImageDataSchema.merge( ArtworkVersionParentSchema, ) +export const AssetImageArtworkVersionVisibleDataSchema = z.object({ + visible: z.boolean(), +}) + export const NewAssetImageArtworkVersionSchema = NewAssetImageSchema.merge( ArtworkVersionParentSchema, ) +// this is more like a clone from artwork +export const CloneAssetImageArtworkToArtworkVersionSchema = z.object({ + assetImageId: z.string(), + artworkVersionId: z.string(), +}) export const EditAssetImageArtworkVersionSchema = EditAssetImageSchema.merge( ArtworkVersionParentSchema, ) +export const EditVisibleAssetImageArtworkVersionSchema = z.object({ + id: z.string(), + artworkVersionId: z.string(), +}) + export const DeleteAssetImageArtworkVersionSchema = DeleteAssetImageSchema.merge(ArtworkVersionParentSchema) diff --git a/app/schema/asset/image.ts b/app/schema/asset/image.ts index 7867cce5..30bdb773 100644 --- a/app/schema/asset/image.ts +++ b/app/schema/asset/image.ts @@ -1,5 +1,9 @@ import { z } from 'zod' -import { AssetDescriptionSchema, AssetNameSchema } from './__shared' +import { + AssetDataSchema, + AssetDescriptionSchema, + AssetNameSchema, +} from './__shared' const MAX_ALT_TEXT_LENGTH = 240 const AltTextSchema = z.string().max(MAX_ALT_TEXT_LENGTH).optional() @@ -46,13 +50,12 @@ export const AssetAttributesImageSchema = z.object({ // zod schema for blob Buffer/File is not working // pass in separately from validation -export const AssetImageDataSchema = z.object({ - name: AssetNameSchema, - description: AssetDescriptionSchema, - type: z.literal('image'), - attributes: AssetAttributesImageSchema, - ownerId: z.string(), -}) +export const AssetImageDataSchema = z + .object({ + type: z.literal('image'), + attributes: AssetAttributesImageSchema, + }) + .merge(AssetDataSchema) // form data validation diff --git a/app/schema/entity.ts b/app/schema/entity.ts index af31ea12..63618b77 100644 --- a/app/schema/entity.ts +++ b/app/schema/entity.ts @@ -48,7 +48,7 @@ export type IEntity = | ITemplate | IAssetType -export type IEntityVisible = IDesign | IDesignWithType | ILayer +export type IEntityVisible = IDesign | IDesignWithType | ILayer | IAssetType export type IEntitySelectable = ILayer export type IEntityWithSlug = | IArtwork diff --git a/app/services/asset.image.artwork-version.delete.service.ts b/app/services/asset.image.artwork-version.delete.service.ts new file mode 100644 index 00000000..7382aa2d --- /dev/null +++ b/app/services/asset.image.artwork-version.delete.service.ts @@ -0,0 +1,31 @@ +import { + deleteAssetImage, + type IAssetImageDeletedResponse, +} from '#app/models/asset/image/image.delete.server' +import { type IAssetImage } from '#app/models/asset/image/image.server' +import { type IUser } from '#app/models/user/user.server' +import { prisma } from '#app/utils/db.server' + +export const assetImageArtworkVersionDeleteService = async ({ + userId, + id, +}: { + userId: IUser['id'] + id: IAssetImage['id'] +}): Promise => { + try { + // Step 1: delete the asset image via promise + const deleteAssetImageArtworkVersionPromise = deleteAssetImage({ id }) + + // Step 2: execute the transaction + await prisma.$transaction([deleteAssetImageArtworkVersionPromise]) + + return { success: true } + } catch (error) { + console.log(error) + return { + success: false, + message: 'Unknown error: assetImageArtworkVersionDeleteService', + } + } +} diff --git a/app/services/asset.image.artwork-version.update.visible.service.ts b/app/services/asset.image.artwork-version.update.visible.service.ts new file mode 100644 index 00000000..9749dbec --- /dev/null +++ b/app/services/asset.image.artwork-version.update.visible.service.ts @@ -0,0 +1,51 @@ +import { invariant } from '@epic-web/invariant' +import { getAssetImage } from '#app/models/asset/image/image.get.server' +import { + updateAssetImageArtworkVersionVisible, + type IAssetImageArtworkVersionUpdateVisibleSubmission, +} from '#app/models/asset/image/image.update.artwork-version.visible.server' +import { type IAssetImageUpdatedResponse } from '#app/models/asset/image/image.update.server' +import { AssetImageArtworkVersionVisibleDataSchema } from '#app/schema/asset/image.artwork-version' +import { prisma } from '#app/utils/db.server' + +export const assetImageArtworkVersionUpdateVisibleService = async ({ + userId, + id, + artworkVersionId, +}: IAssetImageArtworkVersionUpdateVisibleSubmission): Promise => { + try { + // Step 1: verify the asset image exists + const assetImage = await getAssetImage({ + where: { id, artworkVersionId, ownerId: userId }, + }) + invariant(assetImage, 'Asset Image not found') + + // Step 2: validate asset image data + const data = { + visible: !assetImage.visible, + } + const assetImageData = AssetImageArtworkVersionVisibleDataSchema.parse(data) + + // Step 3: update the asset image via promise + const updateAssetImagePromise = updateAssetImageArtworkVersionVisible({ + id, + data: { ...assetImageData }, + }) + + // Step 4: execute the transaction + const [updatedAssetImage] = await prisma.$transaction([ + updateAssetImagePromise, + ]) + + return { + updatedAssetImage, + success: true, + } + } catch (error) { + console.error(error) + return { + success: false, + message: 'Unknown error: assetImageArtworkVersionUpdateVisibleService', + } + } +} diff --git a/app/strategies/component/dashboard-panel/delete-entity.strategy.ts b/app/strategies/component/dashboard-panel/delete-entity.strategy.ts index 9ea1caee..8b71243d 100644 --- a/app/strategies/component/dashboard-panel/delete-entity.strategy.ts +++ b/app/strategies/component/dashboard-panel/delete-entity.strategy.ts @@ -12,6 +12,14 @@ export interface IDashboardPanelDeleteEntityStrategy { parentType: entityParentTypeEnum } +export class DashboardPanelDeleteArtworkVersionAssetStrategy + implements IDashboardPanelDeleteEntityStrategy +{ + actionType: entityActionTypeEnum = EntityActionType.DELETE + entityType: entityTypeEnum = EntityType.ASSET + parentType: entityParentTypeEnum = EntityParentType.ARTWORK_VERSION +} + export class DashboardPanelDeleteArtworkVersionDesignStrategy implements IDashboardPanelDeleteEntityStrategy { diff --git a/app/strategies/component/dashboard-panel/entity-action/entity-action.ts b/app/strategies/component/dashboard-panel/entity-action/entity-action.ts index 7057042c..86896749 100644 --- a/app/strategies/component/dashboard-panel/entity-action/entity-action.ts +++ b/app/strategies/component/dashboard-panel/entity-action/entity-action.ts @@ -1,10 +1,12 @@ import { + DashboardPanelDeleteArtworkVersionAssetStrategy, DashboardPanelDeleteArtworkVersionDesignStrategy, DashboardPanelDeleteLayerDesignStrategy, type IDashboardPanelDeleteEntityStrategy, } from '../delete-entity.strategy' import { DashboardPanelSelectArtworkVersionLayerStrategy } from '../update-entity-selected.strategy' import { + DashboardPanelUpdateArtworkVersionAssetVisibleStrategy, DashboardPanelUpdateArtworkVersionDesignVisibleStrategy, DashboardPanelUpdateArtworkVersionLayerVisibleStrategy, DashboardPanelUpdateLayerDesignVisibleStrategy, @@ -19,7 +21,18 @@ export interface IDashboardPanelEntityActionStrategy { getPanelActions(): IPanelEntityActionStrategy[] } -// artwork design +export class DashboardPanelArtworkVersionAssetActionStrategy + implements IDashboardPanelEntityActionStrategy +{ + getPanelActions(): IPanelEntityActionStrategy[] { + const strategyToggleVisible = + new DashboardPanelUpdateArtworkVersionAssetVisibleStrategy() + const strategyDelete = new DashboardPanelDeleteArtworkVersionAssetStrategy() + + return [strategyToggleVisible, strategyDelete] + } +} + export class DashboardPanelArtworkVersionDesignActionStrategy implements IDashboardPanelEntityActionStrategy { @@ -41,8 +54,7 @@ export class DashboardPanelArtworkVersionLayerActionStrategy const strategyToggleVisible = new DashboardPanelUpdateArtworkVersionLayerVisibleStrategy() - const strategySelect = - new DashboardPanelSelectArtworkVersionLayerStrategy() + const strategySelect = new DashboardPanelSelectArtworkVersionLayerStrategy() // delete in popover so it's less easy to click accidentally from left sidebar diff --git a/app/strategies/component/dashboard-panel/update-entity-order.strategy.ts b/app/strategies/component/dashboard-panel/update-entity-order.strategy.ts index 0369c02e..4c314863 100644 --- a/app/strategies/component/dashboard-panel/update-entity-order.strategy.ts +++ b/app/strategies/component/dashboard-panel/update-entity-order.strategy.ts @@ -10,6 +10,13 @@ export interface IDashboardPanelUpdateEntityOrderStrategy { parentType: entityParentTypeEnum } +export class DashboardPanelUpdateArtworkVersionAssetTypeOrderStrategy + implements IDashboardPanelUpdateEntityOrderStrategy +{ + entityType: entityTypeEnum = EntityType.ASSET + parentType: entityParentTypeEnum = EntityParentType.ARTWORK_VERSION +} + export class DashboardPanelUpdateArtworkVersionDesignTypeOrderStrategy implements IDashboardPanelUpdateEntityOrderStrategy { diff --git a/app/strategies/component/dashboard-panel/update-entity-visible.strategy.ts b/app/strategies/component/dashboard-panel/update-entity-visible.strategy.ts index 94a4dcb1..97020291 100644 --- a/app/strategies/component/dashboard-panel/update-entity-visible.strategy.ts +++ b/app/strategies/component/dashboard-panel/update-entity-visible.strategy.ts @@ -13,6 +13,14 @@ export interface IDashboardPanelUpdateEntityVisibleStrategy { parentType: entityParentTypeEnum } +export class DashboardPanelUpdateArtworkVersionAssetVisibleStrategy + implements IDashboardPanelUpdateEntityVisibleStrategy +{ + actionType: entityActionTypeEnum = EntityActionType.TOGGLE_VISIBLE + entityType: entityTypeEnum = EntityType.ASSET + parentType: entityParentTypeEnum = EntityParentType.ARTWORK_VERSION +} + export class DashboardPanelUpdateArtworkVersionDesignVisibleStrategy implements IDashboardPanelUpdateEntityVisibleStrategy { diff --git a/app/utils/routes.const.ts b/app/utils/routes.const.ts index b7178ee8..43fac77d 100644 --- a/app/utils/routes.const.ts +++ b/app/utils/routes.const.ts @@ -45,8 +45,10 @@ export const Routes = { }, ARTWORK_VERSION: { CREATE: `${pathBase}/asset/image/artwork-version/create`, + CLONE: `${pathBase}/asset/image/artwork-version/clone`, DELETE: `${pathBase}/asset/image/artwork-version/delete`, UPDATE: `${pathBase}/asset/image/artwork-version/update`, + UPDATE_VISIBLE: `${pathBase}/asset/image/artwork-version/update/visible`, }, }, }, diff --git a/package-lock.json b/package-lock.json index d70113ab..668e59f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@radix-ui/react-hover-card": "^1.0.7", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.0.3", @@ -2732,6 +2733,38 @@ } } }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.1.3.tgz", + "integrity": "sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-roving-focus": "1.0.4", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-use-size": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz", @@ -22916,6 +22949,24 @@ "@radix-ui/react-slot": "1.0.2" } }, + "@radix-ui/react-radio-group": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.1.3.tgz", + "integrity": "sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-roving-focus": "1.0.4", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-use-size": "1.0.1" + } + }, "@radix-ui/react-roving-focus": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz", diff --git a/package.json b/package.json index 0ba5ddf8..4ddcc910 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "@radix-ui/react-hover-card": "^1.0.7", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.0.3", diff --git a/prisma/migrations/20240613070911_add_visible_to_asset/migration.sql b/prisma/migrations/20240613070911_add_visible_to_asset/migration.sql new file mode 100644 index 00000000..be677a54 --- /dev/null +++ b/prisma/migrations/20240613070911_add_visible_to_asset/migration.sql @@ -0,0 +1,30 @@ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Asset" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "description" TEXT, + "type" TEXT NOT NULL, + "attributes" TEXT NOT NULL DEFAULT '{}', + "blob" BLOB, + "visible" BOOLEAN NOT NULL DEFAULT true, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "ownerId" TEXT NOT NULL, + "projectId" TEXT, + "artworkId" TEXT, + "artworkVersionId" TEXT, + "layerId" TEXT, + CONSTRAINT "Asset_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "Asset_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "Asset_artworkId_fkey" FOREIGN KEY ("artworkId") REFERENCES "Artwork" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "Asset_artworkVersionId_fkey" FOREIGN KEY ("artworkVersionId") REFERENCES "ArtworkVersion" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "Asset_layerId_fkey" FOREIGN KEY ("layerId") REFERENCES "Layer" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); +INSERT INTO "new_Asset" ("artworkId", "artworkVersionId", "attributes", "blob", "createdAt", "description", "id", "layerId", "name", "ownerId", "projectId", "type", "updatedAt") SELECT "artworkId", "artworkVersionId", "attributes", "blob", "createdAt", "description", "id", "layerId", "name", "ownerId", "projectId", "type", "updatedAt" FROM "Asset"; +DROP TABLE "Asset"; +ALTER TABLE "new_Asset" RENAME TO "Asset"; +CREATE INDEX "Asset_ownerId_idx" ON "Asset"("ownerId"); +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4d955fb6..3e90b049 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -549,6 +549,7 @@ model Asset { type String // e.g. image, media, palette, gradient, shapes, etc. attributes String @default("{}") // json string of attributes specific to the type blob Bytes? + visible Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt