Add Privacy Policy related improvements

various thing to make sure we're GDPR, et al compliant
This commit is contained in:
Kurt Hutten
2020-12-25 17:29:01 +11:00
parent 6623939f78
commit 7d262e9f58
51 changed files with 1480 additions and 95 deletions

View File

@@ -8,3 +8,7 @@ DATABASE_URL=file:./dev.db
# disables Prisma CLI update notifier
PRISMA_HIDE_UPDATE_MESSAGE=true
CLOUDINARY_API_KEY=476712943135152
# ask Kurt for help getting set up with a secret
# CLOUDINARY_API_SECRET=

View File

@@ -3,6 +3,7 @@
"version": "0.0.0",
"private": true,
"dependencies": {
"@redwoodjs/api": "^0.20.0"
"@redwoodjs/api": "^0.20.0",
"cloudinary": "^1.23.0"
}
}

View File

@@ -0,0 +1,71 @@
# Migration `20201227195638-add-subject-access-request-table`
This migration has been generated by Kurt Hutten at 12/28/2020, 6:56:38 AM.
You can check out the [state of the schema](./schema.prisma) after the migration.
## Database Steps
```sql
CREATE TABLE "SubjectAccessRequest" (
"id" TEXT NOT NULL,
"comment" TEXT NOT NULL,
"payload" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY ("id")
)
```
## Changes
```diff
diff --git schema.prisma schema.prisma
migration 20201213004819-add-delete-on-part..20201227195638-add-subject-access-request-table
--- datamodel.dml
+++ datamodel.dml
@@ -1,7 +1,7 @@
datasource DS {
provider = ["sqlite", "postgresql"]
- url = "***"
+ url = "***"
}
generator client {
provider = "prisma-client-js"
@@ -30,13 +30,14 @@
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[]
+ image String? // url maybe id or file storage service? cloudinary?
+ bio String? //mark down
+ Part Part[]
+ Reaction PartReaction[]
+ Comment Comment[]
+ SubjectAccessRequest SubjectAccessRequest[]
}
model Part {
id String @id @default(uuid())
@@ -78,4 +79,15 @@
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
+
+model SubjectAccessRequest {
+ id String @id @default(uuid())
+ comment String
+ payload String // json dump
+ user User @relation(fields: [userId], references: [id])
+ userId String
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+}
```

View File

@@ -0,0 +1,93 @@
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[]
SubjectAccessRequest SubjectAccessRequest[]
}
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
}
model SubjectAccessRequest {
id String @id @default(uuid())
comment String
payload String // json dump
user User @relation(fields: [userId], references: [id])
userId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

View File

@@ -0,0 +1,176 @@
{
"version": "0.3.14-fixed",
"steps": [
{
"tag": "CreateModel",
"model": "SubjectAccessRequest"
},
{
"tag": "CreateField",
"model": "SubjectAccessRequest",
"field": "id",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "SubjectAccessRequest",
"field": "id"
},
"directive": "id"
}
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "SubjectAccessRequest",
"field": "id"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "SubjectAccessRequest",
"field": "id"
},
"directive": "default"
},
"argument": "",
"value": "uuid()"
},
{
"tag": "CreateField",
"model": "SubjectAccessRequest",
"field": "comment",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateField",
"model": "SubjectAccessRequest",
"field": "payload",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateField",
"model": "SubjectAccessRequest",
"field": "user",
"type": "User",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "SubjectAccessRequest",
"field": "user"
},
"directive": "relation"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "SubjectAccessRequest",
"field": "user"
},
"directive": "relation"
},
"argument": "fields",
"value": "[userId]"
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "SubjectAccessRequest",
"field": "user"
},
"directive": "relation"
},
"argument": "references",
"value": "[id]"
},
{
"tag": "CreateField",
"model": "SubjectAccessRequest",
"field": "userId",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateField",
"model": "SubjectAccessRequest",
"field": "createdAt",
"type": "DateTime",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "SubjectAccessRequest",
"field": "createdAt"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "SubjectAccessRequest",
"field": "createdAt"
},
"directive": "default"
},
"argument": "",
"value": "now()"
},
{
"tag": "CreateField",
"model": "SubjectAccessRequest",
"field": "updatedAt",
"type": "DateTime",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "SubjectAccessRequest",
"field": "updatedAt"
},
"directive": "updatedAt"
}
},
{
"tag": "CreateField",
"model": "User",
"field": "SubjectAccessRequest",
"type": "SubjectAccessRequest",
"arity": "List"
}
]
}

View File

@@ -2,4 +2,5 @@
20201101183848-db-init
20201105184423-add-name-to-user
20201213004819-add-delete-on-part
20201213004819-add-delete-on-part
20201227195638-add-subject-access-request-table

View File

@@ -31,11 +31,12 @@ model 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[]
image String? // url maybe id or file storage service? cloudinary?
bio String? //mark down
Part Part[]
Reaction PartReaction[]
Comment Comment[]
SubjectAccessRequest SubjectAccessRequest[]
}
model Part {
@@ -79,3 +80,14 @@ model Comment {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model SubjectAccessRequest {
id String @id @default(uuid())
comment String
payload String // json dump
user User @relation(fields: [userId], references: [id])
userId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

View File

@@ -0,0 +1,39 @@
export const schema = gql`
type SubjectAccessRequest {
id: String!
comment: String!
payload: String!
user: User!
userId: String!
createdAt: DateTime!
updatedAt: DateTime!
}
type Query {
subjectAccessRequests: [SubjectAccessRequest!]!
subjectAccessRequest(id: String!): SubjectAccessRequest
}
input CreateSubjectAccessRequestInput {
comment: String!
payload: String!
userId: String!
}
input UpdateSubjectAccessRequestInput {
comment: String
payload: String
userId: String
}
type Mutation {
createSubjectAccessRequest(
input: CreateSubjectAccessRequestInput!
): SubjectAccessRequest!
updateSubjectAccessRequest(
id: String!
input: UpdateSubjectAccessRequestInput!
): SubjectAccessRequest!
deleteSubjectAccessRequest(id: String!): SubjectAccessRequest!
}
`

View File

@@ -12,6 +12,7 @@ export const schema = gql`
Part(partTitle: String): Part
Reaction: [PartReaction]!
Comment: [Comment]!
SubjectAccessRequest: [SubjectAccessRequest]!
}
type Query {

View File

@@ -1,3 +1,10 @@
import { v2 as cloudinary } from 'cloudinary'
cloudinary.config({
cloud_name: 'irevdev',
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
})
export const foreignKeyReplacement = (input) => {
let output = input
const foreignKeys = Object.keys(input).filter((k) => k.match(/Id$/))
@@ -28,3 +35,14 @@ export const generateUniqueString = async (
const newSeed = count === 1 ? `${seed}_${count}` : seed.slice(0, -1) + count
return generateUniqueString(newSeed, isUniqueCallback, count)
}
export const destroyImage = ({ publicId }) =>
new Promise((resolve, reject) => {
cloudinary.uploader.destroy(publicId, (error, result) => {
if (error) {
reject(error)
return
}
resolve(result)
})
})

View File

@@ -3,6 +3,7 @@ import {
foreignKeyReplacement,
enforceAlphaNumeric,
generateUniqueString,
destroyImage,
} from 'src/services/helpers'
import { requireAuth } from 'src/lib/auth'
import { requireOwnership } from 'src/lib/owner'
@@ -74,10 +75,18 @@ export const updatePart = async ({ id, input }) => {
if (input.title) {
input.title = enforceAlphaNumeric(input.title)
}
return db.part.update({
const originalPart = await db.part.findOne({ where: { id } })
const imageToDestroy =
originalPart.mainImage !== input.mainImage && originalPart.mainImage
const update = await db.part.update({
data: foreignKeyReplacement(input),
where: { id },
})
if (imageToDestroy) {
// destroy after the db has been updated
destroyImage({ publicId: imageToDestroy })
}
return update
}
export const deletePart = async ({ id }) => {

View File

@@ -0,0 +1,42 @@
import { db } from 'src/lib/db'
import { requireAuth } from 'src/lib/auth'
import { foreignKeyReplacement } from 'src/services/helpers'
export const subjectAccessRequests = () => {
requireAuth({ role: 'admin' })
return db.subjectAccessRequest.findMany()
}
export const subjectAccessRequest = ({ id }) => {
requireAuth({ role: 'admin' })
return db.subjectAccessRequest.findOne({
where: { id },
})
}
export const createSubjectAccessRequest = ({ input }) => {
requireAuth({ role: 'admin' })
return db.subjectAccessRequest.create({
data: foreignKeyReplacement(input),
})
}
export const updateSubjectAccessRequest = ({ id, input }) => {
requireAuth({ role: 'admin' })
return db.subjectAccessRequest.update({
data: foreignKeyReplacement(input),
where: { id },
})
}
export const deleteSubjectAccessRequest = ({ id }) => {
requireAuth({ role: 'admin' })
return db.subjectAccessRequest.delete({
where: { id },
})
}
export const SubjectAccessRequest = {
user: (_obj, { root }) =>
db.subjectAccessRequest.findOne({ where: { id: root.id } }).user(),
}

View File

@@ -0,0 +1,9 @@
/*
import { subjectAccessRequests } from './subjectAccessRequests'
*/
describe('subjectAccessRequests', () => {
it('returns true', () => {
expect(true).toBe(true)
})
})

View File

@@ -2,7 +2,7 @@ import { db } from 'src/lib/db'
import { requireAuth } from 'src/lib/auth'
import { requireOwnership } from 'src/lib/owner'
import { UserInputError } from '@redwoodjs/api'
import { enforceAlphaNumeric } from 'src/services/helpers'
import { enforceAlphaNumeric, destroyImage } from 'src/services/helpers'
export const users = () => {
requireAuth({ role: 'admin' })
@@ -51,10 +51,18 @@ export const updateUserByUserName = async ({ userName, input }) => {
`You've tried to used a protected word as you userName, try something other than `
)
}
return db.user.update({
const originalPart = await db.user.findOne({ where: { userName } })
const imageToDestroy =
originalPart.image !== input.image && originalPart.image
const update = await db.user.update({
data: input,
where: { userName },
})
if (imageToDestroy) {
// destroy after the db has been updated
destroyImage({ publicId: imageToDestroy })
}
return update
}
export const deleteUser = ({ id }) => {
@@ -80,4 +88,6 @@ export const User = {
db.user.findOne({ where: { id: root.id } }).Reaction(),
Comment: (_obj, { root }) =>
db.user.findOne({ where: { id: root.id } }).Comment(),
SubjectAccessRequest: (_obj, { root }) =>
db.user.findOne({ where: { id: root.id } }).SubjectAccessRequest(),
}

View File

@@ -8,7 +8,7 @@
[web]
port = 8910
apiProxyPath = "/.netlify/functions"
includeEnvironmentVariables = ['GOOGLE_ANALYTICS_ID']
includeEnvironmentVariables = ['GOOGLE_ANALYTICS_ID', 'CLOUDINARY_API_KEY', 'CLOUDINARY_API_SECRET']
experimentalFastRefresh = true
[api]
port = 8911

View File

@@ -32,6 +32,7 @@ const Routes = () => {
)
return (
<Router>
<Route path="/policies/privacy-policy" page={PrivacyPolicyPage} name="privacyPolicy" />
<Route path="/policies/code-of-conduct" page={CodeOfConductPage} name="codeOfConduct" />
<Route path="/account-recovery/update-password" page={UpdatePasswordPage} name="updatePassword" />
<Route path="/account-recovery" page={AccountRecoveryPage} name="accountRecovery" />
@@ -53,6 +54,9 @@ const Routes = () => {
<Private unauthenticated="home" role="admin">
<Route path="/admin/users" page={UsersPage} name="users" />
<Route path="/admin/parts" page={AdminPartsPage} name="parts" />
<Route path="/admin/subject-access-requests/{id}/edit" page={EditSubjectAccessRequestPage} name="editSubjectAccessRequest" />
<Route path="/admin/subject-access-requests/{id}" page={SubjectAccessRequestPage} name="subjectAccessRequest" />
<Route path="/admin/subject-access-requests" page={SubjectAccessRequestsPage} name="subjectAccessRequests" />
</Private>
</Router>
)

View File

@@ -2,7 +2,13 @@ import { getActiveClasses } from 'get-active-classes'
import InputText from 'src/components/InputText'
const Breadcrumb = ({ userName, partTitle, onPartTitleChange, className }) => {
const Breadcrumb = ({
userName,
partTitle,
onPartTitleChange,
className,
isInvalid,
}) => {
return (
<h3 className={getActiveClasses('text-2xl font-roboto', className)}>
<div className="w-1 inline-block text-indigo-800 bg-indigo-800 mr-2">
@@ -26,6 +32,7 @@ const Breadcrumb = ({ userName, partTitle, onPartTitleChange, className }) => {
className={getActiveClasses('text-indigo-800 text-2xl', {
'-ml-2': !onPartTitleChange,
})}
isInvalid={isInvalid}
/>
</h3>
)

View File

@@ -0,0 +1,70 @@
import { useMutation, useFlash } from '@redwoodjs/web'
import { navigate, routes } from '@redwoodjs/router'
import SubjectAccessRequestForm from 'src/components/SubjectAccessRequestForm'
export const QUERY = gql`
query FIND_SUBJECT_ACCESS_REQUEST_BY_ID($id: String!) {
subjectAccessRequest: subjectAccessRequest(id: $id) {
id
comment
payload
userId
createdAt
updatedAt
}
}
`
const UPDATE_SUBJECT_ACCESS_REQUEST_MUTATION = gql`
mutation UpdateSubjectAccessRequestMutation(
$id: String!
$input: UpdateSubjectAccessRequestInput!
) {
updateSubjectAccessRequest(id: $id, input: $input) {
id
comment
payload
userId
createdAt
updatedAt
}
}
`
export const Loading = () => <div>Loading...</div>
export const Success = ({ subjectAccessRequest }) => {
const { addMessage } = useFlash()
const [updateSubjectAccessRequest, { loading, error }] = useMutation(
UPDATE_SUBJECT_ACCESS_REQUEST_MUTATION,
{
onCompleted: () => {
navigate(routes.subjectAccessRequests())
addMessage('SubjectAccessRequest updated.', {
classes: 'rw-flash-success',
})
},
}
)
const onSave = (input, id) => {
updateSubjectAccessRequest({ variables: { id, input } })
}
return (
<div className="rw-segment">
<header className="rw-segment-header">
<h2 className="rw-heading rw-heading-secondary">
Edit SubjectAccessRequest {subjectAccessRequest.id}
</h2>
</header>
<div className="rw-segment-main">
<SubjectAccessRequestForm
subjectAccessRequest={subjectAccessRequest}
onSave={onSave}
error={error}
loading={loading}
/>
</div>
</div>
)
}

View File

@@ -32,7 +32,7 @@ export const Empty = () => <div>Empty</div>
export const Failure = ({ error }) => <div>Error: {error.message}</div>
export const Success = ({ user }) => {
export const Success = ({ user, variables: { isEditable } }) => {
const { addMessage } = useFlash()
const [updateUser, { loading, error }] = useMutation(UPDATE_USER_MUTATION, {
onCompleted: ({ updateUserByUserName }) => {
@@ -51,7 +51,7 @@ export const Success = ({ user }) => {
onSave={onSave}
loading={loading}
error={error}
isEditable
isEditable={isEditable}
/>
)
}

View File

@@ -0,0 +1,16 @@
import { Link, routes } from '@redwoodjs/router'
const Footer = () => {
return (
<div className="bg-indigo-900 text-indigo-200 font-roboto mt-20 text-sm">
<div className="flex h-16 justify-end items-center mx-16">
<Link className="mr-8" to={routes.codeOfConduct()}>
Code of Conduct
</Link>
<Link to={routes.privacyPolicy()}>Privacy Policy</Link>
</div>
</div>
)
}
export default Footer

View File

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

View File

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

View File

@@ -59,7 +59,6 @@ const IdeToolbar = ({ canEdit, isChanges, onSave, onExport, userNamePart }) => {
<div className="h-8 w-8 ml-4">
<ImageUploader
className="rounded-full object-cover"
onImageUpload={() => {}}
aspectRatio={1}
imageUrl={userNamePart?.image}
width={80}

View File

@@ -12,7 +12,7 @@ const CLOUDINARY_UPLOAD_PRESET = 'CadHub_project_images'
const CLOUDINARY_UPLOAD_URL = 'https://api.cloudinary.com/v1_1/irevdev/upload'
export default function ImageUploader({
onImageUpload,
onImageUpload = () => {},
imageUrl,
aspectRatio,
className,
@@ -67,11 +67,12 @@ export default function ImageUploader({
>
<div className="absolute w-full h-full" {...getRootProps()}>
{cloudinaryId && isEditable && (
<button className="absolute z-10 w-full inset-0 bg-indigo-900 opacity-50 flex justify-center items-center">
<button className="absolute z-10 bg-indigo-900 opacity-75 bottom-0 right-0 flex items-center p-1 mb-6 mr-2 rounded-lg">
<span className="text-gray-100 pr-2">Update</span>
<Svg
name="pencil"
strokeWidth={2}
className="text-gray-300 h-24 w-24"
className=" text-gray-100 h-6 w-6"
/>
</button>
)}

View File

@@ -1,6 +1,12 @@
import { getActiveClasses } from 'get-active-classes'
const InputText = ({ value, isEditable, onChange, className }) => {
const InputText = ({
value,
isEditable,
onChange,
className,
isInvalid = false,
}) => {
return (
<>
<div
@@ -10,7 +16,12 @@ const InputText = ({ value, isEditable, onChange, className }) => {
className
)}
>
<div className="absolute inset-0 mb-2 rounded bg-gray-200 shadow-inner" />
<div
className={getActiveClasses(
'absolute inset-0 mb-2 rounded bg-gray-200 shadow-inner',
{ 'border border-red-500': isInvalid }
)}
/>
<input
className="pl-2 pt-1 text-indigo-800 font-medium mb-px pb-px bg-transparent relative"
onChange={onChange}

View File

@@ -191,6 +191,14 @@ const SignUpForm = ({ onSubmitSignUp, checkBox, setCheckBox, onClose }) => (
>
Code of Conduct
</Link>
, and agree with our{' '}
<Link
onClick={onClose}
to={routes.privacyPolicy()}
className="underline"
>
Privacy Policy
</Link>
</span>
</div>
<HeroButton text="Sign Up" />

View File

@@ -21,8 +21,10 @@ const PartProfile = ({
}) => {
const [comment, setComment] = useState('')
const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false)
const [isInvalid, setIsInvalid] = useState(false)
const { currentUser } = useAuth()
const canEdit = currentUser?.sub === userPart.id
const isImageEditable = !isEditable && canEdit // image is editable when not in profile edit mode in order to separate them as it's too hard too to upload an image to cloudinary temporarily until the use saves (and maybe have to clean up) for the time being
const part = userPart?.Part
const emotes = countEmotes(part?.Reaction)
const userEmotes = part?.userReactions.map(({ emote }) => emote)
@@ -48,11 +50,19 @@ const PartProfile = ({
setProperty('title', target.value.replace(/([^a-zA-Z\d_:])/g, '-'))
const onDescriptionChange = (description) =>
setProperty('description', description())
const onImageUpload = ({ cloudinaryPublicId }) =>
setProperty('mainImage', cloudinaryPublicId)
const onEditSaveClick = () => {
const onImageUpload = ({ cloudinaryPublicId }) => {
onSave(part?.id, { ...input, mainImage: cloudinaryPublicId })
}
// setProperty('mainImage', cloudinaryPublicId)
const onEditSaveClick = (hi) => {
// do a thing
if (isEditable) {
input.title && onSave(part?.id, input)
if (!input.title) {
setIsInvalid(true)
return
}
setIsInvalid(false)
onSave(part?.id, input)
return
}
navigate(
@@ -69,7 +79,6 @@ const PartProfile = ({
<aside className="col-start-2 relative">
<ImageUploader
className="rounded-half rounded-br-lg shadow-md border-2 border-gray-200 border-solid"
onImageUpload={() => {}}
aspectRatio={1}
imageUrl={userPart?.image}
width={300}
@@ -144,13 +153,14 @@ const PartProfile = ({
onPartTitleChange={isEditable && onTitleChange}
userName={userPart?.userName}
partTitle={input?.title}
isInvalid={isInvalid}
/>
{!!(input?.mainImage || isEditable) && (
{!!input?.mainImage && !isEditable && part?.id && (
<ImageUploader
className="rounded-lg shadow-md border-2 border-gray-200 border-solid mt-8"
onImageUpload={onImageUpload}
aspectRatio={16 / 9}
isEditable={isEditable}
isEditable={isImageEditable}
imageUrl={input?.mainImage}
width={1010}
/>
@@ -183,7 +193,6 @@ const PartProfile = ({
<div className="w-8 h-8 overflow-hidden rounded-full border border-indigo-300 shadow flex-shrink-0">
<ImageUploader
className=""
onImageUpload={() => {}}
aspectRatio={1}
imageUrl={user?.image}
width={50}

View File

@@ -36,7 +36,6 @@ const PartsList = ({ parts, shouldFilterPartsWithoutImage = false }) => {
<div className="w-8 h-8 overflow-hidden rounded-full border border-indigo-300 shadow">
<ImageUploader
className=""
onImageUpload={() => {}}
aspectRatio={1}
imageUrl={user?.image}
width={50}
@@ -49,7 +48,6 @@ const PartsList = ({ parts, shouldFilterPartsWithoutImage = false }) => {
<div className="w-full overflow-hidden relative rounded-b-lg">
<ImageUploader
className=""
onImageUpload={() => {}}
aspectRatio={1.4}
imageUrl={mainImage}
width={700}

View File

@@ -0,0 +1,119 @@
import { useMutation, useFlash } from '@redwoodjs/web'
import { Link, routes, navigate } from '@redwoodjs/router'
import { QUERY } from 'src/components/SubjectAccessRequestsCell'
const DELETE_SUBJECT_ACCESS_REQUEST_MUTATION = gql`
mutation DeleteSubjectAccessRequestMutation($id: String!) {
deleteSubjectAccessRequest(id: $id) {
id
}
}
`
const jsonDisplay = (obj) => {
return (
<pre>
<code>{JSON.stringify(obj, null, 2)}</code>
</pre>
)
}
const timeTag = (datetime) => {
return (
<time dateTime={datetime} title={datetime}>
{new Date(datetime).toUTCString()}
</time>
)
}
const checkboxInputTag = (checked) => {
return <input type="checkbox" checked={checked} disabled />
}
const SubjectAccessRequest = ({ subjectAccessRequest }) => {
const { addMessage } = useFlash()
const [deleteSubjectAccessRequest] = useMutation(
DELETE_SUBJECT_ACCESS_REQUEST_MUTATION,
{
onCompleted: () => {
navigate(routes.subjectAccessRequests())
addMessage('SubjectAccessRequest deleted.', {
classes: 'rw-flash-success',
})
},
// This refetches the query on the list page. Read more about other ways to
// update the cache over here:
// https://www.apollographql.com/docs/react/data/mutations/#making-all-other-cache-updates
refetchQueries: [{ query: QUERY }],
awaitRefetchQueries: true,
}
)
const onDeleteClick = (id) => {
if (
confirm(
'Are you sure you want to delete subjectAccessRequest ' + id + '?'
)
) {
deleteSubjectAccessRequest({ variables: { id } })
}
}
return (
<>
<div className="rw-segment">
<header className="rw-segment-header">
<h2 className="rw-heading rw-heading-secondary">
SubjectAccessRequest {subjectAccessRequest.id} Detail
</h2>
</header>
<table className="rw-table">
<tbody>
<tr>
<th>Id</th>
<td>{subjectAccessRequest.id}</td>
</tr>
<tr>
<th>Comment</th>
<td>{subjectAccessRequest.comment}</td>
</tr>
<tr>
<th>Payload</th>
<td>{subjectAccessRequest.payload}</td>
</tr>
<tr>
<th>User id</th>
<td>{subjectAccessRequest.userId}</td>
</tr>
<tr>
<th>Created at</th>
<td>{timeTag(subjectAccessRequest.createdAt)}</td>
</tr>
<tr>
<th>Updated at</th>
<td>{timeTag(subjectAccessRequest.updatedAt)}</td>
</tr>
</tbody>
</table>
</div>
<nav className="rw-button-group">
<Link
to={routes.editSubjectAccessRequest({ id: subjectAccessRequest.id })}
className="rw-button rw-button-blue"
>
Edit
</Link>
<a
href="#"
className="rw-button rw-button-red"
onClick={() => onDeleteClick(subjectAccessRequest.id)}
>
Delete
</a>
</nav>
</>
)
}
export default SubjectAccessRequest

View File

@@ -0,0 +1,22 @@
import SubjectAccessRequest from 'src/components/SubjectAccessRequest'
export const QUERY = gql`
query FIND_SUBJECT_ACCESS_REQUEST_BY_ID($id: String!) {
subjectAccessRequest: subjectAccessRequest(id: $id) {
id
comment
payload
userId
createdAt
updatedAt
}
}
`
export const Loading = () => <div>Loading...</div>
export const Empty = () => <div>SubjectAccessRequest not found</div>
export const Success = ({ subjectAccessRequest }) => {
return <SubjectAccessRequest subjectAccessRequest={subjectAccessRequest} />
}

View File

@@ -0,0 +1,83 @@
import {
Form,
FormError,
FieldError,
Label,
TextField,
Submit,
} from '@redwoodjs/forms'
const SubjectAccessRequestForm = (props) => {
const onSubmit = (data) => {
props.onSave(data, props?.subjectAccessRequest?.id)
}
return (
<div className="rw-form-wrapper">
<Form onSubmit={onSubmit} error={props.error}>
<FormError
error={props.error}
wrapperClassName="rw-form-error-wrapper"
titleClassName="rw-form-error-title"
listClassName="rw-form-error-list"
/>
<Label
name="comment"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Comment
</Label>
<TextField
name="comment"
defaultValue={props.subjectAccessRequest?.comment}
className="rw-input"
errorClassName="rw-input rw-input-error"
validation={{ required: true }}
/>
<FieldError name="comment" className="rw-field-error" />
<Label
name="payload"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Payload
</Label>
<TextField
name="payload"
defaultValue={props.subjectAccessRequest?.payload}
className="rw-input"
errorClassName="rw-input rw-input-error"
validation={{ required: true }}
/>
<FieldError name="payload" className="rw-field-error" />
<Label
name="userId"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
User id
</Label>
<TextField
name="userId"
defaultValue={props.subjectAccessRequest?.userId}
className="rw-input"
errorClassName="rw-input rw-input-error"
validation={{ required: true }}
/>
<FieldError name="userId" className="rw-field-error" />
<div className="rw-button-group">
<Submit disabled={props.loading} className="rw-button rw-button-blue">
Save
</Submit>
</div>
</Form>
</div>
)
}
export default SubjectAccessRequestForm

View File

@@ -0,0 +1,137 @@
import { useMutation, useFlash } from '@redwoodjs/web'
import { Link, routes } from '@redwoodjs/router'
import { QUERY } from 'src/components/SubjectAccessRequestsCell'
const DELETE_SUBJECT_ACCESS_REQUEST_MUTATION = gql`
mutation DeleteSubjectAccessRequestMutation($id: String!) {
deleteSubjectAccessRequest(id: $id) {
id
}
}
`
const MAX_STRING_LENGTH = 150
const truncate = (text) => {
let output = text
if (text && text.length > MAX_STRING_LENGTH) {
output = output.substring(0, MAX_STRING_LENGTH) + '...'
}
return output
}
const jsonTruncate = (obj) => {
return truncate(JSON.stringify(obj, null, 2))
}
const timeTag = (datetime) => {
return (
<time dateTime={datetime} title={datetime}>
{new Date(datetime).toUTCString()}
</time>
)
}
const checkboxInputTag = (checked) => {
return <input type="checkbox" checked={checked} disabled />
}
const SubjectAccessRequestsList = ({ subjectAccessRequests }) => {
const { addMessage } = useFlash()
const [deleteSubjectAccessRequest] = useMutation(
DELETE_SUBJECT_ACCESS_REQUEST_MUTATION,
{
onCompleted: () => {
addMessage('SubjectAccessRequest deleted.', {
classes: 'rw-flash-success',
})
},
// This refetches the query on the list page. Read more about other ways to
// update the cache over here:
// https://www.apollographql.com/docs/react/data/mutations/#making-all-other-cache-updates
refetchQueries: [{ query: QUERY }],
awaitRefetchQueries: true,
}
)
const onDeleteClick = (id) => {
if (
confirm(
'Are you sure you want to delete subjectAccessRequest ' + id + '?'
)
) {
deleteSubjectAccessRequest({ variables: { id } })
}
}
return (
<div className="rw-segment rw-table-wrapper-responsive">
<table className="rw-table">
<thead>
<tr>
<th>Id</th>
<th>Comment</th>
<th>Payload</th>
<th>User id</th>
<th>Created at</th>
<th>Updated at</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
{subjectAccessRequests.map((subjectAccessRequest) => (
<tr key={subjectAccessRequest.id}>
<td>{truncate(subjectAccessRequest.id)}</td>
<td>{truncate(subjectAccessRequest.comment)}</td>
<td>{truncate(subjectAccessRequest.payload)}</td>
<td>{truncate(subjectAccessRequest.userId)}</td>
<td>{timeTag(subjectAccessRequest.createdAt)}</td>
<td>{timeTag(subjectAccessRequest.updatedAt)}</td>
<td>
<nav className="rw-table-actions">
<Link
to={routes.subjectAccessRequest({
id: subjectAccessRequest.id,
})}
title={
'Show subjectAccessRequest ' +
subjectAccessRequest.id +
' detail'
}
className="rw-button rw-button-small"
>
Show
</Link>
<Link
to={routes.editSubjectAccessRequest({
id: subjectAccessRequest.id,
})}
title={
'Edit subjectAccessRequest ' + subjectAccessRequest.id
}
className="rw-button rw-button-small rw-button-blue"
>
Edit
</Link>
<a
href="#"
title={
'Delete subjectAccessRequest ' + subjectAccessRequest.id
}
className="rw-button rw-button-small rw-button-red"
onClick={() => onDeleteClick(subjectAccessRequest.id)}
>
Delete
</a>
</nav>
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
export default SubjectAccessRequestsList

View File

@@ -0,0 +1,24 @@
import SubjectAccessRequests from 'src/components/SubjectAccessRequests'
export const QUERY = gql`
query SUBJECT_ACCESS_REQUESTS {
subjectAccessRequests {
id
comment
payload
userId
createdAt
updatedAt
}
}
`
export const Loading = () => <div>Loading...</div>
export const Empty = () => {
return <div className="rw-text-center">No subjectAccessRequests yet.</div>
}
export const Success = ({ subjectAccessRequests }) => {
return <SubjectAccessRequests subjectAccessRequests={subjectAccessRequests} />
}

View File

@@ -1,23 +0,0 @@
import UserProfile from 'src/components/UserProfile'
export const QUERY = gql`
query FIND_USER_BY_ID($userName: String!) {
user: userName(userName: $userName) {
id
userName
name
createdAt
updatedAt
image
bio
}
}
`
export const Loading = () => <div>Loading...</div>
export const Empty = () => <div>User not found</div>
export const Success = ({ user }) => {
return <UserProfile user={user} />
}

View File

@@ -10,6 +10,7 @@ import PartsOfUser from 'src/components/PartsOfUserCell'
const UserProfile = ({ user, isEditable, loading, onSave, error, parts }) => {
const { currentUser } = useAuth()
const canEdit = currentUser?.sub === user.id
const isImageEditable = !isEditable && canEdit // image is editable when not in profile edit mode in order to separate them as it's too hard too to upload an image to cloudinary temporarily until the use saves (and maybe have to clean up) for the time being
useEffect(() => {
isEditable && !canEdit && navigate(routes.user({ userName: user.userName }))
}, [currentUser])
@@ -25,21 +26,23 @@ const UserProfile = ({ user, isEditable, loading, onSave, error, parts }) => {
<>
<section className="max-w-2xl mx-auto mt-20 ">
<div className="flex">
<div className="w-40 flex-shrink-0">
<ImageUploader
className="rounded-half rounded-br-lg shadow-md border-2 border-gray-200 border-solid"
onImageUpload={({ cloudinaryPublicId: image }) =>
setInput({
...input,
image,
})
}
aspectRatio={1}
isEditable={isEditable}
imageUrl={user.image}
width={300}
/>
</div>
{!isEditable && (
<div className="w-40 flex-shrink-0">
<ImageUploader
className="rounded-half rounded-br-lg shadow-md border-2 border-gray-200 border-solid"
onImageUpload={({ cloudinaryPublicId: image }) => {
onSave(user.userName, {
...input,
image,
})
}}
aspectRatio={1}
isEditable={isImageEditable}
imageUrl={user.image}
width={300}
/>
</div>
)}
<div className="ml-6 flex flex-col justify-between">
<ProfileTextInput
fields={editableTextFields}

View File

@@ -29,7 +29,7 @@
<div id="redwood-app"></div>
<div
id="cascade-container"
style="opacity: 0; overflow: hidden; height: calc(100vh - 8rem)"
style="opacity: 0; overflow: hidden; height: 0"
></div>
</body>
</html>

View File

@@ -5,6 +5,7 @@ import { Flash, useQuery, useFlash } from '@redwoodjs/web'
import Tooltip from '@material-ui/core/Tooltip'
import Popover from '@material-ui/core/Popover'
import { getActiveClasses } from 'get-active-classes'
import Footer from 'src/components/Footer'
import { useLocation } from '@redwoodjs/router'
import LoginModal from 'src/components/LoginModal'
import ReactGA from 'react-ga'
@@ -23,9 +24,8 @@ import Svg from 'src/components/Svg'
import ImageUploader from 'src/components/ImageUploader'
let previousSubmission = ''
let previousUserID = ''
const MainLayout = ({ children }) => {
const MainLayout = ({ children, shouldRemoveFooterInIde }) => {
const { logOut, isAuthenticated, currentUser, client } = useAuth()
const { addMessage } = useFlash()
const { data, loading } = useQuery(QUERY, {
@@ -75,18 +75,6 @@ const MainLayout = ({ children }) => {
previousSubmission = newSubmission
}
}, [pathname, params])
useEffect(() => {
// not the "React" way of doing think, but otherwise it will submit twice
// it's because the old page submits it and when the new page loads it happens again
if (
isAuthenticated &&
previousUserID !== currentUser &&
data?.user?.userName
) {
ReactGA.set({ userName: data.user.userName, userId: currentUser })
previousUserID = currentUser
}
}, [data, currentUser, isAuthenticated])
const hash = window.location.hash
useEffect(() => {
const [key, token] = hash.slice(1).split('=')
@@ -115,7 +103,7 @@ const MainLayout = ({ children }) => {
}
}, [hash, client]) // complaining about not having addMessage, however adding it puts useEffect into a loop
return (
<>
<div className="h-full">
<header id="cadhub-main-header">
<nav className="flex justify-between h-20 px-12 bg-gradient-to-r from-gray-900 to-indigo-900">
<ul className="flex items-center">
@@ -173,7 +161,6 @@ const MainLayout = ({ children }) => {
{!loading && (
<ImageUploader
className="rounded-full object-cover"
onImageUpload={() => {}}
aspectRatio={1}
imageUrl={data?.user?.image}
width={80}
@@ -222,9 +209,6 @@ const MainLayout = ({ children }) => {
<a href="#" className="text-indigo-800" onClick={logOut}>
Logout
</a>
<Link to={routes.codeOfConduct()}>
<div className="text-indigo-400 pt-8">Code of Conduct</div>
</Link>
</div>
</Popover>
)}
@@ -236,7 +220,8 @@ const MainLayout = ({ children }) => {
onClose={() => setIsLoginModalOpen(false)}
/>
<main>{children}</main>
</>
{!shouldRemoveFooterInIde && <Footer />}
</div>
)
}

View File

@@ -0,0 +1,20 @@
import { Link, routes } from '@redwoodjs/router'
import { Flash } from '@redwoodjs/web'
const SubjectAccessRequestsLayout = (props) => {
return (
<div className="rw-scaffold">
<Flash timeout={1000} />
<header className="rw-header">
<h1 className="rw-heading rw-heading-primary">
<Link to={routes.subjectAccessRequests()} className="rw-link">
SubjectAccessRequests
</Link>
</h1>
</header>
<main className="rw-main">{props.children}</main>
</div>
)
}
export default SubjectAccessRequestsLayout

View File

@@ -18,6 +18,7 @@ const CodeOfConductPage = () => {
lang="en-US"
/>
<div className="max-w-5xl mx-auto mt-20">
<h1 className="text-3xl">Code of Conduct</h1>
<h2 className="text-2xl font-ropa-sans py-4">What's Required</h2>
<ul className="list-disc pl-4">
<Li>

View File

@@ -0,0 +1,12 @@
import SubjectAccessRequestsLayout from 'src/layouts/SubjectAccessRequestsLayout'
import EditSubjectAccessRequestCell from 'src/components/EditSubjectAccessRequestCell'
const EditSubjectAccessRequestPage = ({ id }) => {
return (
<SubjectAccessRequestsLayout>
<EditSubjectAccessRequestCell id={id} />
</SubjectAccessRequestsLayout>
)
}
export default EditSubjectAccessRequestPage

View File

@@ -7,7 +7,7 @@ const UserPage = ({ userName }) => {
<MainLayout>
<Seo title={userName} description="Add new part page" lang="en-US" />
<EditUserCell userName={userName} />
<EditUserCell userName={userName} isEditable />
</MainLayout>
)
}

View File

@@ -4,7 +4,7 @@ import Seo from 'src/components/Seo/Seo'
const IdePartPage = ({ userName, partTitle }) => {
return (
<MainLayout>
<MainLayout shouldRemoveFooterInIde>
<Seo title={partTitle} description={partTitle} lang="en-US" />
<IdePartCell userName={userName} partTitle={partTitle} />

View File

@@ -0,0 +1,153 @@
import MainLayout from 'src/layouts/MainLayout'
import Seo from 'src/components/Seo/Seo'
import OutBound from 'src/components/OutBound'
const Li = ({ children }) => (
<li className="pb-2 font-roboto text-lg text-gray-800">{children}</li>
)
const P = ({ children }) => <p className="py-2">{children}</p>
const A = ({ to }) => <OutBound to={to}>{to}</OutBound>
const SubHeading = ({ children }) => <h3>{children}</h3>
const PrivacyPolicyPage = () => {
return (
<MainLayout>
<Seo
title={'Privacy Policy'}
description="Outlines CadHub's policy in regards to User's right to privacy"
lang="en-US"
/>
<div className="max-w-5xl mx-auto mt-20">
<h1 className="text-3xl">CadHub Privacy Policy</h1>
<P>
This Privacy Policy describes how your personal information is
collected, used, and shared when you visit or use{' '}
<A to="https://cadhub.xyz" /> (the Site).
</P>
<SubHeading>PERSONAL INFORMATION WE COLLECT</SubHeading>
<P>
When you visit the Site, we automatically collect certain information
about your device, including information about your web browser, IP
address, time zone, and some of the cookies that are installed on your
device. Additionally, as you browse the Site, we collect information
about the individual web pages that you view, what websites or search
terms referred you to the Site, and information about how you interact
with the Site. We refer to this automatically-collected information as
Device Information.
</P>
<P>We collect Device Information using the following technologies:</P>
<ul className="list-disc pl-4">
<Li>
Cookies are data files that are placed on your device or computer
and often include an anonymous unique identifier. For more
information about cookies, and how to disable cookies, visit
<A to="http://www.allaboutcookies.org" />.
</Li>
<Li>
Log files track actions occurring on the Site, and collect data
including your IP address, browser type, Internet service provider,
referring/exit pages, and date/time stamps.
</Li>
<Li>
Web beacons, tags, and pixels are electronic files used to
record information about how you browse the Site.
</Li>
</ul>
<P>
Additionally when you make an account or sign in to the app through
the Site, we collect certain information from you, including your
name, email address as well as any information you add to the website,
such as your profile bio, or "parts" you have added. We refer to this
information as Account Information.
</P>
<P>
When we talk about Personal Information in this Privacy Policy, we
are talking both about Device Information and Account Information.
</P>
<SubHeading>HOW DO WE USE YOUR PERSONAL INFORMATION?</SubHeading>
<P>
We use the Account Information that we collect generally to link you
to your account and the content you create through CadHub.xyz.
Additionally, we use this Account Information to communicate with you.
We use the Device Information that we collect to help us screen for
potential risk (in particular, your IP address), and more generally to
improve and optimize our Site (for example, by generating analytics
about how our customers browse and interact with the Site, and to
assess the success of our marketing and advertising campaigns).
</P>
<SubHeading>SHARING YOUR PERSONAL INFORMATION</SubHeading>
<P>
We share your Personal Information with third parties to help us use
your Personal Information, as described above. For example, we use
Netlify's idenity service to handle user logins and authentication --
you can read more about how Netlify uses your Personal Information
here: <A to="https://www.netlify.com/privacy/" /> --{' '}
<A to="https://www.netlify.com/gdpr-ccpa" />. We also use Google
Analytics to help us understand how our customers use the Site -- you
can read more about how Google uses your Personal Information here:{' '}
<A to="https://www.google.com/intl/en/policies/privacy/" />. You can
also opt-out of Google Analytics here:{' '}
<A to="https://tools.google.com/dlpage/gaoptout" />. We also use
MailChimp to send newsletters, You can read more about how MailChip
uses your Personal Information here:{' '}
<A to="https://mailchimp.com/legal/privacy/" />.
</P>
<P>
Finally, we may also share your Personal Information to comply with
applicable laws and regulations, to respond to a subpoena, search
warrant or other lawful request for information we receive, or to
otherwise protect our rights.
</P>
<SubHeading>DO NOT TRACK</SubHeading>
<P>
Please note that we do not alter our Sites data collection and use
practices when we see a Do Not Track signal from your browser.
</P>
<SubHeading>YOUR RIGHTS</SubHeading>
<P>
If you are a European resident, you have the right to access personal
information we hold about you and to ask that your personal
information be corrected, updated, or deleted. If you would like to
exercise this right, please contact us through the contact information
below.
</P>
<P>
Additionally, if you are a European resident we note that we are
processing your information in order to fulfill contracts we might
have with you (for example if you make an order through the Site), or
otherwise to pursue our legitimate business interests listed above.
Additionally, please note that your information will be transferred
outside of Europe, including to Australia, Canada and the United
States.
</P>
<SubHeading>DATA RETENTION</SubHeading>
<P>
When you place an create a "part" through the Site, we will keep this
record to become part of the public website, you can delete you parts
at anytime.
</P>
<SubHeading>CHANGES</SubHeading>
<P>
We may update this privacy policy from time to time in order to
reflect, for example, changes to our practices or for other
operational, legal or regulatory reasons.
</P>
<SubHeading>CONTACT US</SubHeading>
<P>
For more information about our privacy practices, if you have
questions, or if you would like to make a complaint, please contact us
by e-mail at{' '}
<a href="mailto:privacy@kurthutten.com">privacy@kurthutten.com</a> or
by mail using the details provided below:
</P>
<P>PO Box 462, Figtree, NSW, 2525, Australia</P>
</div>
</MainLayout>
)
}
export default PrivacyPolicyPage

View File

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

View File

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

View File

@@ -0,0 +1,12 @@
import SubjectAccessRequestsLayout from 'src/layouts/SubjectAccessRequestsLayout'
import SubjectAccessRequestCell from 'src/components/SubjectAccessRequestCell'
const SubjectAccessRequestPage = ({ id }) => {
return (
<SubjectAccessRequestsLayout>
<SubjectAccessRequestCell id={id} />
</SubjectAccessRequestsLayout>
)
}
export default SubjectAccessRequestPage

View File

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

View File

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

View File

@@ -0,0 +1,164 @@
import { useState, useEffect } from 'react'
import SubjectAccessRequestsCell from 'src/components/SubjectAccessRequestsCell'
import { Flash, useQuery, useMutation, useFlash } from '@redwoodjs/web'
import { Form, Submit } from '@redwoodjs/forms'
import MainLayout from 'src/layouts/MainLayout'
import Seo from 'src/components/Seo/Seo'
import InputTextForm from 'src/components/InputTextForm'
export const QUERY = gql`
query SUBJECT_ACCESS_REQUEST($userName: String!) {
userName(userName: $userName) {
id
userName
email
name
createdAt
updatedAt
image
bio
Parts {
id
title
description
code
mainImage
createdAt
updatedAt
deleted
}
Reaction {
id
emote
part {
id
title
}
createdAt
updatedAt
}
Comment {
id
text
part {
id
title
}
createdAt
updatedAt
}
SubjectAccessRequest {
id
comment
createdAt
updatedAt
}
}
}
`
const CREATE_SUBJECT_ACCESS_REQUEST_MUTATION = gql`
mutation CreateSubjectAccessRequestMutation(
$input: CreateSubjectAccessRequestInput!
) {
createSubjectAccessRequest(input: $input) {
id
comment
payload
userId
createdAt
updatedAt
}
}
`
const SubjectAccessRequestPage = () => {
const { addMessage } = useFlash()
const [input, setInput] = useState({})
const { data } = useQuery(QUERY, {
skip: !input.userName,
variables: { userName: input.userName },
})
const onSubmit = (input) => {
setInput(input)
}
const stringData = JSON.stringify(data?.userName)
const [createSubjectAccessRequest] = useMutation(
CREATE_SUBJECT_ACCESS_REQUEST_MUTATION,
{
onCompleted: () => {
addMessage('SubjectAccessRequest created.', {
classes: 'rw-flash-success',
})
},
}
)
useEffect(() => {
if (stringData) {
createSubjectAccessRequest({
variables: {
input: {
comment: input?.comment,
payload: stringData,
userId: data?.userName?.id,
},
},
})
}
}, [stringData])
return (
<MainLayout>
<Seo
title={'Subject Access Request'}
description="Code of Conduct"
lang="en-US"
/>
<section className="max-w-6xl mx-auto mt-20">
<SubjectAccessRequestsCell />
Here to fulfill a user's right to portability, before running this
please check that the query in
"pages/SubjectAccessRequestPage/SubjectAccessRequestPage.js" is
up-to-date.
<Form onSubmit={onSubmit}>
<div
className="grid items-center gap-2"
style={{ gridTemplateColumns: 'auto 1fr' }}
>
<span className="capitalize text-gray-500 text-sm align-middle my-3">
userName:
</span>
<InputTextForm
className="text-xl"
name="userName"
validation={{
required: true,
}}
/>
<div />
<div className="mt-10">
Please add how they made the request, who is fulling it (who you
are) and any other details
</div>
<span className="capitalize text-gray-500 text-sm align-middle my-3">
comment:
</span>
<InputTextForm
className="text-xl"
name="comment"
validation={{
required: true,
}}
/>
</div>
<Submit className="bg-indigo-200 text-indigo-800 p-2 px-4 shadow hover:shadow-lg mt-4 rounded">
Submit
</Submit>
</Form>
<pre className="whitespace-pre-wrap">{stringData}</pre>
</section>
</MainLayout>
)
}
export default SubjectAccessRequestPage

View File

@@ -1,5 +1,5 @@
import MainLayout from 'src/layouts/MainLayout'
import UserCell from 'src/components/UserCell'
import EditUserCell from 'src/components/EditUserCell'
import Seo from 'src/components/Seo/Seo'
const UserPage = ({ userName }) => {
@@ -7,7 +7,7 @@ const UserPage = ({ userName }) => {
<MainLayout>
<Seo title={userName} description="User page" lang="en-US" />
<UserCell userName={userName} />
<EditUserCell userName={userName} />
</MainLayout>
)
}

View File

@@ -5959,7 +5959,7 @@ clone-deep@^4.0.1:
kind-of "^6.0.2"
shallow-clone "^3.0.0"
cloudinary-core@^2.11.3:
cloudinary-core@^2.10.2, cloudinary-core@^2.11.3:
version "2.11.3"
resolved "https://registry.yarnpkg.com/cloudinary-core/-/cloudinary-core-2.11.3.tgz#1440f61c6280485094aac87021b7e10f746dc69e"
integrity sha512-ZRnpjSgvx+LbSf+aEz5NKzxDB4Z0436aY/0BSDa90kAHiwAyd84VyEi95I74SE80e15Ri9t5S2xtksTXpzk9Xw==
@@ -5972,6 +5972,16 @@ cloudinary-react@^1.6.7:
cloudinary-core "^2.11.3"
prop-types "^15.6.2"
cloudinary@^1.23.0:
version "1.23.0"
resolved "https://registry.yarnpkg.com/cloudinary/-/cloudinary-1.23.0.tgz#27885567cf698e4ed5f5cc8d57974cb4b16db52b"
integrity sha512-akOxzroonvwWkuSVq7BI50nYpZPRXc5DbQIYETCVeKX9ZoToH2Gvc3MdUH63UtKiszuGYE51q2B+jQsJkBp2AQ==
dependencies:
cloudinary-core "^2.10.2"
core-js "3.6.5"
lodash "^4.17.11"
q "^1.5.1"
clsx@^1.0.4, clsx@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
@@ -13181,7 +13191,7 @@ purgecss@^2.3.0:
postcss "7.0.32"
postcss-selector-parser "^6.0.2"
q@^1.1.2:
q@^1.1.2, q@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=