mirror of
https://github.com/tiddly-gittly/TidGi-Desktop.git
synced 2025-12-06 02:30:47 -08:00
fix: load sub-wiki content and prevent echo
This commit is contained in:
parent
cd96e60eb9
commit
09a2c7a005
26 changed files with 388 additions and 175 deletions
|
|
@ -61,11 +61,15 @@
|
|||
"SyncedWorkspace": "云端同步知识库",
|
||||
"SyncedWorkspaceDescription": "同步到在线存储服务(例如Github),需要你登录存储服务或输入登录凭证,并有良好的网络连接。可以跨设备同步数据,在使用了值得信任的存储服务的情况下,数据仍归你所有。而且文件夹被不慎删除后,还可以从在线服务重新下载数据到本地。",
|
||||
"TagName": "标签名",
|
||||
"TagNameHelp": "加上此标签的笔记将会自动被放入这个子知识库内(可先不填,之后右键点击这个工作区的图标选择编辑工作区修改)",
|
||||
"TagNameHelpForMain": "带有此标签的新条目将优先保存在此工作区",
|
||||
"TagNameHelp": "加上这些标签之一的笔记将会自动被放入这个子知识库内(可先不填,之后右键点击这个工作区的图标选择编辑工作区修改)",
|
||||
"TagNameHelpForMain": "带有这些标签的新条目将优先保存在此工作区",
|
||||
"IncludeTagTree": "包括整个标签树",
|
||||
"IncludeTagTreeHelp": "勾选后,只要标签的标签的标签(任意级……)是这个,就会被划分到此子工作区里",
|
||||
"IncludeTagTreeHelpForMain": "勾选后,只要标签的标签的标签(任意级……)是这个,就会被划分到此工作区里",
|
||||
"UseFilter": "使用筛选器",
|
||||
"UseFilterHelp": "用筛选器表达式而不是标签来匹配条目,决定是否存入当前工作区",
|
||||
"FilterExpression": "筛选器表达式",
|
||||
"FilterExpressionHelp": "每行一个TiddlyWiki筛选器表达式,任一匹配即存入此工作区。例如 [in-tagtree-of[Calendar]!tag[Public]]",
|
||||
"SubWorkspaceOptions": "子工作区设置",
|
||||
"SubWorkspaceOptionsDescriptionForMain": "配置此主工作区优先保存哪些条目。设置标签后,带有此标签的新条目会优先保存在主工作区,而非子工作区。匹配顺序按侧边栏从上到下,先匹配到的工作区优先",
|
||||
"SubWorkspaceOptionsDescriptionForSub": "配置此子工作区保存哪些条目。带有指定标签的新条目将被保存到此子工作区。匹配顺序按侧边栏从上到下,先匹配到的工作区优先",
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ const defaultWorkspaces: IWorkspace[] = [
|
|||
port: 5212,
|
||||
isSubWiki: false,
|
||||
mainWikiToLink: null,
|
||||
tagName: null,
|
||||
tagNames: [],
|
||||
lastUrl: null,
|
||||
active: true,
|
||||
hibernated: false,
|
||||
|
|
@ -173,7 +173,7 @@ const defaultWorkspaces: IWorkspace[] = [
|
|||
port: 5213,
|
||||
isSubWiki: false,
|
||||
mainWikiToLink: null,
|
||||
tagName: null,
|
||||
tagNames: [],
|
||||
lastUrl: null,
|
||||
active: true,
|
||||
hibernated: false,
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import { useMemo } from 'react';
|
|||
import { PageType } from '@/constants/pageTypes';
|
||||
import { WindowNames } from '@services/windows/WindowProperties';
|
||||
import { IWorkspace, IWorkspaceWithMetadata } from '@services/workspaces/interface';
|
||||
import { SortableWorkspaceSelectorButton } from './SortableWorkspaceSelectorButton';
|
||||
import { workspaceSorter } from '@services/workspaces/utilities';
|
||||
import { SortableWorkspaceSelectorButton } from './SortableWorkspaceSelectorButton';
|
||||
|
||||
export interface ISortableListProps {
|
||||
showSideBarIcon: boolean;
|
||||
|
|
|
|||
|
|
@ -363,7 +363,7 @@ export class MenuService implements IMenuService {
|
|||
submenu: workspaces.map((workspace) => ({
|
||||
label: i18n.t('WorkspaceSelector.OpenWorkspaceTagTiddler', {
|
||||
tagName: isWikiWorkspace(workspace)
|
||||
? (workspace.tagName ?? (workspace.isSubWiki ? workspace.name : `${workspace.name} ${i18n.t('WorkspaceSelector.DefaultTiddlers')}`))
|
||||
? (workspace.tagNames[0] ?? (workspace.isSubWiki ? workspace.name : `${workspace.name} ${i18n.t('WorkspaceSelector.DefaultTiddlers')}`))
|
||||
: workspace.name,
|
||||
}),
|
||||
click: async () => {
|
||||
|
|
|
|||
|
|
@ -87,33 +87,41 @@ export class FileSystemAdaptor {
|
|||
|
||||
const allWorkspaces = await workspace.getWorkspacesAsList();
|
||||
|
||||
// Include both main workspace and sub-wikis for tag-based routing
|
||||
const isWikiWorkspaceWithTag = (workspaceItem: IWorkspace): workspaceItem is IWikiWorkspace => {
|
||||
// Include if it's the main workspace with tagName
|
||||
const isMainWithTag = workspaceItem.id === currentWorkspace.id &&
|
||||
'tagName' in workspaceItem &&
|
||||
workspaceItem.tagName &&
|
||||
'wikiFolderLocation' in workspaceItem &&
|
||||
workspaceItem.wikiFolderLocation;
|
||||
// Include both main workspace and sub-wikis for tag-based routing or filter-based routing
|
||||
const isWikiWorkspaceWithRouting = (workspaceItem: IWorkspace): workspaceItem is IWikiWorkspace => {
|
||||
if (!('wikiFolderLocation' in workspaceItem) || !workspaceItem.wikiFolderLocation) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Include if it's a sub-wiki with tagName
|
||||
const isSubWikiWithTag = 'isSubWiki' in workspaceItem &&
|
||||
// Check if workspace has routing config (either tagNames or fileSystemPathFilter)
|
||||
const hasRoutingConfig = ('tagNames' in workspaceItem && workspaceItem.tagNames.length > 0) ||
|
||||
('fileSystemPathFilterEnable' in workspaceItem && workspaceItem.fileSystemPathFilterEnable && 'fileSystemPathFilter' in workspaceItem &&
|
||||
workspaceItem.fileSystemPathFilter);
|
||||
|
||||
if (!hasRoutingConfig) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Include if it's the main workspace
|
||||
const isMain = workspaceItem.id === currentWorkspace.id;
|
||||
|
||||
// Include if it's a sub-wiki of the current main workspace
|
||||
const isSubWiki = 'isSubWiki' in workspaceItem &&
|
||||
workspaceItem.isSubWiki &&
|
||||
workspaceItem.mainWikiID === currentWorkspace.id &&
|
||||
'tagName' in workspaceItem &&
|
||||
workspaceItem.tagName &&
|
||||
'wikiFolderLocation' in workspaceItem &&
|
||||
workspaceItem.wikiFolderLocation;
|
||||
workspaceItem.mainWikiID === currentWorkspace.id;
|
||||
|
||||
return Boolean(isMainWithTag) || Boolean(isSubWikiWithTag);
|
||||
return isMain || isSubWiki;
|
||||
};
|
||||
const workspacesWithTag = allWorkspaces.filter(isWikiWorkspaceWithTag).sort(workspaceSorter);
|
||||
const workspacesWithTag = allWorkspaces.filter(isWikiWorkspaceWithRouting).sort(workspaceSorter);
|
||||
|
||||
this.wikisWithTag = workspacesWithTag;
|
||||
|
||||
this.tagNameToWiki.clear();
|
||||
for (const workspaceWithTag of workspacesWithTag) {
|
||||
this.tagNameToWiki.set(workspaceWithTag.tagName!, workspaceWithTag);
|
||||
// Build map for all tag names in this workspace
|
||||
for (const tagName of workspaceWithTag.tagNames) {
|
||||
this.tagNameToWiki.set(tagName, workspaceWithTag);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.alert('filesystem: Failed to update sub-wikis cache:', error);
|
||||
|
|
@ -133,10 +141,15 @@ export class FileSystemAdaptor {
|
|||
* Main routing logic: determine where a tiddler should be saved based on its tags.
|
||||
* For draft tiddlers, check the original tiddler's tags.
|
||||
*
|
||||
* For existing tiddlers (already in boot.files), we use the existing file path.
|
||||
* For new tiddlers, we check:
|
||||
* 1. Direct tag match with sub-wiki tagName
|
||||
* 2. If includeTagTree is enabled, use in-tagtree-of filter for recursive tag matching
|
||||
* Priority:
|
||||
* 1. If fileSystemPathFilterEnable is enabled, use custom filterExpression
|
||||
* 2. Direct tag match with sub-wiki tagNames
|
||||
* 3. If includeTagTree is enabled, use in-tagtree-of filter for recursive tag matching
|
||||
* 4. Fall back to TiddlyWiki's FileSystemPaths logic
|
||||
*
|
||||
* IMPORTANT: We check if the target directory has changed. Only when directory changes
|
||||
* do we regenerate the file path. This prevents echo loops where slightly different
|
||||
* filenames trigger constant saves.
|
||||
*/
|
||||
async getTiddlerFileInfo(tiddler: Tiddler): Promise<IFileInfo | null> {
|
||||
if (!this.boot.wikiTiddlersPath) {
|
||||
|
|
@ -145,7 +158,7 @@ export class FileSystemAdaptor {
|
|||
|
||||
const title = tiddler.fields.title;
|
||||
let tags = tiddler.fields.tags ?? [];
|
||||
const fileInfo = this.boot.files[title];
|
||||
const existingFileInfo = this.boot.files[title];
|
||||
|
||||
try {
|
||||
// For draft tiddlers (draft.of field), also check the original tiddler's tags
|
||||
|
|
@ -161,49 +174,95 @@ export class FileSystemAdaptor {
|
|||
}
|
||||
}
|
||||
|
||||
// First try direct tag match (O(1) lookup)
|
||||
let matchingSubWiki: IWikiWorkspace | undefined;
|
||||
for (const tag of tags) {
|
||||
matchingSubWiki = this.tagNameToWiki.get(tag);
|
||||
if (matchingSubWiki) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Find matching workspace using the routing logic
|
||||
const matchingWiki = this.matchTitleToWiki(title, tags);
|
||||
|
||||
// If no direct match, try in-tagtree-of for sub-wikis with includeTagTree enabled
|
||||
// Only for new tiddlers (no existing fileInfo) to save CPU
|
||||
if (!matchingSubWiki) {
|
||||
matchingSubWiki = this.matchTitleToWikiByTagTree(title);
|
||||
// Determine the target directory based on routing
|
||||
// Sub-wikis store tiddlers directly in their root folder (not in /tiddlers subfolder)
|
||||
// Only the main wiki uses /tiddlers because it has other meta files like .github
|
||||
let targetDirectory: string;
|
||||
if (matchingWiki) {
|
||||
targetDirectory = matchingWiki.wikiFolderLocation;
|
||||
// Resolve symlinks
|
||||
try {
|
||||
targetDirectory = fs.realpathSync(targetDirectory);
|
||||
} catch {
|
||||
// If realpath fails, use original
|
||||
}
|
||||
|
||||
if (matchingSubWiki) {
|
||||
return this.generateSubWikiFileInfo(tiddler, matchingSubWiki, fileInfo);
|
||||
} else {
|
||||
return this.generateDefaultFileInfo(tiddler, fileInfo);
|
||||
targetDirectory = this.boot.wikiTiddlersPath;
|
||||
}
|
||||
|
||||
// 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);
|
||||
// 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 normalizedTarget = path.normalize(targetDirectory);
|
||||
|
||||
// Check if existing file is within the target directory tree
|
||||
if (normalizedExisting.startsWith(normalizedTarget) || normalizedExisting === normalizedTarget) {
|
||||
// File is already in correct location, return existing fileInfo with overwrite flag
|
||||
return { ...existingFileInfo, overwrite: true };
|
||||
}
|
||||
}
|
||||
|
||||
// Directory has changed (or no existing file), generate new file info
|
||||
if (matchingWiki) {
|
||||
return this.generateSubWikiFileInfo(tiddler, matchingWiki);
|
||||
} else {
|
||||
return this.generateDefaultFileInfo(tiddler);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.alert(`filesystem: Error in getTiddlerFileInfo for "${title}":`, error);
|
||||
return this.generateDefaultFileInfo(tiddler, fileInfo);
|
||||
return this.generateDefaultFileInfo(tiddler);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find matching sub-wiki using in-tagtree-of filter for sub-wikis with includeTagTree enabled.
|
||||
* Iterates through sub-wikis sorted by order (priority).
|
||||
* 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
|
||||
*/
|
||||
protected matchTitleToWikiByTagTree(title: string): IWikiWorkspace | undefined {
|
||||
for (const subWiki of this.wikisWithTag) {
|
||||
if (!subWiki.includeTagTree || !subWiki.tagName) {
|
||||
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;
|
||||
}
|
||||
/**
|
||||
* Use build-in in-tagtree-of (at `src/services/wiki/plugin/watchFileSystemAdaptor/in-tagtree-of.ts`) filter
|
||||
* to check if tiddler is in tag tree.
|
||||
* The filter returns the title if it's in the tag tree, empty otherwise
|
||||
*/
|
||||
const result = $tw.wiki.filterTiddlers(`[in-tagtree-of:inclusive[${subWiki.tagName}]]`, undefined, $tw.wiki.makeTiddlerIterator([title]));
|
||||
|
||||
// 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) {
|
||||
const hasMatchingTag = wiki.tagNames.some(tagName => tags.includes(tagName));
|
||||
const isTitleATagName = wiki.tagNames.includes(title);
|
||||
if (hasMatchingTag || isTitleATagName) {
|
||||
return wiki;
|
||||
}
|
||||
}
|
||||
|
||||
// Tag tree match if enabled - check all tagNames
|
||||
if (wiki.includeTagTree && wiki.tagNames.length > 0) {
|
||||
for (const tagName of wiki.tagNames) {
|
||||
const result = $tw.wiki.filterTiddlers(`[in-tagtree-of:inclusive[${tagName}]]`, undefined, $tw.wiki.makeTiddlerIterator([title]));
|
||||
if (result.length > 0) {
|
||||
return subWiki;
|
||||
return wiki;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
|
|
@ -212,8 +271,14 @@ export class FileSystemAdaptor {
|
|||
/**
|
||||
* Generate file info for sub-wiki directory
|
||||
* Handles symlinks correctly across platforms (Windows junctions and Linux symlinks)
|
||||
*
|
||||
* CRITICAL: We must temporarily remove the tiddler from boot.files before calling
|
||||
* generateTiddlerFileInfo, otherwise TiddlyWiki will use the old path as a base
|
||||
* and FileSystemPaths filters will apply repeatedly, causing path accumulation.
|
||||
*/
|
||||
protected generateSubWikiFileInfo(tiddler: Tiddler, subWiki: IWikiWorkspace, fileInfo: IFileInfo | undefined): IFileInfo {
|
||||
protected generateSubWikiFileInfo(tiddler: Tiddler, subWiki: IWikiWorkspace): IFileInfo {
|
||||
// Sub-wikis store tiddlers directly in their root folder (not in /tiddlers subfolder)
|
||||
// Only the main wiki uses /tiddlers because it has other meta files like .github
|
||||
let targetDirectory = subWiki.wikiFolderLocation;
|
||||
|
||||
// Resolve symlinks to ensure consistent path handling across platforms
|
||||
|
|
@ -229,19 +294,38 @@ export class FileSystemAdaptor {
|
|||
|
||||
$tw.utils.createDirectory(targetDirectory);
|
||||
|
||||
const title = tiddler.fields.title;
|
||||
const oldFileInfo = this.boot.files[title];
|
||||
|
||||
// Temporarily remove from boot.files to force fresh path generation
|
||||
if (oldFileInfo) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete this.boot.files[title];
|
||||
}
|
||||
|
||||
try {
|
||||
return $tw.utils.generateTiddlerFileInfo(tiddler, {
|
||||
directory: targetDirectory,
|
||||
pathFilters: undefined,
|
||||
extFilters: this.extensionFilters,
|
||||
wiki: this.wiki,
|
||||
fileInfo: fileInfo ? { ...fileInfo, overwrite: true } : { overwrite: true } as IFileInfo,
|
||||
});
|
||||
} finally {
|
||||
// Restore old fileInfo for potential cleanup in saveTiddler
|
||||
if (oldFileInfo) {
|
||||
this.boot.files[title] = oldFileInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate file info using default FileSystemPaths logic
|
||||
*
|
||||
* CRITICAL: We must temporarily remove the tiddler from boot.files before calling
|
||||
* generateTiddlerFileInfo, otherwise TiddlyWiki will use the old path as a base
|
||||
* and FileSystemPaths filters will apply repeatedly, causing path accumulation.
|
||||
*/
|
||||
protected generateDefaultFileInfo(tiddler: Tiddler, fileInfo: IFileInfo | undefined): IFileInfo {
|
||||
protected generateDefaultFileInfo(tiddler: Tiddler): IFileInfo {
|
||||
let pathFilters: string[] | undefined;
|
||||
|
||||
if (this.wiki.tiddlerExists('$:/config/FileSystemPaths')) {
|
||||
|
|
@ -249,13 +333,28 @@ export class FileSystemAdaptor {
|
|||
pathFilters = pathFiltersText.split('\n').filter(line => line.trim().length > 0);
|
||||
}
|
||||
|
||||
const title = tiddler.fields.title;
|
||||
const oldFileInfo = this.boot.files[title];
|
||||
|
||||
// Temporarily remove from boot.files to force fresh path generation
|
||||
if (oldFileInfo) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete this.boot.files[title];
|
||||
}
|
||||
|
||||
try {
|
||||
return $tw.utils.generateTiddlerFileInfo(tiddler, {
|
||||
directory: this.boot.wikiTiddlersPath ?? '',
|
||||
pathFilters,
|
||||
extFilters: this.extensionFilters,
|
||||
wiki: this.wiki,
|
||||
fileInfo: fileInfo ? { ...fileInfo, overwrite: true } : { overwrite: true } as IFileInfo,
|
||||
});
|
||||
} finally {
|
||||
// Restore old fileInfo for potential cleanup in saveTiddler
|
||||
if (oldFileInfo) {
|
||||
this.boot.files[title] = oldFileInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
|||
// Initialize sub-wiki watchers
|
||||
await this.initializeSubWikiWatchers();
|
||||
// Log stabilization marker for tests
|
||||
this.logger.log('[test-id-WATCH_FS_STABILIZED] Watcher has stabilized', { level: 'debug' });
|
||||
this.logger.log('[test-id-WATCH_FS_STABILIZED] Watcher has stabilized');
|
||||
} catch (error) {
|
||||
this.logger.alert('WatchFileSystemAdaptor Failed to initialize file watching:', error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@ describe('FileSystemAdaptor - Routing Logic', () => {
|
|||
name: 'Sub Wiki',
|
||||
isSubWiki: true,
|
||||
mainWikiID: 'test-workspace',
|
||||
tagName: 'SubWikiTag',
|
||||
tagNames: ['SubWikiTag'],
|
||||
wikiFolderLocation: '/test/wiki/subwiki/sub1',
|
||||
};
|
||||
|
||||
|
|
@ -278,7 +278,7 @@ describe('FileSystemAdaptor - Routing Logic', () => {
|
|||
id: 'sub-1',
|
||||
isSubWiki: true,
|
||||
mainWikiID: 'test-workspace',
|
||||
tagName: 'Tag1',
|
||||
tagNames: ['Tag1'],
|
||||
wikiFolderLocation: '/test/wiki/sub1',
|
||||
};
|
||||
|
||||
|
|
@ -286,7 +286,7 @@ describe('FileSystemAdaptor - Routing Logic', () => {
|
|||
id: 'sub-2',
|
||||
isSubWiki: true,
|
||||
mainWikiID: 'test-workspace',
|
||||
tagName: 'Tag2',
|
||||
tagNames: ['Tag2'],
|
||||
wikiFolderLocation: '/test/wiki/sub2',
|
||||
};
|
||||
|
||||
|
|
@ -322,15 +322,16 @@ describe('FileSystemAdaptor - Routing Logic', () => {
|
|||
id: 'sub-wiki-1',
|
||||
isSubWiki: true,
|
||||
mainWikiID: 'test-workspace',
|
||||
tagName: 'SubWikiTag',
|
||||
tagNames: ['SubWikiTag'],
|
||||
wikiFolderLocation: '/test/wiki/subwiki',
|
||||
};
|
||||
|
||||
// Test scenario 2: Sub-wiki without tagName
|
||||
// Test scenario 2: Sub-wiki without tagNames
|
||||
const subWikiWithoutTag = {
|
||||
id: 'sub-wiki-2',
|
||||
isSubWiki: true,
|
||||
mainWikiID: 'test-workspace',
|
||||
tagNames: [],
|
||||
wikiFolderLocation: '/test/wiki/subwiki2',
|
||||
};
|
||||
|
||||
|
|
@ -339,7 +340,7 @@ describe('FileSystemAdaptor - Routing Logic', () => {
|
|||
id: 'sub-wiki-3',
|
||||
isSubWiki: true,
|
||||
mainWikiID: 'other-workspace',
|
||||
tagName: 'AnotherTag',
|
||||
tagNames: ['AnotherTag'],
|
||||
wikiFolderLocation: '/test/otherwiki/subwiki',
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -28,16 +28,20 @@ private async getCurrentWorkspace(): Promise<IWorkspace | undefined>
|
|||
private async getSubWikis(currentWorkspace: IWorkspace): Promise<IWorkspace[]>
|
||||
```
|
||||
|
||||
!!!! 2. Tag-Based Sub-Wiki Routing
|
||||
!!!! 2. Tag-Based and Filter-Based Sub-Wiki Routing
|
||||
|
||||
* ''Original'': Routes based on filter expressions in `FileSystemPaths`
|
||||
* ''Modified'': Automatically routes tiddlers to sub-wikis based on tag matching
|
||||
* ''Modified'': Automatically routes tiddlers to sub-wikis based on:
|
||||
** Multiple tag names (`tagNames: string[]`) - tiddler matches if any of its tags matches any of workspace's `tagNames`
|
||||
** Tag tree matching (`includeTagTree`) - recursive tag hierarchy matching using `in-tagtree-of` filter
|
||||
** Custom filter expressions (`fileSystemPathFilter`) - user-defined TiddlyWiki filters (one per line, any match wins)
|
||||
* ''Modified'': Made `getTiddlerFileInfo`, `saveTiddler`, and `deleteTiddler` async for cleaner code
|
||||
* ''Modified'': Caches sub-wikis list to avoid repeated IPC calls on every save operation
|
||||
* ''Important'': Always recalculates path on save to handle tag changes - old `fileInfo` only used for cleanup
|
||||
* ''Implementation'':
|
||||
** Checks tiddler tags against sub-workspace `tagName` fields
|
||||
** Checks tiddler tags/filters against sub-workspace routing rules (in priority order)
|
||||
** Routes matching tiddlers to sub-wiki's `tiddlers` folder
|
||||
** Falls back to default `FileSystemPaths` logic for non-matching tiddlers
|
||||
** Falls back to TiddlyWiki's `$:/config/FileSystemPaths` logic for non-matching tiddlers
|
||||
** Loads sub-wikis cache on initialization
|
||||
** Currently loads sub-wikis once, future enhancements can watch for workspace changes
|
||||
|
||||
|
|
|
|||
|
|
@ -3,22 +3,38 @@ type: text/vnd.tiddlywiki
|
|||
|
||||
!! Watch Filesystem Adaptor
|
||||
|
||||
This plugin provides an enhanced filesystem adaptor that automatically routes tiddlers to sub-wikis based on their tags.
|
||||
This plugin provides an enhanced filesystem adaptor that automatically routes tiddlers to sub-wikis based on their tags or custom filters.
|
||||
|
||||
!!! How It Works
|
||||
|
||||
# Queries workspace information from TidGi's main process via IPC
|
||||
# Checks each tiddler's tags against sub-workspace `tagName` fields
|
||||
# Routes tiddlers with matching tags to the corresponding sub-wiki's tiddlers folder
|
||||
# Checks each tiddler's tags/filters against sub-workspace routing rules:
|
||||
#* Multiple tag names (`tagNames`) - matches if any tag matches
|
||||
#* 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
|
||||
# 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)
|
||||
# Existing tiddlers in wrong location will be moved when modified
|
||||
|
||||
!!! Directory Structure
|
||||
|
||||
* ''Main wiki'': tiddlers are stored in `wiki/tiddlers/` (because main wiki has other meta files like `.github`)
|
||||
* ''Sub-wikis'': tiddlers are stored directly in the sub-wiki root folder (e.g., `wiki-sub/`)
|
||||
|
||||
!!! Advantages
|
||||
|
||||
* No need to manually edit `$:/config/FileSystemPaths`
|
||||
* Automatically stays in sync with workspace configuration
|
||||
* Supports multiple tags per workspace
|
||||
* Supports tag hierarchy matching
|
||||
* Supports custom TiddlyWiki filter expressions
|
||||
* 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
|
||||
* Works seamlessly with TidGi's workspace management
|
||||
|
||||
!!! Technical Details
|
||||
|
||||
This adaptor runs in the wiki worker (Node.js) and communicates with TidGi's main process using worker threads IPC to access workspace service methods. It dynamically resolves the current workspace and its sub-wikis, then routes tiddlers based on their tags.
|
||||
This adaptor runs in the wiki worker (Node.js) and communicates with TidGi's main process using worker threads IPC to access workspace service methods. It dynamically resolves the current workspace and its sub-wikis, then routes tiddlers based on their tags or custom filters.
|
||||
|
|
|
|||
|
|
@ -36,15 +36,14 @@ export function createLoadWikiTiddlersWithSubWikis(
|
|||
// Call original function first to load main wiki
|
||||
const wikiInfo = originalLoadWikiTiddlers(wikiPath, options);
|
||||
|
||||
// defensive check
|
||||
if (wikiPath === homePath && wikiInfo && subWikis.length > 0) {
|
||||
return;
|
||||
// Only inject sub-wikis when loading the main wiki (not when loading included wikis)
|
||||
if (wikiPath !== homePath || !wikiInfo || subWikis.length === 0) {
|
||||
return wikiInfo;
|
||||
}
|
||||
for (const subWiki of subWikis) {
|
||||
const subWikiTiddlersPath = path.resolve(
|
||||
subWiki.wikiFolderLocation,
|
||||
wikiInstance.config.wikiTiddlersSubDir,
|
||||
);
|
||||
// Sub-wikis store tiddlers directly in their root folder (not in /tiddlers subfolder)
|
||||
// Only the main wiki uses /tiddlers because it has other meta files like .github
|
||||
const subWikiTiddlersPath = subWiki.wikiFolderLocation;
|
||||
|
||||
try {
|
||||
// Load tiddlers from sub-wiki directory
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ describe('WikiEmbeddingService Integration Tests', () => {
|
|||
port: 5212,
|
||||
isSubWiki: false,
|
||||
mainWikiToLink: null,
|
||||
tagName: null,
|
||||
tagNames: [],
|
||||
lastUrl: null,
|
||||
active: true,
|
||||
hibernated: false,
|
||||
|
|
|
|||
|
|
@ -150,13 +150,15 @@ export class WikiGitWorkspace implements IWikiGitWorkspaceService {
|
|||
backupOnInterval: true,
|
||||
readOnlyMode: false,
|
||||
tokenAuth: false,
|
||||
tagName: null,
|
||||
tagNames: [],
|
||||
mainWikiToLink: null,
|
||||
mainWikiID: null,
|
||||
excludedPlugins: [],
|
||||
enableHTTPAPI: false,
|
||||
enableFileSystemWatch: true,
|
||||
includeTagTree: false,
|
||||
fileSystemPathFilterEnable: false,
|
||||
fileSystemPathFilter: null,
|
||||
lastNodeJSArgv: [],
|
||||
homeUrl: '',
|
||||
gitUrl: null,
|
||||
|
|
|
|||
|
|
@ -131,12 +131,12 @@ export async function getWorkspaceMenuTemplate(
|
|||
}];
|
||||
}
|
||||
|
||||
const { hibernated, tagName, isSubWiki, wikiFolderLocation, gitUrl, storageService, port, enableHTTPAPI, lastUrl, homeUrl } = workspace;
|
||||
const { hibernated, tagNames, isSubWiki, wikiFolderLocation, gitUrl, storageService, port, enableHTTPAPI, lastUrl, homeUrl } = workspace;
|
||||
|
||||
const template: MenuItemConstructorOptions[] = [
|
||||
{
|
||||
label: t('WorkspaceSelector.OpenWorkspaceTagTiddler', {
|
||||
tagName: tagName ?? (isSubWiki ? name : `${name} ${t('WorkspaceSelector.DefaultTiddlers')}`),
|
||||
tagName: tagNames[0] ?? (isSubWiki ? name : `${name} ${t('WorkspaceSelector.DefaultTiddlers')}`),
|
||||
}),
|
||||
click: async () => {
|
||||
await service.workspace.openWorkspaceTiddler(workspace);
|
||||
|
|
|
|||
|
|
@ -235,6 +235,9 @@ export class Workspace implements IWorkspaceService {
|
|||
excludedPlugins: [],
|
||||
enableHTTPAPI: false,
|
||||
includeTagTree: false,
|
||||
fileSystemPathFilterEnable: false,
|
||||
fileSystemPathFilter: null,
|
||||
tagNames: [],
|
||||
};
|
||||
const fixingValues: Partial<typeof workspaceToSanitize> = {};
|
||||
// we add mainWikiID in creation, we fix this value for old existed workspaces
|
||||
|
|
@ -244,9 +247,11 @@ export class Workspace implements IWorkspaceService {
|
|||
fixingValues.mainWikiID = mainWorkspace.id;
|
||||
}
|
||||
}
|
||||
// fix WikiChannel.openTiddler in src/services/wiki/wikiOperations/executor/wikiOperationInBrowser.ts have \n on the end
|
||||
if (workspaceToSanitize.tagName?.endsWith('\n') === true) {
|
||||
fixingValues.tagName = workspaceToSanitize.tagName.replaceAll('\n', '');
|
||||
// Migrate old tagName (string) to tagNames (string[])
|
||||
|
||||
const legacyTagName = (workspaceToSanitize as { tagName?: string | null }).tagName;
|
||||
if (legacyTagName && (!workspaceToSanitize.tagNames || workspaceToSanitize.tagNames.length === 0)) {
|
||||
fixingValues.tagNames = [legacyTagName.replaceAll('\n', '')];
|
||||
}
|
||||
// before 0.8.0, tidgi was loading http content, so lastUrl will be http protocol, but later we switch to tidgi:// protocol, so old value can't be used.
|
||||
if (!workspaceToSanitize.lastUrl?.startsWith('tidgi')) {
|
||||
|
|
@ -271,11 +276,11 @@ export class Workspace implements IWorkspaceService {
|
|||
if (!isWikiWorkspace(newWorkspaceConfig)) return;
|
||||
|
||||
const existedWorkspace = this.getSync(newWorkspaceConfig.id);
|
||||
const { id, tagName } = newWorkspaceConfig;
|
||||
// when update tagName of subWiki
|
||||
const { id, tagNames } = newWorkspaceConfig;
|
||||
// when update tagNames of subWiki
|
||||
if (
|
||||
existedWorkspace !== undefined && isWikiWorkspace(existedWorkspace) && existedWorkspace.isSubWiki && typeof tagName === 'string' && tagName.length > 0 &&
|
||||
existedWorkspace.tagName !== tagName
|
||||
existedWorkspace !== undefined && isWikiWorkspace(existedWorkspace) && existedWorkspace.isSubWiki && tagNames.length > 0 &&
|
||||
JSON.stringify(existedWorkspace.tagNames) !== JSON.stringify(tagNames)
|
||||
) {
|
||||
const { mainWikiToLink } = existedWorkspace;
|
||||
if (typeof mainWikiToLink !== 'string') {
|
||||
|
|
@ -546,7 +551,7 @@ export class Workspace implements IWorkspaceService {
|
|||
// Only handle wiki workspaces
|
||||
if (!isWikiWorkspace(workspace)) return;
|
||||
|
||||
const { isSubWiki, mainWikiID, tagName } = workspace;
|
||||
const { isSubWiki, mainWikiID, tagNames } = workspace;
|
||||
|
||||
logger.log('debug', 'openWorkspaceTiddler', { workspace });
|
||||
// If is main wiki, open the wiki, and open provided title, or simply switch to it if no title provided
|
||||
|
|
@ -568,7 +573,8 @@ export class Workspace implements IWorkspaceService {
|
|||
if (oldActiveWorkspace?.id !== mainWikiID) {
|
||||
await workspaceViewService.setActiveWorkspaceView(mainWikiID);
|
||||
}
|
||||
const subWikiTag = title ?? tagName;
|
||||
// Use provided title, or first tag name, or nothing
|
||||
const subWikiTag = title ?? tagNames[0];
|
||||
if (subWikiTag) {
|
||||
await wikiService.wikiOperationInBrowser(WikiChannel.openTiddler, mainWikiID, [subWikiTag]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -136,15 +136,27 @@ export interface IWikiWorkspace extends IDedicatedWorkspace {
|
|||
*/
|
||||
syncOnStartup: boolean;
|
||||
/**
|
||||
* Tag name in tiddlywiki's filesystemPath, tiddler with this tag will be save into this subwiki
|
||||
* Tag names in tiddlywiki's filesystemPath, tiddlers with any of these tags will be saved into this subwiki
|
||||
*/
|
||||
tagName: string | null;
|
||||
tagNames: string[];
|
||||
/**
|
||||
* When enabled, tiddlers that are indirectly tagged (tag of tag of tag...) with this sub-wiki's tagName
|
||||
* 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.
|
||||
*/
|
||||
includeTagTree: boolean;
|
||||
/**
|
||||
* When enabled, use fileSystemPathFilter instead of tagName/includeTagTree to match tiddlers.
|
||||
* This allows more complex matching logic using TiddlyWiki filter expressions.
|
||||
*/
|
||||
fileSystemPathFilterEnable: boolean;
|
||||
/**
|
||||
* TiddlyWiki filter expressions to match tiddlers for this workspace (one per line).
|
||||
* Example: `[in-tagtree-of[Calendar]!tag[Public]!tag[Draft]]`
|
||||
* Any matching filter will route the tiddler to this workspace.
|
||||
* Only used when fileSystemPathFilterEnable is true.
|
||||
*/
|
||||
fileSystemPathFilter: string | null;
|
||||
/**
|
||||
* Use authenticated-user-header to provide `TIDGI_AUTH_TOKEN_HEADER` as header key to receive a value as username (we use it as token)
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -361,8 +361,9 @@ export class WorkspaceView implements IWorkspaceViewService {
|
|||
if (isWikiWorkspace(newWorkspace) && newWorkspace.isSubWiki && typeof newWorkspace.mainWikiID === 'string') {
|
||||
logger.debug(`${nextWorkspaceID} is a subwiki, set its main wiki ${newWorkspace.mainWikiID} to active instead.`);
|
||||
await this.setActiveWorkspaceView(newWorkspace.mainWikiID);
|
||||
if (typeof newWorkspace.tagName === 'string') {
|
||||
await container.get<IWikiService>(serviceIdentifier.Wiki).wikiOperationInBrowser(WikiChannel.openTiddler, newWorkspace.mainWikiID, [newWorkspace.tagName]);
|
||||
// Open the first tag if available
|
||||
if (newWorkspace.tagNames.length > 0) {
|
||||
await container.get<IWikiService>(serviceIdentifier.Wiki).wikiOperationInBrowser(WikiChannel.openTiddler, newWorkspace.mainWikiID, [newWorkspace.tagNames[0]]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import { AutocompleteRenderInputParams, MenuItem, Typography } from '@mui/material';
|
||||
import { Autocomplete, AutocompleteRenderInputParams, Chip, MenuItem, Typography } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { isWikiWorkspace } from '@services/workspaces/interface';
|
||||
import { CreateContainer, LocationPickerButton, LocationPickerContainer, LocationPickerInput, SoftLinkToMainWikiSelect, SubWikiTagAutoComplete } from './FormComponents';
|
||||
import { CreateContainer, LocationPickerButton, LocationPickerContainer, LocationPickerInput, SoftLinkToMainWikiSelect } from './FormComponents';
|
||||
|
||||
import { useAvailableTags } from './useAvailableTags';
|
||||
import { useValidateCloneWiki } from './useCloneWiki';
|
||||
|
|
@ -83,17 +83,23 @@ export function CloneWikiForm({ form, isCreateMainWorkspace, errorInWhichCompone
|
|||
</MenuItem>
|
||||
))}
|
||||
</SoftLinkToMainWikiSelect>
|
||||
<SubWikiTagAutoComplete
|
||||
<Autocomplete<string, true, false, true>
|
||||
multiple
|
||||
freeSolo
|
||||
options={availableTags}
|
||||
value={form.tagName}
|
||||
onInputChange={(_event: React.SyntheticEvent, value: string) => {
|
||||
form.tagNameSetter(value);
|
||||
value={form.tagNames}
|
||||
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} />;
|
||||
})}
|
||||
renderInput={(parameters: AutocompleteRenderInputParams) => (
|
||||
<LocationPickerInput
|
||||
{...parameters}
|
||||
error={errorInWhichComponent.tagName}
|
||||
error={errorInWhichComponent.tagNames}
|
||||
label={t('AddWorkspace.TagName')}
|
||||
helperText={t('AddWorkspace.TagNameHelp')}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import { AutocompleteRenderInputParams, MenuItem, Typography } from '@mui/material';
|
||||
import { Autocomplete, AutocompleteRenderInputParams, Chip, MenuItem, Typography } from '@mui/material';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { isWikiWorkspace } from '@services/workspaces/interface';
|
||||
import { CreateContainer, LocationPickerButton, LocationPickerContainer, LocationPickerInput, SoftLinkToMainWikiSelect, SubWikiTagAutoComplete } from './FormComponents';
|
||||
import { CreateContainer, LocationPickerButton, LocationPickerContainer, LocationPickerInput, SoftLinkToMainWikiSelect } from './FormComponents';
|
||||
|
||||
import { useAvailableTags } from './useAvailableTags';
|
||||
import { useValidateExistedWiki } from './useExistedWiki';
|
||||
|
|
@ -32,8 +32,8 @@ export function ExistedWikiForm({
|
|||
mainWikiToLinkIndex,
|
||||
mainWikiToLinkSetter,
|
||||
mainWorkspaceList,
|
||||
tagName,
|
||||
tagNameSetter,
|
||||
tagNames,
|
||||
tagNamesSetter,
|
||||
} = form;
|
||||
|
||||
// Local state for the full path input - like NewWikiForm's direct state binding
|
||||
|
|
@ -132,17 +132,23 @@ export function ExistedWikiForm({
|
|||
</MenuItem>
|
||||
))}
|
||||
</SoftLinkToMainWikiSelect>
|
||||
<SubWikiTagAutoComplete
|
||||
<Autocomplete<string, true, false, true>
|
||||
multiple
|
||||
freeSolo
|
||||
options={availableTags}
|
||||
value={tagName}
|
||||
onInputChange={(_event: React.SyntheticEvent, value: string) => {
|
||||
tagNameSetter(value);
|
||||
value={tagNames}
|
||||
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} />;
|
||||
})}
|
||||
renderInput={(parameters: AutocompleteRenderInputParams) => (
|
||||
<LocationPickerInput
|
||||
{...parameters}
|
||||
error={errorInWhichComponent.tagName}
|
||||
error={errorInWhichComponent.tagNames}
|
||||
label={t('AddWorkspace.TagName')}
|
||||
helperText={t('AddWorkspace.TagNameHelp')}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import { AutocompleteRenderInputParams, MenuItem, Typography } from '@mui/material';
|
||||
import { Autocomplete, AutocompleteRenderInputParams, Chip, MenuItem, Typography } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { isWikiWorkspace } from '@services/workspaces/interface';
|
||||
import { CreateContainer, LocationPickerButton, LocationPickerContainer, LocationPickerInput, SoftLinkToMainWikiSelect, SubWikiTagAutoComplete } from './FormComponents';
|
||||
import { CreateContainer, LocationPickerButton, LocationPickerContainer, LocationPickerInput, SoftLinkToMainWikiSelect } from './FormComponents';
|
||||
|
||||
import { useAvailableTags } from './useAvailableTags';
|
||||
import type { IWikiWorkspaceFormProps } from './useForm';
|
||||
|
|
@ -89,16 +89,22 @@ export function NewWikiForm({
|
|||
</MenuItem>
|
||||
))}
|
||||
</SoftLinkToMainWikiSelect>
|
||||
<SubWikiTagAutoComplete
|
||||
<Autocomplete<string, true, false, true>
|
||||
multiple
|
||||
freeSolo
|
||||
options={availableTags}
|
||||
value={form.tagName}
|
||||
onInputChange={(_event: React.SyntheticEvent, value: string) => {
|
||||
form.tagNameSetter(value);
|
||||
value={form.tagNames}
|
||||
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} />;
|
||||
})}
|
||||
renderInput={(parameters: AutocompleteRenderInputParams) => (
|
||||
<LocationPickerInput
|
||||
error={errorInWhichComponent.tagName}
|
||||
error={errorInWhichComponent.tagNames}
|
||||
{...parameters}
|
||||
label={t('AddWorkspace.TagName')}
|
||||
helperText={t('AddWorkspace.TagNameHelp')}
|
||||
|
|
|
|||
|
|
@ -47,8 +47,8 @@ const createMockForm = (overrides: Partial<IWikiWorkspaceForm> = {}): IWikiWorks
|
|||
metadata: {},
|
||||
} as unknown as IWorkspace,
|
||||
],
|
||||
tagName: '',
|
||||
tagNameSetter: vi.fn(),
|
||||
tagNames: [] as string[],
|
||||
tagNamesSetter: vi.fn(),
|
||||
gitRepoUrl: '',
|
||||
gitRepoUrlSetter: vi.fn(),
|
||||
gitUserInfo: undefined as IGitUserInfos | undefined,
|
||||
|
|
@ -194,7 +194,7 @@ describe('NewWikiForm Component', () => {
|
|||
const user = userEvent.setup();
|
||||
const mockSetter = vi.fn();
|
||||
const form = createMockForm({
|
||||
tagNameSetter: mockSetter,
|
||||
tagNamesSetter: mockSetter,
|
||||
});
|
||||
|
||||
await renderNewWikiForm({
|
||||
|
|
@ -206,7 +206,7 @@ describe('NewWikiForm Component', () => {
|
|||
const tagInput = screen.getByTestId('tagname-autocomplete-input');
|
||||
await user.type(tagInput, 'MyTag');
|
||||
await user.keyboard('{enter}');
|
||||
expect(mockSetter).toHaveBeenCalledWith('MyTag');
|
||||
expect(mockSetter).toHaveBeenCalledWith(['MyTag']);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -231,7 +231,7 @@ describe('NewWikiForm Component', () => {
|
|||
isCreateMainWorkspace: false,
|
||||
errorInWhichComponent: {
|
||||
mainWikiToLink: true,
|
||||
tagName: true,
|
||||
tagNames: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export function useValidateCloneWiki(
|
|||
form.gitRepoUrl,
|
||||
form.gitUserInfo,
|
||||
form.mainWikiToLink.wikiFolderLocation,
|
||||
form.tagName,
|
||||
form.tagNames,
|
||||
errorInWhichComponentSetter,
|
||||
]);
|
||||
return [hasError, wikiCreationMessage, wikiCreationMessageSetter, hasErrorSetter];
|
||||
|
|
@ -76,7 +76,7 @@ export function useCloneWiki(
|
|||
form.mainWikiToLink.wikiFolderLocation,
|
||||
form.gitRepoUrl,
|
||||
form.gitUserInfo!,
|
||||
form.tagName,
|
||||
form.tagNames[0] ?? '',
|
||||
);
|
||||
}
|
||||
await callWikiInitialization(newWorkspaceConfig, wikiCreationMessageSetter, t, form.gitUserInfo, { from: WikiCreationMethod.Clone });
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ export function useValidateExistedWiki(
|
|||
form.gitRepoUrl,
|
||||
form.gitUserInfo,
|
||||
form.mainWikiToLink.wikiFolderLocation,
|
||||
form.tagName,
|
||||
form.tagNames,
|
||||
errorInWhichComponentSetter,
|
||||
]);
|
||||
return [hasError, wikiCreationMessage, wikiCreationMessageSetter, hasErrorSetter];
|
||||
|
|
@ -86,7 +86,7 @@ export function useExistedWiki(
|
|||
wikiFolderNameForExistedFolder,
|
||||
'subwiki',
|
||||
form.mainWikiToLink.wikiFolderLocation,
|
||||
form.tagName,
|
||||
form.tagNames[0] ?? '',
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export function useWikiWorkspaceForm(options?: { fromExisted: boolean }) {
|
|||
return firstMainWiki ? { wikiFolderLocation: firstMainWiki.wikiFolderLocation, port: firstMainWiki.port, id: firstMainWiki.id } : { wikiFolderLocation: '', port: 0, id: '' };
|
||||
},
|
||||
);
|
||||
const [tagName, tagNameSetter] = useState<string>('');
|
||||
const [tagNames, tagNamesSetter] = useState<string[]>([]);
|
||||
let mainWikiToLinkIndex = mainWorkspaceList.findIndex((workspace) => workspace.id === mainWikiToLink.id);
|
||||
if (mainWikiToLinkIndex < 0) {
|
||||
mainWikiToLinkIndex = 0;
|
||||
|
|
@ -123,8 +123,8 @@ export function useWikiWorkspaceForm(options?: { fromExisted: boolean }) {
|
|||
wikiPortSetter,
|
||||
mainWikiToLink,
|
||||
mainWikiToLinkSetter,
|
||||
tagName,
|
||||
tagNameSetter,
|
||||
tagNames,
|
||||
tagNamesSetter,
|
||||
gitRepoUrl,
|
||||
gitRepoUrlSetter,
|
||||
parentFolderLocation,
|
||||
|
|
@ -162,7 +162,7 @@ export function workspaceConfigFromForm(form: INewWikiRequiredFormData, isCreate
|
|||
mainWikiID: isCreateMainWorkspace ? null : form.mainWikiToLink.id,
|
||||
name: form.wikiFolderName,
|
||||
storageService: form.storageProvider,
|
||||
tagName: isCreateMainWorkspace ? null : form.tagName,
|
||||
tagNames: isCreateMainWorkspace ? [] : form.tagNames,
|
||||
port: form.wikiPort,
|
||||
wikiFolderLocation: form.wikiFolderLocation!,
|
||||
backupOnInterval: true,
|
||||
|
|
@ -174,6 +174,8 @@ export function workspaceConfigFromForm(form: INewWikiRequiredFormData, isCreate
|
|||
enableHTTPAPI: false,
|
||||
enableFileSystemWatch: true,
|
||||
includeTagTree: false,
|
||||
fileSystemPathFilterEnable: false,
|
||||
fileSystemPathFilter: null,
|
||||
lastNodeJSArgv: [],
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export function useValidateNewWiki(
|
|||
form.gitRepoUrl,
|
||||
form.gitUserInfo,
|
||||
form.mainWikiToLink.wikiFolderLocation,
|
||||
form.tagName,
|
||||
form.tagNames,
|
||||
errorInWhichComponentSetter,
|
||||
]);
|
||||
|
||||
|
|
@ -80,7 +80,7 @@ export function useNewWiki(
|
|||
await window.service.wiki.copyWikiTemplate(form.parentFolderLocation, form.wikiFolderName);
|
||||
}
|
||||
} else {
|
||||
await window.service.wiki.createSubWiki(form.parentFolderLocation, form.wikiFolderName, 'subwiki', form.mainWikiToLink.wikiFolderLocation, form.tagName);
|
||||
await window.service.wiki.createSubWiki(form.parentFolderLocation, form.wikiFolderName, 'subwiki', form.mainWikiToLink.wikiFolderLocation, form.tagNames[0] ?? '');
|
||||
}
|
||||
await callWikiInitialization(newWorkspaceConfig, wikiCreationMessageSetter, t, form.gitUserInfo, { notClose: options?.notClose, from: WikiCreationMethod.Create });
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
Autocomplete,
|
||||
AutocompleteRenderInputParams,
|
||||
Button as ButtonRaw,
|
||||
Chip,
|
||||
Divider,
|
||||
Paper,
|
||||
Switch,
|
||||
|
|
@ -167,8 +168,10 @@ export default function EditWorkspace(): React.JSX.Element {
|
|||
const storageService = isWiki ? workspace.storageService : SupportedStorageServices.github;
|
||||
const syncOnInterval = isWiki ? workspace.syncOnInterval : false;
|
||||
const syncOnStartup = isWiki ? workspace.syncOnStartup : false;
|
||||
const tagName = isWiki ? workspace.tagName : null;
|
||||
const tagNames = isWiki ? workspace.tagNames : [];
|
||||
const includeTagTree = isWiki ? workspace.includeTagTree : false;
|
||||
const fileSystemPathFilterEnable = isWiki ? workspace.fileSystemPathFilterEnable : false;
|
||||
const fileSystemPathFilter = isWiki ? workspace.fileSystemPathFilter : null;
|
||||
const transparentBackground = isWiki ? workspace.transparentBackground : false;
|
||||
const userName = isWiki ? workspace.userName : '';
|
||||
const lastUrl = isWiki ? workspace.lastUrl : null;
|
||||
|
|
@ -428,14 +431,58 @@ export default function EditWorkspace(): React.JSX.Element {
|
|||
<Typography variant='body2' color='textSecondary' sx={{ mb: 2 }}>
|
||||
{isSubWiki ? t('AddWorkspace.SubWorkspaceOptionsDescriptionForSub') : t('AddWorkspace.SubWorkspaceOptionsDescriptionForMain')}
|
||||
</Typography>
|
||||
<List>
|
||||
<ListItem
|
||||
disableGutters
|
||||
secondaryAction={
|
||||
<Switch
|
||||
edge='end'
|
||||
color='primary'
|
||||
checked={fileSystemPathFilterEnable}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
workspaceSetter({ ...workspace, fileSystemPathFilterEnable: event.target.checked }, true);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ListItemText
|
||||
primary={t('AddWorkspace.UseFilter')}
|
||||
secondary={t('AddWorkspace.UseFilterHelp')}
|
||||
/>
|
||||
</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={tagName}
|
||||
onInputChange={(_event: React.SyntheticEvent, value: string) => {
|
||||
value={tagNames}
|
||||
onChange={(_event: React.SyntheticEvent, newValue: string[]) => {
|
||||
void _event;
|
||||
workspaceSetter({ ...workspace, tagName: value }, true);
|
||||
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}
|
||||
|
|
@ -464,6 +511,8 @@ export default function EditWorkspace(): React.JSX.Element {
|
|||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</>
|
||||
)}
|
||||
</AccordionDetails>
|
||||
</OptionsAccordion>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ const mockWorkspaces: IWorkspace[] = [
|
|||
port: 5212,
|
||||
isSubWiki: false,
|
||||
mainWikiToLink: null,
|
||||
tagName: null,
|
||||
tagNames: [],
|
||||
lastUrl: null,
|
||||
active: true,
|
||||
hibernated: false,
|
||||
|
|
@ -57,7 +57,7 @@ const mockWorkspaces: IWorkspace[] = [
|
|||
port: 5213,
|
||||
isSubWiki: false,
|
||||
mainWikiToLink: null,
|
||||
tagName: null,
|
||||
tagNames: [],
|
||||
lastUrl: null,
|
||||
active: false,
|
||||
hibernated: false,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue