452 lines
18 KiB
JavaScript
452 lines
18 KiB
JavaScript
/**
|
|
* WordPress dependencies
|
|
*/
|
|
import { __ } from '@wordpress/i18n';
|
|
import {
|
|
Button,
|
|
Modal,
|
|
TextControl,
|
|
ColorPicker,
|
|
Card,
|
|
CardBody,
|
|
Popover,
|
|
} from '@wordpress/components';
|
|
import { useState, useRef } from '@wordpress/element';
|
|
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
|
import apiFetch from '@wordpress/api-fetch';
|
|
|
|
const ItemTypes = {
|
|
DATASET_ITEM: 'dataset_item'
|
|
};
|
|
|
|
const DatasetItem = ({ item, datasetKey, moveItem, updateItem, items, onDelete }) => {
|
|
const [showColorPicker, setShowColorPicker] = useState(false);
|
|
const ref = useRef(null);
|
|
|
|
const [{ isDragging }, drag] = useDrag({
|
|
type: ItemTypes.DATASET_ITEM,
|
|
item: { id: item.id, datasetKey },
|
|
collect: (monitor) => ({
|
|
isDragging: monitor.isDragging(),
|
|
}),
|
|
});
|
|
|
|
const [{ isOver }, drop] = useDrop({
|
|
accept: ItemTypes.DATASET_ITEM,
|
|
hover(draggedItem, monitor) {
|
|
if (!ref.current) return;
|
|
if (draggedItem.id === item.id) return;
|
|
|
|
const hoverBoundingRect = ref.current.getBoundingClientRect();
|
|
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
|
const hoverMiddleX = (hoverBoundingRect.right - hoverBoundingRect.left) / 2;
|
|
const clientOffset = monitor.getClientOffset();
|
|
|
|
if (!clientOffset) return;
|
|
|
|
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
|
|
const hoverClientX = clientOffset.x - hoverBoundingRect.left;
|
|
|
|
// If hovering in the right half and below middle, make it a child
|
|
const shouldBeChild = hoverClientX > hoverMiddleX && hoverClientY > hoverMiddleY;
|
|
|
|
moveItem(draggedItem.id, item.id, shouldBeChild);
|
|
},
|
|
collect: (monitor) => ({
|
|
isOver: monitor.isOver(),
|
|
}),
|
|
});
|
|
|
|
// Initialize drag and drop refs
|
|
drag(drop(ref));
|
|
|
|
// Get child items
|
|
const childItems = items.filter(i => i.parent === item.id);
|
|
|
|
// Calculate the nesting level
|
|
const getItemDepth = (itemId) => {
|
|
let depth = 0;
|
|
let currentItem = items.find(i => i.id === itemId);
|
|
while (currentItem && currentItem.parent) {
|
|
depth++;
|
|
currentItem = items.find(i => i.id === currentItem.parent);
|
|
}
|
|
return depth;
|
|
};
|
|
|
|
const depth = getItemDepth(item.id);
|
|
|
|
const style = {
|
|
marginLeft: `${depth * 20}px`, // 20px indentation per level
|
|
opacity: isDragging ? 0.5 : 1,
|
|
cursor: 'move',
|
|
background: isOver ? '#f0f0f0' : 'white',
|
|
border: isOver ? '2px dashed #0073aa' : '1px solid #e2e4e7',
|
|
marginBottom: '8px',
|
|
transition: 'all 0.2s ease',
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<div ref={ref} style={style}>
|
|
<Card>
|
|
<CardBody>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
|
<TextControl
|
|
placeholder={__('Label', 'lcp')}
|
|
value={item.label}
|
|
onChange={(value) => updateItem(datasetKey, item.id, 'label', value)}
|
|
style={{ flex: 2 }}
|
|
/>
|
|
<TextControl
|
|
type="number"
|
|
placeholder={__('Value', 'lcp')}
|
|
value={item.value}
|
|
onChange={(value) => updateItem(datasetKey, item.id, 'value', parseFloat(value))}
|
|
style={{ flex: 1 }}
|
|
/>
|
|
<div style={{ position: 'relative' }}>
|
|
<Button
|
|
onClick={() => setShowColorPicker(true)}
|
|
style={{
|
|
backgroundColor: item.color,
|
|
width: '30px',
|
|
height: '30px',
|
|
borderRadius: '50%',
|
|
padding: 0,
|
|
border: '1px solid #ddd'
|
|
}}
|
|
/>
|
|
{showColorPicker && (
|
|
<Popover
|
|
onClose={() => setShowColorPicker(false)}
|
|
position="bottom center"
|
|
>
|
|
<div style={{ padding: '10px' }}>
|
|
<ColorPicker
|
|
color={item.color}
|
|
onChange={(value) => updateItem(datasetKey, item.id, 'color', value)}
|
|
enableAlpha={false}
|
|
/>
|
|
</div>
|
|
</Popover>
|
|
)}
|
|
</div>
|
|
<Button
|
|
variant="secondary"
|
|
isDestructive
|
|
isSmall
|
|
onClick={() => {
|
|
if (window.confirm(__('Are you sure you want to delete this item?', 'lcp'))) {
|
|
onDelete(item.id);
|
|
}
|
|
}}
|
|
>
|
|
{__('Delete', 'lcp')}
|
|
</Button>
|
|
</div>
|
|
</CardBody>
|
|
</Card>
|
|
</div>
|
|
{/* Render child items */}
|
|
{childItems.map(childItem => (
|
|
<DatasetItem
|
|
key={childItem.id}
|
|
item={childItem}
|
|
datasetKey={datasetKey}
|
|
moveItem={moveItem}
|
|
updateItem={updateItem}
|
|
items={items}
|
|
onDelete={onDelete}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const LCPDatasetBuilder = ({ value, onChange }) => {
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [datasets, setDatasets] = useState(value || {});
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
const [editingDataset, setEditingDataset] = useState(null);
|
|
|
|
const addNewDataset = () => {
|
|
const defaultName = 'New Dataset';
|
|
let newName = defaultName;
|
|
let counter = 1;
|
|
|
|
// Ensure unique name
|
|
while (datasets[newName]) {
|
|
newName = `${defaultName} ${counter}`;
|
|
counter++;
|
|
}
|
|
|
|
const updatedDatasets = {
|
|
...datasets,
|
|
[newName]: [{
|
|
id: `dataset-${Date.now()}`,
|
|
label: 'New Item',
|
|
parent: null,
|
|
value: 0,
|
|
color: '#000000'
|
|
}]
|
|
};
|
|
setDatasets(updatedDatasets);
|
|
onChange(updatedDatasets);
|
|
setEditingDataset(newName);
|
|
};
|
|
|
|
const renameDataset = (oldName, newName) => {
|
|
if (oldName === newName || !newName.trim() || datasets[newName]) {
|
|
setEditingDataset(null);
|
|
return;
|
|
}
|
|
|
|
const updatedDatasets = { ...datasets };
|
|
updatedDatasets[newName] = updatedDatasets[oldName];
|
|
delete updatedDatasets[oldName];
|
|
setDatasets(updatedDatasets);
|
|
onChange(updatedDatasets);
|
|
setEditingDataset(null);
|
|
};
|
|
|
|
const deleteDataset = (datasetKey) => {
|
|
const updatedDatasets = { ...datasets };
|
|
delete updatedDatasets[datasetKey];
|
|
setDatasets(updatedDatasets);
|
|
onChange(updatedDatasets);
|
|
};
|
|
|
|
const deleteItem = (datasetKey, itemId) => {
|
|
const updatedDatasets = { ...datasets };
|
|
// Remove the item and its children
|
|
const removeItemAndChildren = (items, targetId) => {
|
|
return items.filter(item => {
|
|
if (item.id === targetId) return false;
|
|
if (item.parent === targetId) return false;
|
|
return true;
|
|
});
|
|
};
|
|
|
|
updatedDatasets[datasetKey] = removeItemAndChildren(updatedDatasets[datasetKey], itemId);
|
|
setDatasets(updatedDatasets);
|
|
onChange(updatedDatasets);
|
|
};
|
|
|
|
const addNewItem = (datasetKey) => {
|
|
const newItem = {
|
|
id: `item-${Date.now()}`,
|
|
label: 'New Item',
|
|
parent: null,
|
|
value: 0,
|
|
color: '#000000'
|
|
};
|
|
|
|
const updatedDatasets = {
|
|
...datasets,
|
|
[datasetKey]: [...datasets[datasetKey], newItem]
|
|
};
|
|
setDatasets(updatedDatasets);
|
|
onChange(updatedDatasets);
|
|
};
|
|
|
|
const updateItem = (datasetKey, itemId, field, value) => {
|
|
const updatedDatasets = {
|
|
...datasets,
|
|
[datasetKey]: datasets[datasetKey].map(item =>
|
|
item.id === itemId ? { ...item, [field]: value } : item
|
|
)
|
|
};
|
|
setDatasets(updatedDatasets);
|
|
onChange(updatedDatasets);
|
|
};
|
|
|
|
const moveItem = (draggedId, targetId, makeChild) => {
|
|
const datasetKey = Object.keys(datasets).find(key =>
|
|
datasets[key].some(item => item.id === draggedId)
|
|
);
|
|
|
|
if (!datasetKey) return;
|
|
|
|
const items = [...datasets[datasetKey]];
|
|
const draggedItem = items.find(item => item.id === draggedId);
|
|
const targetItem = items.find(item => item.id === targetId);
|
|
|
|
if (!draggedItem || !targetItem) return;
|
|
|
|
// If target is a descendant of dragged item, prevent the move
|
|
let current = targetItem;
|
|
while (current.parent) {
|
|
if (current.parent === draggedId) return;
|
|
current = items.find(item => item.id === current.parent);
|
|
}
|
|
|
|
// Update parent reference
|
|
draggedItem.parent = makeChild ? targetId : targetItem.parent;
|
|
|
|
const updatedDatasets = {
|
|
...datasets,
|
|
[datasetKey]: items
|
|
};
|
|
|
|
setDatasets(updatedDatasets);
|
|
onChange(updatedDatasets);
|
|
};
|
|
|
|
const saveToCollection = async () => {
|
|
try {
|
|
setIsSaving(true);
|
|
console.log('Chart data:', datasets);
|
|
|
|
const formattedDatasets = Object.entries(datasets).map(([name, data]) => ({
|
|
dataset_name: name,
|
|
dataset_source: 'json',
|
|
dataset_json: data
|
|
}));
|
|
|
|
console.log('Formatted datasets:', formattedDatasets);
|
|
|
|
// Create the post first
|
|
const postResponse = await apiFetch({
|
|
path: '/wp/v2/lcp-data-collection',
|
|
method: 'POST',
|
|
data: {
|
|
title: Object.keys(datasets)[0] || 'Dataset Collection',
|
|
status: 'publish',
|
|
meta: {
|
|
lcp_datasets: formattedDatasets
|
|
}
|
|
}
|
|
});
|
|
|
|
console.log('Post created:', postResponse);
|
|
console.log('Meta data in response:', postResponse.meta);
|
|
|
|
// Double-check the meta was saved by fetching the post
|
|
const savedPost = await apiFetch({
|
|
path: `/wp/v2/lcp-data-collection/${postResponse.id}`,
|
|
method: 'GET'
|
|
});
|
|
|
|
console.log('Saved post data:', savedPost);
|
|
console.log('Saved meta data:', savedPost.meta?.lcp_datasets);
|
|
|
|
} catch (error) {
|
|
console.error('Error saving dataset:', error);
|
|
console.log('Error details:', {
|
|
message: error.message,
|
|
code: error.code,
|
|
data: error.data
|
|
});
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Button
|
|
variant="primary"
|
|
onClick={() => setIsModalOpen(true)}
|
|
>
|
|
{__('Open Dataset Builder', 'lcp')}
|
|
</Button>
|
|
{isModalOpen && (
|
|
<Modal
|
|
title={__('Dataset Builder', 'lcp')}
|
|
onRequestClose={() => setIsModalOpen(false)}
|
|
style={{ width: '100%', maxWidth: '800px' }}
|
|
>
|
|
<DndProvider backend={HTML5Backend}>
|
|
<div style={{ padding: '20px' }}>
|
|
{Object.entries(datasets).map(([datasetKey, items]) => (
|
|
<div key={datasetKey} style={{ marginBottom: '20px' }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '10px' }}>
|
|
{editingDataset === datasetKey ? (
|
|
<TextControl
|
|
value={datasetKey}
|
|
onChange={(newName) => renameDataset(datasetKey, newName)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') {
|
|
renameDataset(datasetKey, e.target.value);
|
|
} else if (e.key === 'Escape') {
|
|
setEditingDataset(null);
|
|
}
|
|
}}
|
|
autoFocus
|
|
/>
|
|
) : (
|
|
<>
|
|
<h3 style={{ margin: 0 }}>{datasetKey}</h3>
|
|
<Button
|
|
variant="secondary"
|
|
isSmall
|
|
onClick={() => setEditingDataset(datasetKey)}
|
|
>
|
|
{__('Rename', 'lcp')}
|
|
</Button>
|
|
<Button
|
|
variant="secondary"
|
|
isDestructive
|
|
isSmall
|
|
onClick={() => {
|
|
if (window.confirm(__('Are you sure you want to delete this dataset?', 'lcp'))) {
|
|
deleteDataset(datasetKey);
|
|
}
|
|
}}
|
|
>
|
|
{__('Delete', 'lcp')}
|
|
</Button>
|
|
</>
|
|
)}
|
|
</div>
|
|
<div>
|
|
{items
|
|
.filter(item => !item.parent)
|
|
.map(item => (
|
|
<DatasetItem
|
|
key={item.id}
|
|
datasetKey={datasetKey}
|
|
item={item}
|
|
items={items}
|
|
moveItem={moveItem}
|
|
updateItem={updateItem}
|
|
onDelete={(itemId) => deleteItem(datasetKey, itemId)}
|
|
/>
|
|
))}
|
|
</div>
|
|
<Button
|
|
variant="secondary"
|
|
onClick={() => addNewItem(datasetKey)}
|
|
style={{ marginTop: '10px' }}
|
|
>
|
|
{__('Add Item', 'lcp')}
|
|
</Button>
|
|
</div>
|
|
))}
|
|
<div style={{ display: 'flex', gap: '10px', marginTop: '20px' }}>
|
|
<Button
|
|
variant="primary"
|
|
onClick={addNewDataset}
|
|
>
|
|
{__('Add New Dataset', 'lcp')}
|
|
</Button>
|
|
<Button
|
|
variant="secondary"
|
|
onClick={saveToCollection}
|
|
disabled={isSaving}
|
|
>
|
|
{isSaving ? __('Saving...', 'lcp') : __('Save to Collection', 'lcp')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DndProvider>
|
|
</Modal>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default LCPDatasetBuilder;
|