sanity-plugin-media 4.3.6 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -17
- package/dist/index.cjs +0 -4721
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -239
- package/dist/index.d.cts.map +0 -1
- package/sanity.json +0 -8
- package/src/__tests__/fixtures/createEpicTestStore.ts +0 -28
- package/src/__tests__/fixtures/listenMock.ts +0 -9
- package/src/__tests__/fixtures/mockSanityClient.ts +0 -84
- package/src/__tests__/fixtures/renderWithProviders.tsx +0 -55
- package/src/__tests__/fixtures/rootState.ts +0 -27
- package/src/__tests__/fixtures/withinDialog.ts +0 -28
- package/src/components/AssetGridVirtualized/index.tsx +0 -94
- package/src/components/AssetMetadata/index.tsx +0 -122
- package/src/components/AssetTableVirtualized/index.tsx +0 -73
- package/src/components/AutoTagInputWrapper/index.tsx +0 -85
- package/src/components/Browser/Browser.test.tsx +0 -45
- package/src/components/Browser/index.tsx +0 -90
- package/src/components/Browser/useBrowserInit.ts +0 -126
- package/src/components/ButtonAssetCopy/index.tsx +0 -65
- package/src/components/ButtonViewGroup/index.tsx +0 -39
- package/src/components/CardAsset/CardAsset.test.tsx +0 -323
- package/src/components/CardAsset/index.tsx +0 -290
- package/src/components/CardUpload/index.tsx +0 -161
- package/src/components/Controls/index.tsx +0 -136
- package/src/components/DebugControls/index.tsx +0 -80
- package/src/components/Dialog/index.tsx +0 -11
- package/src/components/DialogAssetEdit/Details.tsx +0 -181
- package/src/components/DialogAssetEdit/DialogAssetEdit.test.tsx +0 -216
- package/src/components/DialogAssetEdit/index.tsx +0 -493
- package/src/components/DialogConfirm/index.tsx +0 -90
- package/src/components/DialogSearchFacets/index.tsx +0 -42
- package/src/components/DialogTagCreate/DialogTagCreate.test.tsx +0 -121
- package/src/components/DialogTagCreate/index.tsx +0 -111
- package/src/components/DialogTagEdit/DialogTagEdit.test.tsx +0 -165
- package/src/components/DialogTagEdit/index.tsx +0 -201
- package/src/components/DialogTags/index.tsx +0 -45
- package/src/components/Dialogs/index.tsx +0 -76
- package/src/components/DocumentList/index.tsx +0 -62
- package/src/components/FileAssetPreview/index.tsx +0 -37
- package/src/components/FileIcon/index.tsx +0 -43
- package/src/components/FormBuilderTool/FormBuilderTool.test.tsx +0 -63
- package/src/components/FormBuilderTool/index.tsx +0 -69
- package/src/components/FormFieldInputLabel/index.tsx +0 -66
- package/src/components/FormFieldInputTags/index.tsx +0 -98
- package/src/components/FormFieldInputText/index.tsx +0 -41
- package/src/components/FormFieldInputTextarea/index.tsx +0 -43
- package/src/components/FormSubmitButton/index.tsx +0 -59
- package/src/components/Header/index.tsx +0 -80
- package/src/components/Image/index.tsx +0 -41
- package/src/components/Items/index.tsx +0 -68
- package/src/components/Notifications/index.tsx +0 -24
- package/src/components/OrderSelect/index.tsx +0 -66
- package/src/components/PickedBar/index.tsx +0 -77
- package/src/components/Progress/index.tsx +0 -38
- package/src/components/ReduxProvider/index.tsx +0 -96
- package/src/components/SearchFacet/index.tsx +0 -66
- package/src/components/SearchFacetNumber/index.tsx +0 -133
- package/src/components/SearchFacetSelect/index.tsx +0 -110
- package/src/components/SearchFacetString/index.tsx +0 -88
- package/src/components/SearchFacetTags/index.tsx +0 -121
- package/src/components/SearchFacets/index.tsx +0 -72
- package/src/components/SearchFacetsControl/index.tsx +0 -140
- package/src/components/TableHeader/index.tsx +0 -110
- package/src/components/TableHeaderItem/index.tsx +0 -61
- package/src/components/TableRowAsset/index.tsx +0 -419
- package/src/components/TableRowUpload/index.tsx +0 -164
- package/src/components/Tag/index.tsx +0 -200
- package/src/components/TagIcon/index.tsx +0 -22
- package/src/components/TagView/index.tsx +0 -39
- package/src/components/TagViewHeader/index.tsx +0 -70
- package/src/components/TagsPanel/index.tsx +0 -40
- package/src/components/TagsVirtualized/index.tsx +0 -160
- package/src/components/TextInputNumber/index.tsx +0 -32
- package/src/components/TextInputSearch/index.tsx +0 -60
- package/src/components/Tool/index.tsx +0 -13
- package/src/components/UploadDropzone/UploadDropzone.test.tsx +0 -40
- package/src/components/UploadDropzone/index.tsx +0 -173
- package/src/config/orders.ts +0 -28
- package/src/config/searchFacets.ts +0 -312
- package/src/constants.ts +0 -87
- package/src/contexts/AssetSourceDispatchContext.tsx +0 -38
- package/src/contexts/DropzoneDispatchContext.tsx +0 -32
- package/src/contexts/ToolOptionsContext.tsx +0 -66
- package/src/formSchema/index.test.ts +0 -56
- package/src/formSchema/index.ts +0 -39
- package/src/hooks/useBreakpointIndex.ts +0 -50
- package/src/hooks/useKeyPress.ts +0 -39
- package/src/hooks/usePortalPopoverProps.ts +0 -13
- package/src/hooks/useTypedSelector.ts +0 -7
- package/src/hooks/useVersionedClient.ts +0 -6
- package/src/index.ts +0 -5
- package/src/modules/assets/actions.ts +0 -42
- package/src/modules/assets/deleteAndUpdateEpics.test.ts +0 -87
- package/src/modules/assets/fetchEpic.test.ts +0 -73
- package/src/modules/assets/index.ts +0 -782
- package/src/modules/assets/reducer.test.ts +0 -91
- package/src/modules/assets/tagsAndListenerEpics.test.ts +0 -206
- package/src/modules/debug/index.ts +0 -28
- package/src/modules/dialog/actions.ts +0 -10
- package/src/modules/dialog/epics.test.ts +0 -168
- package/src/modules/dialog/index.ts +0 -238
- package/src/modules/dialog/reducer.test.ts +0 -185
- package/src/modules/index.ts +0 -117
- package/src/modules/notifications/epics.test.ts +0 -374
- package/src/modules/notifications/index.ts +0 -199
- package/src/modules/notifications/reducer.test.ts +0 -54
- package/src/modules/search/index.test.ts +0 -36
- package/src/modules/search/index.ts +0 -167
- package/src/modules/selected/index.ts +0 -22
- package/src/modules/selectors.test.ts +0 -21
- package/src/modules/selectors.ts +0 -17
- package/src/modules/tags/epics.test.ts +0 -96
- package/src/modules/tags/index.test.ts +0 -42
- package/src/modules/tags/index.ts +0 -540
- package/src/modules/types.ts +0 -3
- package/src/modules/uploads/actions.ts +0 -13
- package/src/modules/uploads/epics.test.ts +0 -109
- package/src/modules/uploads/index.test.ts +0 -59
- package/src/modules/uploads/index.ts +0 -272
- package/src/operators/checkTagName.test.ts +0 -29
- package/src/operators/checkTagName.ts +0 -33
- package/src/operators/debugThrottle.ts +0 -25
- package/src/plugin.tsx +0 -54
- package/src/schemas/tag.ts +0 -28
- package/src/styled/GlobalStyles/index.tsx +0 -40
- package/src/styled/react-select/creatable.tsx +0 -184
- package/src/styled/react-select/single.tsx +0 -184
- package/src/types/index.ts +0 -346
- package/src/types/sanity-ui.d.ts +0 -5
- package/src/utils/applyMediaTags.ts +0 -87
- package/src/utils/blocksToText.test.ts +0 -43
- package/src/utils/blocksToText.ts +0 -27
- package/src/utils/constructFilter.test.ts +0 -120
- package/src/utils/constructFilter.ts +0 -98
- package/src/utils/generatePreviewBlobUrl.test.ts +0 -68
- package/src/utils/generatePreviewBlobUrl.ts +0 -53
- package/src/utils/getAssetResolution.test.ts +0 -13
- package/src/utils/getAssetResolution.ts +0 -7
- package/src/utils/getDocumentAssetIds.test.ts +0 -50
- package/src/utils/getDocumentAssetIds.ts +0 -35
- package/src/utils/getSchemeColor.test.ts +0 -12
- package/src/utils/getSchemeColor.ts +0 -43
- package/src/utils/getTagSelectOptions.test.ts +0 -44
- package/src/utils/getTagSelectOptions.ts +0 -16
- package/src/utils/getUniqueDocuments.test.ts +0 -26
- package/src/utils/getUniqueDocuments.ts +0 -15
- package/src/utils/imageDprUrl.test.ts +0 -46
- package/src/utils/imageDprUrl.ts +0 -27
- package/src/utils/isSupportedAssetType.test.ts +0 -16
- package/src/utils/isSupportedAssetType.ts +0 -15
- package/src/utils/mediaField.ts +0 -73
- package/src/utils/sanitizeFormData.test.ts +0 -59
- package/src/utils/sanitizeFormData.ts +0 -26
- package/src/utils/typeGuards.test.ts +0 -18
- package/src/utils/typeGuards.ts +0 -9
- package/src/utils/uploadSanityAsset.test.ts +0 -29
- package/src/utils/uploadSanityAsset.ts +0 -97
- package/src/utils/withMaxConcurrency.test.ts +0 -43
- package/src/utils/withMaxConcurrency.ts +0 -55
- package/src/utils/zodFormResolver.ts +0 -17
- package/v2-incompatible.js +0 -11
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
import {fireEvent, screen, waitFor} from '@testing-library/react'
|
|
2
|
-
import userEvent from '@testing-library/user-event'
|
|
3
|
-
import {Subject} from 'rxjs'
|
|
4
|
-
import {describe, expect, it, vi} from 'vitest'
|
|
5
|
-
|
|
6
|
-
import DialogAssetEdit from './index'
|
|
7
|
-
|
|
8
|
-
vi.mock('../Image', () => ({default: () => null}))
|
|
9
|
-
vi.mock('../FileAssetPreview', () => ({default: () => null}))
|
|
10
|
-
vi.mock('../DocumentList', () => ({default: () => null}))
|
|
11
|
-
vi.mock('../AssetMetadata', () => ({default: () => null}))
|
|
12
|
-
import {createMockSanityClient} from '../../__tests__/fixtures/mockSanityClient'
|
|
13
|
-
import {renderWithProviders} from '../../__tests__/fixtures/renderWithProviders'
|
|
14
|
-
import {createTestRootState} from '../../__tests__/fixtures/rootState'
|
|
15
|
-
import {inputByName, withinDialog} from '../../__tests__/fixtures/withinDialog'
|
|
16
|
-
import {assetsActions, initialState as assetsInitialState} from '../../modules/assets'
|
|
17
|
-
import type {RootReducerState} from '../../modules/types'
|
|
18
|
-
import type {AssetType, ImageAsset, MediaToolOptions} from '../../types'
|
|
19
|
-
|
|
20
|
-
const asset = {
|
|
21
|
-
_id: 'a1',
|
|
22
|
-
_type: 'sanity.imageAsset',
|
|
23
|
-
_createdAt: '',
|
|
24
|
-
_updatedAt: '',
|
|
25
|
-
_rev: 'r1',
|
|
26
|
-
originalFilename: 'x.png',
|
|
27
|
-
size: 1,
|
|
28
|
-
mimeType: 'image/png',
|
|
29
|
-
url: 'https://example.com/x.png',
|
|
30
|
-
metadata: {dimensions: {width: 100, height: 100}, isOpaque: true},
|
|
31
|
-
} as ImageAsset
|
|
32
|
-
|
|
33
|
-
const assetsPreloaded = {
|
|
34
|
-
...assetsInitialState,
|
|
35
|
-
assetTypes: ['image'] as AssetType[],
|
|
36
|
-
allIds: ['a1'],
|
|
37
|
-
byIds: {
|
|
38
|
-
a1: {_type: 'asset' as const, asset, picked: false, updating: false},
|
|
39
|
-
},
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
vi.mock('sanity', async (importOriginal) => {
|
|
43
|
-
const actual = await importOriginal<typeof import('sanity')>()
|
|
44
|
-
return {
|
|
45
|
-
...actual,
|
|
46
|
-
WithReferringDocuments: ({children}: {children: (args: unknown) => unknown}) =>
|
|
47
|
-
children({isLoading: false, referringDocuments: []}),
|
|
48
|
-
useDocumentStore: () => ({}),
|
|
49
|
-
}
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
vi.mock('../../hooks/useVersionedClient', () => ({
|
|
53
|
-
default: () =>
|
|
54
|
-
createMockSanityClient({
|
|
55
|
-
listen: vi.fn(() => new Subject()),
|
|
56
|
-
}),
|
|
57
|
-
}))
|
|
58
|
-
|
|
59
|
-
function renderAssetDialog(
|
|
60
|
-
dialog: {id: string; type: 'assetEdit'; assetId: string},
|
|
61
|
-
opts: {
|
|
62
|
-
preloaded?: Partial<RootReducerState>
|
|
63
|
-
toolOptions?: Partial<MediaToolOptions>
|
|
64
|
-
} = {},
|
|
65
|
-
) {
|
|
66
|
-
const {preloaded: extraPreloaded, toolOptions} = opts
|
|
67
|
-
return renderWithProviders(
|
|
68
|
-
<DialogAssetEdit dialog={dialog}>
|
|
69
|
-
<span />
|
|
70
|
-
</DialogAssetEdit>,
|
|
71
|
-
{
|
|
72
|
-
preloaded: {
|
|
73
|
-
assets: assetsPreloaded,
|
|
74
|
-
...extraPreloaded,
|
|
75
|
-
},
|
|
76
|
-
toolOptions: {creditLine: {enabled: true}, ...toolOptions},
|
|
77
|
-
},
|
|
78
|
-
)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
describe('DialogAssetEdit', () => {
|
|
82
|
-
it('renders asset details header and details tab', () => {
|
|
83
|
-
renderAssetDialog({
|
|
84
|
-
id: 'dlg-1',
|
|
85
|
-
type: 'assetEdit',
|
|
86
|
-
assetId: 'a1',
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
const dlg = withinDialog(/asset details/i, screen)
|
|
90
|
-
expect(dlg.getByText('Asset details')).toBeInTheDocument()
|
|
91
|
-
expect(dlg.getByRole('tab', {name: 'Details'})).toBeInTheDocument()
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
it('keeps Save disabled until a field is edited', () => {
|
|
95
|
-
renderAssetDialog({
|
|
96
|
-
id: 'dlg-1',
|
|
97
|
-
type: 'assetEdit',
|
|
98
|
-
assetId: 'a1',
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
const dlg = withinDialog(/asset details/i, screen)
|
|
102
|
-
expect(dlg.getByRole('button', {name: /save and close/i})).toBeDisabled()
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
it('dispatches asset update when a field changes and the form is submitted', async () => {
|
|
106
|
-
const user = userEvent.setup()
|
|
107
|
-
const {store} = renderAssetDialog({
|
|
108
|
-
id: 'dlg-1',
|
|
109
|
-
type: 'assetEdit',
|
|
110
|
-
assetId: 'a1',
|
|
111
|
-
})
|
|
112
|
-
const dispatchSpy = vi.spyOn(store, 'dispatch')
|
|
113
|
-
const dlg = withinDialog(/asset details/i, screen)
|
|
114
|
-
|
|
115
|
-
await user.type(inputByName(/asset details/i, screen, 'title'), 'Hero image')
|
|
116
|
-
await user.click(dlg.getByRole('button', {name: /save and close/i}))
|
|
117
|
-
|
|
118
|
-
expect(store.getState().assets.byIds['a1']!.updating).toBe(true)
|
|
119
|
-
|
|
120
|
-
await waitFor(() => {
|
|
121
|
-
let updateAction
|
|
122
|
-
for (const call of dispatchSpy.mock.calls) {
|
|
123
|
-
const action = call[0]
|
|
124
|
-
if (assetsActions.updateRequest.match(action)) {
|
|
125
|
-
updateAction = action
|
|
126
|
-
break
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
expect(updateAction).toBeDefined()
|
|
130
|
-
expect(updateAction?.payload).toMatchObject({
|
|
131
|
-
asset,
|
|
132
|
-
closeDialogId: 'a1',
|
|
133
|
-
formData: expect.objectContaining({
|
|
134
|
-
title: 'Hero image',
|
|
135
|
-
originalFilename: 'x.png',
|
|
136
|
-
}),
|
|
137
|
-
})
|
|
138
|
-
})
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
it('removes only this dialog when closed', async () => {
|
|
142
|
-
const user = userEvent.setup()
|
|
143
|
-
const base = createTestRootState({
|
|
144
|
-
dialog: {
|
|
145
|
-
items: [
|
|
146
|
-
{id: 'dlg-1', type: 'assetEdit', assetId: 'a1'},
|
|
147
|
-
{id: 'tags', type: 'tags'},
|
|
148
|
-
],
|
|
149
|
-
},
|
|
150
|
-
assets: assetsPreloaded,
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
const {store} = renderWithProviders(
|
|
154
|
-
<DialogAssetEdit
|
|
155
|
-
dialog={{
|
|
156
|
-
id: 'dlg-1',
|
|
157
|
-
type: 'assetEdit',
|
|
158
|
-
assetId: 'a1',
|
|
159
|
-
}}
|
|
160
|
-
>
|
|
161
|
-
<span />
|
|
162
|
-
</DialogAssetEdit>,
|
|
163
|
-
{
|
|
164
|
-
preloaded: base,
|
|
165
|
-
toolOptions: {creditLine: {enabled: true}},
|
|
166
|
-
},
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
const dlg = withinDialog(/asset details/i, screen)
|
|
170
|
-
await user.click(dlg.getByRole('button', {name: /close dialog/i}))
|
|
171
|
-
|
|
172
|
-
expect(store.getState().dialog.items).toEqual([{id: 'tags', type: 'tags'}])
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
it('opens the delete confirmation dialog when Delete is clicked', async () => {
|
|
176
|
-
const {store} = renderAssetDialog({
|
|
177
|
-
id: 'dlg-1',
|
|
178
|
-
type: 'assetEdit',
|
|
179
|
-
assetId: 'a1',
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
const dlg = withinDialog(/asset details/i, screen)
|
|
183
|
-
fireEvent.click(dlg.getByRole('button', {name: /^delete$/i}))
|
|
184
|
-
|
|
185
|
-
await waitFor(() => {
|
|
186
|
-
let confirm
|
|
187
|
-
for (const d of store.getState().dialog.items) {
|
|
188
|
-
if (d.type === 'confirm') {
|
|
189
|
-
confirm = d
|
|
190
|
-
break
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
expect(confirm).toBeDefined()
|
|
194
|
-
expect(confirm?.title).toMatch(/permanently delete/i)
|
|
195
|
-
expect(confirm?.headerTitle).toBe('Confirm deletion')
|
|
196
|
-
})
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
it('switches to the References tab when that tab is activated', async () => {
|
|
200
|
-
const user = userEvent.setup()
|
|
201
|
-
renderAssetDialog({
|
|
202
|
-
id: 'dlg-1',
|
|
203
|
-
type: 'assetEdit',
|
|
204
|
-
assetId: 'a1',
|
|
205
|
-
})
|
|
206
|
-
|
|
207
|
-
const dlg = withinDialog(/asset details/i, screen)
|
|
208
|
-
const referencesTab = dlg.getByRole('tab', {name: /references/i})
|
|
209
|
-
expect(referencesTab).toHaveAttribute('aria-selected', 'false')
|
|
210
|
-
|
|
211
|
-
await user.click(referencesTab)
|
|
212
|
-
|
|
213
|
-
expect(referencesTab).toHaveAttribute('aria-selected', 'true')
|
|
214
|
-
expect(dlg.getByRole('tab', {name: 'Details'})).toHaveAttribute('aria-selected', 'false')
|
|
215
|
-
})
|
|
216
|
-
})
|
|
@@ -1,493 +0,0 @@
|
|
|
1
|
-
import type {MutationEvent} from '@sanity/client'
|
|
2
|
-
import {Box, Button, Card, Flex, Stack, Tab, TabList, TabPanel, Text} from '@sanity/ui'
|
|
3
|
-
import groq from 'groq'
|
|
4
|
-
import {type ReactNode, useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
|
5
|
-
import {type SubmitHandler, useForm} from 'react-hook-form'
|
|
6
|
-
import {useDispatch} from 'react-redux'
|
|
7
|
-
import {WithReferringDocuments, useColorSchemeValue, useDocumentStore} from 'sanity'
|
|
8
|
-
|
|
9
|
-
import {useToolOptions} from '../../contexts/ToolOptionsContext'
|
|
10
|
-
import {getAssetFormSchema} from '../../formSchema'
|
|
11
|
-
import useTypedSelector from '../../hooks/useTypedSelector'
|
|
12
|
-
import useVersionedClient from '../../hooks/useVersionedClient'
|
|
13
|
-
import {assetsActions, selectAssetById} from '../../modules/assets'
|
|
14
|
-
import {dialogActions} from '../../modules/dialog'
|
|
15
|
-
import {selectTags, selectTagSelectOptions, tagsActions} from '../../modules/tags'
|
|
16
|
-
import type {Asset, AssetFormData, DialogAssetEditProps, TagSelectOption} from '../../types'
|
|
17
|
-
import getTagSelectOptions from '../../utils/getTagSelectOptions'
|
|
18
|
-
import {getUniqueDocuments} from '../../utils/getUniqueDocuments'
|
|
19
|
-
import imageDprUrl from '../../utils/imageDprUrl'
|
|
20
|
-
import sanitizeFormData from '../../utils/sanitizeFormData'
|
|
21
|
-
import {isFileAsset, isImageAsset} from '../../utils/typeGuards'
|
|
22
|
-
import zodFormResolver from '../../utils/zodFormResolver'
|
|
23
|
-
import AssetMetadata from '../AssetMetadata'
|
|
24
|
-
import Dialog from '../Dialog'
|
|
25
|
-
import DocumentList from '../DocumentList'
|
|
26
|
-
import FileAssetPreview from '../FileAssetPreview'
|
|
27
|
-
import FormSubmitButton from '../FormSubmitButton'
|
|
28
|
-
import Image from '../Image'
|
|
29
|
-
import Details, {type DetailsProps} from './Details'
|
|
30
|
-
|
|
31
|
-
function renderDefaultDetails(props: DetailsProps) {
|
|
32
|
-
return <Details {...props} />
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
type Props = {
|
|
36
|
-
children: ReactNode
|
|
37
|
-
dialog: DialogAssetEditProps
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const DialogAssetEdit = (props: Props) => {
|
|
41
|
-
const {
|
|
42
|
-
children,
|
|
43
|
-
dialog: {assetId, id, lastCreatedTag, lastRemovedTagIds},
|
|
44
|
-
} = props
|
|
45
|
-
|
|
46
|
-
const client = useVersionedClient()
|
|
47
|
-
const scheme = useColorSchemeValue()
|
|
48
|
-
|
|
49
|
-
const documentStore = useDocumentStore()
|
|
50
|
-
|
|
51
|
-
const dispatch = useDispatch()
|
|
52
|
-
const assetItem = useTypedSelector((state) => selectAssetById(state, String(assetId))) // TODO: check casting
|
|
53
|
-
const tags = useTypedSelector(selectTags)
|
|
54
|
-
|
|
55
|
-
const assetUpdatedPrev = useRef<string | undefined>(undefined)
|
|
56
|
-
|
|
57
|
-
// Generate a snapshot of the current asset
|
|
58
|
-
const [assetSnapshot, setAssetSnapshot] = useState(assetItem?.asset)
|
|
59
|
-
const [tabSection, setTabSection] = useState<'details' | 'references'>('details')
|
|
60
|
-
|
|
61
|
-
const currentAsset = assetItem ? assetItem?.asset : assetSnapshot
|
|
62
|
-
const allTagOptions = getTagSelectOptions(tags)
|
|
63
|
-
|
|
64
|
-
const assetTagOptions = useTypedSelector(selectTagSelectOptions(currentAsset))
|
|
65
|
-
|
|
66
|
-
// Check if credit line options are configured
|
|
67
|
-
const {creditLine, components: {details: CustomDetails} = {}, locales} = useToolOptions()
|
|
68
|
-
|
|
69
|
-
const generateDefaultValues = useCallback(
|
|
70
|
-
(asset?: Asset): AssetFormData => {
|
|
71
|
-
if (locales && locales.length > 0) {
|
|
72
|
-
const makeLocaleObj = (field?: Record<string, string> | string) => {
|
|
73
|
-
const obj: Record<string, string> = {}
|
|
74
|
-
for (let i = 0; i < locales.length; i++) {
|
|
75
|
-
const locale = locales[i]!
|
|
76
|
-
if (typeof field === 'object' && field && field[locale.id]) {
|
|
77
|
-
obj[locale.id] = field[locale.id]!
|
|
78
|
-
} else if (typeof field === 'string') {
|
|
79
|
-
// Only populate the first locale to avoid spreading a legacy value
|
|
80
|
-
// across all languages; the user should fill in other translations manually
|
|
81
|
-
obj[locale.id] = i === 0 ? field : ''
|
|
82
|
-
} else {
|
|
83
|
-
obj[locale.id] = ''
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return obj
|
|
87
|
-
}
|
|
88
|
-
return {
|
|
89
|
-
altText: makeLocaleObj(asset?.altText),
|
|
90
|
-
creditLine: makeLocaleObj(asset?.creditLine),
|
|
91
|
-
description: makeLocaleObj(asset?.description),
|
|
92
|
-
originalFilename: asset?.originalFilename || '',
|
|
93
|
-
opt: {media: {tags: assetTagOptions}},
|
|
94
|
-
title: makeLocaleObj(asset?.title),
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
// Normalize: if a field is a localized object but locales are disabled, pick first non-empty value
|
|
98
|
-
const flattenField = (field: unknown): string => {
|
|
99
|
-
if (typeof field === 'string') return field
|
|
100
|
-
if (typeof field === 'object' && field !== null) {
|
|
101
|
-
const values = Object.values(field as Record<string, string>)
|
|
102
|
-
return values.find((v) => v) || ''
|
|
103
|
-
}
|
|
104
|
-
return ''
|
|
105
|
-
}
|
|
106
|
-
return {
|
|
107
|
-
altText: flattenField(asset?.altText),
|
|
108
|
-
creditLine: flattenField(asset?.creditLine),
|
|
109
|
-
description: flattenField(asset?.description),
|
|
110
|
-
originalFilename: asset?.originalFilename || '',
|
|
111
|
-
opt: {media: {tags: assetTagOptions}},
|
|
112
|
-
title: flattenField(asset?.title),
|
|
113
|
-
}
|
|
114
|
-
},
|
|
115
|
-
[assetTagOptions, locales],
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
const {
|
|
119
|
-
control,
|
|
120
|
-
// Read the formState before render to subscribe the form state through Proxy
|
|
121
|
-
formState: {errors, isDirty, isValid},
|
|
122
|
-
getValues,
|
|
123
|
-
handleSubmit,
|
|
124
|
-
register,
|
|
125
|
-
reset,
|
|
126
|
-
setValue,
|
|
127
|
-
} = useForm<AssetFormData>({
|
|
128
|
-
defaultValues: generateDefaultValues(assetItem?.asset),
|
|
129
|
-
mode: 'onChange',
|
|
130
|
-
resolver: zodFormResolver<AssetFormData>(getAssetFormSchema(locales)),
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
const formUpdating = !assetItem || assetItem?.updating
|
|
134
|
-
|
|
135
|
-
const handleClose = useCallback(() => {
|
|
136
|
-
dispatch(dialogActions.remove({id}))
|
|
137
|
-
}, [dispatch, id])
|
|
138
|
-
|
|
139
|
-
const handleDelete = useCallback(() => {
|
|
140
|
-
if (!assetItem?.asset) {
|
|
141
|
-
return
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
dispatch(
|
|
145
|
-
dialogActions.showConfirmDeleteAssets({
|
|
146
|
-
assets: [assetItem],
|
|
147
|
-
closeDialogId: assetItem?.asset._id,
|
|
148
|
-
}),
|
|
149
|
-
)
|
|
150
|
-
}, [assetItem, dispatch])
|
|
151
|
-
|
|
152
|
-
const handleAssetUpdate = useCallback((update: MutationEvent) => {
|
|
153
|
-
const {result, transition} = update
|
|
154
|
-
if (result && transition === 'update') {
|
|
155
|
-
// Regenerate asset snapshot
|
|
156
|
-
setAssetSnapshot(result as Asset)
|
|
157
|
-
}
|
|
158
|
-
}, [])
|
|
159
|
-
|
|
160
|
-
const handleCreateTag = useCallback(
|
|
161
|
-
(tagName: string) => {
|
|
162
|
-
// Dispatch action to create new tag
|
|
163
|
-
dispatch(
|
|
164
|
-
tagsActions.createRequest({
|
|
165
|
-
assetId: currentAsset?._id,
|
|
166
|
-
name: tagName,
|
|
167
|
-
}),
|
|
168
|
-
)
|
|
169
|
-
},
|
|
170
|
-
[currentAsset?._id, dispatch],
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
// Detect if asset has localized fields (objects) with keys not in the configured locales
|
|
174
|
-
const hasOrphanedLocales = useMemo(() => {
|
|
175
|
-
if (!currentAsset) return false
|
|
176
|
-
const isLocaleObj = (v: unknown) => typeof v === 'object' && v !== null && !Array.isArray(v)
|
|
177
|
-
const fields = [
|
|
178
|
-
currentAsset.title,
|
|
179
|
-
currentAsset.altText,
|
|
180
|
-
currentAsset.description,
|
|
181
|
-
...(currentAsset._type === 'sanity.imageAsset' ? [currentAsset.creditLine] : []),
|
|
182
|
-
]
|
|
183
|
-
const anyLocalized = fields.some((f) => isLocaleObj(f))
|
|
184
|
-
if (!anyLocalized) return false
|
|
185
|
-
if (!locales || locales.length === 0) return true
|
|
186
|
-
const configuredIds = new Set(locales.map((l) => l.id))
|
|
187
|
-
return fields.some((f) => {
|
|
188
|
-
if (!isLocaleObj(f)) return false
|
|
189
|
-
return Object.keys(f as object).some((k) => !configuredIds.has(k))
|
|
190
|
-
})
|
|
191
|
-
}, [currentAsset, locales])
|
|
192
|
-
|
|
193
|
-
const handleCleanupLocales = useCallback(async () => {
|
|
194
|
-
if (!currentAsset) return
|
|
195
|
-
|
|
196
|
-
const cleanField = (field: unknown): unknown => {
|
|
197
|
-
if (typeof field !== 'object' || field === null || Array.isArray(field)) return field
|
|
198
|
-
const obj = field as Record<string, string>
|
|
199
|
-
if (!locales || locales.length === 0) {
|
|
200
|
-
// Pick the first non-empty value sorted by key for determinism
|
|
201
|
-
const sorted = Object.keys(obj).sort()
|
|
202
|
-
return sorted.map((k) => obj[k]).find((v) => v) || ''
|
|
203
|
-
}
|
|
204
|
-
const configuredIds = new Set(locales.map((l) => l.id))
|
|
205
|
-
const cleaned: Record<string, string> = {}
|
|
206
|
-
for (const [key, val] of Object.entries(obj)) {
|
|
207
|
-
if (configuredIds.has(key)) cleaned[key] = val
|
|
208
|
-
}
|
|
209
|
-
return cleaned
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
await client
|
|
213
|
-
.patch(currentAsset._id)
|
|
214
|
-
.set({
|
|
215
|
-
title: cleanField(currentAsset.title),
|
|
216
|
-
altText: cleanField(currentAsset.altText),
|
|
217
|
-
description: cleanField(currentAsset.description),
|
|
218
|
-
...(currentAsset._type === 'sanity.imageAsset' && {
|
|
219
|
-
creditLine: cleanField(currentAsset.creditLine),
|
|
220
|
-
}),
|
|
221
|
-
})
|
|
222
|
-
.commit()
|
|
223
|
-
}, [client, currentAsset, locales])
|
|
224
|
-
|
|
225
|
-
// Submit react-hook-form
|
|
226
|
-
const onSubmit: SubmitHandler<AssetFormData> = useCallback(
|
|
227
|
-
(formData) => {
|
|
228
|
-
if (!assetItem?.asset) {
|
|
229
|
-
return
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const sanitizedFormData = sanitizeFormData(formData)
|
|
233
|
-
|
|
234
|
-
dispatch(
|
|
235
|
-
assetsActions.updateRequest({
|
|
236
|
-
asset: assetItem?.asset,
|
|
237
|
-
closeDialogId: assetItem?.asset._id,
|
|
238
|
-
formData: {
|
|
239
|
-
...sanitizedFormData,
|
|
240
|
-
// Map tags to sanity references
|
|
241
|
-
opt: {
|
|
242
|
-
media: {
|
|
243
|
-
...sanitizedFormData['opt'].media,
|
|
244
|
-
tags:
|
|
245
|
-
sanitizedFormData['opt'].media.tags?.map((tag: TagSelectOption) => ({
|
|
246
|
-
_ref: tag.value,
|
|
247
|
-
_type: 'reference',
|
|
248
|
-
_weak: true,
|
|
249
|
-
})) || null,
|
|
250
|
-
},
|
|
251
|
-
},
|
|
252
|
-
},
|
|
253
|
-
}),
|
|
254
|
-
)
|
|
255
|
-
},
|
|
256
|
-
[assetItem?.asset, dispatch],
|
|
257
|
-
)
|
|
258
|
-
|
|
259
|
-
// Listen for asset mutations and update snapshot
|
|
260
|
-
useEffect(() => {
|
|
261
|
-
if (!assetItem?.asset) {
|
|
262
|
-
return undefined
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Remember that Sanity listeners ignore joins, order clauses and projections
|
|
266
|
-
const subscriptionAsset = client
|
|
267
|
-
.listen(groq`*[_id == $id]`, {id: assetItem?.asset._id})
|
|
268
|
-
.subscribe(handleAssetUpdate)
|
|
269
|
-
|
|
270
|
-
return () => {
|
|
271
|
-
subscriptionAsset?.unsubscribe()
|
|
272
|
-
}
|
|
273
|
-
}, [assetItem?.asset, client, handleAssetUpdate])
|
|
274
|
-
|
|
275
|
-
// Update tags form field (react-select) when a new _inline_ tag has been created
|
|
276
|
-
useEffect(() => {
|
|
277
|
-
if (lastCreatedTag) {
|
|
278
|
-
const existingTags = (getValues('opt.media.tags') as TagSelectOption[]) || []
|
|
279
|
-
const updatedTags = existingTags.concat([lastCreatedTag])
|
|
280
|
-
setValue('opt.media.tags', updatedTags, {shouldDirty: true})
|
|
281
|
-
}
|
|
282
|
-
}, [getValues, lastCreatedTag, setValue])
|
|
283
|
-
|
|
284
|
-
// Update tags form field (react-select) when an _inline_ tag has been removed elsewhere
|
|
285
|
-
useEffect(() => {
|
|
286
|
-
if (lastRemovedTagIds) {
|
|
287
|
-
const existingTags = (getValues('opt.media.tags') as TagSelectOption[]) || []
|
|
288
|
-
const updatedTags = existingTags.filter((tag) => {
|
|
289
|
-
return !lastRemovedTagIds.includes(tag.value)
|
|
290
|
-
})
|
|
291
|
-
|
|
292
|
-
setValue('opt.media.tags', updatedTags, {shouldDirty: true})
|
|
293
|
-
}
|
|
294
|
-
}, [getValues, lastRemovedTagIds, setValue])
|
|
295
|
-
|
|
296
|
-
// Reset react-hook-form local state on mount and every time the asset has been updated elsewhere
|
|
297
|
-
useEffect(() => {
|
|
298
|
-
if (assetUpdatedPrev.current !== assetItem?.asset._updatedAt) {
|
|
299
|
-
reset(generateDefaultValues(assetItem?.asset))
|
|
300
|
-
}
|
|
301
|
-
assetUpdatedPrev.current = assetItem?.asset._updatedAt
|
|
302
|
-
}, [assetItem?.asset, generateDefaultValues, reset])
|
|
303
|
-
|
|
304
|
-
const Footer = () => (
|
|
305
|
-
<Box padding={3}>
|
|
306
|
-
<Stack space={3}>
|
|
307
|
-
{hasOrphanedLocales && (
|
|
308
|
-
<Card padding={3} radius={2} shadow={1} tone="caution">
|
|
309
|
-
<Flex align="center" justify="space-between" gap={3}>
|
|
310
|
-
<Text size={1}>
|
|
311
|
-
This asset has localized fields that are no longer configured. Clean them up to
|
|
312
|
-
avoid validation errors.
|
|
313
|
-
</Text>
|
|
314
|
-
<Button
|
|
315
|
-
fontSize={1}
|
|
316
|
-
mode="ghost"
|
|
317
|
-
onClick={handleCleanupLocales}
|
|
318
|
-
text="Cleanup localized fields"
|
|
319
|
-
tone="caution"
|
|
320
|
-
/>
|
|
321
|
-
</Flex>
|
|
322
|
-
</Card>
|
|
323
|
-
)}
|
|
324
|
-
<Flex justify="space-between">
|
|
325
|
-
{/* Delete button */}
|
|
326
|
-
<Button
|
|
327
|
-
disabled={formUpdating}
|
|
328
|
-
fontSize={1}
|
|
329
|
-
mode="bleed"
|
|
330
|
-
onClick={handleDelete}
|
|
331
|
-
text="Delete"
|
|
332
|
-
tone="critical"
|
|
333
|
-
/>
|
|
334
|
-
|
|
335
|
-
{/* Submit button */}
|
|
336
|
-
<FormSubmitButton
|
|
337
|
-
disabled={formUpdating || !isDirty || !isValid || hasOrphanedLocales}
|
|
338
|
-
isValid={isValid}
|
|
339
|
-
lastUpdated={currentAsset?._updatedAt}
|
|
340
|
-
onClick={handleSubmit(onSubmit)}
|
|
341
|
-
/>
|
|
342
|
-
</Flex>
|
|
343
|
-
</Stack>
|
|
344
|
-
</Box>
|
|
345
|
-
)
|
|
346
|
-
|
|
347
|
-
if (!currentAsset) {
|
|
348
|
-
return null
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const detailsProps = {
|
|
352
|
-
control,
|
|
353
|
-
errors,
|
|
354
|
-
formUpdating,
|
|
355
|
-
register,
|
|
356
|
-
setValue,
|
|
357
|
-
assetTagOptions,
|
|
358
|
-
allTagOptions,
|
|
359
|
-
handleCreateTag,
|
|
360
|
-
currentAsset,
|
|
361
|
-
creditLine,
|
|
362
|
-
locales,
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
return (
|
|
366
|
-
<Dialog
|
|
367
|
-
animate
|
|
368
|
-
// oxlint-disable-next-line react/react-compiler
|
|
369
|
-
footer={<Footer />}
|
|
370
|
-
header="Asset details"
|
|
371
|
-
id={id}
|
|
372
|
-
onClose={handleClose}
|
|
373
|
-
width={3}
|
|
374
|
-
>
|
|
375
|
-
{/*
|
|
376
|
-
We reverse direction to ensure the download button doesn't appear (in the DOM) before other tabbable items.
|
|
377
|
-
This ensures that the dialog doesn't scroll down to the download button (which on smaller screens, can sometimes
|
|
378
|
-
be below the fold).
|
|
379
|
-
*/}
|
|
380
|
-
<Flex direction={['column-reverse', 'column-reverse', 'row-reverse']}>
|
|
381
|
-
<Box flex={1} marginTop={[5, 5, 0]} padding={4}>
|
|
382
|
-
<WithReferringDocuments documentStore={documentStore} id={currentAsset._id}>
|
|
383
|
-
{({isLoading, referringDocuments}) => {
|
|
384
|
-
const uniqueReferringDocuments = getUniqueDocuments(referringDocuments)
|
|
385
|
-
return (
|
|
386
|
-
<>
|
|
387
|
-
{/* Tabs */}
|
|
388
|
-
<TabList space={2}>
|
|
389
|
-
<Tab
|
|
390
|
-
aria-controls="details-panel"
|
|
391
|
-
disabled={formUpdating}
|
|
392
|
-
id="details-tab"
|
|
393
|
-
label="Details"
|
|
394
|
-
onClick={() => setTabSection('details')}
|
|
395
|
-
selected={tabSection === 'details'}
|
|
396
|
-
size={2}
|
|
397
|
-
/>
|
|
398
|
-
<Tab
|
|
399
|
-
aria-controls="references-panel"
|
|
400
|
-
disabled={formUpdating}
|
|
401
|
-
id="references-tab"
|
|
402
|
-
label={`References${
|
|
403
|
-
!isLoading && Array.isArray(uniqueReferringDocuments)
|
|
404
|
-
? ` (${uniqueReferringDocuments.length})`
|
|
405
|
-
: ''
|
|
406
|
-
}`}
|
|
407
|
-
onClick={() => setTabSection('references')}
|
|
408
|
-
selected={tabSection === 'references'}
|
|
409
|
-
size={2}
|
|
410
|
-
/>
|
|
411
|
-
</TabList>
|
|
412
|
-
|
|
413
|
-
{/* Form fields */}
|
|
414
|
-
<Box as="form" marginTop={4} onSubmit={handleSubmit(onSubmit)}>
|
|
415
|
-
{/* Deleted notification */}
|
|
416
|
-
{!assetItem && (
|
|
417
|
-
<Card marginBottom={3} padding={3} radius={2} shadow={1} tone="critical">
|
|
418
|
-
<Text size={1}>This file cannot be found – it may have been deleted.</Text>
|
|
419
|
-
</Card>
|
|
420
|
-
)}
|
|
421
|
-
|
|
422
|
-
{/* Hidden button to enable enter key submissions */}
|
|
423
|
-
<button style={{display: 'none'}} tabIndex={-1} type="submit" />
|
|
424
|
-
|
|
425
|
-
{/* Panel: details */}
|
|
426
|
-
<TabPanel
|
|
427
|
-
aria-labelledby="details"
|
|
428
|
-
hidden={tabSection !== 'details'}
|
|
429
|
-
id="details-panel"
|
|
430
|
-
>
|
|
431
|
-
{CustomDetails ? (
|
|
432
|
-
<CustomDetails
|
|
433
|
-
{...detailsProps}
|
|
434
|
-
renderDefaultDetails={renderDefaultDetails}
|
|
435
|
-
/>
|
|
436
|
-
) : (
|
|
437
|
-
<Details {...detailsProps} />
|
|
438
|
-
)}
|
|
439
|
-
</TabPanel>
|
|
440
|
-
|
|
441
|
-
{/* Panel: References */}
|
|
442
|
-
<TabPanel
|
|
443
|
-
aria-labelledby="references"
|
|
444
|
-
hidden={tabSection !== 'references'}
|
|
445
|
-
id="references-panel"
|
|
446
|
-
>
|
|
447
|
-
<Box marginTop={5}>
|
|
448
|
-
{assetItem?.asset && (
|
|
449
|
-
<DocumentList
|
|
450
|
-
documents={uniqueReferringDocuments}
|
|
451
|
-
isLoading={isLoading}
|
|
452
|
-
/>
|
|
453
|
-
)}
|
|
454
|
-
</Box>
|
|
455
|
-
</TabPanel>
|
|
456
|
-
</Box>
|
|
457
|
-
</>
|
|
458
|
-
)
|
|
459
|
-
}}
|
|
460
|
-
</WithReferringDocuments>
|
|
461
|
-
</Box>
|
|
462
|
-
|
|
463
|
-
<Box flex={1} padding={4}>
|
|
464
|
-
<Box style={{aspectRatio: '1'}}>
|
|
465
|
-
{/* File */}
|
|
466
|
-
{isFileAsset(currentAsset) && <FileAssetPreview asset={currentAsset} />}
|
|
467
|
-
|
|
468
|
-
{/* Image */}
|
|
469
|
-
{isImageAsset(currentAsset) && (
|
|
470
|
-
<Image
|
|
471
|
-
draggable={false}
|
|
472
|
-
$scheme={scheme}
|
|
473
|
-
$showCheckerboard={!currentAsset?.metadata?.isOpaque}
|
|
474
|
-
src={imageDprUrl(currentAsset, {height: 600, width: 600})}
|
|
475
|
-
/>
|
|
476
|
-
)}
|
|
477
|
-
</Box>
|
|
478
|
-
|
|
479
|
-
{/* Metadata */}
|
|
480
|
-
{currentAsset && (
|
|
481
|
-
<Box marginTop={4}>
|
|
482
|
-
<AssetMetadata asset={currentAsset} item={assetItem} />
|
|
483
|
-
</Box>
|
|
484
|
-
)}
|
|
485
|
-
</Box>
|
|
486
|
-
</Flex>
|
|
487
|
-
|
|
488
|
-
{children}
|
|
489
|
-
</Dialog>
|
|
490
|
-
)
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
export default DialogAssetEdit
|