feat: basic load and save to sub wiki using in-tag-tree-of

This commit is contained in:
linonetwo 2025-12-05 01:09:34 +08:00
parent 6af957fef6
commit cd96e60eb9
26 changed files with 437 additions and 68 deletions

View file

@ -30,6 +30,9 @@
"GitUserName": "Git Username",
"GitUserNameDescription": "The account name used to log in to Git, note that it is the name part of your repository URL",
"ImportWiki": "Import Wiki: ",
"IncludeTagTree": "Include Entire Tag Tree",
"IncludeTagTreeHelp": "When enabled, tiddlers whose tag (or tag's tag, recursively) matches this sub-wiki's tag will also be saved to this sub-wiki",
"IncludeTagTreeHelpForMain": "When checked, any label whose parent label (at any level...) is this one will be categorized into this workspace.",
"LocalWikiHtml": "path to html file",
"LocalWorkspace": "Local Workspace",
"LocalWorkspaceDescription": "Only use locally, fully control your own data. TidGi will create a local git backup system for you, allowing you to go back to the previous versions of tiddlers, but all contents will be lost when the local folder is deleted.",
@ -56,12 +59,16 @@
"SubWikiCreationCompleted": "Sub Wiki is created",
"SubWorkspace": "Sub Workspace",
"SubWorkspaceDescription": "It must be attached to a main repository, which can be used to store private content, Note two points: the sub-knowledge base cannot be placed in the main knowledge base folder; the sub-knowledge base is generally used to synchronize data to a private Github repository, which can only be read and written by me, so the repository address cannot be the same as the main knowledge base.\nThe sub-knowledge base takes effect by creating a soft link (shortcut) to the main knowledge base. After the link is created, the content in the sub-knowledge base can be seen in the main knowledge base.",
"SubWorkspaceOptions": "Sub-Workspace Options",
"SubWorkspaceOptionsDescriptionForMain": "Configure which tiddlers this main workspace prioritizes. When a tag is set, new tiddlers with this tag will be saved to the main workspace instead of sub-workspaces",
"SubWorkspaceOptionsDescriptionForSub": "Configure which tiddlers are saved to this sub-workspace. New tiddlers with the specified tag will be saved here",
"SubWorkspaceWillLinkTo": "Sub-Workspace will link to",
"SwitchCreateNewOrOpenExisted": "Switch to create a new or open an existing WIKI",
"SyncedWorkspace": "Synced Workspace",
"SyncedWorkspaceDescription": "To synchronize to an online storage service (such as Github), you need to login to a storage service or enter your login credentials, and have a good network connection. You can sync data across devices, and you still own the data when you use a trusted storage service. And even after the folder is accidentally deleted, you can still download the data from the online service to the local again.",
"TagName": "Tag Name",
"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}}\"",
"WaitForLogin": "Wait for Login",
"WikiExisted": "Wiki already exists at this location \"{{newWikiPath}}\"",

View file

@ -30,6 +30,9 @@
"GitUserName": "Nom d'utilisateur Git",
"GitUserNameDescription": "Le nom de compte utilisé pour se connecter à Git. Pas le surnom",
"ImportWiki": "Importer un Wiki : ",
"IncludeTagTree": "Inclure l'arbre d'étiquettes entier",
"IncludeTagTreeHelp": "Lorsqu'activé, les tiddlers dont l'étiquette (ou l'étiquette de l'étiquette, récursivement) correspond à l'étiquette de ce sous-wiki seront également enregistrés dans ce sous-wiki",
"IncludeTagTreeHelpForMain": "Une fois coché, tout étiquette dont l'étiquette (à n'importe quel niveau...) est celle-ci sera classée dans cet espace de travail.",
"LocalWikiHtml": "chemin vers le fichier html",
"LocalWorkspace": "Espace de travail local",
"LocalWorkspaceDescription": "Utilisation uniquement locale, contrôle total de vos propres données. TidGi créera un système de sauvegarde git local pour vous, vous permettant de revenir aux versions précédentes des tiddlers, mais tout le contenu sera perdu lorsque le dossier local sera supprimé.",
@ -56,12 +59,16 @@
"SubWikiCreationCompleted": "Le sous-wiki est créé",
"SubWorkspace": "Espace de travail secondaire",
"SubWorkspaceDescription": "Il doit être attaché à un dépôt principal, qui peut être utilisé pour stocker du contenu privé. Notez deux points : la base de connaissances secondaire ne peut pas être placée dans le dossier de la base de connaissances principale ; la base de connaissances secondaire est généralement utilisée pour synchroniser les données avec un dépôt Github privé, qui ne peut être lu et écrit que par moi, donc l'adresse du dépôt ne peut pas être la même que celle de la base de connaissances principale.\nLa base de connaissances secondaire prend effet en créant un lien symbolique (raccourci) vers la base de connaissances principale. Après la création du lien, le contenu de la base de connaissances secondaire peut être vu dans la base de connaissances principale.",
"SubWorkspaceOptions": "Paramètres du sous-espace de travail",
"SubWorkspaceOptionsDescriptionForMain": "Configurez les tiddlers que cet espace de travail principal priorise. Lorsqu'une étiquette est définie, les nouveaux tiddlers avec cette étiquette seront enregistrés dans l'espace principal au lieu des sous-espaces",
"SubWorkspaceOptionsDescriptionForSub": "Configurez les tiddlers enregistrés dans ce sous-espace de travail. Les nouveaux tiddlers avec l'étiquette spécifiée seront enregistrés ici",
"SubWorkspaceWillLinkTo": "L'espace de travail secondaire sera lié à",
"SwitchCreateNewOrOpenExisted": "Passer à la création d'un nouveau Wiki ou à l'ouverture d'un Wiki existant",
"SyncedWorkspace": "Espace de travail synchronisé",
"SyncedWorkspaceDescription": "Pour synchroniser avec un service de stockage en ligne (comme Github), vous devez vous connecter à un service de stockage ou entrer vos informations d'identification, et avoir une bonne connexion réseau. Vous pouvez synchroniser les données entre les appareils, et vous possédez toujours les données lorsque vous utilisez un service de stockage de confiance. Et même après la suppression accidentelle du dossier, vous pouvez toujours télécharger les données du service en ligne vers le local à nouveau.",
"TagName": "Nom de l'étiquette",
"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}}\"",
"WaitForLogin": "Attendre la connexion",
"WikiExisted": "Le Wiki existe déjà à cet emplacement \"{{newWikiPath}}\"",

View file

@ -30,6 +30,9 @@
"GitUserName": "Git ユーザー名",
"GitUserNameDescription": "Gitにログインするために使用されるアカウント名。ニックネームではありません",
"ImportWiki": "Wikiをインポート: ",
"IncludeTagTree": "タグツリー全体を含める",
"IncludeTagTreeHelp": "有効にすると、タグまたはタグのタグ、再帰的にがこのサブWikiのタグに一致するTiddlerもこのサブWikiに保存されます",
"IncludeTagTreeHelpForMain": "チェックを入れると、タグのタグのタグ(任意のレベル…)がこれである限り、このワークスペースに分類されます。",
"LocalWikiHtml": "htmlファイルへのパス",
"LocalWorkspace": "ローカルワークスペース",
"LocalWorkspaceDescription": "ローカルでのみ使用し、データを完全に管理します。TidGiはローカルGitバックアップシステムを作成し、以前のバージョンに戻ることができますが、ローカルフォルダが削除されるとすべての内容が失われます。",
@ -56,12 +59,16 @@
"SubWikiCreationCompleted": "サブWikiが作成されました",
"SubWorkspace": "サブワークスペース",
"SubWorkspaceDescription": "メインリポジトリに付随する必要があり、プライベートコンテンツを保存するために使用できます。注意点は2つありますサブナレッジベースはメインナレッジベースフォルダ内に配置できませんサブナレッジベースは一般的にプライベートGithubリポジトリにデータを同期するために使用され、私だけが読み書きできます。そのため、リポジトリアドレスはメインナレッジベースと同じにすることはできません。\nサブナレッジベースはメインナレッジベースへのソフトリンクショートカットを作成することで有効になります。リンクが作成されると、メインナレッジベース内でサブナレッジベースの内容を見ることができます。",
"SubWorkspaceOptions": "子ワークスペース設定",
"SubWorkspaceOptionsDescriptionForMain": "このメインワークスペースが優先的に保存するTiddlerを設定します。タグを設定すると、このタグを持つ新しいTiddlerはサブワークスペースではなくメインワークスペースに保存されます",
"SubWorkspaceOptionsDescriptionForSub": "このサブワークスペースに保存されるTiddlerを設定します。指定されたタグを持つ新しいTiddlerはここに保存されます",
"SubWorkspaceWillLinkTo": "サブワークスペースは次にリンクされます",
"SwitchCreateNewOrOpenExisted": "新しいWikiを作成するか、既存のWikiを開くかを切り替える",
"SyncedWorkspace": "同期されたワークスペース",
"SyncedWorkspaceDescription": "オンラインストレージサービスGithubなどに同期するには、ストレージサービスにログインするか、ログイン資格情報を入力し、良好なネットワーク接続が必要です。デバイス間でデータを同期でき、信頼できるストレージサービスを使用している場合でもデータはあなたのものです。フォルダが誤って削除された場合でも、オンラインサービスからデータを再度ローカルにダウンロードできます。",
"TagName": "タグ名",
"TagNameHelp": "このタグを持つTiddlerはこのサブWikiに追加されます後で右クリックしてワークスペースアイコンを選択し、ワークスペースを編集することでこのタグを追加または変更できます",
"TagNameHelpForMain": "このタグが付いた新しいエントリは、このワークスペースに優先的に保存されます",
"ThisPathIsNotAWikiFolder": "このディレクトリはWikiフォルダではありません \"{{wikiPath}}\"",
"WaitForLogin": "ログインを待っています",
"WikiExisted": "この場所にWikiが既に存在します \"{{newWikiPath}}\"",

View file

@ -30,6 +30,9 @@
"GitUserName": "Имя пользователя Git",
"GitUserNameDescription": "Имя учетной записи, используемое для входа в Git. Не псевдоним",
"ImportWiki": "Импортировать Wiki: ",
"IncludeTagTree": "Включить все дерево тегов",
"IncludeTagTreeHelp": "При включении тидлеры, чей тег (или тег тега, рекурсивно) соответствует тегу этой под-Wiki, также будут сохранены в этой под-Wiki",
"IncludeTagTreeHelpForMain": "После выбора этой опции все метки (любого уровня вложенности), которые имеют данную метку, будут отнесены к этой рабочей области.",
"LocalWikiHtml": "путь к html файлу",
"LocalWorkspace": "Локальное рабочее пространство",
"LocalWorkspaceDescription": "Используется только локально, полностью контролируйте свои данные. TidGi создаст для вас локальную систему резервного копирования git, позволяющую вернуться к предыдущим версиям тидлеров, но все содержимое будет потеряно при удалении локальной папки.",
@ -56,12 +59,16 @@
"SubWikiCreationCompleted": "Под-Wiki создана",
"SubWorkspace": "Подрабочее пространство",
"SubWorkspaceDescription": "Должен быть привязан к основному репозиторию, который можно использовать для хранения личного контента. Обратите внимание на два момента: подбаза знаний не может быть размещена в папке основной базы знаний; подбаза знаний обычно используется для синхронизации данных с частным репозиторием Github, который может быть доступен только мне, поэтому адрес репозитория не может совпадать с адресом основной базы знаний.\nПодбаза знаний вступает в силу путем создания символической ссылки (ярлыка) на основную базу знаний. После создания ссылки содержимое подбазы знаний можно увидеть в основной базе знаний.",
"SubWorkspaceOptions": "Настройки подрабочей области",
"SubWorkspaceOptionsDescriptionForMain": "Настройте, какие тидлеры это основное рабочее пространство сохраняет приоритетно. При установке тега новые тидлеры с этим тегом будут сохраняться в основном рабочем пространстве",
"SubWorkspaceOptionsDescriptionForSub": "Настройте, какие тидлеры сохраняются в этом под-рабочем пространстве. Новые тидлеры с указанным тегом будут сохраняться здесь",
"SubWorkspaceWillLinkTo": "Подрабочее пространство будет привязано к",
"SwitchCreateNewOrOpenExisted": "Переключиться на создание новой или открытие существующей WIKI",
"SyncedWorkspace": "Синхронизированное рабочее пространство",
"SyncedWorkspaceDescription": "Для синхронизации с онлайн-сервисом хранения (например, Github) необходимо войти в сервис хранения или ввести свои учетные данные и иметь хорошее сетевое соединение. Вы можете синхронизировать данные между устройствами, и вы все равно будете владеть данными, используя надежный сервис хранения. И даже после случайного удаления папки вы все равно сможете загрузить данные с онлайн-сервиса на локальный компьютер.",
"TagName": "Имя тега",
"TagNameHelp": "Тидлеры с этим тегом будут добавлены в эту под-Wiki (вы можете добавить или изменить этот тег позже, щелкнув правой кнопкой мыши значок рабочего пространства и выбрав Настроить рабочее пространство)",
"TagNameHelpForMain": "Новые записи с этой меткой будут сохраняться в первую очередь в этой рабочей области.",
"ThisPathIsNotAWikiFolder": "Каталог не является папкой Wiki \"{{wikiPath}}\"",
"WaitForLogin": "Ожидание входа",
"WikiExisted": "Wiki уже существует в этом месте \"{{newWikiPath}}\"",

View file

@ -62,6 +62,13 @@
"SyncedWorkspaceDescription": "同步到在线存储服务例如Github需要你登录存储服务或输入登录凭证并有良好的网络连接。可以跨设备同步数据在使用了值得信任的存储服务的情况下数据仍归你所有。而且文件夹被不慎删除后还可以从在线服务重新下载数据到本地。",
"TagName": "标签名",
"TagNameHelp": "加上此标签的笔记将会自动被放入这个子知识库内(可先不填,之后右键点击这个工作区的图标选择编辑工作区修改)",
"TagNameHelpForMain": "带有此标签的新条目将优先保存在此工作区",
"IncludeTagTree": "包括整个标签树",
"IncludeTagTreeHelp": "勾选后,只要标签的标签的标签(任意级……)是这个,就会被划分到此子工作区里",
"IncludeTagTreeHelpForMain": "勾选后,只要标签的标签的标签(任意级……)是这个,就会被划分到此工作区里",
"SubWorkspaceOptions": "子工作区设置",
"SubWorkspaceOptionsDescriptionForMain": "配置此主工作区优先保存哪些条目。设置标签后,带有此标签的新条目会优先保存在主工作区,而非子工作区。匹配顺序按侧边栏从上到下,先匹配到的工作区优先",
"SubWorkspaceOptionsDescriptionForSub": "配置此子工作区保存哪些条目。带有指定标签的新条目将被保存到此子工作区。匹配顺序按侧边栏从上到下,先匹配到的工作区优先",
"ThisPathIsNotAWikiFolder": "该目录不是一个知识库文件夹 \"{{wikiPath}}\"",
"WaitForLogin": "等待登录",
"WikiExisted": "知识库已经存在于该位置 \"{{newWikiPath}}\"",

View file

@ -30,6 +30,9 @@
"GitUserName": "Git 使用者名稱",
"GitUserNameDescription": "用於登入Git的帳戶名注意是你的倉庫網址中你的名字部分",
"ImportWiki": "導入知識庫: ",
"IncludeTagTree": "包括整個標籤樹",
"IncludeTagTreeHelp": "勾選後,只要標籤的標籤的標籤(任意級……)是這個,就會被劃分到此子工作區裡",
"IncludeTagTreeHelpForMain": "勾選後,只要標籤的標籤的標籤(任意級……)是這個,就會被劃分到此工作區裡",
"LocalWikiHtml": "HTML文件的路徑",
"LocalWorkspace": "本地知識庫",
"LocalWorkspaceDescription": "僅在本地使用,完全掌控自己的數據。太記會為你創建一個本地的 git 備份系統,讓你可以回退到之前的版本,但當文件夾被刪除時所有內容還是會遺失。",
@ -56,12 +59,16 @@
"SubWikiCreationCompleted": "子知識庫創建完畢",
"SubWorkspace": "子知識庫",
"SubWorkspaceDescription": "必須依附於一個主知識庫可用於存放私有內容。注意兩點子知識庫不能放在主知識庫文件夾內子知識庫一般用於同步數據到一個私有的Github倉庫內僅本人可讀寫故倉庫地址不能與主知識庫一樣。\n子知識庫透過創建一個到主知識庫的軟連結捷徑來生效創建連結後主知識庫內便可看到子知識庫內的內容了。",
"SubWorkspaceOptions": "子工作區設定",
"SubWorkspaceOptionsDescriptionForMain": "配置此主工作區優先保存哪些條目。設置標籤後,帶有此標籤的新條目會優先保存在主工作區,而非子工作區",
"SubWorkspaceOptionsDescriptionForSub": "配置此子工作區保存哪些條目。帶有指定標籤的新條目將被保存到此子工作區",
"SubWorkspaceWillLinkTo": "子知識庫將連結到",
"SwitchCreateNewOrOpenExisted": "切換創建新的還是打開現有的知識庫",
"SyncedWorkspace": "雲端同步知識庫",
"SyncedWorkspaceDescription": "同步到在線儲存服務例如Github需要你登錄儲存服務或輸入登錄憑證並有良好的網路連接。可以跨設備同步數據在使用了值得信任的儲存服務的情況下數據仍歸你所有。而且文件夾被不慎刪除後還可以從在線服務重新下載數據到本地。",
"TagName": "標籤名",
"TagNameHelp": "加上此標籤的筆記將會自動被放入這個子知識庫內(可先不填,之後右鍵點擊這個工作區的圖示選擇編輯工作區修改)",
"TagNameHelpForMain": "帶有此標籤的新條目將優先保存在此工作區",
"ThisPathIsNotAWikiFolder": "該目錄不是一個知識庫文件夾 \"{{wikiPath}}\"",
"WaitForLogin": "等待登錄",
"WikiExisted": "知識庫已經存在於該位置 \"{{newWikiPath}}\"",

View file

@ -179,7 +179,7 @@
"rimraf": "^6.1.2",
"ts-node": "10.9.2",
"tsx": "^4.20.6",
"tw5-typed": "^1.0.5",
"tw5-typed": "^1.1.1",
"typescript": "5.9.3",
"typesync": "0.14.3",
"unplugin-swc": "^1.5.8",

10
pnpm-lock.yaml generated
View file

@ -418,8 +418,8 @@ importers:
specifier: ^4.20.6
version: 4.20.6
tw5-typed:
specifier: ^1.0.5
version: 1.0.5
specifier: ^1.1.1
version: 1.1.1
typescript:
specifier: 5.9.3
version: 5.9.3
@ -7092,8 +7092,8 @@ packages:
tunnel-agent@0.6.0:
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
tw5-typed@1.0.5:
resolution: {integrity: sha512-LUNQnzkqt7QhIb10VDLtyWBqmGAxQpU9Xh6sPam9I7Ras388X7WToyiAhgQuC6jNO238GeanPLx8CF+nhTZ2PQ==}
tw5-typed@1.1.1:
resolution: {integrity: sha512-hjuWQgG6grHRyOesOldwOuHIxTB2DuUKoSA8M2QiJoNgKSjBOFQ9jytWEEzgsPhhLOpGowE0bIxiyLQ89LbL1w==}
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
@ -15312,7 +15312,7 @@ snapshots:
dependencies:
safe-buffer: 5.2.1
tw5-typed@1.0.5:
tw5-typed@1.1.1:
dependencies:
'@types/codemirror': 5.60.17
'@types/echarts': 5.0.0

View file

@ -7,6 +7,7 @@ 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';
export interface ISortableListProps {
showSideBarIcon: boolean;
@ -62,7 +63,7 @@ export function SortableWorkspaceSelectorList({ workspacesList, showSideBarText,
>
<SortableContext items={workspaceIDs} strategy={verticalListSortingStrategy}>
{filteredWorkspacesList
.sort((a, b) => a.order - b.order)
.sort(workspaceSorter)
.map((workspace, index) => (
<SortableWorkspaceSelectorButton
key={`item-${workspace.id}`}

View file

@ -134,6 +134,10 @@ export class Wiki implements IWikiService {
});
}
const shouldUseDarkColors = await this.themeService.shouldUseDarkColors();
// Get sub-wikis for this main wiki to load their tiddlers
const subWikis = await workspaceService.getSubWorkspacesAsList(workspaceID);
const workerData: IStartNodeJSWikiConfigs = {
authToken,
constants: { TIDDLYWIKI_PACKAGE_FOLDER: getTiddlyWikiBootPath(wikiFolderLocation) },
@ -146,6 +150,7 @@ export class Wiki implements IWikiService {
readOnlyMode,
rootTiddler,
shouldUseDarkColors,
subWikis,
tiddlyWikiHost: defaultServerIP,
tiddlyWikiPort: port,
tokenAuth,

View file

@ -1,6 +1,7 @@
import type { Logger } from '$:/core/modules/utils/logger.js';
import { workspace } from '@services/wiki/wikiWorker/services';
import type { IWikiWorkspace, IWorkspace } from '@services/workspaces/interface';
import { workspaceSorter } from '@services/workspaces/utilities';
import { backOff } from 'exponential-backoff';
import fs from 'fs';
import path from 'path';
@ -19,9 +20,10 @@ export class FileSystemAdaptor {
boot: typeof $tw.boot;
logger: Logger;
workspaceID: string;
protected subWikisWithTag: IWikiWorkspace[] = [];
/** Map of tagName -> subWiki for O(1) tag lookup instead of O(n) find */
protected tagNameToSubWiki: Map<string, IWikiWorkspace> = new Map();
/** 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();
/** Cached extension filters from $:/config/FileSystemExtensions. Requires restart to reflect changes. */
protected extensionFilters: string[] | undefined;
protected watchPathBase!: string;
@ -64,40 +66,54 @@ export class FileSystemAdaptor {
}
/**
* Update the cached sub-wikis list and rebuild tag lookup map
* Update the cached workspaces list (main + sub-wikis) and rebuild tag lookup map.
* Sorted by order to ensure consistent priority when matching tags.
* Main workspace can also have tagName/includeTagTree for priority routing.
*/
protected async updateSubWikisCache(): Promise<void> {
try {
if (!this.workspaceID) {
this.subWikisWithTag = [];
this.tagNameToSubWiki.clear();
this.wikisWithTag = [];
this.tagNameToWiki.clear();
return;
}
const currentWorkspace = await workspace.get(this.workspaceID);
if (!currentWorkspace) {
this.subWikisWithTag = [];
this.tagNameToSubWiki.clear();
this.wikisWithTag = [];
this.tagNameToWiki.clear();
return;
}
const allWorkspaces = await workspace.getWorkspacesAsList();
const subWikisWithTag = allWorkspaces.filter((workspaceItem: IWorkspace) =>
'isSubWiki' in workspaceItem &&
workspaceItem.isSubWiki &&
workspaceItem.mainWikiID === currentWorkspace.id &&
'tagName' in workspaceItem &&
workspaceItem.tagName &&
'wikiFolderLocation' in workspaceItem &&
workspaceItem.wikiFolderLocation
) as IWikiWorkspace[];
// 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;
this.subWikisWithTag = subWikisWithTag;
// Include if it's a sub-wiki with tagName
const isSubWikiWithTag = 'isSubWiki' in workspaceItem &&
workspaceItem.isSubWiki &&
workspaceItem.mainWikiID === currentWorkspace.id &&
'tagName' in workspaceItem &&
workspaceItem.tagName &&
'wikiFolderLocation' in workspaceItem &&
workspaceItem.wikiFolderLocation;
this.tagNameToSubWiki.clear();
for (const subWiki of subWikisWithTag) {
this.tagNameToSubWiki.set(subWiki.tagName!, subWiki);
return Boolean(isMainWithTag) || Boolean(isSubWikiWithTag);
};
const workspacesWithTag = allWorkspaces.filter(isWikiWorkspaceWithTag).sort(workspaceSorter);
this.wikisWithTag = workspacesWithTag;
this.tagNameToWiki.clear();
for (const workspaceWithTag of workspacesWithTag) {
this.tagNameToWiki.set(workspaceWithTag.tagName!, workspaceWithTag);
}
} catch (error) {
this.logger.alert('filesystem: Failed to update sub-wikis cache:', error);
@ -116,6 +132,11 @@ 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
*/
async getTiddlerFileInfo(tiddler: Tiddler): Promise<IFileInfo | null> {
if (!this.boot.wikiTiddlersPath) {
@ -140,14 +161,21 @@ export class FileSystemAdaptor {
}
}
// First try direct tag match (O(1) lookup)
let matchingSubWiki: IWikiWorkspace | undefined;
for (const tag of tags) {
matchingSubWiki = this.tagNameToSubWiki.get(tag);
matchingSubWiki = this.tagNameToWiki.get(tag);
if (matchingSubWiki) {
break;
}
}
// 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);
}
if (matchingSubWiki) {
return this.generateSubWikiFileInfo(tiddler, matchingSubWiki, fileInfo);
} else {
@ -159,6 +187,28 @@ export class FileSystemAdaptor {
}
}
/**
* Find matching sub-wiki using in-tagtree-of filter for sub-wikis with includeTagTree enabled.
* Iterates through sub-wikis sorted by order (priority).
*/
protected matchTitleToWikiByTagTree(title: string): IWikiWorkspace | undefined {
for (const subWiki of this.wikisWithTag) {
if (!subWiki.includeTagTree || !subWiki.tagName) {
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]));
if (result.length > 0) {
return subWiki;
}
}
return undefined;
}
/**
* Generate file info for sub-wiki directory
* Handles symlinks correctly across platforms (Windows junctions and Linux symlinks)

View file

@ -438,8 +438,8 @@ describe('FileSystemAdaptor - Routing Logic', () => {
// Manually trigger cache update and wait for it
await adaptor['updateSubWikisCache']();
expect(adaptor['subWikisWithTag']).toEqual([]);
expect(adaptor['tagNameToSubWiki'].size).toBe(0);
expect(adaptor['wikisWithTag']).toEqual([]);
expect(adaptor['tagNameToWiki'].size).toBe(0);
});
it('should clear cache when currentWorkspace is not found', async () => {
@ -454,8 +454,8 @@ describe('FileSystemAdaptor - Routing Logic', () => {
// Manually trigger cache update and wait for it
await adaptor['updateSubWikisCache']();
expect(adaptor['subWikisWithTag']).toEqual([]);
expect(adaptor['tagNameToSubWiki'].size).toBe(0);
expect(adaptor['wikisWithTag']).toEqual([]);
expect(adaptor['tagNameToWiki'].size).toBe(0);
});
it('should handle errors in updateSubWikisCache gracefully', async () => {

View file

@ -0,0 +1,93 @@
/**
Finds out where a tiddler originates from, is it in a tag tree with xxx as root?
based on:
- https://github.com/tiddly-gittly/in-tagtree-of/
- https://github.com/bimlas/tw5-kin-filter/blob/master/plugins/kin-filter/kin.js
- https://talk.tiddlywiki.org/t/recursive-filter-operators-to-show-all-tiddlers-beneath-a-tag-and-all-tags-above-a-tiddler/3814
*/
import type { IFilterOperator, IFilterOperatorParameterOperator, SourceIterator, Tiddler } from 'tiddlywiki';
declare const exports: Record<string, IFilterOperator>;
exports['in-tagtree-of'] = function inTagTreeOfFilterOperator(
source: (iter: SourceIterator) => void,
operator: IFilterOperatorParameterOperator,
): ReturnType<IFilterOperator> {
const rootTiddler = operator.operand;
/**
* By default we check tiddler passed-in is tagged with the operand (or is its child), we output the tiddler passed-in, otherwise output empty.
* But if `isInclusive` is true, if tiddler operand itself is passed-in, we output it, even if the operand itself is not tagged with itself.
*/
const isInclusive = operator.suffix === 'inclusive';
/**
* If add `!` prefix, means output the input if input is not in rootTiddlerChildren
*/
const isNotInTagTreeOf = operator.prefix === '!';
const sourceTiddlers = new Set<string>();
let firstTiddler: Tiddler | undefined;
source((tiddler, title) => {
sourceTiddlers.add(title);
if (firstTiddler === undefined) {
firstTiddler = tiddler;
}
});
// optimize for fileSystemPath and cascade usage, where input will only be one tiddler, and often is just tagged with the rootTiddler
if (sourceTiddlers.size === 1 && !isNotInTagTreeOf) {
const [theOnlyTiddlerTitle] = sourceTiddlers;
if (firstTiddler?.fields?.tags?.includes(rootTiddler) === true) {
return [theOnlyTiddlerTitle];
}
if (isInclusive && theOnlyTiddlerTitle === rootTiddler) {
return [theOnlyTiddlerTitle];
}
}
const rootTiddlerChildren = $tw.wiki.getGlobalCache(`in-tagtree-of-${rootTiddler}`, () => {
const results = new Set<string>();
getTiddlersRecursively(rootTiddler, results);
return results;
});
if (isInclusive) {
rootTiddlerChildren.add(rootTiddler);
}
if (isNotInTagTreeOf) {
const sourceTiddlerCheckedToNotBeChildrenOfRootTiddler: string[] = [...sourceTiddlers].filter(title => !rootTiddlerChildren.has(title));
return sourceTiddlerCheckedToNotBeChildrenOfRootTiddler;
}
const sourceTiddlerCheckedToBeChildrenOfRootTiddler: string[] = [...sourceTiddlers].filter(title => rootTiddlerChildren.has(title));
return sourceTiddlerCheckedToBeChildrenOfRootTiddler;
};
function getTiddlersRecursively(title: string, results: Set<string>) {
// get tagging[] list at this level
const intermediate = new Set<string>($tw.wiki.getTiddlersWithTag(title));
// remove any TiddlersWithTag in intermediate that are already in the results set to avoid loops
// code adapted from $tw.utils.pushTop
if (intermediate.size > 0) {
if (results.size > 0) {
if (results.size < intermediate.size) {
results.forEach(alreadyExisted => {
if (intermediate.has(alreadyExisted)) {
intermediate.delete(alreadyExisted);
}
});
} else {
intermediate.forEach(alreadyExisted => {
if (results.has(alreadyExisted)) {
intermediate.delete(alreadyExisted);
}
});
}
}
// add the remaining intermediate results and traverse the hierarchy further
intermediate.forEach((title) => results.add(title));
intermediate.forEach((title) => {
getTiddlersRecursively(title, results);
});
}
}

View file

@ -0,0 +1,4 @@
creator: LinOnetwo
title: $:/plugins/linonetwo/watch-filesystem-adaptor/in-tagtree-of/index.js
type: application/javascript
module-type: filteroperator

View file

@ -39,6 +39,12 @@ export interface IStartNodeJSWikiConfigs {
readOnlyMode?: boolean;
rootTiddler?: string;
shouldUseDarkColors: boolean;
/**
* Sub-wikis to load their tiddlers into the main wiki.
* Sorted by order (lower = higher priority).
* Note: Tag-based routing is handled separately by FileSystemAdaptor.
*/
subWikis?: IWikiWorkspace[];
tiddlyWikiHost: string;
tiddlyWikiPort: number;
tokenAuth?: boolean;

View file

@ -0,0 +1,85 @@
import type { IWikiWorkspace } from '@services/workspaces/interface';
import path from 'path';
import type { TiddlyWiki } from 'tiddlywiki';
/**
* Factory function to create a custom loadWikiTiddlers function that loads sub-wiki tiddlers.
* This ensures sub-wiki tiddlers are loaded into the main wiki's $tw.boot.files
* and $tw.wiki, making them available alongside main wiki tiddlers.
*
* TiddlyWiki's includeWikis mechanism normally requires modifying tiddlywiki.info,
* but we dynamically inject sub-wikis based on workspace configuration instead.
* This wraps TiddlyWiki's original loadWikiTiddlers to dynamically inject sub-wiki tiddlers
* after the main wiki is loaded, without modifying tiddlywiki.info.
*
* @param wikiInstance - The TiddlyWiki instance
* @param homePath - Main wiki home path
* @param subWikis - Array of sub-wiki workspaces sorted by order (priority)
* @param workspaceName - Workspace name for logging
* @param nativeLogger - Logger function
*/
export function createLoadWikiTiddlersWithSubWikis(
wikiInstance: ReturnType<typeof TiddlyWiki>,
homePath: string,
subWikis: IWikiWorkspace[],
workspaceName: string,
nativeLogger: {
logFor: (name: string, level: 'info' | 'error', message: string) => Promise<void>;
},
) {
const originalLoadWikiTiddlers = wikiInstance.loadWikiTiddlers.bind(wikiInstance);
return function loadWikiTiddlersWithSubWikis(
wikiPath: string,
options?: { parentPaths?: string[]; readOnly?: boolean },
) {
// Call original function first to load main wiki
const wikiInfo = originalLoadWikiTiddlers(wikiPath, options);
// defensive check
if (wikiPath === homePath && wikiInfo && subWikis.length > 0) {
return;
}
for (const subWiki of subWikis) {
const subWikiTiddlersPath = path.resolve(
subWiki.wikiFolderLocation,
wikiInstance.config.wikiTiddlersSubDir,
);
try {
// Load tiddlers from sub-wiki directory
const tiddlerFiles = wikiInstance.loadTiddlersFromPath(subWikiTiddlersPath);
for (const tiddlerFile of tiddlerFiles) {
// Register file info for filesystem adaptor (so tiddlers save back to correct location)
if (tiddlerFile.filepath) {
for (const tiddler of tiddlerFile.tiddlers) {
wikiInstance.boot.files[tiddler.title] = {
filepath: tiddlerFile.filepath,
type: tiddlerFile.type ?? 'application/x-tiddler',
hasMetaFile: tiddlerFile.hasMetaFile ?? false,
isEditableFile: tiddlerFile.isEditableFile ?? true,
};
}
}
// Add tiddlers to wiki
wikiInstance.wiki.addTiddlers(tiddlerFile.tiddlers);
}
void nativeLogger.logFor(
workspaceName,
'info',
`Loaded sub-wiki tiddlers from: ${subWikiTiddlersPath}`,
);
} catch (error) {
void nativeLogger.logFor(
workspaceName,
'error',
`Failed to load sub-wiki tiddlers from ${subWikiTiddlersPath}: ${(error as Error).message}`,
);
}
}
return wikiInfo;
};
}

View file

@ -18,6 +18,7 @@ import { wikiOperationsInWikiWorker } from '../wikiOperations/executor/wikiOpera
import type { IStartNodeJSWikiConfigs } from '../wikiWorker';
import { setWikiInstance } from './globals';
import { ipcServerRoutes } from './ipcServerRoutes';
import { createLoadWikiTiddlersWithSubWikis } from './loadWikiTiddlersWithSubWikis';
import { authTokenIsProvided } from './wikiWorkerUtilities';
export function startNodeJSWiki({
@ -32,6 +33,7 @@ export function startNodeJSWiki({
readOnlyMode,
rootTiddler = '$:/core/save/all',
shouldUseDarkColors,
subWikis = [],
tiddlyWikiHost = defaultServerIP,
tiddlyWikiPort = 5112,
tokenAuth,
@ -102,6 +104,20 @@ export function startNodeJSWiki({
setWikiInstance(wikiInstance);
process.env.TIDDLYWIKI_PLUGIN_PATH = path.resolve(homePath, 'plugins');
process.env.TIDDLYWIKI_THEME_PATH = path.resolve(homePath, 'themes');
/**
* Hook loadWikiTiddlers to inject sub-wiki tiddlers after main wiki is loaded.
*/
if (subWikis.length > 0) {
wikiInstance.loadWikiTiddlers = createLoadWikiTiddlersWithSubWikis(
wikiInstance,
homePath,
subWikis,
workspace.name,
native,
);
}
// don't add `+` prefix to plugin name here. `+` only used in args[0], but we are not prepend this list to the args list.
wikiInstance.boot.extraPlugins = [
// add tiddly filesystem back if is not readonly https://github.com/Jermolene/TiddlyWiki5/issues/4484#issuecomment-596779416

View file

@ -156,6 +156,7 @@ export class WikiGitWorkspace implements IWikiGitWorkspaceService {
excludedPlugins: [],
enableHTTPAPI: false,
enableFileSystemWatch: true,
includeTagTree: false,
lastNodeJSArgv: [],
homeUrl: '',
gitUrl: null,

View file

@ -24,7 +24,16 @@ import type { IViewService } from '@services/view/interface';
import type { IWikiService } from '@services/wiki/interface';
import { WindowNames } from '@services/windows/WindowProperties';
import type { IWorkspaceViewService } from '@services/workspacesView/interface';
import type { IDedicatedWorkspace, INewWikiWorkspaceConfig, IWorkspace, IWorkspaceMetaData, IWorkspaceService, IWorkspacesWithMetadata, IWorkspaceWithMetadata } from './interface';
import type {
IDedicatedWorkspace,
INewWikiWorkspaceConfig,
IWikiWorkspace,
IWorkspace,
IWorkspaceMetaData,
IWorkspaceService,
IWorkspacesWithMetadata,
IWorkspaceWithMetadata,
} from './interface';
import { isWikiWorkspace } from './interface';
import { registerMenu } from './registerMenu';
import { workspaceSorter } from './utilities';
@ -136,18 +145,18 @@ export class Workspace implements IWorkspaceService {
return Object.values(this.getWorkspacesSync()).sort(workspaceSorter);
}
public async getSubWorkspacesAsList(workspaceID: string): Promise<IWorkspace[]> {
public async getSubWorkspacesAsList(workspaceID: string): Promise<IWikiWorkspace[]> {
const workspace = this.getSync(workspaceID);
if (workspace === undefined || !isWikiWorkspace(workspace)) return [];
if (workspace.isSubWiki) return [];
return this.getWorkspacesAsListSync().filter((w) => isWikiWorkspace(w) && w.mainWikiID === workspaceID).sort(workspaceSorter);
return this.getWorkspacesAsListSync().filter((w): w is IWikiWorkspace => isWikiWorkspace(w) && w.mainWikiID === workspaceID).sort(workspaceSorter);
}
public getSubWorkspacesAsListSync(workspaceID: string): IWorkspace[] {
public getSubWorkspacesAsListSync(workspaceID: string): IWikiWorkspace[] {
const workspace = this.getSync(workspaceID);
if (workspace === undefined || !isWikiWorkspace(workspace)) return [];
if (workspace.isSubWiki) return [];
return this.getWorkspacesAsListSync().filter((w) => isWikiWorkspace(w) && w.mainWikiID === workspaceID).sort(workspaceSorter);
return this.getWorkspacesAsListSync().filter((w): w is IWikiWorkspace => isWikiWorkspace(w) && w.mainWikiID === workspaceID).sort(workspaceSorter);
}
public async get(id: string): Promise<IWorkspace | undefined> {
@ -225,6 +234,7 @@ export class Workspace implements IWorkspaceService {
backupOnInterval: true,
excludedPlugins: [],
enableHTTPAPI: false,
includeTagTree: false,
};
const fixingValues: Partial<typeof workspaceToSanitize> = {};
// we add mainWikiID in creation, we fix this value for old existed workspaces
@ -289,7 +299,7 @@ export class Workspace implements IWorkspaceService {
public async getByWikiName(wikiName: string): Promise<IWorkspace | undefined> {
return (await this.getWorkspacesAsList())
.sort((a, b) => a.order - b.order)
.sort(workspaceSorter)
.find((workspace) => workspace.name === wikiName);
}

View file

@ -139,6 +139,12 @@ export interface IWikiWorkspace extends IDedicatedWorkspace {
* Tag name in tiddlywiki's filesystemPath, tiddler with this tag will be save into this subwiki
*/
tagName: string | null;
/**
* When enabled, tiddlers that are indirectly tagged (tag of tag of tag...) with this sub-wiki's tagName
* 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;
/**
* Use authenticated-user-header to provide `TIDGI_AUTH_TOKEN_HEADER` as header key to receive a value as username (we use it as token)
*/
@ -240,11 +246,11 @@ export interface IWorkspaceService {
getMetaData: (id: string) => Promise<Partial<IWorkspaceMetaData>>;
getNextWorkspace: (id: string) => Promise<IWorkspace | undefined>;
getPreviousWorkspace: (id: string) => Promise<IWorkspace | undefined>;
getSubWorkspacesAsList(workspaceID: string): Promise<IWorkspace[]>;
getSubWorkspacesAsList(workspaceID: string): Promise<IWikiWorkspace[]>;
/**
* Only meant to be used in TidGi's services internally.
*/
getSubWorkspacesAsListSync(workspaceID: string): IWorkspace[];
getSubWorkspacesAsListSync(workspaceID: string): IWikiWorkspace[];
getWorkspaces(): Promise<Record<string, IWorkspace>>;
getWorkspacesAsList(): Promise<IWorkspace[]>;
getWorkspacesWithMetadata(): IWorkspacesWithMetadata;

View file

@ -22,6 +22,7 @@ import { isWikiWorkspace } from '@services/workspaces/interface';
import { DELAY_MENU_REGISTER } from '@/constants/parameters';
import type { ISyncService } from '@services/sync/interface';
import { workspaceSorter } from '@services/workspaces/utilities';
import type { IInitializeWorkspaceOptions, IWorkspaceViewService } from './interface';
import { registerMenu } from './registerMenu';
import { getTidgiMiniWindowTargetWorkspace } from './utilities';
@ -49,9 +50,8 @@ export class WorkspaceView implements IWorkspaceViewService {
workspacesList.filter((workspace) => isWikiWorkspace(workspace) && !workspace.isSubWiki && !workspace.pageType).forEach((workspace) => {
wikiService.setWikiStartLockOn(workspace.id);
});
// sorting (-1 will make a in the front, b in the back)
const sortedList = workspacesList
.sort((a, b) => a.order - b.order) // sort by order, 1-2<0, so first will be the first
.sort(workspaceSorter)
.sort((a, b) => (a.active && !b.active ? -1 : 0)) // put active wiki first
.sort((a, b) => (isWikiWorkspace(a) && a.isSubWiki && (!isWikiWorkspace(b) || !b.isSubWiki) ? -1 : 0)); // put subwiki on top, they can't restart wiki, so need to sync them first, then let main wiki restart the wiki // revert this after tw can reload tid from fs
await mapSeries(sortedList, async (workspace) => {

View file

@ -63,7 +63,7 @@ export function CloneWikiForm({ form, isCreateMainWorkspace, errorInWhichCompone
label={t('AddWorkspace.MainWorkspaceLocation')}
helperText={form.mainWikiToLink.wikiFolderLocation &&
`${t('AddWorkspace.SubWorkspaceWillLinkTo')}
${form.mainWikiToLink.wikiFolderLocation}/tiddlers/${form.wikiFolderName}`}
${form.mainWikiToLink.wikiFolderLocation}`}
value={form.mainWikiToLinkIndex}
onChange={(event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const index = Number(event.target.value);

View file

@ -112,7 +112,7 @@ export function ExistedWikiForm({
label={t('AddWorkspace.MainWorkspaceLocation')}
helperText={mainWikiToLink.wikiFolderLocation &&
`${t('AddWorkspace.SubWorkspaceWillLinkTo')}
${mainWikiToLink.wikiFolderLocation}/tiddlers/${wikiFolderName}`}
${mainWikiToLink.wikiFolderLocation}`}
value={mainWikiToLinkIndex}
onChange={(event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const index = Number(event.target.value);

View file

@ -68,7 +68,7 @@ export function NewWikiForm({
label={t('AddWorkspace.MainWorkspaceLocation')}
helperText={form.mainWikiToLink.wikiFolderLocation &&
`${t('AddWorkspace.SubWorkspaceWillLinkTo')}
${form.mainWikiToLink.wikiFolderLocation}/tiddlers/subwiki/${form.wikiFolderName}`}
${form.mainWikiToLink.wikiFolderLocation}`}
value={form.mainWikiToLinkIndex}
slotProps={{ htmlInput: { 'data-testid': 'main-wiki-select' } }}
onChange={(event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {

View file

@ -173,6 +173,7 @@ export function workspaceConfigFromForm(form: INewWikiRequiredFormData, isCreate
excludedPlugins: [],
enableHTTPAPI: false,
enableFileSystemWatch: true,
includeTagTree: false,
lastNodeJSArgv: [],
};
}

View file

@ -168,6 +168,7 @@ export default function EditWorkspace(): React.JSX.Element {
const syncOnInterval = isWiki ? workspace.syncOnInterval : false;
const syncOnStartup = isWiki ? workspace.syncOnStartup : false;
const tagName = isWiki ? workspace.tagName : null;
const includeTagTree = isWiki ? workspace.includeTagTree : false;
const transparentBackground = isWiki ? workspace.transparentBackground : false;
const userName = isWiki ? workspace.userName : '';
const lastUrl = isWiki ? workspace.lastUrl : null;
@ -177,6 +178,16 @@ export default function EditWorkspace(): React.JSX.Element {
// Fetch all tags from main wiki for autocomplete suggestions
const availableTags = useAvailableTags(mainWikiToLink ?? undefined, isSubWiki);
// Check if there are sub-workspaces for this main workspace
const hasSubWorkspaces = usePromiseValue(async () => {
if (isSubWiki) return false;
const subWorkspaces = await window.service.workspace.getSubWorkspacesAsList(workspaceID);
return subWorkspaces.length > 0;
}, false);
// Show sub-workspace routing options for sub-wikis, or for main wikis that have sub-workspaces
const showSubWorkspaceRouting = isSubWiki || hasSubWorkspaces;
const rememberLastPageVisited = usePromiseValue(async () => await window.service.preference.get('rememberLastPageVisited'));
if (workspaceID === undefined) {
return <Root>Error {workspaceID ?? '-'} not exists</Root>;
@ -309,31 +320,19 @@ export default function EditWorkspace(): React.JSX.Element {
disabled
/>
)}
<TextField
helperText={t('AddWorkspace.WorkspaceUserNameDetail')}
fullWidth
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
workspaceSetter({ ...workspace, userName: event.target.value }, true);
}}
label={t('AddWorkspace.WorkspaceUserName')}
placeholder={fallbackUserName}
value={userName}
/>
<Divider />
{isSubWiki && (
<Autocomplete
freeSolo
options={availableTags}
value={tagName}
onInputChange={(_event: React.SyntheticEvent, value: string) => {
void _event;
workspaceSetter({ ...workspace, tagName: value }, true);
{!isSubWiki && (
<TextField
helperText={t('AddWorkspace.WorkspaceUserNameDetail')}
fullWidth
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
workspaceSetter({ ...workspace, userName: event.target.value }, true);
}}
renderInput={(parameters: AutocompleteRenderInputParams) => (
<TextField {...parameters} label={t('AddWorkspace.TagName')} helperText={t('AddWorkspace.TagNameHelp')} />
)}
label={t('AddWorkspace.WorkspaceUserName')}
placeholder={fallbackUserName}
value={userName}
/>
)}
<Divider />
<SyncedWikiDescription
isCreateSyncedWorkspace={isCreateSyncedWorkspace}
isCreateSyncedWorkspaceSetter={(isSynced: boolean) => {
@ -418,6 +417,56 @@ export default function EditWorkspace(): React.JSX.Element {
)}
</AccordionDetails>
</OptionsAccordion>
{showSubWorkspaceRouting && (
<OptionsAccordion defaultExpanded={isSubWiki}>
<Tooltip title={t('EditWorkspace.ClickToExpand')}>
<OptionsAccordionSummary expandIcon={<ExpandMoreIcon />} data-testid='preference-section-subWorkspaceOptions'>
{t('AddWorkspace.SubWorkspaceOptions')}
</OptionsAccordionSummary>
</Tooltip>
<AccordionDetails>
<Typography variant='body2' color='textSecondary' sx={{ mb: 2 }}>
{isSubWiki ? t('AddWorkspace.SubWorkspaceOptionsDescriptionForSub') : t('AddWorkspace.SubWorkspaceOptionsDescriptionForMain')}
</Typography>
<Autocomplete
freeSolo
options={availableTags}
value={tagName}
onInputChange={(_event: React.SyntheticEvent, value: string) => {
void _event;
workspaceSetter({ ...workspace, tagName: value }, true);
}}
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>
</OptionsAccordion>
)}
<OptionsAccordion>
<Tooltip title={t('EditWorkspace.ClickToExpand')}>
<OptionsAccordionSummary expandIcon={<ExpandMoreIcon />} data-testid='preference-section-miscOptions'>