[[Datacore Section Projects]]
```datacorejsx
////////////////////////////////////////////////////
/// Initial Settings ///
////////////////////////////////////////////////////
const initialSettings = {
vaultName: "My Vault 2.0", // Replace with your actual vault name
queryTag: "",
initialNameFilter: "",
dynamicColumnProperties: {
"File Name": "name.obsidian",
"Last Modified": "mtime.obsidian",
"Creation Date": "ctime.obsidian",
"Tags": "tags",
},
groupByColumns: ,
pagination: {
isEnabled: true,
itemsPerPage: 100,
},
viewHeight: "600px",
placeholders: {
nameFilter: "Search notes...",
queryTag: "Enter tag...",
headerTitle: "Map of Content",
newHeaderLabel: "New Header Label",
newDataField: "New Data Field",
},
excludedFolders: ["Hidden", "My Calendar"],
};
////////////////////////////////////////////////////
/// Helper Functions ///
////////////////////////////////////////////////////
const { useState, useMemo, useEffect } = dc; // Assuming 'dc' is the Dataview context
function getProperty(entry, property) {
if (property.endsWith(".obsidian")) {
const key = property.replace(".obsidian", "");
const obsidianProps = {
ctime: entry.$ctime?.toISODate() || "Unknown Date",
mtime: entry.$mtime?.toISODate() || "Unknown Last Modified Date",
name: entry.$name || "Unnamed",
};
return obsidianProps[key] || "No Data";
}
const frontmatterField = entry.$frontmatter?.[property];
if (frontmatterField !== undefined) {
if (
frontmatterField &&
typeof frontmatterField === "object" &&
frontmatterField.hasOwnProperty("value")
) {
const value = frontmatterField.value;
if (Array.isArray(value)) {
return value.join(", ");
} else if (value !== null && value !== undefined) {
return value.toString();
}
} else if (Array.isArray(frontmatterField)) {
return frontmatterField.join(", ");
} else if (
typeof frontmatterField === "string" ||
typeof frontmatterField === "number"
) {
return frontmatterField.toString();
} else if (frontmatterField !== null && frontmatterField !== undefined) {
return frontmatterField.toString();
}
}
return " ";
}
////////////////////////////////////////////////////
/// Components ///
////////////////////////////////////////////////////
function DraggableLink({ entry, title }) {
const handleDragStart = (e) => {
e.dataTransfer.setData("text/plain", `[[${title}]]`);
e.dataTransfer.effectAllowed = "copy";
};
return (
<a
href="#"
className="internal-link"
draggable
onDragStart={handleDragStart}
data-href={entry.$path || title}
data-type="file"
title={`Drag to copy [[${title}]]`}
style={styles.draggableLink}
>
{title}
</a>
);
}
function EditableCell({ entry, property, onUpdate }) {
const [value, setValue] = useState(getProperty(entry, property));
const [isEditing, setIsEditing] = useState(false);
useEffect(() => {
setValue(getProperty(entry, property));
}, [entry, property]);
const handleBlur = () => {
setIsEditing(false);
onUpdate(entry, property, value);
};
return isEditing ? (
<dc.Textbox
value={value}
onChange={(e) => setValue(e.target.value)}
onBlur={handleBlur}
autoFocus
style={styles.cellTextbox}
/>
) : (
<div
style={styles.tableCellContent}
onClick={() => setIsEditing(true)}
title="Click to edit"
>
{value}
</div>
);
}
function EditableHeader({ columnId, editedHeaders, setEditedHeaders }) {
const [isEditing, setIsEditing] = useState(false);
const [headerValue, setHeaderValue] = useState(
editedHeaders[columnId] || columnId
);
const handleBlur = () => {
const trimmedValue = headerValue.trim();
if (trimmedValue === "") {
alert("Header name cannot be empty.");
setHeaderValue(editedHeaders[columnId] || columnId);
} else {
setIsEditing(false);
setEditedHeaders((prev) => ({
...prev,
[columnId]: trimmedValue,
}));
}
};
return isEditing ? (
<dc.Textbox
value={headerValue}
onChange={(e) => setHeaderValue(e.target.value)}
onBlur={handleBlur}
autoFocus
placeholder="Edit header..."
style={styles.headerTextbox}
/>
) : (
<label
style={styles.editBlockHeader}
onClick={() => setIsEditing(true)}
title="Click to edit header"
>
{headerValue}
</label>
);
}
function EditColumnBlock(props) {
const {
columnId,
index,
columnsToShow,
setColumnsToShow,
editedHeaders,
setEditedHeaders,
editedFields,
setEditedFields,
updateColumn,
removeColumn,
dynamicColumnProperties,
groupByColumns,
setGroupByColumns,
groupSortOrders,
setGroupSortOrders,
} = props;
const isGrouped = groupByColumns.includes(columnId);
const groupIndex = groupByColumns.indexOf(columnId) + 1;
const sortOrder = groupSortOrders[columnId] || "asc";
const handleDragStart = (e) => {
e.dataTransfer.setData("dragIndex", index);
};
const handleDrop = (e) => {
const dragIndex = e.dataTransfer.getData("dragIndex");
if (dragIndex === "") return;
const parsedDragIndex = parseInt(dragIndex, 10);
if (isNaN(parsedDragIndex)) return;
const newColumns = [...columnsToShow];
const draggedColumn = newColumns[parsedDragIndex];
newColumns.splice(parsedDragIndex, 1);
newColumns.splice(index, 0, draggedColumn);
setColumnsToShow(newColumns);
};
const toggleSortOrder = () => {
const newSortOrder = sortOrder === "asc" ? "desc" : "asc";
setGroupSortOrders({
...groupSortOrders,
[columnId]: newSortOrder,
});
};
const handleDataFieldChange = (e) => {
const newField = e.target.value;
setEditedFields((prev) => ({
...prev,
[columnId]: newField,
}));
};
const handleDataFieldUpdate = () => {
const newField =
editedFields[columnId] || dynamicColumnProperties[columnId];
updateColumn(columnId, editedHeaders[columnId] || columnId, newField);
};
return (
<div
draggable
onDragStart={handleDragStart}
onDrop={handleDrop}
onDragOver={(e) => e.preventDefault()}
style={styles.editBlock}
>
<div style={styles.editBlockHeaderContainer}>
<EditableHeader
columnId={columnId}
editedHeaders={editedHeaders}
setEditedHeaders={setEditedHeaders}
/>
</div>
<div style={styles.inlineButtonGroup}>
<button
onClick={() => {
handleDataFieldUpdate();
}}
style={styles.inlineButton}
>
Update
</button>
<button
onClick={() => removeColumn(columnId)}
style={styles.inlineButton}
>
Remove
</button>
<button
onClick={() =>
setGroupByColumns(
isGrouped
? groupByColumns.filter((col) => col !== columnId)
: [...groupByColumns, columnId]
)
}
style={{
...styles.inlineButton,
backgroundColor: isGrouped
? "var(--background-modifier-border)"
: "var(--interactive-accent)",
color: isGrouped
? "var(--text-normal)"
: "var(--text-on-accent)",
}}
data-active={isGrouped}
>
{isGrouped ? "Ungroup" : "Group By"}
</button>
</div>
{isGrouped && (
<div style={styles.groupOrderControls}>
<span style={styles.groupOrderLabel}>Group Order: {groupIndex}</span>
<button
onClick={() =>
setGroupByColumns((prev) => {
const idx = prev.indexOf(columnId);
if (idx > 0) {
const newGroup = [...prev];
[newGroup[idx - 1], newGroup[idx]] = [
newGroup[idx],
newGroup[idx - 1],
];
return newGroup;
}
return prev;
})
}
disabled={groupByColumns.indexOf(columnId) === 0}
style={styles.buttonSmall}
>
↑
</button>
<button
onClick={() =>
setGroupByColumns((prev) => {
const idx = prev.indexOf(columnId);
if (idx < prev.length - 1) {
const newGroup = [...prev];
[newGroup[idx], newGroup[idx + 1]] = [
newGroup[idx + 1],
newGroup[idx],
];
return newGroup;
}
return prev;
})
}
disabled={
groupByColumns.indexOf(columnId) === groupByColumns.length - 1
}
style={styles.buttonSmall}
>
↓
</button>
<button onClick={toggleSortOrder} style={styles.buttonSmall}>
{sortOrder === "asc" ? "Asc" : "Desc"}
</button>
</div>
)}
<div style={styles.dataFieldContainer}>
<dc.Textbox
value={editedFields[columnId] || dynamicColumnProperties[columnId]}
onChange={handleDataFieldChange}
placeholder="Data Field..."
style={styles.dataFieldTextbox}
onBlur={handleDataFieldUpdate}
autoFocus={false}
/>
</div>
</div>
);
}
function AddColumn({
newHeaderLabel,
setNewHeaderLabel,
newFieldLabel,
setNewFieldLabel,
addNewColumn,
}) {
return (
<div style={styles.addColumnContainer}>
<div style={styles.editBlock}>
<div style={styles.editBlockHeaderContainer}>
<label style={styles.editBlockHeader}>Add New Column</label>
</div>
<div style={styles.addColumnInputs}>
<dc.Textbox
value={newHeaderLabel}
onChange={(e) => setNewHeaderLabel(e.target.value)}
placeholder="New Header Label"
style={styles.addColumnTextbox}
/>
<dc.Textbox
value={newFieldLabel}
onChange={(e) => setNewFieldLabel(e.target.value)}
placeholder="New Data Field"
style={styles.addColumnTextbox}
/>
</div>
<div style={styles.addColumnButtonContainer}>
<button onClick={addNewColumn} style={styles.centeredAddButton}>
Add Column
</button>
</div>
</div>
</div>
);
}
function PaginationSettings({
isEnabled,
setIsEnabled,
itemsPerPage,
setItemsPerPage,
}) {
return (
<div style={styles.paginationSettingsContainer}>
<div style={styles.paginationMain}>
<div style={styles.paginationLeft}>
<label style={styles.paginationTitle}>Pagination:</label>
<dc.Checkbox
label="Enable"
checked={isEnabled}
onChange={(e) => setIsEnabled(e.target.checked)}
style={{ marginLeft: "10px" }}
/>
</div>
{isEnabled && (
<div style={styles.paginationRight}>
<label style={styles.paginationLabel}>Items per Page:</label>
<dc.Textbox
type="number"
min="1"
value={itemsPerPage}
onChange={(e) => setItemsPerPage(Number(e.target.value))}
style={styles.paginationTextbox}
placeholder="10"
/>
</div>
)}
</div>
</div>
);
}
function DataTable({
columnsToShow,
dynamicColumnProperties,
data,
groupByColumns,
groupSortOrders,
onUpdateEntry,
}) {
return (
<div style={styles.tableContainer}>
<div style={styles.tableHeader}>
{columnsToShow.map((col) => (
<div key={col} style={styles.tableHeaderCell}>
{col}
</div>
))}
</div>
<div style={styles.tableContent}>
{data.length > 0 ? (
<RenderRows
data={data}
columnsToShow={columnsToShow}
dynamicColumnProperties={dynamicColumnProperties}
groupByColumns={groupByColumns}
groupSortOrders={groupSortOrders}
onUpdateEntry={onUpdateEntry}
/>
) : (
<div style={styles.noData}>No data to display.</div>
)}
</div>
</div>
);
}
function RenderRows({
data,
columnsToShow,
dynamicColumnProperties,
groupByColumns,
groupSortOrders,
onUpdateEntry,
groupLevel = 0,
}) {
if (groupByColumns.length === 0) {
return data.map((entry, idx) => {
// Retrieve key properties to ensure this is an existing note
const name = getProperty(entry, "name.obsidian");
const mtime = getProperty(entry, "mtime.obsidian");
const ctime = getProperty(entry, "ctime.obsidian");
// Skip non-existing notes that have placeholder values
if (name === "Unnamed" || mtime === "Unknown Last Modified Date" || ctime === "Unknown Date") {
return null; // Prevent rendering
}
return (
<div key={idx} style={styles.tableRow}>
{columnsToShow.map((columnId) => {
const property = dynamicColumnProperties[columnId];
return (
<div key={columnId} style={styles.tableCell}>
{property === "name.obsidian" ? (
<DraggableLink
entry={entry}
title={getProperty(entry, property)}
/>
) : (
<EditableCell
entry={entry}
property={property}
onUpdate={onUpdateEntry}
/>
)}
</div>
);
})}
</div>
);
});
} else {
const groupKey = groupByColumns[0];
const property = dynamicColumnProperties[groupKey];
const sortOrder = groupSortOrders[groupKey] || "asc";
const groups = {};
data.forEach((entry) => {
// Retrieve key properties to ensure this is an existing note
const name = getProperty(entry, "name.obsidian");
const mtime = getProperty(entry, "mtime.obsidian");
const ctime = getProperty(entry, "ctime.obsidian");
// Exclude entries with placeholder values directly in the grouping stage
if (name === "Unnamed" || mtime === "Unknown Last Modified Date" || ctime === "Unknown Date") {
return; // Skip this entry
}
const key = getProperty(entry, property) || "Uncategorized";
if (!groups[key]) groups[key] = [];
groups[key].push(entry);
});
const sortedKeys = Object.keys(groups).sort((a, b) => {
if (a < b) return sortOrder === "asc" ? -1 : 1;
if (a > b) return sortOrder === "asc" ? 1 : -1;
return 0;
});
return sortedKeys.map((key, idx) => (
<div key={idx}>
<div
style={{
...styles.groupHeader,
paddingLeft: `${groupLevel * 20}px`,
}}
>
{key}
</div>
<RenderRows
data={groups[key]}
columnsToShow={columnsToShow}
dynamicColumnProperties={dynamicColumnProperties}
groupByColumns={groupByColumns.slice(1)}
groupSortOrders={groupSortOrders}
onUpdateEntry={onUpdateEntry}
groupLevel={groupLevel + 1}
/>
</div>
));
}
}
function Pagination({
currentPage,
totalPages,
onPageChange,
pageInput,
setPageInput,
totalEntries,
}) {
return (
<div style={styles.pagination}>
{totalPages > 1 ? (
<div style={styles.paginationControls}>
<dc.Button
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage === 1}
style={styles.button}
>
Previous
</dc.Button>
<span style={styles.paginationText}>
Page {currentPage} of {totalPages}
</span>
<dc.Textbox
type="number"
min="1"
max={totalPages}
value={pageInput}
placeholder="Page #"
onChange={(e) => setPageInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
const pageNumber = parseInt(pageInput, 10);
if (!isNaN(pageNumber)) onPageChange(pageNumber);
}
}}
style={styles.paginationTextbox}
/>
<dc.Button
onClick={() => {
const pageNumber = parseInt(pageInput, 10);
if (!isNaN(pageNumber)) onPageChange(pageNumber);
}}
style={styles.button}
>
Go
</dc.Button>
<dc.Button
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage === totalPages}
style={styles.button}
>
Next
</dc.Button>
</div>
) : (
<span>Total Entries: {totalEntries}</span>
)}
</div>
);
}
////////////////////////////////////////////////////
/// Styles ///
////////////////////////////////////////////////////
const styles = {
mainContainer: {
display: "flex",
flexDirection: "column",
backgroundColor: "var(--background-primary)",
color: "var(--text-normal)",
height: "100%",
},
header: {
padding: "10px",
backgroundColor: "var(--background-primary)",
},
headerTitle: {
margin: 0,
paddingBottom: "10px",
},
controlGroup: {
display: "flex",
gap: "10px",
flexWrap: "wrap",
alignItems: "center",
},
textbox: {
padding: "8px",
border: "1px solid var(--background-modifier-border)",
backgroundColor: "var(--background-primary)",
color: "var(--text-normal)",
width: "200px",
boxSizing: "border-box",
},
headerTextbox: {
padding: "4px 6px",
border: "1px solid var(--background-modifier-border)",
backgroundColor: "var(--background-secondary)",
color: "var(--text-normal)",
width: "100%",
boxSizing: "border-box",
fontSize: "14px",
},
button: {
padding: "8px 12px",
backgroundColor: "var(--interactive-accent)",
color: "var(--text-on-accent)",
border: "none",
borderRadius: "5px",
cursor: "pointer",
flex: "1",
textAlign: "center",
fontWeight: "bold",
},
addNewFileButton: {
padding: "8px 12px",
backgroundColor: "grey",
color: "var(--text-on-accent)",
border: "none",
borderRadius: "5px",
cursor: "pointer",
flex: "1",
textAlign: "center",
fontWeight: "bold",
},
buttonSmall: {
padding: "4px 6px",
backgroundColor: "var(--interactive-accent)",
color: "var(--text-on-accent)",
border: "none",
borderRadius: "3px",
cursor: "pointer",
fontSize: "12px",
flex: "0 0 auto",
},
editBlock: {
flex: "0 0 auto",
padding: "10px",
border: "1px solid var(--background-modifier-border)",
marginBottom: "10px",
backgroundColor: "var(--background-secondary)",
color: "var(--text-normal)",
borderRadius: "8px",
cursor: "grab",
display: "flex",
flexDirection: "column",
gap: "10px",
minWidth: "250px",
},
editBlockHeaderContainer: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
},
editBlockHeader: {
fontSize: "14px",
fontWeight: "bold",
cursor: "pointer",
},
editBlockSubheader: {
color: "var(--text-faint)",
fontSize: "12px",
marginBottom: "3px",
},
editingContainer: {
display: "flex",
flexDirection: "row",
gap: "10px",
padding: "5px",
overflowX: "auto",
},
inlineButtonGroup: {
display: "flex",
flexDirection: "row",
gap: "5px",
alignItems: "center",
flexWrap: "wrap",
width: "100%",
},
inlinePagination: {
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: "10px",
marginTop: "5px",
},
inlineButton: {
flex: "1",
padding: "6px 10px",
backgroundColor: "var(--interactive-accent)",
color: "var(--text-on-accent)",
border: "none",
borderRadius: "5px",
cursor: "pointer",
fontSize: "12px",
textAlign: "center",
fontWeight: "bold",
'&:active': {
opacity: 0.8,
},
'&[data-active="true"]': {
backgroundColor: "var(--interactive-accent-hover)",
},
},
addColumnContainer: {
display: "flex",
flexDirection: "column",
gap: "10px",
width: "100%",
},
addColumnInputs: {
display: "flex",
flexDirection: "column",
gap: "10px",
width: "100%",
},
addColumnTextbox: {
flex: "1",
padding: "8px",
border: "1px solid var(--background-modifier-border)",
backgroundColor: "var(--background-primary)",
color: "var(--text-normal)",
boxSizing: "border-box",
},
addColumnButtonContainer: {
display: "flex",
justifyContent: "center",
marginTop: "10px",
},
centeredAddButton: {
padding: "8px 16px",
backgroundColor: "var(--interactive-accent)",
color: "var(--text-on-accent)",
border: "none",
borderRadius: "5px",
cursor: "pointer",
flex: "0 0 auto",
fontWeight: "bold",
},
groupOrderControls: {
display: "flex",
alignItems: "center",
gap: "5px",
flexWrap: "wrap",
marginTop: "5px",
},
groupOrderLabel: {
fontSize: "12px",
color: "var(--text-faint)",
},
select: {
padding: "4px",
backgroundColor: "var(--background-primary)",
color: "var(--text-normal)",
border: "1px solid var(--background-modifier-border)",
borderRadius: "3px",
},
paginationTitle: {
fontSize: "14px",
fontWeight: "bold",
},
paginationLabel: {
fontSize: "14px",
color: "var(--text-normal)",
},
paginationTextbox: {
width: "60px",
padding: "8px",
border: "1px solid var(--background-modifier-border)",
backgroundColor: "var(--background-primary)",
color: "var(--text-normal)",
boxSizing: "border-box",
},
paginationSettingsContainer: {
display: "flex",
flexDirection: "column",
width: "100%",
},
paginationMain: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
flexWrap: "nowrap",
width: "100%",
},
paginationLeft: {
display: "flex",
alignItems: "center",
gap: "10px",
},
paginationRight: {
display: "flex",
alignItems: "center",
gap: "10px",
},
tableAndPaginationContainer: {
flex: 1,
display: "flex",
flexDirection: "column",
gap: "10px",
},
tableContainer: {
flex: 1,
overflowY: "auto",
position: "relative",
},
tableHeader: {
display: "flex",
backgroundColor: "var(--background-primary)",
position: "sticky",
top: 0,
zIndex: 2,
},
tableHeaderCell: {
flex: "1 0 150px",
minWidth: "150px",
padding: "10px",
fontWeight: "bold",
textAlign: "left",
borderBottom: "1px solid var(--background-modifier-border)",
},
tableContent: {
display: "flex",
flexDirection: "column",
},
tableRow: {
display: "flex",
borderBottom: "1px solid var(--background-modifier-border)",
},
tableCell: {
flex: "1 0 150px",
minWidth: "150px",
padding: "10px",
},
tableCellContent: {
cursor: "pointer",
},
cellTextbox: {
width: "100%",
padding: "5px",
boxSizing: "border-box",
},
groupHeader: {
padding: "10px",
backgroundColor: "var(--background-secondary)",
fontWeight: "bold",
borderBottom: "1px solid var(--background-modifier-border)",
position: "sticky",
top: 0,
zIndex: 1,
flex: "1 0 100%",
},
noData: {
padding: "20px",
textAlign: "center",
},
pagination: {
backgroundColor: "var(--background-primary)",
display: "flex",
justifyContent: "center",
alignItems: "center",
padding: "10px",
gap: "10px",
borderTop: "1px solid var(--background-modifier-border)",
},
paginationControls: {
display: "flex",
alignItems: "center",
gap: "10px",
},
paginationText: {
margin: "0 10px",
verticalAlign: "middle",
},
draggableLink: {
cursor: "pointer",
textDecoration: "underline",
color: "var(--interactive-accent)",
},
dataFieldContainer: {
marginTop: "10px",
},
dataFieldTextbox: {
width: "100%",
padding: "5px",
boxSizing: "border-box",
},
};
////////////////////////////////////////////////////
/// Main View Component ///
////////////////////////////////////////////////////
function View({ initialSettingsOverride = {}, app }) {
const mergedSettings = useMemo(() => {
return {
...initialSettings,
...initialSettingsOverride,
pagination: {
...initialSettings.pagination,
...initialSettingsOverride.pagination,
},
placeholders: {
...initialSettings.placeholders,
...initialSettingsOverride.placeholders,
},
dynamicColumnProperties: initialSettingsOverride.dynamicColumnProperties
? { ...initialSettingsOverride.dynamicColumnProperties }
: { ...initialSettings.dynamicColumnProperties },
vaultName: initialSettingsOverride.vaultName
? initialSettingsOverride.vaultName
: initialSettings.vaultName,
excludedFolders: initialSettingsOverride.excludedFolders
? [...initialSettingsOverride.excludedFolders]
: [...initialSettings.excludedFolders],
};
}, [initialSettingsOverride]);
const [nameFilter, setNameFilter] = useState(
mergedSettings.initialNameFilter || ""
);
const [queryTag, setQueryTag] = useState(mergedSettings.queryTag);
const [isEditing, setIsEditing] = useState(false);
const [editedHeaders, setEditedHeaders] = useState({});
const [editedFields, setEditedFields] = useState({});
const [newHeaderLabel, setNewHeaderLabel] = useState("");
const [newFieldLabel, setNewFieldLabel] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const [pageInput, setPageInput] = useState("");
const [isPaginationEnabled, setIsPaginationEnabled] = useState(
mergedSettings.pagination.isEnabled
);
const [itemsPerPage, setItemsPerPage] = useState(
mergedSettings.pagination.itemsPerPage
);
const [groupByColumns, setGroupByColumns] = useState(
mergedSettings.groupByColumns
);
const [groupSortOrders, setGroupSortOrders] = useState({});
const [dynamicColumnProperties, setDynamicColumnProperties] = useState(
mergedSettings.dynamicColumnProperties
);
const [columnsToShow, setColumnsToShow] = useState(
Object.keys(mergedSettings.dynamicColumnProperties)
);
useEffect(() => {
setColumnsToShow(Object.keys(dynamicColumnProperties));
}, [dynamicColumnProperties]);
const qdata = dc.useQuery(`@page and #${queryTag}`);
const filteredData = useMemo(() => {
return qdata.filter((entry) => {
const searchTerm = nameFilter.toLowerCase();
const isExcluded = mergedSettings.excludedFolders.some(folder =>
entry.$path.startsWith(folder + '/')
);
if (isExcluded) return false;
const matchesSearch = Object.values(dynamicColumnProperties).some(property => {
const value = getProperty(entry, property);
return value.toString().toLowerCase().includes(searchTerm);
});
const title = getProperty(entry, "name.obsidian");
const isUnnamed = title === "Unnamed";
const hasData = Object.keys(dynamicColumnProperties).some((header) => {
const property = dynamicColumnProperties[header];
const value = getProperty(entry, property);
return value !== "No Data";
});
return matchesSearch && !isUnnamed && hasData;
});
}, [qdata, nameFilter, dynamicColumnProperties, mergedSettings.excludedFolders]);
const groupedData = useMemo(() => {
const flattenData = (data, groupKeys, level = 0) => {
if (groupKeys.length === 0) return data;
const groupKey = groupKeys[0];
const property = dynamicColumnProperties[groupKey];
const sortOrder = groupSortOrders[groupKey] || "asc";
const groups = {};
data.forEach((entry) => {
const key = getProperty(entry, property) || "Uncategorized";
if (!groups[key]) groups[key] = [];
groups[key].push(entry);
});
const sortedKeys = Object.keys(groups).sort((a, b) => {
if (a < b) return sortOrder === "asc" ? -1 : 1;
if (a > b) return sortOrder === "asc" ? 1 : -1;
return 0;
});
let result = [];
sortedKeys.forEach((key) => {
result.push({ type: "group", level, key });
result = result.concat(
flattenData(groups[key], groupKeys.slice(1), level + 1)
);
});
return result;
};
return flattenData(filteredData, groupByColumns);
}, [filteredData, groupByColumns, dynamicColumnProperties, groupSortOrders]);
const paginatedData = useMemo(() => {
if (isPaginationEnabled) {
const start = (currentPage - 1) * itemsPerPage;
const end = currentPage * itemsPerPage;
return groupedData.slice(start, end);
} else {
return groupedData;
}
}, [groupedData, currentPage, itemsPerPage, isPaginationEnabled]);
const totalPages = useMemo(() => {
if (isPaginationEnabled) {
const totalEntries = groupedData.filter((item) => item.type !== "group")
.length;
return Math.ceil(totalEntries / itemsPerPage);
} else {
return 1;
}
}, [groupedData, itemsPerPage, isPaginationEnabled]);
const handlePageChange = (pageNumber) => {
if (pageNumber >= 1 && pageNumber <= totalPages) {
setCurrentPage(pageNumber);
setPageInput("");
} else {
alert("Invalid page number.");
}
};
const addNewColumn = () => {
if (!newHeaderLabel || !newFieldLabel) {
alert("Please provide both a new header label and a data field.");
return;
}
if (columnsToShow.includes(newHeaderLabel)) {
alert("Header label already exists. Please choose a different name.");
return;
}
const updatedColumns = {
...dynamicColumnProperties,
[newHeaderLabel]: newFieldLabel,
};
setDynamicColumnProperties(updatedColumns);
setColumnsToShow([...columnsToShow, newHeaderLabel]);
setNewHeaderLabel("");
setNewFieldLabel("");
};
const updateColumn = (columnId, newHeader, newField) => {
if (newHeader !== columnId && columnsToShow.includes(newHeader)) {
alert(
`Header "${newHeader}" already exists. Please choose a different name.`
);
setEditedHeaders((prev) => ({
...prev,
[columnId]: columnId,
}));
return;
}
setDynamicColumnProperties((prev) => {
const updated = { ...prev };
delete updated[columnId];
updated[newHeader] = newField;
return updated;
});
setColumnsToShow((prev) =>
prev.map((col) => (col === columnId ? newHeader : col))
);
setGroupByColumns((prev) =>
prev.map((col) => (col === columnId ? newHeader : col))
);
setEditedHeaders((prev) => {
const newHeaders = { ...prev };
delete newHeaders[columnId];
return newHeaders;
});
setEditedFields((prev) => {
const newFields = { ...prev };
delete newFields[columnId];
return newFields;
});
};
const removeColumn = (columnId) => {
const confirmDelete = confirm(
`Are you sure you want to remove "${columnId}"?`
);
if (!confirmDelete) return;
const updatedColumns = { ...dynamicColumnProperties };
delete updatedColumns[columnId];
setDynamicColumnProperties(updatedColumns);
setColumnsToShow(columnsToShow.filter((col) => col !== columnId));
setGroupByColumns(groupByColumns.filter((col) => col !== columnId));
setEditedHeaders((prev) => {
const newHeaders = { ...prev };
delete newHeaders[columnId];
return newHeaders;
});
setEditedFields((prev) => {
const newFields = { ...prev };
delete newFields[columnId];
return newFields;
});
};
const onUpdateEntry = (entry, property, newValue) => {
console.log(`Updating ${entry.$name}: Set ${property} to ${newValue}`);
// Implement the actual update logic here
};
const handleAddNewFile = () => {
const event = new KeyboardEvent('keydown', {
key: 'Q',
code: 'KeyQ',
ctrlKey: true,
shiftKey: true,
bubbles: true
});
document.dispatchEvent(event);
};
const totalEntries = groupedData.filter((item) => item.type !== "group")
.length;
return (
<dc.Stack
style={{ ...styles.mainContainer, height: mergedSettings.viewHeight }}
>
<div style={styles.header}>
<h1 style={styles.headerTitle}>
{mergedSettings.placeholders.headerTitle}
</h1>
<dc.Group style={styles.controlGroup}>
<dc.Textbox
type="search"
placeholder={mergedSettings.placeholders.nameFilter}
value={nameFilter}
onChange={(e) => {
setNameFilter(e.target.value);
setCurrentPage(1);
}}
style={styles.textbox}
/>
<dc.Textbox
value={queryTag}
placeholder={mergedSettings.placeholders.queryTag}
onChange={(e) => {
setQueryTag(e.target.value);
setCurrentPage(1);
}}
style={styles.textbox}
/>
<dc.Button
onClick={() => setIsEditing(!isEditing)}
style={styles.button}
>
{isEditing ? "Finish Editing" : "Edit"}
</dc.Button>
<dc.Button
onClick={handleAddNewFile}
style={styles.addNewFileButton}
>
Add New File
</dc.Button>
</dc.Group>
{isEditing && (
<dc.Group style={styles.controlGroup}>
<PaginationSettings
isEnabled={isPaginationEnabled}
setIsEnabled={setIsPaginationEnabled}
itemsPerPage={itemsPerPage}
setItemsPerPage={setItemsPerPage}
/>
</dc.Group>
)}
{isEditing && (
<div style={styles.editingContainer}>
{columnsToShow.map((columnId, index) => (
<EditColumnBlock
key={columnId}
columnId={columnId}
index={index}
columnsToShow={columnsToShow}
setColumnsToShow={setColumnsToShow}
editedHeaders={editedHeaders}
setEditedHeaders={setEditedHeaders}
editedFields={editedFields}
setEditedFields={setEditedFields}
updateColumn={updateColumn}
removeColumn={removeColumn}
dynamicColumnProperties={dynamicColumnProperties}
groupByColumns={groupByColumns}
setGroupByColumns={setGroupByColumns}
groupSortOrders={groupSortOrders}
setGroupSortOrders={setGroupSortOrders}
/>
))}
<AddColumn
newHeaderLabel={newHeaderLabel}
setNewHeaderLabel={setNewHeaderLabel}
newFieldLabel={newFieldLabel}
setNewFieldLabel={setNewFieldLabel}
addNewColumn={addNewColumn}
/>
</div>
)}
</div>
<div style={styles.tableAndPaginationContainer}>
<DataTable
columnsToShow={columnsToShow}
dynamicColumnProperties={dynamicColumnProperties}
data={paginatedData}
groupByColumns={groupByColumns}
groupSortOrders={groupSortOrders}
onUpdateEntry={onUpdateEntry}
/>
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={handlePageChange}
pageInput={pageInput}
setPageInput={setPageInput}
totalEntries={totalEntries}
/>
</div>
</dc.Stack>
);
}
return View;
```