has correct effect for element inside editor mod has correct paging just missing bottom pagination not displaying proper number search + path isnt being properly handles seeing files that shouldnt be inside initial folder path ```datacorejsx const initialPath = "COOKBOOK/RECIPES/ALL"; // Master controller for column properties const MASTER_COLUMN_CONTROLLER = { defineColumns: (props) => props, getFallbackValue: () => 'Unknown', getNoDataFallback: () => 'No Data' }; // Define the initial dynamic column properties let DYNAMIC_COLUMN_PROPERTIES = MASTER_COLUMN_CONTROLLER.defineColumns({ "Recipes": "name.obsidian", "Source": "source", "Genre": "genre", "Tags": "tags", "Ingredients": "ingredients", "Creation Date": "ctime.obsidian" }); // Retrieve properties with fallback function getProperty(entry, property) { if (!property || typeof property !== 'string') { return MASTER_COLUMN_CONTROLLER.getNoDataFallback(property); } if (property.endsWith('.obsidian')) { const cleanProp = property.replace('.obsidian', ''); switch (cleanProp) { case 'ctime': return entry.$ctime ? entry.$ctime.toISODate() : MASTER_COLUMN_CONTROLLER.getFallbackValue(); case 'name': return entry.$name || 'Unnamed'; default: return MASTER_COLUMN_CONTROLLER.getNoDataFallback(property); } } if (entry.$frontmatter?.hasOwnProperty(property)) { const field = entry.$frontmatter[property]; return field?.value ? field.value.toString() : 'Unknown'; } return MASTER_COLUMN_CONTROLLER.getNoDataFallback(property); } // Draggable link component with hover preview effect function DraggableLink({ title }) { const handleDrag = (e) => e.dataTransfer.setData("text/plain", `[[${title}]]`); return ( <a href={title} draggable onDragStart={handleDrag} title={`Preview ${title}`} // Add hover effect for preview style={{ cursor: 'pointer', textDecoration: 'underline' }} > {title} </a> ); } // Draggable Edit Control Block with minimal black-and-white UI and hover effect function DraggableEditBlock({ columnId, index, columnsToShow, setColumnsToShow, editedHeaders, setEditedHeaders, editedFields, setEditedFields, updateColumn, removeColumn }) { const handleDragStart = (e) => { e.dataTransfer.setData('dragIndex', index); }; const handleDrop = (e) => { const dragIndex = e.dataTransfer.getData('dragIndex'); const newColumns = [...columnsToShow]; const draggedColumn = newColumns[dragIndex]; newColumns.splice(dragIndex, 1); newColumns.splice(index, 0, draggedColumn); setColumnsToShow(newColumns); }; return ( <div key={columnId} draggable onDragStart={handleDragStart} onDrop={handleDrop} onDragOver={(e) => e.preventDefault()} style={{ padding: '20px', border: '1px solid #ccc', marginBottom: '15px', marginRight: '15px', width: '300px', cursor: 'grab', backgroundColor: '#222', color: '#fff', borderRadius: '8px', transition: 'background-color 0.2s ease-in-out', display: 'flex', flexDirection: 'column', gap: '10px', }} onMouseEnter={(e) => { e.currentTarget.style.backgroundColor = '#333'; }} onMouseLeave={(e) => { e.currentTarget.style.backgroundColor = '#222'; }} > <label style={{ fontSize: '16px', fontWeight: 'bold', marginBottom: '5px' }}> {editedHeaders[columnId] || columnId} </label> <label style={{ color: 'grey', fontSize: '12px', display: 'block', marginBottom: '5px' }}> Header Label: </label> <dc.Textbox type="text" value={editedHeaders[columnId] || columnId} onChange={e => setEditedHeaders((prev) => ({ ...prev, [columnId]: e.target.value }))} style={{ padding: '8px', border: '1px solid #444', width: '100%', backgroundColor: '#111', color: '#fff' }} /> <label style={{ color: 'grey', fontSize: '12px', marginTop: '10px', display: 'block' }}>Data Field:</label> <dc.Textbox placeholder="Data Field" value={editedFields[columnId] || DYNAMIC_COLUMN_PROPERTIES[columnId]} onChange={e => setEditedFields((prev) => ({ ...prev, [columnId]: e.target.value }))} style={{ padding: '8px', border: '1px solid #444', width: '100%', backgroundColor: '#111', color: '#fff' }} /> <div style={{ display: 'flex', gap: '10px', marginTop: '10px', justifyContent: 'space-between', width: '100%' }}> <button onClick={() => updateColumn(columnId)} style={{ padding: '8px 12px', cursor: 'pointer', width: '45%', backgroundColor: '#444', color: '#fff', borderRadius: '5px', border: '1px solid #555', }} > Update </button> <button onClick={() => removeColumn(columnId)} style={{ padding: '8px 12px', cursor: 'pointer', width: '45%', backgroundColor: '#444', color: '#fff', borderRadius: '5px', border: '1px solid #555', }} > Remove </button> </div> </div> ); } // Main View function View() { const [nameFilter, setNameFilter] = dc.useState(""); const [queryPath, setQueryPath] = dc.useState(initialPath); const [groupBy, setGroupBy] = dc.useState("Genre"); const [columnsToShow, setColumnsToShow] = dc.useState(Object.keys(DYNAMIC_COLUMN_PROPERTIES)); const [sortColumn, setSortColumn] = dc.useState("Recipes"); const [sortOrder, setSortOrder] = dc.useState("asc"); const [isEditingHeaders, setIsEditingHeaders] = dc.useState(false); const [editedHeaders, setEditedHeaders] = dc.useState({}); const [editedFields, setEditedFields] = dc.useState({}); const [currentPage, setCurrentPage] = dc.useState(1); const itemsPerPage = 10; // Fetch query data from the path const qdata = dc.useQuery(`@page and path("${queryPath}")`); // Apply filtering const filteredData = qdata.filter(entry => getProperty(entry, "name.obsidian").toLowerCase().includes(nameFilter.toLowerCase()) ); // Pagination logic const indexOfLastItem = currentPage * itemsPerPage; const indexOfFirstItem = indexOfLastItem - itemsPerPage; const currentItems = filteredData.slice(indexOfFirstItem, indexOfLastItem); // Handle page change const handlePageChange = (pageNumber) => { setCurrentPage(pageNumber); }; // Sort data const sortedData = currentItems.sort((a, b) => { const aValue = getProperty(a, DYNAMIC_COLUMN_PROPERTIES[sortColumn]); const bValue = getProperty(b, DYNAMIC_COLUMN_PROPERTIES[sortColumn]); return sortOrder === 'asc' ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue); }); // Group data const grouped = dc.useArray(sortedData, array => array.groupBy(x => getProperty(x, DYNAMIC_COLUMN_PROPERTIES[groupBy])).sort(x => x.key) ); // Handle header editing const toggleHeaderEdit = () => { setIsEditingHeaders(!isEditingHeaders); }; const updateColumn = (columnId) => { const newHeader = editedHeaders[columnId] || columnId; const newField = editedFields[columnId] || DYNAMIC_COLUMN_PROPERTIES[columnId]; const updatedColumns = { ...DYNAMIC_COLUMN_PROPERTIES }; const updatedColumnsToShow = [...columnsToShow]; const index = updatedColumnsToShow.indexOf(columnId); if (index !== -1) { updatedColumnsToShow[index] = newHeader; } delete updatedColumns[columnId]; updatedColumns[newHeader] = newField; DYNAMIC_COLUMN_PROPERTIES = MASTER_COLUMN_CONTROLLER.defineColumns(updatedColumns); setColumnsToShow(updatedColumnsToShow); }; const removeColumn = (columnId) => { setColumnsToShow(prev => prev.filter(id => id !== columnId)); }; const finishEditing = () => { const updatedDynamicColumns = {}; Object.keys(DYNAMIC_COLUMN_PROPERTIES).forEach((key) => { const newHeader = editedHeaders[key] || key; const newField = editedFields[key] || DYNAMIC_COLUMN_PROPERTIES[key]; updatedDynamicColumns[newHeader] = newField; }); DYNAMIC_COLUMN_PROPERTIES = MASTER_COLUMN_CONTROLLER.defineColumns(updatedDynamicColumns); setIsEditingHeaders(false); }; return ( <dc.Stack> <dc.Group> <dc.Textbox type="search" placeholder="Filter recipes..." value={nameFilter} onChange={e => setNameFilter(e.target.value)} /> <dc.Textbox value={queryPath} placeholder="Enter path..." onChange={e => setQueryPath(e.target.value)} /> <dc.Dropdown options={Object.keys(DYNAMIC_COLUMN_PROPERTIES)} selected={groupBy} onChange={setGroupBy} /> <button onClick={toggleHeaderEdit}> {isEditingHeaders ? 'Finish Editing' : 'Edit Headers'} </button> </dc.Group> {/* Draggable Column Edit Blocks */} {isEditingHeaders && ( <div style={{ maxHeight: '600px', overflowY: 'auto', padding: '10px', border: '1px solid #ccc', marginTop: '20px' }}> <dc.Group style={{ display: 'flex', flexDirection: 'row', gap: '20px', flexWrap: 'wrap' }}> {columnsToShow.map((columnId, index) => ( <DraggableEditBlock key={columnId} columnId={columnId} index={index} columnsToShow={columnsToShow} setColumnsToShow={setColumnsToShow} editedHeaders={editedHeaders} setEditedHeaders={setEditedHeaders} editedFields={editedFields} setEditedFields={setEditedFields} updateColumn={updateColumn} removeColumn={removeColumn} /> ))} </dc.Group> </div> )} {/* Scrollable table wrapper */} <div style={{ overflowX: 'auto', overflowY: 'auto', maxHeight: '400px', width: '100%', marginTop: '20px', position: 'relative' }}> <dc.VanillaTable groupings={{ render: (label, rows) => <h2>{label || 'Uncategorized'}</h2> }} columns={columnsToShow.map(columnId => ({ id: columnId, value: (entry) => columnId === "Recipes" ? <DraggableLink title={getProperty(entry, DYNAMIC_COLUMN_PROPERTIES[columnId])} /> : getProperty(entry, DYNAMIC_COLUMN_PROPERTIES[columnId]) }))} rows={grouped} paging={itemsPerPage} style={{ tableLayout: 'fixed', minWidth: '1000px' }} // Adjust table width for scrollability /> </div> {/* Sticky pagination */} <div style={{ position: 'sticky', bottom: '0', width: '100%', background: '#fff', padding: '10px', boxShadow: '0px -2px 10px rgba(0,0,0,0.1)' }}> <dc.Pagination total={filteredData.length} perPage={itemsPerPage} currentPage={currentPage} onPageChange={handlePageChange} /> </div> </dc.Stack> ); } return View; ``` ```jsx const initialPath = "COOKBOOK/RECIPES/ALL"; // Master controller for column properties const MASTER_COLUMN_CONTROLLER = { defineColumns: (props) => props, getFallbackValue: () => 'Unknown', getNoDataFallback: () => 'No Data' }; // Define the initial dynamic column properties let DYNAMIC_COLUMN_PROPERTIES = MASTER_COLUMN_CONTROLLER.defineColumns({ "Recipes": "name.obsidian", "Source": "source", "Genre": "genre", "Tags": "tags", "Ingredients": "ingredients", "Creation Date": "ctime.obsidian" }); // Retrieve properties with fallback function getProperty(entry, property) { if (!property || typeof property !== 'string') { return MASTER_COLUMN_CONTROLLER.getNoDataFallback(property); } if (property.endsWith('.obsidian')) { const cleanProp = property.replace('.obsidian', ''); switch (cleanProp) { case 'ctime': return entry.$ctime ? entry.$ctime.toISODate() : MASTER_COLUMN_CONTROLLER.getFallbackValue(); case 'name': return entry.$name || 'Unnamed'; default: return MASTER_COLUMN_CONTROLLER.getNoDataFallback(property); } } if (entry.$frontmatter?.hasOwnProperty(property)) { const field = entry.$frontmatter[property]; return field?.value ? field.value.toString() : 'Unknown'; } return MASTER_COLUMN_CONTROLLER.getNoDataFallback(property); } // Draggable link component with hover preview effect function DraggableLink({ title }) { const handleDrag = (e) => e.dataTransfer.setData("text/plain", `[[${title}]]`); return ( <a href={title} draggable onDragStart={handleDrag} title={`Preview ${title}`} // Add hover effect for preview style={{ cursor: 'pointer', textDecoration: 'underline' }} > {title} </a> ); } // Draggable Edit Control Block with minimal black-and-white UI and hover effect function DraggableEditBlock({ columnId, index, columnsToShow, setColumnsToShow, editedHeaders, setEditedHeaders, editedFields, setEditedFields, updateColumn, removeColumn }) { const handleDragStart = (e) => { e.dataTransfer.setData('dragIndex', index); }; const handleDrop = (e) => { const dragIndex = e.dataTransfer.getData('dragIndex'); const newColumns = [...columnsToShow]; const draggedColumn = newColumns[dragIndex]; newColumns.splice(dragIndex, 1); newColumns.splice(index, 0, draggedColumn); setColumnsToShow(newColumns); }; return ( <div key={columnId} draggable onDragStart={handleDragStart} onDrop={handleDrop} onDragOver={(e) => e.preventDefault()} style={{ padding: '20px', border: '1px solid #ccc', marginBottom: '15px', marginRight: '15px', width: '300px', cursor: 'grab', backgroundColor: '#222', color: '#fff', borderRadius: '8px', transition: 'background-color 0.2s ease-in-out', display: 'flex', flexDirection: 'column', gap: '10px', }} onMouseEnter={(e) => { e.currentTarget.style.backgroundColor = '#333'; }} onMouseLeave={(e) => { e.currentTarget.style.backgroundColor = '#222'; }} > <label style={{ fontSize: '16px', fontWeight: 'bold', marginBottom: '5px' }}> {editedHeaders[columnId] || columnId} </label> <label style={{ color: 'grey', fontSize: '12px', display: 'block', marginBottom: '5px' }}> Header Label: </label> <dc.Textbox type="text" value={editedHeaders[columnId] || columnId} onChange={e => setEditedHeaders((prev) => ({ ...prev, [columnId]: e.target.value }))} style={{ padding: '8px', border: '1px solid #444', width: '100%', backgroundColor: '#111', color: '#fff' }} /> <label style={{ color: 'grey', fontSize: '12px', marginTop: '10px', display: 'block' }}>Data Field:</label> <dc.Textbox placeholder="Data Field" value={editedFields[columnId] || DYNAMIC_COLUMN_PROPERTIES[columnId]} onChange={e => setEditedFields((prev) => ({ ...prev, [columnId]: e.target.value }))} style={{ padding: '8px', border: '1px solid #444', width: '100%', backgroundColor: '#111', color: '#fff' }} /> <div style={{ display: 'flex', gap: '10px', marginTop: '10px', justifyContent: 'space-between', width: '100%' }}> <button onClick={() => updateColumn(columnId)} style={{ padding: '8px 12px', cursor: 'pointer', width: '45%', backgroundColor: '#444', color: '#fff', borderRadius: '5px', border: '1px solid #555', }} > Update </button> <button onClick={() => removeColumn(columnId)} style={{ padding: '8px 12px', cursor: 'pointer', width: '45%', backgroundColor: '#444', color: '#fff', borderRadius: '5px', border: '1px solid #555', }} > Remove </button> </div> </div> ); } // Main View function View() { const [nameFilter, setNameFilter] = dc.useState(""); const [queryPath, setQueryPath] = dc.useState(initialPath); const [groupBy, setGroupBy] = dc.useState("Genre"); const [columnsToShow, setColumnsToShow] = dc.useState(Object.keys(DYNAMIC_COLUMN_PROPERTIES)); const [sortColumn, setSortColumn] = dc.useState("Recipes"); const [sortOrder, setSortOrder] = dc.useState("asc"); const [isEditingHeaders, setIsEditingHeaders] = dc.useState(false); const [editedHeaders, setEditedHeaders] = dc.useState({}); const [editedFields, setEditedFields] = dc.useState({}); const [currentPage, setCurrentPage] = dc.useState(1); const itemsPerPage = 10; // Fetch query data from the path const qdata = dc.useQuery(`@page and path("${queryPath}")`); // Apply filtering const filteredData = qdata.filter(entry => getProperty(entry, "name.obsidian").toLowerCase().includes(nameFilter.toLowerCase()) ); // Pagination logic const indexOfLastItem = currentPage * itemsPerPage; const indexOfFirstItem = indexOfLastItem - itemsPerPage; const currentItems = filteredData.slice(indexOfFirstItem, indexOfLastItem); // Handle page change const handlePageChange = (pageNumber) => { setCurrentPage(pageNumber); }; // Sort data const sortedData = currentItems.sort((a, b) => { const aValue = getProperty(a, DYNAMIC_COLUMN_PROPERTIES[sortColumn]); const bValue = getProperty(b, DYNAMIC_COLUMN_PROPERTIES[sortColumn]); return sortOrder === 'asc' ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue); }); // Group data const grouped = dc.useArray(sortedData, array => array.groupBy(x => getProperty(x, DYNAMIC_COLUMN_PROPERTIES[groupBy])).sort(x => x.key) ); // Handle header editing const toggleHeaderEdit = () => { setIsEditingHeaders(!isEditingHeaders); }; const updateColumn = (columnId) => { const newHeader = editedHeaders[columnId] || columnId; const newField = editedFields[columnId] || DYNAMIC_COLUMN_PROPERTIES[columnId]; const updatedColumns = { ...DYNAMIC_COLUMN_PROPERTIES }; const updatedColumnsToShow = [...columnsToShow]; const index = updatedColumnsToShow.indexOf(columnId); if (index !== -1) { updatedColumnsToShow[index] = newHeader; } delete updatedColumns[columnId]; updatedColumns[newHeader] = newField; DYNAMIC_COLUMN_PROPERTIES = MASTER_COLUMN_CONTROLLER.defineColumns(updatedColumns); setColumnsToShow(updatedColumnsToShow); }; const removeColumn = (columnId) => { setColumnsToShow(prev => prev.filter(id => id !== columnId)); }; const finishEditing = () => { const updatedDynamicColumns = {}; Object.keys(DYNAMIC_COLUMN_PROPERTIES).forEach((key) => { const newHeader = editedHeaders[key] || key; const newField = editedFields[key] || DYNAMIC_COLUMN_PROPERTIES[key]; updatedDynamicColumns[newHeader] = newField; }); DYNAMIC_COLUMN_PROPERTIES = MASTER_COLUMN_CONTROLLER.defineColumns(updatedDynamicColumns); setIsEditingHeaders(false); }; return ( <dc.Stack> <dc.Group> <dc.Textbox type="search" placeholder="Filter recipes..." value={nameFilter} onChange={e => setNameFilter(e.target.value)} /> <dc.Textbox value={queryPath} placeholder="Enter path..." onChange={e => setQueryPath(e.target.value)} /> <dc.Dropdown options={Object.keys(DYNAMIC_COLUMN_PROPERTIES)} selected={groupBy} onChange={setGroupBy} /> <button onClick={toggleHeaderEdit}> {isEditingHeaders ? 'Finish Editing' : 'Edit Headers'} </button> </dc.Group> {/* Draggable Column Edit Blocks */} {isEditingHeaders && ( <div style={{ maxHeight: '600px', overflowY: 'auto', padding: '10px', border: '1px solid #ccc', marginTop: '20px' }}> <dc.Group style={{ display: 'flex', flexDirection: 'row', gap: '20px', flexWrap: 'wrap' }}> {columnsToShow.map((columnId, index) => ( <DraggableEditBlock key={columnId} columnId={columnId} index={index} columnsToShow={columnsToShow} setColumnsToShow={setColumnsToShow} editedHeaders={editedHeaders} setEditedHeaders={setEditedHeaders} editedFields={editedFields} setEditedFields={setEditedFields} updateColumn={updateColumn} removeColumn={removeColumn} /> ))} </dc.Group> </div> )} {/* Scrollable table wrapper */} <div style={{ overflowX: 'auto', overflowY: 'auto', maxHeight: '400px', width: '100%', marginTop: '20px', position: 'relative' }}> <dc.VanillaTable groupings={{ render: (label, rows) => <h2>{label || 'Uncategorized'}</h2> }} columns={columnsToShow.map(columnId => ({ id: columnId, value: (entry) => columnId === "Recipes" ? <DraggableLink title={getProperty(entry, DYNAMIC_COLUMN_PROPERTIES[columnId])} /> : getProperty(entry, DYNAMIC_COLUMN_PROPERTIES[columnId]) }))} rows={grouped} paging={itemsPerPage} style={{ tableLayout: 'fixed', minWidth: '1000px' }} // Adjust table width for scrollability /> </div> {/* Sticky pagination */} <div style={{ position: 'sticky', bottom: '0', width: '100%', background: '#fff', padding: '10px', boxShadow: '0px -2px 10px rgba(0,0,0,0.1)' }}> <dc.Pagination total={filteredData.length} perPage={itemsPerPage} currentPage={currentPage} onPageChange={handlePageChange} /> </div> </dc.Stack> ); } return View; ```