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)",
|
"CustomServerUrlDescription": "Base URL of the OAuth server (e.g., http://127.0.0.1:8888)",
|
||||||
"ExistedWikiLocation": "Existed Wiki Location",
|
"ExistedWikiLocation": "Existed Wiki Location",
|
||||||
"ExtractedWikiFolderName": "Converted WIKI folder name",
|
"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",
|
"GitBranch": "Git Branch",
|
||||||
"GitBranchDescription": "Git branch to use (default: main)",
|
"GitBranchDescription": "Git branch to use (default: main)",
|
||||||
"GitDefaultBranchDescription": "The default branch of your Git, Github changed it from master to main after that event",
|
"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)",
|
"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.",
|
"TagNameHelpForMain": "New entries with this tag will be prioritized for storage in this workspace.",
|
||||||
"ThisPathIsNotAWikiFolder": "The directory is not a Wiki folder \"{{wikiPath}}\"",
|
"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",
|
"WaitForLogin": "Wait for Login",
|
||||||
"WikiExisted": "Wiki already exists at this location \"{{newWikiPath}}\"",
|
"WikiExisted": "Wiki already exists at this location \"{{newWikiPath}}\"",
|
||||||
"WikiNotStarted": "Wiki is not started or not loaded",
|
"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)",
|
"CustomServerUrlDescription": "URL de base du serveur OAuth (par exemple : http://127.0.0.1:8888)",
|
||||||
"ExistedWikiLocation": "Emplacement du Wiki existant",
|
"ExistedWikiLocation": "Emplacement du Wiki existant",
|
||||||
"ExtractedWikiFolderName": "Nom du dossier WIKI converti",
|
"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",
|
"GitBranch": "Branche Git",
|
||||||
"GitBranchDescription": "Branche Git à utiliser (par défaut : main)",
|
"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",
|
"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)",
|
"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.",
|
"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}}\"",
|
"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",
|
"WaitForLogin": "Attendre la connexion",
|
||||||
"WikiExisted": "Le Wiki existe déjà à cet emplacement \"{{newWikiPath}}\"",
|
"WikiExisted": "Le Wiki existe déjà à cet emplacement \"{{newWikiPath}}\"",
|
||||||
"WikiNotStarted": "Le Wiki n'est pas démarré ou n'est pas chargé",
|
"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)",
|
"CustomServerUrlDescription": "OAuthサーバーのベースURL(例:http://127.0.0.1:8888)",
|
||||||
"ExistedWikiLocation": "既存のWikiの場所",
|
"ExistedWikiLocation": "既存のWikiの場所",
|
||||||
"ExtractedWikiFolderName": "変換されたWIKIフォルダ名",
|
"ExtractedWikiFolderName": "変換されたWIKIフォルダ名",
|
||||||
|
"FilterExpression": "フィルター式",
|
||||||
|
"FilterExpressionHelp": "TiddlyWikiフィルター式を1行ずつ入力してください。いずれかが一致すると、このワークスペースに保存されます。例:[in-tagtree-of[Calendar]!tag[Public]]",
|
||||||
"GitBranch": "Git ブランチ",
|
"GitBranch": "Git ブランチ",
|
||||||
"GitBranchDescription": "使用するGitブランチ(デフォルト:main)",
|
"GitBranchDescription": "使用するGitブランチ(デフォルト:main)",
|
||||||
"GitDefaultBranchDescription": "Gitのデフォルトブランチ。Githubはそのイベント後にmasterからmainに変更しました",
|
"GitDefaultBranchDescription": "Gitのデフォルトブランチ。Githubはそのイベント後にmasterからmainに変更しました",
|
||||||
|
|
@ -70,6 +72,8 @@
|
||||||
"TagNameHelp": "このタグを持つTiddlerはこのサブWikiに追加されます(後で右クリックしてワークスペースアイコンを選択し、ワークスペースを編集することでこのタグを追加または変更できます)",
|
"TagNameHelp": "このタグを持つTiddlerはこのサブWikiに追加されます(後で右クリックしてワークスペースアイコンを選択し、ワークスペースを編集することでこのタグを追加または変更できます)",
|
||||||
"TagNameHelpForMain": "このタグが付いた新しいエントリは、このワークスペースに優先的に保存されます",
|
"TagNameHelpForMain": "このタグが付いた新しいエントリは、このワークスペースに優先的に保存されます",
|
||||||
"ThisPathIsNotAWikiFolder": "このディレクトリはWikiフォルダではありません \"{{wikiPath}}\"",
|
"ThisPathIsNotAWikiFolder": "このディレクトリはWikiフォルダではありません \"{{wikiPath}}\"",
|
||||||
|
"UseFilter": "フィルターを使用する",
|
||||||
|
"UseFilterHelp": "フィルター式を使用してエントリをマッチングし、現在のワークスペースに保存するかどうかを決定します。タグではなくフィルター式を用います。",
|
||||||
"WaitForLogin": "ログインを待っています",
|
"WaitForLogin": "ログインを待っています",
|
||||||
"WikiExisted": "この場所にWikiが既に存在します \"{{newWikiPath}}\"",
|
"WikiExisted": "この場所にWikiが既に存在します \"{{newWikiPath}}\"",
|
||||||
"WikiNotStarted": "Wikiが開始されていないか、読み込まれていません",
|
"WikiNotStarted": "Wikiが開始されていないか、読み込まれていません",
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@
|
||||||
"CustomServerUrlDescription": "Базовый URL сервера OAuth (например: http://127.0.0.1:8888)",
|
"CustomServerUrlDescription": "Базовый URL сервера OAuth (например: http://127.0.0.1:8888)",
|
||||||
"ExistedWikiLocation": "Местоположение существующей Wiki",
|
"ExistedWikiLocation": "Местоположение существующей Wiki",
|
||||||
"ExtractedWikiFolderName": "Имя папки извлеченной WIKI",
|
"ExtractedWikiFolderName": "Имя папки извлеченной WIKI",
|
||||||
|
"FilterExpression": "выражение фильтра",
|
||||||
|
"FilterExpressionHelp": "Каждая строка содержит выражение фильтра TiddlyWiki. Если хотя бы одно из них совпадает, запись сохраняется в этой рабочей области. Например: [in-tagtree-of[Calendar]!tag[Public]].",
|
||||||
"GitBranch": "Ветка Git",
|
"GitBranch": "Ветка Git",
|
||||||
"GitBranchDescription": "Используемая ветка Git (по умолчанию: main)",
|
"GitBranchDescription": "Используемая ветка Git (по умолчанию: main)",
|
||||||
"GitDefaultBranchDescription": "Основная ветка вашего Git, Github изменил ее с master на main после того событ<D18B><D182>я",
|
"GitDefaultBranchDescription": "Основная ветка вашего Git, Github изменил ее с master на main после того событ<D18B><D182>я",
|
||||||
|
|
@ -70,6 +72,8 @@
|
||||||
"TagNameHelp": "Тидлеры с этим тегом будут добавлены в эту под-Wiki (вы можете добавить или изменить этот тег позже, щелкнув правой кнопкой мыши значок рабочего пространства и выбрав Настроить рабочее пространство)",
|
"TagNameHelp": "Тидлеры с этим тегом будут добавлены в эту под-Wiki (вы можете добавить или изменить этот тег позже, щелкнув правой кнопкой мыши значок рабочего пространства и выбрав Настроить рабочее пространство)",
|
||||||
"TagNameHelpForMain": "Новые записи с этой меткой будут сохраняться в первую очередь в этой рабочей области.",
|
"TagNameHelpForMain": "Новые записи с этой меткой будут сохраняться в первую очередь в этой рабочей области.",
|
||||||
"ThisPathIsNotAWikiFolder": "Каталог не является папкой Wiki \"{{wikiPath}}\"",
|
"ThisPathIsNotAWikiFolder": "Каталог не является папкой Wiki \"{{wikiPath}}\"",
|
||||||
|
"UseFilter": "использовать фильтр",
|
||||||
|
"UseFilterHelp": "Используйте выражения фильтров вместо меток для сопоставления записей и определения, следует ли сохранять их в текущей рабочей области.",
|
||||||
"WaitForLogin": "Ожидание входа",
|
"WaitForLogin": "Ожидание входа",
|
||||||
"WikiExisted": "Wiki уже существует в этом месте \"{{newWikiPath}}\"",
|
"WikiExisted": "Wiki уже существует в этом месте \"{{newWikiPath}}\"",
|
||||||
"WikiNotStarted": "Wiki не запущена или не загружена",
|
"WikiNotStarted": "Wiki не запущена или не загружена",
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@
|
||||||
"CustomServerUrlDescription": "OAuth 伺服器的基礎 URL(例如:http://127.0.0.1:8888)",
|
"CustomServerUrlDescription": "OAuth 伺服器的基礎 URL(例如:http://127.0.0.1:8888)",
|
||||||
"ExistedWikiLocation": "現有的知識庫的位置",
|
"ExistedWikiLocation": "現有的知識庫的位置",
|
||||||
"ExtractedWikiFolderName": "轉換後的知識庫文件夾名稱",
|
"ExtractedWikiFolderName": "轉換後的知識庫文件夾名稱",
|
||||||
|
"FilterExpression": "篩選器表達式",
|
||||||
|
"FilterExpressionHelp": "每行一個TiddlyWiki篩選器表達式,任一匹配即存入此工作區。例如 [in-tagtree-of[Calendar]!tag[Public]]",
|
||||||
"GitBranch": "Git 分支",
|
"GitBranch": "Git 分支",
|
||||||
"GitBranchDescription": "要使用的 Git 分支(預設:main)",
|
"GitBranchDescription": "要使用的 Git 分支(預設:main)",
|
||||||
"GitDefaultBranchDescription": "你的Git的預設分支,Github在黑命貴事件後將其從master改為了main",
|
"GitDefaultBranchDescription": "你的Git的預設分支,Github在黑命貴事件後將其從master改為了main",
|
||||||
|
|
@ -70,6 +72,8 @@
|
||||||
"TagNameHelp": "加上此標籤的筆記將會自動被放入這個子知識庫內(可先不填,之後右鍵點擊這個工作區的圖示選擇編輯工作區修改)",
|
"TagNameHelp": "加上此標籤的筆記將會自動被放入這個子知識庫內(可先不填,之後右鍵點擊這個工作區的圖示選擇編輯工作區修改)",
|
||||||
"TagNameHelpForMain": "帶有此標籤的新條目將優先保存在此工作區",
|
"TagNameHelpForMain": "帶有此標籤的新條目將優先保存在此工作區",
|
||||||
"ThisPathIsNotAWikiFolder": "該目錄不是一個知識庫文件夾 \"{{wikiPath}}\"",
|
"ThisPathIsNotAWikiFolder": "該目錄不是一個知識庫文件夾 \"{{wikiPath}}\"",
|
||||||
|
"UseFilter": "使用篩選器",
|
||||||
|
"UseFilterHelp": "用篩選器運算式而不是標籤來匹配條目,決定是否存入當前工作區",
|
||||||
"WaitForLogin": "等待登錄",
|
"WaitForLogin": "等待登錄",
|
||||||
"WikiExisted": "知識庫已經存在於該位置 \"{{newWikiPath}}\"",
|
"WikiExisted": "知識庫已經存在於該位置 \"{{newWikiPath}}\"",
|
||||||
"WikiNotStarted": "知識庫 頁面未成功啟動或未成功載入",
|
"WikiNotStarted": "知識庫 頁面未成功啟動或未成功載入",
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,10 @@ const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
||||||
</ThemeProvider>
|
</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', () => {
|
describe('KeyboardShortcutRegister Component', () => {
|
||||||
let mockOnChange: ReturnType<typeof vi.fn>;
|
let mockOnChange: ReturnType<typeof vi.fn>;
|
||||||
|
|
||||||
|
|
@ -158,6 +162,7 @@ describe('KeyboardShortcutRegister Component', () => {
|
||||||
const dialogContent = screen.getByTestId('shortcut-dialog-content');
|
const dialogContent = screen.getByTestId('shortcut-dialog-content');
|
||||||
|
|
||||||
// Simulate keyboard event with Ctrl+Shift+T
|
// Simulate keyboard event with Ctrl+Shift+T
|
||||||
|
// On macOS, ctrlKey is displayed as 'Cmd'
|
||||||
fireEvent.keyDown(dialogContent, {
|
fireEvent.keyDown(dialogContent, {
|
||||||
key: 'T',
|
key: 'T',
|
||||||
ctrlKey: true,
|
ctrlKey: true,
|
||||||
|
|
@ -167,7 +172,7 @@ describe('KeyboardShortcutRegister Component', () => {
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const display = screen.getByTestId('shortcut-display');
|
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(() => {
|
await waitFor(() => {
|
||||||
const display = screen.getByTestId('shortcut-display');
|
const display = screen.getByTestId('shortcut-display');
|
||||||
expect(display).toHaveTextContent('Ctrl+A');
|
expect(display).toHaveTextContent(`${getCtrlModifier()}+A`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Press second combination - should replace
|
// Press second combination - should replace
|
||||||
|
|
@ -297,7 +302,7 @@ describe('KeyboardShortcutRegister Component', () => {
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const display = screen.getByTestId('shortcut-display');
|
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(() => {
|
await waitFor(() => {
|
||||||
const display = screen.getByTestId('shortcut-display');
|
const display = screen.getByTestId('shortcut-display');
|
||||||
expect(display).toHaveTextContent('Ctrl+N');
|
expect(display).toHaveTextContent(`${getCtrlModifier()}+N`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Press Enter to confirm
|
// Press Enter to confirm
|
||||||
fireEvent.keyDown(document, { key: 'Enter', code: 'Enter' });
|
fireEvent.keyDown(document, { key: 'Enter', code: 'Enter' });
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockOnChange).toHaveBeenCalledWith('Ctrl+N');
|
expect(mockOnChange).toHaveBeenCalledWith(`${getCtrlModifier()}+N`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -406,7 +411,7 @@ describe('KeyboardShortcutRegister Component', () => {
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const display = screen.getByTestId('shortcut-display');
|
const display = screen.getByTestId('shortcut-display');
|
||||||
expect(display).toHaveTextContent('Ctrl+B');
|
expect(display).toHaveTextContent(`${getCtrlModifier()}+B`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Press ESC to cancel without saving
|
// Press ESC to cancel without saving
|
||||||
|
|
@ -485,6 +490,8 @@ describe('KeyboardShortcutRegister Component', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Simulate Ctrl+X key press on document
|
// 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, {
|
fireEvent.keyDown(document, {
|
||||||
key: 'X',
|
key: 'X',
|
||||||
ctrlKey: true,
|
ctrlKey: true,
|
||||||
|
|
@ -493,14 +500,14 @@ describe('KeyboardShortcutRegister Component', () => {
|
||||||
|
|
||||||
// Wait for the key combination to be processed
|
// Wait for the key combination to be processed
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText('Ctrl+X')).toBeInTheDocument();
|
expect(screen.getByText(`${expectedModifier}+X`)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Press Enter to confirm
|
// Press Enter to confirm
|
||||||
fireEvent.keyDown(document, { key: 'Enter', code: 'Enter' });
|
fireEvent.keyDown(document, { key: 'Enter', code: 'Enter' });
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(customOnChange).toHaveBeenCalledWith('Ctrl+X');
|
expect(customOnChange).toHaveBeenCalledWith(`${expectedModifier}+X`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,8 @@ export class FileSystemAdaptor {
|
||||||
boot: typeof $tw.boot;
|
boot: typeof $tw.boot;
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
workspaceID: string;
|
workspaceID: string;
|
||||||
/** All workspaces (main + sub-wikis) that have tagName configured, sorted by order */
|
/** All workspaces (main + sub-wikis) that have tagName or filter configured, sorted by order */
|
||||||
protected wikisWithTag: IWikiWorkspace[] = [];
|
protected wikisWithRouting: IWikiWorkspace[] = [];
|
||||||
/** Map of tagName -> workspace for O(1) tag lookup instead of O(n) find */
|
|
||||||
protected tagNameToWiki: Map<string, IWikiWorkspace> = new Map();
|
|
||||||
/** Cached extension filters from $:/config/FileSystemExtensions. Requires restart to reflect changes. */
|
/** Cached extension filters from $:/config/FileSystemExtensions. Requires restart to reflect changes. */
|
||||||
protected extensionFilters: string[] | undefined;
|
protected extensionFilters: string[] | undefined;
|
||||||
protected watchPathBase!: string;
|
protected watchPathBase!: string;
|
||||||
|
|
@ -73,15 +71,13 @@ export class FileSystemAdaptor {
|
||||||
protected async updateSubWikisCache(): Promise<void> {
|
protected async updateSubWikisCache(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
if (!this.workspaceID) {
|
if (!this.workspaceID) {
|
||||||
this.wikisWithTag = [];
|
this.wikisWithRouting = [];
|
||||||
this.tagNameToWiki.clear();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentWorkspace = await workspace.get(this.workspaceID);
|
const currentWorkspace = await workspace.get(this.workspaceID);
|
||||||
if (!currentWorkspace) {
|
if (!currentWorkspace) {
|
||||||
this.wikisWithTag = [];
|
this.wikisWithRouting = [];
|
||||||
this.tagNameToWiki.clear();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,17 +108,9 @@ export class FileSystemAdaptor {
|
||||||
|
|
||||||
return isMain || isSubWiki;
|
return isMain || isSubWiki;
|
||||||
};
|
};
|
||||||
const workspacesWithTag = allWorkspaces.filter(isWikiWorkspaceWithRouting).sort(workspaceSorter);
|
const workspacesWithRouting = allWorkspaces.filter(isWikiWorkspaceWithRouting).sort(workspaceSorter);
|
||||||
|
|
||||||
this.wikisWithTag = workspacesWithTag;
|
this.wikisWithRouting = workspacesWithRouting;
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.alert('filesystem: Failed to update sub-wikis cache:', 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
|
// Check if existing file is already in the correct directory
|
||||||
// If so, just return the existing fileInfo to avoid echo loops
|
// If so, just return the existing fileInfo to avoid echo loops
|
||||||
if (existingFileInfo?.filepath) {
|
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 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)
|
// 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);
|
const normalizedTarget = path.normalize(targetDirectory);
|
||||||
|
|
||||||
// Check if existing file is within the target directory tree
|
// 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.
|
* Match a tiddler to a workspace based on routing rules.
|
||||||
* Checks workspaces in order (priority) and returns the first match.
|
* Checks workspaces in order (priority) and returns the first match.
|
||||||
*
|
*
|
||||||
* For each workspace:
|
* For each workspace, checks in order (any match wins):
|
||||||
* 1. If fileSystemPathFilterEnable is enabled, use custom filter expressions (one per line, any match wins)
|
* 1. Direct tag match (including if tiddler's title IS one of the tagNames - it's a "tag tiddler")
|
||||||
* 2. Else try 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. Else if includeTagTree is enabled, use in-tagtree-of filter
|
* 3. If fileSystemPathFilterEnable is enabled, use custom filter expressions (one per line, any match wins)
|
||||||
*/
|
*/
|
||||||
protected matchTitleToWiki(title: string, tags: string[]): IWikiWorkspace | undefined {
|
protected matchTitleToWiki(title: string, tags: string[]): IWikiWorkspace | undefined {
|
||||||
for (const wiki of this.wikisWithTag) {
|
for (const wiki of this.wikisWithRouting) {
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Direct tag match - check if any of the tiddler's tags match any of the wiki's tagNames
|
// 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)
|
// 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) {
|
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;
|
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 = {
|
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',
|
type: 'application/x-tiddler',
|
||||||
hasMetaFile: false,
|
hasMetaFile: false,
|
||||||
};
|
};
|
||||||
|
|
@ -181,14 +203,8 @@ describe('FileSystemAdaptor - Routing Logic', () => {
|
||||||
|
|
||||||
await adaptor.getTiddlerFileInfo(tiddler);
|
await adaptor.getTiddlerFileInfo(tiddler);
|
||||||
|
|
||||||
expect(mockUtils.generateTiddlerFileInfo).toHaveBeenCalledWith(
|
// Should call generateTiddlerFileInfo since file needs to be moved
|
||||||
tiddler,
|
expect(mockUtils.generateTiddlerFileInfo).toHaveBeenCalled();
|
||||||
expect.objectContaining({
|
|
||||||
fileInfo: expect.objectContaining({
|
|
||||||
overwrite: true,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error when wikiTiddlersPath is not set', async () => {
|
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
|
// Manually trigger cache update and wait for it
|
||||||
await adaptor['updateSubWikisCache']();
|
await adaptor['updateSubWikisCache']();
|
||||||
|
|
||||||
expect(adaptor['wikisWithTag']).toEqual([]);
|
expect(adaptor['wikisWithRouting']).toEqual([]);
|
||||||
expect(adaptor['tagNameToWiki'].size).toBe(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should clear cache when currentWorkspace is not found', async () => {
|
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
|
// Manually trigger cache update and wait for it
|
||||||
await adaptor['updateSubWikisCache']();
|
await adaptor['updateSubWikisCache']();
|
||||||
|
|
||||||
expect(adaptor['wikisWithTag']).toEqual([]);
|
expect(adaptor['wikisWithRouting']).toEqual([]);
|
||||||
expect(adaptor['tagNameToWiki'].size).toBe(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle errors in updateSubWikisCache gracefully', async () => {
|
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
|
!!! How It Works
|
||||||
|
|
||||||
# Queries workspace information from TidGi's main process via IPC
|
# Queries workspace information from TidGi's main process via IPC
|
||||||
# Checks each tiddler's tags/filters against sub-workspace routing rules:
|
# Checks each tiddler against sub-workspace routing rules (any match wins):
|
||||||
#* Multiple tag names (`tagNames`) - matches if any tag matches
|
#* 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 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`
|
#* Tag tree (`includeTagTree`) - if enabled, uses `in-tagtree-of` filter for recursive tag hierarchy matching
|
||||||
#* Custom filter expressions (`fileSystemPathFilter`) - one per line, any match wins
|
#* 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
|
# Routes tiddlers with matching rules to the corresponding sub-wiki's root folder
|
||||||
# Falls back to standard `$:/config/FileSystemPaths` logic for non-matching tiddlers
|
# Falls back to standard `$:/config/FileSystemPaths` logic for non-matching tiddlers
|
||||||
# Only moves files when target directory changes (avoids echo loops)
|
# 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 multiple tags per workspace
|
||||||
* Supports tag hierarchy matching
|
* Supports tag hierarchy matching
|
||||||
* Supports custom TiddlyWiki filter expressions
|
* 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
|
* Handles tag changes - moves tiddlers when tags are modified
|
||||||
* Tag tiddlers (tiddlers whose title matches a tag name) are also routed correctly
|
* Tag tiddlers (tiddlers whose title matches a tag name) are also routed correctly
|
||||||
* More robust than string manipulation
|
* More robust than string manipulation
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import type { IWikiWorkspace } from '@services/workspaces/interface';
|
import type { IWikiWorkspace } from '@services/workspaces/interface';
|
||||||
import path from 'path';
|
|
||||||
import type { TiddlyWiki } from 'tiddlywiki';
|
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
|
* 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.
|
* 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;
|
includeTagTree: boolean;
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import FolderIcon from '@mui/icons-material/Folder';
|
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 React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
|
@ -91,11 +91,11 @@ export function CloneWikiForm({ form, isCreateMainWorkspace, errorInWhichCompone
|
||||||
onChange={(_event, newValue) => {
|
onChange={(_event, newValue) => {
|
||||||
form.tagNamesSetter(newValue);
|
form.tagNamesSetter(newValue);
|
||||||
}}
|
}}
|
||||||
renderTags={(value, getTagProps) =>
|
slotProps={{
|
||||||
value.map((option, index) => {
|
chip: {
|
||||||
const { key, ...tagProps } = getTagProps({ index });
|
variant: 'outlined',
|
||||||
return <Chip variant='outlined' label={option} key={key} {...tagProps} />;
|
},
|
||||||
})}
|
}}
|
||||||
renderInput={(parameters: AutocompleteRenderInputParams) => (
|
renderInput={(parameters: AutocompleteRenderInputParams) => (
|
||||||
<LocationPickerInput
|
<LocationPickerInput
|
||||||
{...parameters}
|
{...parameters}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import FolderIcon from '@mui/icons-material/Folder';
|
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 { useCallback, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
|
@ -140,11 +140,11 @@ export function ExistedWikiForm({
|
||||||
onChange={(_event, newValue) => {
|
onChange={(_event, newValue) => {
|
||||||
tagNamesSetter(newValue);
|
tagNamesSetter(newValue);
|
||||||
}}
|
}}
|
||||||
renderTags={(value, getTagProps) =>
|
slotProps={{
|
||||||
value.map((option, index) => {
|
chip: {
|
||||||
const { key, ...tagProps } = getTagProps({ index });
|
variant: 'outlined',
|
||||||
return <Chip variant='outlined' label={option} key={key} {...tagProps} />;
|
},
|
||||||
})}
|
}}
|
||||||
renderInput={(parameters: AutocompleteRenderInputParams) => (
|
renderInput={(parameters: AutocompleteRenderInputParams) => (
|
||||||
<LocationPickerInput
|
<LocationPickerInput
|
||||||
{...parameters}
|
{...parameters}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import FolderIcon from '@mui/icons-material/Folder';
|
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 { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { isWikiWorkspace } from '@services/workspaces/interface';
|
import { isWikiWorkspace } from '@services/workspaces/interface';
|
||||||
|
|
@ -97,11 +97,11 @@ export function NewWikiForm({
|
||||||
onChange={(_event, newValue) => {
|
onChange={(_event, newValue) => {
|
||||||
form.tagNamesSetter(newValue);
|
form.tagNamesSetter(newValue);
|
||||||
}}
|
}}
|
||||||
renderTags={(value, getTagProps) =>
|
slotProps={{
|
||||||
value.map((option, index) => {
|
chip: {
|
||||||
const { key, ...tagProps } = getTagProps({ index });
|
variant: 'outlined',
|
||||||
return <Chip variant='outlined' label={option} key={key} {...tagProps} />;
|
},
|
||||||
})}
|
}}
|
||||||
renderInput={(parameters: AutocompleteRenderInputParams) => (
|
renderInput={(parameters: AutocompleteRenderInputParams) => (
|
||||||
<LocationPickerInput
|
<LocationPickerInput
|
||||||
error={errorInWhichComponent.tagNames}
|
error={errorInWhichComponent.tagNames}
|
||||||
|
|
|
||||||
|
|
@ -276,11 +276,11 @@ describe('NewWikiForm Component', () => {
|
||||||
isCreateMainWorkspace: false,
|
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) => {
|
expect(screen.getByText((content, _element) => {
|
||||||
// The actual text might have whitespace and newlines
|
// The actual text might have whitespace and newlines
|
||||||
const normalized = content.replace(/\s+/g, ' ').trim();
|
const normalized = content.replace(/\s+/g, ' ').trim();
|
||||||
return normalized === 'AddWorkspace.SubWorkspaceWillLinkTo /main/wiki/tiddlers/subwiki/sub-wiki';
|
return normalized === 'AddWorkspace.SubWorkspaceWillLinkTo /main/wiki';
|
||||||
})).toBeInTheDocument();
|
})).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import {
|
||||||
Autocomplete,
|
Autocomplete,
|
||||||
AutocompleteRenderInputParams,
|
AutocompleteRenderInputParams,
|
||||||
Button as ButtonRaw,
|
Button as ButtonRaw,
|
||||||
Chip,
|
|
||||||
Divider,
|
Divider,
|
||||||
Paper,
|
Paper,
|
||||||
Switch,
|
Switch,
|
||||||
|
|
@ -431,7 +430,47 @@ export default function EditWorkspace(): React.JSX.Element {
|
||||||
<Typography variant='body2' color='textSecondary' sx={{ mb: 2 }}>
|
<Typography variant='body2' color='textSecondary' sx={{ mb: 2 }}>
|
||||||
{isSubWiki ? t('AddWorkspace.SubWorkspaceOptionsDescriptionForSub') : t('AddWorkspace.SubWorkspaceOptionsDescriptionForMain')}
|
{isSubWiki ? t('AddWorkspace.SubWorkspaceOptionsDescriptionForSub') : t('AddWorkspace.SubWorkspaceOptionsDescriptionForMain')}
|
||||||
</Typography>
|
</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>
|
<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
|
<ListItem
|
||||||
disableGutters
|
disableGutters
|
||||||
secondaryAction={
|
secondaryAction={
|
||||||
|
|
@ -451,68 +490,21 @@ export default function EditWorkspace(): React.JSX.Element {
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
{fileSystemPathFilterEnable
|
{fileSystemPathFilterEnable && (
|
||||||
? (
|
<TextField
|
||||||
<TextField
|
fullWidth
|
||||||
fullWidth
|
multiline
|
||||||
multiline
|
minRows={2}
|
||||||
minRows={2}
|
maxRows={10}
|
||||||
maxRows={10}
|
value={fileSystemPathFilter ?? ''}
|
||||||
value={fileSystemPathFilter ?? ''}
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
workspaceSetter({ ...workspace, fileSystemPathFilter: event.target.value || null }, true);
|
||||||
workspaceSetter({ ...workspace, fileSystemPathFilter: event.target.value || null }, true);
|
}}
|
||||||
}}
|
label={t('AddWorkspace.FilterExpression')}
|
||||||
label={t('AddWorkspace.FilterExpression')}
|
helperText={t('AddWorkspace.FilterExpressionHelp')}
|
||||||
helperText={t('AddWorkspace.FilterExpressionHelp')}
|
sx={{ mb: 2 }}
|
||||||
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>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
</OptionsAccordion>
|
</OptionsAccordion>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue