Add dropdown to change object publish state and fix z-index
This commit is contained in:
parent
f124fd1e69
commit
9ba787cd8b
15 changed files with 180 additions and 145 deletions
|
|
@ -131,7 +131,7 @@
|
|||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
z-index: 1000;
|
||||
z-index: 1400;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
z-index: 1400;
|
||||
padding: $unit-2x;
|
||||
|
||||
@include breakpoint('phone') {
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@
|
|||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
min-width: 150px;
|
||||
z-index: 1000;
|
||||
z-index: 1050;
|
||||
overflow: hidden;
|
||||
animation: slideDown 0.2s ease;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -272,7 +272,7 @@
|
|||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
min-width: 180px;
|
||||
z-index: 10;
|
||||
z-index: 1050;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@
|
|||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
min-width: 180px;
|
||||
z-index: 1000;
|
||||
z-index: 1050;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,6 @@
|
|||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
min-width: 180px;
|
||||
z-index: 10;
|
||||
z-index: 1050;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@
|
|||
popoverElement.style.position = 'fixed'
|
||||
popoverElement.style.top = `${top}px`
|
||||
popoverElement.style.left = `${left}px`
|
||||
popoverElement.style.zIndex = '1000'
|
||||
popoverElement.style.zIndex = '1200'
|
||||
}
|
||||
|
||||
function handleFieldUpdate(key: string, value: any) {
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@
|
|||
popoverElement.style.position = 'fixed'
|
||||
popoverElement.style.top = `${top}px`
|
||||
popoverElement.style.left = `${left}px`
|
||||
popoverElement.style.zIndex = '1000'
|
||||
popoverElement.style.zIndex = '1200'
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@
|
|||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
z-index: 1400;
|
||||
padding: $unit-2x;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@
|
|||
border-radius: $unit-2x;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
min-width: 140px;
|
||||
z-index: 100;
|
||||
z-index: 1050;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
|
|
|||
|
|
@ -220,28 +220,26 @@
|
|||
</div>
|
||||
<div class="header-actions">
|
||||
{#if !isLoading}
|
||||
{#if formData.status === 'published'}
|
||||
<Button variant="primary" buttonSize="large" onclick={handleSave} disabled={isSaving}>
|
||||
{isSaving ? 'Saving...' : 'Save'}
|
||||
</Button>
|
||||
{:else}
|
||||
<StatusDropdown
|
||||
currentStatus={formData.status}
|
||||
onStatusChange={handleStatusChange}
|
||||
disabled={isSaving}
|
||||
isLoading={isSaving}
|
||||
primaryAction={{ label: 'Publish', status: 'published' }}
|
||||
dropdownActions={[
|
||||
{ label: 'Save as Draft', status: 'draft' },
|
||||
{ label: 'List Only', status: 'list-only', show: formData.status !== 'list-only' },
|
||||
{
|
||||
label: 'Password Protected',
|
||||
status: 'password-protected',
|
||||
show: formData.status !== 'password-protected'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
{/if}
|
||||
<StatusDropdown
|
||||
currentStatus={formData.status}
|
||||
onStatusChange={handleStatusChange}
|
||||
disabled={isSaving}
|
||||
isLoading={isSaving}
|
||||
primaryAction={
|
||||
formData.status === 'published'
|
||||
? { label: 'Save', status: 'published' }
|
||||
: { label: 'Publish', status: 'published' }
|
||||
}
|
||||
dropdownActions={[
|
||||
{ label: 'Save as Draft', status: 'draft', show: formData.status !== 'draft' },
|
||||
{ label: 'List Only', status: 'list-only', show: formData.status !== 'list-only' },
|
||||
{
|
||||
label: 'Password Protected',
|
||||
status: 'password-protected',
|
||||
show: formData.status !== 'password-protected'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -77,8 +77,11 @@ export async function uploadFile(
|
|||
customOptions?: any
|
||||
): Promise<UploadResult> {
|
||||
try {
|
||||
// TEMPORARY: Force Cloudinary usage for testing
|
||||
const FORCE_CLOUDINARY_IN_DEV = true; // Toggle this to test
|
||||
|
||||
// Use local storage in development or when Cloudinary is not configured
|
||||
if (dev || !isCloudinaryConfigured()) {
|
||||
if ((dev && !FORCE_CLOUDINARY_IN_DEV) || !isCloudinaryConfigured()) {
|
||||
logger.info('Using local storage for file upload')
|
||||
const localResult = await uploadFileLocally(file, type)
|
||||
|
||||
|
|
@ -123,14 +126,13 @@ export async function uploadFile(
|
|||
}
|
||||
|
||||
// Log upload attempt for debugging
|
||||
if (isSvg) {
|
||||
logger.info('Attempting SVG upload with options:', {
|
||||
filename: file.name,
|
||||
mimeType: file.type,
|
||||
size: file.size,
|
||||
uploadOptions
|
||||
})
|
||||
}
|
||||
logger.info('Attempting file upload:', {
|
||||
filename: file.name,
|
||||
mimeType: file.type,
|
||||
size: file.size,
|
||||
isSvg,
|
||||
uploadOptions
|
||||
})
|
||||
|
||||
// Upload to Cloudinary
|
||||
const result = await new Promise<UploadApiResponse>((resolve, reject) => {
|
||||
|
|
@ -168,6 +170,17 @@ export async function uploadFile(
|
|||
} catch (error) {
|
||||
logger.error('Cloudinary upload failed', error as Error)
|
||||
logger.mediaUpload(file.name, file.size, file.type, false)
|
||||
|
||||
// Enhanced error logging
|
||||
if (error instanceof Error) {
|
||||
logger.error('Upload error details:', {
|
||||
filename: file.name,
|
||||
mimeType: file.type,
|
||||
size: file.size,
|
||||
errorMessage: error.message,
|
||||
errorStack: error.stack
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
import MediaLibraryModal from '$lib/components/admin/MediaLibraryModal.svelte'
|
||||
import MediaDetailsModal from '$lib/components/admin/MediaDetailsModal.svelte'
|
||||
import GalleryUploader from '$lib/components/admin/GalleryUploader.svelte'
|
||||
import SaveActionsGroup from '$lib/components/admin/SaveActionsGroup.svelte'
|
||||
import StatusDropdown from '$lib/components/admin/StatusDropdown.svelte'
|
||||
import AlbumMetadataPopover from '$lib/components/admin/AlbumMetadataPopover.svelte'
|
||||
|
||||
// Form state
|
||||
|
|
@ -93,7 +93,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function handleSave(publishStatus?: 'draft' | 'published') {
|
||||
async function handleSave(newStatus?: string) {
|
||||
if (!title.trim()) {
|
||||
error = 'Title is required'
|
||||
return
|
||||
|
|
@ -122,7 +122,7 @@
|
|||
location: location.trim() || null,
|
||||
isPhotography,
|
||||
showInUniverse,
|
||||
status: publishStatus || status
|
||||
status: newStatus || status
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/albums/${album.id}`, {
|
||||
|
|
@ -142,8 +142,8 @@
|
|||
const updatedAlbum = await response.json()
|
||||
album = updatedAlbum
|
||||
|
||||
if (publishStatus) {
|
||||
status = publishStatus
|
||||
if (newStatus) {
|
||||
status = newStatus
|
||||
}
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Failed to update album'
|
||||
|
|
@ -591,12 +591,19 @@
|
|||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<SaveActionsGroup
|
||||
{status}
|
||||
onSave={handleSave}
|
||||
<StatusDropdown
|
||||
currentStatus={status}
|
||||
onStatusChange={handleSave}
|
||||
disabled={isSaving}
|
||||
isLoading={isSaving}
|
||||
{canSave}
|
||||
primaryAction={
|
||||
status === 'published'
|
||||
? { label: 'Save', status: 'published' }
|
||||
: { label: 'Publish', status: 'published' }
|
||||
}
|
||||
dropdownActions={[
|
||||
{ label: 'Save as Draft', status: 'draft', show: status !== 'draft' }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
import PostMetadataPopover from '$lib/components/admin/PostMetadataPopover.svelte'
|
||||
import DeleteConfirmationModal from '$lib/components/admin/DeleteConfirmationModal.svelte'
|
||||
import Button from '$lib/components/admin/Button.svelte'
|
||||
import SaveActionsGroup from '$lib/components/admin/SaveActionsGroup.svelte'
|
||||
import StatusDropdown from '$lib/components/admin/StatusDropdown.svelte'
|
||||
import type { JSONContent } from '@tiptap/core'
|
||||
|
||||
let post = $state<any>(null)
|
||||
|
|
@ -48,49 +48,55 @@
|
|||
type: 'paragraph',
|
||||
content: block.content ? [{ type: 'text', text: block.content }] : []
|
||||
}
|
||||
|
||||
|
||||
case 'heading':
|
||||
return {
|
||||
type: 'heading',
|
||||
attrs: { level: block.level || 1 },
|
||||
content: block.content ? [{ type: 'text', text: block.content }] : []
|
||||
}
|
||||
|
||||
|
||||
case 'bulletList':
|
||||
case 'ul':
|
||||
return {
|
||||
type: 'bulletList',
|
||||
content: (block.content || []).map((item: any) => ({
|
||||
type: 'listItem',
|
||||
content: [{
|
||||
type: 'paragraph',
|
||||
content: [{ type: 'text', text: item.content || item }]
|
||||
}]
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [{ type: 'text', text: item.content || item }]
|
||||
}
|
||||
]
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
case 'orderedList':
|
||||
case 'ol':
|
||||
return {
|
||||
type: 'orderedList',
|
||||
content: (block.content || []).map((item: any) => ({
|
||||
type: 'listItem',
|
||||
content: [{
|
||||
type: 'paragraph',
|
||||
content: [{ type: 'text', text: item.content || item }]
|
||||
}]
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [{ type: 'text', text: item.content || item }]
|
||||
}
|
||||
]
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
case 'blockquote':
|
||||
return {
|
||||
type: 'blockquote',
|
||||
content: [{
|
||||
type: 'paragraph',
|
||||
content: [{ type: 'text', text: block.content || '' }]
|
||||
}]
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [{ type: 'text', text: block.content || '' }]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
case 'codeBlock':
|
||||
case 'code':
|
||||
return {
|
||||
|
|
@ -98,7 +104,7 @@
|
|||
attrs: { language: block.language || '' },
|
||||
content: [{ type: 'text', text: block.content || '' }]
|
||||
}
|
||||
|
||||
|
||||
case 'image':
|
||||
return {
|
||||
type: 'image',
|
||||
|
|
@ -108,11 +114,11 @@
|
|||
title: block.caption || ''
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
case 'hr':
|
||||
case 'horizontalRule':
|
||||
return { type: 'horizontalRule' }
|
||||
|
||||
|
||||
default:
|
||||
// Default to paragraph for unknown types
|
||||
return {
|
||||
|
|
@ -134,66 +140,74 @@
|
|||
return { blocks: [] }
|
||||
}
|
||||
|
||||
const blocks = tiptapContent.content.map((node: any) => {
|
||||
switch (node.type) {
|
||||
case 'paragraph':
|
||||
const text = extractTextFromNode(node)
|
||||
return text ? { type: 'paragraph', content: text } : null
|
||||
|
||||
case 'heading':
|
||||
return {
|
||||
type: 'heading',
|
||||
level: node.attrs?.level || 1,
|
||||
content: extractTextFromNode(node)
|
||||
}
|
||||
|
||||
case 'bulletList':
|
||||
return {
|
||||
type: 'bulletList',
|
||||
content: node.content?.map((item: any) => {
|
||||
const itemText = extractTextFromNode(item.content?.[0])
|
||||
return itemText
|
||||
}).filter(Boolean) || []
|
||||
}
|
||||
|
||||
case 'orderedList':
|
||||
return {
|
||||
type: 'orderedList',
|
||||
content: node.content?.map((item: any) => {
|
||||
const itemText = extractTextFromNode(item.content?.[0])
|
||||
return itemText
|
||||
}).filter(Boolean) || []
|
||||
}
|
||||
|
||||
case 'blockquote':
|
||||
return {
|
||||
type: 'blockquote',
|
||||
content: extractTextFromNode(node.content?.[0])
|
||||
}
|
||||
|
||||
case 'codeBlock':
|
||||
return {
|
||||
type: 'codeBlock',
|
||||
language: node.attrs?.language || '',
|
||||
content: node.content?.[0]?.text || ''
|
||||
}
|
||||
|
||||
case 'image':
|
||||
return {
|
||||
type: 'image',
|
||||
src: node.attrs?.src || '',
|
||||
alt: node.attrs?.alt || '',
|
||||
caption: node.attrs?.title || ''
|
||||
}
|
||||
|
||||
case 'horizontalRule':
|
||||
return { type: 'hr' }
|
||||
|
||||
default:
|
||||
// Skip unknown types
|
||||
return null
|
||||
}
|
||||
}).filter(Boolean)
|
||||
const blocks = tiptapContent.content
|
||||
.map((node: any) => {
|
||||
switch (node.type) {
|
||||
case 'paragraph':
|
||||
const text = extractTextFromNode(node)
|
||||
return text ? { type: 'paragraph', content: text } : null
|
||||
|
||||
case 'heading':
|
||||
return {
|
||||
type: 'heading',
|
||||
level: node.attrs?.level || 1,
|
||||
content: extractTextFromNode(node)
|
||||
}
|
||||
|
||||
case 'bulletList':
|
||||
return {
|
||||
type: 'bulletList',
|
||||
content:
|
||||
node.content
|
||||
?.map((item: any) => {
|
||||
const itemText = extractTextFromNode(item.content?.[0])
|
||||
return itemText
|
||||
})
|
||||
.filter(Boolean) || []
|
||||
}
|
||||
|
||||
case 'orderedList':
|
||||
return {
|
||||
type: 'orderedList',
|
||||
content:
|
||||
node.content
|
||||
?.map((item: any) => {
|
||||
const itemText = extractTextFromNode(item.content?.[0])
|
||||
return itemText
|
||||
})
|
||||
.filter(Boolean) || []
|
||||
}
|
||||
|
||||
case 'blockquote':
|
||||
return {
|
||||
type: 'blockquote',
|
||||
content: extractTextFromNode(node.content?.[0])
|
||||
}
|
||||
|
||||
case 'codeBlock':
|
||||
return {
|
||||
type: 'codeBlock',
|
||||
language: node.attrs?.language || '',
|
||||
content: node.content?.[0]?.text || ''
|
||||
}
|
||||
|
||||
case 'image':
|
||||
return {
|
||||
type: 'image',
|
||||
src: node.attrs?.src || '',
|
||||
alt: node.attrs?.alt || '',
|
||||
caption: node.attrs?.title || ''
|
||||
}
|
||||
|
||||
case 'horizontalRule':
|
||||
return { type: 'hr' }
|
||||
|
||||
default:
|
||||
// Skip unknown types
|
||||
return null
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
return { blocks }
|
||||
}
|
||||
|
|
@ -244,7 +258,7 @@
|
|||
status = post.status || 'draft'
|
||||
slug = post.slug || ''
|
||||
excerpt = post.excerpt || ''
|
||||
|
||||
|
||||
// Convert blocks format to Tiptap format if needed
|
||||
if (post.content && post.content.blocks) {
|
||||
content = convertBlocksToTiptap(post.content)
|
||||
|
|
@ -253,7 +267,7 @@
|
|||
} else {
|
||||
content = { type: 'doc', content: [] }
|
||||
}
|
||||
|
||||
|
||||
tags = post.tags || []
|
||||
} else {
|
||||
if (response.status === 404) {
|
||||
|
|
@ -283,7 +297,7 @@
|
|||
tags = tags.filter((t) => t !== tag)
|
||||
}
|
||||
|
||||
async function handleSave(publishStatus?: 'draft' | 'published') {
|
||||
async function handleSave(newStatus?: string) {
|
||||
const auth = localStorage.getItem('admin_auth')
|
||||
if (!auth) {
|
||||
goto('/admin/login')
|
||||
|
|
@ -291,18 +305,18 @@
|
|||
}
|
||||
|
||||
saving = true
|
||||
|
||||
|
||||
// Convert content to blocks format if it's in Tiptap format
|
||||
let saveContent = content
|
||||
if (config?.showContent && content && content.type === 'doc') {
|
||||
saveContent = convertTiptapToBlocks(content)
|
||||
}
|
||||
|
||||
|
||||
const postData = {
|
||||
title: config?.showTitle ? title : null,
|
||||
slug,
|
||||
type: postType,
|
||||
status: publishStatus || status,
|
||||
status: newStatus || status,
|
||||
content: config?.showContent ? saveContent : null,
|
||||
excerpt: postType === 'essay' ? excerpt : undefined,
|
||||
link_url: undefined,
|
||||
|
|
@ -322,8 +336,8 @@
|
|||
|
||||
if (response.ok) {
|
||||
post = await response.json()
|
||||
if (publishStatus) {
|
||||
status = publishStatus
|
||||
if (newStatus) {
|
||||
status = newStatus
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -431,12 +445,15 @@
|
|||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<SaveActionsGroup
|
||||
{status}
|
||||
onSave={handleSave}
|
||||
<StatusDropdown
|
||||
currentStatus={status}
|
||||
onStatusChange={handleSave}
|
||||
disabled={saving}
|
||||
isLoading={saving}
|
||||
canSave={true}
|
||||
primaryAction={status === 'published'
|
||||
? { label: 'Save', status: 'published' }
|
||||
: { label: 'Publish', status: 'published' }}
|
||||
dropdownActions={[{ label: 'Save as Draft', status: 'draft', show: status !== 'draft' }]}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -591,7 +608,7 @@
|
|||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
min-width: 150px;
|
||||
z-index: 100;
|
||||
z-index: 1050;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -276,7 +276,7 @@
|
|||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
min-width: 150px;
|
||||
z-index: 100;
|
||||
z-index: 1050;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue