Skip to content

Commit

Permalink
feat(cms): setup many to many relationship field on form
Browse files Browse the repository at this point in the history
  • Loading branch information
bahdcoder committed Dec 9, 2021
1 parent 5f11116 commit 1be2619
Show file tree
Hide file tree
Showing 12 changed files with 354 additions and 114 deletions.
5 changes: 3 additions & 2 deletions examples/typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import {
select,
boolean,
dateTime,
json
json,
hasMany
} from '@tensei/core'

export default tensei()
Expand All @@ -45,7 +46,7 @@ export default tensei()
belongsToMany('Product Option'),
belongsToMany('Order Item'),
belongsToMany('Collection'),
belongsToMany('Review'),
hasMany('Review'),
files('Image')
])
.displayField('Name'),
Expand Down
8 changes: 4 additions & 4 deletions packages/cms/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import FormNumber from './form/number'
import FormSlug from './form/slug'
import FormBoolean from './form/boolean'
import FormSelect from './form/select'
import FormJson from './form/json'
import { BelongsToMany as FormBelongsToMany } from './form/belongs-to-many'

// Index

Expand All @@ -34,7 +34,7 @@ class Core {
registered = ___tensei___.shouldShowRegistrationScreen === 'false'

resources.forEach((resource: any) => {
resourcesMap[resource.slug] = resource
resourcesMap[resource.name] = resource
})

admin = JSON.parse(___tensei___.admin || '')
Expand Down Expand Up @@ -74,11 +74,11 @@ class Core {
form: {
Slug: FormSlug,
Text: FormText,
Json: FormJson,
Select: FormSelect,
Integer: FormNumber,
Boolean: FormBoolean,
Textarea: FormTextarea
Textarea: FormTextarea,
ManyToMany: FormBelongsToMany
},
index: {}
}
Expand Down
181 changes: 181 additions & 0 deletions packages/cms/form/belongs-to-many/belongs-to-many.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import React, { useEffect } from 'react'
import styled from 'styled-components'
import { FormComponentProps } from '@tensei/components'
import {
EuiButtonEmpty,
EuiButton,
EuiButtonIcon
} from '@tensei/eui/lib/components/button'
import { EuiBadge } from '@tensei/eui/lib/components/badge'
import { EuiText } from '@tensei/eui/lib/components/text'
import { EuiTitle } from '@tensei/eui/lib/components/title'
import { EuiFlexGroup, EuiFlexItem } from '@tensei/eui/lib/components/flex'
import { useGeneratedHtmlId } from '@tensei/eui/lib/services'
import {
EuiFlyout,
EuiFlyoutHeader,
EuiFlyoutBody,
EuiFlyoutFooter
} from '@tensei/eui/lib/components/flyout'
import { useState } from 'react'
import { Resource } from '../../pages/resources/resource'

const Wrapper = styled.div`
display: flex;
flex-direction: column;
width: 100%;
gap: 6px;
`

const SelectedDocument = styled.div`
${({ theme }) => `
border: ${theme.border.thin};
padding: 8px 12px;
width: 100%;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: space-between;
`}
`

const ResourceNameWrapper = styled.div`
display: flex;
align-items: center;
gap: 8px;
`

const DocumentActions = styled.div`
gap: 8px;
display: flex;
align-items: center;
`

export const BelongsToMany: React.FunctionComponent<FormComponentProps> = ({
field,
onChange,
editingId,
editing,
resource
}) => {
const [documents, setDocuments] = useState<any[]>([])
const [selectedItems, setSelectedItems] = useState<any[]>([])
const [flyOutOpen, setFlyoutOpen] = useState(false)
const flyOutId = useGeneratedHtmlId()
const relatedResource = window.Tensei.state.resourcesMap[field.name]

function closeFlyout() {
setFlyoutOpen(false)
}

async function fetchDocuments() {
if (!editing || !editingId) {
return
}

const [response, error] = await window.Tensei.api.get(
`${resource?.slugPlural}/${editingId}/${relatedResource?.slugPlural}`
)

if (!error) {
setDocuments(response?.data.data)
}
}

useEffect(() => {
onChange(documents.map(document => document.id))
}, [documents])

useEffect(() => {
fetchDocuments()
}, [])

return (
<>
{flyOutOpen ? (
<EuiFlyout
ownFocus
onClose={closeFlyout}
size={'l'}
aria-labelledby={flyOutId}
>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2 id={flyOutId}>Add existing {relatedResource?.label}</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<Resource
resource={relatedResource}
tableProps={{ onSelect: setSelectedItems }}
/>
</EuiFlyoutBody>

<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
iconType="cross"
onClick={closeFlyout}
flush="left"
>
Close
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
disabled={selectedItems.length === 0}
onClick={() => {
setDocuments(selectedItems)
closeFlyout()
}}
fill
>
Add selected {relatedResource?.label}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
) : null}
<Wrapper>
{documents.map(item => (
<SelectedDocument key={item.id}>
<ResourceNameWrapper>
<EuiBadge color="hollow">{relatedResource?.name}</EuiBadge>

<EuiText size="s">
{item[relatedResource?.displayFieldSnakeCase]}
</EuiText>
</ResourceNameWrapper>

<DocumentActions>
<EuiBadge color="success">Published</EuiBadge>

<EuiButtonIcon
iconType="trash"
color="accent"
onClick={() => {
setDocuments(
documents.filter(document => document.id !== item.id)
)
}}
aria-label={`Remove ${relatedResource?.label}`}
/>
</DocumentActions>
</SelectedDocument>
))}

<div>
<EuiButtonEmpty
onClick={() => setFlyoutOpen(true)}
size="xs"
iconType="link"
>
Add existing {relatedResource?.label}
</EuiButtonEmpty>
</div>
</Wrapper>
</>
)
}
1 change: 1 addition & 0 deletions packages/cms/form/belongs-to-many/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { BelongsToMany } from './belongs-to-many'
2 changes: 2 additions & 0 deletions packages/cms/load-icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { icon as EuiArrowRightIcon } from '@tensei/eui/lib/components/icon/asset
import { icon as EuiExitIcon } from '@tensei/eui/lib/components/icon/assets/exit'
import { icon as EuiUserIcon } from '@tensei/eui/lib/components/icon/assets/user'
import { icon as EuiPlusIcon } from '@tensei/eui/lib/components/icon/assets/plus'
import { icon as EuiLinkIcon } from '@tensei/eui/lib/components/icon/assets/link'
import { icon as EuiPlusInCircleIcon } from '@tensei/eui/lib/components/icon/assets/plus_in_circle'
import { icon as EuiPencilIcon } from '@tensei/eui/lib/components/icon/assets/pencil'

Expand All @@ -45,6 +46,7 @@ appendIconComponentCache({
exit: EuiExitIcon,
user: EuiUserIcon,
plus: EuiPlusIcon,
link: EuiLinkIcon,
plusInCircle: EuiPlusInCircleIcon,
pencil: EuiPencilIcon
})
4 changes: 2 additions & 2 deletions packages/cms/pages/components/dashboard/routes/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Route } from 'react-router-dom'
import { NoAsset } from '../../../assests/asset-manager'
import { AssetManager } from '../../../assests/asset-manager'
import { Dashboard } from '../../../dashboard'
import { Resource, ResourceForm } from '../../../resources'
import { Resource, ResourceForm, ResourceView } from '../../../resources'
import { MustBeAuthComponent } from '../../auth/guards/must-be-authenticated'

export const DashboardRoutes: React.FunctionComponent = () => {
Expand All @@ -22,7 +22,7 @@ export const DashboardRoutes: React.FunctionComponent = () => {
/>
<Route
exact
component={MustBeAuthComponent(Resource)}
component={MustBeAuthComponent(ResourceView)}
path={window.Tensei.getPath('resources/:resource')}
/>
<Route
Expand Down
2 changes: 1 addition & 1 deletion packages/cms/pages/resources/index.tsx
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { ResourceForm } from './resource-form'
export { Resource } from './resource'
export { Resource, ResourceView } from './resource'
25 changes: 18 additions & 7 deletions packages/cms/pages/resources/resource-form/resource-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ export function resolveDefaultFormValues(
return form
}

export const ResourceFormWrapper: React.FunctionComponent = ({ children }) => {
export const ResourceFormWrapper: React.FunctionComponent = () => {
const { findResource, fetchResourceData, resource } = useResourceStore()
const { push } = useHistory()
const { toast } = useToastStore()
Expand All @@ -244,8 +244,11 @@ export const ResourceFormWrapper: React.FunctionComponent = ({ children }) => {
const isEditing =
match.path === window.Tensei.getPath('resources/:resource/:id/edit')

const getData = async () => {
const [response, error] = await fetchResourceData(resourceId)
const getData = async (foundResource: ResourceContract) => {
const [response, error] = await fetchResourceData(
foundResource!,
resourceId
)

if (response?.data?.data) {
setResourceData(response?.data.data)
Expand All @@ -257,7 +260,7 @@ export const ResourceFormWrapper: React.FunctionComponent = ({ children }) => {
'We could not find the resource to edit.'
)

window.Tensei.getPath(`/resources/${resource?.slugPlural}`)
window.Tensei.getPath(`/resources/${foundResource?.slugPlural}`)
}
}

Expand All @@ -266,14 +269,18 @@ export const ResourceFormWrapper: React.FunctionComponent = ({ children }) => {

if (!found) {
push(window.Tensei.getPath(''))

return
}

if (isEditing) {
if (!resourceId) {
push(window.Tensei.getPath(`resources/${found?.slugPlural}`))

return
}

getData()
getData(found)
}
}, [resourceSlug])

Expand Down Expand Up @@ -301,7 +308,9 @@ export const ResourceForm: React.FunctionComponent<{
const { form, errors, submit, loading, setValue } = useForm<AbstractData>({
defaultValues: resolveDefaultFormValues(resource!, resourceData, isEditing),
onSubmit: (form: AbstractData) =>
isEditing ? updateResource(resourceId, form) : createResource(form),
isEditing
? updateResource(resource!, resourceId, form)
: createResource(resource!, form),
onSuccess: () => {
if (isEditing) {
toast(
Expand Down Expand Up @@ -393,7 +402,7 @@ export const ResourceForm: React.FunctionComponent<{
>
<EuiFormRow
fullWidth
label={field.name}
label={field.label || field.name}
helpText={field.description}
error={errors?.[field.inputName]}
isInvalid={!!errors?.[field.inputName]}
Expand All @@ -411,6 +420,8 @@ export const ResourceForm: React.FunctionComponent<{
onFocus={() => {
setActiveField(field)
}}
editing={isEditing}
editingId={resourceId}
activeField={activeField}
error={errors?.[field.inputName] as string}
onChange={(value: any) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/cms/pages/resources/resource/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { Resource } from './resource'
export { Resource, ResourceView } from './resource'
Loading

0 comments on commit 1be2619

Please sign in to comment.