Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(corel): integrate bundles store #7040

Merged
merged 8 commits into from
Jul 1, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {Card, Flex, Stack, Text} from '@sanity/ui'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This story will be useful for testing purposes.

import {type ComponentType, type FormEvent, useCallback, useState} from 'react'

import {Button} from '../../../../ui-components'
import {LoadingBlock} from '../../../components/loadingBlock/LoadingBlock'
import {AddonDatasetProvider} from '../../../studio/addonDataset/AddonDatasetProvider'
import {type BundleDocument} from '../types'
import {useBundleOperations} from '../useBundleOperations'
import {useBundlesStore} from '../useBundlesStore'
import {ReleaseForm} from './ReleaseForm'

const WithAddonDatasetProvider = <P extends object>(Component: ComponentType<P>): React.FC<P> => {
const WrappedComponent: React.FC<P> = (props) => (
<AddonDatasetProvider>
<Component {...props} />
</AddonDatasetProvider>
)
WrappedComponent.displayName = `WithAddonDatasetProvider(${Component.displayName || Component.name || 'Component'})`

return WrappedComponent
}

const initialValue = {name: '', title: '', tone: undefined, publishAt: undefined}
const BundlesStoreStory = () => {
const {data, loading} = useBundlesStore()
const {createBundle, deleteBundle} = useBundleOperations()
const [creating, setCreating] = useState(false)
const [deleting, setDeleting] = useState<string | null>(null)
const [value, setValue] = useState<Partial<BundleDocument>>(initialValue)
const handleCreateBundle = useCallback(
async (event: FormEvent<HTMLFormElement>) => {
try {
event.preventDefault()
setCreating(true)
await createBundle(value)
setValue(initialValue)
} catch (err) {
console.error(err)
} finally {
setCreating(false)
}
},
[createBundle, value],
)

const handleDeleteBundle = useCallback(
async (id: string) => {
try {
setDeleting(id)
await deleteBundle(id)
} catch (err) {
console.error(err)
} finally {
setDeleting(null)
}
},
[deleteBundle],
)

return (
<Stack space={3}>
<Flex gap={2}>
<Card margin={3} padding={3} border>
<form onSubmit={handleCreateBundle}>
<Stack space={4}>
<Text weight="medium">Create a new release</Text>
<ReleaseForm onChange={setValue} value={value} />
<Flex justify="flex-end">
<Button
text="Create"
tone="primary"
type="submit"
disabled={creating}
loading={creating}
/>
</Flex>
</Stack>
</form>
</Card>
<Card margin={3} border padding={3}>
<div style={{maxHeight: '400px', overflow: 'scroll'}}>
<Text>Data</Text>
{loading ? <LoadingBlock /> : <pre>{JSON.stringify(data, null, 2)}</pre>}
</div>
</Card>
</Flex>
<Card margin={3} border padding={3}>
<Stack space={3}>
{data?.map((bundle) => (
<Card key={bundle._id} padding={3} border radius={3}>
<Flex align="center" gap={3} justify={'space-between'}>
<Text>{bundle.name}</Text>
<Button
text="Delete"
tone="critical"
// eslint-disable-next-line react/jsx-no-bind
onClick={() => handleDeleteBundle(bundle._id)}
disabled={deleting === bundle._id}
loading={deleting === bundle._id}
/>
</Flex>
</Card>
))}
</Stack>
</Card>
</Stack>
)
}

export default WithAddonDatasetProvider(BundlesStoreStory)
133 changes: 133 additions & 0 deletions packages/sanity/src/core/store/bundles/__workshop__/ReleaseForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import {COLOR_HUES} from '@sanity/color'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a copy from the prototype, that's why I'm adding it in __workshop__

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To add a note for future proof: we spoke about this on slack, this has changed slightly (naming and structure) but we can update this once both our PRs are merged

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

import {CalendarIcon} from '@sanity/icons'
import {
Box,
type ButtonTone,
Card,
Flex,
Select,
Stack,
Text,
TextArea,
TextInput,
} from '@sanity/ui'
import speakingurl from 'speakingurl'

import {Button} from '../../../../ui-components/button'
import {type BundleDocument} from '../types'

function toSlug(value: string): string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not super important, but we already include SpeakingURL with Studio for the default slugify function. It might be more robust than doing it ourselves.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL, thanks.
@RitaDias we should consider this once we merge the forms

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll update this on my PR just so we use it already 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added 👍
Unrelated to this comment: but related to the Hues, I'll update the UI to use the hues instead of tones once I start using this store.

Thanks 🙏

return speakingurl(value, {truncate: 200, symbols: true})
}

/**
* Copy from Prototype, not a final or complete working implementation.
*/
export function ReleaseForm(props: {
onChange: (params: Partial<BundleDocument>) => void
value: Partial<BundleDocument>
}) {
const {onChange, value} = props

const handleReleaseTitleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const v = event.target.value

onChange({...value, title: v, name: toSlug(v)})
}

const handleReleaseDescriptionChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
const v = event.target.value

onChange({...value, description: v || undefined})
}

const handleReleaseToneChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
onChange({...value, tone: (event.target.value || undefined) as ButtonTone | undefined})
}

const handleReleasePublishAtChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const v = event.target.value

onChange({...value, publishAt: v})
}

return (
<Stack space={5}>
<Stack space={3}>
<Text size={1} weight="medium">
Title
</Text>
<TextInput fontSize={3} onChange={handleReleaseTitleChange} value={value.title} />
</Stack>

<Stack space={3}>
<Text size={1} weight="medium">
Description
</Text>
<TextArea onChange={handleReleaseDescriptionChange} value={value.description} />
</Stack>

<Stack hidden space={3}>
<Text size={1} weight="medium">
Schedule for publishing at
</Text>
<TextInput
onChange={handleReleasePublishAtChange}
suffix={
<Box padding={1} style={{border: '1px solid transparent'}}>
<Button icon={CalendarIcon} mode="bleed" />
</Box>
}
value={value.publishAt || ''}
/>
</Stack>

<Stack space={3}>
<Text size={1} weight="medium">
Color
</Text>
<Flex>
<Card
borderTop
borderLeft
borderBottom
flex="none"
radius={2}
padding={2}
style={{
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
}}
>
<div
style={{
borderRadius: 1,
width: 17,
height: 17,
backgroundColor: `var(--card-avatar-${value.tone || 'gray'}-bg-color)`,
}}
>
&nbsp;
</div>
</Card>
<Stack flex={1}>
<Select
onChange={handleReleaseToneChange}
style={{
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
}}
value={value.tone || ''}
>
{COLOR_HUES.map((hue) => (
<option key={hue} value={hue === 'gray' ? '' : hue}>
{hue}
</option>
))}
</Select>
</Stack>
</Flex>
</Stack>
</Stack>
)
}
14 changes: 14 additions & 0 deletions packages/sanity/src/core/store/bundles/__workshop__/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {defineScope} from '@sanity/ui-workshop'
import {lazy} from 'react'

export default defineScope({
name: 'core/bundles',
title: 'bundles',
stories: [
{
name: 'bundles-store',
title: 'BundlesStore',
component: lazy(() => import('./BundlesStoreStory')),
},
],
})
1 change: 1 addition & 0 deletions packages/sanity/src/core/store/bundles/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useBundlesStore'
109 changes: 109 additions & 0 deletions packages/sanity/src/core/store/bundles/reducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import {type BundleDocument} from './types'

interface BundleAddedAction {
payload: BundleDocument
type: 'BUNDLE_ADDED'
}

interface BundleDeletedAction {
id: string
type: 'BUNDLE_DELETED'
}

interface BundleUpdatedAction {
payload: BundleDocument
type: 'BUNDLE_UPDATED'
}

interface BundlesSetAction {
payload: BundleDocument[]
type: 'BUNDLES_SET'
}

interface BundleReceivedAction {
payload: BundleDocument
type: 'BUNDLE_RECEIVED'
}

export type bundlesReducerAction =
| BundleAddedAction
| BundleDeletedAction
| BundleUpdatedAction
| BundlesSetAction
| BundleReceivedAction

export interface bundlesReducerState {
bundles: Map<string, BundleDocument>
}

function createBundlesSet(bundles: BundleDocument[]) {
const bundlesById = bundles.reduce((acc, bundle) => {
acc.set(bundle._id, bundle)
return acc
}, new Map<string, BundleDocument>())
return bundlesById
}

export function bundlesReducer(
state: bundlesReducerState,
action: bundlesReducerAction,
): bundlesReducerState {
switch (action.type) {
case 'BUNDLES_SET': {
// Create an object with the BUNDLE id as key
const bundlesById = createBundlesSet(action.payload)

return {
...state,
bundles: bundlesById,
}
}

case 'BUNDLE_ADDED': {
const addedBundle = action.payload as BundleDocument
const currentBundles = new Map(state.bundles)
currentBundles.set(addedBundle._id, addedBundle)

return {
...state,
bundles: currentBundles,
}
}

case 'BUNDLE_RECEIVED': {
const receivedBundle = action.payload as BundleDocument
const currentBundles = new Map(state.bundles)
currentBundles.set(receivedBundle._id, receivedBundle)

return {
...state,
bundles: currentBundles,
}
}

case 'BUNDLE_DELETED': {
const currentBundles = new Map(state.bundles)
currentBundles.delete(action.id)

return {
...state,
bundles: currentBundles,
}
}

case 'BUNDLE_UPDATED': {
const updatedBundle = action.payload
const id = updatedBundle._id as string
const currentBundles = new Map(state.bundles)
currentBundles.set(id, updatedBundle)

return {
...state,
bundles: currentBundles,
}
}

default:
return state
}
}
12 changes: 12 additions & 0 deletions packages/sanity/src/core/store/bundles/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {type SanityDocument} from '@sanity/types'
import {type ButtonTone} from '@sanity/ui'

export interface BundleDocument extends SanityDocument {
_type: 'bundle'
title: string
name: string
description?: string
tone?: ButtonTone
icon?: string
authorId: string
}
Loading
Loading