-
Notifications
You must be signed in to change notification settings - Fork 432
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
Changes from all commits
ff0e9e1
01ad6fe
1fd7376
c85934b
36a4013
8a0b624
70d738f
2ef4b67
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import {Card, Flex, Stack, Text} from '@sanity/ui' | ||
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) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
import {COLOR_HUES} from '@sanity/color' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TIL, thanks. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added 👍 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)`, | ||
}} | ||
> | ||
| ||
</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> | ||
) | ||
} |
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')), | ||
}, | ||
], | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './useBundlesStore' |
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 | ||
} | ||
} |
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 | ||
} |
There was a problem hiding this comment.
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.