From 1eaf76fce216238ae51443786d46cbb82ee91727 Mon Sep 17 00:00:00 2001 From: Ahmed Al-Taiar Date: Fri, 27 Oct 2023 13:25:08 -0400 Subject: [PATCH] Parts schema and scaffolld, with file uploading for the image (through Filestack) --- .env.defaults | 2 + .env.example | 2 + .../migration.sql | 9 + api/db/migrations/migration_lock.toml | 3 + api/db/schema.prisma | 14 +- api/package.json | 3 +- api/src/graphql/parts.sdl.ts | 35 +++ api/src/services/parts/parts.scenarios.ts | 11 + api/src/services/parts/parts.test.ts | 49 ++++ api/src/services/parts/parts.ts | 48 ++++ web/package.json | 4 + web/public/no_image.png | Bin 0 -> 19816 bytes web/src/App.tsx | 1 + web/src/Routes.tsx | 7 + .../Part/EditPartCell/EditPartCell.tsx | 68 +++++ web/src/components/Part/NewPart/NewPart.tsx | 44 ++++ web/src/components/Part/Part/Part.tsx | 94 +++++++ web/src/components/Part/PartCell/PartCell.tsx | 30 +++ web/src/components/Part/PartForm/PartForm.tsx | 153 +++++++++++ web/src/components/Part/Parts/Parts.tsx | 110 ++++++++ .../components/Part/PartsCell/PartsCell.tsx | 40 +++ .../layouts/ScaffoldLayout/ScaffoldLayout.tsx | 37 +++ web/src/lib/formatters.test.tsx | 192 ++++++++++++++ web/src/lib/formatters.tsx | 58 +++++ .../pages/Part/EditPartPage/EditPartPage.tsx | 11 + .../pages/Part/NewPartPage/NewPartPage.tsx | 7 + web/src/pages/Part/PartPage/PartPage.tsx | 11 + web/src/pages/Part/PartsPage/PartsPage.tsx | 7 + web/src/scaffold.css | 243 ++++++++++++++++++ web/tsconfig.json | 2 +- yarn.lock | 210 ++++++++++++++- 31 files changed, 1492 insertions(+), 13 deletions(-) create mode 100644 api/db/migrations/20231027135109_create_part_schema/migration.sql create mode 100644 api/db/migrations/migration_lock.toml create mode 100644 api/src/graphql/parts.sdl.ts create mode 100644 api/src/services/parts/parts.scenarios.ts create mode 100644 api/src/services/parts/parts.test.ts create mode 100644 api/src/services/parts/parts.ts create mode 100644 web/public/no_image.png create mode 100644 web/src/components/Part/EditPartCell/EditPartCell.tsx create mode 100644 web/src/components/Part/NewPart/NewPart.tsx create mode 100644 web/src/components/Part/Part/Part.tsx create mode 100644 web/src/components/Part/PartCell/PartCell.tsx create mode 100644 web/src/components/Part/PartForm/PartForm.tsx create mode 100644 web/src/components/Part/Parts/Parts.tsx create mode 100644 web/src/components/Part/PartsCell/PartsCell.tsx create mode 100644 web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx create mode 100644 web/src/lib/formatters.test.tsx create mode 100644 web/src/lib/formatters.tsx create mode 100644 web/src/pages/Part/EditPartPage/EditPartPage.tsx create mode 100644 web/src/pages/Part/NewPartPage/NewPartPage.tsx create mode 100644 web/src/pages/Part/PartPage/PartPage.tsx create mode 100644 web/src/pages/Part/PartsPage/PartsPage.tsx create mode 100644 web/src/scaffold.css diff --git a/.env.defaults b/.env.defaults index fb88fb3..358c897 100644 --- a/.env.defaults +++ b/.env.defaults @@ -17,3 +17,5 @@ PRISMA_HIDE_UPDATE_MESSAGE=true # Most applications want "debug" or "info" during dev, "trace" when you have issues and "warn" in production. # Ordered by how verbose they are: trace | debug | info | warn | error | silent # LOG_LEVEL=debug +REDWOOD_ENV_FILESTACK_API_KEY= +REDWOOD_ENV_FILESTACK_SECRET= diff --git a/.env.example b/.env.example index 2a2de6c..d93c75b 100644 --- a/.env.example +++ b/.env.example @@ -2,3 +2,5 @@ # TEST_DATABASE_URL=file:./.redwood/test.db # PRISMA_HIDE_UPDATE_MESSAGE=true # LOG_LEVEL=trace +REDWOOD_ENV_FILESTACK_API_KEY= +REDWOOD_ENV_FILESTACK_SECRET= diff --git a/api/db/migrations/20231027135109_create_part_schema/migration.sql b/api/db/migrations/20231027135109_create_part_schema/migration.sql new file mode 100644 index 0000000..d96ec31 --- /dev/null +++ b/api/db/migrations/20231027135109_create_part_schema/migration.sql @@ -0,0 +1,9 @@ +-- CreateTable +CREATE TABLE "Part" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "name" TEXT NOT NULL, + "description" TEXT DEFAULT 'No description provided', + "availableStock" INTEGER NOT NULL DEFAULT 0, + "imageUrl" TEXT NOT NULL DEFAULT '/no_image.png', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); diff --git a/api/db/migrations/migration_lock.toml b/api/db/migrations/migration_lock.toml new file mode 100644 index 0000000..e5e5c47 --- /dev/null +++ b/api/db/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "sqlite" \ No newline at end of file diff --git a/api/db/schema.prisma b/api/db/schema.prisma index 3dea71a..d249aae 100644 --- a/api/db/schema.prisma +++ b/api/db/schema.prisma @@ -8,11 +8,11 @@ generator client { binaryTargets = "native" } -// Define your own datamodels here and run `yarn redwood prisma migrate dev` -// to create migrations for them and apply to your dev DB. -// TODO: Please remove the following example: -model UserExample { - id Int @id @default(autoincrement()) - email String @unique - name String? +model Part { + id Int @id @default(autoincrement()) + name String + description String? @default("No description provided") + availableStock Int @default(0) + imageUrl String @default("/no_image.png") + createdAt DateTime @default(now()) } diff --git a/api/package.json b/api/package.json index 1ef60a7..6bc4a45 100644 --- a/api/package.json +++ b/api/package.json @@ -4,6 +4,7 @@ "private": true, "dependencies": { "@redwoodjs/api": "6.3.2", - "@redwoodjs/graphql-server": "6.3.2" + "@redwoodjs/graphql-server": "6.3.2", + "filestack-js": "^3.27.0" } } diff --git a/api/src/graphql/parts.sdl.ts b/api/src/graphql/parts.sdl.ts new file mode 100644 index 0000000..622f814 --- /dev/null +++ b/api/src/graphql/parts.sdl.ts @@ -0,0 +1,35 @@ +export const schema = gql` + type Part { + id: Int! + name: String! + description: String + availableStock: Int! + imageUrl: String! + createdAt: DateTime! + } + + type Query { + parts: [Part!]! @requireAuth + part(id: Int!): Part @requireAuth + } + + input CreatePartInput { + name: String! + description: String + availableStock: Int! + imageUrl: String! + } + + input UpdatePartInput { + name: String + description: String + availableStock: Int + imageUrl: String + } + + type Mutation { + createPart(input: CreatePartInput!): Part! @requireAuth + updatePart(id: Int!, input: UpdatePartInput!): Part! @requireAuth + deletePart(id: Int!): Part! @requireAuth + } +` diff --git a/api/src/services/parts/parts.scenarios.ts b/api/src/services/parts/parts.scenarios.ts new file mode 100644 index 0000000..81e0d58 --- /dev/null +++ b/api/src/services/parts/parts.scenarios.ts @@ -0,0 +1,11 @@ +import type { Prisma, Part } from '@prisma/client' +import type { ScenarioData } from '@redwoodjs/testing/api' + +export const standard = defineScenario({ + part: { + one: { data: { name: 'String' } }, + two: { data: { name: 'String' } }, + }, +}) + +export type StandardScenario = ScenarioData diff --git a/api/src/services/parts/parts.test.ts b/api/src/services/parts/parts.test.ts new file mode 100644 index 0000000..12ee523 --- /dev/null +++ b/api/src/services/parts/parts.test.ts @@ -0,0 +1,49 @@ +import type { Part } from '@prisma/client' + +import { parts, part, createPart, updatePart, deletePart } from './parts' +import type { StandardScenario } from './parts.scenarios' + +// Generated boilerplate tests do not account for all circumstances +// and can fail without adjustments, e.g. Float. +// Please refer to the RedwoodJS Testing Docs: +// https://redwoodjs.com/docs/testing#testing-services +// https://redwoodjs.com/docs/testing#jest-expect-type-considerations + +describe('parts', () => { + scenario('returns all parts', async (scenario: StandardScenario) => { + const result = await parts() + + expect(result.length).toEqual(Object.keys(scenario.part).length) + }) + + scenario('returns a single part', async (scenario: StandardScenario) => { + const result = await part({ id: scenario.part.one.id }) + + expect(result).toEqual(scenario.part.one) + }) + + scenario('creates a part', async () => { + const result = await createPart({ + input: { name: 'String' }, + }) + + expect(result.name).toEqual('String') + }) + + scenario('updates a part', async (scenario: StandardScenario) => { + const original = (await part({ id: scenario.part.one.id })) as Part + const result = await updatePart({ + id: original.id, + input: { name: 'String2' }, + }) + + expect(result.name).toEqual('String2') + }) + + scenario('deletes a part', async (scenario: StandardScenario) => { + const original = (await deletePart({ id: scenario.part.one.id })) as Part + const result = await part({ id: original.id }) + + expect(result).toEqual(null) + }) +}) diff --git a/api/src/services/parts/parts.ts b/api/src/services/parts/parts.ts new file mode 100644 index 0000000..7eae11a --- /dev/null +++ b/api/src/services/parts/parts.ts @@ -0,0 +1,48 @@ +import * as Filestack from 'filestack-js' +import type { QueryResolvers, MutationResolvers } from 'types/graphql' + +import { db } from 'src/lib/db' + +export const parts: QueryResolvers['parts'] = () => { + return db.part.findMany() +} + +export const part: QueryResolvers['part'] = ({ id }) => { + return db.part.findUnique({ + where: { id }, + }) +} + +export const createPart: MutationResolvers['createPart'] = ({ input }) => { + return db.part.create({ + data: input, + }) +} + +export const updatePart: MutationResolvers['updatePart'] = ({ id, input }) => { + return db.part.update({ + data: input, + where: { id }, + }) +} + +export const deletePart: MutationResolvers['deletePart'] = async ({ id }) => { + const client = Filestack.init(process.env.REDWOOD_ENV_FILESTACK_API_KEY) + const part = await db.part.findUnique({ where: { id } }) + const handle = part.imageUrl.split('/').pop() + + const security = Filestack.getSecurity( + { + expiry: new Date().getTime() + 5 * 60 * 1000, + handle, + call: ['remove'], + }, + process.env.REDWOOD_ENV_FILESTACK_SECRET + ) + + await client.remove(handle, security) + + return db.part.delete({ + where: { id }, + }) +} diff --git a/web/package.json b/web/package.json index 122c394..bfe3a5d 100644 --- a/web/package.json +++ b/web/package.json @@ -16,6 +16,8 @@ "@redwoodjs/forms": "6.3.2", "@redwoodjs/router": "6.3.2", "@redwoodjs/web": "6.3.2", + "filestack-react": "^4.0.1", + "humanize-string": "2.1.0", "prop-types": "15.8.1", "react": "18.2.0", "react-dom": "18.2.0", @@ -23,6 +25,8 @@ }, "devDependencies": { "@redwoodjs/vite": "6.3.2", + "@types/filestack-react": "^4.0.3", + "@types/node": "^20.8.9", "@types/react": "18.2.14", "@types/react-dom": "18.2.6", "autoprefixer": "^10.4.16", diff --git a/web/public/no_image.png b/web/public/no_image.png new file mode 100644 index 0000000000000000000000000000000000000000..fe7e6f3fba065063fee3885adec8a1dea8a95768 GIT binary patch literal 19816 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 literal 0 HcmV?d00001 diff --git a/web/src/App.tsx b/web/src/App.tsx index 97fb5e0..5e7beac 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -4,6 +4,7 @@ import { RedwoodApolloProvider } from '@redwoodjs/web/apollo' import FatalErrorPage from 'src/pages/FatalErrorPage' import Routes from 'src/Routes' +import './scaffold.css' import './index.css' const App = () => ( diff --git a/web/src/Routes.tsx b/web/src/Routes.tsx index e630b02..f7ce2cd 100644 --- a/web/src/Routes.tsx +++ b/web/src/Routes.tsx @@ -10,10 +10,17 @@ import { Router, Route, Set } from '@redwoodjs/router' import NavbarLayout from 'src/layouts/NavbarLayout' +import ScaffoldLayout from 'src/layouts/ScaffoldLayout' const Routes = () => { return ( + + + + + + diff --git a/web/src/components/Part/EditPartCell/EditPartCell.tsx b/web/src/components/Part/EditPartCell/EditPartCell.tsx new file mode 100644 index 0000000..ee118ef --- /dev/null +++ b/web/src/components/Part/EditPartCell/EditPartCell.tsx @@ -0,0 +1,68 @@ +import type { EditPartById, UpdatePartInput } from 'types/graphql' + +import { navigate, routes } from '@redwoodjs/router' +import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web' +import { useMutation } from '@redwoodjs/web' +import { toast } from '@redwoodjs/web/toast' + +import PartForm from 'src/components/Part/PartForm' + +export const QUERY = gql` + query EditPartById($id: Int!) { + part: part(id: $id) { + id + name + description + availableStock + imageUrl + createdAt + } + } +` +const UPDATE_PART_MUTATION = gql` + mutation UpdatePartMutation($id: Int!, $input: UpdatePartInput!) { + updatePart(id: $id, input: $input) { + id + name + description + availableStock + imageUrl + createdAt + } + } +` + +export const Loading = () =>
Loading...
+ +export const Failure = ({ error }: CellFailureProps) => ( +
{error?.message}
+) + +export const Success = ({ part }: CellSuccessProps) => { + const [updatePart, { loading, error }] = useMutation(UPDATE_PART_MUTATION, { + onCompleted: () => { + toast.success('Part updated') + navigate(routes.parts()) + }, + onError: (error) => { + toast.error(error.message) + }, + }) + + const onSave = (input: UpdatePartInput, id: EditPartById['part']['id']) => { + updatePart({ variables: { id, input } }) + } + + return ( +
+
+

+ Edit Part {part?.id} +

+
+
+ +
+
+ ) +} diff --git a/web/src/components/Part/NewPart/NewPart.tsx b/web/src/components/Part/NewPart/NewPart.tsx new file mode 100644 index 0000000..e548441 --- /dev/null +++ b/web/src/components/Part/NewPart/NewPart.tsx @@ -0,0 +1,44 @@ +import { navigate, routes } from '@redwoodjs/router' +import { useMutation } from '@redwoodjs/web' +import { toast } from '@redwoodjs/web/toast' + +import PartForm from 'src/components/Part/PartForm' + +import type { CreatePartInput } from 'types/graphql' + +const CREATE_PART_MUTATION = gql` + mutation CreatePartMutation($input: CreatePartInput!) { + createPart(input: $input) { + id + } + } +` + +const NewPart = () => { + const [createPart, { loading, error }] = useMutation(CREATE_PART_MUTATION, { + onCompleted: () => { + toast.success('Part created') + navigate(routes.parts()) + }, + onError: (error) => { + toast.error(error.message) + }, + }) + + const onSave = (input: CreatePartInput) => { + createPart({ variables: { input } }) + } + + return ( +
+
+

New Part

+
+
+ +
+
+ ) +} + +export default NewPart diff --git a/web/src/components/Part/Part/Part.tsx b/web/src/components/Part/Part/Part.tsx new file mode 100644 index 0000000..5a72a45 --- /dev/null +++ b/web/src/components/Part/Part/Part.tsx @@ -0,0 +1,94 @@ +import type { DeletePartMutationVariables, FindPartById } from 'types/graphql' + +import { Link, routes, navigate } from '@redwoodjs/router' +import { useMutation } from '@redwoodjs/web' +import { toast } from '@redwoodjs/web/toast' + +import { timeTag } from 'src/lib/formatters' + +const DELETE_PART_MUTATION = gql` + mutation DeletePartMutation($id: Int!) { + deletePart(id: $id) { + id + } + } +` + +interface Props { + part: NonNullable +} + +const Part = ({ part }: Props) => { + const [deletePart] = useMutation(DELETE_PART_MUTATION, { + onCompleted: () => { + toast.success('Part deleted') + navigate(routes.parts()) + }, + onError: (error) => { + toast.error(error.message) + }, + }) + + const onDeleteClick = (id: DeletePartMutationVariables['id']) => { + if (confirm('Are you sure you want to delete part ' + id + '?')) { + deletePart({ variables: { id } }) + } + } + + return ( + <> +
+
+

+ Part {part.id} Detail +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Id{part.id}
Name{part.name}
Description{part.description}
Available stock{part.availableStock}
Image{part.imageUrl}
Created at{timeTag(part.createdAt)}
+
+ + + ) +} + +export default Part diff --git a/web/src/components/Part/PartCell/PartCell.tsx b/web/src/components/Part/PartCell/PartCell.tsx new file mode 100644 index 0000000..2e71e35 --- /dev/null +++ b/web/src/components/Part/PartCell/PartCell.tsx @@ -0,0 +1,30 @@ +import type { FindPartById } from 'types/graphql' + +import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web' + +import Part from 'src/components/Part/Part' + +export const QUERY = gql` + query FindPartById($id: Int!) { + part: part(id: $id) { + id + name + description + availableStock + imageUrl + createdAt + } + } +` + +export const Loading = () =>
Loading...
+ +export const Empty = () =>
Part not found
+ +export const Failure = ({ error }: CellFailureProps) => ( +
{error?.message}
+) + +export const Success = ({ part }: CellSuccessProps) => { + return +} diff --git a/web/src/components/Part/PartForm/PartForm.tsx b/web/src/components/Part/PartForm/PartForm.tsx new file mode 100644 index 0000000..ad54b2b --- /dev/null +++ b/web/src/components/Part/PartForm/PartForm.tsx @@ -0,0 +1,153 @@ +import { useState } from 'react' + +import { PickerInline } from 'filestack-react' +import type { EditPartById, UpdatePartInput } from 'types/graphql' + +import { + Form, + FormError, + FieldError, + Label, + TextField, + NumberField, + Submit, +} from '@redwoodjs/forms' +import type { RWGqlError } from '@redwoodjs/forms' + +type FormPart = NonNullable + +interface PartFormProps { + part?: EditPartById['part'] + onSave: (data: UpdatePartInput, id?: FormPart['id']) => void + error: RWGqlError + loading: boolean +} + +const PartForm = (props: PartFormProps) => { + const [imageUrl, setImageUrl] = useState(props?.part?.imageUrl) + + const onSubmit = (data: FormPart) => { + const dataWithImageUrl = Object.assign(data, { imageUrl }) + props.onSave(dataWithImageUrl, props?.part?.id) + } + + const onImageUpload = (response) => { + setImageUrl(response.filesUploaded[0].url) + } + + const preview = (url: string) => { + const parts = url.split('/') + parts.splice(3, 0, 'resize=height:500') + return parts.join('/') + } + + return ( +
+ onSubmit={onSubmit} error={props.error}> + + + + + + + + + + + + + + + + + + + + + + + {!imageUrl && ( +
+ +
+ )} + + {imageUrl && ( +
+ + +
+ )} + + + +
+ + Save + +
+ +
+ ) +} + +export default PartForm diff --git a/web/src/components/Part/Parts/Parts.tsx b/web/src/components/Part/Parts/Parts.tsx new file mode 100644 index 0000000..a2d540d --- /dev/null +++ b/web/src/components/Part/Parts/Parts.tsx @@ -0,0 +1,110 @@ +import type { DeletePartMutationVariables, FindParts } from 'types/graphql' + +import { Link, routes } from '@redwoodjs/router' +import { useMutation } from '@redwoodjs/web' +import { toast } from '@redwoodjs/web/toast' + +import { QUERY } from 'src/components/Part/PartsCell' +import { timeTag, truncate } from 'src/lib/formatters' + +const DELETE_PART_MUTATION = gql` + mutation DeletePartMutation($id: Int!) { + deletePart(id: $id) { + id + } + } +` + +const PartsList = ({ parts }: FindParts) => { + const [deletePart] = useMutation(DELETE_PART_MUTATION, { + onCompleted: () => { + toast.success('Part deleted') + }, + onError: (error) => { + toast.error(error.message) + }, + // This refetches the query on the list page. Read more about other ways to + // update the cache over here: + // https://www.apollographql.com/docs/react/data/mutations/#making-all-other-cache-updates + refetchQueries: [{ query: QUERY }], + awaitRefetchQueries: true, + }) + + const onDeleteClick = (id: DeletePartMutationVariables['id']) => { + if (confirm('Are you sure you want to delete part ' + id + '?')) { + deletePart({ variables: { id } }) + } + } + + const thumbnail = (url: string) => { + const parts = url.split('/') + parts.splice(3, 0, 'resize=width:100') + return parts.join('/') + } + + return ( +
+ + + + + + + + + + + + + + {parts.map((part) => ( + + + + + + + + + + ))} + +
IdNameDescriptionAvailable stockImageCreated at 
{truncate(part.id)}{truncate(part.name)}{truncate(part.description)}{truncate(part.availableStock)} + + {`${part.name} + + {timeTag(part.createdAt)} + +
+
+ ) +} + +export default PartsList diff --git a/web/src/components/Part/PartsCell/PartsCell.tsx b/web/src/components/Part/PartsCell/PartsCell.tsx new file mode 100644 index 0000000..6ebdd28 --- /dev/null +++ b/web/src/components/Part/PartsCell/PartsCell.tsx @@ -0,0 +1,40 @@ +import type { FindParts } from 'types/graphql' + +import { Link, routes } from '@redwoodjs/router' +import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web' + +import Parts from 'src/components/Part/Parts' + +export const QUERY = gql` + query FindParts { + parts { + id + name + description + availableStock + imageUrl + createdAt + } + } +` + +export const Loading = () =>
Loading...
+ +export const Empty = () => { + return ( +
+ {'No parts yet. '} + + {'Create one?'} + +
+ ) +} + +export const Failure = ({ error }: CellFailureProps) => ( +
{error?.message}
+) + +export const Success = ({ parts }: CellSuccessProps) => { + return +} diff --git a/web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx b/web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx new file mode 100644 index 0000000..2912b56 --- /dev/null +++ b/web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx @@ -0,0 +1,37 @@ +import { Link, routes } from '@redwoodjs/router' +import { Toaster } from '@redwoodjs/web/toast' + +type LayoutProps = { + title: string + titleTo: string + buttonLabel: string + buttonTo: string + children: React.ReactNode +} + +const ScaffoldLayout = ({ + title, + titleTo, + buttonLabel, + buttonTo, + children, +}: LayoutProps) => { + return ( +
+ +
+

+ + {title} + +

+ +
+
{buttonLabel} + +
+
{children}
+
+ ) +} + +export default ScaffoldLayout diff --git a/web/src/lib/formatters.test.tsx b/web/src/lib/formatters.test.tsx new file mode 100644 index 0000000..5659338 --- /dev/null +++ b/web/src/lib/formatters.test.tsx @@ -0,0 +1,192 @@ +import { render, waitFor, screen } from '@redwoodjs/testing/web' + +import { + formatEnum, + jsonTruncate, + truncate, + timeTag, + jsonDisplay, + checkboxInputTag, +} from './formatters' + +describe('formatEnum', () => { + it('handles nullish values', () => { + expect(formatEnum(null)).toEqual('') + expect(formatEnum('')).toEqual('') + expect(formatEnum(undefined)).toEqual('') + }) + + it('formats a list of values', () => { + expect( + formatEnum(['RED', 'ORANGE', 'YELLOW', 'GREEN', 'BLUE', 'VIOLET']) + ).toEqual('Red, Orange, Yellow, Green, Blue, Violet') + }) + + it('formats a single value', () => { + expect(formatEnum('DARK_BLUE')).toEqual('Dark blue') + }) + + it('returns an empty string for values of the wrong type (for JS projects)', () => { + // @ts-expect-error - Testing JS scenario + expect(formatEnum(5)).toEqual('') + }) +}) + +describe('truncate', () => { + it('truncates really long strings', () => { + expect(truncate('na '.repeat(1000) + 'batman').length).toBeLessThan(1000) + expect(truncate('na '.repeat(1000) + 'batman')).not.toMatch(/batman/) + }) + + it('does not modify short strings', () => { + expect(truncate('Short strinG')).toEqual('Short strinG') + }) + + it('adds ... to the end of truncated strings', () => { + expect(truncate('repeat'.repeat(1000))).toMatch(/\w\.\.\.$/) + }) + + it('accepts numbers', () => { + expect(truncate(123)).toEqual('123') + expect(truncate(0)).toEqual('0') + expect(truncate(0o000)).toEqual('0') + }) + + it('handles arguments of invalid type', () => { + // @ts-expect-error - Testing JS scenario + expect(truncate(false)).toEqual('false') + + expect(truncate(undefined)).toEqual('') + expect(truncate(null)).toEqual('') + }) +}) + +describe('jsonTruncate', () => { + it('truncates large json structures', () => { + expect( + jsonTruncate({ + foo: 'foo', + bar: 'bar', + baz: 'baz', + kittens: 'kittens meow', + bazinga: 'Sheldon', + nested: { + foobar: 'I have no imagination', + two: 'Second nested item', + }, + five: 5, + bool: false, + }) + ).toMatch(/.+\n.+\w\.\.\.$/s) + }) +}) + +describe('timeTag', () => { + it('renders a date', async () => { + render(
{timeTag(new Date('1970-08-20').toUTCString())}
) + + await waitFor(() => screen.getByText(/1970.*00:00:00/)) + }) + + it('can take an empty input string', async () => { + expect(timeTag('')).toEqual('') + }) +}) + +describe('jsonDisplay', () => { + it('produces the correct output', () => { + expect( + jsonDisplay({ + title: 'TOML Example (but in JSON)', + database: { + data: [['delta', 'phi'], [3.14]], + enabled: true, + ports: [8000, 8001, 8002], + temp_targets: { + case: 72.0, + cpu: 79.5, + }, + }, + owner: { + dob: '1979-05-27T07:32:00-08:00', + name: 'Tom Preston-Werner', + }, + servers: { + alpha: { + ip: '10.0.0.1', + role: 'frontend', + }, + beta: { + ip: '10.0.0.2', + role: 'backend', + }, + }, + }) + ).toMatchInlineSnapshot(` +
+        
+          {
+        "title": "TOML Example (but in JSON)",
+        "database": {
+          "data": [
+            [
+              "delta",
+              "phi"
+            ],
+            [
+              3.14
+            ]
+          ],
+          "enabled": true,
+          "ports": [
+            8000,
+            8001,
+            8002
+          ],
+          "temp_targets": {
+            "case": 72,
+            "cpu": 79.5
+          }
+        },
+        "owner": {
+          "dob": "1979-05-27T07:32:00-08:00",
+          "name": "Tom Preston-Werner"
+        },
+        "servers": {
+          "alpha": {
+            "ip": "10.0.0.1",
+            "role": "frontend"
+          },
+          "beta": {
+            "ip": "10.0.0.2",
+            "role": "backend"
+          }
+        }
+      }
+        
+      
+ `) + }) +}) + +describe('checkboxInputTag', () => { + it('can be checked', () => { + render(checkboxInputTag(true)) + expect(screen.getByRole('checkbox')).toBeChecked() + }) + + it('can be unchecked', () => { + render(checkboxInputTag(false)) + expect(screen.getByRole('checkbox')).not.toBeChecked() + }) + + it('is disabled when checked', () => { + render(checkboxInputTag(true)) + expect(screen.getByRole('checkbox')).toBeDisabled() + }) + + it('is disabled when unchecked', () => { + render(checkboxInputTag(false)) + expect(screen.getByRole('checkbox')).toBeDisabled() + }) +}) diff --git a/web/src/lib/formatters.tsx b/web/src/lib/formatters.tsx new file mode 100644 index 0000000..8ab9e80 --- /dev/null +++ b/web/src/lib/formatters.tsx @@ -0,0 +1,58 @@ +import React from 'react' + +import humanize from 'humanize-string' + +const MAX_STRING_LENGTH = 150 + +export const formatEnum = (values: string | string[] | null | undefined) => { + let output = '' + + if (Array.isArray(values)) { + const humanizedValues = values.map((value) => humanize(value)) + output = humanizedValues.join(', ') + } else if (typeof values === 'string') { + output = humanize(values) + } + + return output +} + +export const jsonDisplay = (obj: unknown) => { + return ( +
+      {JSON.stringify(obj, null, 2)}
+    
+ ) +} + +export const truncate = (value: string | number) => { + let output = value?.toString() ?? '' + + if (output.length > MAX_STRING_LENGTH) { + output = output.substring(0, MAX_STRING_LENGTH) + '...' + } + + return output +} + +export const jsonTruncate = (obj: unknown) => { + return truncate(JSON.stringify(obj, null, 2)) +} + +export const timeTag = (dateTime?: string) => { + let output: string | JSX.Element = '' + + if (dateTime) { + output = ( + + ) + } + + return output +} + +export const checkboxInputTag = (checked: boolean) => { + return +} diff --git a/web/src/pages/Part/EditPartPage/EditPartPage.tsx b/web/src/pages/Part/EditPartPage/EditPartPage.tsx new file mode 100644 index 0000000..ab486ed --- /dev/null +++ b/web/src/pages/Part/EditPartPage/EditPartPage.tsx @@ -0,0 +1,11 @@ +import EditPartCell from 'src/components/Part/EditPartCell' + +type PartPageProps = { + id: number +} + +const EditPartPage = ({ id }: PartPageProps) => { + return +} + +export default EditPartPage diff --git a/web/src/pages/Part/NewPartPage/NewPartPage.tsx b/web/src/pages/Part/NewPartPage/NewPartPage.tsx new file mode 100644 index 0000000..dd89082 --- /dev/null +++ b/web/src/pages/Part/NewPartPage/NewPartPage.tsx @@ -0,0 +1,7 @@ +import NewPart from 'src/components/Part/NewPart' + +const NewPartPage = () => { + return +} + +export default NewPartPage diff --git a/web/src/pages/Part/PartPage/PartPage.tsx b/web/src/pages/Part/PartPage/PartPage.tsx new file mode 100644 index 0000000..2427752 --- /dev/null +++ b/web/src/pages/Part/PartPage/PartPage.tsx @@ -0,0 +1,11 @@ +import PartCell from 'src/components/Part/PartCell' + +type PartPageProps = { + id: number +} + +const PartPage = ({ id }: PartPageProps) => { + return +} + +export default PartPage diff --git a/web/src/pages/Part/PartsPage/PartsPage.tsx b/web/src/pages/Part/PartsPage/PartsPage.tsx new file mode 100644 index 0000000..bfe4eba --- /dev/null +++ b/web/src/pages/Part/PartsPage/PartsPage.tsx @@ -0,0 +1,7 @@ +import PartsCell from 'src/components/Part/PartsCell' + +const PartsPage = () => { + return +} + +export default PartsPage diff --git a/web/src/scaffold.css b/web/src/scaffold.css new file mode 100644 index 0000000..ffa9142 --- /dev/null +++ b/web/src/scaffold.css @@ -0,0 +1,243 @@ +.rw-scaffold { + @apply bg-white text-gray-600; +} +.rw-scaffold h1, +.rw-scaffold h2 { + @apply m-0; +} +.rw-scaffold a { + @apply bg-transparent; +} +.rw-scaffold ul { + @apply m-0 p-0; +} +.rw-scaffold input:-ms-input-placeholder { + @apply text-gray-500; +} +.rw-scaffold input::-ms-input-placeholder { + @apply text-gray-500; +} +.rw-scaffold input::placeholder { + @apply text-gray-500; +} +.rw-header { + @apply flex justify-between px-8 py-4; +} +.rw-main { + @apply mx-4 pb-4; +} +.rw-segment { + @apply w-full overflow-hidden rounded-lg border border-gray-200; + scrollbar-color: theme('colors.zinc.400') transparent; +} +.rw-segment::-webkit-scrollbar { + height: initial; +} +.rw-segment::-webkit-scrollbar-track { + @apply rounded-b-[10px] rounded-t-none border-0 border-t border-solid border-gray-200 bg-transparent p-[2px]; +} +.rw-segment::-webkit-scrollbar-thumb { + @apply rounded-full border-[3px] border-solid border-transparent bg-zinc-400 bg-clip-content; +} +.rw-segment-header { + @apply bg-gray-200 px-4 py-3 text-gray-700; +} +.rw-segment-main { + @apply bg-gray-100 p-4; +} +.rw-link { + @apply text-blue-400 underline; +} +.rw-link:hover { + @apply text-blue-500; +} +.rw-forgot-link { + @apply mt-1 text-right text-xs text-gray-400 underline; +} +.rw-forgot-link:hover { + @apply text-blue-500; +} +.rw-heading { + @apply font-semibold; +} +.rw-heading.rw-heading-primary { + @apply text-xl; +} +.rw-heading.rw-heading-secondary { + @apply text-sm; +} +.rw-heading .rw-link { + @apply text-gray-600 no-underline; +} +.rw-heading .rw-link:hover { + @apply text-gray-900 underline; +} +.rw-cell-error { + @apply text-sm font-semibold; +} +.rw-form-wrapper { + @apply -mt-4 text-sm; +} +.rw-cell-error, +.rw-form-error-wrapper { + @apply my-4 rounded border border-red-100 bg-red-50 p-4 text-red-600; +} +.rw-form-error-title { + @apply m-0 font-semibold; +} +.rw-form-error-list { + @apply mt-2 list-inside list-disc; +} +.rw-button { + @apply flex cursor-pointer justify-center rounded border-0 bg-gray-200 px-4 py-1 text-xs font-semibold uppercase leading-loose tracking-wide text-gray-500 no-underline transition duration-100; +} +.rw-button:hover { + @apply bg-gray-500 text-white; +} +.rw-button.rw-button-small { + @apply rounded-sm px-2 py-1 text-xs; +} +.rw-button.rw-button-green { + @apply bg-green-500 text-white; +} +.rw-button.rw-button-green:hover { + @apply bg-green-700; +} +.rw-button.rw-button-blue { + @apply bg-blue-500 text-white; +} +.rw-button.rw-button-blue:hover { + @apply bg-blue-700; +} +.rw-button.rw-button-red { + @apply bg-red-500 text-white; +} +.rw-button.rw-button-red:hover { + @apply bg-red-700 text-white; +} +.rw-button-icon { + @apply mr-1 text-xl leading-5; +} +.rw-button-group { + @apply mx-2 my-3 flex justify-center; +} +.rw-button-group .rw-button { + @apply mx-1; +} +.rw-form-wrapper .rw-button-group { + @apply mt-8; +} +.rw-label { + @apply mt-6 block text-left font-semibold text-gray-600; +} +.rw-label.rw-label-error { + @apply text-red-600; +} +.rw-input { + @apply mt-2 block w-full rounded border border-gray-200 bg-white p-2 outline-none; +} +.rw-check-radio-items { + @apply flex justify-items-center; +} +.rw-check-radio-item-none { + @apply text-gray-600; +} +.rw-input[type='checkbox'], +.rw-input[type='radio'] { + @apply ml-0 mr-1 mt-1 inline w-4; +} +.rw-input:focus { + @apply border-gray-400; +} +.rw-input-error { + @apply border-red-600 text-red-600; +} +.rw-input-error:focus { + @apply border-red-600 outline-none; + box-shadow: 0 0 5px #c53030; +} +.rw-field-error { + @apply mt-1 block text-xs font-semibold uppercase text-red-600; +} +.rw-table-wrapper-responsive { + @apply overflow-x-auto; +} +.rw-table-wrapper-responsive .rw-table { + min-width: 48rem; +} +.rw-table { + @apply w-full text-sm; +} +.rw-table th, +.rw-table td { + @apply p-3; +} +.rw-table td { + @apply bg-white text-gray-900; +} +.rw-table tr:nth-child(odd) td, +.rw-table tr:nth-child(odd) th { + @apply bg-gray-50; +} +.rw-table thead tr { + @apply bg-gray-200 text-gray-600; +} +.rw-table th { + @apply text-left font-semibold; +} +.rw-table thead th { + @apply text-left; +} +.rw-table tbody th { + @apply text-right; +} +@media (min-width: 768px) { + .rw-table tbody th { + @apply w-1/5; + } +} +.rw-table tbody tr { + @apply border-t border-gray-200; +} +.rw-table input { + @apply ml-0; +} +.rw-table-actions { + @apply flex h-4 items-center justify-end pr-1; +} +.rw-table-actions .rw-button { + @apply bg-transparent; +} +.rw-table-actions .rw-button:hover { + @apply bg-gray-500 text-white; +} +.rw-table-actions .rw-button-blue { + @apply text-blue-500; +} +.rw-table-actions .rw-button-blue:hover { + @apply bg-blue-500 text-white; +} +.rw-table-actions .rw-button-red { + @apply text-red-600; +} +.rw-table-actions .rw-button-red:hover { + @apply bg-red-600 text-white; +} +.rw-text-center { + @apply text-center; +} +.rw-login-container { + @apply mx-auto my-16 flex w-96 flex-wrap items-center justify-center; +} +.rw-login-container .rw-form-wrapper { + @apply w-full text-center; +} +.rw-login-link { + @apply mt-4 w-full text-center text-sm text-gray-600; +} +.rw-webauthn-wrapper { + @apply mx-4 mt-6 leading-6; +} +.rw-webauthn-wrapper h2 { + @apply mb-4 text-xl font-bold; +} diff --git a/web/tsconfig.json b/web/tsconfig.json index 8b5649a..ea3fad2 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -26,7 +26,7 @@ "@redwoodjs/testing": ["../node_modules/@redwoodjs/testing/web"] }, "typeRoots": ["../node_modules/@types", "./node_modules/@types"], - "types": ["jest", "@testing-library/jest-dom"], + "types": ["jest", "@testing-library/jest-dom", "node"], "jsx": "preserve" }, "include": [ diff --git a/yarn.lock b/yarn.lock index be7bfdc..863ed8c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2449,6 +2449,13 @@ __metadata: languageName: node linkType: hard +"@filestack/loader@npm:^1.0.4": + version: 1.0.9 + resolution: "@filestack/loader@npm:1.0.9" + checksum: d2ee1d83502eef02422af94a4280ff21c3e5eb64b07f37ebdb99a34bbe540a8eaec64b1199a097bebe1d10f3b6fee3a32db89fe10a5e45375d8ab327b12b62e4 + languageName: node + linkType: hard + "@graphql-codegen/add@npm:4.0.1": version: 4.0.1 resolution: "@graphql-codegen/add@npm:4.0.1" @@ -4814,6 +4821,45 @@ __metadata: languageName: node linkType: hard +"@sentry/hub@npm:6.19.7": + version: 6.19.7 + resolution: "@sentry/hub@npm:6.19.7" + dependencies: + "@sentry/types": 6.19.7 + "@sentry/utils": 6.19.7 + tslib: ^1.9.3 + checksum: 586ac17c01c4ae4d4202adc0d0cfe861ee1087b637ad8692f01c265408b5792f4c14e0dd73506aa266be310665e461d785d083285d63e0ef6c1a1ae43c3d6d50 + languageName: node + linkType: hard + +"@sentry/minimal@npm:^6.2.1": + version: 6.19.7 + resolution: "@sentry/minimal@npm:6.19.7" + dependencies: + "@sentry/hub": 6.19.7 + "@sentry/types": 6.19.7 + tslib: ^1.9.3 + checksum: 86f77d62d8ab5364cc1d14088b557045f24543f2354a959840fbc170c2fc38f9406c2d1be2ae33cad501398c0cc066a7f02b6c8f0155e844e70372c77c56f860 + languageName: node + linkType: hard + +"@sentry/types@npm:6.19.7": + version: 6.19.7 + resolution: "@sentry/types@npm:6.19.7" + checksum: b428ee58ca5f1587a5bdcf5ae19de0116f5c73eba056872b3a54ff2221d0f5166f3ef28867a8563f00d3da08e55ed3e24baad207b4d1d918596867f99c0ec705 + languageName: node + linkType: hard + +"@sentry/utils@npm:6.19.7": + version: 6.19.7 + resolution: "@sentry/utils@npm:6.19.7" + dependencies: + "@sentry/types": 6.19.7 + tslib: ^1.9.3 + checksum: 3c15e6bc75800124924da5b180137007e74d39e605c01bd28d2cfd63ee97fac1ea0c3ec8be712a1ef70802730184b71d0f3b6d50c41da9947fef348f1fd68e12 + languageName: node + linkType: hard + "@sinclair/typebox@npm:^0.27.8": version: 0.27.8 resolution: "@sinclair/typebox@npm:0.27.8" @@ -5228,6 +5274,17 @@ __metadata: languageName: node linkType: hard +"@types/filestack-react@npm:^4.0.3": + version: 4.0.3 + resolution: "@types/filestack-react@npm:4.0.3" + dependencies: + "@types/node": "*" + "@types/react": "*" + filestack-js: ^3.20.0 + checksum: c00f56f83dcc495944ef6a10ae5de6b6c0c7ff442aa4aa4adc2dc14a815635c9462d91044d30ee9daef63468772c435bed2743b966826c962a7174112a34c0d4 + languageName: node + linkType: hard + "@types/graceful-fs@npm:^4.1.3": version: 4.1.7 resolution: "@types/graceful-fs@npm:4.1.7" @@ -5411,6 +5468,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^20.8.9": + version: 20.8.9 + resolution: "@types/node@npm:20.8.9" + dependencies: + undici-types: ~5.26.4 + checksum: 6fb5604ac087c8be9aeb9ee1413fae2e691c603c9a691bd722e113597b883f21e8380a44d114ab894b435a491bfc939c8478cd57bcf890c585b961343b124964 + languageName: node + linkType: hard + "@types/normalize-package-data@npm:^2.4.0": version: 2.4.2 resolution: "@types/normalize-package-data@npm:2.4.2" @@ -6275,7 +6341,7 @@ __metadata: languageName: node linkType: hard -"abab@npm:^2.0.6": +"abab@npm:^2.0.3, abab@npm:^2.0.6": version: 2.0.6 resolution: "abab@npm:2.0.6" checksum: 0b245c3c3ea2598fe0025abf7cc7bb507b06949d51e8edae5d12c1b847a0a0c09639abcb94788332b4e2044ac4491c1e8f571b51c7826fd4b0bda1685ad4a278 @@ -6633,6 +6699,7 @@ __metadata: dependencies: "@redwoodjs/api": 6.3.2 "@redwoodjs/graphql-server": 6.3.2 + filestack-js: ^3.27.0 languageName: unknown linkType: soft @@ -10711,6 +10778,13 @@ __metadata: languageName: node linkType: hard +"eventemitter3@npm:^3.1.0": + version: 3.1.2 + resolution: "eventemitter3@npm:3.1.2" + checksum: c67262eccbf85848b7cc6d4abb6c6e34155e15686db2a01c57669fd0d44441a574a19d44d25948b442929e065774cbe5003d8e77eed47674fbf876ac77887793 + languageName: node + linkType: hard + "eventemitter3@npm:^4.0.0": version: 4.0.7 resolution: "eventemitter3@npm:4.0.7" @@ -11002,6 +11076,17 @@ __metadata: languageName: node linkType: hard +"fast-xml-parser@npm:^4.2.4": + version: 4.3.2 + resolution: "fast-xml-parser@npm:4.3.2" + dependencies: + strnum: ^1.0.5 + bin: + fxparser: src/cli/cli.js + checksum: 7c1611349384656ec4faa9802fbc8cf8c01206a1b79193d5cd54586307801562509007f6cf16e5da7d43da4fa4639770f38959a285b9466aa98dab0a9b8ca171 + languageName: node + linkType: hard + "fastest-levenshtein@npm:^1.0.12": version: 1.0.16 resolution: "fastest-levenshtein@npm:1.0.16" @@ -11132,6 +11217,13 @@ __metadata: languageName: node linkType: hard +"file-type@npm:^10.11.0": + version: 10.11.0 + resolution: "file-type@npm:10.11.0" + checksum: 2d6280d84f2499878ebdf8236a6e83b3c747f08b91d84cf99785afe3c9ac52775e52dcec15a4141cc24eb3006f274eb46dc7d13395920a1763d936c6d6e8afde + languageName: node + linkType: hard + "file-uri-to-path@npm:1.0.0": version: 1.0.0 resolution: "file-uri-to-path@npm:1.0.0" @@ -11139,6 +11231,41 @@ __metadata: languageName: node linkType: hard +"filestack-js@npm:^3.20.0, filestack-js@npm:^3.27.0": + version: 3.27.0 + resolution: "filestack-js@npm:3.27.0" + dependencies: + "@babel/runtime": ^7.8.4 + "@filestack/loader": ^1.0.4 + "@sentry/minimal": ^6.2.1 + abab: ^2.0.3 + debug: ^4.1.1 + eventemitter3: ^4.0.0 + fast-xml-parser: ^4.2.4 + file-type: ^10.11.0 + follow-redirects: ^1.10.0 + isutf8: ^2.1.0 + jsonschema: ^1.2.5 + lodash.clonedeep: ^4.5.0 + p-queue: ^4.0.0 + spark-md5: ^3.0.0 + ts-node: ^8.10.2 + checksum: a7dc07e94e00d9da13cf297cef4ae504e1a798beac42ec63662166dd9479eb2fda266c005ca18d798b44683f3cf7cfb75a49bb42a4b234fe761348f211c4f341 + languageName: node + linkType: hard + +"filestack-react@npm:^4.0.1": + version: 4.0.1 + resolution: "filestack-react@npm:4.0.1" + dependencies: + filestack-js: ^3.20.0 + peerDependencies: + react: ^16.13.1 + react-dom: ^16.13.1 + checksum: 80125b40059eb64313403c6d4bad4782b3f6c253628efeb8967972b2aca7462c834fd104e0912d8c26ca1e5807dbab65821c6156cf3fa05f2be94b2341f582f0 + languageName: node + linkType: hard + "fill-keys@npm:^1.0.2": version: 1.0.2 resolution: "fill-keys@npm:1.0.2" @@ -11315,7 +11442,7 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.0.0": +"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.10.0": version: 1.15.3 resolution: "follow-redirects@npm:1.15.3" peerDependenciesMeta: @@ -13285,6 +13412,13 @@ __metadata: languageName: node linkType: hard +"isutf8@npm:^2.1.0": + version: 2.1.0 + resolution: "isutf8@npm:2.1.0" + checksum: 613b9ad066a026001577db32df496849e45ac06c6cac3cc4ccdfbd326a3597322c421f18669bcb26440973f209363d17068971d6c880adfe9391158587dfd56a + languageName: node + linkType: hard + "jackspeak@npm:^2.3.5": version: 2.3.6 resolution: "jackspeak@npm:2.3.6" @@ -14022,6 +14156,13 @@ __metadata: languageName: node linkType: hard +"jsonschema@npm:^1.2.5": + version: 1.4.1 + resolution: "jsonschema@npm:1.4.1" + checksum: c3422d3fc7d33ff7234a806ffa909bb6fb5d1cd664bea229c64a1785dc04cbccd5fc76cf547c6ab6dd7881dbcaf3540a6a9f925a5956c61a9cd3e23a3c1796ef + languageName: node + linkType: hard + "jsonwebtoken@npm:9.0.0": version: 9.0.0 resolution: "jsonwebtoken@npm:9.0.0" @@ -14377,6 +14518,13 @@ __metadata: languageName: node linkType: hard +"lodash.clonedeep@npm:^4.5.0": + version: 4.5.0 + resolution: "lodash.clonedeep@npm:4.5.0" + checksum: 2caf0e4808f319d761d2939ee0642fa6867a4bbf2cfce43276698828380756b99d4c4fa226d881655e6ac298dd453fe12a5ec8ba49861777759494c534936985 + languageName: node + linkType: hard + "lodash.debounce@npm:^4.0.8": version: 4.0.8 resolution: "lodash.debounce@npm:4.0.8" @@ -15935,6 +16083,15 @@ __metadata: languageName: node linkType: hard +"p-queue@npm:^4.0.0": + version: 4.0.0 + resolution: "p-queue@npm:4.0.0" + dependencies: + eventemitter3: ^3.1.0 + checksum: 9ae4e2fd428447d4ae27028e8bd8bc3384bee93a6a115b96463de14b8580b326fc4c546e003052bd1c5bb079991b888f898757e8c4c0a65fcb36de0f110c00af + languageName: node + linkType: hard + "p-retry@npm:4.6.2, p-retry@npm:^4.5.0": version: 4.6.2 resolution: "p-retry@npm:4.6.2" @@ -18714,7 +18871,7 @@ __metadata: languageName: node linkType: hard -"source-map-support@npm:^0.5.16, source-map-support@npm:~0.5.12, source-map-support@npm:~0.5.20": +"source-map-support@npm:^0.5.16, source-map-support@npm:^0.5.17, source-map-support@npm:~0.5.12, source-map-support@npm:~0.5.20": version: 0.5.21 resolution: "source-map-support@npm:0.5.21" dependencies: @@ -18752,6 +18909,13 @@ __metadata: languageName: node linkType: hard +"spark-md5@npm:^3.0.0": + version: 3.0.2 + resolution: "spark-md5@npm:3.0.2" + checksum: 3fd11735eac5e7d60d6006d99ac0a055f148a89e9baf5f0b51ac103022dec30556b44190b37f6737ca50f81e8e50dc13e724f9edf6290c412ff5ab2101ce7780 + languageName: node + linkType: hard + "spawn-command@npm:0.0.2": version: 0.0.2 resolution: "spawn-command@npm:0.0.2" @@ -19171,6 +19335,13 @@ __metadata: languageName: node linkType: hard +"strnum@npm:^1.0.5": + version: 1.0.5 + resolution: "strnum@npm:1.0.5" + checksum: 64fb8cc2effbd585a6821faa73ad97d4b553c8927e49086a162ffd2cc818787643390b89d567460a8e74300148d11ac052e21c921ef2049f2987f4b1b89a7ff1 + languageName: node + linkType: hard + "style-loader@npm:3.3.3": version: 3.3.3 resolution: "style-loader@npm:3.3.3" @@ -19812,6 +19983,26 @@ __metadata: languageName: node linkType: hard +"ts-node@npm:^8.10.2": + version: 8.10.2 + resolution: "ts-node@npm:8.10.2" + dependencies: + arg: ^4.1.0 + diff: ^4.0.1 + make-error: ^1.1.1 + source-map-support: ^0.5.17 + yn: 3.1.1 + peerDependencies: + typescript: ">=2.7" + bin: + ts-node: dist/bin.js + ts-node-script: dist/bin-script.js + ts-node-transpile-only: dist/bin-transpile.js + ts-script: dist/bin-script-deprecated.js + checksum: 628343f62fff2543b4559a93eb27005084aea7609945e77f311031c5e96c4099736646856e1792605b90e8007d2c060fe80783be21c94788d91d6f259aab92e2 + languageName: node + linkType: hard + "ts-pattern@npm:4.3.0": version: 4.3.0 resolution: "ts-pattern@npm:4.3.0" @@ -19838,7 +20029,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^1.8.1, tslib@npm:^1.9.2": +"tslib@npm:^1.8.1, tslib@npm:^1.9.2, tslib@npm:^1.9.3": version: 1.14.1 resolution: "tslib@npm:1.14.1" checksum: 69ae09c49eea644bc5ebe1bca4fa4cc2c82b7b3e02f43b84bd891504edf66dbc6b2ec0eef31a957042de2269139e4acff911e6d186a258fb14069cd7f6febce2 @@ -20080,6 +20271,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~5.26.4": + version: 5.26.5 + resolution: "undici-types@npm:5.26.5" + checksum: bb673d7876c2d411b6eb6c560e0c571eef4a01c1c19925175d16e3a30c4c428181fb8d7ae802a261f283e4166a0ac435e2f505743aa9e45d893f9a3df017b501 + languageName: node + linkType: hard + "undici@npm:^5.19.1": version: 5.25.4 resolution: "undici@npm:5.25.4" @@ -20658,10 +20856,14 @@ __metadata: "@redwoodjs/router": 6.3.2 "@redwoodjs/vite": 6.3.2 "@redwoodjs/web": 6.3.2 + "@types/filestack-react": ^4.0.3 + "@types/node": ^20.8.9 "@types/react": 18.2.14 "@types/react-dom": 18.2.6 autoprefixer: ^10.4.16 daisyui: ^3.9.3 + filestack-react: ^4.0.1 + humanize-string: 2.1.0 postcss: ^8.4.31 postcss-loader: ^7.3.3 prop-types: 15.8.1