issue-159 Add delete button to part profile (for part owner)

schema had to be update to add a deleted boolean to part.
Easier than setting up cascading deletes for comments and reactions.
resolves #159
This commit is contained in:
Kurt Hutten
2020-12-13 12:25:54 +11:00
parent 6b97307c3f
commit 2b763f23d8
14 changed files with 326 additions and 21 deletions

View File

@@ -0,0 +1,63 @@
# Migration `20201213004819-add-delete-on-part`
This migration has been generated by Kurt Hutten at 12/13/2020, 11:48:20 AM.
You can check out the [state of the schema](./schema.prisma) after the migration.
## Database Steps
```sql
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Part" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
"code" TEXT,
"mainImage" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"userId" TEXT NOT NULL,
"deleted" BOOLEAN NOT NULL DEFAULT false,
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY ("id")
);
INSERT INTO "new_Part" ("id", "title", "description", "code", "mainImage", "createdAt", "updatedAt", "userId") SELECT "id", "title", "description", "code", "mainImage", "createdAt", "updatedAt", "userId" FROM "Part";
DROP TABLE "Part";
ALTER TABLE "new_Part" RENAME TO "Part";
CREATE UNIQUE INDEX "Part.title_userId_unique" ON "Part"("title", "userId");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON
```
## Changes
```diff
diff --git schema.prisma schema.prisma
migration 20201105184423-add-name-to-user..20201213004819-add-delete-on-part
--- datamodel.dml
+++ datamodel.dml
@@ -1,12 +1,12 @@
datasource DS {
provider = ["sqlite", "postgresql"]
- url = "***"
+ url = "***"
}
generator client {
provider = "prisma-client-js"
- binaryTargets = "native"
+ binaryTargets = ["native", "rhel-openssl-1.0.x"]
}
// sqlLight does not suport enums so we can't use enums until we set up postgresql in dev mode
// enum Role {
@@ -47,8 +47,9 @@
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
userId String
+ deleted Boolean @default(false)
Comment Comment[]
Reaction PartReaction[]
@@unique([title, userId])
```

View File

@@ -0,0 +1,81 @@
datasource DS {
provider = ["sqlite", "postgresql"]
url = "***"
}
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "rhel-openssl-1.0.x"]
}
// sqlLight does not suport enums so we can't use enums until we set up postgresql in dev mode
// enum Role {
// USER
// ADMIN
// }
// enum PartType {
// CASCADESTUDIO
// JSCAD
// }
model User {
id String @id @default(uuid())
userName String @unique // reffered to as userId in @relations
email String @unique
name String?
// role should probably be a list [] and also use enums, neither are supported by sqllight, so we need to set up postgresql in dev
// maybe let netlify handle roles for now.
// role String @default("user")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
image String? // url maybe id or file storage service? cloudinary?
bio String? //mark down
Part Part[]
Reaction PartReaction[]
Comment Comment[]
}
model Part {
id String @id @default(uuid())
title String
description String? // markdown string
code String?
mainImage String? // link to cloudinary
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
userId String
deleted Boolean @default(false)
Comment Comment[]
Reaction PartReaction[]
@@unique([title, userId])
}
model PartReaction {
id String @id @default(uuid())
emote String // an emoji
user User @relation(fields: [userId], references: [id])
userId String
part Part @relation(fields: [partId], references: [id])
partId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([emote, userId, partId])
}
model Comment {
id String @id @default(uuid())
text String // the comment, should I allow mark down?
user User @relation(fields: [userId], references: [id])
userId String
part Part @relation(fields: [partId], references: [id])
partId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

View File

@@ -0,0 +1,37 @@
{
"version": "0.3.14-fixed",
"steps": [
{
"tag": "CreateField",
"model": "Part",
"field": "deleted",
"type": "Boolean",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Part",
"field": "deleted"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Part",
"field": "deleted"
},
"directive": "default"
},
"argument": "",
"value": "false"
}
]
}

View File

@@ -1,4 +1,5 @@
# Prisma Migrate lockfile v1
20201101183848-db-init
20201105184423-add-name-to-user
20201105184423-add-name-to-user
20201213004819-add-delete-on-part

View File

@@ -48,6 +48,7 @@ model Part {
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
userId String
deleted Boolean @default(false)
Comment Comment[]
Reaction PartReaction[]

View File

@@ -8,7 +8,7 @@ import { requireAuth } from 'src/lib/auth'
import { requireOwnership } from 'src/lib/owner'
export const parts = () => {
return db.part.findMany()
return db.part.findMany({ where: { deleted: false } })
}
export const part = ({ id }) => {
@@ -70,9 +70,13 @@ export const updatePart = async ({ id, input }) => {
})
}
export const deletePart = ({ id }) => {
export const deletePart = async ({ id }) => {
requireAuth()
return db.part.delete({
await requireOwnership({ partId: id })
return db.part.update({
data: {
deleted: true,
},
where: { id },
})
}

View File

@@ -8,18 +8,23 @@ const Button = ({
className,
shouldAnimateHover,
disabled,
type,
}) => {
return (
<button
disabled={disabled}
className={getActiveClasses(
'flex items-center bg-opacity-50 rounded-xl p-2 px-6 text-indigo-600',
{
'bg-gray-300 shadow-none hover:shadow-none': disabled,
'text-red-600 bg-red-200 border border-red-600': type === 'danger',
'text-indigo-600': !type,
},
'flex items-center bg-opacity-50 rounded-xl p-2 px-6',
{
'mx-px transform hover:-translate-y-px transition-all duration-150':
shouldAnimateHover && !disabled,
},
className,
{ 'bg-gray-300 shadow-none hover:shadow-none': disabled }
className
)}
onClick={onClick}
>

View File

@@ -0,0 +1,38 @@
import Dialog from '@material-ui/core/Dialog'
import Button from 'src/components/Button'
const ConfirmDialog = ({ open, onClose, message, onConfirm }) => {
return (
<Dialog open={open} onClose={onClose}>
<div className="bg-gray-100 max-w-3xl rounded-lg shadow-lg">
<div className="p-4">
<span className="text-gray-600 text-center">{message}</span>
<div className="flex gap-4">
<Button
className="mt-4 ml-auto shadow-md hover:shadow-lg bg-indigo-200 relative z-20"
shouldAnimateHover
iconName={'save'}
onClick={onClose}
>
Don't delete
</Button>
<Button
className="mt-4 ml-auto shadow-md hover:shadow-lg bg-red-200 relative z-20"
shouldAnimateHover
iconName={'trash'}
onClick={() => {
onClose()
onConfirm()
}}
type="danger"
>
Yes, Delete
</Button>
</div>
</div>
</div>
</Dialog>
)
}
export default ConfirmDialog

View File

@@ -0,0 +1,7 @@
import ConfirmDialog from './ConfirmDialog'
export const generated = () => {
return <ConfirmDialog />
}
export default { title: 'Components/ConfirmDialog' }

View File

@@ -0,0 +1,11 @@
import { render } from '@redwoodjs/testing'
import ConfirmDialog from './ConfirmDialog'
describe('ConfirmDialog', () => {
it('renders successfully', () => {
expect(() => {
render(<ConfirmDialog />)
}).not.toThrow()
})
})

View File

@@ -84,6 +84,18 @@ const CREATE_COMMENT_MUTATION = gql`
}
}
`
const DELETE_PART_MUTATION = gql`
mutation DeletePartMutation($id: String!) {
deletePart(id: $id) {
id
title
user {
id
userName
}
}
}
`
export const Loading = () => <div>Loading...</div>
@@ -123,6 +135,16 @@ export const Success = ({ userPart, variables: { isEditable }, refetch }) => {
}
updateUser({ variables: { id, input } })
}
const [deletePart] = useMutation(DELETE_PART_MUTATION, {
onCompleted: ({ deletePart }) => {
navigate(routes.home())
addMessage('Part deleted.', { classes: 'rw-flash-success' })
},
})
const onDelete = () => {
userPart?.Part?.id && deletePart({ variables: { id: userPart?.Part?.id } })
}
const [toggleReaction] = useMutation(TOGGLE_REACTION_MUTATION, {
onCompleted: () => refetch(),
@@ -156,6 +178,7 @@ export const Success = ({ userPart, variables: { isEditable }, refetch }) => {
<PartProfile
userPart={userPart}
onSave={onSave}
onDelete={onDelete}
loading={loading}
error={error}
isEditable={isEditable}

View File

@@ -4,6 +4,7 @@ import { Link, navigate, routes } from '@redwoodjs/router'
import Editor from 'rich-markdown-editor'
import ImageUploader from 'src/components/ImageUploader'
import ConfirmDialog from 'src/components/ConfirmDialog'
import Breadcrumb from 'src/components/Breadcrumb'
import EmojiReaction from 'src/components/EmojiReaction'
import Button from 'src/components/Button'
@@ -14,14 +15,15 @@ const PartProfile = ({
userPart,
isEditable,
onSave,
onDelete,
onReaction,
onComment,
}) => {
const [comment, setComment] = useState('')
const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false)
const { currentUser } = useAuth()
const canEdit = currentUser?.sub === userPart.id
const part = userPart?.Part
console.log(part)
const emotes = countEmotes(part?.Reaction)
const userEmotes = part?.userReactions.map(({ emote }) => emote)
useEffect(() => {
@@ -100,7 +102,7 @@ const PartProfile = ({
})}
>
<Button
className="mt-4 ml-auto shadow-md hover:shadow-lg bg-indigo-200"
className="mt-4 ml-auto shadow-md hover:shadow-lg bg-indigo-200 w-full justify-end"
shouldAnimateHover
iconName="terminal"
onClick={() => {}}
@@ -109,15 +111,27 @@ const PartProfile = ({
</Button>
</Link>
{canEdit && (
<Button
className="mt-4 ml-auto shadow-md hover:shadow-lg bg-indigo-200 relative z-20"
shouldAnimateHover
iconName={isEditable ? 'save' : 'pencil'}
onClick={onEditSaveClick}
>
{isEditable ? 'Save Details' : 'Edit Details'}
</Button>
<>
<Button
className="mt-4 ml-auto shadow-md hover:shadow-lg bg-indigo-200 relative z-20 w-full justify-end"
shouldAnimateHover
iconName={isEditable ? 'save' : 'pencil'}
onClick={onEditSaveClick}
>
{isEditable ? 'Save Details' : 'Edit Details'}
</Button>
<Button
className="mt-4 ml-auto shadow-md hover:shadow-lg bg-red-200 relative z-20 w-full justify-end"
shouldAnimateHover
iconName={'trash'}
onClick={() => setIsConfirmDialogOpen(true)}
type="danger"
>
Delete
</Button>
</>
)}
{/* gray overlay */}
{isEditable && (
<div className="absolute inset-0 bg-gray-300 opacity-75 z-10 transform scale-x-110 -ml-1 -mt-2" />
)}
@@ -216,6 +230,12 @@ const PartProfile = ({
)}
</section>
</div>
<ConfirmDialog
open={isConfirmDialogOpen}
onClose={() => setIsConfirmDialogOpen(false)}
onConfirm={onDelete}
message="Are you sure you want to delete? This action cannot be undone."
/>
</>
)
}

View File

@@ -307,6 +307,21 @@ const Svg = ({ name, className: className2, strokeWidth = 2 }) => {
/>
</svg>
),
trash: (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={strokeWidth}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
),
}
return <div className={className2 || 'h-10 w-10'}>{svgs[name]}</div>

View File

@@ -91,7 +91,6 @@ const MainLayout = ({ children }) => {
useEffect(() => {
const [key, token] = hash.slice(1).split('=')
if (key === 'confirmation_token') {
console.log('confirming with', token)
client
.confirm(token, true)
.then(() => {
@@ -209,7 +208,7 @@ const MainLayout = ({ children }) => {
horizontal: 'right',
}}
>
<div className="p-4 w-40">
<div className="p-4 w-48">
<Link to={routes.user({ userName: data?.user?.userName })}>
<h3 className="text-indigo-800" style={{ fontWeight: '500' }}>
Hello {data?.user?.name}
@@ -217,8 +216,8 @@ const MainLayout = ({ children }) => {
</Link>
<hr />
<br />
<Link to={routes.editUser({ userName: data?.user?.userName })}>
<div className="text-indigo-800">Edit Profile</div>
<Link to={routes.user({ userName: data?.user?.userName })}>
<div className="text-indigo-800">Your Profile</div>
</Link>
<a href="#" className="text-indigo-800" onClick={logOut}>
Logout