fix: test and ui logic

This commit is contained in:
linonetwo 2025-12-05 21:32:34 +08:00
parent 09a2c7a005
commit d76b3ff098
16 changed files with 169 additions and 149 deletions

View file

@ -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",

View file

@ -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é",

View file

@ -19,6 +19,8 @@
"CustomServerUrlDescription": "OAuthサーバーのベースURLhttp://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が開始されていないか、読み込まれていません",

View file

@ -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 не запущена или не загружена",

View file

@ -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": "知識庫 頁面未成功啟動或未成功載入",

View file

@ -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`);
});
});
});

View file

@ -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;
}

View file

@ -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 () => {

View file

@ -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

View file

@ -1,5 +1,4 @@
import type { IWikiWorkspace } from '@services/workspaces/interface';
import path from 'path';
import type { TiddlyWiki } from 'tiddlywiki';
/**

View file

@ -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;
/**

View file

@ -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}

View file

@ -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}

View file

@ -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}

View file

@ -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();
});
});

View file

@ -7,7 +7,6 @@ import {
Autocomplete,
AutocompleteRenderInputParams,
Button as ButtonRaw,
Chip,
Divider,
Paper,
Switch,
@ -431,44 +430,6 @@ 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
@ -478,11 +439,11 @@ export default function EditWorkspace(): React.JSX.Element {
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} />;
})}
slotProps={{
chip: {
variant: 'outlined',
},
}}
renderInput={(parameters: AutocompleteRenderInputParams) => (
<TextField
{...parameters}
@ -510,8 +471,39 @@ export default function EditWorkspace(): React.JSX.Element {
secondary={isSubWiki ? t('AddWorkspace.IncludeTagTreeHelp') : t('AddWorkspace.IncludeTagTreeHelpForMain')}
/>
</ListItem>
<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 }}
/>
)}
</AccordionDetails>
</OptionsAccordion>