Skip to content

Commit bc1c050

Browse files
joshenlimjordienr
andauthored
Chore/custom content hook (supabase#38073)
* Init custom content hook * Implement useCustomContent hook similarly to useIsFeatureEnabled, and implement extension of organization documents * Attempt to type things nicely * add test --------- Co-authored-by: Jordi Enric <[email protected]>
1 parent e65c8f0 commit bc1c050

File tree

8 files changed

+197
-0
lines changed

8 files changed

+197
-0
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import {
2+
ScaffoldContainer,
3+
ScaffoldSection,
4+
ScaffoldSectionContent,
5+
ScaffoldSectionDetail,
6+
} from 'components/layouts/Scaffold'
7+
import { CustomContentTypes } from 'hooks/custom-content/CustomContent.types'
8+
import { ExternalLink } from 'lucide-react'
9+
import { Button } from 'ui'
10+
11+
interface CustomDocumentProps {
12+
doc: CustomContentTypes['organizationLegalDocuments'][number]
13+
}
14+
15+
export const CustomDocument = ({ doc }: CustomDocumentProps) => {
16+
return (
17+
<ScaffoldContainer id={doc.id}>
18+
<ScaffoldSection>
19+
<ScaffoldSectionDetail className="sticky top-12 flex flex-col gap-y-8">
20+
<p className="text-base m-0">{doc.name}</p>
21+
<p className="text-sm text-foreground-light m-0">{doc.description}</p>
22+
</ScaffoldSectionDetail>
23+
<ScaffoldSectionContent className="flex items-center justify-center h-full">
24+
<Button asChild type="default" iconRight={<ExternalLink />}>
25+
<a download href={doc.action.url} target="_blank" rel="noreferrer noopener">
26+
{doc.action.text}
27+
</a>
28+
</Button>
29+
</ScaffoldSectionContent>
30+
</ScaffoldSection>
31+
</ScaffoldContainer>
32+
)
33+
}

apps/studio/components/interfaces/Organization/Documents/Documents.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,29 @@
11
import Link from 'next/link'
22

33
import { ScaffoldContainer, ScaffoldDivider, ScaffoldSection } from 'components/layouts/Scaffold'
4+
import { useCustomContent } from 'hooks/custom-content/useCustomContent'
5+
import { Fragment } from 'react'
6+
import { CustomDocument } from './CustomDocument'
47
import { DPA } from './DPA'
58
import { HIPAA } from './HIPAA'
69
import { SecurityQuestionnaire } from './SecurityQuestionnaire'
710
import { SOC2 } from './SOC2'
811
import { TIA } from './TIA'
912

1013
const Documents = () => {
14+
const { organizationLegalDocuments } = useCustomContent(['organization:legal_documents'])
15+
16+
if (Array.isArray(organizationLegalDocuments)) {
17+
return organizationLegalDocuments.map((doc, idx) => {
18+
return (
19+
<Fragment key={doc.id}>
20+
<CustomDocument doc={doc} />
21+
{idx !== organizationLegalDocuments.length - 1 && <ScaffoldDivider />}
22+
</Fragment>
23+
)
24+
})
25+
}
26+
1127
return (
1228
<>
1329
<ScaffoldContainer id="dpa">
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export type CustomContentTypes = {
2+
organizationLegalDocuments: {
3+
id: string
4+
name: string
5+
description: string
6+
action: { text: string; url: string }
7+
}[]
8+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"$schema": "./custom-content.schema.json",
3+
4+
"organization:legal_documents": null
5+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"$schema": "./custom-content.schema.json",
3+
4+
"organization:legal_documents": [
5+
{
6+
"id": "doc1",
7+
"name": "Document 1",
8+
"description": "This is a description of Document 1",
9+
"action": {
10+
"text": "Download document",
11+
"url": "https://supabase.com"
12+
}
13+
},
14+
{
15+
"id": "doc2",
16+
"name": "Document 2",
17+
"description": "This is a description of Document 2",
18+
"action": {
19+
"text": "Download document",
20+
"url": "https://supabase.com"
21+
}
22+
}
23+
]
24+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"type": "object",
4+
"properties": {
5+
"$schema": {
6+
"type": "string"
7+
},
8+
9+
"organization:legal_documents": {
10+
"type": ["array", "null"],
11+
"description": "Renders a provided set of documents under the organization legal documents page",
12+
"items": {
13+
"type": "object",
14+
"properties": {
15+
"id": { "type": "string" },
16+
"name": { "type": "string" },
17+
"description": { "type": "string" },
18+
"action": {
19+
"type": "object",
20+
"properties": {
21+
"text": { "type": "string" },
22+
"url": { "type": "string" }
23+
}
24+
}
25+
}
26+
}
27+
}
28+
},
29+
"required": ["organization:legal_documents"],
30+
"additionalProperties": false
31+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { renderHook, cleanup } from '@testing-library/react'
2+
import { beforeEach, describe, expect, it, vi } from 'vitest'
3+
4+
beforeEach(() => {
5+
vi.clearAllMocks()
6+
vi.resetModules()
7+
cleanup()
8+
})
9+
10+
describe('useCustomContent', () => {
11+
it('should return null if content is not found in the custom-content.json file', async () => {
12+
vi.doMock('./custom-content.json', () => ({
13+
default: {
14+
'organization:legal_documents': null,
15+
},
16+
}))
17+
18+
const { useCustomContent } = await import('./useCustomContent')
19+
const { result } = renderHook(() => useCustomContent(['organization:legal_documents']))
20+
expect(result.current.organizationLegalDocuments).toEqual(null)
21+
})
22+
23+
it('should return the content for the key passed in if it exists in the custom-content.json file', async () => {
24+
vi.doMock('./custom-content.json', () => ({
25+
default: {
26+
'organization:legal_documents': {
27+
someValue: 'foo',
28+
},
29+
},
30+
}))
31+
32+
const { useCustomContent } = await import('./useCustomContent')
33+
const { result } = renderHook(() => useCustomContent(['organization:legal_documents']))
34+
expect(result.current.organizationLegalDocuments).toEqual({ someValue: 'foo' })
35+
})
36+
})
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import customContentRaw from './custom-content.json'
2+
import { CustomContentTypes } from './CustomContent.types'
3+
4+
// [Joshen] See if we can de-dupe any of the logic here with enabled-features
5+
// For now just getting something working going first
6+
// Also not sure if CustomContentTypes is the right way to go here with trying to dynamically type
7+
8+
const customContentStaticObj = customContentRaw as Omit<typeof customContentRaw, '$schema'>
9+
type CustomContent = keyof typeof customContentStaticObj
10+
11+
type SnakeToCamelCase<S extends string> = S extends `${infer First}_${infer Rest}`
12+
? `${First}${SnakeToCamelCase<Capitalize<Rest>>}`
13+
: S
14+
15+
type CustomContentToCamelCase<S extends CustomContent> = S extends `${infer P}:${infer R}`
16+
? `${SnakeToCamelCase<P>}${Capitalize<SnakeToCamelCase<R>>}`
17+
: SnakeToCamelCase<S>
18+
19+
function contentToCamelCase(feature: CustomContent) {
20+
return feature
21+
.replace(/:/g, '_')
22+
.split('_')
23+
.map((word, index) => (index === 0 ? word : word[0].toUpperCase() + word.slice(1)))
24+
.join('') as CustomContentToCamelCase<typeof feature>
25+
}
26+
27+
const useCustomContent = <T extends CustomContent[]>(
28+
contents: T
29+
): {
30+
[key in CustomContentToCamelCase<T[number]>]:
31+
| (typeof customContentStaticObj)[CustomContent]
32+
| CustomContentTypes[CustomContentToCamelCase<T[number]>]
33+
} => {
34+
// [Joshen] Running into some TS errors without the `as` here - must be overlooking something super simple
35+
return Object.fromEntries(
36+
contents.map((content) => [contentToCamelCase(content), customContentStaticObj[content]])
37+
) as {
38+
[key in CustomContentToCamelCase<T[number]>]:
39+
| (typeof customContentStaticObj)[CustomContent]
40+
| CustomContentTypes[CustomContentToCamelCase<T[number]>]
41+
}
42+
}
43+
44+
export { useCustomContent }

0 commit comments

Comments
 (0)