mirror of
https://github.com/liufree/obsidian-querydash.git
synced 2026-07-22 05:41:49 +00:00
commit
b335970c0b
6 changed files with 459 additions and 53 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "querydash",
|
||||
"name": "QueryDash",
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.6",
|
||||
"minAppVersion": "1.8.0",
|
||||
"description": "Refer to Dataview and add search, sorting, and pagination functions, just like Notion.",
|
||||
"author": "lwx",
|
||||
|
|
|
|||
242
src/components/EditableTable.tsx
Normal file
242
src/components/EditableTable.tsx
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
import {PlusOutlined} from '@ant-design/icons';
|
||||
import type {ActionType, ProColumns} from '@ant-design/pro-components';
|
||||
import {
|
||||
EditableProTable,
|
||||
ProCard,
|
||||
ProFormField,
|
||||
} from '@ant-design/pro-components';
|
||||
import type {InputRef} from 'antd';
|
||||
import {Button, Form, Input, Space, Tag} from 'antd';
|
||||
import React, {useRef, useState} from 'react';
|
||||
|
||||
const waitTime = (time: number = 100) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(true);
|
||||
}, time);
|
||||
});
|
||||
};
|
||||
|
||||
const TagList: React.FC<{
|
||||
value?: {
|
||||
key: string;
|
||||
label: string;
|
||||
}[];
|
||||
onChange?: (
|
||||
value: {
|
||||
key: string;
|
||||
label: string;
|
||||
}[],
|
||||
) => void;
|
||||
}> = ({value, onChange}) => {
|
||||
const ref = useRef<InputRef | null>(null);
|
||||
const [newTags, setNewTags] = useState<
|
||||
{
|
||||
key: string;
|
||||
label: string;
|
||||
}[]
|
||||
>([]);
|
||||
const [inputValue, setInputValue] = useState<string>('');
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(e.target.value);
|
||||
};
|
||||
|
||||
const handleInputConfirm = () => {
|
||||
let tempsTags = [...(value || [])];
|
||||
if (
|
||||
inputValue &&
|
||||
tempsTags.filter((tag) => tag.label === inputValue).length === 0
|
||||
) {
|
||||
tempsTags = [
|
||||
...tempsTags,
|
||||
{key: `new-${tempsTags.length}`, label: inputValue},
|
||||
];
|
||||
}
|
||||
onChange?.(tempsTags);
|
||||
setNewTags([]);
|
||||
setInputValue('');
|
||||
};
|
||||
|
||||
return (
|
||||
<Space>
|
||||
{(value || []).concat(newTags).map((item) => (
|
||||
<Tag key={item.key}>{item.label}</Tag>
|
||||
))}
|
||||
<Input
|
||||
ref={ref}
|
||||
type="text"
|
||||
size="small"
|
||||
style={{width: 78}}
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
onBlur={handleInputConfirm}
|
||||
onPressEnter={handleInputConfirm}
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
type DataSourceType = {
|
||||
id: React.Key;
|
||||
title?: string;
|
||||
labels?: {
|
||||
key: string;
|
||||
label: string;
|
||||
}[];
|
||||
state?: string;
|
||||
created_at?: number;
|
||||
children?: DataSourceType[];
|
||||
};
|
||||
|
||||
const defaultData: DataSourceType[] = [
|
||||
{
|
||||
id: 624748504,
|
||||
title: '活动名称一',
|
||||
labels: [{key: 'woman', label: '川妹子'}],
|
||||
state: 'open',
|
||||
created_at: 1590486176000,
|
||||
},
|
||||
{
|
||||
id: 624691229,
|
||||
title: '活动名称二',
|
||||
labels: [{key: 'man', label: '西北汉子'}],
|
||||
state: 'closed',
|
||||
created_at: 1590481162000,
|
||||
},
|
||||
];
|
||||
|
||||
const columns: ProColumns<DataSourceType>[] = [
|
||||
{
|
||||
title: '活动名称',
|
||||
dataIndex: 'title',
|
||||
formItemProps: {
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: '此项为必填项',
|
||||
},
|
||||
],
|
||||
},
|
||||
width: '30%',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'state',
|
||||
dataIndex: 'state',
|
||||
valueType: 'select',
|
||||
valueEnum: {
|
||||
all: {text: '全部', status: 'Default'},
|
||||
open: {
|
||||
text: '未解决',
|
||||
status: 'Error',
|
||||
},
|
||||
closed: {
|
||||
text: '已解决',
|
||||
status: 'Success',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '标签',
|
||||
dataIndex: 'labels',
|
||||
width: '20%',
|
||||
formItemProps: {
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: '此项为必填项',
|
||||
},
|
||||
],
|
||||
},
|
||||
renderFormItem: (_, {isEditable}) => {
|
||||
return isEditable ? <TagList/> : <Input/>;
|
||||
},
|
||||
render: (_, row) =>
|
||||
row?.labels?.map((item) => <Tag key={item.key}>{item.label}</Tag>),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
valueType: 'option',
|
||||
width: 250,
|
||||
render: (text, record, _, action) => [
|
||||
<a
|
||||
key="editable"
|
||||
onClick={() => {
|
||||
action?.startEditable?.(record.id);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</a>,
|
||||
<EditableProTable.RecordCreator
|
||||
key="copy"
|
||||
record={{
|
||||
...record,
|
||||
id: (Math.random() * 1000000).toFixed(0),
|
||||
}}
|
||||
>
|
||||
<a>复制此项到末尾</a>
|
||||
</EditableProTable.RecordCreator>,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default () => {
|
||||
const [editableKeys, setEditableRowKeys] = useState<React.Key[]>([]);
|
||||
const [dataSource, setDataSource] = useState<readonly DataSourceType[]>([]);
|
||||
const [form] = Form.useForm();
|
||||
return (
|
||||
<>
|
||||
<EditableProTable<DataSourceType>
|
||||
rowKey="id"
|
||||
scroll={{
|
||||
x: 960,
|
||||
}}
|
||||
headerTitle="可编辑表格"
|
||||
maxLength={5}
|
||||
// 关闭默认的新建按钮
|
||||
recordCreatorProps={{
|
||||
position: 'top',
|
||||
record: () => ({id: (Math.random() * 1000000).toFixed(0)}),
|
||||
}}
|
||||
columns={columns}
|
||||
request={async () => ({
|
||||
data: defaultData,
|
||||
total: 3,
|
||||
success: true,
|
||||
})}
|
||||
value={dataSource}
|
||||
onChange={setDataSource}
|
||||
search={{
|
||||
labelWidth: 'auto',
|
||||
}}
|
||||
pagination={{
|
||||
pageSize: 5,
|
||||
onChange: (page) => console.log(page),
|
||||
}}
|
||||
editable={{
|
||||
form,
|
||||
editableKeys,
|
||||
onSave: async () => {
|
||||
await waitTime(2000);
|
||||
},
|
||||
onChange: setEditableRowKeys,
|
||||
actionRender: (row, config, dom) => [dom.save, dom.cancel],
|
||||
}}
|
||||
/>
|
||||
<ProCard title="表格数据" headerBordered collapsible defaultCollapsed>
|
||||
<ProFormField
|
||||
ignoreFormItem
|
||||
fieldProps={{
|
||||
style: {
|
||||
width: '100%',
|
||||
},
|
||||
}}
|
||||
mode="read"
|
||||
valueType="jsonCode"
|
||||
text={JSON.stringify(dataSource)}
|
||||
/>
|
||||
</ProCard>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,29 +1,45 @@
|
|||
import React, {useEffect} from 'react';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {App} from "obsidian";
|
||||
import TableView from "./tableview/TableView";
|
||||
import ListView from "./listview/ListView";
|
||||
import {ConfigProvider, ConfigProviderProps, Timeline} from "antd";
|
||||
import {ConfigProvider, theme} from "antd";
|
||||
import enUS from 'antd/locale/en_US';
|
||||
import TimeLineView from "./timelineview/TimeLineView";
|
||||
import KanbanView from "./kanbanview/KanbanView";
|
||||
|
||||
|
||||
interface QueryDashViewDashs {
|
||||
app: App; // Obsidian app
|
||||
source: string;
|
||||
}
|
||||
|
||||
|
||||
const QueryDashView: React.FC<QueryDashViewDashs> = ({app, source}) => {
|
||||
|
||||
|
||||
const [sourceType, setSourceType] = React.useState<string>("table");
|
||||
const [sourceType, setSourceType] = useState<string>("table");
|
||||
const [isDarkTheme, setIsDarkTheme] = useState<boolean>(
|
||||
document.body.classList.contains("theme-dark")
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// 获取第一个单词
|
||||
// Detect Obsidian's theme
|
||||
const updateTheme = () => {
|
||||
const isDark = document.body.classList.contains("theme-dark");
|
||||
setIsDarkTheme(isDark);
|
||||
};
|
||||
|
||||
// Initial theme detection
|
||||
updateTheme();
|
||||
|
||||
// Listen for theme changes
|
||||
const observer = new MutationObserver(updateTheme);
|
||||
observer.observe(document.body, {attributes: true, attributeFilter: ["class"]});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Get the first word of the source
|
||||
const sourceType = source.split(" ")[0].toLowerCase();
|
||||
setSourceType(sourceType);
|
||||
}, []);
|
||||
}, [source]);
|
||||
|
||||
const getView = (app: App, source: string) => {
|
||||
if (sourceType === "table") {
|
||||
|
|
@ -35,13 +51,23 @@ const QueryDashView: React.FC<QueryDashViewDashs> = ({app, source}) => {
|
|||
} else if (sourceType === "task") {
|
||||
return <KanbanView app={app} source={source}/>;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfigProvider locale={enUS}>
|
||||
<ConfigProvider
|
||||
locale={enUS}
|
||||
theme={{
|
||||
algorithm: isDarkTheme ? theme.darkAlgorithm : theme.defaultAlgorithm,
|
||||
// token: {
|
||||
// colorPrimary: isDarkTheme ? "#1E90FF" : "#1890FF", // 自定义主色
|
||||
// colorBgBase: isDarkTheme ? "#1F1F1F" : "#FFFFFF", // 自定义背景色
|
||||
// colorText: isDarkTheme ? "#E0E0E0" : "#000000", // 自定义文本颜色
|
||||
// },
|
||||
}}
|
||||
>
|
||||
{getView(app, source)}
|
||||
</ConfigProvider>
|
||||
)
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default QueryDashView;
|
||||
|
|
|
|||
77
src/pages/kanbanview/EditableText.tsx
Normal file
77
src/pages/kanbanview/EditableText.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import React, {useState} from "react";
|
||||
import {Input, Typography} from "antd";
|
||||
import {EditOutlined} from "@ant-design/icons";
|
||||
import {TFile} from "obsidian";
|
||||
|
||||
const {Text, Link} = Typography;
|
||||
|
||||
|
||||
const EditableText: React.FC<{ app: any, item: any; onSave: (newText: string) => void; }> = ({app, item, onSave}) => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editableText, setEditableText] = useState(item.text);
|
||||
|
||||
const handleSave = () => {
|
||||
setIsEditing(false);
|
||||
onSave(editableText); // 保存编辑后的值
|
||||
};
|
||||
|
||||
const getSelectionState = (item: any) => {
|
||||
const selectionState = {
|
||||
eState: {
|
||||
cursor: {
|
||||
from: {line: item.line, ch: item.position.start.col},
|
||||
to: {line: item.line + item.lineCount - 1, ch: item.position.end.col},
|
||||
},
|
||||
line: item.line,
|
||||
},
|
||||
};
|
||||
return selectionState;
|
||||
};
|
||||
|
||||
const ellipsisLink = (item: any, display: string, path: string) => (
|
||||
<Link
|
||||
onClick={() => {
|
||||
const selectionState = getSelectionState(item);
|
||||
// MacOS interprets the Command key as Meta.
|
||||
app.workspace.openLinkText(
|
||||
item.link.toFile().obsidianLink(),
|
||||
item.path,
|
||||
false,
|
||||
selectionState as any
|
||||
);
|
||||
/*const file = app.vault.getAbstractFileByPath(path);
|
||||
if (file && file instanceof TFile) {
|
||||
const leaf = app.workspace.getLeaf();
|
||||
leaf.openFile(file);
|
||||
}*/
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
ellipsis={{expanded: true}}
|
||||
>
|
||||
{display}
|
||||
</Text>
|
||||
|
||||
</Link>
|
||||
);
|
||||
|
||||
return isEditing ? (
|
||||
<Input
|
||||
value={editableText}
|
||||
onChange={(e) => setEditableText(e.target.value)}
|
||||
onBlur={handleSave} // 失去焦点时保存
|
||||
onPressEnter={handleSave} // 按下回车时保存
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<div style={{display: "flex"}}>
|
||||
{ellipsisLink(item, editableText, item.path)}
|
||||
<EditOutlined
|
||||
style={{marginLeft: 8, cursor: "pointer"}}
|
||||
onClick={() => setIsEditing(true)} // 点击图标进入编辑模式
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditableText;
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
import React, {useEffect, useState} from "react";
|
||||
import {ProCard} from "@ant-design/pro-components";
|
||||
import {Checkbox, CheckboxChangeEvent, CheckboxProps, Typography} from "antd";
|
||||
import {Checkbox, CheckboxChangeEvent, CheckboxProps, Col, Input, Row, Typography} from "antd";
|
||||
import {getAPI, STask} from "obsidian-dataview";
|
||||
import {ViewProps} from "../../models/ViewProps";
|
||||
import {TFile, Vault} from "obsidian";
|
||||
import EditableText from "./EditableText";
|
||||
import EditableTable from "../../components/EditableTable";
|
||||
|
||||
const {Text, Link} = Typography;
|
||||
|
||||
|
|
@ -21,21 +23,29 @@ const KanbanView: React.FC<ViewProps> = ({app, source}) => {
|
|||
leaf.openFile(file);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Text ellipsis>{display}</Text>
|
||||
>
|
||||
<Text
|
||||
style={display ? {maxWidth: 200, color: '#1890ff', cursor: 'pointer'} : {
|
||||
color: '#1890ff',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
ellipsis={{expanded: true}}
|
||||
>
|
||||
→
|
||||
</Text>
|
||||
|
||||
</Link>
|
||||
);
|
||||
|
||||
const parseTaskData = (value: any) => {
|
||||
const tasks = value.values || [];
|
||||
const groupedTasks: Record<string, any[]> = {
|
||||
done: [],
|
||||
doing: [],
|
||||
todo: [],
|
||||
done: [],
|
||||
};
|
||||
|
||||
tasks.forEach((task: any) => {
|
||||
const status = task.status === "x" ? "done" : task.status === "?" ? "doing" : "todo";
|
||||
const status = task.status === "x" ? "done" : "todo";
|
||||
groupedTasks[status].push({
|
||||
...task,
|
||||
type: "link",
|
||||
|
|
@ -48,24 +58,25 @@ const KanbanView: React.FC<ViewProps> = ({app, source}) => {
|
|||
}));
|
||||
};
|
||||
|
||||
const executeTaskQuery = async (dvApi: any, source: any) => {
|
||||
const queryResult = await dvApi.query(source);
|
||||
console.log("queryResult", queryResult);
|
||||
if (queryResult.successful) {
|
||||
return parseTaskData(queryResult.value);
|
||||
}
|
||||
return [];
|
||||
const executeTaskQuery = (dvApi: any, source: any) => {
|
||||
dvApi.query(source).then((result: any) => {
|
||||
console.log("queryResult", result);
|
||||
if (result.successful) {
|
||||
const data = parseTaskData(result.value);
|
||||
console.log("Data:", data); // 输出 data
|
||||
setColumns([...data]);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onChange: (e: CheckboxChangeEvent, item: STask) => void = async (e, item) => {
|
||||
console.log("completed", e.target.checked);
|
||||
const onChange: (checked: boolean, updateText: string, item: STask) => void = async (checked, updateText, item) => {
|
||||
console.log("completed", checked);
|
||||
console.log("item", item);
|
||||
const completed = e.target.checked;
|
||||
const completed = checked;
|
||||
const status = completed ? "x" : " ";
|
||||
console.log("status", status);
|
||||
let updatedText: string = item.text;
|
||||
await rewriteTask(app.vault, item, status, updatedText)
|
||||
setRefresh(true)
|
||||
let updatedText: string = updateText;
|
||||
await rewriteTask(app.vault, item, status, updatedText);
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -118,34 +129,84 @@ const KanbanView: React.FC<ViewProps> = ({app, source}) => {
|
|||
let newText = filetext.join(hasRN ? "\r\n" : "\n");
|
||||
console.log("newText", newText);
|
||||
console.log("taskPath", task.path);
|
||||
await vault.adapter.write(task.path, newText);
|
||||
vault.adapter.write(task.path, newText).then((res) => {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
executeTaskQuery(dv, source)
|
||||
.then((data) => setColumns(JSON.parse(JSON.stringify(data))))
|
||||
.catch((error) => console.error("Error executing query:", error));
|
||||
}, [app, source, refresh]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
|
||||
const onModify = (file: TFile) => {
|
||||
setTimeout(() => {
|
||||
setRefresh((prev) => !prev); // 触发刷新
|
||||
}, 300); // 延迟 500 毫秒,可根据实际情况调整
|
||||
console.log("文件已修改:", file.path);
|
||||
};
|
||||
|
||||
const onCreate = (file: TFile) => {
|
||||
console.log("文件已创建:", file.path);
|
||||
setRefresh(!refresh); // 触发刷新
|
||||
};
|
||||
|
||||
const onDelete = (file: TFile) => {
|
||||
console.log("文件已删除:", file.path);
|
||||
setRefresh(!refresh); // 触发刷新
|
||||
};
|
||||
|
||||
// 监听文件事件
|
||||
app.vault.on("modify", onModify);
|
||||
app.vault.on("create", onCreate);
|
||||
app.vault.on("delete", onDelete);
|
||||
|
||||
// 清理事件监听器
|
||||
return () => {
|
||||
app.vault.off("modify", onModify);
|
||||
app.vault.off("create", onCreate);
|
||||
app.vault.off("delete", onDelete);
|
||||
};
|
||||
}, [app]);
|
||||
|
||||
return (
|
||||
<ProCard ghost gutter={8}>
|
||||
{columns.map((column) => (
|
||||
<ProCard key={column.title} title={column.title} bordered>
|
||||
{column.items.map((item: any, index: number) => (
|
||||
<>
|
||||
<ProCard key={index} bordered>
|
||||
<Checkbox onChange={(e) => onChange(e, item)} checked={item.checked}>
|
||||
{item.type === "link"
|
||||
? ellipsisLink(item.text, item.path)
|
||||
: <Text>{item.text}</Text>}
|
||||
</Checkbox>
|
||||
</ProCard>
|
||||
</>
|
||||
))}
|
||||
</ProCard>
|
||||
))}
|
||||
</ProCard>
|
||||
<>
|
||||
{/*<EditableTable></EditableTable>*/}
|
||||
|
||||
<ProCard ghost gutter={8}>
|
||||
{columns.map((column) => (
|
||||
<ProCard key={column.title} title={column.title} bordered>
|
||||
{column.items.map((item: any, index: number) => (
|
||||
<>
|
||||
<ProCard key={index} bordered>
|
||||
<Row>
|
||||
<Col span={1}>
|
||||
<Checkbox
|
||||
onChange={(e) => onChange(e.target.checked, item.text, item)}
|
||||
checked={item.checked}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={18}>
|
||||
<EditableText
|
||||
app={app}
|
||||
item={item}
|
||||
onSave={(newText) => {
|
||||
console.log("保存的新文本:", newText);
|
||||
onChange(item.checked, newText, item);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</ProCard>
|
||||
</>
|
||||
))}
|
||||
</ProCard>
|
||||
))}
|
||||
</ProCard>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue