mirror of
https://github.com/liubinfighter/csv-lite.git
synced 2026-07-22 05:43:52 +00:00
Merge pull request #44 from LIUBINfighter/dev
Some fixes (Fixes #41, Fixes #35)
This commit is contained in:
commit
c73e596e08
13 changed files with 149 additions and 25 deletions
42
.github/workflows/ci.yml
vendored
Normal file
42
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "dev", "master", "main" ]
|
||||
pull_request:
|
||||
branches: [ "dev", "master", "main" ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Prettier check (if present)
|
||||
run: |
|
||||
if npx --no-install prettier --version >/dev/null 2>&1; then
|
||||
npx prettier --check "**/*.{ts,js,json,md,css}" || (echo "Prettier check failed" && exit 1)
|
||||
else
|
||||
echo "Prettier not installed - skipping check"
|
||||
fi
|
||||
|
||||
- name: TypeScript typecheck and build
|
||||
run: npm run build
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "csv-lite",
|
||||
"name": "CSV Lite",
|
||||
"version": "1.1.4",
|
||||
"version": "1.1.5",
|
||||
"minAppVersion": "1.8.0",
|
||||
"description": "Just open and edit CSV files directly, no more. Keep it simple.",
|
||||
"author": "Jay Bridge",
|
||||
|
|
|
|||
2
package-lock.json
generated
2
package-lock.json
generated
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "obsidian-csv",
|
||||
"version": "1.1.4",
|
||||
"version": "1.1.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "obsidian-csv",
|
||||
"version": "1.1.4",
|
||||
"version": "1.1.5",
|
||||
"description": "CSV viewer and editor for Obsidian",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ export const enUS = {
|
|||
},
|
||||
csv: {
|
||||
error: 'Error',
|
||||
parsingFailed: 'Failed to parse CSV. Please check file format.'
|
||||
parsingFailed: 'Failed to parse CSV. Please check file format.',
|
||||
parseWarning: 'CSV parse warning:'
|
||||
},
|
||||
settings: {
|
||||
fieldSeparator: 'Field Separator',
|
||||
|
|
@ -45,4 +46,16 @@ export const enUS = {
|
|||
moveColLeft: 'Move column left',
|
||||
moveColRight: 'Move column right',
|
||||
}
|
||||
,
|
||||
tableMessages: {
|
||||
atLeastOneRow: 'At least one row must remain',
|
||||
atLeastOneColumn: 'At least one column must remain'
|
||||
}
|
||||
,
|
||||
notifications: {
|
||||
undo: 'Undid last action',
|
||||
noMoreUndo: 'There is nothing more to undo',
|
||||
redo: 'Redid action',
|
||||
noMoreRedo: 'There is nothing more to redo'
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ export const zhCN = {
|
|||
},
|
||||
csv: {
|
||||
error: '错误',
|
||||
parsingFailed: 'CSV解析失败,请检查文件格式'
|
||||
parsingFailed: 'CSV解析失败,请检查文件格式',
|
||||
parseWarning: 'CSV解析提示:'
|
||||
},
|
||||
settings: {
|
||||
fieldSeparator: '字段分隔符',
|
||||
|
|
@ -47,4 +48,16 @@ export const zhCN = {
|
|||
moveColLeft: '向左移动一列',
|
||||
moveColRight: '向右移动一列',
|
||||
}
|
||||
,
|
||||
tableMessages: {
|
||||
atLeastOneRow: '至少需要保留一行',
|
||||
atLeastOneColumn: '至少需要保留一列'
|
||||
}
|
||||
,
|
||||
notifications: {
|
||||
undo: '已撤销上一步操作',
|
||||
noMoreUndo: '没有更多可撤销的操作',
|
||||
redo: '已重做操作',
|
||||
noMoreRedo: '没有更多可重做的操作'
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { EditorState, Extension, RangeSetBuilder } from "@codemirror/state";
|
|||
import { EditorView, keymap, placeholder, lineNumbers, drawSelection, Decoration, ViewPlugin, ViewUpdate, DecorationSet } from "@codemirror/view";
|
||||
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
|
||||
|
||||
export const VIEW_TYPE_CSV_SOURCE = "csv-source-view";
|
||||
export const VIEW_TYPE_CSV_SOURCE = "csv-lite-source-view";
|
||||
|
||||
// 分隔符高亮插件(逗号、分号、制表符)
|
||||
const separatorHighlightPlugin = ViewPlugin.fromClass(class {
|
||||
|
|
@ -62,7 +62,7 @@ export class SourceView extends TextFileView {
|
|||
// 1. 在 view header 的 view-actions 区域插入切换按钮(lucide/table 图标)
|
||||
// 交互说明:
|
||||
// - 切换按钮始终位于 header 区域,风格与 Obsidian 原生一致。
|
||||
// - 点击时遍历所有 leaf,查找同一文件的目标视图(csv-view)。
|
||||
// - 点击时遍历所有 leaf,查找同一文件的目标视图(csv-lite-view)。
|
||||
// - 若有,则激活该 leaf(workspace.setActiveLeaf)。
|
||||
// - 若无,则新建 leaf 并打开目标视图。
|
||||
// - 不主动关闭原有视图,用户可自行关闭。
|
||||
|
|
@ -75,7 +75,7 @@ export class SourceView extends TextFileView {
|
|||
btn.onclick = async () => {
|
||||
const file = this.file;
|
||||
if (!file) return;
|
||||
const leaves = this.app.workspace.getLeavesOfType('csv-view');
|
||||
const leaves = this.app.workspace.getLeavesOfType('csv-lite-view');
|
||||
let found = false;
|
||||
for (const leaf of leaves) {
|
||||
if (leaf.view && (leaf.view as any).file && (leaf.view as any).file.path === file.path) {
|
||||
|
|
@ -88,7 +88,7 @@ export class SourceView extends TextFileView {
|
|||
const newLeaf = this.app.workspace.getLeaf(true);
|
||||
await newLeaf.openFile(file, { active: true });
|
||||
await newLeaf.setViewState({
|
||||
type: 'csv-view',
|
||||
type: 'csv-lite-view',
|
||||
active: true,
|
||||
state: { file: file.path }
|
||||
});
|
||||
|
|
|
|||
|
|
@ -114,14 +114,14 @@ export class CSVUtils {
|
|||
const parseResult: any = Papa.parse(csvString, parseConfig as any);
|
||||
|
||||
if (parseResult.errors && parseResult.errors.length > 0) {
|
||||
console.warn("CSV解析警告:", parseResult.errors);
|
||||
new Notice(`CSV解析提示: ${parseResult.errors[0].message}`);
|
||||
console.warn("CSV parse warnings:", parseResult.errors);
|
||||
new Notice(`${i18n.t("csv.parseWarning")} ${parseResult.errors[0].message}`);
|
||||
}
|
||||
|
||||
return parseResult.data as string[][];
|
||||
} catch (error) {
|
||||
console.error("CSV解析错误:", error);
|
||||
new Notice(`${i18n.t("csv.error")}: CSV解析失败,请检查文件格式`);
|
||||
console.error("CSV parse error:", error);
|
||||
new Notice(i18n.t("csv.parsingFailed"));
|
||||
return [[""]];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Notice } from "obsidian";
|
||||
import { i18n } from "../i18n";
|
||||
|
||||
export class HistoryManager<T> {
|
||||
private history: T[] = [];
|
||||
|
|
@ -38,10 +39,10 @@ export class HistoryManager<T> {
|
|||
undo(): T | null {
|
||||
if (this.canUndo()) {
|
||||
this.currentIndex--;
|
||||
new Notice("已撤销上一步操作");
|
||||
new Notice(i18n.t("notifications.undo"));
|
||||
return this.getCurrentState();
|
||||
} else {
|
||||
new Notice("没有更多可撤销的操作");
|
||||
new Notice(i18n.t("notifications.noMoreUndo"));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -52,10 +53,10 @@ export class HistoryManager<T> {
|
|||
redo(): T | null {
|
||||
if (this.canRedo()) {
|
||||
this.currentIndex++;
|
||||
new Notice("已重做操作");
|
||||
new Notice(i18n.t("notifications.redo"));
|
||||
return this.getCurrentState();
|
||||
} else {
|
||||
new Notice("没有更多可重做的操作");
|
||||
new Notice(i18n.t("notifications.noMoreRedo"));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Notice } from "obsidian";
|
||||
import { i18n } from "../i18n";
|
||||
|
||||
export class TableUtils {
|
||||
/**
|
||||
|
|
@ -42,7 +43,7 @@ export class TableUtils {
|
|||
*/
|
||||
static deleteRow(tableData: string[][]): string[][] {
|
||||
if (tableData.length <= 1) {
|
||||
new Notice("至少需要保留一行");
|
||||
new Notice(i18n.t("tableMessages.atLeastOneRow"));
|
||||
return tableData;
|
||||
}
|
||||
|
||||
|
|
@ -61,7 +62,7 @@ export class TableUtils {
|
|||
*/
|
||||
static deleteColumn(tableData: string[][]): string[][] {
|
||||
if (!tableData[0] || tableData[0].length <= 1) {
|
||||
new Notice("至少需要保留一列");
|
||||
new Notice(i18n.t("tableMessages.atLeastOneColumn"));
|
||||
return tableData;
|
||||
}
|
||||
|
||||
|
|
|
|||
13
src/view.ts
13
src/view.ts
|
|
@ -19,7 +19,7 @@ import { renderTable } from "./view/table-render";
|
|||
import { HighlightManager } from "./utils/highlight-manager";
|
||||
import { setupHeaderContextMenu } from "./view/header-context-menu";
|
||||
|
||||
export const VIEW_TYPE_CSV = "csv-view";
|
||||
export const VIEW_TYPE_CSV = "csv-lite-view";
|
||||
|
||||
export class CSVView extends TextFileView {
|
||||
public file: TFile | null;
|
||||
|
|
@ -559,7 +559,7 @@ export class CSVView extends TextFileView {
|
|||
// 1. 在 view header 的 view-actions 区域插入切换按钮(lucide/file-code 图标)
|
||||
// 交互说明:
|
||||
// - 切换按钮始终位于 header 区域,风格与 Obsidian 原生一致。
|
||||
// - 点击时遍历所有 leaf,查找同一文件的目标视图(csv-source-view)。
|
||||
// - 点击时遍历所有 leaf,查找同一文件的目标视图(csv-lite-source-view)。
|
||||
// - 若有,则激活该 leaf(workspace.setActiveLeaf)。
|
||||
// - 若无,则新建 leaf 并打开目标视图。
|
||||
// - 不主动关闭原有视图,用户可自行关闭。
|
||||
|
|
@ -572,7 +572,7 @@ export class CSVView extends TextFileView {
|
|||
btn.onclick = async () => {
|
||||
const file = this.file;
|
||||
if (!file) return;
|
||||
const leaves = this.app.workspace.getLeavesOfType('csv-source-view');
|
||||
const leaves = this.app.workspace.getLeavesOfType('csv-lite-source-view');
|
||||
let found = false;
|
||||
for (const leaf of leaves) {
|
||||
if (leaf.view && (leaf.view as any).file && (leaf.view as any).file.path === file.path) {
|
||||
|
|
@ -585,7 +585,7 @@ export class CSVView extends TextFileView {
|
|||
const newLeaf = this.app.workspace.getLeaf(true);
|
||||
await newLeaf.openFile(file, { active: true, state: { mode: "source" } });
|
||||
await newLeaf.setViewState({
|
||||
type: "csv-source-view",
|
||||
type: "csv-lite-source-view",
|
||||
active: true,
|
||||
state: { file: file.path }
|
||||
});
|
||||
|
|
@ -793,6 +793,9 @@ export class CSVView extends TextFileView {
|
|||
document,
|
||||
"keydown",
|
||||
(event: KeyboardEvent) => {
|
||||
// Only handle undo/redo when this view is the active leaf
|
||||
if (this.app.workspace.activeLeaf !== this.leaf) return;
|
||||
|
||||
// 检测Ctrl+Z (或Mac上的Cmd+Z)
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === "z") {
|
||||
if (event.shiftKey) {
|
||||
|
|
@ -983,7 +986,7 @@ export class CSVView extends TextFileView {
|
|||
const leaf = this.app.workspace.getLeaf(true);
|
||||
await leaf.openFile(file, { active: true, state: { mode: "source" } });
|
||||
await leaf.setViewState({
|
||||
type: "csv-source-view",
|
||||
type: "csv-lite-source-view",
|
||||
active: true,
|
||||
state: { file: file.path }
|
||||
});
|
||||
|
|
|
|||
50
test/history-manager.test.ts
Normal file
50
test/history-manager.test.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { TableHistoryManager } from "../src/utils/history-manager";
|
||||
import { Notice } from "obsidian";
|
||||
import { i18n } from "../src/i18n";
|
||||
|
||||
beforeEach(() => {
|
||||
// reset Notice mock
|
||||
(Notice as any).mockClear && (Notice as any).mockClear();
|
||||
});
|
||||
|
||||
test('undo on empty history shows noMoreUndo message', () => {
|
||||
const hm = new TableHistoryManager([["a"]], 10);
|
||||
// Initially only one state, cannot undo
|
||||
const res = hm.undo();
|
||||
expect(res).toBeNull();
|
||||
expect(Notice).toHaveBeenCalledTimes(1);
|
||||
expect((Notice as any).mock.calls[0][0]).toBe(i18n.t('notifications.noMoreUndo'));
|
||||
});
|
||||
|
||||
test('undo when available shows undo message', () => {
|
||||
const hm = new TableHistoryManager([["a"]], 10);
|
||||
hm.push([["b"]]);
|
||||
// Now we can undo
|
||||
const res = hm.undo();
|
||||
expect(res).toEqual([["a"]]);
|
||||
expect(Notice).toHaveBeenCalledTimes(1);
|
||||
expect((Notice as any).mock.calls[0][0]).toBe(i18n.t('notifications.undo'));
|
||||
});
|
||||
|
||||
test('redo on no-op shows noMoreRedo message', () => {
|
||||
const hm = new TableHistoryManager([["a"]], 10);
|
||||
// Nothing to redo
|
||||
const res = hm.redo();
|
||||
expect(res).toBeNull();
|
||||
expect(Notice).toHaveBeenCalledTimes(1);
|
||||
expect((Notice as any).mock.calls[0][0]).toBe(i18n.t('notifications.noMoreRedo'));
|
||||
});
|
||||
|
||||
test('redo when available shows redo message', () => {
|
||||
const hm = new TableHistoryManager([["a"]], 10);
|
||||
hm.push([["b"]]);
|
||||
// undo back to first
|
||||
const u = hm.undo();
|
||||
expect(u).toEqual([["a"]]);
|
||||
// redo
|
||||
const r = hm.redo();
|
||||
expect(r).toEqual([["b"]]);
|
||||
// Two notices were called (undo, redo)
|
||||
expect(Notice).toHaveBeenCalledTimes(2);
|
||||
expect((Notice as any).mock.calls[1][0]).toBe(i18n.t('notifications.redo'));
|
||||
});
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
{
|
||||
"1.0.0": "0.15.0"
|
||||
"1.0.0": "0.15.0",
|
||||
"1.1.5": "1.8.0"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue