From 28cf98b61a6046cbbf7025f7c9624ae80738250f Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Mon, 22 Apr 2024 17:26:08 -0400 Subject: [PATCH] panel entity update design type rotate through fetcher template and api --- .../design-type.update.rotate.server.ts | 122 ++++++++++++++++ .../design.type.rotate.update.basis.tsx | 53 +++++++ .../design.type.rotate.update.value.tsx | 53 +++++++ ...idebars.panel.artboard-version.designs.tsx | 2 +- app/schema/rotate.ts | 4 + .../design-type/update-rotate.service.ts | 68 +++++++++ ...update-entity-values.design.type.rotate.ts | 135 ++++++++++++++++++ app/utils/design.ts | 15 +- app/utils/routes.utils.ts | 30 ++++ 9 files changed, 475 insertions(+), 7 deletions(-) create mode 100644 app/models/design-type/design-type.update.rotate.server.ts create mode 100644 app/routes/resources+/api.v1+/design.type.rotate.update.basis.tsx create mode 100644 app/routes/resources+/api.v1+/design.type.rotate.update.value.tsx create mode 100644 app/services/design-type/update-rotate.service.ts create mode 100644 app/strategies/component/dashboard-panel/update-entity/update-entity-values.design.type.rotate.ts diff --git a/app/models/design-type/design-type.update.rotate.server.ts b/app/models/design-type/design-type.update.rotate.server.ts new file mode 100644 index 00000000..34f7e0c1 --- /dev/null +++ b/app/models/design-type/design-type.update.rotate.server.ts @@ -0,0 +1,122 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { + type DesignRotateUpdateSchemaType, + EditDesignRotateBasisSchema, + EditDesignRotateValueSchema, +} from '#app/schema/rotate' +import { ValidateDesignParentSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { validateEntitySubmission } from '#app/utils/conform-utils' +import { findFirstRotateInstance } from '#app/utils/prisma-extensions-rotate' +import { type IDesign } from '../design.server' +import { type IRotate } from '../rotate.server' + +export interface IDesignTypeRotateUpdatedResponse { + success: boolean + message?: string + updatedRotate?: IRotate +} + +const validateUpdateSubmission = async ({ + userId, + formData, + schema, +}: IntentActionArgs & { + schema: DesignRotateUpdateSchemaType +}) => { + const strategy = new ValidateDesignParentSubmissionStrategy() + + return await validateEntitySubmission({ + userId, + formData, + schema, + strategy, + }) +} + +export async function validateDesignTypeUpdateRotateValueSubmission( + args: IntentActionArgs, +) { + return validateUpdateSubmission({ + ...args, + schema: EditDesignRotateValueSchema, + }) +} + +export async function validateDesignTypeUpdateRotateBasisSubmission( + args: IntentActionArgs, +) { + return validateUpdateSubmission({ + ...args, + schema: EditDesignRotateBasisSchema, + }) +} + +const getRotateInstance = async ({ id }: { id: IRotate['id'] }) => { + return await findFirstRotateInstance({ + where: { id }, + }) +} + +// updating instance instead of regular prism update +// this may not be easier, but it's more explicit +export const updateDesignTypeRotateValue = async ({ + id, + designId, + value, +}: { + id: IRotate['id'] + designId: IDesign['id'] + value: number +}): Promise => { + const rotate = await getRotateInstance({ id }) + if (!rotate) return { success: false } + + try { + const data = EditDesignRotateValueSchema.parse({ id, designId, value }) + rotate.value = data.value + rotate.updatedAt = new Date() + await rotate.save() + + return { success: true, updatedRotate: rotate } + } catch (error) { + // consider how to handle this error where this is called + console.log('updateDesignTypeRotateValue error:', error) + const errorType = error instanceof Error + const errorMessage = errorType ? error.message : 'An unknown error occurred' + return { + success: false, + message: errorMessage, + } + } +} + +export const updateDesignTypeRotateBasis = async ({ + id, + designId, + basis, +}: { + id: IRotate['id'] + designId: IDesign['id'] + basis: string +}): Promise => { + const rotate = await getRotateInstance({ id }) + if (!rotate) return { success: false } + + try { + const data = EditDesignRotateBasisSchema.parse({ id, designId, basis }) + rotate.basis = data.basis + rotate.updatedAt = new Date() + await rotate.save() + + return { success: true, updatedRotate: rotate } + } catch (error) { + // consider how to handle this error where this is called + console.log('updateDesignTypeRotateBasis error:', error) + const errorType = error instanceof Error + const errorMessage = errorType ? error.message : 'An unknown error occurred' + return { + success: false, + message: errorMessage, + } + } +} diff --git a/app/routes/resources+/api.v1+/design.type.rotate.update.basis.tsx b/app/routes/resources+/api.v1+/design.type.rotate.update.basis.tsx new file mode 100644 index 00000000..57ac145a --- /dev/null +++ b/app/routes/resources+/api.v1+/design.type.rotate.update.basis.tsx @@ -0,0 +1,53 @@ +import { + type LoaderFunctionArgs, + json, + type DataFunctionArgs, +} from '@remix-run/node' +import { redirectBack } from 'remix-utils/redirect-back' +import { + updateDesignTypeRotateBasis, + validateDesignTypeUpdateRotateBasisSubmission, +} from '#app/models/design-type/design-type.update.rotate.server' +import { validateNoJS } from '#app/schema/form-data' +import { requireUserId } from '#app/utils/auth.server' + +// https://www.epicweb.dev/full-stack-components + +export async function loader({ request }: LoaderFunctionArgs) { + await requireUserId(request) + return json({}) +} + +export async function action({ request }: DataFunctionArgs) { + const userId = await requireUserId(request) + const formData = await request.formData() + const noJS = validateNoJS({ formData }) + + let updateSuccess = false + const { status, submission } = + await validateDesignTypeUpdateRotateBasisSubmission({ + userId, + formData, + }) + + if (status === 'success') { + const { success } = await updateDesignTypeRotateBasis({ + userId, + ...submission.value, + }) + updateSuccess = success + } + + if (noJS) { + throw redirectBack(request, { + fallback: '/', + }) + } + + return json( + { status, submission }, + { + status: status === 'error' || !updateSuccess ? 404 : 200, + }, + ) +} diff --git a/app/routes/resources+/api.v1+/design.type.rotate.update.value.tsx b/app/routes/resources+/api.v1+/design.type.rotate.update.value.tsx new file mode 100644 index 00000000..c453b8e5 --- /dev/null +++ b/app/routes/resources+/api.v1+/design.type.rotate.update.value.tsx @@ -0,0 +1,53 @@ +import { + type LoaderFunctionArgs, + json, + type DataFunctionArgs, +} from '@remix-run/node' +import { redirectBack } from 'remix-utils/redirect-back' +import { + updateDesignTypeRotateValue, + validateDesignTypeUpdateRotateValueSubmission, +} from '#app/models/design-type/design-type.update.rotate.server' +import { validateNoJS } from '#app/schema/form-data' +import { requireUserId } from '#app/utils/auth.server' + +// https://www.epicweb.dev/full-stack-components + +export async function loader({ request }: LoaderFunctionArgs) { + await requireUserId(request) + return json({}) +} + +export async function action({ request }: DataFunctionArgs) { + const userId = await requireUserId(request) + const formData = await request.formData() + const noJS = validateNoJS({ formData }) + + let updateSuccess = false + const { status, submission } = + await validateDesignTypeUpdateRotateValueSubmission({ + userId, + formData, + }) + + if (status === 'success') { + const { success } = await updateDesignTypeRotateValue({ + userId, + ...submission.value, + }) + updateSuccess = success + } + + if (noJS) { + throw redirectBack(request, { + fallback: '/', + }) + } + + return json( + { status, submission }, + { + status: status === 'error' || !updateSuccess ? 404 : 200, + }, + ) +} diff --git a/app/routes/sketch+/projects+/$projectSlug_+/artboards+/$artboardSlug+/__components/sidebars.panel.artboard-version.designs.tsx b/app/routes/sketch+/projects+/$projectSlug_+/artboards+/$artboardSlug+/__components/sidebars.panel.artboard-version.designs.tsx index 0f0d488c..4f54d2aa 100644 --- a/app/routes/sketch+/projects+/$projectSlug_+/artboards+/$artboardSlug+/__components/sidebars.panel.artboard-version.designs.tsx +++ b/app/routes/sketch+/projects+/$projectSlug_+/artboards+/$artboardSlug+/__components/sidebars.panel.artboard-version.designs.tsx @@ -22,7 +22,7 @@ export const PanelArtboardVersionDesigns = ({ designs: orderedDesigns, }) // remove trim after testing actions work for one design type - const designsTrimmed = designTypePanels.slice(0, 6) + const designsTrimmed = designTypePanels.slice(0, 7) // const designsTrimmed = designTypePanels const strategyEntityNew = diff --git a/app/schema/rotate.ts b/app/schema/rotate.ts index 56d880ff..e4b9ec2b 100644 --- a/app/schema/rotate.ts +++ b/app/schema/rotate.ts @@ -43,6 +43,10 @@ export const RotateDataSchema = z.object({ basis: RotateBasisSchema.optional(), }) +export type DesignRotateUpdateSchemaType = + | typeof EditDesignRotateValueSchema + | typeof EditDesignRotateBasisSchema + export const EditDesignRotateValueSchema = z.object({ id: z.string(), designId: z.string(), diff --git a/app/services/design-type/update-rotate.service.ts b/app/services/design-type/update-rotate.service.ts new file mode 100644 index 00000000..f6443de9 --- /dev/null +++ b/app/services/design-type/update-rotate.service.ts @@ -0,0 +1,68 @@ +import { type User } from '@sentry/remix' +import { + type IDesignTypeRotateUpdatedResponse, + updateDesignTypeRotateValue, + updateDesignTypeRotateBasis, +} from '#app/models/design-type/design-type.update.rotate.server' +import { type IDesign } from '#app/models/design.server' +import { type IRotate } from '#app/models/rotate.server' + +export const updateDesignTypeRotateValueService = async ({ + userId, + id, + designId, + value, +}: { + userId: User['id'] + id: IRotate['id'] + designId: IDesign['id'] + value: number +}): Promise => { + try { + return await updateDesignTypeRotateValue({ + id, + designId, + value, + }) + // later will be adding Activity class + // i.e, edit history so you can undo changes and/or see who made them + } catch (error) { + console.log('updateDesignTypeRotateValueService error:', error) + const errorType = error instanceof Error + const errorMessage = errorType ? error.message : 'An unknown error occurred' + return { + success: false, + message: errorMessage, + } + } +} + +export const updateDesignTypeRotateBasisService = async ({ + userId, + id, + designId, + basis, +}: { + userId: User['id'] + id: IRotate['id'] + designId: IDesign['id'] + basis: string +}): Promise => { + try { + return await updateDesignTypeRotateBasis({ + id, + designId, + basis, + }) + // later will be adding Activity class + // i.e, edit history so you can undo changes and/or see who made them + } catch (error) { + console.log('updateDesignTypeRotateBasisService error:', error) + const errorType = error instanceof Error + const errorMessage = errorType ? error.message : 'An unknown error occurred' + return { + success: false, + message: errorMessage, + } + } +} diff --git a/app/strategies/component/dashboard-panel/update-entity/update-entity-values.design.type.rotate.ts b/app/strategies/component/dashboard-panel/update-entity/update-entity-values.design.type.rotate.ts new file mode 100644 index 00000000..fa148869 --- /dev/null +++ b/app/strategies/component/dashboard-panel/update-entity/update-entity-values.design.type.rotate.ts @@ -0,0 +1,135 @@ +import { type IDesignWithRotate } from '#app/models/design.server' +import { EntityFormType, EntityParentIdType } from '#app/schema/entity' +import { + EditDesignRotateBasisSchema, + EditDesignRotateValueSchema, + RotateBasisTypeEnum, +} from '#app/schema/rotate' +import { Routes } from '#app/utils/routes.utils' +import { transformEntityEnumValueForSelect } from '#app/utils/string-formatting' +import { + type IPanelEntityFormArgsOptionalMultiple, + type IDashboardPanelUpdateEntityValuesStrategy, + type IPanelEntityFormArgs, + type IDashboardPanelIcon, +} from './update-entity-values' + +const baseRoute = Routes.RESOURCES.API.V1.DESIGN.TYPE.ROTATE.UPDATE +const globalEntityFormArgs = { + parentTypeId: EntityParentIdType.DESIGN_ID, +} +const globalRotateValueArgs = { + route: baseRoute.VALUE, + formType: EntityFormType.HEX, + formId: 'design-type-update-rotate-count', + schema: EditDesignRotateValueSchema, + label: 'Value', +} +const globalRotateBasisArgs = { + route: baseRoute.BASIS, + formType: EntityFormType.SELECT, + formId: 'design-type-update-rotate-basis', + schema: EditDesignRotateBasisSchema, + label: 'Basis', +} + +export class DashboardPanelUpdateDesignTypeRotateValuesStrategy + implements IDashboardPanelUpdateEntityValuesStrategy +{ + getMainPanelForm({ + entity, + }: { + entity: IDesignWithRotate + }): IPanelEntityFormArgsOptionalMultiple { + const { rotate } = entity + const { value, basis } = rotate + + const sharedEntityFormArgs = { + ...globalEntityFormArgs, + entityId: rotate.id, + parentId: entity.id, + } + + if (basis !== RotateBasisTypeEnum.DEFINED) { + const optionsBasis = Object.values(RotateBasisTypeEnum).map( + rotateBasisEnum => ({ + [rotateBasisEnum]: transformEntityEnumValueForSelect(rotateBasisEnum), + }), + ) + + const RotateBasisArgs = { + ...sharedEntityFormArgs, + ...globalRotateBasisArgs, + options: optionsBasis, + defaultValue: { basis }, + } + return RotateBasisArgs + } + + const RotateValueArgs = { + ...sharedEntityFormArgs, + ...globalRotateValueArgs, + defaultValue: { value }, + } + return RotateValueArgs + } + + getPopoverForms({ + entity, + }: { + entity: IDesignWithRotate + }): IPanelEntityFormArgs[] { + const { rotate } = entity + const { value, basis } = rotate + const sharedEntityFormArgs = { + ...globalEntityFormArgs, + entityId: rotate.id, + parentId: entity.id, + } + + const RotateValueArgs = { + ...sharedEntityFormArgs, + ...globalRotateValueArgs, + defaultValue: { value }, + } + + const optionsBasis = Object.values(RotateBasisTypeEnum).map( + rotateBasisEnum => ({ + [rotateBasisEnum]: transformEntityEnumValueForSelect(rotateBasisEnum), + }), + ) + + const RotateBasisArgs = { + ...sharedEntityFormArgs, + ...globalRotateBasisArgs, + options: optionsBasis, + defaultValue: { basis }, + } + + return [RotateValueArgs, RotateBasisArgs] + } + + getPopoverTriggerColor({ entity }: { entity: IDesignWithRotate }): undefined { + return undefined + } + + getPanelFormatIcon({ entity }: { entity: IDesignWithRotate }): undefined { + return undefined + } + + getPanelBasisIcon({ + entity, + }: { + entity: IDesignWithRotate + }): IDashboardPanelIcon | undefined { + const { rotate } = entity + const { basis } = rotate + + if (basis !== RotateBasisTypeEnum.DEFINED) return undefined + + return { + symbol: '°', + text: `Rotate Basis: ${basis}`, + } + } +} diff --git a/app/utils/design.ts b/app/utils/design.ts index 5f81b572..90ec08ce 100644 --- a/app/utils/design.ts +++ b/app/utils/design.ts @@ -27,6 +27,7 @@ import { DashboardPanelUpdateDesignTypeFillValuesStrategy } from '#app/strategie import { DashboardPanelUpdateDesignTypeLayoutValuesStrategy } from '#app/strategies/component/dashboard-panel/update-entity/update-entity-values.design.type.layout' import { DashboardPanelUpdateDesignTypeLineValuesStrategy } from '#app/strategies/component/dashboard-panel/update-entity/update-entity-values.design.type.line' import { DashboardPanelUpdateDesignTypePaletteValuesStrategy } from '#app/strategies/component/dashboard-panel/update-entity/update-entity-values.design.type.palette' +import { DashboardPanelUpdateDesignTypeRotateValuesStrategy } from '#app/strategies/component/dashboard-panel/update-entity/update-entity-values.design.type.rotate' import { DashboardPanelUpdateDesignTypeSizeValuesStrategy } from '#app/strategies/component/dashboard-panel/update-entity/update-entity-values.design.type.size' import { DashboardPanelUpdateDesignTypeStrokeValuesStrategy } from '#app/strategies/component/dashboard-panel/update-entity/update-entity-values.design.type.stroke' import { orderLinkedItems } from './linked-list.utils' @@ -391,7 +392,7 @@ export const designsByTypeToPanelArray = ({ designFills, designStrokes, designLines, - // designRotates, + designRotates, designLayouts, // designTemplates, } = designs @@ -408,6 +409,8 @@ export const designsByTypeToPanelArray = ({ new DashboardPanelUpdateDesignTypeStrokeValuesStrategy() const strategyLineEntityValues = new DashboardPanelUpdateDesignTypeLineValuesStrategy() + const strategyRotateEntityValues = + new DashboardPanelUpdateDesignTypeRotateValuesStrategy() return [ { @@ -440,11 +443,11 @@ export const designsByTypeToPanelArray = ({ designs: designLines, strategyEntityValues: strategyLineEntityValues, }, - // { - // type: DesignTypeEnum.ROTATE, - // designs: designRotates, - // strategyEntityValues, - // }, + { + type: DesignTypeEnum.ROTATE, + designs: designRotates, + strategyEntityValues: strategyRotateEntityValues, + }, // { // type: DesignTypeEnum.TEMPLATE, // designs: designTemplates, diff --git a/app/utils/routes.utils.ts b/app/utils/routes.utils.ts index 9565bc14..af13e9e0 100644 --- a/app/utils/routes.utils.ts +++ b/app/utils/routes.utils.ts @@ -71,6 +71,14 @@ import { loader as apiV1DesignTypePaletteValueLoader, action as apiV1DesignTypePaletteValueAction, } from '#app/routes/resources+/api.v1+/design.type.palette.update.value' +import { + loader as apiV1DesignTypeRotateBasisLoader, + action as apiV1DesignTypeRotateBasisAction, +} from '#app/routes/resources+/api.v1+/design.type.rotate.update.basis' +import { + loader as apiV1DesignTypeRotateValueLoader, + action as apiV1DesignTypeRotateValueAction, +} from '#app/routes/resources+/api.v1+/design.type.rotate.update.value' import { loader as apiV1DesignTypeSizeBasisLoader, action as apiV1DesignTypeSizeBasisAction, @@ -162,6 +170,12 @@ export const Routes = { WIDTH: `${pathBase}/design/type/line/update/width`, }, }, + ROTATE: { + UPDATE: { + BASIS: `${pathBase}/design/type/rotate/update/basis`, + VALUE: `${pathBase}/design/type/rotate/update/value`, + }, + }, }, }, }, @@ -218,6 +232,10 @@ export interface ApiRouteLoaders { .FORMAT]: typeof apiV1DesignTypeLineFormatLoader [Routes.RESOURCES.API.V1.DESIGN.TYPE.LINE.UPDATE .WIDTH]: typeof apiV1DesignTypeLineWidthLoader + [Routes.RESOURCES.API.V1.DESIGN.TYPE.ROTATE.UPDATE + .VALUE]: typeof apiV1DesignTypeRotateValueLoader + [Routes.RESOURCES.API.V1.DESIGN.TYPE.ROTATE.UPDATE + .BASIS]: typeof apiV1DesignTypeRotateBasisLoader } export const loaders: ApiRouteLoaders = { @@ -269,6 +287,10 @@ export const loaders: ApiRouteLoaders = { apiV1DesignTypeLineFormatLoader, [Routes.RESOURCES.API.V1.DESIGN.TYPE.LINE.UPDATE.WIDTH]: apiV1DesignTypeLineWidthLoader, + [Routes.RESOURCES.API.V1.DESIGN.TYPE.ROTATE.UPDATE.VALUE]: + apiV1DesignTypeRotateValueLoader, + [Routes.RESOURCES.API.V1.DESIGN.TYPE.ROTATE.UPDATE.BASIS]: + apiV1DesignTypeRotateBasisLoader, } export function getLoaderType( @@ -328,6 +350,10 @@ export interface ApiRouteActions { .BASIS]: typeof apiV1DesignTypeLineBasisAction [Routes.RESOURCES.API.V1.DESIGN.TYPE.LINE.UPDATE .FORMAT]: typeof apiV1DesignTypeLineFormatAction + [Routes.RESOURCES.API.V1.DESIGN.TYPE.ROTATE.UPDATE + .VALUE]: typeof apiV1DesignTypeRotateValueAction + [Routes.RESOURCES.API.V1.DESIGN.TYPE.ROTATE.UPDATE + .BASIS]: typeof apiV1DesignTypeRotateBasisAction } export const actions: ApiRouteActions = { @@ -379,6 +405,10 @@ export const actions: ApiRouteActions = { apiV1DesignTypeLineBasisAction, [Routes.RESOURCES.API.V1.DESIGN.TYPE.LINE.UPDATE.FORMAT]: apiV1DesignTypeLineFormatAction, + [Routes.RESOURCES.API.V1.DESIGN.TYPE.ROTATE.UPDATE.VALUE]: + apiV1DesignTypeRotateValueAction, + [Routes.RESOURCES.API.V1.DESIGN.TYPE.ROTATE.UPDATE.BASIS]: + apiV1DesignTypeRotateBasisAction, } export function getActionType(