mirror of
https://github.com/tiddly-gittly/TidGi-Desktop.git
synced 2025-12-06 02:30:47 -08:00
fix: test and ui logic
This commit is contained in:
parent
09a2c7a005
commit
d76b3ff098
16 changed files with 169 additions and 149 deletions
|
|
@ -19,6 +19,8 @@
|
|||
"CustomServerUrlDescription": "Base URL of the OAuth server (e.g., http://127.0.0.1:8888)",
|
||||
"ExistedWikiLocation": "Existed Wiki Location",
|
||||
"ExtractedWikiFolderName": "Converted WIKI folder name",
|
||||
"FilterExpression": "filter expression",
|
||||
"FilterExpressionHelp": "One TiddlyWiki filter expression per line; any match will be saved to this workspace. For example: [in-tagtree-of[Calendar]!tag[Public]]",
|
||||
"GitBranch": "Git Branch",
|
||||
"GitBranchDescription": "Git branch to use (default: main)",
|
||||
"GitDefaultBranchDescription": "The default branch of your Git, Github changed it from master to main after that event",
|
||||
|
|
@ -70,6 +72,8 @@
|
|||
"TagNameHelp": "Tiddlers with this Tag will be add to this sub-wiki (you can add or change this Tag later, by right-click workspace Icon and choose Edit Workspace)",
|
||||
"TagNameHelpForMain": "New entries with this tag will be prioritized for storage in this workspace.",
|
||||
"ThisPathIsNotAWikiFolder": "The directory is not a Wiki folder \"{{wikiPath}}\"",
|
||||
"UseFilter": "Use filters",
|
||||
"UseFilterHelp": "Use filter expressions instead of tags to match entries and determine whether to save them in the current workspace.",
|
||||
"WaitForLogin": "Wait for Login",
|
||||
"WikiExisted": "Wiki already exists at this location \"{{newWikiPath}}\"",
|
||||
"WikiNotStarted": "Wiki is not started or not loaded",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@
|
|||
"CustomServerUrlDescription": "URL de base du serveur OAuth (par exemple : http://127.0.0.1:8888)",
|
||||
"ExistedWikiLocation": "Emplacement du Wiki existant",
|
||||
"ExtractedWikiFolderName": "Nom du dossier WIKI converti",
|
||||
"FilterExpression": "expression du filtre",
|
||||
"FilterExpressionHelp": "Un filtre d'expression TiddlyWiki par ligne, toute correspondance sera enregistrée dans cet espace de travail. Par exemple : [in-tagtree-of[Calendar]!tag[Public]]",
|
||||
"GitBranch": "Branche Git",
|
||||
"GitBranchDescription": "Branche Git à utiliser (par défaut : main)",
|
||||
"GitDefaultBranchDescription": "La branche par défaut de votre Git, Github l'a changée de master à main après cet événement",
|
||||
|
|
@ -70,6 +72,8 @@
|
|||
"TagNameHelp": "Les tiddlers avec cette étiquette seront ajoutés à ce sous-wiki (vous pouvez ajouter ou modifier cette étiquette plus tard, en cliquant avec le bouton droit sur l'icône de l'espace de travail et en choisissant Modifier l'espace de travail)",
|
||||
"TagNameHelpForMain": "Les nouvelles entrées avec cette étiquette seront prioritairement enregistrées dans cet espace de travail.",
|
||||
"ThisPathIsNotAWikiFolder": "Le répertoire n'est pas un dossier Wiki \"{{wikiPath}}\"",
|
||||
"UseFilter": "utiliser un filtre",
|
||||
"UseFilterHelp": "Utilisez des expressions de filtre plutôt que des étiquettes pour correspondre aux entrées et décider si elles doivent être stockées dans l'espace de travail actuel.",
|
||||
"WaitForLogin": "Attendre la connexion",
|
||||
"WikiExisted": "Le Wiki existe déjà à cet emplacement \"{{newWikiPath}}\"",
|
||||
"WikiNotStarted": "Le Wiki n'est pas démarré ou n'est pas chargé",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@
|
|||
"CustomServerUrlDescription": "OAuthサーバーのベースURL(例:http://127.0.0.1:8888)",
|
||||
"ExistedWikiLocation": "既存のWikiの場所",
|
||||
"ExtractedWikiFolderName": "変換されたWIKIフォルダ名",
|
||||
"FilterExpression": "フィルター式",
|
||||
"FilterExpressionHelp": "TiddlyWikiフィルター式を1行ずつ入力してください。いずれかが一致すると、このワークスペースに保存されます。例:[in-tagtree-of[Calendar]!tag[Public]]",
|
||||
"GitBranch": "Git ブランチ",
|
||||
"GitBranchDescription": "使用するGitブランチ(デフォルト:main)",
|
||||
"GitDefaultBranchDescription": "Gitのデフォルトブランチ。Githubはそのイベント後にmasterからmainに変更しました",
|
||||
|
|
@ -70,6 +72,8 @@
|
|||
"TagNameHelp": "このタグを持つTiddlerはこのサブWikiに追加されます(後で右クリックしてワークスペースアイコンを選択し、ワークスペースを編集することでこのタグを追加または変更できます)",
|
||||
"TagNameHelpForMain": "このタグが付いた新しいエントリは、このワークスペースに優先的に保存されます",
|
||||
"ThisPathIsNotAWikiFolder": "このディレクトリはWikiフォルダではありません \"{{wikiPath}}\"",
|
||||
"UseFilter": "フィルターを使用する",
|
||||
"UseFilterHelp": "フィルター式を使用してエントリをマッチングし、現在のワークスペースに保存するかどうかを決定します。タグではなくフィルター式を用います。",
|
||||
"WaitForLogin": "ログインを待っています",
|
||||
"WikiExisted": "この場所にWikiが既に存在します \"{{newWikiPath}}\"",
|
||||
"WikiNotStarted": "Wikiが開始されていないか、読み込まれていません",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@
|
|||
"CustomServerUrlDescription": "Базовый URL сервера OAuth (например: http://127.0.0.1:8888)",
|
||||
"ExistedWikiLocation": "Местоположение существующей Wiki",
|
||||
"ExtractedWikiFolderName": "Имя папки извлеченной WIKI",
|
||||
"FilterExpression": "выражение фильтра",
|
||||
"FilterExpressionHelp": "Каждая строка содержит выражение фильтра TiddlyWiki. Если хотя бы одно из них совпадает, запись сохраняется в этой рабочей области. Например: [in-tagtree-of[Calendar]!tag[Public]].",
|
||||
"GitBranch": "Ветка Git",
|
||||
"GitBranchDescription": "Используемая ветка Git (по умолчанию: main)",
|
||||
"GitDefaultBranchDescription": "Основная ветка вашего Git, Github изменил ее с master на main после того событ<D18B><D182>я",
|
||||
|
|
@ -70,6 +72,8 @@
|
|||
"TagNameHelp": "Тидлеры с этим тегом будут добавлены в эту под-Wiki (вы можете добавить или изменить этот тег позже, щелкнув правой кнопкой мыши значок рабочего пространства и выбрав Настроить рабочее пространство)",
|
||||
"TagNameHelpForMain": "Новые записи с этой меткой будут сохраняться в первую очередь в этой рабочей области.",
|
||||
"ThisPathIsNotAWikiFolder": "Каталог не является папкой Wiki \"{{wikiPath}}\"",
|
||||
"UseFilter": "использовать фильтр",
|
||||
"UseFilterHelp": "Используйте выражения фильтров вместо меток для сопоставления записей и определения, следует ли сохранять их в текущей рабочей области.",
|
||||
"WaitForLogin": "Ожидание входа",
|
||||
"WikiExisted": "Wiki уже существует в этом месте \"{{newWikiPath}}\"",
|
||||
"WikiNotStarted": "Wiki не запущена или не загружена",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@
|
|||
"CustomServerUrlDescription": "OAuth 伺服器的基礎 URL(例如:http://127.0.0.1:8888)",
|
||||
"ExistedWikiLocation": "現有的知識庫的位置",
|
||||
"ExtractedWikiFolderName": "轉換後的知識庫文件夾名稱",
|
||||
"FilterExpression": "篩選器表達式",
|
||||
"FilterExpressionHelp": "每行一個TiddlyWiki篩選器表達式,任一匹配即存入此工作區。例如 [in-tagtree-of[Calendar]!tag[Public]]",
|
||||
"GitBranch": "Git 分支",
|
||||
"GitBranchDescription": "要使用的 Git 分支(預設:main)",
|
||||
"GitDefaultBranchDescription": "你的Git的預設分支,Github在黑命貴事件後將其從master改為了main",
|
||||
|
|
@ -70,6 +72,8 @@
|
|||
"TagNameHelp": "加上此標籤的筆記將會自動被放入這個子知識庫內(可先不填,之後右鍵點擊這個工作區的圖示選擇編輯工作區修改)",
|
||||
"TagNameHelpForMain": "帶有此標籤的新條目將優先保存在此工作區",
|
||||
"ThisPathIsNotAWikiFolder": "該目錄不是一個知識庫文件夾 \"{{wikiPath}}\"",
|
||||
"UseFilter": "使用篩選器",
|
||||
"UseFilterHelp": "用篩選器運算式而不是標籤來匹配條目,決定是否存入當前工作區",
|
||||
"WaitForLogin": "等待登錄",
|
||||
"WikiExisted": "知識庫已經存在於該位置 \"{{newWikiPath}}\"",
|
||||
"WikiNotStarted": "知識庫 頁面未成功啟動或未成功載入",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
|||
</ThemeProvider>
|
||||
);
|
||||
|
||||
// Helper to get the correct modifier key based on platform
|
||||
// On macOS, ctrlKey is displayed as 'Cmd', on other platforms as 'Ctrl'
|
||||
const getCtrlModifier = () => process.platform === 'darwin' ? 'Cmd' : 'Ctrl';
|
||||
|
||||
describe('KeyboardShortcutRegister Component', () => {
|
||||
let mockOnChange: ReturnType<typeof vi.fn>;
|
||||
|
||||
|
|
@ -158,6 +162,7 @@ describe('KeyboardShortcutRegister Component', () => {
|
|||
const dialogContent = screen.getByTestId('shortcut-dialog-content');
|
||||
|
||||
// Simulate keyboard event with Ctrl+Shift+T
|
||||
// On macOS, ctrlKey is displayed as 'Cmd'
|
||||
fireEvent.keyDown(dialogContent, {
|
||||
key: 'T',
|
||||
ctrlKey: true,
|
||||
|
|
@ -167,7 +172,7 @@ describe('KeyboardShortcutRegister Component', () => {
|
|||
|
||||
await waitFor(() => {
|
||||
const display = screen.getByTestId('shortcut-display');
|
||||
expect(display).toHaveTextContent('Ctrl+Shift+T');
|
||||
expect(display).toHaveTextContent(`${getCtrlModifier()}+Shift+T`);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -284,7 +289,7 @@ describe('KeyboardShortcutRegister Component', () => {
|
|||
|
||||
await waitFor(() => {
|
||||
const display = screen.getByTestId('shortcut-display');
|
||||
expect(display).toHaveTextContent('Ctrl+A');
|
||||
expect(display).toHaveTextContent(`${getCtrlModifier()}+A`);
|
||||
});
|
||||
|
||||
// Press second combination - should replace
|
||||
|
|
@ -297,7 +302,7 @@ describe('KeyboardShortcutRegister Component', () => {
|
|||
|
||||
await waitFor(() => {
|
||||
const display = screen.getByTestId('shortcut-display');
|
||||
expect(display).toHaveTextContent('Ctrl+Shift+B');
|
||||
expect(display).toHaveTextContent(`${getCtrlModifier()}+Shift+B`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -372,14 +377,14 @@ describe('KeyboardShortcutRegister Component', () => {
|
|||
|
||||
await waitFor(() => {
|
||||
const display = screen.getByTestId('shortcut-display');
|
||||
expect(display).toHaveTextContent('Ctrl+N');
|
||||
expect(display).toHaveTextContent(`${getCtrlModifier()}+N`);
|
||||
});
|
||||
|
||||
// Press Enter to confirm
|
||||
fireEvent.keyDown(document, { key: 'Enter', code: 'Enter' });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnChange).toHaveBeenCalledWith('Ctrl+N');
|
||||
expect(mockOnChange).toHaveBeenCalledWith(`${getCtrlModifier()}+N`);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -406,7 +411,7 @@ describe('KeyboardShortcutRegister Component', () => {
|
|||
|
||||
await waitFor(() => {
|
||||
const display = screen.getByTestId('shortcut-display');
|
||||
expect(display).toHaveTextContent('Ctrl+B');
|
||||
expect(display).toHaveTextContent(`${getCtrlModifier()}+B`);
|
||||
});
|
||||
|
||||
// Press ESC to cancel without saving
|
||||
|
|
@ -485,6 +490,8 @@ describe('KeyboardShortcutRegister Component', () => {
|
|||
});
|
||||
|
||||
// Simulate Ctrl+X key press on document
|
||||
// On macOS, ctrlKey is displayed as 'Cmd', on other platforms as 'Ctrl'
|
||||
const expectedModifier = process.platform === 'darwin' ? 'Cmd' : 'Ctrl';
|
||||
fireEvent.keyDown(document, {
|
||||
key: 'X',
|
||||
ctrlKey: true,
|
||||
|
|
@ -493,14 +500,14 @@ describe('KeyboardShortcutRegister Component', () => {
|
|||
|
||||
// Wait for the key combination to be processed
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Ctrl+X')).toBeInTheDocument();
|
||||
expect(screen.getByText(`${expectedModifier}+X`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Press Enter to confirm
|
||||
fireEvent.keyDown(document, { key: 'Enter', code: 'Enter' });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(customOnChange).toHaveBeenCalledWith('Ctrl+X');
|
||||
expect(customOnChange).toHaveBeenCalledWith(`${expectedModifier}+X`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,10 +20,8 @@ export class FileSystemAdaptor {
|
|||
boot: typeof $tw.boot;
|
||||
logger: Logger;
|
||||
workspaceID: string;
|
||||
/** All workspaces (main + sub-wikis) that have tagName configured, sorted by order */
|
||||
protected wikisWithTag: IWikiWorkspace[] = [];
|
||||
/** Map of tagName -> workspace for O(1) tag lookup instead of O(n) find */
|
||||
protected tagNameToWiki: Map<string, IWikiWorkspace> = new Map();
|
||||
/** All workspaces (main + sub-wikis) that have tagName or filter configured, sorted by order */
|
||||
protected wikisWithRouting: IWikiWorkspace[] = [];
|
||||
/** Cached extension filters from $:/config/FileSystemExtensions. Requires restart to reflect changes. */
|
||||
protected extensionFilters: string[] | undefined;
|
||||
protected watchPathBase!: string;
|
||||
|
|
@ -73,15 +71,13 @@ export class FileSystemAdaptor {
|
|||
protected async updateSubWikisCache(): Promise<void> {
|
||||
try {
|
||||
if (!this.workspaceID) {
|
||||
this.wikisWithTag = [];
|
||||
this.tagNameToWiki.clear();
|
||||
this.wikisWithRouting = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const currentWorkspace = await workspace.get(this.workspaceID);
|
||||
if (!currentWorkspace) {
|
||||
this.wikisWithTag = [];
|
||||
this.tagNameToWiki.clear();
|
||||
this.wikisWithRouting = [];
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -112,17 +108,9 @@ export class FileSystemAdaptor {
|
|||
|
||||
return isMain || isSubWiki;
|
||||
};
|
||||
const workspacesWithTag = allWorkspaces.filter(isWikiWorkspaceWithRouting).sort(workspaceSorter);
|
||||
const workspacesWithRouting = allWorkspaces.filter(isWikiWorkspaceWithRouting).sort(workspaceSorter);
|
||||
|
||||
this.wikisWithTag = workspacesWithTag;
|
||||
|
||||
this.tagNameToWiki.clear();
|
||||
for (const workspaceWithTag of workspacesWithTag) {
|
||||
// Build map for all tag names in this workspace
|
||||
for (const tagName of workspaceWithTag.tagNames) {
|
||||
this.tagNameToWiki.set(tagName, workspaceWithTag);
|
||||
}
|
||||
}
|
||||
this.wikisWithRouting = workspacesWithRouting;
|
||||
} catch (error) {
|
||||
this.logger.alert('filesystem: Failed to update sub-wikis cache:', error);
|
||||
}
|
||||
|
|
@ -196,10 +184,10 @@ export class FileSystemAdaptor {
|
|||
// Check if existing file is already in the correct directory
|
||||
// If so, just return the existing fileInfo to avoid echo loops
|
||||
if (existingFileInfo?.filepath) {
|
||||
const existingDir = path.dirname(existingFileInfo.filepath);
|
||||
const existingDirectory = path.dirname(existingFileInfo.filepath);
|
||||
// For sub-wikis, check if file is in that wiki's folder (or subfolder)
|
||||
// For main wiki, check if file is in main wiki's tiddlers folder (or subfolder)
|
||||
const normalizedExisting = path.normalize(existingDir);
|
||||
const normalizedExisting = path.normalize(existingDirectory);
|
||||
const normalizedTarget = path.normalize(targetDirectory);
|
||||
|
||||
// Check if existing file is within the target directory tree
|
||||
|
|
@ -225,26 +213,13 @@ export class FileSystemAdaptor {
|
|||
* Match a tiddler to a workspace based on routing rules.
|
||||
* Checks workspaces in order (priority) and returns the first match.
|
||||
*
|
||||
* For each workspace:
|
||||
* 1. If fileSystemPathFilterEnable is enabled, use custom filter expressions (one per line, any match wins)
|
||||
* 2. Else try direct tag match (including if tiddler's title IS one of the tagNames - it's a "tag tiddler")
|
||||
* 3. Else if includeTagTree is enabled, use in-tagtree-of filter
|
||||
* For each workspace, checks in order (any match wins):
|
||||
* 1. Direct tag match (including if tiddler's title IS one of the tagNames - it's a "tag tiddler")
|
||||
* 2. If includeTagTree is enabled, use in-tagtree-of filter for recursive tag matching
|
||||
* 3. If fileSystemPathFilterEnable is enabled, use custom filter expressions (one per line, any match wins)
|
||||
*/
|
||||
protected matchTitleToWiki(title: string, tags: string[]): IWikiWorkspace | undefined {
|
||||
for (const wiki of this.wikisWithTag) {
|
||||
// If fileSystemPathFilterEnable is enabled, use the custom filter expressions
|
||||
if (wiki.fileSystemPathFilterEnable && wiki.fileSystemPathFilter) {
|
||||
// Split by newlines and try each filter
|
||||
const filters = wiki.fileSystemPathFilter.split('\n').map(f => f.trim()).filter(f => f.length > 0);
|
||||
for (const filter of filters) {
|
||||
const result = $tw.wiki.filterTiddlers(filter, undefined, $tw.wiki.makeTiddlerIterator([title]));
|
||||
if (result.length > 0) {
|
||||
return wiki;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const wiki of this.wikisWithRouting) {
|
||||
// Direct tag match - check if any of the tiddler's tags match any of the wiki's tagNames
|
||||
// Also check if the tiddler's title IS one of the tagNames (it's a "tag tiddler" that defines that tag)
|
||||
if (wiki.tagNames.length > 0) {
|
||||
|
|
@ -264,6 +239,18 @@ export class FileSystemAdaptor {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom filter match if enabled
|
||||
if (wiki.fileSystemPathFilterEnable && wiki.fileSystemPathFilter) {
|
||||
// Split by newlines and try each filter
|
||||
const filters = wiki.fileSystemPathFilter.split('\n').map(f => f.trim()).filter(f => f.length > 0);
|
||||
for (const filter of filters) {
|
||||
const result = $tw.wiki.filterTiddlers(filter, undefined, $tw.wiki.makeTiddlerIterator([title]));
|
||||
if (result.length > 0) {
|
||||
return wiki;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -165,9 +165,31 @@ describe('FileSystemAdaptor - Routing Logic', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should pass existing fileInfo with overwrite flag', async () => {
|
||||
it('should return existing fileInfo with overwrite flag when file is in correct directory', async () => {
|
||||
const existingFileInfo: IFileInfo = {
|
||||
filepath: '/test/old.tid',
|
||||
filepath: '/test/wiki/tiddlers/old.tid', // Already in the correct tiddlers directory
|
||||
type: 'application/x-tiddler',
|
||||
hasMetaFile: false,
|
||||
};
|
||||
|
||||
// @ts-expect-error - TiddlyWiki global
|
||||
global.$tw.boot.files['TestTiddler'] = existingFileInfo;
|
||||
|
||||
const tiddler: Tiddler = {
|
||||
fields: { title: 'TestTiddler', tags: [] },
|
||||
} as unknown as Tiddler;
|
||||
|
||||
const result = await adaptor.getTiddlerFileInfo(tiddler);
|
||||
|
||||
// Should return the existing fileInfo with overwrite flag, not call generateTiddlerFileInfo
|
||||
expect(result).toEqual({ ...existingFileInfo, overwrite: true });
|
||||
// Should NOT call generateTiddlerFileInfo since file is already in correct location
|
||||
expect(mockUtils.generateTiddlerFileInfo).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should regenerate fileInfo when file is in wrong directory', async () => {
|
||||
const existingFileInfo: IFileInfo = {
|
||||
filepath: '/wrong/directory/old.tid', // In wrong directory
|
||||
type: 'application/x-tiddler',
|
||||
hasMetaFile: false,
|
||||
};
|
||||
|
|
@ -181,14 +203,8 @@ describe('FileSystemAdaptor - Routing Logic', () => {
|
|||
|
||||
await adaptor.getTiddlerFileInfo(tiddler);
|
||||
|
||||
expect(mockUtils.generateTiddlerFileInfo).toHaveBeenCalledWith(
|
||||
tiddler,
|
||||
expect.objectContaining({
|
||||
fileInfo: expect.objectContaining({
|
||||
overwrite: true,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
// Should call generateTiddlerFileInfo since file needs to be moved
|
||||
expect(mockUtils.generateTiddlerFileInfo).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error when wikiTiddlersPath is not set', async () => {
|
||||
|
|
@ -439,8 +455,7 @@ describe('FileSystemAdaptor - Routing Logic', () => {
|
|||
// Manually trigger cache update and wait for it
|
||||
await adaptor['updateSubWikisCache']();
|
||||
|
||||
expect(adaptor['wikisWithTag']).toEqual([]);
|
||||
expect(adaptor['tagNameToWiki'].size).toBe(0);
|
||||
expect(adaptor['wikisWithRouting']).toEqual([]);
|
||||
});
|
||||
|
||||
it('should clear cache when currentWorkspace is not found', async () => {
|
||||
|
|
@ -455,8 +470,7 @@ describe('FileSystemAdaptor - Routing Logic', () => {
|
|||
// Manually trigger cache update and wait for it
|
||||
await adaptor['updateSubWikisCache']();
|
||||
|
||||
expect(adaptor['wikisWithTag']).toEqual([]);
|
||||
expect(adaptor['tagNameToWiki'].size).toBe(0);
|
||||
expect(adaptor['wikisWithRouting']).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle errors in updateSubWikisCache gracefully', async () => {
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ This plugin provides an enhanced filesystem adaptor that automatically routes ti
|
|||
!!! How It Works
|
||||
|
||||
# Queries workspace information from TidGi's main process via IPC
|
||||
# Checks each tiddler's tags/filters against sub-workspace routing rules:
|
||||
#* Multiple tag names (`tagNames`) - matches if any tag matches
|
||||
# Checks each tiddler against sub-workspace routing rules (any match wins):
|
||||
#* Direct tag match - if any of the tiddler's tags match any of the workspace's `tagNames`
|
||||
#* Tag tiddlers - if a tiddler's title IS one of the `tagNames`, it's also routed (e.g., tiddler "Test" goes to sub-wiki with tagNames=["Test"])
|
||||
#* Tag tree (`includeTagTree`) - recursive tag hierarchy using `in-tagtree-of`
|
||||
#* Custom filter expressions (`fileSystemPathFilter`) - one per line, any match wins
|
||||
#* Tag tree (`includeTagTree`) - if enabled, uses `in-tagtree-of` filter for recursive tag hierarchy matching
|
||||
#* Custom filter expressions (`fileSystemPathFilter`) - if enabled, uses custom TiddlyWiki filter expressions (one per line, any match wins)
|
||||
# Routes tiddlers with matching rules to the corresponding sub-wiki's root folder
|
||||
# Falls back to standard `$:/config/FileSystemPaths` logic for non-matching tiddlers
|
||||
# Only moves files when target directory changes (avoids echo loops)
|
||||
|
|
@ -30,6 +30,7 @@ This plugin provides an enhanced filesystem adaptor that automatically routes ti
|
|||
* Supports multiple tags per workspace
|
||||
* Supports tag hierarchy matching
|
||||
* Supports custom TiddlyWiki filter expressions
|
||||
* All routing methods work together (tag match, tag tree, custom filter)
|
||||
* Handles tag changes - moves tiddlers when tags are modified
|
||||
* Tag tiddlers (tiddlers whose title matches a tag name) are also routed correctly
|
||||
* More robust than string manipulation
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import type { IWikiWorkspace } from '@services/workspaces/interface';
|
||||
import path from 'path';
|
||||
import type { TiddlyWiki } from 'tiddlywiki';
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ export interface IWikiWorkspace extends IDedicatedWorkspace {
|
|||
/**
|
||||
* When enabled, tiddlers that are indirectly tagged (tag of tag of tag...) with any of this sub-wiki's tagNames
|
||||
* will also be saved to this sub-wiki. Uses the in-tagtree-of filter operator.
|
||||
* Only applies when creating new tiddlers, not when modifying existing ones.
|
||||
* Applies when creating new tiddlers and when modifying existing ones (e.g., when tags change).
|
||||
*/
|
||||
includeTagTree: boolean;
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import { Autocomplete, AutocompleteRenderInputParams, Chip, MenuItem, Typography } from '@mui/material';
|
||||
import { Autocomplete, AutocompleteRenderInputParams, MenuItem, Typography } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
|
@ -91,11 +91,11 @@ export function CloneWikiForm({ form, isCreateMainWorkspace, errorInWhichCompone
|
|||
onChange={(_event, newValue) => {
|
||||
form.tagNamesSetter(newValue);
|
||||
}}
|
||||
renderTags={(value, getTagProps) =>
|
||||
value.map((option, index) => {
|
||||
const { key, ...tagProps } = getTagProps({ index });
|
||||
return <Chip variant='outlined' label={option} key={key} {...tagProps} />;
|
||||
})}
|
||||
slotProps={{
|
||||
chip: {
|
||||
variant: 'outlined',
|
||||
},
|
||||
}}
|
||||
renderInput={(parameters: AutocompleteRenderInputParams) => (
|
||||
<LocationPickerInput
|
||||
{...parameters}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import { Autocomplete, AutocompleteRenderInputParams, Chip, MenuItem, Typography } from '@mui/material';
|
||||
import { Autocomplete, AutocompleteRenderInputParams, MenuItem, Typography } from '@mui/material';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
|
@ -140,11 +140,11 @@ export function ExistedWikiForm({
|
|||
onChange={(_event, newValue) => {
|
||||
tagNamesSetter(newValue);
|
||||
}}
|
||||
renderTags={(value, getTagProps) =>
|
||||
value.map((option, index) => {
|
||||
const { key, ...tagProps } = getTagProps({ index });
|
||||
return <Chip variant='outlined' label={option} key={key} {...tagProps} />;
|
||||
})}
|
||||
slotProps={{
|
||||
chip: {
|
||||
variant: 'outlined',
|
||||
},
|
||||
}}
|
||||
renderInput={(parameters: AutocompleteRenderInputParams) => (
|
||||
<LocationPickerInput
|
||||
{...parameters}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import { Autocomplete, AutocompleteRenderInputParams, Chip, MenuItem, Typography } from '@mui/material';
|
||||
import { Autocomplete, AutocompleteRenderInputParams, MenuItem, Typography } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { isWikiWorkspace } from '@services/workspaces/interface';
|
||||
|
|
@ -97,11 +97,11 @@ export function NewWikiForm({
|
|||
onChange={(_event, newValue) => {
|
||||
form.tagNamesSetter(newValue);
|
||||
}}
|
||||
renderTags={(value, getTagProps) =>
|
||||
value.map((option, index) => {
|
||||
const { key, ...tagProps } = getTagProps({ index });
|
||||
return <Chip variant='outlined' label={option} key={key} {...tagProps} />;
|
||||
})}
|
||||
slotProps={{
|
||||
chip: {
|
||||
variant: 'outlined',
|
||||
},
|
||||
}}
|
||||
renderInput={(parameters: AutocompleteRenderInputParams) => (
|
||||
<LocationPickerInput
|
||||
error={errorInWhichComponent.tagNames}
|
||||
|
|
|
|||
|
|
@ -276,11 +276,11 @@ describe('NewWikiForm Component', () => {
|
|||
isCreateMainWorkspace: false,
|
||||
});
|
||||
|
||||
// Because the text is rendered with a template literal and newlines, we need to use a regex
|
||||
// The helper text shows the main wiki location that will be linked
|
||||
expect(screen.getByText((content, _element) => {
|
||||
// The actual text might have whitespace and newlines
|
||||
const normalized = content.replace(/\s+/g, ' ').trim();
|
||||
return normalized === 'AddWorkspace.SubWorkspaceWillLinkTo /main/wiki/tiddlers/subwiki/sub-wiki';
|
||||
return normalized === 'AddWorkspace.SubWorkspaceWillLinkTo /main/wiki';
|
||||
})).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import {
|
|||
Autocomplete,
|
||||
AutocompleteRenderInputParams,
|
||||
Button as ButtonRaw,
|
||||
Chip,
|
||||
Divider,
|
||||
Paper,
|
||||
Switch,
|
||||
|
|
@ -431,7 +430,47 @@ export default function EditWorkspace(): React.JSX.Element {
|
|||
<Typography variant='body2' color='textSecondary' sx={{ mb: 2 }}>
|
||||
{isSubWiki ? t('AddWorkspace.SubWorkspaceOptionsDescriptionForSub') : t('AddWorkspace.SubWorkspaceOptionsDescriptionForMain')}
|
||||
</Typography>
|
||||
<Autocomplete
|
||||
multiple
|
||||
freeSolo
|
||||
options={availableTags}
|
||||
value={tagNames}
|
||||
onChange={(_event: React.SyntheticEvent, newValue: string[]) => {
|
||||
void _event;
|
||||
workspaceSetter({ ...workspace, tagNames: newValue }, true);
|
||||
}}
|
||||
slotProps={{
|
||||
chip: {
|
||||
variant: 'outlined',
|
||||
},
|
||||
}}
|
||||
renderInput={(parameters: AutocompleteRenderInputParams) => (
|
||||
<TextField
|
||||
{...parameters}
|
||||
label={t('AddWorkspace.TagName')}
|
||||
helperText={isSubWiki ? t('AddWorkspace.TagNameHelp') : t('AddWorkspace.TagNameHelpForMain')}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<List>
|
||||
<ListItem
|
||||
disableGutters
|
||||
secondaryAction={
|
||||
<Switch
|
||||
edge='end'
|
||||
color='primary'
|
||||
checked={includeTagTree}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
workspaceSetter({ ...workspace, includeTagTree: event.target.checked }, true);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ListItemText
|
||||
primary={t('AddWorkspace.IncludeTagTree')}
|
||||
secondary={isSubWiki ? t('AddWorkspace.IncludeTagTreeHelp') : t('AddWorkspace.IncludeTagTreeHelpForMain')}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
disableGutters
|
||||
secondaryAction={
|
||||
|
|
@ -451,68 +490,21 @@ export default function EditWorkspace(): React.JSX.Element {
|
|||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
{fileSystemPathFilterEnable
|
||||
? (
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
minRows={2}
|
||||
maxRows={10}
|
||||
value={fileSystemPathFilter ?? ''}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
workspaceSetter({ ...workspace, fileSystemPathFilter: event.target.value || null }, true);
|
||||
}}
|
||||
label={t('AddWorkspace.FilterExpression')}
|
||||
helperText={t('AddWorkspace.FilterExpressionHelp')}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<Autocomplete
|
||||
multiple
|
||||
freeSolo
|
||||
options={availableTags}
|
||||
value={tagNames}
|
||||
onChange={(_event: React.SyntheticEvent, newValue: string[]) => {
|
||||
void _event;
|
||||
workspaceSetter({ ...workspace, tagNames: newValue }, true);
|
||||
}}
|
||||
renderTags={(value: string[], getTagProps) =>
|
||||
value.map((option: string, index: number) => {
|
||||
const { key, ...tagProps } = getTagProps({ index });
|
||||
return <Chip variant='outlined' label={option} key={key} {...tagProps} />;
|
||||
})}
|
||||
renderInput={(parameters: AutocompleteRenderInputParams) => (
|
||||
<TextField
|
||||
{...parameters}
|
||||
label={t('AddWorkspace.TagName')}
|
||||
helperText={isSubWiki ? t('AddWorkspace.TagNameHelp') : t('AddWorkspace.TagNameHelpForMain')}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<List>
|
||||
<ListItem
|
||||
disableGutters
|
||||
secondaryAction={
|
||||
<Switch
|
||||
edge='end'
|
||||
color='primary'
|
||||
checked={includeTagTree}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
workspaceSetter({ ...workspace, includeTagTree: event.target.checked }, true);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ListItemText
|
||||
primary={t('AddWorkspace.IncludeTagTree')}
|
||||
secondary={isSubWiki ? t('AddWorkspace.IncludeTagTreeHelp') : t('AddWorkspace.IncludeTagTreeHelpForMain')}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</>
|
||||
)}
|
||||
{fileSystemPathFilterEnable && (
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
minRows={2}
|
||||
maxRows={10}
|
||||
value={fileSystemPathFilter ?? ''}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
workspaceSetter({ ...workspace, fileSystemPathFilter: event.target.value || null }, true);
|
||||
}}
|
||||
label={t('AddWorkspace.FilterExpression')}
|
||||
helperText={t('AddWorkspace.FilterExpressionHelp')}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
)}
|
||||
</AccordionDetails>
|
||||
</OptionsAccordion>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue