sanity-plugin-smart-asset-manager 1.0.0
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/LICENSE +21 -0
- package/README.md +109 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1356 -0
- package/dist/index.mjs +1336 -0
- package/package.json +72 -0
- package/src/assets/images/all-assets.png +0 -0
- package/src/assets/images/details-popup.png +0 -0
- package/src/assets/images/size-analyzer.png +0 -0
- package/src/assets/images/unused-assets.png +0 -0
- package/src/components/AssetCard.tsx +177 -0
- package/src/components/AssetDetailsDialog.tsx +423 -0
- package/src/components/SmartAssetManagerTool.tsx +284 -0
- package/src/components/TopToolbar.tsx +129 -0
- package/src/components/__tests__/AssetCard.test.tsx +64 -0
- package/src/components/common/Icons.tsx +239 -0
- package/src/components/tabs/SizeAnalyzer.tsx +142 -0
- package/src/components/tabs/UnusedAssets.tsx +129 -0
- package/src/components/tabs/__tests__/SizeAnalyzer.test.tsx +73 -0
- package/src/hooks/useAssets.ts +75 -0
- package/src/index.ts +17 -0
- package/src/setupTests.ts +1 -0
- package/src/types/index.ts +29 -0
- package/src/utils/__tests__/formatBytes.test.ts +25 -0
- package/src/utils/assetQueries.ts +59 -0
- package/src/utils/formatBytes.ts +10 -0
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sanity-plugin-smart-asset-manager",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "An advanced asset management plugin for Sanity Studio with smart filtering, size analysis, unused asset detection, broken asset checks, and usage tracking.",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"source": "./src/index.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js",
|
|
13
|
+
"default": "./dist/index.mjs"
|
|
14
|
+
},
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --external react,react-dom,sanity,@sanity/ui,styled-components",
|
|
23
|
+
"prepublishOnly": "npm run build",
|
|
24
|
+
"format": "prettier --write .",
|
|
25
|
+
"format:check": "prettier --check .",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"test:watch": "vitest",
|
|
28
|
+
"dev": "tsup src/index.ts --format esm,cjs --watch --dts --external react,react-dom,sanity,@sanity/ui,styled-components"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"sanity",
|
|
32
|
+
"sanity-plugin",
|
|
33
|
+
"asset-management",
|
|
34
|
+
"media-library",
|
|
35
|
+
"asset-manager",
|
|
36
|
+
"media-manager",
|
|
37
|
+
"sanity-plugin-smart-asset-manager"
|
|
38
|
+
],
|
|
39
|
+
"author": "Code-Journey <codejourney.info@gmail.com>",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
43
|
+
"react-dom": "^18.0.0 || ^19.0.0",
|
|
44
|
+
"sanity": "^3.0.0 || ^4.0.0 || ^5.0.0",
|
|
45
|
+
"styled-components": "^6.0.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@sanity/ui": "^3.1.13",
|
|
49
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
50
|
+
"@testing-library/react": "^16.3.2",
|
|
51
|
+
"@testing-library/user-event": "^14.6.1",
|
|
52
|
+
"@types/react": "^19.2.14",
|
|
53
|
+
"@vitejs/plugin-react": "^5.1.4",
|
|
54
|
+
"jsdom": "^27.0.1",
|
|
55
|
+
"prettier": "^3.5.1",
|
|
56
|
+
"react": "^19.2.4",
|
|
57
|
+
"react-dom": "^19.2.4",
|
|
58
|
+
"sanity": "^5.12.0",
|
|
59
|
+
"styled-components": "^6.1.8",
|
|
60
|
+
"tsup": "^8.5.1",
|
|
61
|
+
"typescript": "^5.9.3",
|
|
62
|
+
"vitest": "^3.2.4"
|
|
63
|
+
},
|
|
64
|
+
"repository": {
|
|
65
|
+
"type": "git",
|
|
66
|
+
"url": "https://github.com/Code-Journey-77/sanity-plugin-smart-asset-manager"
|
|
67
|
+
},
|
|
68
|
+
"bugs": {
|
|
69
|
+
"url": "https://github.com/Code-Journey-77/sanity-plugin-smart-asset-manager/issues"
|
|
70
|
+
},
|
|
71
|
+
"homepage": "https://github.com/Code-Journey-77/sanity-plugin-smart-asset-manager#readme"
|
|
72
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {Box, Flex, Stack, Text, Badge, Button, Card, Checkbox} from '@sanity/ui'
|
|
3
|
+
import type {Asset} from '@/types'
|
|
4
|
+
import {formatBytes} from '@/utils/formatBytes'
|
|
5
|
+
import {DocumentsIcon, DownloadIcon, PdfIcon, AudioIcon} from '@/components/common/Icons'
|
|
6
|
+
import styled from 'styled-components'
|
|
7
|
+
|
|
8
|
+
const StyledCard = styled(Card)`
|
|
9
|
+
overflow: hidden;
|
|
10
|
+
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
11
|
+
cursor: pointer;
|
|
12
|
+
position: relative;
|
|
13
|
+
border: 1px solid
|
|
14
|
+
${(props) => (props.selected ? 'var(--card-focus-ring-color)' : 'var(--card-border-color)')};
|
|
15
|
+
background-color: ${(props) => (props.selected ? 'var(--card-accent-bg-color)' : 'inherit')};
|
|
16
|
+
|
|
17
|
+
&:hover {
|
|
18
|
+
transform: translateY(-4px);
|
|
19
|
+
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.08);
|
|
20
|
+
border-color: var(--card-focus-ring-color);
|
|
21
|
+
}
|
|
22
|
+
`
|
|
23
|
+
|
|
24
|
+
const ImageContainer = styled(Box)`
|
|
25
|
+
width: 100%;
|
|
26
|
+
aspect-ratio: 1.25;
|
|
27
|
+
background-color: transparent;
|
|
28
|
+
position: relative;
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
justify-content: center;
|
|
32
|
+
border-bottom: 1px solid var(--card-border-color);
|
|
33
|
+
overflow: hidden;
|
|
34
|
+
`
|
|
35
|
+
|
|
36
|
+
const SelectOverlay = styled(Box)`
|
|
37
|
+
position: absolute;
|
|
38
|
+
top: 8px;
|
|
39
|
+
left: 8px;
|
|
40
|
+
z-index: 5;
|
|
41
|
+
`
|
|
42
|
+
|
|
43
|
+
const AssetPreview = styled.img`
|
|
44
|
+
width: 100%;
|
|
45
|
+
height: 100%;
|
|
46
|
+
object-fit: cover;
|
|
47
|
+
transition: transform 0.3s ease;
|
|
48
|
+
|
|
49
|
+
${StyledCard}:hover & {
|
|
50
|
+
transform: scale(1.05);
|
|
51
|
+
}
|
|
52
|
+
`
|
|
53
|
+
|
|
54
|
+
const VideoPreview = styled.video`
|
|
55
|
+
width: 100%;
|
|
56
|
+
height: 100%;
|
|
57
|
+
object-fit: cover;
|
|
58
|
+
background: #000;
|
|
59
|
+
`
|
|
60
|
+
|
|
61
|
+
const ActionBar = styled(Flex)`
|
|
62
|
+
position: absolute;
|
|
63
|
+
bottom: 8px;
|
|
64
|
+
right: 8px;
|
|
65
|
+
background: var(--card-bg-color);
|
|
66
|
+
opacity: 0.95;
|
|
67
|
+
backdrop-filter: blur(4px);
|
|
68
|
+
border-radius: 8px;
|
|
69
|
+
padding: 2px;
|
|
70
|
+
opacity: 0;
|
|
71
|
+
border: 1px solid var(--card-border-color);
|
|
72
|
+
transition: opacity 0.2s ease;
|
|
73
|
+
|
|
74
|
+
${StyledCard}:hover & {
|
|
75
|
+
opacity: 1;
|
|
76
|
+
}
|
|
77
|
+
`
|
|
78
|
+
|
|
79
|
+
interface AssetCardProps {
|
|
80
|
+
asset: Asset
|
|
81
|
+
onClick: (asset: Asset) => void
|
|
82
|
+
isSelected?: boolean
|
|
83
|
+
onSelect?: (id: string, selected: boolean) => void
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const AssetCard: React.FC<AssetCardProps> = ({asset, onClick, isSelected, onSelect}) => {
|
|
87
|
+
const isImage = asset._type === 'sanity.imageAsset'
|
|
88
|
+
const isVideo = asset?.mimeType?.startsWith('video/')
|
|
89
|
+
const isAudio = asset?.mimeType?.startsWith('audio/')
|
|
90
|
+
const isPdf = asset?.extension === 'pdf' || asset?.mimeType === 'application/pdf'
|
|
91
|
+
|
|
92
|
+
const handleCardClick = (e: React.MouseEvent) => {
|
|
93
|
+
if ((e.target as HTMLElement).closest('button')) return
|
|
94
|
+
onClick(asset)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<StyledCard radius={3} onClick={handleCardClick} selected={isSelected}>
|
|
99
|
+
<ImageContainer>
|
|
100
|
+
{onSelect && (
|
|
101
|
+
<SelectOverlay onClick={(e) => e.stopPropagation()}>
|
|
102
|
+
<Checkbox
|
|
103
|
+
checked={isSelected}
|
|
104
|
+
onChange={(e) => onSelect(asset._id, e.target.checked)}
|
|
105
|
+
/>
|
|
106
|
+
</SelectOverlay>
|
|
107
|
+
)}
|
|
108
|
+
{isImage ? (
|
|
109
|
+
<AssetPreview
|
|
110
|
+
src={`${asset.url}?w=400&h=320&fit=crop`}
|
|
111
|
+
alt={asset.originalFilename || ''}
|
|
112
|
+
/>
|
|
113
|
+
) : isVideo ? (
|
|
114
|
+
<VideoPreview src={asset.url} muted playsInline />
|
|
115
|
+
) : (
|
|
116
|
+
<Flex
|
|
117
|
+
direction="column"
|
|
118
|
+
align="center"
|
|
119
|
+
justify="center"
|
|
120
|
+
gap={3}
|
|
121
|
+
style={{height: '100%', width: '100%'}}
|
|
122
|
+
>
|
|
123
|
+
{isPdf ? (
|
|
124
|
+
<PdfIcon
|
|
125
|
+
style={{
|
|
126
|
+
fontSize: '48px',
|
|
127
|
+
color: '#ef4444',
|
|
128
|
+
}}
|
|
129
|
+
/>
|
|
130
|
+
) : isAudio ? (
|
|
131
|
+
<AudioIcon
|
|
132
|
+
style={{
|
|
133
|
+
fontSize: '48px',
|
|
134
|
+
color: '#ef4444',
|
|
135
|
+
}}
|
|
136
|
+
/>
|
|
137
|
+
) : (
|
|
138
|
+
<DocumentsIcon
|
|
139
|
+
style={{
|
|
140
|
+
fontSize: '48px',
|
|
141
|
+
color: '#9ca3af',
|
|
142
|
+
}}
|
|
143
|
+
/>
|
|
144
|
+
)}
|
|
145
|
+
<Text size={1} weight="bold" muted>
|
|
146
|
+
{(asset.extension || asset.mimeType?.split('/')[1])?.toUpperCase() || 'OTHER'}
|
|
147
|
+
</Text>
|
|
148
|
+
</Flex>
|
|
149
|
+
)}
|
|
150
|
+
<ActionBar gap={1}>
|
|
151
|
+
<Button
|
|
152
|
+
icon={DownloadIcon}
|
|
153
|
+
mode="bleed"
|
|
154
|
+
fontSize={1}
|
|
155
|
+
padding={2}
|
|
156
|
+
onClick={(e) => {
|
|
157
|
+
e.stopPropagation()
|
|
158
|
+
window.open(asset.url)
|
|
159
|
+
}}
|
|
160
|
+
/>
|
|
161
|
+
</ActionBar>
|
|
162
|
+
</ImageContainer>
|
|
163
|
+
<Box padding={3}>
|
|
164
|
+
<Stack space={3}>
|
|
165
|
+
<Flex align="center" direction="column" gap={2}>
|
|
166
|
+
<Text size={1} weight="semibold" textOverflow="ellipsis" style={{flex: 1}}>
|
|
167
|
+
{asset.originalFilename || asset._id.substring(0, 10)}
|
|
168
|
+
</Text>
|
|
169
|
+
<Badge tone={asset.size > 1048576 ? 'caution' : 'default'} fontSize={0}>
|
|
170
|
+
{formatBytes(asset.size)}
|
|
171
|
+
</Badge>
|
|
172
|
+
</Flex>
|
|
173
|
+
</Stack>
|
|
174
|
+
</Box>
|
|
175
|
+
</StyledCard>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import React, {useState, useEffect} from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Box,
|
|
4
|
+
Flex,
|
|
5
|
+
Stack,
|
|
6
|
+
Text,
|
|
7
|
+
Badge,
|
|
8
|
+
Button,
|
|
9
|
+
Dialog,
|
|
10
|
+
TextInput,
|
|
11
|
+
Label,
|
|
12
|
+
Card,
|
|
13
|
+
Grid,
|
|
14
|
+
Spinner,
|
|
15
|
+
useToast,
|
|
16
|
+
Tooltip,
|
|
17
|
+
} from '@sanity/ui'
|
|
18
|
+
import type {Asset, ReferencedDocument} from '@/types'
|
|
19
|
+
import {formatBytes} from '@/utils/formatBytes'
|
|
20
|
+
import {DocumentsIcon, DownloadIcon, TrashIcon, PdfIcon, AudioIcon} from '@/components/common/Icons'
|
|
21
|
+
import {type SanityClient} from 'sanity'
|
|
22
|
+
import {useRouter} from 'sanity/router'
|
|
23
|
+
import {getAssetUsage, updateAssetFilename} from '@/utils/assetQueries'
|
|
24
|
+
import styled from 'styled-components'
|
|
25
|
+
|
|
26
|
+
const PreviewContainer = styled(Box)`
|
|
27
|
+
width: 100%;
|
|
28
|
+
border-radius: 8px;
|
|
29
|
+
overflow: hidden;
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
justify-content: center;
|
|
33
|
+
min-height: 200px;
|
|
34
|
+
margin-bottom: 16px;
|
|
35
|
+
border: 1px solid var(--card-border-color);
|
|
36
|
+
`
|
|
37
|
+
|
|
38
|
+
const PreviewImage = styled.img`
|
|
39
|
+
max-width: 100%;
|
|
40
|
+
max-height: 400px;
|
|
41
|
+
object-fit: contain;
|
|
42
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
43
|
+
`
|
|
44
|
+
|
|
45
|
+
const VideoPreview = styled.video`
|
|
46
|
+
max-width: 100%;
|
|
47
|
+
max-height: 400px;
|
|
48
|
+
object-fit: contain;
|
|
49
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
50
|
+
background: #000;
|
|
51
|
+
`
|
|
52
|
+
|
|
53
|
+
const PdfPreview = styled.iframe`
|
|
54
|
+
width: 100%;
|
|
55
|
+
height: 400px;
|
|
56
|
+
border: none;
|
|
57
|
+
border-radius: 4px;
|
|
58
|
+
`
|
|
59
|
+
|
|
60
|
+
const AudioPreview = styled.audio`
|
|
61
|
+
width: 100%;
|
|
62
|
+
margin-top: 16px;
|
|
63
|
+
`
|
|
64
|
+
|
|
65
|
+
const UsageCard = styled(Card)`
|
|
66
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
border: 1px solid var(--card-border-color);
|
|
69
|
+
&:hover {
|
|
70
|
+
background: var(--card-bg-color);
|
|
71
|
+
border-color: var(--card-focus-ring-color);
|
|
72
|
+
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12);
|
|
73
|
+
}
|
|
74
|
+
`
|
|
75
|
+
|
|
76
|
+
const ViewText = styled(Text)`
|
|
77
|
+
transition: all 0.2s ease;
|
|
78
|
+
color: var(--card-focus-ring-color);
|
|
79
|
+
`
|
|
80
|
+
|
|
81
|
+
interface AssetDetailsDialogProps {
|
|
82
|
+
asset: Asset
|
|
83
|
+
onClose: () => void
|
|
84
|
+
sanityClient: SanityClient
|
|
85
|
+
onUpdate: (id: string, newName: string) => void
|
|
86
|
+
onDelete: (id: string) => void
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const AssetDetailsDialog: React.FC<AssetDetailsDialogProps> = ({
|
|
90
|
+
asset,
|
|
91
|
+
onClose,
|
|
92
|
+
sanityClient,
|
|
93
|
+
onUpdate,
|
|
94
|
+
onDelete,
|
|
95
|
+
}) => {
|
|
96
|
+
const [newName, setNewName] = useState(asset.originalFilename || '')
|
|
97
|
+
const [isUpdating, setIsUpdating] = useState(false)
|
|
98
|
+
const [isDeleting, setIsDeleting] = useState(false)
|
|
99
|
+
const [confirmDelete, setConfirmDelete] = useState(false)
|
|
100
|
+
const [usage, setUsage] = useState<ReferencedDocument[]>([])
|
|
101
|
+
const [loadingUsage, setLoadingUsage] = useState(true)
|
|
102
|
+
const toast = useToast()
|
|
103
|
+
const router = useRouter()
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
async function fetchUsage() {
|
|
107
|
+
setLoadingUsage(true)
|
|
108
|
+
try {
|
|
109
|
+
const results = await getAssetUsage(sanityClient, asset._id)
|
|
110
|
+
setUsage(results)
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error(err)
|
|
113
|
+
} finally {
|
|
114
|
+
setLoadingUsage(false)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
fetchUsage()
|
|
118
|
+
}, [asset._id, sanityClient])
|
|
119
|
+
|
|
120
|
+
const handleUpdate = async () => {
|
|
121
|
+
if (!newName.trim()) return
|
|
122
|
+
setIsUpdating(true)
|
|
123
|
+
try {
|
|
124
|
+
await updateAssetFilename(sanityClient, asset._id, newName)
|
|
125
|
+
onUpdate(asset._id, newName)
|
|
126
|
+
toast.push({
|
|
127
|
+
status: 'success',
|
|
128
|
+
title: 'Filename updated',
|
|
129
|
+
})
|
|
130
|
+
} catch (err) {
|
|
131
|
+
toast.push({
|
|
132
|
+
status: 'error',
|
|
133
|
+
title: 'Failed to update filename',
|
|
134
|
+
})
|
|
135
|
+
} finally {
|
|
136
|
+
setIsUpdating(false)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const handleDelete = async () => {
|
|
141
|
+
setIsDeleting(true)
|
|
142
|
+
try {
|
|
143
|
+
onDelete(asset._id)
|
|
144
|
+
onClose()
|
|
145
|
+
toast.push({
|
|
146
|
+
status: 'success',
|
|
147
|
+
title: 'Asset deleted',
|
|
148
|
+
})
|
|
149
|
+
} catch (err) {
|
|
150
|
+
toast.push({
|
|
151
|
+
status: 'error',
|
|
152
|
+
title: 'Failed to delete asset',
|
|
153
|
+
})
|
|
154
|
+
} finally {
|
|
155
|
+
setIsDeleting(false)
|
|
156
|
+
setConfirmDelete(false)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const handleDocClick = (docId: string, docType: string) => {
|
|
161
|
+
if (router && typeof router.navigateIntent === 'function') {
|
|
162
|
+
router.navigateIntent('edit', {id: docId, type: docType})
|
|
163
|
+
onClose()
|
|
164
|
+
} else {
|
|
165
|
+
// Fallback for cases where router might not be fully available
|
|
166
|
+
window.location.hash = `intent/edit/id=${docId};type=${docType}`
|
|
167
|
+
onClose()
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const isImage = asset._type === 'sanity.imageAsset'
|
|
172
|
+
const isVideo = asset?.mimeType?.startsWith('video/')
|
|
173
|
+
const isAudio = asset?.mimeType?.startsWith('audio/')
|
|
174
|
+
const isPdf = asset?.extension === 'pdf' || asset?.mimeType === 'application/pdf'
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<Dialog
|
|
178
|
+
id="asset-details-dialog"
|
|
179
|
+
header="Asset Details"
|
|
180
|
+
onClose={onClose}
|
|
181
|
+
width={2}
|
|
182
|
+
footer={
|
|
183
|
+
<Box padding={3}>
|
|
184
|
+
<Flex justify="space-between" align="center">
|
|
185
|
+
<Tooltip
|
|
186
|
+
content={
|
|
187
|
+
<Box padding={2} style={{maxWidth: '350px'}}>
|
|
188
|
+
<Text size={1}>
|
|
189
|
+
{usage.length > 0
|
|
190
|
+
? 'This asset is being used by documents and cannot be deleted. Remove all references first.'
|
|
191
|
+
: 'Delete this asset permanently'}
|
|
192
|
+
</Text>
|
|
193
|
+
</Box>
|
|
194
|
+
}
|
|
195
|
+
placement="top"
|
|
196
|
+
portal
|
|
197
|
+
>
|
|
198
|
+
<Box>
|
|
199
|
+
<Button
|
|
200
|
+
icon={TrashIcon}
|
|
201
|
+
text="Delete Asset"
|
|
202
|
+
tone="critical"
|
|
203
|
+
mode="ghost"
|
|
204
|
+
onClick={() => setConfirmDelete(true)}
|
|
205
|
+
disabled={usage.length > 0 || loadingUsage}
|
|
206
|
+
/>
|
|
207
|
+
</Box>
|
|
208
|
+
</Tooltip>
|
|
209
|
+
<Flex gap={2}>
|
|
210
|
+
<Button text="Close" mode="ghost" onClick={onClose} />
|
|
211
|
+
</Flex>
|
|
212
|
+
</Flex>
|
|
213
|
+
</Box>
|
|
214
|
+
}
|
|
215
|
+
>
|
|
216
|
+
<Box padding={4}>
|
|
217
|
+
{confirmDelete && (
|
|
218
|
+
<Dialog
|
|
219
|
+
id="confirm-asset-delete"
|
|
220
|
+
header="Delete Asset?"
|
|
221
|
+
onClose={() => setConfirmDelete(false)}
|
|
222
|
+
footer={
|
|
223
|
+
<Box padding={3}>
|
|
224
|
+
<Flex gap={2} justify="flex-end">
|
|
225
|
+
<Button text="Cancel" mode="ghost" onClick={() => setConfirmDelete(false)} />
|
|
226
|
+
<Button
|
|
227
|
+
text="Delete Permanently"
|
|
228
|
+
tone="critical"
|
|
229
|
+
onClick={handleDelete}
|
|
230
|
+
loading={isDeleting}
|
|
231
|
+
/>
|
|
232
|
+
</Flex>
|
|
233
|
+
</Box>
|
|
234
|
+
}
|
|
235
|
+
>
|
|
236
|
+
<Box padding={4}>
|
|
237
|
+
<Text>
|
|
238
|
+
Are you sure you want to delete{' '}
|
|
239
|
+
<strong>{asset.originalFilename || 'this asset'}</strong>? This action cannot be
|
|
240
|
+
undone.
|
|
241
|
+
</Text>
|
|
242
|
+
</Box>
|
|
243
|
+
</Dialog>
|
|
244
|
+
)}
|
|
245
|
+
<Grid columns={[1, 1, 2]} gap={4}>
|
|
246
|
+
<Stack space={4}>
|
|
247
|
+
<PreviewContainer>
|
|
248
|
+
{isImage ? (
|
|
249
|
+
<PreviewImage src={`${asset.url}?w=800`} alt="" />
|
|
250
|
+
) : isVideo ? (
|
|
251
|
+
<VideoPreview src={asset.url} controls muted playsInline />
|
|
252
|
+
) : isPdf ? (
|
|
253
|
+
<PdfPreview src={asset.url} title="PDF Preview" />
|
|
254
|
+
) : isAudio ? (
|
|
255
|
+
<Flex direction="column" align="center" gap={4} style={{width: '100%'}}>
|
|
256
|
+
<AudioIcon style={{fontSize: '64px', color: '#ef4444'}} />
|
|
257
|
+
<AudioPreview src={asset.url} controls />
|
|
258
|
+
</Flex>
|
|
259
|
+
) : (
|
|
260
|
+
<Flex
|
|
261
|
+
direction="column"
|
|
262
|
+
align="center"
|
|
263
|
+
justify="center"
|
|
264
|
+
gap={3}
|
|
265
|
+
style={{height: '300px', width: '100%'}}
|
|
266
|
+
>
|
|
267
|
+
{isPdf ? (
|
|
268
|
+
<PdfIcon style={{fontSize: '80px', color: '#ef4444'}} />
|
|
269
|
+
) : isAudio ? (
|
|
270
|
+
<AudioIcon style={{fontSize: '80px', color: '#ef4444'}} />
|
|
271
|
+
) : (
|
|
272
|
+
<DocumentsIcon
|
|
273
|
+
style={{
|
|
274
|
+
fontSize: '80px',
|
|
275
|
+
color: '#9ca3af',
|
|
276
|
+
}}
|
|
277
|
+
/>
|
|
278
|
+
)}
|
|
279
|
+
</Flex>
|
|
280
|
+
)}
|
|
281
|
+
</PreviewContainer>
|
|
282
|
+
|
|
283
|
+
<Stack space={3}>
|
|
284
|
+
<Box>
|
|
285
|
+
<Label size={1}>Original Filename</Label>
|
|
286
|
+
<Flex gap={2} marginTop={2}>
|
|
287
|
+
<Box style={{flex: 1}}>
|
|
288
|
+
<TextInput
|
|
289
|
+
value={newName}
|
|
290
|
+
onChange={(e) => setNewName(e.currentTarget.value)}
|
|
291
|
+
placeholder="Enter filename..."
|
|
292
|
+
/>
|
|
293
|
+
</Box>
|
|
294
|
+
<Button
|
|
295
|
+
text="Save"
|
|
296
|
+
tone="primary"
|
|
297
|
+
onClick={handleUpdate}
|
|
298
|
+
loading={isUpdating}
|
|
299
|
+
disabled={newName === asset.originalFilename}
|
|
300
|
+
/>
|
|
301
|
+
</Flex>
|
|
302
|
+
</Box>
|
|
303
|
+
|
|
304
|
+
<Card padding={3} border radius={2}>
|
|
305
|
+
<Stack space={3}>
|
|
306
|
+
<Text weight="bold" size={1}>
|
|
307
|
+
File Information
|
|
308
|
+
</Text>
|
|
309
|
+
<Stack space={3}>
|
|
310
|
+
<Flex align="flex-start" gap={3}>
|
|
311
|
+
<Box style={{minWidth: '85px'}}>
|
|
312
|
+
<Text size={1} muted>
|
|
313
|
+
ID:
|
|
314
|
+
</Text>
|
|
315
|
+
</Box>
|
|
316
|
+
<Box flex={1}>
|
|
317
|
+
<Text size={1} style={{wordBreak: 'break-all'}}>
|
|
318
|
+
{asset._id}
|
|
319
|
+
</Text>
|
|
320
|
+
</Box>
|
|
321
|
+
</Flex>
|
|
322
|
+
|
|
323
|
+
<Flex align="center" gap={3}>
|
|
324
|
+
<Box style={{minWidth: '85px'}}>
|
|
325
|
+
<Text size={1} muted>
|
|
326
|
+
Type:
|
|
327
|
+
</Text>
|
|
328
|
+
</Box>
|
|
329
|
+
<Box flex={1}>
|
|
330
|
+
<Badge tone="primary" mode="outline" fontSize={0}>
|
|
331
|
+
{(asset.extension || asset.mimeType?.split('/')[1])?.toUpperCase() ||
|
|
332
|
+
'OTHER'}
|
|
333
|
+
</Badge>
|
|
334
|
+
</Box>
|
|
335
|
+
</Flex>
|
|
336
|
+
|
|
337
|
+
<Flex align="center" gap={3}>
|
|
338
|
+
<Box style={{minWidth: '85px'}}>
|
|
339
|
+
<Text size={1} muted>
|
|
340
|
+
Size:
|
|
341
|
+
</Text>
|
|
342
|
+
</Box>
|
|
343
|
+
<Box flex={1}>
|
|
344
|
+
<Text size={1}>{formatBytes(asset.size)}</Text>
|
|
345
|
+
</Box>
|
|
346
|
+
</Flex>
|
|
347
|
+
|
|
348
|
+
{asset.metadata?.dimensions && (
|
|
349
|
+
<Flex align="center" gap={3}>
|
|
350
|
+
<Box style={{minWidth: '85px'}}>
|
|
351
|
+
<Text size={1} muted>
|
|
352
|
+
Dimensions:
|
|
353
|
+
</Text>
|
|
354
|
+
</Box>
|
|
355
|
+
<Box flex={1}>
|
|
356
|
+
<Text size={1}>
|
|
357
|
+
{asset.metadata.dimensions.width} × {asset.metadata.dimensions.height}{' '}
|
|
358
|
+
px
|
|
359
|
+
</Text>
|
|
360
|
+
</Box>
|
|
361
|
+
</Flex>
|
|
362
|
+
)}
|
|
363
|
+
</Stack>
|
|
364
|
+
<Button
|
|
365
|
+
icon={DownloadIcon}
|
|
366
|
+
text="Download File"
|
|
367
|
+
mode="ghost"
|
|
368
|
+
onClick={() => window.open(asset.url)}
|
|
369
|
+
/>
|
|
370
|
+
</Stack>
|
|
371
|
+
</Card>
|
|
372
|
+
</Stack>
|
|
373
|
+
</Stack>
|
|
374
|
+
|
|
375
|
+
<Stack space={4}>
|
|
376
|
+
<Text weight="bold" size={1}>
|
|
377
|
+
Usage in Documents
|
|
378
|
+
</Text>
|
|
379
|
+
{loadingUsage ? (
|
|
380
|
+
<Flex align="center" justify="center" style={{padding: '40px'}}>
|
|
381
|
+
<Spinner />
|
|
382
|
+
</Flex>
|
|
383
|
+
) : usage?.length > 0 ? (
|
|
384
|
+
<Stack space={2} style={{maxHeight: '500px', overflowY: 'auto'}}>
|
|
385
|
+
{usage?.map((doc) => (
|
|
386
|
+
<UsageCard
|
|
387
|
+
key={doc._id}
|
|
388
|
+
padding={3}
|
|
389
|
+
border
|
|
390
|
+
radius={2}
|
|
391
|
+
onClick={() => handleDocClick(doc._id, doc._type)}
|
|
392
|
+
>
|
|
393
|
+
<Flex align="center" justify="space-between" gap={3}>
|
|
394
|
+
<Flex direction="column" align="flex-start" gap={2}>
|
|
395
|
+
<Text size={1} weight="semibold">
|
|
396
|
+
{doc.title}
|
|
397
|
+
</Text>
|
|
398
|
+
<Badge tone="caution" fontSize={0}>
|
|
399
|
+
{doc._type}
|
|
400
|
+
</Badge>
|
|
401
|
+
</Flex>
|
|
402
|
+
<Flex align="center" gap={3}>
|
|
403
|
+
<ViewText size={0} weight="bold" className="view-text">
|
|
404
|
+
VIEW →
|
|
405
|
+
</ViewText>
|
|
406
|
+
</Flex>
|
|
407
|
+
</Flex>
|
|
408
|
+
</UsageCard>
|
|
409
|
+
))}
|
|
410
|
+
</Stack>
|
|
411
|
+
) : (
|
|
412
|
+
<Card padding={4} border radius={2} tone="transparent" style={{textAlign: 'center'}}>
|
|
413
|
+
<Text size={1} muted>
|
|
414
|
+
No references found for this asset.
|
|
415
|
+
</Text>
|
|
416
|
+
</Card>
|
|
417
|
+
)}
|
|
418
|
+
</Stack>
|
|
419
|
+
</Grid>
|
|
420
|
+
</Box>
|
|
421
|
+
</Dialog>
|
|
422
|
+
)
|
|
423
|
+
}
|