Add Privacy Policy related improvements
various thing to make sure we're GDPR, et al compliant
This commit is contained in:
@@ -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=
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
+}
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
39
api/src/graphql/subjectAccessRequests.sdl.js
Normal file
39
api/src/graphql/subjectAccessRequests.sdl.js
Normal 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!
|
||||
}
|
||||
`
|
||||
@@ -12,6 +12,7 @@ export const schema = gql`
|
||||
Part(partTitle: String): Part
|
||||
Reaction: [PartReaction]!
|
||||
Comment: [Comment]!
|
||||
SubjectAccessRequest: [SubjectAccessRequest]!
|
||||
}
|
||||
|
||||
type Query {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
import { subjectAccessRequests } from './subjectAccessRequests'
|
||||
*/
|
||||
|
||||
describe('subjectAccessRequests', () => {
|
||||
it('returns true', () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
16
web/src/components/Footer/Footer.js
Normal file
16
web/src/components/Footer/Footer.js
Normal 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
|
||||
7
web/src/components/Footer/Footer.stories.js
Normal file
7
web/src/components/Footer/Footer.stories.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import Footer from './Footer'
|
||||
|
||||
export const generated = () => {
|
||||
return <Footer />
|
||||
}
|
||||
|
||||
export default { title: 'Components/Footer' }
|
||||
11
web/src/components/Footer/Footer.test.js
Normal file
11
web/src/components/Footer/Footer.test.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { render } from '@redwoodjs/testing'
|
||||
|
||||
import Footer from './Footer'
|
||||
|
||||
describe('Footer', () => {
|
||||
it('renders successfully', () => {
|
||||
expect(() => {
|
||||
render(<Footer />)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
119
web/src/components/SubjectAccessRequest/SubjectAccessRequest.js
Normal file
119
web/src/components/SubjectAccessRequest/SubjectAccessRequest.js
Normal 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
|
||||
@@ -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} />
|
||||
}
|
||||
@@ -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
|
||||
@@ -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> </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
|
||||
@@ -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} />
|
||||
}
|
||||
@@ -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} />
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
153
web/src/pages/PrivacyPolicyPage/PrivacyPolicyPage.js
Normal file
153
web/src/pages/PrivacyPolicyPage/PrivacyPolicyPage.js
Normal 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 Site’s 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
|
||||
@@ -0,0 +1,7 @@
|
||||
import PrivacyPolicyPage from './PrivacyPolicyPage'
|
||||
|
||||
export const generated = () => {
|
||||
return <PrivacyPolicyPage />
|
||||
}
|
||||
|
||||
export default { title: 'Pages/PrivacyPolicyPage' }
|
||||
11
web/src/pages/PrivacyPolicyPage/PrivacyPolicyPage.test.js
Normal file
11
web/src/pages/PrivacyPolicyPage/PrivacyPolicyPage.test.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { render } from '@redwoodjs/testing'
|
||||
|
||||
import PrivacyPolicyPage from './PrivacyPolicyPage'
|
||||
|
||||
describe('PrivacyPolicyPage', () => {
|
||||
it('renders successfully', () => {
|
||||
expect(() => {
|
||||
render(<PrivacyPolicyPage />)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
@@ -0,0 +1,7 @@
|
||||
import SubjectAccessRequestPage from './SubjectAccessRequestPage'
|
||||
|
||||
export const generated = () => {
|
||||
return <SubjectAccessRequestPage />
|
||||
}
|
||||
|
||||
export default { title: 'Pages/SubjectAccessRequestPage' }
|
||||
@@ -0,0 +1,11 @@
|
||||
import { render } from '@redwoodjs/testing'
|
||||
|
||||
import SubjectAccessRequestPage from './SubjectAccessRequestPage'
|
||||
|
||||
describe('SubjectAccessRequestPage', () => {
|
||||
it('renders successfully', () => {
|
||||
expect(() => {
|
||||
render(<SubjectAccessRequestPage />)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
14
yarn.lock
14
yarn.lock
@@ -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=
|
||||
|
||||
Reference in New Issue
Block a user