Initial table finished

This commit is contained in:
Akaswan 2025-01-01 14:21:31 -06:00
parent a0cd532701
commit 6eb0f4273c
10 changed files with 2618 additions and 140 deletions

View file

@ -15,7 +15,7 @@ const context = await esbuild.context({
banner: {
js: banner,
},
entryPoints: ["main.ts"],
entryPoints: ["src/main/main.ts"],
bundle: true,
external: [
"obsidian",

42
main.css Normal file
View file

@ -0,0 +1,42 @@
/* styles.css */
.table-container {
border: var(--border-width) solid var(--background-modifier-border);
border-radius: 8px;
overflow: hidden;
max-width: 100%;
--one-seventh-width: 0px;
}
.table {
width: 100%;
border-collapse: collapse;
text-align: left;
}
.table-header {
background-color: var(--background-secondary);
}
.table th {
padding: 12px 16px;
font-size: small;
font-weight: 500;
}
.top-left {
width: 128px;
}
.table td {
padding: 12px 16px;
font-size: small;
height: 68px;
border-top: var(--border-width) solid var(--background-modifier-border);
}
.taskcell-enclosure {
border-left: var(--border-width) solid var(--background-modifier-border);
}
.project-input {
border: none !important;
width: 100%;
}
.date {
border-left: var(--border-width) solid var(--background-modifier-border);
width: var(--taskcell-enclosure-width);
}
/*# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsic3R5bGVzLmNzcyJdLAogICJzb3VyY2VzQ29udGVudCI6IFsiLyogRGVmaW5lIGEgY3VzdG9tIGJvcmRlciBjb2xvciAqL1xyXG4udGFibGUtY29udGFpbmVyIHtcclxuICAgIGJvcmRlcjogdmFyKC0tYm9yZGVyLXdpZHRoKSBzb2xpZCB2YXIoLS1iYWNrZ3JvdW5kLW1vZGlmaWVyLWJvcmRlcik7IC8qIEN1c3RvbSBib3JkZXIgY29sb3IgKi9cclxuICAgIGJvcmRlci1yYWRpdXM6IDhweDsgLyogUm91bmRlZCBjb3JuZXJzICovXHJcbiAgICBvdmVyZmxvdzogaGlkZGVuO1xyXG4gICAgbWF4LXdpZHRoOiAxMDAlO1xyXG4gICAgLS1vbmUtc2V2ZW50aC13aWR0aDogMHB4O1xyXG4gIH1cclxuICBcclxuICAudGFibGUge1xyXG4gICAgd2lkdGg6IDEwMCU7XHJcbiAgICBib3JkZXItY29sbGFwc2U6IGNvbGxhcHNlO1xyXG4gICAgdGV4dC1hbGlnbjogbGVmdDtcclxuICB9XHJcbiAgXHJcbiAgLnRhYmxlLWhlYWRlciB7XHJcbiAgICBiYWNrZ3JvdW5kLWNvbG9yOiB2YXIoLS1iYWNrZ3JvdW5kLXNlY29uZGFyeSk7XHJcbiAgfVxyXG4gIFxyXG4gIC50YWJsZSB0aCB7XHJcbiAgICBwYWRkaW5nOiAxMnB4IDE2cHg7XHJcbiAgICBmb250LXNpemU6IHNtYWxsO1xyXG4gICAgZm9udC13ZWlnaHQ6IDUwMDtcclxuICB9IFxyXG5cclxuICAudG9wLWxlZnQge1xyXG4gICAgd2lkdGg6IDEyOHB4O1xyXG4gIH1cclxuXHJcbiAgLnRhYmxlIHRkIHtcclxuICAgIHBhZGRpbmc6IDEycHggMTZweDtcclxuICAgIGZvbnQtc2l6ZTogc21hbGw7XHJcbiAgICBoZWlnaHQ6IDY4cHg7XHJcbiAgICBib3JkZXItdG9wOiB2YXIoLS1ib3JkZXItd2lkdGgpIHNvbGlkIHZhcigtLWJhY2tncm91bmQtbW9kaWZpZXItYm9yZGVyKTtcclxuICB9XHJcblxyXG4gIC50YXNrY2VsbC1lbmNsb3N1cmUge1xyXG4gICAgYm9yZGVyLWxlZnQ6IHZhcigtLWJvcmRlci13aWR0aCkgc29saWQgdmFyKC0tYmFja2dyb3VuZC1tb2RpZmllci1ib3JkZXIpO1xyXG4gIH1cclxuXHJcbiAgLnByb2plY3QtaW5wdXQge1xyXG4gICAgYm9yZGVyOiBub25lICFpbXBvcnRhbnQ7XHJcbiAgICB3aWR0aDogMTAwJTtcclxuICB9XHJcblxyXG4gIC5kYXRlIHtcclxuICAgIGJvcmRlci1sZWZ0OiB2YXIoLS1ib3JkZXItd2lkdGgpIHNvbGlkIHZhcigtLWJhY2tncm91bmQtbW9kaWZpZXItYm9yZGVyKTsgXHJcbiAgICB3aWR0aDogdmFyKC0tdGFza2NlbGwtZW5jbG9zdXJlLXdpZHRoKTtcclxuICB9Il0sCiAgIm1hcHBpbmdzIjogIjtBQUNBO0FBQ0k7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUFBO0FBR0Y7QUFDRTtBQUNBO0FBQ0E7QUFBQTtBQUdGO0FBQ0U7QUFBQTtBQUdGO0FBQ0U7QUFDQTtBQUNBO0FBQUE7QUFHRjtBQUNFO0FBQUE7QUFHRjtBQUNFO0FBQ0E7QUFDQTtBQUNBO0FBQUE7QUFHRjtBQUNFO0FBQUE7QUFHRjtBQUNFO0FBQ0E7QUFBQTtBQUdGO0FBQ0U7QUFDQTtBQUFBOyIsCiAgIm5hbWVzIjogW10KfQo= */

134
main.ts
View file

@ -1,134 +0,0 @@
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
// Remember to rename these classes and interfaces!
interface MyPluginSettings {
mySetting: string;
}
const DEFAULT_SETTINGS: MyPluginSettings = {
mySetting: 'default'
}
export default class MyPlugin extends Plugin {
settings: MyPluginSettings;
async onload() {
await this.loadSettings();
// This creates an icon in the left ribbon.
const ribbonIconEl = this.addRibbonIcon('dice', 'Sample Plugin', (evt: MouseEvent) => {
// Called when the user clicks the icon.
new Notice('This is a notice!');
});
// Perform additional things with the ribbon
ribbonIconEl.addClass('my-plugin-ribbon-class');
// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
const statusBarItemEl = this.addStatusBarItem();
statusBarItemEl.setText('Status Bar Text');
// This adds a simple command that can be triggered anywhere
this.addCommand({
id: 'open-sample-modal-simple',
name: 'Open sample modal (simple)',
callback: () => {
new SampleModal(this.app).open();
}
});
// This adds an editor command that can perform some operation on the current editor instance
this.addCommand({
id: 'sample-editor-command',
name: 'Sample editor command',
editorCallback: (editor: Editor, view: MarkdownView) => {
console.log(editor.getSelection());
editor.replaceSelection('Sample Editor Command');
}
});
// This adds a complex command that can check whether the current state of the app allows execution of the command
this.addCommand({
id: 'open-sample-modal-complex',
name: 'Open sample modal (complex)',
checkCallback: (checking: boolean) => {
// Conditions to check
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (markdownView) {
// If checking is true, we're simply "checking" if the command can be run.
// If checking is false, then we want to actually perform the operation.
if (!checking) {
new SampleModal(this.app).open();
}
// This command will only show up in Command Palette when the check function returns true
return true;
}
}
});
// This adds a settings tab so the user can configure various aspects of the plugin
this.addSettingTab(new SampleSettingTab(this.app, this));
// If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
// Using this function will automatically remove the event listener when this plugin is disabled.
this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
console.log('click', evt);
});
// When registering intervals, this function will automatically clear the interval when the plugin is disabled.
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
}
onunload() {
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
}
class SampleModal extends Modal {
constructor(app: App) {
super(app);
}
onOpen() {
const {contentEl} = this;
contentEl.setText('Woah!');
}
onClose() {
const {contentEl} = this;
contentEl.empty();
}
}
class SampleSettingTab extends PluginSettingTab {
plugin: MyPlugin;
constructor(app: App, plugin: MyPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const {containerEl} = this;
containerEl.empty();
new Setting(containerEl)
.setName('Setting #1')
.setDesc('It\'s a secret')
.addText(text => text
.setPlaceholder('Enter your secret')
.setValue(this.plugin.settings.mySetting)
.onChange(async (value) => {
this.plugin.settings.mySetting = value;
await this.plugin.saveSettings();
}));
}
}

2291
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -13,6 +13,8 @@
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
@ -20,5 +22,10 @@
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4"
},
"dependencies": {
"date-fns": "^4.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}

47
src/main/main.ts Normal file
View file

@ -0,0 +1,47 @@
import { Plugin, WorkspaceLeaf } from "obsidian";
import { TableView, TABLE_VIEW_TYPE } from "./views/TableView";
export default class TableList extends Plugin {
async onload() {
this.registerView(TABLE_VIEW_TYPE, (leaf) => new TableView(leaf));
this.addRibbonIcon("table-2", "Activate view", () => {
this.activateView();
});
if (!this.app.vault.getFolderByPath("TableList")) {
this.app.vault.createFolder("TableList");
} else {
console.log("Folder already exists");
}
}
async onunload() {}
async activateView() {
const { workspace } = this.app;
let leaf: WorkspaceLeaf | null = null;
const leaves = workspace.getLeavesOfType(TABLE_VIEW_TYPE);
if (leaves.length > 0) {
// A leaf with our view already exists, use that
leaf = leaves[0];
} else {
// Our view could not be found in the workspace, create a new leaf
// in the right sidebar for it
leaf = workspace.getLeaf(false);
if (leaf) {
await leaf.setViewState({
type: TABLE_VIEW_TYPE,
active: true,
});
}
}
// "Reveal" the leaf in case it is in a collapsed sidebar
if (leaf) {
workspace.revealLeaf(leaf);
}
}
}

144
src/main/react/Table.tsx Normal file
View file

@ -0,0 +1,144 @@
import "styles.css";
import { format, addDays, startOfWeek } from "date-fns";
import { useEffect, useRef, useState } from "react";
import * as React from "react";
interface Project {
id: number;
name: string;
tasks: Task[];
}
interface Task {
id: number;
name: string;
date: Date;
parentProjectId: number;
}
const getInitialDates = () => {
const startDate = startOfWeek(new Date(), { weekStartsOn: 1 });
const dates = Array.from({ length: 7 }, (_, i) => {
return format(addDays(startDate, i), "yyyy-MM-dd");
});
return dates;
};
const getInitialProjects = () => {
const projects: Project[] = [
{
id: 1,
name: "Project 1",
tasks: [
{
id: 1,
name: "Task 1",
date: new Date(),
parentProjectId: 1,
},
],
},
{
id: 2,
name: "Project 2",
tasks: [
{
id: 2,
name: "Task 2",
date: new Date(),
parentProjectId: 2,
},
],
},
];
return projects;
};
const Table: React.FC = () => {
const [dates, setDates] = useState(getInitialDates);
const [projects, setProjects] = useState(getInitialProjects);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleResize = () => {
const containerWidth = containerRef.current?.clientWidth;
if (containerWidth) {
document.documentElement.style.setProperty(
"--taskcell-enclosure-width",
`${(containerWidth - 128) / 7}px`
);
}
};
const resizeObserver = new ResizeObserver(handleResize);
if (containerRef.current) {
resizeObserver.observe(containerRef.current);
}
// Initial call to set the variable
handleResize();
return () => {
if (containerRef.current) {
resizeObserver.unobserve(containerRef.current);
}
};
}, [containerRef]);
const handleProjectNameChange = (id: number, newName: string) => {
setProjects((prevProjects) =>
prevProjects.map((project) =>
project.id === id ? { ...project, name: newName } : project
)
);
console.log(projects);
};
return (
<div className="table-container" ref={containerRef}>
<table className="table headings-center">
<thead className="table-header">
<tr>
<th>Projects</th>
{dates.map((date) => (
<th className="date" key={date}>{`${format(
date,
"EEEE"
)} - ${format(date, "yyyy-MM-dd")}`}</th>
))}
</tr>
</thead>
<tbody>
{projects.map((project) => (
<tr key={project.id}>
<td key={project.id}>
<input
type="text"
value={project.name}
className="project-input"
placeholder="New Project"
onChange={(e) =>
handleProjectNameChange(
project.id,
e.target.value
)
}
/>
</td>
{dates.map((date, index) => (
<td
className="taskcell-enclosure"
key={`${project.id}:${date}:${index}`}
></td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
};
export default Table;

View file

@ -0,0 +1,39 @@
import { StrictMode } from 'react';
import { ItemView, WorkspaceLeaf } from 'obsidian';
import { Root, createRoot } from 'react-dom/client';
import Table from 'src/main/react/Table';
export const TABLE_VIEW_TYPE = 'table-view';
export class TableView extends ItemView {
root: Root | null = null;
constructor(leaf: WorkspaceLeaf) {
super(leaf);
}
getViewType() {
return TABLE_VIEW_TYPE;
}
getDisplayText() {
return 'TableList';
}
getIcon() {
return 'table-2';
}
async onOpen() {
this.root = createRoot(this.containerEl.children[1]);
this.root.render(
<StrictMode>
<Table />
</StrictMode>
);
}
async onClose() {
this.root?.unmount();
}
}

View file

@ -1,8 +1,49 @@
/*
/* Define a custom border color */
.table-container {
border: var(--border-width) solid var(--background-modifier-border); /* Custom border color */
border-radius: 8px; /* Rounded corners */
overflow: hidden;
max-width: 100%;
--one-seventh-width: 0px;
}
.table {
width: 100%;
border-collapse: collapse;
text-align: left;
}
.table-header {
background-color: var(--background-secondary);
}
.table th {
padding: 12px 16px;
font-size: small;
font-weight: 500;
}
This CSS file will be included with your plugin, and
available in the app when your plugin is enabled.
.top-left {
width: 128px;
}
If your plugin does not need CSS, delete this file.
.table td {
padding: 12px 16px;
font-size: small;
height: 68px;
border-top: var(--border-width) solid var(--background-modifier-border);
}
*/
.taskcell-enclosure {
border-left: var(--border-width) solid var(--background-modifier-border);
}
.project-input {
border: none !important;
width: 100%;
}
.date {
border-left: var(--border-width) solid var(--background-modifier-border);
width: var(--taskcell-enclosure-width);
}

View file

@ -6,6 +6,7 @@
"module": "ESNext",
"target": "ES6",
"allowJs": true,
"jsx": "react-jsx",
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,