Basic transaction system
This commit is contained in:
@ -1,25 +0,0 @@
|
||||
-- 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
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"firstName" TEXT NOT NULL,
|
||||
"lastName" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"hashedPassword" TEXT NOT NULL,
|
||||
"salt" TEXT NOT NULL,
|
||||
"resetToken" TEXT,
|
||||
"resetTokenExpiresAt" DATETIME,
|
||||
"roles" TEXT NOT NULL DEFAULT 'user'
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
@ -0,0 +1,41 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Part" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"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" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Part_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"firstName" TEXT NOT NULL,
|
||||
"lastName" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"hashedPassword" TEXT NOT NULL,
|
||||
"salt" TEXT NOT NULL,
|
||||
"resetToken" TEXT,
|
||||
"resetTokenExpiresAt" TIMESTAMP(3),
|
||||
"roles" TEXT NOT NULL DEFAULT 'user',
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Transaction" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"userId" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "Transaction_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Transaction" ADD CONSTRAINT "Transaction_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
18
api/db/migrations/20231107152332_transaction/migration.sql
Normal file
18
api/db/migrations/20231107152332_transaction/migration.sql
Normal file
@ -0,0 +1,18 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `type` to the `Transaction` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TransactionType" AS ENUM ('in', 'out');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Part" ADD COLUMN "transactionId" INTEGER;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Transaction" ADD COLUMN "quantities" INTEGER[],
|
||||
ADD COLUMN "type" "TransactionType" NOT NULL;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Part" ADD CONSTRAINT "Part_transactionId_fkey" FOREIGN KEY ("transactionId") REFERENCES "Transaction"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
27
api/db/migrations/20231107163803_transaction/migration.sql
Normal file
27
api/db/migrations/20231107163803_transaction/migration.sql
Normal file
@ -0,0 +1,27 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `quantities` on the `Transaction` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Part" DROP CONSTRAINT "Part_transactionId_fkey";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Transaction" DROP COLUMN "quantities";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PartTransaction" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"quantity" INTEGER NOT NULL,
|
||||
"partId" INTEGER NOT NULL,
|
||||
"transactionId" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "PartTransaction_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PartTransaction" ADD CONSTRAINT "PartTransaction_partId_fkey" FOREIGN KEY ("partId") REFERENCES "Part"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PartTransaction" ADD CONSTRAINT "PartTransaction_transactionId_fkey" FOREIGN KEY ("transactionId") REFERENCES "Transaction"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
24
api/db/migrations/20231107165858_del/migration.sql
Normal file
24
api/db/migrations/20231107165858_del/migration.sql
Normal file
@ -0,0 +1,24 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `PartTransaction` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `Transaction` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "PartTransaction" DROP CONSTRAINT "PartTransaction_partId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "PartTransaction" DROP CONSTRAINT "PartTransaction_transactionId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Transaction" DROP CONSTRAINT "Transaction_userId_fkey";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "PartTransaction";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "Transaction";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "TransactionType";
|
16
api/db/migrations/20231107224945_transaction/migration.sql
Normal file
16
api/db/migrations/20231107224945_transaction/migration.sql
Normal file
@ -0,0 +1,16 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TransactionType" AS ENUM ('in', 'out');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Transaction" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"userId" INTEGER NOT NULL,
|
||||
"type" "TransactionType" NOT NULL,
|
||||
"parts" JSONB[],
|
||||
|
||||
CONSTRAINT "Transaction_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Transaction" ADD CONSTRAINT "Transaction_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
@ -1,3 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "sqlite"
|
||||
provider = "postgresql"
|
@ -1,5 +1,5 @@
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
@ -15,16 +15,32 @@ model Part {
|
||||
availableStock Int @default(0)
|
||||
imageUrl String @default("/no_image.png")
|
||||
createdAt DateTime @default(now())
|
||||
transactionId Int?
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
id Int @id @default(autoincrement())
|
||||
firstName String
|
||||
lastName String
|
||||
email String @unique
|
||||
email String @unique
|
||||
hashedPassword String
|
||||
salt String
|
||||
resetToken String?
|
||||
resetTokenExpiresAt DateTime?
|
||||
roles String @default("user")
|
||||
roles String @default("user")
|
||||
transactions Transaction[]
|
||||
}
|
||||
|
||||
enum TransactionType {
|
||||
in
|
||||
out
|
||||
}
|
||||
|
||||
model Transaction {
|
||||
id Int @id @default(autoincrement())
|
||||
date DateTime @default(now())
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId Int
|
||||
type TransactionType
|
||||
parts Json[] // { part: Part, quantity: Int }[]
|
||||
}
|
||||
|
@ -3,9 +3,9 @@
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@redwoodjs/api": "6.3.2",
|
||||
"@redwoodjs/auth-dbauth-api": "6.3.2",
|
||||
"@redwoodjs/graphql-server": "6.3.2",
|
||||
"@redwoodjs/api": "6.3.3",
|
||||
"@redwoodjs/auth-dbauth-api": "6.3.3",
|
||||
"@redwoodjs/graphql-server": "6.3.3",
|
||||
"filestack-js": "^3.27.0"
|
||||
}
|
||||
}
|
||||
|
58
api/src/graphql/transactions.sdl.ts
Normal file
58
api/src/graphql/transactions.sdl.ts
Normal file
@ -0,0 +1,58 @@
|
||||
export const schema = gql`
|
||||
type Transaction {
|
||||
id: Int!
|
||||
date: DateTime!
|
||||
user: User!
|
||||
userId: Int!
|
||||
type: TransactionType!
|
||||
parts: [JSON]!
|
||||
}
|
||||
|
||||
enum TransactionType {
|
||||
in
|
||||
out
|
||||
}
|
||||
|
||||
enum FilterTransactionsByType {
|
||||
in
|
||||
out
|
||||
both
|
||||
}
|
||||
|
||||
type UserTransactions {
|
||||
transactions: [Transaction!]!
|
||||
filter: FilterTransactionsByType!
|
||||
}
|
||||
|
||||
type Query {
|
||||
transactions(filter: FilterTransactionsByType!): UserTransactions!
|
||||
@requireAuth(roles: "admin")
|
||||
transaction(id: Int!): Transaction @requireAuth(roles: "admin")
|
||||
userTransactions(
|
||||
userId: Int!
|
||||
filter: FilterTransactionsByType!
|
||||
): UserTransactions! @requireAuth
|
||||
}
|
||||
|
||||
input CreateTransactionInput {
|
||||
date: DateTime!
|
||||
userId: Int!
|
||||
type: TransactionType!
|
||||
parts: [JSON]!
|
||||
}
|
||||
|
||||
input UpdateTransactionInput {
|
||||
date: DateTime
|
||||
userId: Int
|
||||
type: TransactionType
|
||||
parts: [JSON]!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createTransaction(input: CreateTransactionInput!): Transaction! @requireAuth
|
||||
returnTransaction(id: Int!, userId: Int!): Transaction! @requireAuth
|
||||
updateTransaction(id: Int!, input: UpdateTransactionInput!): Transaction!
|
||||
@requireAuth(roles: "admin")
|
||||
deleteTransaction(id: Int!): Transaction! @requireAuth(roles: "admin")
|
||||
}
|
||||
`
|
48
api/src/graphql/users.sdl.ts
Normal file
48
api/src/graphql/users.sdl.ts
Normal file
@ -0,0 +1,48 @@
|
||||
export const schema = gql`
|
||||
type User {
|
||||
id: Int!
|
||||
firstName: String!
|
||||
lastName: String!
|
||||
email: String!
|
||||
hashedPassword: String!
|
||||
salt: String!
|
||||
resetToken: String
|
||||
resetTokenExpiresAt: DateTime
|
||||
roles: String!
|
||||
transactions: [Transaction]!
|
||||
}
|
||||
|
||||
type Query {
|
||||
users: [User!]! @requireAuth(roles: "admin")
|
||||
user(id: Int!): User @requireAuth(roles: "admin")
|
||||
}
|
||||
|
||||
input CreateUserInput {
|
||||
firstName: String!
|
||||
lastName: String!
|
||||
email: String!
|
||||
hashedPassword: String!
|
||||
salt: String!
|
||||
resetToken: String
|
||||
resetTokenExpiresAt: DateTime
|
||||
roles: String!
|
||||
}
|
||||
|
||||
input UpdateUserInput {
|
||||
firstName: String
|
||||
lastName: String
|
||||
email: String
|
||||
hashedPassword: String
|
||||
salt: String
|
||||
resetToken: String
|
||||
resetTokenExpiresAt: DateTime
|
||||
roles: String
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createUser(input: CreateUserInput!): User! @requireAuth(roles: "admin")
|
||||
updateUser(id: Int!, input: UpdateUserInput!): User!
|
||||
@requireAuth(roles: "admin")
|
||||
deleteUser(id: Int!): User! @requireAuth(roles: "admin")
|
||||
}
|
||||
`
|
@ -27,7 +27,7 @@ export const getCurrentUser = async (session: Decoded) => {
|
||||
|
||||
return await db.user.findUnique({
|
||||
where: { id: session.id },
|
||||
select: { id: true, firstName: true, roles: true },
|
||||
select: { id: true, firstName: true, roles: true, transactions: true },
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -77,12 +77,22 @@ export const partPage = ({
|
||||
}
|
||||
|
||||
export const createPart: MutationResolvers['createPart'] = ({ input }) => {
|
||||
input.description =
|
||||
input.description.length == 0
|
||||
? 'No description provided'
|
||||
: input.description
|
||||
|
||||
return db.part.create({
|
||||
data: input,
|
||||
})
|
||||
}
|
||||
|
||||
export const updatePart: MutationResolvers['updatePart'] = ({ id, input }) => {
|
||||
input.description =
|
||||
input.description.length == 0
|
||||
? 'No description provided'
|
||||
: input.description
|
||||
|
||||
return db.part.update({
|
||||
data: input,
|
||||
where: { id },
|
||||
|
39
api/src/services/transactions/transactions.scenarios.ts
Normal file
39
api/src/services/transactions/transactions.scenarios.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import type { Prisma, Transaction } from '@prisma/client'
|
||||
import type { ScenarioData } from '@redwoodjs/testing/api'
|
||||
|
||||
export const standard = defineScenario<Prisma.TransactionCreateArgs>({
|
||||
transaction: {
|
||||
one: {
|
||||
data: {
|
||||
type: 'in',
|
||||
parts: { foo: 'bar' },
|
||||
user: {
|
||||
create: {
|
||||
firstName: 'String',
|
||||
lastName: 'String',
|
||||
email: 'String2420106',
|
||||
hashedPassword: 'String',
|
||||
salt: 'String',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
two: {
|
||||
data: {
|
||||
type: 'in',
|
||||
parts: { foo: 'bar' },
|
||||
user: {
|
||||
create: {
|
||||
firstName: 'String',
|
||||
lastName: 'String',
|
||||
email: 'String1596322',
|
||||
hashedPassword: 'String',
|
||||
salt: 'String',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export type StandardScenario = ScenarioData<Transaction, 'transaction'>
|
68
api/src/services/transactions/transactions.test.ts
Normal file
68
api/src/services/transactions/transactions.test.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import type { Transaction } from '@prisma/client'
|
||||
|
||||
import {
|
||||
transactions,
|
||||
transaction,
|
||||
createTransaction,
|
||||
updateTransaction,
|
||||
deleteTransaction,
|
||||
} from './transactions'
|
||||
import type { StandardScenario } from './transactions.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('transactions', () => {
|
||||
scenario('returns all transactions', async (scenario: StandardScenario) => {
|
||||
const result = await transactions()
|
||||
|
||||
expect(result.length).toEqual(Object.keys(scenario.transaction).length)
|
||||
})
|
||||
|
||||
scenario(
|
||||
'returns a single transaction',
|
||||
async (scenario: StandardScenario) => {
|
||||
const result = await transaction({ id: scenario.transaction.one.id })
|
||||
|
||||
expect(result).toEqual(scenario.transaction.one)
|
||||
}
|
||||
)
|
||||
|
||||
scenario('creates a transaction', async (scenario: StandardScenario) => {
|
||||
const result = await createTransaction({
|
||||
input: {
|
||||
userId: scenario.transaction.two.userId,
|
||||
type: 'in',
|
||||
parts: { foo: 'bar' },
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.userId).toEqual(scenario.transaction.two.userId)
|
||||
expect(result.type).toEqual('in')
|
||||
expect(result.parts).toEqual({ foo: 'bar' })
|
||||
})
|
||||
|
||||
scenario('updates a transaction', async (scenario: StandardScenario) => {
|
||||
const original = (await transaction({
|
||||
id: scenario.transaction.one.id,
|
||||
})) as Transaction
|
||||
const result = await updateTransaction({
|
||||
id: original.id,
|
||||
input: { type: 'out' },
|
||||
})
|
||||
|
||||
expect(result.type).toEqual('out')
|
||||
})
|
||||
|
||||
scenario('deletes a transaction', async (scenario: StandardScenario) => {
|
||||
const original = (await deleteTransaction({
|
||||
id: scenario.transaction.one.id,
|
||||
})) as Transaction
|
||||
const result = await transaction({ id: original.id })
|
||||
|
||||
expect(result).toEqual(null)
|
||||
})
|
||||
})
|
158
api/src/services/transactions/transactions.ts
Normal file
158
api/src/services/transactions/transactions.ts
Normal file
@ -0,0 +1,158 @@
|
||||
import type {
|
||||
QueryResolvers,
|
||||
MutationResolvers,
|
||||
TransactionRelationResolvers,
|
||||
Part,
|
||||
} from 'types/graphql'
|
||||
|
||||
import { UserInputError } from '@redwoodjs/graphql-server'
|
||||
|
||||
import { db } from 'src/lib/db'
|
||||
|
||||
export const transactions: QueryResolvers['transactions'] = async ({
|
||||
filter,
|
||||
}) => {
|
||||
const transactions =
|
||||
filter == 'both'
|
||||
? await db.transaction.findMany({
|
||||
orderBy: {
|
||||
date: 'desc',
|
||||
},
|
||||
})
|
||||
: await db.transaction.findMany({
|
||||
where: {
|
||||
type: filter,
|
||||
},
|
||||
orderBy: {
|
||||
date: 'desc',
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
transactions,
|
||||
filter,
|
||||
}
|
||||
}
|
||||
|
||||
export const userTransactions: QueryResolvers['userTransactions'] = async ({
|
||||
userId,
|
||||
filter,
|
||||
}) => {
|
||||
return {
|
||||
transactions:
|
||||
filter == 'both'
|
||||
? await db.transaction.findMany({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
orderBy: {
|
||||
date: 'desc',
|
||||
},
|
||||
})
|
||||
: await db.transaction.findMany({
|
||||
where: {
|
||||
AND: {
|
||||
userId,
|
||||
type: filter,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
date: 'desc',
|
||||
},
|
||||
}),
|
||||
filter,
|
||||
}
|
||||
}
|
||||
|
||||
export const transaction: QueryResolvers['transaction'] = ({ id }) => {
|
||||
return db.transaction.findUnique({
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const returnTransaction: MutationResolvers['returnTransaction'] =
|
||||
async ({ id, userId }) => {
|
||||
const transaction = await db.transaction.findUnique({ where: { id } })
|
||||
|
||||
if (transaction.type == 'out' && userId == transaction.userId) {
|
||||
for (const partRaw of transaction.parts) {
|
||||
const transactionPart = JSON.parse(partRaw['part']) as Part
|
||||
const part = await db.part.findUnique({
|
||||
where: { id: transactionPart.id },
|
||||
})
|
||||
|
||||
await db.part.update({
|
||||
where: { id: part.id },
|
||||
data: {
|
||||
availableStock: {
|
||||
increment: partRaw['quantity'],
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return await db.transaction.update({
|
||||
where: { id: transaction.id },
|
||||
data: { type: 'in' },
|
||||
})
|
||||
} else return transaction
|
||||
}
|
||||
|
||||
export const createTransaction: MutationResolvers['createTransaction'] =
|
||||
async ({ input }) => {
|
||||
const basket = input.parts.map((item) => {
|
||||
const part: Part = JSON.parse(item['part']) as Part
|
||||
return { part: part.id, quantity: item['quantity'] }
|
||||
})
|
||||
|
||||
for (const item of basket) {
|
||||
const part = await db.part.findUnique({ where: { id: item.part } })
|
||||
|
||||
if (!part) throw new UserInputError(`Part ${item.part} does not exist`)
|
||||
|
||||
if (input.type == 'out') {
|
||||
if (part.availableStock < item.quantity)
|
||||
throw new UserInputError(
|
||||
`Cannot take out more than available stock (${part.availableStock} available, ${item.quantity} requested)`
|
||||
)
|
||||
|
||||
await db.part.update({
|
||||
where: { id: item.part },
|
||||
data: { availableStock: { decrement: item.quantity } },
|
||||
})
|
||||
} else if (input.type == 'in') {
|
||||
await db.part.update({
|
||||
where: { id: item.part },
|
||||
data: { availableStock: { increment: item.quantity } },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return db.transaction.create({
|
||||
data: input,
|
||||
})
|
||||
}
|
||||
|
||||
export const updateTransaction: MutationResolvers['updateTransaction'] = ({
|
||||
id,
|
||||
input,
|
||||
}) => {
|
||||
return db.transaction.update({
|
||||
data: input,
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteTransaction: MutationResolvers['deleteTransaction'] = ({
|
||||
id,
|
||||
}) => {
|
||||
return db.transaction.delete({
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const Transaction: TransactionRelationResolvers = {
|
||||
user: (_obj, { root }) => {
|
||||
return db.transaction.findUnique({ where: { id: root?.id } }).user()
|
||||
},
|
||||
}
|
27
api/src/services/users/users.scenarios.ts
Normal file
27
api/src/services/users/users.scenarios.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import type { Prisma, User } from '@prisma/client'
|
||||
import type { ScenarioData } from '@redwoodjs/testing/api'
|
||||
|
||||
export const standard = defineScenario<Prisma.UserCreateArgs>({
|
||||
user: {
|
||||
one: {
|
||||
data: {
|
||||
firstName: 'String',
|
||||
lastName: 'String',
|
||||
email: 'String6743396',
|
||||
hashedPassword: 'String',
|
||||
salt: 'String',
|
||||
},
|
||||
},
|
||||
two: {
|
||||
data: {
|
||||
firstName: 'String',
|
||||
lastName: 'String',
|
||||
email: 'String6958264',
|
||||
hashedPassword: 'String',
|
||||
salt: 'String',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export type StandardScenario = ScenarioData<User, 'user'>
|
59
api/src/services/users/users.test.ts
Normal file
59
api/src/services/users/users.test.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import type { User } from '@prisma/client'
|
||||
|
||||
import { users, user, createUser, updateUser, deleteUser } from './users'
|
||||
import type { StandardScenario } from './users.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('users', () => {
|
||||
scenario('returns all users', async (scenario: StandardScenario) => {
|
||||
const result = await users()
|
||||
|
||||
expect(result.length).toEqual(Object.keys(scenario.user).length)
|
||||
})
|
||||
|
||||
scenario('returns a single user', async (scenario: StandardScenario) => {
|
||||
const result = await user({ id: scenario.user.one.id })
|
||||
|
||||
expect(result).toEqual(scenario.user.one)
|
||||
})
|
||||
|
||||
scenario('creates a user', async () => {
|
||||
const result = await createUser({
|
||||
input: {
|
||||
firstName: 'String',
|
||||
lastName: 'String',
|
||||
email: 'String3921297',
|
||||
hashedPassword: 'String',
|
||||
salt: 'String',
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.firstName).toEqual('String')
|
||||
expect(result.lastName).toEqual('String')
|
||||
expect(result.email).toEqual('String3921297')
|
||||
expect(result.hashedPassword).toEqual('String')
|
||||
expect(result.salt).toEqual('String')
|
||||
})
|
||||
|
||||
scenario('updates a user', async (scenario: StandardScenario) => {
|
||||
const original = (await user({ id: scenario.user.one.id })) as User
|
||||
const result = await updateUser({
|
||||
id: original.id,
|
||||
input: { firstName: 'String2' },
|
||||
})
|
||||
|
||||
expect(result.firstName).toEqual('String2')
|
||||
})
|
||||
|
||||
scenario('deletes a user', async (scenario: StandardScenario) => {
|
||||
const original = (await deleteUser({ id: scenario.user.one.id })) as User
|
||||
const result = await user({ id: original.id })
|
||||
|
||||
expect(result).toEqual(null)
|
||||
})
|
||||
})
|
42
api/src/services/users/users.ts
Normal file
42
api/src/services/users/users.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import type {
|
||||
QueryResolvers,
|
||||
MutationResolvers,
|
||||
UserRelationResolvers,
|
||||
} from 'types/graphql'
|
||||
|
||||
import { db } from 'src/lib/db'
|
||||
|
||||
export const users: QueryResolvers['users'] = () => {
|
||||
return db.user.findMany()
|
||||
}
|
||||
|
||||
export const user: QueryResolvers['user'] = ({ id }) => {
|
||||
return db.user.findUnique({
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const createUser: MutationResolvers['createUser'] = ({ input }) => {
|
||||
return db.user.create({
|
||||
data: input,
|
||||
})
|
||||
}
|
||||
|
||||
export const updateUser: MutationResolvers['updateUser'] = ({ id, input }) => {
|
||||
return db.user.update({
|
||||
data: input,
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteUser: MutationResolvers['deleteUser'] = ({ id }) => {
|
||||
return db.user.delete({
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const User: UserRelationResolvers = {
|
||||
transactions: (_obj, { root }) => {
|
||||
return db.user.findUnique({ where: { id: root?.id } }).transactions()
|
||||
},
|
||||
}
|
@ -7,7 +7,7 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@redwoodjs/core": "6.3.2",
|
||||
"@redwoodjs/core": "6.3.3",
|
||||
"prettier-plugin-tailwindcss": "0.4.1"
|
||||
},
|
||||
"eslintConfig": {
|
||||
|
@ -6,7 +6,7 @@
|
||||
# https://redwoodjs.com/docs/app-configuration-redwood-toml
|
||||
|
||||
[web]
|
||||
title = "Arduino Parts Inventory"
|
||||
title = "Parts Inventory"
|
||||
port = 8910
|
||||
apiUrl = "/.redwood/functions" # You can customize graphql and dbauth urls individually too: see https://redwoodjs.com/docs/app-configuration-redwood-toml#api-paths
|
||||
includeEnvironmentVariables = [
|
||||
|
@ -13,10 +13,11 @@
|
||||
"dependencies": {
|
||||
"@mdi/js": "^7.3.67",
|
||||
"@mdi/react": "^1.6.1",
|
||||
"@redwoodjs/auth-dbauth-web": "6.3.2",
|
||||
"@redwoodjs/forms": "6.3.2",
|
||||
"@redwoodjs/router": "6.3.2",
|
||||
"@redwoodjs/web": "6.3.2",
|
||||
"@redwoodjs/auth-dbauth-web": "6.3.3",
|
||||
"@redwoodjs/forms": "6.3.3",
|
||||
"@redwoodjs/router": "6.3.3",
|
||||
"@redwoodjs/web": "6.3.3",
|
||||
"dayjs": "^1.11.10",
|
||||
"filestack-react": "^4.0.1",
|
||||
"humanize-string": "2.1.0",
|
||||
"prop-types": "15.8.1",
|
||||
@ -25,7 +26,7 @@
|
||||
"theme-change": "^2.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@redwoodjs/vite": "6.3.2",
|
||||
"@redwoodjs/vite": "6.3.3",
|
||||
"@types/filestack-react": "^4.0.3",
|
||||
"@types/node": "^20.8.9",
|
||||
"@types/react": "18.2.14",
|
||||
|
@ -20,12 +20,18 @@ const Routes = () => {
|
||||
<Route path="/admin/parts/{id:Int}" page={PartPartPage} name="part" />
|
||||
<Route path="/admin/parts" page={PartPartsPage} name="parts" />
|
||||
</Set>
|
||||
<Set wrap={ScaffoldLayout} title="Transactions" titleTo="adminTransactions">
|
||||
<Route path="/admin/transactions" page={AdminTransactionsPage} name="adminTransactions" />
|
||||
</Set>
|
||||
</Private>
|
||||
|
||||
<Set wrap={NavbarLayout}>
|
||||
<Route path="/" page={HomePage} name="home" />
|
||||
<Route path="/part/{id:Int}" page={PartPage} name="partDetails" />
|
||||
<Route path="/basket" page={BasketPage} name="basket" />
|
||||
<Private unauthenticated="login">
|
||||
<Route path="/transactions" page={TransactionsPage} name="userTransactions" />
|
||||
</Private>
|
||||
</Set>
|
||||
|
||||
<Route notfound page={NotFoundPage} />
|
||||
|
25
web/src/components/AdminMenu/AdminMenu.stories.tsx
Normal file
25
web/src/components/AdminMenu/AdminMenu.stories.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
// Pass props to your component by passing an `args` object to your story
|
||||
//
|
||||
// ```tsx
|
||||
// export const Primary: Story = {
|
||||
// args: {
|
||||
// propName: propValue
|
||||
// }
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// See https://storybook.js.org/docs/react/writing-stories/args.
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import AdminMenu from './AdminMenu'
|
||||
|
||||
const meta: Meta<typeof AdminMenu> = {
|
||||
component: AdminMenu,
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof AdminMenu>
|
||||
|
||||
export const Primary: Story = {}
|
14
web/src/components/AdminMenu/AdminMenu.test.tsx
Normal file
14
web/src/components/AdminMenu/AdminMenu.test.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { render } from '@redwoodjs/testing/web'
|
||||
|
||||
import AdminMenu from './AdminMenu'
|
||||
|
||||
// Improve this test with help from the Redwood Testing Doc:
|
||||
// https://redwoodjs.com/docs/testing#testing-components
|
||||
|
||||
describe('AdminMenu', () => {
|
||||
it('renders successfully', () => {
|
||||
expect(() => {
|
||||
render(<AdminMenu />)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
53
web/src/components/AdminMenu/AdminMenu.tsx
Normal file
53
web/src/components/AdminMenu/AdminMenu.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { mdiWrench } from '@mdi/js'
|
||||
import Icon from '@mdi/react'
|
||||
|
||||
import { Link, routes } from '@redwoodjs/router'
|
||||
|
||||
import { useAuth } from 'src/auth'
|
||||
|
||||
interface Props {
|
||||
mobile: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
const AdminMenu = ({ mobile, className }: Props) => {
|
||||
const { isAuthenticated, hasRole } = useAuth()
|
||||
|
||||
return isAuthenticated && hasRole('admin') ? (
|
||||
<div className={className}>
|
||||
<details
|
||||
className={`dropdown ${
|
||||
mobile ? 'dropdown-start space-y-2' : 'dropdown-end space-y-4'
|
||||
}`}
|
||||
>
|
||||
<summary className="btn btn-ghost swap swap-rotate w-12 hover:shadow-lg">
|
||||
<Icon path={mdiWrench} className="h-8 w-8 text-base-content" />
|
||||
</summary>
|
||||
<div className="dropdown-content flex flex-col items-center space-y-3 rounded-xl bg-base-100 p-3 shadow-lg">
|
||||
<ul className="space-y-3 bg-base-100 text-base-content">
|
||||
<li>
|
||||
<Link
|
||||
to={routes.parts()}
|
||||
className="btn btn-ghost w-full font-inter hover:shadow-lg"
|
||||
>
|
||||
Parts
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
to={routes.adminTransactions()}
|
||||
className="btn btn-ghost w-full hover:shadow-lg"
|
||||
>
|
||||
<p className="font-inter">Transactions</p>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
|
||||
export default AdminMenu
|
@ -0,0 +1,4 @@
|
||||
// Define your own mock data here:
|
||||
export const standard = (/* vars, { ctx, req } */) => ({
|
||||
userTransactions: [{ id: 42 }, { id: 43 }, { id: 44 }],
|
||||
})
|
@ -0,0 +1,34 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { Loading, Empty, Failure, Success } from './UserTransactionsCell'
|
||||
import { standard } from './UserTransactionsCell.mock'
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'Cells/UserTransactionsCell',
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
export const loading: StoryObj<typeof Loading> = {
|
||||
render: () => {
|
||||
return Loading ? <Loading /> : <></>
|
||||
},
|
||||
}
|
||||
|
||||
export const empty: StoryObj<typeof Empty> = {
|
||||
render: () => {
|
||||
return Empty ? <Empty /> : <></>
|
||||
},
|
||||
}
|
||||
|
||||
export const failure: StoryObj<typeof Failure> = {
|
||||
render: (args) => {
|
||||
return Failure ? <Failure error={new Error('Oh no')} {...args} /> : <></>
|
||||
},
|
||||
}
|
||||
|
||||
export const success: StoryObj<typeof Success> = {
|
||||
render: (args) => {
|
||||
return Success ? <Success {...standard()} {...args} /> : <></>
|
||||
},
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
import { render } from '@redwoodjs/testing/web'
|
||||
|
||||
import { Loading, Empty, Failure, Success } from './AdminTransactionsCell'
|
||||
import { standard } from './AdminTransactionsCell.mock'
|
||||
|
||||
// Generated boilerplate tests do not account for all circumstances
|
||||
// and can fail without adjustments, e.g. Float and DateTime types.
|
||||
// Please refer to the RedwoodJS Testing Docs:
|
||||
// https://redwoodjs.com/docs/testing#testing-cells
|
||||
// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
|
||||
|
||||
describe('UserTransactionsCell', () => {
|
||||
it('renders Loading successfully', () => {
|
||||
expect(() => {
|
||||
render(<Loading />)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('renders Empty successfully', async () => {
|
||||
expect(() => {
|
||||
render(<Empty />)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('renders Failure successfully', async () => {
|
||||
expect(() => {
|
||||
render(<Failure error={new Error('Oh no')} />)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
// When you're ready to test the actual output of your component render
|
||||
// you could test that, for example, certain text is present:
|
||||
//
|
||||
// 1. import { screen } from '@redwoodjs/testing/web'
|
||||
// 2. Add test: expect(screen.getByText('Hello, world')).toBeInTheDocument()
|
||||
|
||||
it('renders Success successfully', async () => {
|
||||
expect(() => {
|
||||
render(<Success userTransactions={standard().userTransactions} />)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
@ -0,0 +1,104 @@
|
||||
/* eslint-disable jsx-a11y/no-noninteractive-tabindex */
|
||||
import { mdiAlert } from '@mdi/js'
|
||||
import { Icon } from '@mdi/react'
|
||||
import type { TransactionsQuery } from 'types/graphql'
|
||||
|
||||
import { Link, routes } from '@redwoodjs/router'
|
||||
import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web'
|
||||
|
||||
import TransactionListItem from '../TransactionListItem/TransactionListItem'
|
||||
|
||||
export const beforeQuery = ({ filter }) => {
|
||||
filter = filter && ['both', 'in', 'out'].includes(filter) ? filter : 'both'
|
||||
|
||||
return { variables: { filter } }
|
||||
}
|
||||
|
||||
export const QUERY = gql`
|
||||
query TransactionsQuery($filter: FilterTransactionsByType!) {
|
||||
transactions(filter: $filter) {
|
||||
transactions {
|
||||
id
|
||||
date
|
||||
parts
|
||||
type
|
||||
user {
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
}
|
||||
filter
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const Loading = () => (
|
||||
<div className="flex w-auto justify-center">
|
||||
<p className="loading loading-bars loading-lg" />
|
||||
</div>
|
||||
)
|
||||
|
||||
export const Empty = () => (
|
||||
<div className="flex">
|
||||
<div className="alert w-auto">
|
||||
<p className="text-center font-inter">It's empty in here...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export const Failure = ({ error }: CellFailureProps) => (
|
||||
<div className="flex w-auto justify-center">
|
||||
<div className="alert alert-error w-auto">
|
||||
<Icon path={mdiAlert} className="h-6 w-6" />
|
||||
<p className="font-inter">Error! {error?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export const Success = ({
|
||||
transactions,
|
||||
}: CellSuccessProps<TransactionsQuery>) => {
|
||||
if (transactions.transactions.length == 0) return Empty()
|
||||
return (
|
||||
<div className="flex flex-col space-y-6 font-inter">
|
||||
<div className="dropdown">
|
||||
<label tabIndex={0} className="btn m-1 normal-case">
|
||||
Filter:{' '}
|
||||
{transactions.filter == 'both'
|
||||
? 'None'
|
||||
: transactions.filter.replace(
|
||||
transactions.filter[0],
|
||||
transactions.filter[0].toUpperCase()
|
||||
)}
|
||||
</label>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className="dropdown-content rounded-box z-10 mt-3 w-auto space-y-3 bg-base-100 p-3 shadow"
|
||||
>
|
||||
{['None', 'In', 'Out'].map((filter) => (
|
||||
<li key={filter}>
|
||||
<Link
|
||||
className="btn btn-ghost w-full normal-case"
|
||||
to={routes.adminTransactions({
|
||||
filter: filter === 'None' ? 'both' : filter.toLowerCase(),
|
||||
})}
|
||||
>
|
||||
{filter}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
{transactions.transactions.map((item, i) => {
|
||||
return (
|
||||
<TransactionListItem
|
||||
transaction={item}
|
||||
key={i}
|
||||
returnButton={false}
|
||||
admin
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
6
web/src/components/BasketCell/BasketCell.mock.ts
Normal file
6
web/src/components/BasketCell/BasketCell.mock.ts
Normal file
@ -0,0 +1,6 @@
|
||||
// Define your own mock data here:
|
||||
export const standard = (/* vars, { ctx, req } */) => ({
|
||||
basket: {
|
||||
id: 42,
|
||||
},
|
||||
})
|
34
web/src/components/BasketCell/BasketCell.stories.tsx
Normal file
34
web/src/components/BasketCell/BasketCell.stories.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { Loading, Empty, Failure, Success } from './BasketCell'
|
||||
import { standard } from './BasketCell.mock'
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'Cells/BasketCell',
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
export const loading: StoryObj<typeof Loading> = {
|
||||
render: () => {
|
||||
return Loading ? <Loading /> : <></>
|
||||
},
|
||||
}
|
||||
|
||||
export const empty: StoryObj<typeof Empty> = {
|
||||
render: () => {
|
||||
return Empty ? <Empty /> : <></>
|
||||
},
|
||||
}
|
||||
|
||||
export const failure: StoryObj<typeof Failure> = {
|
||||
render: (args) => {
|
||||
return Failure ? <Failure error={new Error('Oh no')} {...args} /> : <></>
|
||||
},
|
||||
}
|
||||
|
||||
export const success: StoryObj<typeof Success> = {
|
||||
render: (args) => {
|
||||
return Success ? <Success {...standard()} {...args} /> : <></>
|
||||
},
|
||||
}
|
41
web/src/components/BasketCell/BasketCell.test.tsx
Normal file
41
web/src/components/BasketCell/BasketCell.test.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { render } from '@redwoodjs/testing/web'
|
||||
import { Loading, Empty, Failure, Success } from './BasketCell'
|
||||
import { standard } from './BasketCell.mock'
|
||||
|
||||
// Generated boilerplate tests do not account for all circumstances
|
||||
// and can fail without adjustments, e.g. Float and DateTime types.
|
||||
// Please refer to the RedwoodJS Testing Docs:
|
||||
// https://redwoodjs.com/docs/testing#testing-cells
|
||||
// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
|
||||
|
||||
describe('BasketCell', () => {
|
||||
it('renders Loading successfully', () => {
|
||||
expect(() => {
|
||||
render(<Loading />)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('renders Empty successfully', async () => {
|
||||
expect(() => {
|
||||
render(<Empty />)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('renders Failure successfully', async () => {
|
||||
expect(() => {
|
||||
render(<Failure error={new Error('Oh no')} />)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
// When you're ready to test the actual output of your component render
|
||||
// you could test that, for example, certain text is present:
|
||||
//
|
||||
// 1. import { screen } from '@redwoodjs/testing/web'
|
||||
// 2. Add test: expect(screen.getByText('Hello, world')).toBeInTheDocument()
|
||||
|
||||
it('renders Success successfully', async () => {
|
||||
expect(() => {
|
||||
render(<Success basket={standard().basket} />)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
207
web/src/components/BasketCell/BasketCell.tsx
Normal file
207
web/src/components/BasketCell/BasketCell.tsx
Normal file
@ -0,0 +1,207 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import { mdiMinus, mdiPlus, mdiDelete } from '@mdi/js'
|
||||
import Icon from '@mdi/react'
|
||||
import type { CreateTransactionInput } from 'types/graphql'
|
||||
|
||||
import { useMutation } from '@redwoodjs/web'
|
||||
import { toast } from '@redwoodjs/web/dist/toast'
|
||||
|
||||
import { useAuth } from 'src/auth'
|
||||
import {
|
||||
getBasket,
|
||||
setBasket,
|
||||
removeFromBasket,
|
||||
clearBasket,
|
||||
} from 'src/lib/basket'
|
||||
|
||||
import ToastNotification from '../ToastNotification'
|
||||
|
||||
export const CREATE_TRANSACTION_MUTATION = gql`
|
||||
mutation CreateTransactionMutation($input: CreateTransactionInput!) {
|
||||
createTransaction(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const thumbnail = (url: string) => {
|
||||
if (url.includes('no_image.png')) return url
|
||||
const parts = url.split('/')
|
||||
parts.splice(3, 0, 'resize=width:160')
|
||||
return parts.join('/')
|
||||
}
|
||||
|
||||
export const BasketCell = () => {
|
||||
const { isAuthenticated, currentUser } = useAuth()
|
||||
const [basket, setBasketState] = useState(getBasket())
|
||||
|
||||
const [createTransaction, { loading }] = useMutation(
|
||||
CREATE_TRANSACTION_MUTATION,
|
||||
{
|
||||
onCompleted: () => {
|
||||
toast.custom((t) => (
|
||||
<ToastNotification
|
||||
toast={t}
|
||||
type="success"
|
||||
message="Transaction complete"
|
||||
/>
|
||||
))
|
||||
|
||||
setBasketState(clearBasket())
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.custom((t) => (
|
||||
<ToastNotification toast={t} type="error" message={error.message} />
|
||||
))
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const submitBasket = (input: CreateTransactionInput) => {
|
||||
createTransaction({ variables: { input } })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{basket.length > 0 ? (
|
||||
basket.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex max-w-5xl items-center rounded-xl bg-base-200 shadow-xl"
|
||||
>
|
||||
<img
|
||||
alt={item.part.name}
|
||||
className="hidden h-20 w-20 rounded-l-xl object-cover sm:flex"
|
||||
src={thumbnail(item.part.imageUrl)}
|
||||
/>
|
||||
<div className="m-3 w-full items-center justify-between space-y-3 sm:flex sm:space-y-0">
|
||||
<p className="overflow-hidden text-ellipsis whitespace-nowrap font-inter text-lg font-bold">
|
||||
{item.part.name}
|
||||
</p>
|
||||
<div className="flex justify-between space-x-3">
|
||||
<div className="join">
|
||||
<button
|
||||
className={`btn join-item ${
|
||||
item.quantity <= 1 ? 'btn-disabled' : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
const newBasket = basket
|
||||
newBasket[i].quantity -= 1
|
||||
setBasketState(setBasket(newBasket))
|
||||
}}
|
||||
>
|
||||
<Icon path={mdiMinus} className="h-6 w-6" />
|
||||
</button>
|
||||
<p className="btn join-item items-center font-inter text-lg">
|
||||
{item.quantity}
|
||||
</p>
|
||||
<button
|
||||
className={`btn join-item ${
|
||||
item.quantity >= item.part.availableStock
|
||||
? 'btn-disabled'
|
||||
: ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
const newBasket = basket
|
||||
newBasket[i].quantity += 1
|
||||
setBasketState(setBasket(newBasket))
|
||||
}}
|
||||
>
|
||||
<Icon path={mdiPlus} className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-ghost hover:shadow-lg"
|
||||
onClick={() => {
|
||||
const newBasket = removeFromBasket(i)
|
||||
|
||||
if (typeof newBasket == 'string')
|
||||
toast.custom((t) => (
|
||||
<ToastNotification
|
||||
toast={t}
|
||||
type="error"
|
||||
message={newBasket}
|
||||
/>
|
||||
))
|
||||
else {
|
||||
setBasketState(newBasket)
|
||||
toast.custom((t) => (
|
||||
<ToastNotification
|
||||
toast={t}
|
||||
type="success"
|
||||
message={`Removed ${item.part.name} from basket`}
|
||||
/>
|
||||
))
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
path={mdiDelete}
|
||||
className="h-8 w-8 text-base-content"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="flex">
|
||||
<div className="alert w-auto shadow-lg">
|
||||
<p className="text-center font-inter">It's empty in here...</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{basket.length > 0 ? (
|
||||
<div className="flex space-x-3 pt-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
setBasketState(clearBasket())
|
||||
toast.custom((t) => (
|
||||
<ToastNotification
|
||||
toast={t}
|
||||
type="success"
|
||||
message="Basket cleared"
|
||||
/>
|
||||
))
|
||||
}}
|
||||
className="btn font-inter"
|
||||
>
|
||||
Clear basket
|
||||
</button>
|
||||
<button
|
||||
disabled={loading}
|
||||
onClick={() => {
|
||||
if (!isAuthenticated)
|
||||
toast.custom((t) => (
|
||||
<ToastNotification
|
||||
toast={t}
|
||||
type="error"
|
||||
message="You must be logged in to do that"
|
||||
/>
|
||||
))
|
||||
else {
|
||||
submitBasket({
|
||||
date: new Date().toISOString(),
|
||||
type: 'out',
|
||||
userId: currentUser.id,
|
||||
parts: basket.map((item) => ({
|
||||
part: JSON.stringify(item.part),
|
||||
quantity: item.quantity,
|
||||
})),
|
||||
})
|
||||
}
|
||||
}}
|
||||
className={`btn btn-primary font-inter ${
|
||||
loading ? 'btn-disabled' : ''
|
||||
}`}
|
||||
>
|
||||
Checkout
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -40,18 +40,25 @@ const Part = ({ part }: Props) => {
|
||||
}
|
||||
}
|
||||
|
||||
const preview = (url: string) => {
|
||||
if (url.includes('no_image.png')) return url
|
||||
const parts = url.split('/')
|
||||
parts.splice(3, 0, 'resize=height:500')
|
||||
return parts.join('/')
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rw-segment">
|
||||
<div className="rw-segment font-inter">
|
||||
<header className="rw-segment-header">
|
||||
<h2 className="rw-heading rw-heading-secondary">
|
||||
Part {part.id} Detail
|
||||
Part {part.id} details
|
||||
</h2>
|
||||
</header>
|
||||
<table className="rw-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>ID</th>
|
||||
<td>{part.id}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -68,7 +75,13 @@ const Part = ({ part }: Props) => {
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Image</th>
|
||||
<td>{part.imageUrl}</td>
|
||||
<td>
|
||||
<img
|
||||
alt=""
|
||||
src={preview(part.imageUrl)}
|
||||
style={{ display: 'block', margin: '2rem 0' }}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Created at</th>
|
||||
|
@ -28,11 +28,11 @@ const PartForm = (props: PartFormProps) => {
|
||||
const [imageUrl, setImageUrl] = useState(props?.part?.imageUrl)
|
||||
|
||||
const onSubmit = (data: FormPart) => {
|
||||
console.log(imageUrl)
|
||||
const dataWithImageUrl = Object.assign(data, {
|
||||
const dataUpdated = Object.assign(data, {
|
||||
imageUrl: imageUrl ?? '/no_image.png',
|
||||
})
|
||||
props.onSave(dataWithImageUrl, props?.part?.id)
|
||||
|
||||
props.onSave(dataUpdated, props?.part?.id)
|
||||
}
|
||||
|
||||
const onImageUpload = (response) => {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { mdiAlert } from '@mdi/js'
|
||||
import Icon from '@mdi/react'
|
||||
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'
|
||||
@ -18,21 +19,27 @@ export const QUERY = gql`
|
||||
}
|
||||
`
|
||||
|
||||
export const Loading = () => <div>Loading...</div>
|
||||
export const Loading = () => (
|
||||
<div className="flex w-auto justify-center">
|
||||
<p className="loading loading-bars loading-lg" />
|
||||
</div>
|
||||
)
|
||||
|
||||
export const Empty = () => {
|
||||
return (
|
||||
<div className="rw-text-center p-4">
|
||||
<span className="font-inter">No parts yet.</span>{' '}
|
||||
<Link to={routes.newPart()} className="rw-link">
|
||||
{'Create one?'}
|
||||
</Link>
|
||||
export const Empty = () => (
|
||||
<div className="flex justify-center">
|
||||
<div className="alert w-auto">
|
||||
<p className="text-center font-inter">It's empty in here...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
|
||||
export const Failure = ({ error }: CellFailureProps) => (
|
||||
<div className="rw-cell-error">{error?.message}</div>
|
||||
<div className="flex w-auto justify-center">
|
||||
<div className="alert alert-error w-auto">
|
||||
<Icon path={mdiAlert} className="h-6 w-6" />
|
||||
<p className="font-inter">Error! {error?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export const Success = ({ parts }: CellSuccessProps<FindParts>) => {
|
||||
|
@ -1,3 +1,5 @@
|
||||
import type { Part } from 'types/graphql'
|
||||
|
||||
import { Link, routes } from '@redwoodjs/router'
|
||||
import { toast } from '@redwoodjs/web/toast'
|
||||
|
||||
@ -6,14 +8,7 @@ import { addToBasket } from 'src/lib/basket'
|
||||
import ToastNotification from '../ToastNotification'
|
||||
|
||||
interface Props {
|
||||
part: {
|
||||
id: number
|
||||
name: string
|
||||
description?: string
|
||||
availableStock: number
|
||||
imageUrl: string
|
||||
createdAt: string
|
||||
}
|
||||
part: Part
|
||||
}
|
||||
|
||||
const thumbnail = (url: string) => {
|
||||
@ -25,66 +20,66 @@ const thumbnail = (url: string) => {
|
||||
|
||||
const PartsGridUnit = ({ part }: Props) => {
|
||||
return (
|
||||
<div className="card-compact card w-72 space-y-3 bg-base-100 font-inter shadow-xl sm:w-96">
|
||||
<figure>
|
||||
<img
|
||||
className="h-48 object-cover"
|
||||
src={thumbnail(part.imageUrl)}
|
||||
width={384}
|
||||
height={128}
|
||||
alt={part.name + ' image'}
|
||||
/>
|
||||
</figure>
|
||||
<div className="card-body">
|
||||
<h2 className="card-title justify-between">
|
||||
<Link
|
||||
className="link-hover link w-auto overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
to={routes.partDetails({ id: part.id })}
|
||||
>
|
||||
<Link to={routes.partDetails({ id: part.id })}>
|
||||
<div className="card-compact card w-72 space-y-3 bg-base-100 font-inter shadow-xl transition-all duration-200 hover:-translate-y-2 hover:shadow-2xl sm:w-96">
|
||||
<figure>
|
||||
<img
|
||||
className="h-48 object-cover"
|
||||
src={thumbnail(part.imageUrl)}
|
||||
width={384}
|
||||
height={128}
|
||||
alt={part.name + ' image'}
|
||||
/>
|
||||
</figure>
|
||||
<div className="card-body">
|
||||
<h2 className="card-title justify-between">
|
||||
{part.name}
|
||||
</Link>
|
||||
{part.availableStock == 0 ? (
|
||||
<div className="badge badge-error">Out of stock</div>
|
||||
) : (
|
||||
<div className="badge badge-ghost whitespace-nowrap">
|
||||
{part.availableStock + ' left'}
|
||||
</div>
|
||||
)}
|
||||
</h2>
|
||||
<p className="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{part.description}
|
||||
</p>
|
||||
<div className="card-actions justify-end">
|
||||
<button
|
||||
className={`btn btn-primary ${
|
||||
part.availableStock == 0 ? 'btn-disabled' : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
const newBasket = addToBasket(part, 1)
|
||||
{part.availableStock == 0 ? (
|
||||
<div className="badge badge-error">Out of stock</div>
|
||||
) : (
|
||||
<div className="badge badge-ghost whitespace-nowrap">
|
||||
{part.availableStock + ' left'}
|
||||
</div>
|
||||
)}
|
||||
</h2>
|
||||
<p className="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{part.description}
|
||||
</p>
|
||||
<div className="card-actions justify-end">
|
||||
<button
|
||||
className={`btn btn-primary ${
|
||||
part.availableStock == 0 ? 'btn-disabled' : ''
|
||||
}`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
|
||||
if (typeof newBasket == 'string')
|
||||
toast.custom((t) => (
|
||||
<ToastNotification
|
||||
toast={t}
|
||||
type="error"
|
||||
message={newBasket}
|
||||
/>
|
||||
))
|
||||
else
|
||||
toast.custom((t) => (
|
||||
<ToastNotification
|
||||
toast={t}
|
||||
type="success"
|
||||
message={`Added 1 ${part.name} to basket`}
|
||||
/>
|
||||
))
|
||||
}}
|
||||
>
|
||||
Add to basket
|
||||
</button>
|
||||
const newBasket = addToBasket(part, 1)
|
||||
|
||||
if (typeof newBasket == 'string')
|
||||
toast.custom((t) => (
|
||||
<ToastNotification
|
||||
toast={t}
|
||||
type="error"
|
||||
message={newBasket}
|
||||
/>
|
||||
))
|
||||
else
|
||||
toast.custom((t) => (
|
||||
<ToastNotification
|
||||
toast={t}
|
||||
type="success"
|
||||
message={`Added 1 ${part.name} to basket`}
|
||||
/>
|
||||
))
|
||||
}}
|
||||
>
|
||||
Add to basket
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -13,12 +13,12 @@ const ThemeToggle = () => {
|
||||
useEffect(() => {
|
||||
themeChange(false)
|
||||
return () => {
|
||||
themeChange(false)
|
||||
themeChange(true)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<label className="btn btn-ghost swap swap-rotate w-12 hover:shadow-lg">
|
||||
<label className="swap-rotate btn btn-ghost swap w-12 hover:shadow-lg">
|
||||
<input
|
||||
type="checkbox"
|
||||
defaultChecked={isDark}
|
||||
|
@ -35,7 +35,7 @@ const ToastNotification = ({ type, message, toast }: Props) => (
|
||||
: mdiInformation
|
||||
}
|
||||
/>
|
||||
<p className="font-inter">{message}</p>
|
||||
<p className="m-3 ml-0 font-inter">{message}</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
|
@ -0,0 +1,25 @@
|
||||
// Pass props to your component by passing an `args` object to your story
|
||||
//
|
||||
// ```tsx
|
||||
// export const Primary: Story = {
|
||||
// args: {
|
||||
// propName: propValue
|
||||
// }
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// See https://storybook.js.org/docs/react/writing-stories/args.
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import TransactionListItem from './TransactionListItem'
|
||||
|
||||
const meta: Meta<typeof TransactionListItem> = {
|
||||
component: TransactionListItem,
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof TransactionListItem>
|
||||
|
||||
export const Primary: Story = {}
|
@ -0,0 +1,14 @@
|
||||
import { render } from '@redwoodjs/testing/web'
|
||||
|
||||
import TransactionListItem from './TransactionListItem'
|
||||
|
||||
// Improve this test with help from the Redwood Testing Doc:
|
||||
// https://redwoodjs.com/docs/testing#testing-components
|
||||
|
||||
describe('TransactionListItem', () => {
|
||||
it('renders successfully', () => {
|
||||
expect(() => {
|
||||
render(<TransactionListItem />)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
137
web/src/components/TransactionListItem/TransactionListItem.tsx
Normal file
137
web/src/components/TransactionListItem/TransactionListItem.tsx
Normal file
@ -0,0 +1,137 @@
|
||||
import { Prisma } from '@prisma/client'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import type { Part, Transaction, TransactionType } from 'types/graphql'
|
||||
|
||||
import { useMutation } from '@redwoodjs/web'
|
||||
import { toast } from '@redwoodjs/web/dist/toast'
|
||||
|
||||
import { useAuth } from 'src/auth'
|
||||
|
||||
import ToastNotification from '../ToastNotification'
|
||||
|
||||
interface Props {
|
||||
transaction:
|
||||
| {
|
||||
id: number
|
||||
date: string
|
||||
parts: Prisma.JsonValue[]
|
||||
type: TransactionType
|
||||
}
|
||||
| Transaction
|
||||
returnButton?: boolean
|
||||
admin?: boolean
|
||||
}
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
export const UPDATE_TRANSACTION_MUTATION = gql`
|
||||
mutation UpdateTransactionMutation($id: Int!, $userId: Int!) {
|
||||
returnTransaction(id: $id, userId: $userId) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const TransactionListItem = ({
|
||||
transaction,
|
||||
returnButton = true,
|
||||
admin = false,
|
||||
}: Props) => {
|
||||
const elapsedTime = dayjs(transaction.date).fromNow()
|
||||
|
||||
const [returnTransaction, { loading }] = useMutation(
|
||||
UPDATE_TRANSACTION_MUTATION,
|
||||
{
|
||||
onCompleted: () => {
|
||||
toast.custom((t) => (
|
||||
<ToastNotification
|
||||
toast={t}
|
||||
type="success"
|
||||
message="Transaction updated"
|
||||
/>
|
||||
))
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.custom((t) => (
|
||||
<ToastNotification toast={t} type="error" message={error.message} />
|
||||
))
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const { currentUser } = useAuth()
|
||||
|
||||
return (
|
||||
<div className="collapse-arrow collapse max-w-5xl bg-base-200 font-inter">
|
||||
<input type="checkbox" />
|
||||
<div className="collapse-title text-xl font-medium">
|
||||
<div className="items-center justify-between space-x-3 space-y-3 sm:flex sm:space-y-0">
|
||||
<p className="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{transaction.parts.length} items
|
||||
</p>
|
||||
<div className="flex w-fit space-x-3">
|
||||
{admin ? (
|
||||
<div className="badge whitespace-nowrap bg-base-300 sm:badge-lg">{`${
|
||||
(transaction as Transaction).user?.firstName
|
||||
} ${(transaction as Transaction).user?.lastName}`}</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<div
|
||||
className={`badge sm:badge-lg ${
|
||||
transaction.type == 'out' ? 'badge-error' : 'badge-success'
|
||||
}`}
|
||||
>
|
||||
{transaction.type.replace(
|
||||
transaction.type[0],
|
||||
transaction.type[0].toUpperCase()
|
||||
)}
|
||||
</div>
|
||||
<div className="badge whitespace-nowrap bg-base-300 sm:badge-lg">
|
||||
{elapsedTime.replace(
|
||||
elapsedTime[0],
|
||||
elapsedTime[0].toUpperCase()
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="collapse-content space-y-3">
|
||||
<ul className="mx-4 list-disc space-y-2">
|
||||
{transaction.parts.map((raw) => {
|
||||
const part = JSON.parse(raw['part']) as Part
|
||||
const quantity = raw['quantity']
|
||||
return (
|
||||
<li className="list-item" key={part.id}>
|
||||
<div className="flex justify-between space-x-3">
|
||||
<p>{part.name}</p>
|
||||
<div className="badge badge-info">
|
||||
<p>Quantity: {quantity}</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
{transaction.type == 'out' && returnButton ? (
|
||||
<button
|
||||
className={`btn btn-primary ${loading ? 'btn-disabled' : ''}`}
|
||||
disabled={loading}
|
||||
onClick={() =>
|
||||
returnTransaction({
|
||||
variables: { id: transaction.id, userId: currentUser.id },
|
||||
})
|
||||
}
|
||||
>
|
||||
Return
|
||||
</button>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TransactionListItem
|
@ -0,0 +1,4 @@
|
||||
// Define your own mock data here:
|
||||
export const standard = (/* vars, { ctx, req } */) => ({
|
||||
userTransactions: [{ id: 42 }, { id: 43 }, { id: 44 }],
|
||||
})
|
@ -0,0 +1,34 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { Loading, Empty, Failure, Success } from './UserTransactionsCell'
|
||||
import { standard } from './UserTransactionsCell.mock'
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'Cells/UserTransactionsCell',
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
export const loading: StoryObj<typeof Loading> = {
|
||||
render: () => {
|
||||
return Loading ? <Loading /> : <></>
|
||||
},
|
||||
}
|
||||
|
||||
export const empty: StoryObj<typeof Empty> = {
|
||||
render: () => {
|
||||
return Empty ? <Empty /> : <></>
|
||||
},
|
||||
}
|
||||
|
||||
export const failure: StoryObj<typeof Failure> = {
|
||||
render: (args) => {
|
||||
return Failure ? <Failure error={new Error('Oh no')} {...args} /> : <></>
|
||||
},
|
||||
}
|
||||
|
||||
export const success: StoryObj<typeof Success> = {
|
||||
render: (args) => {
|
||||
return Success ? <Success {...standard()} {...args} /> : <></>
|
||||
},
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
import { render } from '@redwoodjs/testing/web'
|
||||
import { Loading, Empty, Failure, Success } from './UserTransactionsCell'
|
||||
import { standard } from './UserTransactionsCell.mock'
|
||||
|
||||
// Generated boilerplate tests do not account for all circumstances
|
||||
// and can fail without adjustments, e.g. Float and DateTime types.
|
||||
// Please refer to the RedwoodJS Testing Docs:
|
||||
// https://redwoodjs.com/docs/testing#testing-cells
|
||||
// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
|
||||
|
||||
describe('UserTransactionsCell', () => {
|
||||
it('renders Loading successfully', () => {
|
||||
expect(() => {
|
||||
render(<Loading />)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('renders Empty successfully', async () => {
|
||||
expect(() => {
|
||||
render(<Empty />)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('renders Failure successfully', async () => {
|
||||
expect(() => {
|
||||
render(<Failure error={new Error('Oh no')} />)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
// When you're ready to test the actual output of your component render
|
||||
// you could test that, for example, certain text is present:
|
||||
//
|
||||
// 1. import { screen } from '@redwoodjs/testing/web'
|
||||
// 2. Add test: expect(screen.getByText('Hello, world')).toBeInTheDocument()
|
||||
|
||||
it('renders Success successfully', async () => {
|
||||
expect(() => {
|
||||
render(<Success userTransactions={standard().userTransactions} />)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
@ -0,0 +1,97 @@
|
||||
/* eslint-disable jsx-a11y/no-noninteractive-tabindex */
|
||||
import { mdiAlert } from '@mdi/js'
|
||||
import { Icon } from '@mdi/react'
|
||||
import type { UserTransactionsQuery } from 'types/graphql'
|
||||
|
||||
import { Link, routes } from '@redwoodjs/router'
|
||||
import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web'
|
||||
|
||||
import TransactionListItem from '../TransactionListItem/TransactionListItem'
|
||||
|
||||
export const beforeQuery = ({ filter, userId }) => {
|
||||
userId = userId ?? null
|
||||
filter = filter && ['both', 'in', 'out'].includes(filter) ? filter : 'both'
|
||||
|
||||
return { variables: { filter, userId } }
|
||||
}
|
||||
|
||||
export const QUERY = gql`
|
||||
query UserTransactionsQuery(
|
||||
$userId: Int!
|
||||
$filter: FilterTransactionsByType!
|
||||
) {
|
||||
userTransactions(userId: $userId, filter: $filter) {
|
||||
transactions {
|
||||
id
|
||||
date
|
||||
parts
|
||||
type
|
||||
}
|
||||
filter
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const Loading = () => (
|
||||
<div className="flex w-auto justify-center">
|
||||
<p className="loading loading-bars loading-lg" />
|
||||
</div>
|
||||
)
|
||||
|
||||
export const Empty = () => (
|
||||
<div className="flex">
|
||||
<div className="alert w-auto">
|
||||
<p className="text-center font-inter">It's empty in here...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export const Failure = ({ error }: CellFailureProps) => (
|
||||
<div className="flex w-auto justify-center">
|
||||
<div className="alert alert-error w-auto">
|
||||
<Icon path={mdiAlert} className="h-6 w-6" />
|
||||
<p className="font-inter">Error! {error?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export const Success = ({
|
||||
userTransactions,
|
||||
}: CellSuccessProps<UserTransactionsQuery>) => {
|
||||
if (userTransactions.transactions.length == 0) return Empty()
|
||||
return (
|
||||
<div className="flex flex-col space-y-6 font-inter">
|
||||
<div className="dropdown">
|
||||
<label tabIndex={0} className="btn m-1 normal-case">
|
||||
Filter:{' '}
|
||||
{userTransactions.filter == 'both'
|
||||
? 'None'
|
||||
: userTransactions.filter.replace(
|
||||
userTransactions.filter[0],
|
||||
userTransactions.filter[0].toUpperCase()
|
||||
)}
|
||||
</label>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className="dropdown-content rounded-box z-10 mt-3 w-auto space-y-3 bg-base-100 p-3 shadow"
|
||||
>
|
||||
{['None', 'In', 'Out'].map((filter) => (
|
||||
<li key={filter}>
|
||||
<Link
|
||||
className="btn btn-ghost w-full normal-case"
|
||||
to={routes.userTransactions({
|
||||
filter: filter === 'None' ? 'both' : filter.toLowerCase(),
|
||||
})}
|
||||
>
|
||||
{filter}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
{userTransactions.transactions.map((item, i) => {
|
||||
return <TransactionListItem transaction={item} key={i} />
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -7,6 +7,7 @@ import { Link, routes } from '@redwoodjs/router'
|
||||
import { Toaster } from '@redwoodjs/web/toast'
|
||||
|
||||
import { useAuth } from 'src/auth'
|
||||
import AdminMenu from 'src/components/AdminMenu/AdminMenu'
|
||||
import NavbarAccountIcon from 'src/components/NavbarAccountIcon/NavbarAccountIcon'
|
||||
import ThemeToggle from 'src/components/ThemeToggle/ThemeToggle'
|
||||
import { getBasket } from 'src/lib/basket'
|
||||
@ -38,20 +39,16 @@ const NavBarLayout = ({ children }: NavBarLayoutProps) => {
|
||||
</Link>
|
||||
</div>
|
||||
<div className="ml-auto justify-end space-x-3">
|
||||
{hasRole('admin') ? (
|
||||
<ul className="relative hidden items-center space-x-3 lg:flex">
|
||||
<li>
|
||||
<Link
|
||||
to={routes.parts()}
|
||||
className="btn btn-ghost font-inter hover:shadow-lg"
|
||||
>
|
||||
Parts
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<ul className="relative hidden items-center space-x-3 lg:flex">
|
||||
<li>
|
||||
<Link
|
||||
to={routes.userTransactions()}
|
||||
className="btn btn-ghost font-inter hover:shadow-lg"
|
||||
>
|
||||
Transactions
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
<ThemeToggle />
|
||||
<Link
|
||||
to={routes.basket()}
|
||||
@ -70,6 +67,7 @@ const NavBarLayout = ({ children }: NavBarLayoutProps) => {
|
||||
</div>
|
||||
</Link>
|
||||
<NavbarAccountIcon mobile={false} className="hidden lg:block" />
|
||||
<AdminMenu mobile={false} className="hidden lg:block" />
|
||||
<div className="lg:hidden">
|
||||
<input
|
||||
id="mobile-menu-drawer"
|
||||
@ -92,7 +90,11 @@ const NavBarLayout = ({ children }: NavBarLayoutProps) => {
|
||||
<ul className="min-h-full w-80 space-y-3 bg-base-100 p-3 text-base-content shadow-lg">
|
||||
<li>
|
||||
<div className="flex items-center justify-between">
|
||||
<Icon path={mdiChip} className="ml-3 h-10 text-logo" />
|
||||
{hasRole('admin') ? (
|
||||
<AdminMenu mobile={true} />
|
||||
) : (
|
||||
<Icon path={mdiChip} className="ml-3 h-10 text-logo" />
|
||||
)}
|
||||
<Link
|
||||
to={routes.home()}
|
||||
className="btn btn-ghost items-center hover:shadow-lg"
|
||||
@ -104,18 +106,6 @@ const NavBarLayout = ({ children }: NavBarLayoutProps) => {
|
||||
<NavbarAccountIcon mobile={true} />
|
||||
</div>
|
||||
</li>
|
||||
{hasRole('admin') ? (
|
||||
<li>
|
||||
<Link
|
||||
to={routes.parts()}
|
||||
className="btn btn-ghost w-full hover:shadow-lg"
|
||||
>
|
||||
<p className="font-inter text-base">Parts</p>
|
||||
</Link>
|
||||
</li>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<li>
|
||||
<Link
|
||||
to={routes.basket()}
|
||||
@ -124,6 +114,14 @@ const NavBarLayout = ({ children }: NavBarLayoutProps) => {
|
||||
<p className="font-inter text-base">Basket</p>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
to={routes.userTransactions()}
|
||||
className="btn btn-ghost w-full hover:shadow-lg"
|
||||
>
|
||||
<p className="font-inter text-base">Transactions</p>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -7,8 +7,8 @@ import { Toaster } from '@redwoodjs/web/toast'
|
||||
type LayoutProps = {
|
||||
title: string
|
||||
titleTo: string
|
||||
buttonLabel: string
|
||||
buttonTo: string
|
||||
buttonLabel?: string
|
||||
buttonTo?: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
@ -24,16 +24,20 @@ const ScaffoldLayout = ({
|
||||
<Toaster />
|
||||
<header className="rw-header">
|
||||
<div className="space-x-3">
|
||||
<Link to={routes.home()} className="btn btn-ghost">
|
||||
<Link to={routes.home()} className="btn btn-ghost hover:shadow-lg">
|
||||
<Icon path={mdiHome} className="h-8 w-8" />
|
||||
</Link>
|
||||
<h1 className="rw-heading rw-heading-primary rw-button btn-ghost normal-case">
|
||||
<Link to={routes[titleTo]()}>{title}</Link>
|
||||
</h1>
|
||||
</div>
|
||||
<Link to={routes[buttonTo]()} className="rw-button btn-success">
|
||||
<div className="rw-button-icon">+</div> {buttonLabel}
|
||||
</Link>
|
||||
{buttonLabel && buttonTo ? (
|
||||
<Link to={routes[buttonTo]()} className="rw-button btn-success">
|
||||
<div className="rw-button-icon">+</div> {buttonLabel}
|
||||
</Link>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</header>
|
||||
<main className="rw-main">{children}</main>
|
||||
</div>
|
||||
|
@ -0,0 +1,13 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import AdminTransactionsPage from './AdminTransactionsPage'
|
||||
|
||||
const meta: Meta<typeof AdminTransactionsPage> = {
|
||||
component: AdminTransactionsPage,
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof AdminTransactionsPage>
|
||||
|
||||
export const Primary: Story = {}
|
@ -0,0 +1,14 @@
|
||||
import { render } from '@redwoodjs/testing/web'
|
||||
|
||||
import AdminTransactionsPage from './AdminTransactionsPage'
|
||||
|
||||
// Improve this test with help from the Redwood Testing Doc:
|
||||
// https://redwoodjs.com/docs/testing#testing-pages-layouts
|
||||
|
||||
describe('AdminTransactionsPage', () => {
|
||||
it('renders successfully', () => {
|
||||
expect(() => {
|
||||
render(<AdminTransactionsPage />)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
@ -0,0 +1,50 @@
|
||||
import { mdiAlert } from '@mdi/js'
|
||||
import Icon from '@mdi/react'
|
||||
import { FilterTransactionsByType } from 'types/graphql'
|
||||
|
||||
import { MetaTags } from '@redwoodjs/web'
|
||||
|
||||
import { useAuth } from 'src/auth'
|
||||
import AdminTransactionsCell from 'src/components/AdminTransactionsCell'
|
||||
|
||||
interface Props {
|
||||
filter: FilterTransactionsByType
|
||||
}
|
||||
|
||||
const AdminTransactionsPage = ({ filter = 'both' }: Props) => {
|
||||
const { isAuthenticated, hasRole } = useAuth()
|
||||
|
||||
return (
|
||||
<>
|
||||
<MetaTags
|
||||
title="Admin Transactions"
|
||||
description="Admin Transactions page"
|
||||
/>
|
||||
|
||||
<div className="m-8">
|
||||
<p className="mb-8 font-inter text-3xl font-bold">Transactions</p>
|
||||
{isAuthenticated ? (
|
||||
hasRole('admin') ? (
|
||||
<AdminTransactionsCell filter={filter} />
|
||||
) : (
|
||||
<div className="flex w-auto justify-center">
|
||||
<div className="alert alert-error w-auto">
|
||||
<Icon path={mdiAlert} className="h-6 w-6" />
|
||||
<p className="font-inter">You must be admin to do that.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="flex w-auto justify-center">
|
||||
<div className="alert alert-error w-auto">
|
||||
<Icon path={mdiAlert} className="h-6 w-6" />
|
||||
<p className="font-inter">You must be logged in to do that.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AdminTransactionsPage
|
@ -1,169 +1,15 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import { mdiDelete, mdiMinus, mdiPlus } from '@mdi/js'
|
||||
import Icon from '@mdi/react'
|
||||
|
||||
import { MetaTags } from '@redwoodjs/web'
|
||||
import { toast } from '@redwoodjs/web/toast'
|
||||
|
||||
import { useAuth } from 'src/auth'
|
||||
import ToastNotification from 'src/components/ToastNotification'
|
||||
import {
|
||||
clearBasket,
|
||||
getBasket,
|
||||
removeFromBasket,
|
||||
setBasket,
|
||||
} from 'src/lib/basket'
|
||||
|
||||
const thumbnail = (url: string) => {
|
||||
if (url.includes('no_image.png')) return url
|
||||
const parts = url.split('/')
|
||||
parts.splice(3, 0, 'resize=width:160')
|
||||
return parts.join('/')
|
||||
}
|
||||
import { BasketCell } from 'src/components/BasketCell/BasketCell'
|
||||
|
||||
const BasketPage = () => {
|
||||
const { isAuthenticated } = useAuth()
|
||||
const [basket, setBasketState] = useState(getBasket())
|
||||
return (
|
||||
<>
|
||||
<MetaTags title="Basket" description="Basket page" />
|
||||
|
||||
<div className="m-8">
|
||||
<h1 className="mb-8 font-inter text-3xl font-bold">Basket</h1>
|
||||
<div className="space-y-3">
|
||||
{basket.length > 0 ? (
|
||||
basket.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex max-w-5xl items-center rounded-xl bg-base-100 shadow-xl"
|
||||
>
|
||||
<img
|
||||
alt={item.part.name}
|
||||
className="hidden h-20 w-20 rounded-l-xl object-cover sm:flex"
|
||||
src={thumbnail(item.part.imageUrl)}
|
||||
/>
|
||||
<div className="m-3 w-full items-center justify-between space-y-3 sm:flex sm:space-y-0">
|
||||
<p className="overflow-hidden text-ellipsis whitespace-nowrap font-inter text-lg font-bold">
|
||||
{item.part.name}
|
||||
</p>
|
||||
<div className="flex justify-between space-x-3">
|
||||
<div className="join">
|
||||
<button
|
||||
className={`btn join-item ${
|
||||
item.quantity == 1 ? 'btn-disabled' : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
const newBasket = basket
|
||||
newBasket[i].quantity -= 1
|
||||
setBasketState(setBasket(newBasket))
|
||||
}}
|
||||
>
|
||||
<Icon path={mdiMinus} className="h-6 w-6" />
|
||||
</button>
|
||||
<p className="btn join-item items-center font-inter text-lg">
|
||||
{item.quantity}
|
||||
</p>
|
||||
<button
|
||||
className={`btn join-item ${
|
||||
item.quantity == item.part.availableStock
|
||||
? 'btn-disabled'
|
||||
: ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
const newBasket = basket
|
||||
newBasket[i].quantity += 1
|
||||
setBasketState(setBasket(newBasket))
|
||||
}}
|
||||
>
|
||||
<Icon path={mdiPlus} className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-ghost hover:shadow-lg"
|
||||
onClick={() => {
|
||||
const newBasket = removeFromBasket(i)
|
||||
|
||||
if (typeof newBasket == 'string')
|
||||
toast.custom((t) => (
|
||||
<ToastNotification
|
||||
toast={t}
|
||||
type="error"
|
||||
message={newBasket}
|
||||
/>
|
||||
))
|
||||
else {
|
||||
setBasketState(newBasket)
|
||||
toast.custom((t) => (
|
||||
<ToastNotification
|
||||
toast={t}
|
||||
type="success"
|
||||
message={`Removed ${item.part.name} from basket`}
|
||||
/>
|
||||
))
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
path={mdiDelete}
|
||||
className="h-8 w-8 text-base-content"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="flex">
|
||||
<div className="alert w-auto shadow-lg">
|
||||
<p className="text-center font-inter">
|
||||
It's empty in here...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{basket.length > 0 ? (
|
||||
<div className="flex space-x-3 pt-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
setBasketState(clearBasket())
|
||||
toast.custom((t) => (
|
||||
<ToastNotification
|
||||
toast={t}
|
||||
type="success"
|
||||
message="Basket cleared"
|
||||
/>
|
||||
))
|
||||
}}
|
||||
className="btn font-inter"
|
||||
>
|
||||
Clear basket
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!isAuthenticated)
|
||||
toast.custom((t) => (
|
||||
<ToastNotification
|
||||
toast={t}
|
||||
type="error"
|
||||
message="You must be logged in to do that"
|
||||
/>
|
||||
))
|
||||
else {
|
||||
toast.custom((t) => (
|
||||
<ToastNotification toast={t} type="info" message="Todo" />
|
||||
))
|
||||
}
|
||||
}}
|
||||
className="btn btn-primary font-inter"
|
||||
>
|
||||
Checkout
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
<BasketCell />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
@ -33,6 +33,7 @@ const ForgotPasswordPage = () => {
|
||||
// The function `forgotPassword.handler` in api/src/functions/auth.js has
|
||||
// been invoked, let the user know how to get the link to reset their
|
||||
// password (sent in email, perhaps?)
|
||||
// TODO: forgot password
|
||||
toast.custom((t) => (
|
||||
<ToastNotification
|
||||
toast={t}
|
||||
|
@ -17,7 +17,7 @@ const HomePage = ({ page = 1, sort = 'id', order = 'ascending' }: Props) => {
|
||||
|
||||
<div className="my-16 text-center font-inter md:my-24">
|
||||
<h1 className="text-4xl font-extrabold tracking-wide md:text-6xl">
|
||||
Arduino Parts Inventory
|
||||
Parts Inventory
|
||||
</h1>
|
||||
<p className="pt-4 text-xl">Only take what you need</p>
|
||||
</div>
|
||||
|
13
web/src/pages/TransactionsPage/TransactionsPage.stories.tsx
Normal file
13
web/src/pages/TransactionsPage/TransactionsPage.stories.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import TransactionsPage from './TransactionsPage'
|
||||
|
||||
const meta: Meta<typeof TransactionsPage> = {
|
||||
component: TransactionsPage,
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof TransactionsPage>
|
||||
|
||||
export const Primary: Story = {}
|
14
web/src/pages/TransactionsPage/TransactionsPage.test.tsx
Normal file
14
web/src/pages/TransactionsPage/TransactionsPage.test.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { render } from '@redwoodjs/testing/web'
|
||||
|
||||
import TransactionsPage from './TransactionsPage'
|
||||
|
||||
// Improve this test with help from the Redwood Testing Doc:
|
||||
// https://redwoodjs.com/docs/testing#testing-pages-layouts
|
||||
|
||||
describe('TransactionsPage', () => {
|
||||
it('renders successfully', () => {
|
||||
expect(() => {
|
||||
render(<TransactionsPage />)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
42
web/src/pages/TransactionsPage/TransactionsPage.tsx
Normal file
42
web/src/pages/TransactionsPage/TransactionsPage.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { mdiAlert } from '@mdi/js'
|
||||
import Icon from '@mdi/react'
|
||||
import { FilterTransactionsByType } from 'types/graphql'
|
||||
|
||||
import { MetaTags } from '@redwoodjs/web'
|
||||
|
||||
import { useAuth } from 'src/auth'
|
||||
import UserTransactionsCell from 'src/components/UserTransactionsCell'
|
||||
|
||||
interface Props {
|
||||
filter: FilterTransactionsByType
|
||||
}
|
||||
|
||||
const TransactionsPage = ({ filter = 'both' }: Props) => {
|
||||
const { isAuthenticated, currentUser } = useAuth()
|
||||
return (
|
||||
<>
|
||||
<MetaTags title="Transactions" description="Transactions page" />
|
||||
|
||||
<div className="m-8">
|
||||
<h1 className="mb-8 font-inter text-3xl font-bold">
|
||||
Transaction History
|
||||
</h1>
|
||||
{isAuthenticated ? (
|
||||
<UserTransactionsCell
|
||||
filter={filter}
|
||||
userId={currentUser ? currentUser.id : -1}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex w-auto justify-center">
|
||||
<div className="alert alert-error w-auto">
|
||||
<Icon path={mdiAlert} className="h-6 w-6" />
|
||||
<p className="font-inter">You must be logged in to do that.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TransactionsPage
|
@ -5,9 +5,6 @@
|
||||
.rw-scaffold h2 {
|
||||
@apply m-0;
|
||||
}
|
||||
.rw-scaffold ul {
|
||||
@apply m-0 p-0;
|
||||
}
|
||||
.rw-scaffold input:-ms-input-placeholder {
|
||||
@apply text-gray-500;
|
||||
}
|
||||
|
348
yarn.lock
348
yarn.lock
@ -4209,15 +4209,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@redwoodjs/api-server@npm:6.3.2":
|
||||
version: 6.3.2
|
||||
resolution: "@redwoodjs/api-server@npm:6.3.2"
|
||||
"@redwoodjs/api-server@npm:6.3.3":
|
||||
version: 6.3.3
|
||||
resolution: "@redwoodjs/api-server@npm:6.3.3"
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3": 7.22.15
|
||||
"@fastify/http-proxy": 9.2.1
|
||||
"@fastify/static": 6.11.1
|
||||
"@fastify/url-data": 5.3.1
|
||||
"@redwoodjs/project-config": 6.3.2
|
||||
"@redwoodjs/project-config": 6.3.3
|
||||
ansi-colors: 4.1.3
|
||||
chalk: 4.1.2
|
||||
chokidar: 3.5.3
|
||||
@ -4236,13 +4236,13 @@ __metadata:
|
||||
rw-api-server-watch: dist/watch.js
|
||||
rw-log-formatter: dist/logFormatter/bin.js
|
||||
rw-server: dist/index.js
|
||||
checksum: b78b5492d35801c0219b940da49d19f5ab1e381c9f78f7cfc91a54a7f0364b28190b04db34fdd865b1509a4e9bc5d0d87722c5419a28cc60991a06177668e59d
|
||||
checksum: 29eb571c542093251eb64e1e4741774280f3e5d07369c485f97623ef736181748e70eaedf60be40f4fdeccad0f0003ee177373cc6c996056a4d726c81cd23533
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@redwoodjs/api@npm:6.3.2":
|
||||
version: 6.3.2
|
||||
resolution: "@redwoodjs/api@npm:6.3.2"
|
||||
"@redwoodjs/api@npm:6.3.3":
|
||||
version: 6.3.3
|
||||
resolution: "@redwoodjs/api@npm:6.3.3"
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3": 7.22.15
|
||||
"@prisma/client": 5.3.1
|
||||
@ -4266,50 +4266,49 @@ __metadata:
|
||||
rw: dist/bins/redwood.js
|
||||
rwfw: dist/bins/rwfw.js
|
||||
tsc: dist/bins/tsc.js
|
||||
checksum: 405a6247635cdab502a639ec180364aff57053d9dba83e4cd780caa3ce2015a92511c0297a6063f2b9245210f134131560578c9ca1015b69a0812e42fb2794ee
|
||||
checksum: 60f296d43593ed1a6ca71d2042df8a1ee54bd8f371f222534bb5e9b45b1566e22e2d776a84fc338531bbf46b83c32faebf859b09d7516287bab069b8e20faa5a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@redwoodjs/auth-dbauth-api@npm:6.3.2":
|
||||
version: 6.3.2
|
||||
resolution: "@redwoodjs/auth-dbauth-api@npm:6.3.2"
|
||||
"@redwoodjs/auth-dbauth-api@npm:6.3.3":
|
||||
version: 6.3.3
|
||||
resolution: "@redwoodjs/auth-dbauth-api@npm:6.3.3"
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3": 7.22.15
|
||||
base64url: 3.0.1
|
||||
core-js: 3.32.2
|
||||
crypto-js: 4.1.1
|
||||
md5: 2.3.0
|
||||
uuid: 9.0.0
|
||||
checksum: f282ec993c7b25270cbc825070b736169e4924652e229948fd634e6835719e1133c9d623f81a0bd148b684f8382c7e94503f4a8d269c272d8a192d6d8f5aed23
|
||||
checksum: 87ba04b2b9e7ff1c1fd0636bdc380fe9bf1e1788e83e3af28e557d6d02ea29370e85fb22a0ec184d672d45030e40e5c2d8aa22a7c1011ab3bdf8cb4226e0935c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@redwoodjs/auth-dbauth-web@npm:6.3.2":
|
||||
version: 6.3.2
|
||||
resolution: "@redwoodjs/auth-dbauth-web@npm:6.3.2"
|
||||
"@redwoodjs/auth-dbauth-web@npm:6.3.3":
|
||||
version: 6.3.3
|
||||
resolution: "@redwoodjs/auth-dbauth-web@npm:6.3.3"
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3": 7.22.15
|
||||
"@redwoodjs/auth": 6.3.2
|
||||
"@redwoodjs/auth": 6.3.3
|
||||
"@simplewebauthn/browser": 7.2.0
|
||||
core-js: 3.32.2
|
||||
checksum: 725297f0ba9935f80443d0a4aebb1e500db94c0761ebd79a189d9e835de9329a6ca62a707dec402085dcc8f6595392b1f2e00eecda7b13307b6437b8ccc26ff1
|
||||
checksum: cca850f6e5486690cdfe58bc9f3938e346a07174d1be90ca592240b58df1fb9aa97d5c9a198a8cd870fda144208a8c2caa58243b9f0fb885e44e27f3f30deace
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@redwoodjs/auth@npm:6.3.2":
|
||||
version: 6.3.2
|
||||
resolution: "@redwoodjs/auth@npm:6.3.2"
|
||||
"@redwoodjs/auth@npm:6.3.3":
|
||||
version: 6.3.3
|
||||
resolution: "@redwoodjs/auth@npm:6.3.3"
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3": 7.22.15
|
||||
core-js: 3.32.2
|
||||
react: 18.2.0
|
||||
checksum: 3031d01e8a3bcd0bcd046754c1f41a46f0693f10677a6c7eef39e99f6aa32eb4ca2c562498b2b8d0f0e6456d18187c3faa6c96dcca415326efe3d1a125ede48f
|
||||
checksum: b11adbdb02e161792af53e996ddace608b2b189706fc33b56a02d1a7592dd478d8bfe3b3f6b96f6b5c65f8efe35bf955f5dd86db0e296a5e138f518500ea160a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@redwoodjs/babel-config@npm:6.3.2":
|
||||
version: 6.3.2
|
||||
resolution: "@redwoodjs/babel-config@npm:6.3.2"
|
||||
"@redwoodjs/babel-config@npm:6.3.3":
|
||||
version: 6.3.3
|
||||
resolution: "@redwoodjs/babel-config@npm:6.3.3"
|
||||
dependencies:
|
||||
"@babel/core": ^7.22.20
|
||||
"@babel/parser": ^7.22.16
|
||||
@ -4324,7 +4323,7 @@ __metadata:
|
||||
"@babel/register": ^7.22.15
|
||||
"@babel/runtime-corejs3": 7.22.15
|
||||
"@babel/traverse": ^7.22.20
|
||||
"@redwoodjs/project-config": 6.3.2
|
||||
"@redwoodjs/project-config": 6.3.3
|
||||
babel-plugin-auto-import: 1.1.0
|
||||
babel-plugin-graphql-tag: 3.3.0
|
||||
babel-plugin-module-resolver: 5.0.0
|
||||
@ -4332,19 +4331,19 @@ __metadata:
|
||||
fast-glob: 3.3.1
|
||||
graphql: 16.8.1
|
||||
typescript: 5.2.2
|
||||
checksum: 9a3c08256daaef6a42d735a82823746825b3e132adee811f8798f37c745c6b9d096a0d9233c7f1e29c0e709712dac69a87af3de8cf76d140fe3f5f030009de38
|
||||
checksum: 2b6837d7ce952c1ba8700bee5475d63f5d1c66fe5a1840aac104289a63aa9443df9ff0c86233e935003b206a275e520cc9b83d926dee06d2517fba33d352de02
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@redwoodjs/cli-helpers@npm:6.3.2":
|
||||
version: 6.3.2
|
||||
resolution: "@redwoodjs/cli-helpers@npm:6.3.2"
|
||||
"@redwoodjs/cli-helpers@npm:6.3.3":
|
||||
version: 6.3.3
|
||||
resolution: "@redwoodjs/cli-helpers@npm:6.3.3"
|
||||
dependencies:
|
||||
"@babel/core": ^7.22.20
|
||||
"@babel/runtime-corejs3": 7.22.15
|
||||
"@opentelemetry/api": 1.4.1
|
||||
"@redwoodjs/project-config": 6.3.2
|
||||
"@redwoodjs/telemetry": 6.3.2
|
||||
"@redwoodjs/project-config": 6.3.3
|
||||
"@redwoodjs/telemetry": 6.3.3
|
||||
chalk: 4.1.2
|
||||
core-js: 3.32.2
|
||||
execa: 5.1.1
|
||||
@ -4354,13 +4353,13 @@ __metadata:
|
||||
prettier: 2.8.8
|
||||
prompts: 2.4.2
|
||||
terminal-link: 2.1.1
|
||||
checksum: a54dd0617091c50293d207704059ac88d756ba54d8e9656c74651076db992ecb79056d6cdb90f08c8ab729d42b7ede0177931b0d6c2763a73f01e9549eaddd33
|
||||
checksum: 84b63e75daf82c6a10f6249e4c327202eb86827ab65a6fc0e728d41b8ac653f0babc29e7517a56e86a3d0a4e7ed8af56eebfc1e4a3f175831d73bbf1cab4ce45
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@redwoodjs/cli@npm:6.3.2":
|
||||
version: 6.3.2
|
||||
resolution: "@redwoodjs/cli@npm:6.3.2"
|
||||
"@redwoodjs/cli@npm:6.3.3":
|
||||
version: 6.3.3
|
||||
resolution: "@redwoodjs/cli@npm:6.3.3"
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3": 7.22.15
|
||||
"@iarna/toml": 2.2.5
|
||||
@ -4371,15 +4370,14 @@ __metadata:
|
||||
"@opentelemetry/sdk-trace-node": 1.15.2
|
||||
"@opentelemetry/semantic-conventions": 1.15.2
|
||||
"@prisma/internals": 5.3.1
|
||||
"@redwoodjs/api-server": 6.3.2
|
||||
"@redwoodjs/cli-helpers": 6.3.2
|
||||
"@redwoodjs/fastify": 6.3.2
|
||||
"@redwoodjs/internal": 6.3.2
|
||||
"@redwoodjs/prerender": 6.3.2
|
||||
"@redwoodjs/project-config": 6.3.2
|
||||
"@redwoodjs/structure": 6.3.2
|
||||
"@redwoodjs/telemetry": 6.3.2
|
||||
"@types/secure-random-password": 0.2.1
|
||||
"@redwoodjs/api-server": 6.3.3
|
||||
"@redwoodjs/cli-helpers": 6.3.3
|
||||
"@redwoodjs/fastify": 6.3.3
|
||||
"@redwoodjs/internal": 6.3.3
|
||||
"@redwoodjs/prerender": 6.3.3
|
||||
"@redwoodjs/project-config": 6.3.3
|
||||
"@redwoodjs/structure": 6.3.3
|
||||
"@redwoodjs/telemetry": 6.3.3
|
||||
boxen: 5.1.2
|
||||
camelcase: 6.3.0
|
||||
chalk: 4.1.2
|
||||
@ -4388,7 +4386,6 @@ __metadata:
|
||||
configstore: 3.1.5
|
||||
core-js: 3.32.2
|
||||
cross-env: 7.0.3
|
||||
crypto-js: 4.1.1
|
||||
decamelize: 5.0.1
|
||||
dotenv-defaults: 5.0.2
|
||||
enquirer: 2.4.1
|
||||
@ -4408,7 +4405,6 @@ __metadata:
|
||||
prisma: 5.3.1
|
||||
prompts: 2.4.2
|
||||
rimraf: 5.0.1
|
||||
secure-random-password: 0.2.3
|
||||
semver: 7.5.3
|
||||
string-env-interpolation: 1.0.1
|
||||
systeminformation: 5.21.7
|
||||
@ -4420,23 +4416,23 @@ __metadata:
|
||||
redwood: dist/index.js
|
||||
rw: dist/index.js
|
||||
rwfw: dist/rwfw.js
|
||||
checksum: 8e00b10053bb3effa9a91453d83ca71a07e1273104d95132f64aa27e2d8200bf745b7478cf12011ec66fe80a10f7b5f0f8db7c02e7de9bca19ad4fe49fa67ac5
|
||||
checksum: b40d6192b81ba6778e6324b4106f551e79c4e273f94379447417f03f5687c02ac9edac0c3535856e4a5471fe366c82791d73cf799b6f05202f8f53f856e28f8d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@redwoodjs/core@npm:6.3.2":
|
||||
version: 6.3.2
|
||||
resolution: "@redwoodjs/core@npm:6.3.2"
|
||||
"@redwoodjs/core@npm:6.3.3":
|
||||
version: 6.3.3
|
||||
resolution: "@redwoodjs/core@npm:6.3.3"
|
||||
dependencies:
|
||||
"@babel/cli": 7.22.15
|
||||
"@babel/runtime-corejs3": 7.22.15
|
||||
"@pmmmwh/react-refresh-webpack-plugin": 0.5.10
|
||||
"@redwoodjs/cli": 6.3.2
|
||||
"@redwoodjs/eslint-config": 6.3.2
|
||||
"@redwoodjs/internal": 6.3.2
|
||||
"@redwoodjs/project-config": 6.3.2
|
||||
"@redwoodjs/testing": 6.3.2
|
||||
"@redwoodjs/web-server": 6.3.2
|
||||
"@redwoodjs/cli": 6.3.3
|
||||
"@redwoodjs/eslint-config": 6.3.3
|
||||
"@redwoodjs/internal": 6.3.3
|
||||
"@redwoodjs/project-config": 6.3.3
|
||||
"@redwoodjs/testing": 6.3.3
|
||||
"@redwoodjs/web-server": 6.3.3
|
||||
babel-loader: ^9.1.3
|
||||
babel-timing: 0.9.1
|
||||
copy-webpack-plugin: 11.0.0
|
||||
@ -4476,20 +4472,20 @@ __metadata:
|
||||
rw-log-formatter: dist/bins/rw-log-formatter.js
|
||||
rw-web-server: dist/bins/rw-web-server.js
|
||||
rwfw: dist/bins/rwfw.js
|
||||
checksum: adfea9cf231a089e5f2e5d3f4c4ab5fbab4e23ae6fd155f19539589b57687491295388ab849141323574c962df20bdc6d9599274bb29f724562bec2192ab3d7d
|
||||
checksum: f588e8e03ebd11c8f3bbf741455977ae1086a38d596486aa8ea3a9cacb86379cc1956f7e4e65277872c901cb982851efa01c1bd7962198dfa701b479ac84a132
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@redwoodjs/eslint-config@npm:6.3.2":
|
||||
version: 6.3.2
|
||||
resolution: "@redwoodjs/eslint-config@npm:6.3.2"
|
||||
"@redwoodjs/eslint-config@npm:6.3.3":
|
||||
version: 6.3.3
|
||||
resolution: "@redwoodjs/eslint-config@npm:6.3.3"
|
||||
dependencies:
|
||||
"@babel/core": ^7.22.20
|
||||
"@babel/eslint-parser": 7.22.15
|
||||
"@babel/eslint-plugin": 7.22.10
|
||||
"@redwoodjs/eslint-plugin": 6.3.2
|
||||
"@redwoodjs/internal": 6.3.2
|
||||
"@redwoodjs/project-config": 6.3.2
|
||||
"@redwoodjs/eslint-plugin": 6.3.3
|
||||
"@redwoodjs/internal": 6.3.3
|
||||
"@redwoodjs/project-config": 6.3.3
|
||||
"@typescript-eslint/eslint-plugin": 5.61.0
|
||||
"@typescript-eslint/parser": 5.61.0
|
||||
eslint: 8.46.0
|
||||
@ -4503,42 +4499,42 @@ __metadata:
|
||||
eslint-plugin-react: 7.32.2
|
||||
eslint-plugin-react-hooks: 4.6.0
|
||||
prettier: 2.8.8
|
||||
checksum: 7bcc93633435286bf1d85a4003ad4ac7579adb29e99e0de19304a329b3c8bf35d1cb0434abac8639f33d237683c53e78defbee8ceb5612836956f9b6919bc47c
|
||||
checksum: 2eba9b3bca6f4055efe39d4d6ea02c9d998795068ae99c229d67a39762b73216bf8f74108c6d59fa0f89f79b6006d92872e10f21da4b574dce28409ba828c999
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@redwoodjs/eslint-plugin@npm:6.3.2":
|
||||
version: 6.3.2
|
||||
resolution: "@redwoodjs/eslint-plugin@npm:6.3.2"
|
||||
"@redwoodjs/eslint-plugin@npm:6.3.3":
|
||||
version: 6.3.3
|
||||
resolution: "@redwoodjs/eslint-plugin@npm:6.3.3"
|
||||
dependencies:
|
||||
"@typescript-eslint/utils": 5.61.0
|
||||
eslint: 8.46.0
|
||||
checksum: e327f70873b7fc97d03b5ab7b29d0103d16468d88329d5f958f77f48c03b8de9720873e8140cbda1cffe0ad3b9cf3a945dc9d8bc3dd78435980278b30dbb36b1
|
||||
checksum: 9ca180c3e9449a2c261367b8b57d09150e1e0d0a8c2fb644201534fe008bb8b48c6742949ec0d0bb9c61e674101d98f34f7475a5a6d9b2b6525ad056123028a7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@redwoodjs/fastify@npm:6.3.2":
|
||||
version: 6.3.2
|
||||
resolution: "@redwoodjs/fastify@npm:6.3.2"
|
||||
"@redwoodjs/fastify@npm:6.3.3":
|
||||
version: 6.3.3
|
||||
resolution: "@redwoodjs/fastify@npm:6.3.3"
|
||||
dependencies:
|
||||
"@fastify/http-proxy": 9.2.1
|
||||
"@fastify/static": 6.11.1
|
||||
"@fastify/url-data": 5.3.1
|
||||
"@redwoodjs/graphql-server": 6.3.2
|
||||
"@redwoodjs/project-config": 6.3.2
|
||||
"@redwoodjs/graphql-server": 6.3.3
|
||||
"@redwoodjs/project-config": 6.3.3
|
||||
ansi-colors: 4.1.3
|
||||
fast-glob: 3.3.1
|
||||
fastify: 4.23.2
|
||||
fastify-raw-body: 4.2.2
|
||||
lodash: 4.17.21
|
||||
qs: 6.11.2
|
||||
checksum: 8fe43bd01825fdb10f5f3f32a8d63799fd1648035beb54142e94a58e490b822da6987d2bbd6b387192338e5ca7a39877b0da509274723ca1cb0c4c099865cf90
|
||||
checksum: 1d5f6ea134e18ebf6dbd1f0667b500b98215d956a65db84788916d43333a9ea8ae81ba77a55a15bb16b311f8f1b8ec14f85d1f79a7b383a3e89df5de3f328b47
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@redwoodjs/forms@npm:6.3.2":
|
||||
version: 6.3.2
|
||||
resolution: "@redwoodjs/forms@npm:6.3.2"
|
||||
"@redwoodjs/forms@npm:6.3.3":
|
||||
version: 6.3.3
|
||||
resolution: "@redwoodjs/forms@npm:6.3.3"
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3": 7.22.15
|
||||
core-js: 3.32.2
|
||||
@ -4547,13 +4543,13 @@ __metadata:
|
||||
peerDependencies:
|
||||
graphql: 16.8.1
|
||||
react: 18.2.0
|
||||
checksum: 11714ddd94fb0e72b56831d986146d7603cff9e641fcca07a7722990556bc67133bb13c6be48b053cea79799a668e64bb0c0e815e55dad5a3441cae3ddcd049e
|
||||
checksum: 8d0d17a557f08e7a02307b90668dfd18c4ca5b0c73ae459263c7aaf5511a83329a2c1a103b6182c6d7894dff6ba18b2305c0a6b299aa4f3d60dedb25c3d68434
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@redwoodjs/graphql-server@npm:6.3.2":
|
||||
version: 6.3.2
|
||||
resolution: "@redwoodjs/graphql-server@npm:6.3.2"
|
||||
"@redwoodjs/graphql-server@npm:6.3.3":
|
||||
version: 6.3.3
|
||||
resolution: "@redwoodjs/graphql-server@npm:6.3.3"
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3": 7.22.15
|
||||
"@envelop/core": 4.0.0
|
||||
@ -4566,7 +4562,7 @@ __metadata:
|
||||
"@graphql-tools/schema": 10.0.0
|
||||
"@graphql-tools/utils": 10.0.1
|
||||
"@opentelemetry/api": 1.4.1
|
||||
"@redwoodjs/api": 6.3.2
|
||||
"@redwoodjs/api": 6.3.3
|
||||
core-js: 3.32.2
|
||||
graphql: 16.8.1
|
||||
graphql-scalars: 1.22.2
|
||||
@ -4574,13 +4570,13 @@ __metadata:
|
||||
graphql-yoga: 4.0.2
|
||||
lodash: 4.17.21
|
||||
uuid: 9.0.0
|
||||
checksum: b9df83d7e7be03ecdca51075eb959c269bd529a9bf1897f52765bab886a754cea72955f35b21063b82dd8f9b281d365f59a14da9d7c98590a8e0b86fa89ad641
|
||||
checksum: f66875bd697f272bc950320c321a1edfb176080972a251070c577834bc9095d2ff569a0100330844847db7a7e2a7856bd15aab58779285a50a73f659b5012e69
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@redwoodjs/internal@npm:6.3.2":
|
||||
version: 6.3.2
|
||||
resolution: "@redwoodjs/internal@npm:6.3.2"
|
||||
"@redwoodjs/internal@npm:6.3.3":
|
||||
version: 6.3.3
|
||||
resolution: "@redwoodjs/internal@npm:6.3.3"
|
||||
dependencies:
|
||||
"@babel/parser": ^7.22.16
|
||||
"@babel/plugin-transform-react-jsx": ^7.22.15
|
||||
@ -4595,9 +4591,9 @@ __metadata:
|
||||
"@graphql-codegen/typescript-operations": 3.0.4
|
||||
"@graphql-codegen/typescript-react-apollo": 3.3.7
|
||||
"@graphql-codegen/typescript-resolvers": 3.2.1
|
||||
"@redwoodjs/babel-config": 6.3.2
|
||||
"@redwoodjs/graphql-server": 6.3.2
|
||||
"@redwoodjs/project-config": 6.3.2
|
||||
"@redwoodjs/babel-config": 6.3.3
|
||||
"@redwoodjs/graphql-server": 6.3.3
|
||||
"@redwoodjs/project-config": 6.3.3
|
||||
"@sdl-codegen/node": 0.0.10
|
||||
chalk: 4.1.2
|
||||
core-js: 3.32.2
|
||||
@ -4617,21 +4613,21 @@ __metadata:
|
||||
bin:
|
||||
rw-gen: dist/generate/generate.js
|
||||
rw-gen-watch: dist/generate/watch.js
|
||||
checksum: bbac68265326ec2796ef679378e14757ba94451374a27df008db8716fa777f14289fd98c3897ffe11104736ec71d3848cf55901b5bdf10602d49f21649814d55
|
||||
checksum: 22c8aeedb13e6dd49d463cbe7645fbff13eb19cf1d75a8b7a6d6a25ffce1a20de530eb8e71a68821a15aa562889835cab4137995c0ff65bdf89f3ae946a5a134
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@redwoodjs/prerender@npm:6.3.2":
|
||||
version: 6.3.2
|
||||
resolution: "@redwoodjs/prerender@npm:6.3.2"
|
||||
"@redwoodjs/prerender@npm:6.3.3":
|
||||
version: 6.3.3
|
||||
resolution: "@redwoodjs/prerender@npm:6.3.3"
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3": 7.22.15
|
||||
"@redwoodjs/auth": 6.3.2
|
||||
"@redwoodjs/internal": 6.3.2
|
||||
"@redwoodjs/project-config": 6.3.2
|
||||
"@redwoodjs/router": 6.3.2
|
||||
"@redwoodjs/structure": 6.3.2
|
||||
"@redwoodjs/web": 6.3.2
|
||||
"@redwoodjs/auth": 6.3.3
|
||||
"@redwoodjs/internal": 6.3.3
|
||||
"@redwoodjs/project-config": 6.3.3
|
||||
"@redwoodjs/router": 6.3.3
|
||||
"@redwoodjs/structure": 6.3.3
|
||||
"@redwoodjs/web": 6.3.3
|
||||
"@whatwg-node/fetch": 0.9.9
|
||||
babel-plugin-ignore-html-and-css-imports: 0.1.0
|
||||
cheerio: 1.0.0-rc.12
|
||||
@ -4641,45 +4637,45 @@ __metadata:
|
||||
peerDependencies:
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0
|
||||
checksum: a087cebf9ffec00976497eda2692893889764325d612424efed91baa4b283119ebff3bdb70c548b218f15d5b580f18263810ebb48930bbda6272f10c89e95b4f
|
||||
checksum: a9db268cf8ab3893744b45931f092ee77cfff9db1f8932d52d90a7a7f5febe880b4e3bee45eea73a632e79071646a2369b96434c314310bfb9d2a622193af361
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@redwoodjs/project-config@npm:6.3.2":
|
||||
version: 6.3.2
|
||||
resolution: "@redwoodjs/project-config@npm:6.3.2"
|
||||
"@redwoodjs/project-config@npm:6.3.3":
|
||||
version: 6.3.3
|
||||
resolution: "@redwoodjs/project-config@npm:6.3.3"
|
||||
dependencies:
|
||||
"@iarna/toml": 2.2.5
|
||||
deepmerge: 4.3.1
|
||||
fast-glob: 3.3.1
|
||||
string-env-interpolation: 1.0.1
|
||||
checksum: 4f0c28d1a5273c9ca8d5bfe27644d238f73bf608ac14b55153a8e83ec63e228721e9590f01492b22dd98f8b7c0146daf1dc486865d7227171611ccf7899eb237
|
||||
checksum: d009cddcd953a40dfbb04ea2fd32831cde17177ef0031f12a67bb407115c1dd50a738e4da2aa143479042dea9d77262b5c8f44c0b4f65db6b1afdb5de80c1543
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@redwoodjs/router@npm:6.3.2":
|
||||
version: 6.3.2
|
||||
resolution: "@redwoodjs/router@npm:6.3.2"
|
||||
"@redwoodjs/router@npm:6.3.3":
|
||||
version: 6.3.3
|
||||
resolution: "@redwoodjs/router@npm:6.3.3"
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3": 7.22.15
|
||||
"@reach/skip-nav": 0.18.0
|
||||
"@redwoodjs/auth": 6.3.2
|
||||
"@redwoodjs/auth": 6.3.3
|
||||
core-js: 3.32.2
|
||||
peerDependencies:
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0
|
||||
checksum: 9d41bbe07b2286e10bc37e11987b4ff70e3f50874b68860c54676a21fd64952a04e65083e0d12f497af783f077491cd4925855ba68fe1fc76d39d777baeeb76a
|
||||
checksum: 292bade4975ee90344106997d24ad1f18e8499c8bd95ee60b9e33cd6bff3aeadcf5c46b48923b9d0154f693ea040665d45f7339ca2f191c9ef739d5e664876b2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@redwoodjs/structure@npm:6.3.2":
|
||||
version: 6.3.2
|
||||
resolution: "@redwoodjs/structure@npm:6.3.2"
|
||||
"@redwoodjs/structure@npm:6.3.3":
|
||||
version: 6.3.3
|
||||
resolution: "@redwoodjs/structure@npm:6.3.3"
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3": 7.22.15
|
||||
"@iarna/toml": 2.2.5
|
||||
"@prisma/internals": 5.3.1
|
||||
"@redwoodjs/project-config": 6.3.2
|
||||
"@redwoodjs/project-config": 6.3.3
|
||||
"@types/line-column": 1.0.0
|
||||
camelcase: 6.3.0
|
||||
core-js: 3.32.2
|
||||
@ -4699,17 +4695,17 @@ __metadata:
|
||||
vscode-languageserver-textdocument: 1.0.8
|
||||
vscode-languageserver-types: 3.17.3
|
||||
yargs-parser: 21.1.1
|
||||
checksum: ee186e5b099f479b803ed7d415a27b98763ed8b3382e167208bdceb31aa47594c738e60dd2a305ef954c1edb36f6764ea0cc0714cdd84ccb0d54bd9d4bdf7057
|
||||
checksum: 7b301838c7fe882d3db27083bb4b99d5dcf19635d3b8cc0d4c11667329a2255973e53262efced4979016c5931e7aece923bd85d8303b3772af0c002e20144ee2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@redwoodjs/telemetry@npm:6.3.2":
|
||||
version: 6.3.2
|
||||
resolution: "@redwoodjs/telemetry@npm:6.3.2"
|
||||
"@redwoodjs/telemetry@npm:6.3.3":
|
||||
version: 6.3.3
|
||||
resolution: "@redwoodjs/telemetry@npm:6.3.3"
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3": 7.22.15
|
||||
"@redwoodjs/project-config": 6.3.2
|
||||
"@redwoodjs/structure": 6.3.2
|
||||
"@redwoodjs/project-config": 6.3.3
|
||||
"@redwoodjs/structure": 6.3.3
|
||||
"@whatwg-node/fetch": 0.9.9
|
||||
ci-info: 3.8.0
|
||||
core-js: 3.32.2
|
||||
@ -4717,21 +4713,21 @@ __metadata:
|
||||
systeminformation: 5.21.7
|
||||
uuid: 9.0.0
|
||||
yargs: 17.7.2
|
||||
checksum: 6d041d6c745e74cdd60c266ffbd8d3bb4e767b03999e5464ca5a8b9cb7c125d143f589f04f780732f43e151bd08dabe4801847d37b063cee616f7750c17cfc35
|
||||
checksum: 1034240860fcc333bae802a54ca096411820202812664a21bc9bb4a362567a98f09ffa40adbff339444fc53673fb085a860b3f2d05a731ef73a6a41ca04b5c85
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@redwoodjs/testing@npm:6.3.2":
|
||||
version: 6.3.2
|
||||
resolution: "@redwoodjs/testing@npm:6.3.2"
|
||||
"@redwoodjs/testing@npm:6.3.3":
|
||||
version: 6.3.3
|
||||
resolution: "@redwoodjs/testing@npm:6.3.3"
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3": 7.22.15
|
||||
"@redwoodjs/auth": 6.3.2
|
||||
"@redwoodjs/babel-config": 6.3.2
|
||||
"@redwoodjs/graphql-server": 6.3.2
|
||||
"@redwoodjs/project-config": 6.3.2
|
||||
"@redwoodjs/router": 6.3.2
|
||||
"@redwoodjs/web": 6.3.2
|
||||
"@redwoodjs/auth": 6.3.3
|
||||
"@redwoodjs/babel-config": 6.3.3
|
||||
"@redwoodjs/graphql-server": 6.3.3
|
||||
"@redwoodjs/project-config": 6.3.3
|
||||
"@redwoodjs/router": 6.3.3
|
||||
"@redwoodjs/web": 6.3.3
|
||||
"@testing-library/jest-dom": 5.16.5
|
||||
"@testing-library/react": 14.0.0
|
||||
"@testing-library/user-event": 14.4.3
|
||||
@ -4752,17 +4748,17 @@ __metadata:
|
||||
msw: 1.3.1
|
||||
ts-toolbelt: 9.6.0
|
||||
whatwg-fetch: 3.6.17
|
||||
checksum: 32be1bebf976197707654ccadf8746f47dcf53147fac79d6475ebc6f9b8709b3171237c47685e839dc30b500f8eb1286d8abb9d3c8a8215598e8fdaff7ebb16c
|
||||
checksum: 54cf5937c55526f5c89602af77e6ed945f8f24396735c007f21e36c21fc6001bb000e86556fa0e9a8cab527eb630bb24531b086284b74a90fb6df2052a2a9ba0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@redwoodjs/vite@npm:6.3.2":
|
||||
version: 6.3.2
|
||||
resolution: "@redwoodjs/vite@npm:6.3.2"
|
||||
"@redwoodjs/vite@npm:6.3.3":
|
||||
version: 6.3.3
|
||||
resolution: "@redwoodjs/vite@npm:6.3.3"
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3": 7.22.15
|
||||
"@redwoodjs/internal": 6.3.2
|
||||
"@redwoodjs/project-config": 6.3.2
|
||||
"@redwoodjs/internal": 6.3.3
|
||||
"@redwoodjs/project-config": 6.3.3
|
||||
"@vitejs/plugin-react": 4.0.4
|
||||
buffer: 6.0.3
|
||||
core-js: 3.32.2
|
||||
@ -4772,15 +4768,15 @@ __metadata:
|
||||
rw-vite-build: bins/rw-vite-build.mjs
|
||||
rw-vite-dev: bins/rw-vite-dev.mjs
|
||||
vite: bins/vite.mjs
|
||||
checksum: 229bdf774e4639f2fc0a967c1becae60c7efa4c8fea250e95d74c2b17ed0357d51e17a08d20ced6a372de8835db6ec57024ce8403eee1d360e1f3f306ab78c81
|
||||
checksum: 692fd73f60b7c9c07f680d3f95f6d6cd23dc46ac03f47af79758b0f35a9295e82266851b9a6456007e49452e3f1c4b94574c2a8c51c0ba89faec76f3f31874d0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@redwoodjs/web-server@npm:6.3.2":
|
||||
version: 6.3.2
|
||||
resolution: "@redwoodjs/web-server@npm:6.3.2"
|
||||
"@redwoodjs/web-server@npm:6.3.3":
|
||||
version: 6.3.3
|
||||
resolution: "@redwoodjs/web-server@npm:6.3.3"
|
||||
dependencies:
|
||||
"@redwoodjs/project-config": 6.3.2
|
||||
"@redwoodjs/project-config": 6.3.3
|
||||
chalk: 4.1.2
|
||||
dotenv-defaults: 5.0.2
|
||||
fast-glob: 3.3.1
|
||||
@ -4788,17 +4784,17 @@ __metadata:
|
||||
yargs-parser: 21.1.1
|
||||
bin:
|
||||
rw-web-server: dist/server.js
|
||||
checksum: 21628853111f904ce065677b621282d50edcec1e60cd59a2a5dc0b0424e28631f463581aa274a9b73df2d3c1c933d2f092ad2eb83fb81e4aed61fb50d0a2f865
|
||||
checksum: 5bf07ced27c7a947eec62f63736f877c8f85f8b8b2ebc43195206f658faa5184e658d40e33f877e2c56d17b502b969c8a6773dc64ef900248debde43dc9efcfc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@redwoodjs/web@npm:6.3.2":
|
||||
version: 6.3.2
|
||||
resolution: "@redwoodjs/web@npm:6.3.2"
|
||||
"@redwoodjs/web@npm:6.3.3":
|
||||
version: 6.3.3
|
||||
resolution: "@redwoodjs/web@npm:6.3.3"
|
||||
dependencies:
|
||||
"@apollo/client": 3.8.4
|
||||
"@babel/runtime-corejs3": 7.22.15
|
||||
"@redwoodjs/auth": 6.3.2
|
||||
"@redwoodjs/auth": 6.3.3
|
||||
core-js: 3.32.2
|
||||
graphql: 16.8.1
|
||||
graphql-tag: 2.12.6
|
||||
@ -4819,7 +4815,7 @@ __metadata:
|
||||
storybook: dist/bins/storybook.js
|
||||
tsc: dist/bins/tsc.js
|
||||
webpack: dist/bins/webpack.js
|
||||
checksum: c676d9edd7496ddb0ca101c0c8064fdafe1f50f640cbc31ff27ebb77875175b3bc7f8bf0ca77f0102927dd5e68b5ab5d6a789f9c988a117e3152c4f0cdebae96
|
||||
checksum: ff69b49ea1faa5229d22b968f960ad184b195b4d2653ba335b943ef8a002202b4c826519ebd8fbe6dd0c67459df7d5f6e7c0c542cd6b40231d8798086d4f669f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -5617,13 +5613,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/secure-random-password@npm:0.2.1":
|
||||
version: 0.2.1
|
||||
resolution: "@types/secure-random-password@npm:0.2.1"
|
||||
checksum: 87f0528b7ccb907706b0cc77160c6771279508de3852213f2c4f28a83af482b016cf3e0b414f62d2e8b3713f1733ad787e3bd40c201463ac822c117856a7886a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/semver@npm:^7.3.12":
|
||||
version: 7.5.3
|
||||
resolution: "@types/semver@npm:7.5.3"
|
||||
@ -6739,9 +6728,9 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "api@workspace:api"
|
||||
dependencies:
|
||||
"@redwoodjs/api": 6.3.2
|
||||
"@redwoodjs/auth-dbauth-api": 6.3.2
|
||||
"@redwoodjs/graphql-server": 6.3.2
|
||||
"@redwoodjs/api": 6.3.3
|
||||
"@redwoodjs/auth-dbauth-api": 6.3.3
|
||||
"@redwoodjs/graphql-server": 6.3.3
|
||||
filestack-js: ^3.27.0
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
@ -9049,13 +9038,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"crypto-js@npm:4.1.1":
|
||||
version: 4.1.1
|
||||
resolution: "crypto-js@npm:4.1.1"
|
||||
checksum: 50cc66a35f2738171d9a6d80c85ba7d00cb6440b756db035ba9ccd03032c0a803029a62969ecd4c844106c980af87687c64b204dd967989379c4f354fb482d37
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"crypto-random-string@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "crypto-random-string@npm:1.0.0"
|
||||
@ -9365,6 +9347,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dayjs@npm:^1.11.10":
|
||||
version: 1.11.10
|
||||
resolution: "dayjs@npm:1.11.10"
|
||||
checksum: 4de9af50639d47df87f2e15fa36bb07e0f9ed1e9c52c6caa1482788ee9a384d668f1dbd00c54f82aaab163db07d61d2899384b8254da3a9184fc6deca080e2fe
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"debounce@npm:^1.2.0":
|
||||
version: 1.2.1
|
||||
resolution: "debounce@npm:1.2.1"
|
||||
@ -18237,7 +18226,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "root-workspace-0b6124@workspace:."
|
||||
dependencies:
|
||||
"@redwoodjs/core": 6.3.2
|
||||
"@redwoodjs/core": 6.3.3
|
||||
prettier-plugin-tailwindcss: 0.4.1
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
@ -18422,22 +18411,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"secure-random-password@npm:0.2.3":
|
||||
version: 0.2.3
|
||||
resolution: "secure-random-password@npm:0.2.3"
|
||||
dependencies:
|
||||
secure-random: ^1.1.2
|
||||
checksum: ced04529b96724b921b82b7527e1ba2f1299c3f327f941076e9edaea66445118b67a6b7f6edfe3b705a240e595de49144b2b796116c994cb52eb52efdd3d51b5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"secure-random@npm:^1.1.2":
|
||||
version: 1.1.2
|
||||
resolution: "secure-random@npm:1.1.2"
|
||||
checksum: 612934cd5b1ea217d5e248a16ff2752411474997ede1f460ff37fe3214eedfd66ef6a5936ff76b3a5df3d057a8d2d4ed48298f5500bf837beb911522caac7f5c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"select-hose@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "select-hose@npm:2.0.0"
|
||||
@ -20936,17 +20909,18 @@ __metadata:
|
||||
dependencies:
|
||||
"@mdi/js": ^7.3.67
|
||||
"@mdi/react": ^1.6.1
|
||||
"@redwoodjs/auth-dbauth-web": 6.3.2
|
||||
"@redwoodjs/forms": 6.3.2
|
||||
"@redwoodjs/router": 6.3.2
|
||||
"@redwoodjs/vite": 6.3.2
|
||||
"@redwoodjs/web": 6.3.2
|
||||
"@redwoodjs/auth-dbauth-web": 6.3.3
|
||||
"@redwoodjs/forms": 6.3.3
|
||||
"@redwoodjs/router": 6.3.3
|
||||
"@redwoodjs/vite": 6.3.3
|
||||
"@redwoodjs/web": 6.3.3
|
||||
"@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
|
||||
dayjs: ^1.11.10
|
||||
filestack-react: ^4.0.1
|
||||
humanize-string: 2.1.0
|
||||
postcss: ^8.4.31
|
||||
|
Reference in New Issue
Block a user