mirror of
https://github.com/tiddly-gittly/TidGi-Desktop.git
synced 2025-12-06 02:30:47 -08:00
Chore/upgrade2 (#656)
* upgrade rjsf * Always enable devTools, even in test mode for debugging * Update index.tsx * lint * fix: properly log error to log file * fix: precalculate json plugin route and exclude it even before save * Update WatchFileSystemAdaptor.ts * fix: hooks.addHook usage wrong * fix: tw5-typed * fix: outdated rjsf usage * refactor: cleanup fs watch logs * v0.13.0-rc5 * fix: lint * fix: unused * fix: IFileInfo type * fix: type * fix: failed rjsf test
This commit is contained in:
parent
9a98c7bc47
commit
256e0fcb65
29 changed files with 1896 additions and 1691 deletions
|
|
@ -22,7 +22,6 @@ and run `node node_modules/electron/install.js` manually.
|
||||||
Or `[FAILED: ENOENT: no such file or directory, stat 'C:\Users\linonetwo\Documents\repo-c\TidGi-Desktop\node_modules\.pnpm\node_modules\@radix-ui\react-compose-refs']`
|
Or `[FAILED: ENOENT: no such file or directory, stat 'C:\Users\linonetwo\Documents\repo-c\TidGi-Desktop\node_modules\.pnpm\node_modules\@radix-ui\react-compose-refs']`
|
||||||
|
|
||||||
Remove it by run `rm 'xxx/node_modules/.pnpm/node_modules/@types/lodash-es'` fixes it. Maybe pnpm install gets interrupted, and make a file-like symlink, get recognized as binary file. Remove it will work.
|
Remove it by run `rm 'xxx/node_modules/.pnpm/node_modules/@types/lodash-es'` fixes it. Maybe pnpm install gets interrupted, and make a file-like symlink, get recognized as binary file. Remove it will work.
|
||||||
|
|
||||||
## An unhandled rejection has occurred inside Forge about node-abi
|
## An unhandled rejection has occurred inside Forge about node-abi
|
||||||
|
|
||||||
Solution: Update `@electron/rebuild` to latest version:
|
Solution: Update `@electron/rebuild` to latest version:
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,8 @@ Feature: Create New Agent Workflow
|
||||||
When I type "我的代码助手" in "agent name input" element with selector "[data-testid='agent-name-input-field']"
|
When I type "我的代码助手" in "agent name input" element with selector "[data-testid='agent-name-input-field']"
|
||||||
# Advance to step 2 (Edit Prompt)
|
# Advance to step 2 (Edit Prompt)
|
||||||
When I click on a "next button" element with selector "[data-testid='next-button']"
|
When I click on a "next button" element with selector "[data-testid='next-button']"
|
||||||
# Step 4: Verify second step content (Edit Prompt)
|
# Step 4: Verify second step content (Edit Prompt) - using text selector as a diagnostic
|
||||||
And I should see a "edit prompt title" element with selector "*:has-text('编辑提示词')"
|
And I should see a "edit prompt title" element with selector "h6:has-text('编辑提示词')"
|
||||||
# Step 4.1: Wait for PromptConfigForm to load
|
# Step 4.1: Wait for PromptConfigForm to load
|
||||||
# Verify the PromptConfigForm is present with our new test id
|
# Verify the PromptConfigForm is present with our new test id
|
||||||
And I should see a "prompt config form" element with selector "[data-testid='prompt-config-form']"
|
And I should see a "prompt config form" element with selector "[data-testid='prompt-config-form']"
|
||||||
|
|
@ -51,9 +51,9 @@ Feature: Create New Agent Workflow
|
||||||
| [data-testid='prompt-config-form'] [role='tabpanel']:not([hidden]) button[title*='展开'], [data-testid='prompt-config-form'] [role='tabpanel']:not([hidden]) button svg[data-testid='ExpandMoreIcon'] |
|
| [data-testid='prompt-config-form'] [role='tabpanel']:not([hidden]) button[title*='展开'], [data-testid='prompt-config-form'] [role='tabpanel']:not([hidden]) button svg[data-testid='ExpandMoreIcon'] |
|
||||||
| [data-testid='prompt-config-form'] [role='tabpanel']:not([hidden]) textarea[id*='_text']:not([readonly]) |
|
| [data-testid='prompt-config-form'] [role='tabpanel']:not([hidden]) textarea[id*='_text']:not([readonly]) |
|
||||||
When I clear text in "system prompt text field" element with selector "[data-testid='prompt-config-form'] [role='tabpanel']:not([hidden]) textarea[id*='_text']:not([readonly])"
|
When I clear text in "system prompt text field" element with selector "[data-testid='prompt-config-form'] [role='tabpanel']:not([hidden]) textarea[id*='_text']:not([readonly])"
|
||||||
|
And I wait for 0.1 seconds for "clear to complete and DOM to stabilize"
|
||||||
When I type "你是一个专业的代码助手,请用中文回答编程问题。" in "system prompt text field" element with selector "[data-testid='prompt-config-form'] [role='tabpanel']:not([hidden]) textarea[id*='_text']:not([readonly])"
|
When I type "你是一个专业的代码助手,请用中文回答编程问题。" in "system prompt text field" element with selector "[data-testid='prompt-config-form'] [role='tabpanel']:not([hidden]) textarea[id*='_text']:not([readonly])"
|
||||||
# Wait for form content save to backend
|
And I wait for 0.2 seconds for "form content save to backend"
|
||||||
And I wait for 0.2 seconds
|
|
||||||
# Step 5: Advance to step 3 (Immediate Use)
|
# Step 5: Advance to step 3 (Immediate Use)
|
||||||
When I click on a "next button" element with selector "[data-testid='next-button']"
|
When I click on a "next button" element with selector "[data-testid='next-button']"
|
||||||
# Step 6: Verify third step content (Immediate Use with chat interface)
|
# Step 6: Verify third step content (Immediate Use with chat interface)
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ When('I wait for the page to load completely', async function(this: ApplicationW
|
||||||
|
|
||||||
Then('I should see a(n) {string} element with selector {string}', async function(this: ApplicationWorld, elementComment: string, selector: string) {
|
Then('I should see a(n) {string} element with selector {string}', async function(this: ApplicationWorld, elementComment: string, selector: string) {
|
||||||
const currentWindow = this.currentWindow;
|
const currentWindow = this.currentWindow;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await currentWindow?.waitForSelector(selector, { timeout: 10000 });
|
await currentWindow?.waitForSelector(selector, { timeout: 10000 });
|
||||||
const isVisible = await currentWindow?.isVisible(selector);
|
const isVisible = await currentWindow?.isVisible(selector);
|
||||||
|
|
|
||||||
82
package.json
82
package.json
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "tidgi",
|
"name": "tidgi",
|
||||||
"productName": "TidGi",
|
"productName": "TidGi",
|
||||||
"description": "Customizable personal knowledge-base with Github as unlimited storage and blogging platform.",
|
"description": "Customizable personal knowledge-base with Github as unlimited storage and blogging platform.",
|
||||||
"version": "0.13.0-prerelease4",
|
"version": "0.13.0-rc5",
|
||||||
"license": "MPL 2.0",
|
"license": "MPL 2.0",
|
||||||
"packageManager": "pnpm@10.18.2",
|
"packageManager": "pnpm@10.18.2",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
@ -33,10 +33,10 @@
|
||||||
"author": "Lin Onetwo <linonetwo012@gmail.com>, Quang Lam <quang.lam2807@gmail.com>",
|
"author": "Lin Onetwo <linonetwo012@gmail.com>, Quang Lam <quang.lam2807@gmail.com>",
|
||||||
"main": ".vite/build/main.js",
|
"main": ".vite/build/main.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "^2.0.35",
|
"@ai-sdk/anthropic": "^2.0.45",
|
||||||
"@ai-sdk/deepseek": "^1.0.23",
|
"@ai-sdk/deepseek": "^1.0.29",
|
||||||
"@ai-sdk/openai": "^2.0.53",
|
"@ai-sdk/openai": "^2.0.71",
|
||||||
"@ai-sdk/openai-compatible": "^1.0.22",
|
"@ai-sdk/openai-compatible": "^1.0.27",
|
||||||
"@algolia/autocomplete-js": "^1.19.4",
|
"@algolia/autocomplete-js": "^1.19.4",
|
||||||
"@algolia/autocomplete-theme-classic": "^1.19.4",
|
"@algolia/autocomplete-theme-classic": "^1.19.4",
|
||||||
"@dnd-kit/core": "6.3.1",
|
"@dnd-kit/core": "6.3.1",
|
||||||
|
|
@ -46,75 +46,75 @@
|
||||||
"@dr.pogodin/react-helmet": "^3.0.4",
|
"@dr.pogodin/react-helmet": "^3.0.4",
|
||||||
"@fontsource/roboto": "^5.2.8",
|
"@fontsource/roboto": "^5.2.8",
|
||||||
"@monaco-editor/react": "^4.7.0",
|
"@monaco-editor/react": "^4.7.0",
|
||||||
"@mui/icons-material": "^7.3.4",
|
"@mui/icons-material": "^7.3.5",
|
||||||
"@mui/material": "^7.3.4",
|
"@mui/material": "^7.3.5",
|
||||||
"@mui/system": "^7.3.3",
|
"@mui/system": "^7.3.5",
|
||||||
"@mui/types": "^7.4.7",
|
"@mui/types": "^7.4.8",
|
||||||
"@mui/x-date-pickers": "^8.14.1",
|
"@mui/x-date-pickers": "^8.19.0",
|
||||||
"@rjsf/core": "6.0.0-beta.8",
|
"@rjsf/core": "6.1.2",
|
||||||
"@rjsf/mui": "6.0.0-beta.10",
|
"@rjsf/mui": "6.1.2",
|
||||||
"@rjsf/utils": "6.0.0-beta.10",
|
"@rjsf/utils": "6.1.2",
|
||||||
"@rjsf/validator-ajv8": "6.0.0-beta.8",
|
"@rjsf/validator-ajv8": "6.1.2",
|
||||||
"@tomplum/react-git-log": "^3.5.0",
|
"@tomplum/react-git-log": "^3.5.0",
|
||||||
"ai": "^5.0.76",
|
"ai": "^5.0.98",
|
||||||
"ansi-to-html": "^0.7.2",
|
"ansi-to-html": "^0.7.2",
|
||||||
"app-path": "^4.0.0",
|
"app-path": "^4.0.0",
|
||||||
"beautiful-react-hooks": "5.0.3",
|
"beautiful-react-hooks": "5.0.3",
|
||||||
"best-effort-json-parser": "1.2.1",
|
"best-effort-json-parser": "1.2.1",
|
||||||
"better-sqlite3": "^12.4.1",
|
"better-sqlite3": "^12.4.5",
|
||||||
"bluebird": "3.7.2",
|
"bluebird": "3.7.2",
|
||||||
"date-fns": "3.6.0",
|
"date-fns": "3.6.0",
|
||||||
"default-gateway": "6.0.3",
|
"default-gateway": "6.0.3",
|
||||||
"dugite": "3.0.0-rc12",
|
"dugite": "3.0.0",
|
||||||
"electron-dl": "^4.0.0",
|
"electron-dl": "^4.0.0",
|
||||||
"electron-ipc-cat": "2.2.3",
|
"electron-ipc-cat": "2.2.3",
|
||||||
"electron-settings": "5.0.0",
|
"electron-settings": "5.0.0",
|
||||||
"electron-unhandled": "4.0.1",
|
"electron-unhandled": "4.0.1",
|
||||||
"electron-window-state": "5.0.3",
|
"electron-window-state": "5.0.3",
|
||||||
"espree": "^10.4.0",
|
"espree": "^11.0.0",
|
||||||
"exponential-backoff": "^3.1.3",
|
"exponential-backoff": "^3.1.3",
|
||||||
"fs-extra": "11.3.2",
|
"fs-extra": "11.3.2",
|
||||||
"git-sync-js": "^2.2.1",
|
"git-sync-js": "^2.3.0",
|
||||||
"graphql-hooks": "8.2.0",
|
"graphql-hooks": "8.2.0",
|
||||||
"html-minifier-terser": "^7.2.0",
|
"html-minifier-terser": "^7.2.0",
|
||||||
"i18next": "25.6.0",
|
"i18next": "25.6.3",
|
||||||
"i18next-electron-fs-backend": "3.0.3",
|
"i18next-electron-fs-backend": "3.0.3",
|
||||||
"i18next-fs-backend": "2.6.0",
|
"i18next-fs-backend": "2.6.1",
|
||||||
"immer": "^10.1.3",
|
"immer": "^10.2.0",
|
||||||
"intercept-stdout": "0.1.2",
|
"intercept-stdout": "0.1.2",
|
||||||
"inversify": "7.10.3",
|
"inversify": "7.10.4",
|
||||||
"ipaddr.js": "2.2.0",
|
"ipaddr.js": "2.2.0",
|
||||||
"jimp": "1.6.0",
|
"jimp": "1.6.0",
|
||||||
"json5": "^2.2.3",
|
"json5": "^2.2.3",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"material-ui-popup-state": "^5.3.6",
|
"material-ui-popup-state": "^5.3.6",
|
||||||
"menubar": "9.5.2",
|
"menubar": "9.5.2",
|
||||||
"monaco-editor": "^0.54.0",
|
"monaco-editor": "^0.55.1",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
"new-github-issue-url": "^1.1.0",
|
"new-github-issue-url": "^1.1.0",
|
||||||
"node-fetch": "3.3.2",
|
"node-fetch": "3.3.2",
|
||||||
"nsfw": "^2.2.5",
|
"nsfw": "^2.2.5",
|
||||||
"oidc-client-ts": "^3.3.0",
|
"oidc-client-ts": "^3.4.1",
|
||||||
"ollama-ai-provider-v2": "^1.5.1",
|
"ollama-ai-provider-v2": "^1.5.5",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
"react-i18next": "16.1.3",
|
"react-i18next": "16.3.5",
|
||||||
"react-masonry-css": "^1.0.16",
|
"react-masonry-css": "^1.0.16",
|
||||||
"react-window": "^2.2.1",
|
"react-window": "^2.2.3",
|
||||||
"reflect-metadata": "0.2.2",
|
"reflect-metadata": "0.2.2",
|
||||||
"registry-js": "1.16.1",
|
"registry-js": "1.16.1",
|
||||||
"rotating-file-stream": "^3.2.7",
|
"rotating-file-stream": "^3.2.7",
|
||||||
"rxjs": "7.8.2",
|
"rxjs": "7.8.2",
|
||||||
"semver": "7.7.3",
|
"semver": "7.7.3",
|
||||||
"serialize-error": "^12.0.0",
|
"serialize-error": "^12.0.0",
|
||||||
"simplebar": "6.3.2",
|
"simplebar": "6.3.3",
|
||||||
"simplebar-react": "3.3.2",
|
"simplebar-react": "3.3.2",
|
||||||
"source-map-support": "0.5.21",
|
"source-map-support": "0.5.21",
|
||||||
"sqlite-vec": "0.1.7-alpha.2",
|
"sqlite-vec": "0.1.7-alpha.2",
|
||||||
"strip-ansi": "^7.1.2",
|
"strip-ansi": "^7.1.2",
|
||||||
"tapable": "^2.3.0",
|
"tapable": "^2.3.0",
|
||||||
"tiddlywiki": "5.3.8",
|
"tiddlywiki": "5.3.8",
|
||||||
"type-fest": "5.1.0",
|
"type-fest": "5.2.0",
|
||||||
"typeorm": "^0.3.27",
|
"typeorm": "^0.3.27",
|
||||||
"typescript-styled-is": "^2.1.0",
|
"typescript-styled-is": "^2.1.0",
|
||||||
"v8-compile-cache-lib": "^3.0.1",
|
"v8-compile-cache-lib": "^3.0.1",
|
||||||
|
|
@ -153,37 +153,37 @@
|
||||||
"@types/html-minifier-terser": "^7.0.2",
|
"@types/html-minifier-terser": "^7.0.2",
|
||||||
"@types/intercept-stdout": "0.1.4",
|
"@types/intercept-stdout": "0.1.4",
|
||||||
"@types/lodash": "4.17.20",
|
"@types/lodash": "4.17.20",
|
||||||
"@types/node": "24.9.1",
|
"@types/node": "24.10.1",
|
||||||
"@types/react": "19.2.2",
|
"@types/react": "19.2.6",
|
||||||
"@types/react-dom": "19.2.2",
|
"@types/react-dom": "19.2.3",
|
||||||
"@types/react-jsonschema-form": "^1.7.13",
|
"@types/react-jsonschema-form": "^1.7.13",
|
||||||
"@types/semver": "7.7.1",
|
"@types/semver": "7.7.1",
|
||||||
"@types/source-map-support": "0.5.10",
|
"@types/source-map-support": "0.5.10",
|
||||||
"@vitejs/plugin-react": "^5.0.4",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
"@vitest/coverage-v8": "^3.2.4",
|
"@vitest/coverage-v8": "^3.2.4",
|
||||||
"@vitest/ui": "^3.2.4",
|
"@vitest/ui": "^3.2.4",
|
||||||
"chai": "6.2.0",
|
"chai": "6.2.0",
|
||||||
"cross-env": "10.1.0",
|
"cross-env": "10.1.0",
|
||||||
"dprint": "^0.50.2",
|
"dprint": "^0.50.2",
|
||||||
"electron": "38.3.0",
|
"electron": "39.2.3",
|
||||||
"electron-chrome-web-store": "^0.13.0",
|
"electron-chrome-web-store": "^0.13.0",
|
||||||
"esbuild": "^0.25.11",
|
"esbuild": "^0.27.0",
|
||||||
"eslint-config-tidgi": "^2.2.0",
|
"eslint-config-tidgi": "^2.2.0",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"jsdom": "^27.0.1",
|
"jsdom": "^27.2.0",
|
||||||
"memory-fs": "^0.5.0",
|
"memory-fs": "^0.5.0",
|
||||||
"node-loader": "2.1.0",
|
"node-loader": "2.1.0",
|
||||||
"oauth2-mock-server": "^8.1.0",
|
"oauth2-mock-server": "^8.2.0",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"playwright": "^1.56.1",
|
"playwright": "^1.56.1",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.1.2",
|
||||||
"ts-node": "10.9.2",
|
"ts-node": "10.9.2",
|
||||||
"tsx": "^4.20.6",
|
"tsx": "^4.20.6",
|
||||||
"tw5-typed": "^0.6.8",
|
"tw5-typed": "^1.0.5",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"typesync": "0.14.3",
|
"typesync": "0.14.3",
|
||||||
"unplugin-swc": "^1.5.8",
|
"unplugin-swc": "^1.5.8",
|
||||||
"vite": "^7.1.11",
|
"vite": "^7.2.4",
|
||||||
"vite-bundle-analyzer": "^1.2.3",
|
"vite-bundle-analyzer": "^1.2.3",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
2710
pnpm-lock.yaml
generated
2710
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
60
src/components/ErrorBoundary.tsx
Normal file
60
src/components/ErrorBoundary.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode;
|
||||||
|
fallback?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
hasError: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ErrorBoundary extends Component<Props, State> {
|
||||||
|
public state: State = {
|
||||||
|
hasError: false,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static getDerivedStateFromError(error: Error): State {
|
||||||
|
return { hasError: true, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||||
|
// Log error to console and backend
|
||||||
|
console.error('Uncaught error:', error, errorInfo);
|
||||||
|
|
||||||
|
// Forward to backend log
|
||||||
|
if (window.service?.native?.log) {
|
||||||
|
void window.service.native.log('error', `React Error Boundary caught: ${error.message}`, {
|
||||||
|
error: {
|
||||||
|
name: error.name,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
},
|
||||||
|
componentStack: errorInfo.componentStack,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
if (this.props.fallback) {
|
||||||
|
return this.props.fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '20px', color: 'red' }}>
|
||||||
|
<h2>Something went wrong.</h2>
|
||||||
|
<details style={{ whiteSpace: 'pre-wrap' }}>
|
||||||
|
{this.state.error?.toString()}
|
||||||
|
<br />
|
||||||
|
{this.state.error?.stack}
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -162,10 +162,13 @@ export const CreateNewAgentContent: React.FC<CreateNewAgentContentProps> = ({ ta
|
||||||
}
|
}
|
||||||
}, [temporaryAgentDefinition, saveToBackendDebounced]);
|
}, [temporaryAgentDefinition, saveToBackendDebounced]);
|
||||||
|
|
||||||
// Update current step when tab changes
|
// Only initialize from tab on mount, don't sync back
|
||||||
|
// The component is the source of truth for currentStep during its lifecycle
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentStep(tab.currentStep ?? 0);
|
if (tab.currentStep !== undefined) {
|
||||||
}, [tab.currentStep]);
|
setCurrentStep(tab.currentStep);
|
||||||
|
}
|
||||||
|
}, []); // Only run once on mount
|
||||||
|
|
||||||
// Cleanup when component unmounts or tab closes
|
// Cleanup when component unmounts or tab closes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -359,26 +362,26 @@ export const CreateNewAgentContent: React.FC<CreateNewAgentContentProps> = ({ ta
|
||||||
case 'editPrompt':
|
case 'editPrompt':
|
||||||
return (
|
return (
|
||||||
<StepContainer>
|
<StepContainer>
|
||||||
<Typography variant='h6' gutterBottom>
|
<Typography variant='h6' gutterBottom data-testid='edit-prompt-title'>
|
||||||
{t('CreateAgent.EditPrompt')}
|
{t('CreateAgent.EditPrompt')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant='body2' color='text.secondary' gutterBottom>
|
<Typography variant='body2' color='text.secondary' gutterBottom>
|
||||||
{t('CreateAgent.EditPromptDescription')}
|
{t('CreateAgent.EditPromptDescription')}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{temporaryAgentDefinition
|
{temporaryAgentDefinition && promptSchema
|
||||||
? (
|
? (
|
||||||
<Box sx={{ mt: 2, height: 400, overflow: 'auto' }}>
|
<Box sx={{ mt: 2, height: 400, overflow: 'auto' }}>
|
||||||
<PromptConfigForm
|
<PromptConfigForm
|
||||||
schema={promptSchema || undefined}
|
schema={promptSchema}
|
||||||
formData={temporaryAgentDefinition.handlerConfig as HandlerConfig}
|
formData={(temporaryAgentDefinition.handlerConfig || {}) as HandlerConfig}
|
||||||
onChange={(updatedConfig) => {
|
onChange={(updatedConfig) => {
|
||||||
void handleAgentDefinitionChange({
|
void handleAgentDefinitionChange({
|
||||||
...temporaryAgentDefinition,
|
...temporaryAgentDefinition,
|
||||||
handlerConfig: updatedConfig as Record<string, unknown>,
|
handlerConfig: updatedConfig as Record<string, unknown>,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
loading={!promptSchema}
|
loading={false}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -219,22 +219,27 @@ describe('CreateNewAgentContent', () => {
|
||||||
it('should show correct step content based on currentStep', () => {
|
it('should show correct step content based on currentStep', () => {
|
||||||
// Test step 1 (currentStep: 0) - Setup Agent (name + template)
|
// Test step 1 (currentStep: 0) - Setup Agent (name + template)
|
||||||
const step1Tab = { ...mockTab, currentStep: 0 };
|
const step1Tab = { ...mockTab, currentStep: 0 };
|
||||||
const { rerender } = render(<TestComponent tab={step1Tab} />);
|
const { unmount } = render(<TestComponent tab={step1Tab} />);
|
||||||
|
|
||||||
expect(screen.getByRole('heading', { name: '设置智能体' })).toBeInTheDocument();
|
expect(screen.getByRole('heading', { name: '设置智能体' })).toBeInTheDocument();
|
||||||
expect(screen.getByLabelText('智能体名称')).toBeInTheDocument();
|
expect(screen.getByLabelText('智能体名称')).toBeInTheDocument();
|
||||||
expect(screen.getByTestId('template-search-input')).toBeInTheDocument();
|
expect(screen.getByTestId('template-search-input')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Clean up before next render
|
||||||
|
unmount();
|
||||||
|
|
||||||
// Test step 2 (currentStep: 1) - Edit Prompt
|
// Test step 2 (currentStep: 1) - Edit Prompt
|
||||||
const step2Tab = { ...mockTab, currentStep: 1 };
|
const step2Tab = { ...mockTab, currentStep: 1 };
|
||||||
rerender(<TestComponent tab={step2Tab} />);
|
const { unmount: unmount2 } = render(<TestComponent tab={step2Tab} />);
|
||||||
|
|
||||||
// Should show editPrompt placeholder when no template selected
|
// Should show editPrompt placeholder when no template selected
|
||||||
expect(screen.getByText('请先选择一个模板')).toBeInTheDocument();
|
expect(screen.getByText('请先选择一个模板')).toBeInTheDocument();
|
||||||
|
|
||||||
|
unmount2();
|
||||||
|
|
||||||
// Test step 3 (currentStep: 2) - Immediate Use
|
// Test step 3 (currentStep: 2) - Immediate Use
|
||||||
const step3Tab = { ...mockTab, currentStep: 2 };
|
const step3Tab = { ...mockTab, currentStep: 2 };
|
||||||
rerender(<TestComponent tab={step3Tab} />);
|
render(<TestComponent tab={step3Tab} />);
|
||||||
|
|
||||||
expect(screen.getByRole('heading', { name: '测试并使用' })).toBeInTheDocument();
|
expect(screen.getByRole('heading', { name: '测试并使用' })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
import { closestCenter, DndContext, DragEndEvent, DragOverlay, DragStartEvent, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
|
|
||||||
import { SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
|
||||||
import { ArrayFieldItemTemplateType } from '@rjsf/utils';
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
|
|
||||||
interface DragAndDropProviderProps {
|
|
||||||
/** Array items to be sortable */
|
|
||||||
items: ArrayFieldItemTemplateType[];
|
|
||||||
/** Callback when items are reordered */
|
|
||||||
onReorder: (activeIndex: number, overIndex: number) => void;
|
|
||||||
/** Children components that will be draggable */
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Drag and drop provider for array items
|
|
||||||
* Features:
|
|
||||||
* - Keyboard and pointer sensor support
|
|
||||||
* - Visual drag overlay
|
|
||||||
* - Accessible drag and drop
|
|
||||||
*/
|
|
||||||
export const DragAndDropProvider: React.FC<DragAndDropProviderProps> = ({
|
|
||||||
items,
|
|
||||||
onReorder,
|
|
||||||
children,
|
|
||||||
}) => {
|
|
||||||
const [activeId, setActiveId] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const sensors = useSensors(
|
|
||||||
useSensor(PointerSensor),
|
|
||||||
useSensor(KeyboardSensor, {
|
|
||||||
coordinateGetter: sortableKeyboardCoordinates,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDragStart = (event: DragStartEvent) => {
|
|
||||||
setActiveId(event.active.id as string);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDragEnd = (event: DragEndEvent) => {
|
|
||||||
const { active, over } = event;
|
|
||||||
|
|
||||||
if (over && active.id !== over.id) {
|
|
||||||
const activeIndex = items.findIndex(item => item.key === active.id);
|
|
||||||
const overIndex = items.findIndex(item => item.key === over.id);
|
|
||||||
|
|
||||||
if (activeIndex !== -1 && overIndex !== -1) {
|
|
||||||
onReorder(activeIndex, overIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setActiveId(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const activeItem = items.find(item => item.key === activeId);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DndContext
|
|
||||||
sensors={sensors}
|
|
||||||
collisionDetection={closestCenter}
|
|
||||||
onDragStart={handleDragStart}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
>
|
|
||||||
<SortableContext
|
|
||||||
items={items.map(item => item.key)}
|
|
||||||
strategy={verticalListSortingStrategy}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</SortableContext>
|
|
||||||
|
|
||||||
<DragOverlay>
|
|
||||||
{activeItem
|
|
||||||
? (
|
|
||||||
<div style={{ opacity: 0.5 }}>
|
|
||||||
{activeItem.children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
: null}
|
|
||||||
</DragOverlay>
|
|
||||||
</DndContext>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,147 +0,0 @@
|
||||||
import { useAgentChatStore } from '@/pages/Agent/store/agentChatStore';
|
|
||||||
import { useSortable } from '@dnd-kit/sortable';
|
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
|
||||||
import DragHandleIcon from '@mui/icons-material/DragHandle';
|
|
||||||
import { ArrayFieldItemTemplateType, FormContextType, RJSFSchema } from '@rjsf/utils';
|
|
||||||
import React, { useCallback } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
|
||||||
import { ArrayItemProvider } from '../../context/ArrayItemContext';
|
|
||||||
import { StyledDeleteButton } from '../controls';
|
|
||||||
import { ArrayItemCard, ArrayItemHeader, ArrayItemTitle, DragHandle, ItemContent } from './StyledArrayContainer';
|
|
||||||
import { CollapseIcon, ExpandIcon, StyledCollapse, StyledExpandButton } from './StyledCollapsible';
|
|
||||||
|
|
||||||
/** Interface for sortable array item component props */
|
|
||||||
export interface SortableArrayItemProps<T = unknown, S extends RJSFSchema = RJSFSchema, F extends FormContextType = FormContextType> {
|
|
||||||
/** Array item data from RJSF */
|
|
||||||
item: ArrayFieldItemTemplateType<T, S, F>;
|
|
||||||
/** Index of this item in the array */
|
|
||||||
index: number;
|
|
||||||
/** Whether the item should be collapsible */
|
|
||||||
isCollapsible?: boolean;
|
|
||||||
/** Actual form data for this array item */
|
|
||||||
itemData?: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A sortable array item component with drag-and-drop functionality
|
|
||||||
* Features:
|
|
||||||
* - Drag handle for reordering
|
|
||||||
* - Item title with index
|
|
||||||
* - Delete button
|
|
||||||
* - Collapse/expand toggle (when isCollapsible is true)
|
|
||||||
* - Visual feedback when dragging
|
|
||||||
*/
|
|
||||||
export const SortableArrayItem = <T = unknown, S extends RJSFSchema = RJSFSchema, F extends FormContextType = FormContextType>({
|
|
||||||
item,
|
|
||||||
index,
|
|
||||||
isCollapsible = true,
|
|
||||||
itemData,
|
|
||||||
}: SortableArrayItemProps<T, S, F>) => {
|
|
||||||
const { t } = useTranslation('agent');
|
|
||||||
|
|
||||||
// Extract the actual ID from itemData instead of using path
|
|
||||||
const itemId = itemData && typeof itemData === 'object' && 'id' in itemData
|
|
||||||
? (itemData as { id: string }).id
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const {
|
|
||||||
expanded,
|
|
||||||
setArrayItemExpanded,
|
|
||||||
} = useAgentChatStore(
|
|
||||||
useShallow((state) => ({
|
|
||||||
expanded: itemId ? state.isArrayItemExpanded(itemId) : false,
|
|
||||||
setArrayItemExpanded: state.setArrayItemExpanded,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
|
||||||
attributes,
|
|
||||||
listeners,
|
|
||||||
setNodeRef,
|
|
||||||
transform,
|
|
||||||
transition,
|
|
||||||
isDragging,
|
|
||||||
} = useSortable({
|
|
||||||
id: item.key,
|
|
||||||
data: {
|
|
||||||
type: 'array-item',
|
|
||||||
index,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const style = {
|
|
||||||
transform: CSS.Transform.toString(transform),
|
|
||||||
transition,
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleToggleExpanded = useCallback(() => {
|
|
||||||
if (itemId) {
|
|
||||||
setArrayItemExpanded(itemId, !expanded);
|
|
||||||
}
|
|
||||||
}, [itemId, expanded, setArrayItemExpanded]);
|
|
||||||
|
|
||||||
const handleHeaderClick = useCallback((event: React.MouseEvent) => {
|
|
||||||
// Check if click target is clickable area (exclude buttons and drag handle)
|
|
||||||
const target = event.target as HTMLElement;
|
|
||||||
|
|
||||||
// Skip if clicking buttons or drag handle
|
|
||||||
if (target.closest('button') || target.closest('[data-drag-handle]')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only handle click in collapsible mode
|
|
||||||
if (isCollapsible) {
|
|
||||||
handleToggleExpanded();
|
|
||||||
}
|
|
||||||
}, [isCollapsible, handleToggleExpanded]);
|
|
||||||
|
|
||||||
const handleDeleteClick = useCallback((event: React.MouseEvent) => {
|
|
||||||
event.stopPropagation(); // Prevent event bubbling to header
|
|
||||||
item.buttonsProps.onDropIndexClick(item.index)();
|
|
||||||
}, [item.buttonsProps, item.index]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={setNodeRef} style={style}>
|
|
||||||
<ArrayItemCard $isDragging={isDragging}>
|
|
||||||
<ArrayItemHeader
|
|
||||||
onClick={isCollapsible ? handleHeaderClick : undefined}
|
|
||||||
$isCollapsible={isCollapsible}
|
|
||||||
>
|
|
||||||
<DragHandle {...attributes} {...listeners} data-drag-handle>
|
|
||||||
<DragHandleIcon fontSize='small' />
|
|
||||||
</DragHandle>
|
|
||||||
|
|
||||||
<ArrayItemTitle sx={{ flex: 1 }}>
|
|
||||||
{itemData && typeof itemData === 'object' && 'caption' in itemData ? (itemData as { caption: string }).caption : ''}
|
|
||||||
</ArrayItemTitle>
|
|
||||||
|
|
||||||
{isCollapsible && (
|
|
||||||
<StyledExpandButton onClick={handleToggleExpanded}>
|
|
||||||
{expanded ? <CollapseIcon /> : <ExpandIcon />}
|
|
||||||
</StyledExpandButton>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{item.buttonsProps.hasRemove && (
|
|
||||||
<StyledDeleteButton
|
|
||||||
onClick={handleDeleteClick}
|
|
||||||
size='small'
|
|
||||||
title={t('PromptConfig.RemoveItem')}
|
|
||||||
>
|
|
||||||
<DeleteIcon fontSize='small' />
|
|
||||||
</StyledDeleteButton>
|
|
||||||
)}
|
|
||||||
</ArrayItemHeader>
|
|
||||||
|
|
||||||
<StyledCollapse in={expanded} timeout='auto' unmountOnExit>
|
|
||||||
<ItemContent>
|
|
||||||
<ArrayItemProvider isInArrayItem={true} arrayItemCollapsible={isCollapsible}>
|
|
||||||
{item.children}
|
|
||||||
</ArrayItemProvider>
|
|
||||||
</ItemContent>
|
|
||||||
</StyledCollapse>
|
|
||||||
</ArrayItemCard>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
export * from './DragAndDrop';
|
|
||||||
export * from './SortableArrayItem';
|
|
||||||
export * from './StyledArrayContainer';
|
export * from './StyledArrayContainer';
|
||||||
export * from './StyledCard';
|
export * from './StyledCard';
|
||||||
export * from './StyledCollapsible';
|
export * from './StyledCollapsible';
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { FieldProps } from '@rjsf/utils';
|
import type { FieldProps } from '@rjsf/utils';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { ConditionalFieldConfig, ExtendedFormContext } from '../index';
|
import type { ConditionalFieldConfig, ExtendedFormContext } from '../index';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ConditionalField wraps any field and conditionally shows/hides it based on sibling field values.
|
* ConditionalField wraps any field and conditionally shows/hides it based on sibling field values.
|
||||||
|
|
@ -12,7 +12,7 @@ import { ConditionalFieldConfig, ExtendedFormContext } from '../index';
|
||||||
* 4. Uses useMemo to prevent unnecessary recalculations
|
* 4. Uses useMemo to prevent unnecessary recalculations
|
||||||
*/
|
*/
|
||||||
export const ConditionalField: React.FC<FieldProps> = (props) => {
|
export const ConditionalField: React.FC<FieldProps> = (props) => {
|
||||||
const { uiSchema, registry, idSchema } = props;
|
const { uiSchema, registry, fieldPathId } = props;
|
||||||
|
|
||||||
const condition = uiSchema?.['ui:condition'] as ConditionalFieldConfig | undefined;
|
const condition = uiSchema?.['ui:condition'] as ConditionalFieldConfig | undefined;
|
||||||
|
|
||||||
|
|
@ -28,8 +28,10 @@ export const ConditionalField: React.FC<FieldProps> = (props) => {
|
||||||
if (!rootFormData) return true;
|
if (!rootFormData) return true;
|
||||||
|
|
||||||
// Parse the field's path to find its parent object where sibling fields are located
|
// Parse the field's path to find its parent object where sibling fields are located
|
||||||
const fieldPath = idSchema.$id.replace(/^root_/, '');
|
// In RJSF 6.x, fieldPathId.$id is a string that contains the path
|
||||||
const pathParts = fieldPath.split('_');
|
const fieldPathValue = fieldPathId?.$id ?? '';
|
||||||
|
const fieldPath = (typeof fieldPathValue === 'string' ? fieldPathValue : '').replace(/^root_/, '');
|
||||||
|
const pathParts = fieldPath.split('_').filter(Boolean);
|
||||||
pathParts.pop(); // Remove current field name to get parent path
|
pathParts.pop(); // Remove current field name to get parent path
|
||||||
|
|
||||||
// Navigate to parent object in the form data tree
|
// Navigate to parent object in the form data tree
|
||||||
|
|
@ -58,7 +60,7 @@ export const ConditionalField: React.FC<FieldProps> = (props) => {
|
||||||
|
|
||||||
// Apply inverse logic if specified
|
// Apply inverse logic if specified
|
||||||
return hideWhen ? !conditionMet : conditionMet;
|
return hideWhen ? !conditionMet : conditionMet;
|
||||||
}, [condition, registry.formContext, idSchema.$id]);
|
}, [condition, registry.formContext, fieldPathId?.$id]);
|
||||||
|
|
||||||
// Hidden fields return nothing
|
// Hidden fields return nothing
|
||||||
if (!shouldShow) {
|
if (!shouldShow) {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { ErrorDisplay } from './components/ErrorDisplay';
|
||||||
import { ArrayItemProvider } from './context/ArrayItemContext';
|
import { ArrayItemProvider } from './context/ArrayItemContext';
|
||||||
import { useDefaultUiSchema } from './defaultUiSchema';
|
import { useDefaultUiSchema } from './defaultUiSchema';
|
||||||
import { fields } from './fields';
|
import { fields } from './fields';
|
||||||
import { ArrayFieldTemplate, FieldTemplate, ObjectFieldTemplate, RootObjectFieldTemplate } from './templates';
|
import { ArrayFieldItemTemplate, ArrayFieldTemplate, FieldTemplate, ObjectFieldTemplate, RootObjectFieldTemplate } from './templates';
|
||||||
import { widgets } from './widgets';
|
import { widgets } from './widgets';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -67,7 +67,7 @@ export const PromptConfigForm: React.FC<PromptConfigFormProps> = ({
|
||||||
|
|
||||||
const templates = useMemo(() => {
|
const templates = useMemo(() => {
|
||||||
const rootObjectFieldTemplate = (props: ObjectFieldTemplateProps) => {
|
const rootObjectFieldTemplate = (props: ObjectFieldTemplateProps) => {
|
||||||
const isRootLevel = props.idSchema.$id === 'root';
|
const isRootLevel = props.fieldPathId?.$id === 'root';
|
||||||
return isRootLevel
|
return isRootLevel
|
||||||
? <RootObjectFieldTemplate {...props} />
|
? <RootObjectFieldTemplate {...props} />
|
||||||
: <ObjectFieldTemplate {...props} />;
|
: <ObjectFieldTemplate {...props} />;
|
||||||
|
|
@ -75,6 +75,8 @@ export const PromptConfigForm: React.FC<PromptConfigFormProps> = ({
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ArrayFieldTemplate,
|
ArrayFieldTemplate,
|
||||||
|
|
||||||
|
ArrayFieldItemTemplate,
|
||||||
FieldTemplate,
|
FieldTemplate,
|
||||||
ObjectFieldTemplate: rootObjectFieldTemplate,
|
ObjectFieldTemplate: rootObjectFieldTemplate,
|
||||||
};
|
};
|
||||||
|
|
@ -145,7 +147,7 @@ export const PromptConfigForm: React.FC<PromptConfigFormProps> = ({
|
||||||
widgets={widgets}
|
widgets={widgets}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
showErrorList={false}
|
showErrorList={false}
|
||||||
liveValidate
|
liveValidate='onChange'
|
||||||
noHtml5Validate
|
noHtml5Validate
|
||||||
>
|
>
|
||||||
<div />
|
<div />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
import DragHandleIcon from '@mui/icons-material/DragHandle';
|
||||||
|
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||||
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
|
import { Box, IconButton } from '@mui/material';
|
||||||
|
import { ArrayFieldItemTemplateProps, FormContextType, getTemplate, getUiOptions, RJSFSchema } from '@rjsf/utils';
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { ArrayItemProvider } from '../context/ArrayItemContext';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Array Field Item Template with collapse and drag-and-drop support
|
||||||
|
* In RJSF 6.x, this template is called by ArrayField to render each item
|
||||||
|
*/
|
||||||
|
export function ArrayFieldItemTemplate<T = unknown, S extends RJSFSchema = RJSFSchema, F extends FormContextType = FormContextType>(
|
||||||
|
props: ArrayFieldItemTemplateProps<T, S, F>,
|
||||||
|
): React.ReactElement {
|
||||||
|
const { children, index, hasToolbar, buttonsProps, registry, uiSchema } = props;
|
||||||
|
const { t } = useTranslation('agent');
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
|
const handleToggleExpanded = useCallback(() => {
|
||||||
|
setExpanded((previous) => !previous);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Get the ArrayFieldItemButtonsTemplate to render buttons
|
||||||
|
const uiOptions = getUiOptions<T, S, F>(uiSchema);
|
||||||
|
const ArrayFieldItemButtonsTemplate = getTemplate<'ArrayFieldItemButtonsTemplate', T, S, F>(
|
||||||
|
'ArrayFieldItemButtonsTemplate',
|
||||||
|
registry,
|
||||||
|
uiOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ArrayItemProvider isInArrayItem arrayItemCollapsible>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
borderRadius: 1,
|
||||||
|
mb: 1,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Header with controls */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 1,
|
||||||
|
p: 1,
|
||||||
|
bgcolor: 'background.default',
|
||||||
|
borderBottom: expanded ? '1px solid' : 'none',
|
||||||
|
borderColor: 'divider',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Drag handle */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
cursor: 'grab',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
color: 'text.secondary',
|
||||||
|
'&:active': { cursor: 'grabbing' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DragHandleIcon fontSize='small' />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Item title */}
|
||||||
|
<Box sx={{ flex: 1, display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||||
|
<Box
|
||||||
|
component='span'
|
||||||
|
sx={{
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
color: 'text.primary',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('PromptConfig.ItemIndex', { index: index + 1 })}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Expand/collapse button */}
|
||||||
|
<IconButton
|
||||||
|
size='small'
|
||||||
|
onClick={handleToggleExpanded}
|
||||||
|
title={expanded ? t('PromptConfig.Collapse') : t('PromptConfig.Expand')}
|
||||||
|
sx={{ color: 'text.secondary' }}
|
||||||
|
>
|
||||||
|
{expanded ? <ExpandLessIcon fontSize='small' /> : <ExpandMoreIcon fontSize='small' />}
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
{/* Action buttons (remove, move up/down, etc.) */}
|
||||||
|
{hasToolbar && <ArrayFieldItemButtonsTemplate {...buttonsProps} />}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
{expanded && (
|
||||||
|
<Box sx={{ p: 2 }}>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</ArrayItemProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,40 +1,19 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
||||||
import { DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
|
|
||||||
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
|
|
||||||
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
|
||||||
import { Box, Typography } from '@mui/material';
|
import { Box, Typography } from '@mui/material';
|
||||||
import { ArrayFieldTemplateProps } from '@rjsf/utils';
|
import { ArrayFieldTemplateProps } from '@rjsf/utils';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ArrayAddButton, ArrayContainer, ArrayHeader, ArrayItemCount, EmptyState, HelpTooltip, SortableArrayItem, StyledFieldLabel } from '../components';
|
import { ArrayAddButton, ArrayContainer, ArrayHeader, ArrayItemCount, EmptyState, HelpTooltip, StyledFieldLabel } from '../components';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enhanced Array Field Template with drag-and-drop functionality
|
* Enhanced Array Field Template
|
||||||
|
* In RJSF 6.x, items are pre-rendered ReactElements, so we just display them
|
||||||
|
* The drag-and-drop and collapse logic is handled in ArrayFieldItemTemplate
|
||||||
*/
|
*/
|
||||||
export const ArrayFieldTemplate: React.FC<ArrayFieldTemplateProps> = (props) => {
|
export const ArrayFieldTemplate: React.FC<ArrayFieldTemplateProps> = (props) => {
|
||||||
const { items, onAddClick, canAdd, title, schema, formData } = props;
|
const { items, onAddClick, canAdd, title, schema } = props;
|
||||||
const { t } = useTranslation('agent');
|
const { t } = useTranslation('agent');
|
||||||
|
|
||||||
const sensors = useSensors(
|
|
||||||
useSensor(PointerSensor, {
|
|
||||||
activationConstraint: { distance: 8 },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDragEnd = (event: DragEndEvent) => {
|
|
||||||
const { active, over } = event;
|
|
||||||
if (!over || active.id === over.id) return;
|
|
||||||
const activeIndex = items.findIndex((item) => item.key === active.id);
|
|
||||||
const overIndex = items.findIndex((item) => item.key === over.id);
|
|
||||||
if (activeIndex !== overIndex && activeIndex !== -1 && overIndex !== -1) {
|
|
||||||
const activeItem = items[activeIndex];
|
|
||||||
activeItem.buttonsProps.onReorderClick(activeIndex, overIndex)();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const description = schema.description;
|
const description = schema.description;
|
||||||
const itemIds = items.map((item) => item.key);
|
|
||||||
const isItemsCollapsible = true;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ArrayContainer>
|
<ArrayContainer>
|
||||||
|
|
@ -64,26 +43,9 @@ export const ArrayFieldTemplate: React.FC<ArrayFieldTemplateProps> = (props) =>
|
||||||
</EmptyState>
|
</EmptyState>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<DndContext
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||||
sensors={sensors}
|
{items}
|
||||||
modifiers={[restrictToVerticalAxis]}
|
</Box>
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
>
|
|
||||||
<SortableContext items={itemIds} strategy={verticalListSortingStrategy}>
|
|
||||||
{items.map((item, index) => {
|
|
||||||
const itemData = Array.isArray(formData) ? formData[index] : undefined;
|
|
||||||
return (
|
|
||||||
<SortableArrayItem
|
|
||||||
key={item.key}
|
|
||||||
item={item}
|
|
||||||
index={index}
|
|
||||||
isCollapsible={isItemsCollapsible}
|
|
||||||
itemData={itemData}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</SortableContext>
|
|
||||||
</DndContext>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{canAdd && (
|
{canAdd && (
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ export const RootObjectFieldTemplate: React.FC<ObjectFieldTemplateProps> = (prop
|
||||||
const { properties, schema } = props;
|
const { properties, schema } = props;
|
||||||
const [activeTab, setActiveTab] = useState(0);
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
const { t } = useTranslation('agent');
|
const { t } = useTranslation('agent');
|
||||||
|
|
||||||
const { formFieldsToScrollTo } = useAgentChatStore(
|
const { formFieldsToScrollTo } = useAgentChatStore(
|
||||||
useShallow((state) => ({
|
useShallow((state) => ({
|
||||||
formFieldsToScrollTo: state.formFieldsToScrollTo,
|
formFieldsToScrollTo: state.formFieldsToScrollTo,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
export { ArrayFieldItemTemplate } from './ArrayFieldItemTemplate';
|
||||||
export { ArrayFieldTemplate } from './ArrayFieldTemplate';
|
export { ArrayFieldTemplate } from './ArrayFieldTemplate';
|
||||||
export { FieldTemplate } from './FieldTemplate';
|
export { FieldTemplate } from './FieldTemplate';
|
||||||
export { ObjectFieldTemplate } from './ObjectFieldTemplate';
|
export { ObjectFieldTemplate } from './ObjectFieldTemplate';
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { Helmet } from '@dr.pogodin/react-helmet';
|
import { Helmet } from '@dr.pogodin/react-helmet';
|
||||||
import { styled, Theme } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import { lazy } from 'react';
|
import { lazy } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import is, { isNot } from 'typescript-styled-is';
|
|
||||||
import { Route, Switch } from 'wouter';
|
import { Route, Switch } from 'wouter';
|
||||||
|
|
||||||
import { PageType } from '@/constants/pageTypes';
|
import { PageType } from '@/constants/pageTypes';
|
||||||
|
|
@ -42,20 +41,25 @@ const Root = styled('div')`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ContentRoot = styled('div')<{ $sidebar: boolean }>`
|
const ContentRoot = styled('div')<{ $sidebar: boolean }>(
|
||||||
|
({ theme, $sidebar }) => `
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
${is('$sidebar')`
|
|
||||||
width: calc(100% - ${({ theme }: { theme: Theme }) => theme.sidebar.width}px);
|
|
||||||
max-width: calc(100% - ${({ theme }: { theme: Theme }) => theme.sidebar.width}px);
|
|
||||||
`}
|
|
||||||
${isNot('$sidebar')`
|
|
||||||
width: 100%;
|
|
||||||
`}
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
`;
|
|
||||||
|
${
|
||||||
|
$sidebar
|
||||||
|
? `
|
||||||
|
width: calc(100% - ${theme.sidebar.width}px);
|
||||||
|
max-width: calc(100% - ${theme.sidebar.width}px);
|
||||||
|
`
|
||||||
|
: `
|
||||||
|
width: 100%;
|
||||||
|
`
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
export default function Main(): React.JSX.Element {
|
export default function Main(): React.JSX.Element {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { WindowNames } from '@services/windows/WindowProperties';
|
||||||
import { browserViewMetaData } from './common/browserViewMetaData';
|
import { browserViewMetaData } from './common/browserViewMetaData';
|
||||||
import './view';
|
import './view';
|
||||||
import { syncTidgiStateWhenWikiLoads } from './appState';
|
import { syncTidgiStateWhenWikiLoads } from './appState';
|
||||||
|
import { consoleLogToLogFile } from './fixer/consoleLogToLogFile';
|
||||||
import { fixAlertConfirm } from './fixer/fixAlertConfirm';
|
import { fixAlertConfirm } from './fixer/fixAlertConfirm';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|
@ -26,6 +27,9 @@ declare global {
|
||||||
|
|
||||||
switch (browserViewMetaData.windowName) {
|
switch (browserViewMetaData.windowName) {
|
||||||
case WindowNames.main: {
|
case WindowNames.main: {
|
||||||
|
// Enable console logging to file for main window
|
||||||
|
void consoleLogToLogFile('TidGi');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* automatically reload page/wiki when wifi/network is re-connected to a different one, which may cause local ip changed. Or wifi status changed when wiki startup, causing wiki not loaded properly.
|
* automatically reload page/wiki when wifi/network is re-connected to a different one, which may cause local ip changed. Or wifi status changed when wiki startup, causing wiki not loaded properly.
|
||||||
* @url https://www.electronjs.org/docs/latest/tutorial/online-offline-events
|
* @url https://www.electronjs.org/docs/latest/tutorial/online-offline-events
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import { useThemeObservable } from '@services/theme/hooks';
|
||||||
import { initRendererI18N } from './services/libs/i18n/renderer';
|
import { initRendererI18N } from './services/libs/i18n/renderer';
|
||||||
import 'electron-ipc-cat/fixContextIsolation';
|
import 'electron-ipc-cat/fixContextIsolation';
|
||||||
import { useHashLocation } from 'wouter/use-hash-location';
|
import { useHashLocation } from 'wouter/use-hash-location';
|
||||||
|
import { ErrorBoundary } from './components/ErrorBoundary';
|
||||||
import { RootStyle } from './components/RootStyle';
|
import { RootStyle } from './components/RootStyle';
|
||||||
import { initTestKeyboardShortcutFallback } from './helpers/testKeyboardShortcuts';
|
import { initTestKeyboardShortcutFallback } from './helpers/testKeyboardShortcuts';
|
||||||
import { Pages } from './windows';
|
import { Pages } from './windows';
|
||||||
|
|
@ -33,6 +34,7 @@ function App(): JSX.Element {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
<ErrorBoundary>
|
||||||
<ThemeProvider theme={theme?.shouldUseDarkColors === true ? darkTheme : lightTheme}>
|
<ThemeProvider theme={theme?.shouldUseDarkColors === true ? darkTheme : lightTheme}>
|
||||||
<StyledEngineProvider injectFirst>
|
<StyledEngineProvider injectFirst>
|
||||||
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
||||||
|
|
@ -49,6 +51,7 @@ function App(): JSX.Element {
|
||||||
</LocalizationProvider>
|
</LocalizationProvider>
|
||||||
</StyledEngineProvider>
|
</StyledEngineProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
</ErrorBoundary>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,12 @@ const labeledLoggers = new Map<string, winston.Logger>();
|
||||||
* @returns A winston logger instance for the specified label
|
* @returns A winston logger instance for the specified label
|
||||||
*/
|
*/
|
||||||
export function getLoggerForLabel(label: string): winston.Logger {
|
export function getLoggerForLabel(label: string): winston.Logger {
|
||||||
|
// Special case: if label is 'TidGi', return the main logger to avoid file write conflicts
|
||||||
|
// This allows main window console logs to merge into the same TidGi-*.log file
|
||||||
|
if (label === 'TidGi') {
|
||||||
|
return logger;
|
||||||
|
}
|
||||||
|
|
||||||
const existingLogger = labeledLoggers.get(label);
|
const existingLogger = labeledLoggers.get(label);
|
||||||
if (existingLogger) {
|
if (existingLogger) {
|
||||||
return existingLogger;
|
return existingLogger;
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,10 @@ import type { IWikiWorkspace, IWorkspace } from '@services/workspaces/interface'
|
||||||
import { backOff } from 'exponential-backoff';
|
import { backOff } from 'exponential-backoff';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type { FileInfo } from 'tiddlywiki';
|
import type { IFileInfo } from 'tiddlywiki';
|
||||||
import type { Tiddler, Wiki } from 'tiddlywiki';
|
import type { Tiddler, Wiki } from 'tiddlywiki';
|
||||||
import { isFileLockError } from './utilities';
|
import { isFileLockError } from './utilities';
|
||||||
|
|
||||||
export type IFileSystemAdaptorCallback = (error: Error | null | string, fileInfo?: FileInfo | null) => void;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base filesystem adaptor that handles tiddler save/delete operations and sub-wiki routing.
|
* Base filesystem adaptor that handles tiddler save/delete operations and sub-wiki routing.
|
||||||
* This class can be used standalone or extended for additional functionality like file watching.
|
* This class can be used standalone or extended for additional functionality like file watching.
|
||||||
|
|
@ -110,7 +108,7 @@ export class FileSystemAdaptor {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
getTiddlerInfo(tiddler: Tiddler): FileInfo | undefined {
|
getTiddlerInfo(tiddler: Tiddler): IFileInfo | undefined {
|
||||||
const title = tiddler.fields.title;
|
const title = tiddler.fields.title;
|
||||||
return this.boot.files[title];
|
return this.boot.files[title];
|
||||||
}
|
}
|
||||||
|
|
@ -119,7 +117,7 @@ export class FileSystemAdaptor {
|
||||||
* Main routing logic: determine where a tiddler should be saved based on its tags.
|
* Main routing logic: determine where a tiddler should be saved based on its tags.
|
||||||
* For draft tiddlers, check the original tiddler's tags.
|
* For draft tiddlers, check the original tiddler's tags.
|
||||||
*/
|
*/
|
||||||
async getTiddlerFileInfo(tiddler: Tiddler): Promise<FileInfo | null> {
|
async getTiddlerFileInfo(tiddler: Tiddler): Promise<IFileInfo | null> {
|
||||||
if (!this.boot.wikiTiddlersPath) {
|
if (!this.boot.wikiTiddlersPath) {
|
||||||
throw new Error('filesystem adaptor requires a valid wiki folder');
|
throw new Error('filesystem adaptor requires a valid wiki folder');
|
||||||
}
|
}
|
||||||
|
|
@ -165,7 +163,7 @@ export class FileSystemAdaptor {
|
||||||
* Generate file info for sub-wiki directory
|
* Generate file info for sub-wiki directory
|
||||||
* Handles symlinks correctly across platforms (Windows junctions and Linux symlinks)
|
* Handles symlinks correctly across platforms (Windows junctions and Linux symlinks)
|
||||||
*/
|
*/
|
||||||
protected generateSubWikiFileInfo(tiddler: Tiddler, subWiki: IWikiWorkspace, fileInfo: FileInfo | undefined): FileInfo {
|
protected generateSubWikiFileInfo(tiddler: Tiddler, subWiki: IWikiWorkspace, fileInfo: IFileInfo | undefined): IFileInfo {
|
||||||
let targetDirectory = subWiki.wikiFolderLocation;
|
let targetDirectory = subWiki.wikiFolderLocation;
|
||||||
|
|
||||||
// Resolve symlinks to ensure consistent path handling across platforms
|
// Resolve symlinks to ensure consistent path handling across platforms
|
||||||
|
|
@ -186,14 +184,14 @@ export class FileSystemAdaptor {
|
||||||
pathFilters: undefined,
|
pathFilters: undefined,
|
||||||
extFilters: this.extensionFilters,
|
extFilters: this.extensionFilters,
|
||||||
wiki: this.wiki,
|
wiki: this.wiki,
|
||||||
fileInfo: fileInfo ? { ...fileInfo, overwrite: true } : { overwrite: true } as FileInfo,
|
fileInfo: fileInfo ? { ...fileInfo, overwrite: true } : { overwrite: true } as IFileInfo,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate file info using default FileSystemPaths logic
|
* Generate file info using default FileSystemPaths logic
|
||||||
*/
|
*/
|
||||||
protected generateDefaultFileInfo(tiddler: Tiddler, fileInfo: FileInfo | undefined): FileInfo {
|
protected generateDefaultFileInfo(tiddler: Tiddler, fileInfo: IFileInfo | undefined): IFileInfo {
|
||||||
let pathFilters: string[] | undefined;
|
let pathFilters: string[] | undefined;
|
||||||
|
|
||||||
if (this.wiki.tiddlerExists('$:/config/FileSystemPaths')) {
|
if (this.wiki.tiddlerExists('$:/config/FileSystemPaths')) {
|
||||||
|
|
@ -206,7 +204,7 @@ export class FileSystemAdaptor {
|
||||||
pathFilters,
|
pathFilters,
|
||||||
extFilters: this.extensionFilters,
|
extFilters: this.extensionFilters,
|
||||||
wiki: this.wiki,
|
wiki: this.wiki,
|
||||||
fileInfo: fileInfo ? { ...fileInfo, overwrite: true } : { overwrite: true } as FileInfo,
|
fileInfo: fileInfo ? { ...fileInfo, overwrite: true } : { overwrite: true } as IFileInfo,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -214,7 +212,11 @@ export class FileSystemAdaptor {
|
||||||
* Save a tiddler to the filesystem
|
* Save a tiddler to the filesystem
|
||||||
* Can be used with callback (legacy) or as async/await
|
* Can be used with callback (legacy) or as async/await
|
||||||
*/
|
*/
|
||||||
async saveTiddler(tiddler: Tiddler, callback?: IFileSystemAdaptorCallback, _options?: { tiddlerInfo?: Record<string, unknown> }): Promise<void> {
|
async saveTiddler(
|
||||||
|
tiddler: Tiddler,
|
||||||
|
callback?: (error: Error | null | string, adaptorInfo?: IFileInfo | null, revision?: string) => void,
|
||||||
|
_options?: { tiddlerInfo?: Record<string, unknown> },
|
||||||
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const fileInfo = await this.getTiddlerFileInfo(tiddler);
|
const fileInfo = await this.getTiddlerFileInfo(tiddler);
|
||||||
|
|
||||||
|
|
@ -240,7 +242,7 @@ export class FileSystemAdaptor {
|
||||||
bootInfo: savedFileInfo, // New file info to be kept
|
bootInfo: savedFileInfo, // New file info to be kept
|
||||||
title: tiddler.fields.title,
|
title: tiddler.fields.title,
|
||||||
};
|
};
|
||||||
$tw.utils.cleanupTiddlerFiles(cleanupOptions, (cleanupError: Error | null, _cleanedFileInfo?: FileInfo) => {
|
$tw.utils.cleanupTiddlerFiles(cleanupOptions, (cleanupError: Error | null, _cleanedFileInfo?: IFileInfo) => {
|
||||||
if (cleanupError) {
|
if (cleanupError) {
|
||||||
reject(cleanupError);
|
reject(cleanupError);
|
||||||
return;
|
return;
|
||||||
|
|
@ -260,7 +262,10 @@ export class FileSystemAdaptor {
|
||||||
/**
|
/**
|
||||||
* Load a tiddler - not needed as all tiddlers are loaded during boot
|
* Load a tiddler - not needed as all tiddlers are loaded during boot
|
||||||
*/
|
*/
|
||||||
loadTiddler(_title: string, callback: IFileSystemAdaptorCallback): void {
|
loadTiddler(
|
||||||
|
_title: string,
|
||||||
|
callback: (error: Error | null | string, tiddlerFields?: Record<string, unknown> | null) => void,
|
||||||
|
): void {
|
||||||
callback(null, null);
|
callback(null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -268,7 +273,11 @@ export class FileSystemAdaptor {
|
||||||
* Delete a tiddler from the filesystem
|
* Delete a tiddler from the filesystem
|
||||||
* Can be used with callback (legacy) or as async/await
|
* Can be used with callback (legacy) or as async/await
|
||||||
*/
|
*/
|
||||||
async deleteTiddler(title: string, callback?: IFileSystemAdaptorCallback, _options?: unknown): Promise<void> {
|
async deleteTiddler(
|
||||||
|
title: string,
|
||||||
|
callback?: (error: Error | null | string, adaptorInfo?: IFileInfo | null) => void,
|
||||||
|
_options?: unknown,
|
||||||
|
): Promise<void> {
|
||||||
const fileInfo = this.boot.files[title];
|
const fileInfo = this.boot.files[title];
|
||||||
|
|
||||||
if (!fileInfo) {
|
if (!fileInfo) {
|
||||||
|
|
@ -278,7 +287,7 @@ export class FileSystemAdaptor {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
$tw.utils.deleteTiddlerFile(fileInfo, (error: Error | null, deletedFileInfo?: FileInfo) => {
|
$tw.utils.deleteTiddlerFile(fileInfo, (error: Error | null, deletedFileInfo?: IFileInfo) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
const errorCode = (error as NodeJS.ErrnoException).code;
|
const errorCode = (error as NodeJS.ErrnoException).code;
|
||||||
const errorSyscall = (error as NodeJS.ErrnoException).syscall;
|
const errorSyscall = (error as NodeJS.ErrnoException).syscall;
|
||||||
|
|
@ -339,9 +348,9 @@ export class FileSystemAdaptor {
|
||||||
*/
|
*/
|
||||||
protected async saveTiddlerWithRetry(
|
protected async saveTiddlerWithRetry(
|
||||||
tiddler: Tiddler,
|
tiddler: Tiddler,
|
||||||
fileInfo: FileInfo,
|
fileInfo: IFileInfo,
|
||||||
options: { maxRetries?: number; initialDelay?: number; maxDelay?: number } = {},
|
options: { maxRetries?: number; initialDelay?: number; maxDelay?: number } = {},
|
||||||
): Promise<FileInfo> {
|
): Promise<IFileInfo> {
|
||||||
const maxRetries = options.maxRetries ?? 10;
|
const maxRetries = options.maxRetries ?? 10;
|
||||||
const initialDelay = options.initialDelay ?? 50;
|
const initialDelay = options.initialDelay ?? 50;
|
||||||
const maxDelay = options.maxDelay ?? 2000;
|
const maxDelay = options.maxDelay ?? 2000;
|
||||||
|
|
@ -349,8 +358,8 @@ export class FileSystemAdaptor {
|
||||||
try {
|
try {
|
||||||
return await backOff(
|
return await backOff(
|
||||||
async () => {
|
async () => {
|
||||||
return await new Promise<FileInfo>((resolve, reject) => {
|
return await new Promise<IFileInfo>((resolve, reject) => {
|
||||||
$tw.utils.saveTiddlerToFile(tiddler, fileInfo, (saveError: Error | null, savedFileInfo?: FileInfo) => {
|
$tw.utils.saveTiddlerToFile(tiddler, fileInfo, (saveError: Error | null, savedFileInfo?: IFileInfo) => {
|
||||||
if (saveError) {
|
if (saveError) {
|
||||||
reject(saveError);
|
reject(saveError);
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import type nsfw from 'nsfw';
|
import type nsfw from 'nsfw';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type { FileInfo } from 'tiddlywiki';
|
import type { IFileInfo } from 'tiddlywiki';
|
||||||
|
|
||||||
export type IBootFilesIndexItemWithTitle = FileInfo & { tiddlerTitle: string };
|
export type IBootFilesIndexItemWithTitle = IFileInfo & { tiddlerTitle: string };
|
||||||
|
|
||||||
export interface ISubWikiInfo {
|
export interface ISubWikiInfo {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,9 @@ import { git, workspace } from '@services/wiki/wikiWorker/services';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import nsfw from 'nsfw';
|
import nsfw from 'nsfw';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type { Tiddler, Wiki } from 'tiddlywiki';
|
import type { IFileInfo, Tiddler, Wiki } from 'tiddlywiki';
|
||||||
import { FileSystemAdaptor, type IFileSystemAdaptorCallback } from './FileSystemAdaptor';
|
import { FileSystemAdaptor } from './FileSystemAdaptor';
|
||||||
import { type IBootFilesIndexItemWithTitle, InverseFilesIndex } from './InverseFilesIndex';
|
import { type IBootFilesIndexItemWithTitle, InverseFilesIndex } from './InverseFilesIndex';
|
||||||
import { getActionName } from './utilities';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delay before actually processing file deletion.
|
* Delay before actually processing file deletion.
|
||||||
|
|
@ -99,18 +98,40 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
||||||
* Save a tiddler to the filesystem (with file watching support)
|
* Save a tiddler to the filesystem (with file watching support)
|
||||||
* Can be used with callback (legacy) or as async/await
|
* Can be used with callback (legacy) or as async/await
|
||||||
*/
|
*/
|
||||||
override async saveTiddler(tiddler: Tiddler, callback?: IFileSystemAdaptorCallback, options?: { tiddlerInfo?: Record<string, unknown> }): Promise<void> {
|
override async saveTiddler(
|
||||||
|
tiddler: Tiddler,
|
||||||
|
callback?: (error: Error | null | string, adaptorInfo?: IFileInfo | null, revision?: string) => void,
|
||||||
|
options?: { tiddlerInfo?: Record<string, unknown> },
|
||||||
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Get existing file info (if tiddler already exists on disk)
|
// Get existing file info (if tiddler already exists on disk)
|
||||||
const oldFileInfo = this.boot.files[tiddler.fields.title];
|
const oldFileInfo = this.boot.files[tiddler.fields.title];
|
||||||
|
|
||||||
|
// For new tiddlers, pre-calculate the file path and exclude it to prevent echo
|
||||||
|
// Must exclude both the main file and its .meta file to prevent watch-fs from detecting our own save operations
|
||||||
|
// This is critical for plugin JSON files which always have a separate .meta file
|
||||||
|
let excludedNewFilePath: string | undefined;
|
||||||
|
if (!oldFileInfo) {
|
||||||
|
try {
|
||||||
|
const newFileInfo = await this.getTiddlerFileInfo(tiddler);
|
||||||
|
if (newFileInfo?.filepath) {
|
||||||
|
this.excludeFile(newFileInfo.filepath);
|
||||||
|
// Also exclude the .meta file if it exists
|
||||||
|
const metaFilePath = `${newFileInfo.filepath}.meta`;
|
||||||
|
this.excludeFile(metaFilePath);
|
||||||
|
excludedNewFilePath = newFileInfo.filepath;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.alert(`WatchFileSystemAdaptor Failed to pre-calculate file path for new tiddler: ${tiddler.fields.title}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Exclude old file path before save (if it exists)
|
// Exclude old file path before save (if it exists)
|
||||||
if (oldFileInfo) {
|
if (oldFileInfo) {
|
||||||
this.excludeFile(oldFileInfo.filepath);
|
this.excludeFile(oldFileInfo.filepath);
|
||||||
this.logger.log(`[WATCH_FS_SAVE] Excluded existing file: ${oldFileInfo.filepath}`);
|
// Also exclude the .meta file if it exists
|
||||||
} else {
|
const metaFilePath = `${oldFileInfo.filepath}.meta`;
|
||||||
// For new tiddlers, we can't pre-exclude them since we don't know the path yet
|
this.excludeFile(metaFilePath);
|
||||||
this.logger.log(`[WATCH_FS_NEW_TIDDLER] Saving new tiddler: ${tiddler.fields.title}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call parent's saveTiddler to handle the actual save (including cleanup of old files)
|
// Call parent's saveTiddler to handle the actual save (including cleanup of old files)
|
||||||
|
|
@ -130,11 +151,20 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
||||||
|
|
||||||
// Schedule re-inclusion after delay to avoid echo
|
// Schedule re-inclusion after delay to avoid echo
|
||||||
this.scheduleFileInclusion(finalFileInfo.filepath);
|
this.scheduleFileInclusion(finalFileInfo.filepath);
|
||||||
|
// Also re-include the .meta file
|
||||||
|
this.scheduleFileInclusion(`${finalFileInfo.filepath}.meta`);
|
||||||
|
|
||||||
|
// For edge case, rarely if we wrongly pre-excluded a new file path and it's different from the final path that tw decided to use, re-include it to revoke the influence
|
||||||
|
if (excludedNewFilePath && excludedNewFilePath !== finalFileInfo.filepath) {
|
||||||
|
this.scheduleFileInclusion(excludedNewFilePath);
|
||||||
|
this.scheduleFileInclusion(`${excludedNewFilePath}.meta`);
|
||||||
|
}
|
||||||
|
|
||||||
// If old file path was different and we excluded it, re-include it
|
// If old file path was different and we excluded it, re-include it
|
||||||
// The old file should be deleted by now via cleanupTiddlerFiles
|
// The old file should be deleted by now via cleanupTiddlerFiles
|
||||||
if (oldFileInfo && oldFileInfo.filepath !== finalFileInfo.filepath) {
|
if (oldFileInfo && oldFileInfo.filepath !== finalFileInfo.filepath) {
|
||||||
this.scheduleFileInclusion(oldFileInfo.filepath);
|
this.scheduleFileInclusion(oldFileInfo.filepath);
|
||||||
|
this.scheduleFileInclusion(`${oldFileInfo.filepath}.meta`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorObject = error instanceof Error ? error : new Error(typeof error === 'string' ? error : 'Unknown error');
|
const errorObject = error instanceof Error ? error : new Error(typeof error === 'string' ? error : 'Unknown error');
|
||||||
|
|
@ -147,7 +177,11 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
||||||
* Delete a tiddler from the filesystem (with file watching support)
|
* Delete a tiddler from the filesystem (with file watching support)
|
||||||
* Can be used with callback (legacy) or as async/await
|
* Can be used with callback (legacy) or as async/await
|
||||||
*/
|
*/
|
||||||
override async deleteTiddler(title: string, callback?: IFileSystemAdaptorCallback, _options?: unknown): Promise<void> {
|
override async deleteTiddler(
|
||||||
|
title: string,
|
||||||
|
callback?: (error: Error | null | string, adaptorInfo?: IFileInfo | null) => void,
|
||||||
|
_options?: unknown,
|
||||||
|
): Promise<void> {
|
||||||
const fileInfo = this.boot.files[title];
|
const fileInfo = this.boot.files[title];
|
||||||
|
|
||||||
if (!fileInfo) {
|
if (!fileInfo) {
|
||||||
|
|
@ -195,11 +229,11 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
||||||
try {
|
try {
|
||||||
const currentWorkspace = await workspace.get(this.workspaceID);
|
const currentWorkspace = await workspace.get(this.workspaceID);
|
||||||
if (currentWorkspace && 'enableFileSystemWatch' in currentWorkspace && !currentWorkspace.enableFileSystemWatch) {
|
if (currentWorkspace && 'enableFileSystemWatch' in currentWorkspace && !currentWorkspace.enableFileSystemWatch) {
|
||||||
this.logger.log('[WATCH_FS_DISABLED] File system watching is disabled for this workspace');
|
this.logger.log('WatchFileSystemAdaptor File system watching is disabled for this workspace');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.alert('[WATCH_FS_ERROR] Failed to check enableFileSystemWatch setting:', error);
|
this.logger.alert('WatchFileSystemAdaptor Failed to check enableFileSystemWatch setting:', error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -225,7 +259,7 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
||||||
{
|
{
|
||||||
debounceMS: 100,
|
debounceMS: 100,
|
||||||
errorCallback: (error) => {
|
errorCallback: (error) => {
|
||||||
this.logger.alert('[WATCH_FS_ERROR] NSFW error:', error);
|
this.logger.alert('WatchFileSystemAdaptor NSFW error:', error);
|
||||||
},
|
},
|
||||||
// Start with base excluded paths
|
// Start with base excluded paths
|
||||||
// @ts-expect-error - nsfw types are incorrect, it accepts string[] not just [string]
|
// @ts-expect-error - nsfw types are incorrect, it accepts string[] not just [string]
|
||||||
|
|
@ -240,7 +274,7 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
||||||
// Log stabilization marker for tests
|
// Log stabilization marker for tests
|
||||||
this.logger.log('[test-id-WATCH_FS_STABILIZED] Watcher has stabilized', { level: 'debug' });
|
this.logger.log('[test-id-WATCH_FS_STABILIZED] Watcher has stabilized', { level: 'debug' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.alert('[WATCH_FS_ERROR] Failed to initialize file watching:', error);
|
this.logger.alert('WatchFileSystemAdaptor Failed to initialize file watching:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -256,7 +290,7 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
||||||
// Get sub-wikis for this main wiki
|
// Get sub-wikis for this main wiki
|
||||||
const subWikis = await workspace.getSubWorkspacesAsList(this.workspaceID);
|
const subWikis = await workspace.getSubWorkspacesAsList(this.workspaceID);
|
||||||
|
|
||||||
this.logger.log(`[WATCH_FS_SUBWIKI] Found ${subWikis.length} sub-wikis to watch`);
|
this.logger.log(`WatchFileSystemAdaptor Found ${subWikis.length} sub-wikis to watch`);
|
||||||
|
|
||||||
// Create watcher for each sub-wiki
|
// Create watcher for each sub-wiki
|
||||||
for (const subWiki of subWikis) {
|
for (const subWiki of subWikis) {
|
||||||
|
|
@ -270,7 +304,7 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
||||||
|
|
||||||
// Check if the path exists before trying to watch
|
// Check if the path exists before trying to watch
|
||||||
if (!fs.existsSync(subWikiPath)) {
|
if (!fs.existsSync(subWikiPath)) {
|
||||||
this.logger.log(`[WATCH_FS_SUBWIKI] Path does not exist for sub-wiki ${subWiki.name}: ${subWikiPath}`);
|
this.logger.log(`WatchFileSystemAdaptor Path does not exist for sub-wiki ${subWiki.name}: ${subWikiPath}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -283,7 +317,7 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
||||||
{
|
{
|
||||||
debounceMS: 100,
|
debounceMS: 100,
|
||||||
errorCallback: (error) => {
|
errorCallback: (error) => {
|
||||||
this.logger.alert(`[WATCH_FS_ERROR] NSFW error for sub-wiki ${subWiki.name}:`, error);
|
this.logger.alert(`WatchFileSystemAdaptor NSFW error for sub-wiki ${subWiki.name}:`, error);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -291,13 +325,13 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
||||||
await subWikiWatcher.start();
|
await subWikiWatcher.start();
|
||||||
this.inverseFilesIndex.registerSubWiki(subWiki.id, subWikiPath, subWikiWatcher);
|
this.inverseFilesIndex.registerSubWiki(subWiki.id, subWikiPath, subWikiWatcher);
|
||||||
|
|
||||||
this.logger.log(`[WATCH_FS_SUBWIKI] Watching sub-wiki: ${subWiki.name} at ${subWikiPath}`);
|
this.logger.log(`WatchFileSystemAdaptor Watching sub-wiki: ${subWiki.name} at ${subWikiPath}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.alert(`[WATCH_FS_ERROR] Failed to watch sub-wiki ${subWiki.name}:`, error);
|
this.logger.alert(`WatchFileSystemAdaptor Failed to watch sub-wiki ${subWiki.name}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.alert('[WATCH_FS_ERROR] Failed to initialize sub-wiki watchers:', error);
|
this.logger.alert('WatchFileSystemAdaptor Failed to initialize sub-wiki watchers:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -322,7 +356,6 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
||||||
* @param absoluteFilePath Absolute file path
|
* @param absoluteFilePath Absolute file path
|
||||||
*/
|
*/
|
||||||
private excludeFile(absoluteFilePath: string): void {
|
private excludeFile(absoluteFilePath: string): void {
|
||||||
this.logger.log(`[WATCH_FS_EXCLUDE] Excluding file: ${absoluteFilePath}`);
|
|
||||||
this.inverseFilesIndex.excludeFile(absoluteFilePath);
|
this.inverseFilesIndex.excludeFile(absoluteFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -339,7 +372,6 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
||||||
}
|
}
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
this.logger.log(`[WATCH_FS_INCLUDE] Including file: ${absoluteFilePath}`);
|
|
||||||
this.inverseFilesIndex.includeFile(absoluteFilePath);
|
this.inverseFilesIndex.includeFile(absoluteFilePath);
|
||||||
this.pendingInclusions.delete(absoluteFilePath);
|
this.pendingInclusions.delete(absoluteFilePath);
|
||||||
// Notify git service when file is included after being saved
|
// Notify git service when file is included after being saved
|
||||||
|
|
@ -359,7 +391,7 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
||||||
if (existingTimer) {
|
if (existingTimer) {
|
||||||
clearTimeout(existingTimer);
|
clearTimeout(existingTimer);
|
||||||
this.pendingDeletions.delete(fileAbsolutePath);
|
this.pendingDeletions.delete(fileAbsolutePath);
|
||||||
this.logger.log(`[WATCH_FS_CANCEL_DELETE] Cancelled pending deletion for: ${fileAbsolutePath}`);
|
this.logger.log(`WatchFileSystemAdaptor Cancelled pending deletion for: ${fileAbsolutePath}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -381,7 +413,6 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
||||||
}, FILE_DELETION_DELAY_MS);
|
}, FILE_DELETION_DELAY_MS);
|
||||||
|
|
||||||
this.pendingDeletions.set(fileAbsolutePath, timer);
|
this.pendingDeletions.set(fileAbsolutePath, timer);
|
||||||
this.logger.log(`[WATCH_FS_SCHEDULE_DELETE] Scheduled deletion for: ${fileAbsolutePath}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -411,7 +442,7 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
||||||
: this.inverseFilesIndex.isMainFileExcluded(fileAbsolutePath);
|
: this.inverseFilesIndex.isMainFileExcluded(fileAbsolutePath);
|
||||||
|
|
||||||
if (isExcluded) {
|
if (isExcluded) {
|
||||||
this.logger.log(`[WATCH_FS_SKIP_EXCLUDED] Skipping excluded file: ${fileAbsolutePath}`);
|
this.logger.log(`WatchFileSystemAdaptor Skipping excluded file: ${fileAbsolutePath}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -428,10 +459,20 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
||||||
const fileMimeType = $tw.utils.getFileExtensionInfo(fileExtension)?.type ?? 'text/vnd.tiddlywiki';
|
const fileMimeType = $tw.utils.getFileExtensionInfo(fileExtension)?.type ?? 'text/vnd.tiddlywiki';
|
||||||
const metaFileAbsolutePath = `${fileAbsolutePath}.meta`;
|
const metaFileAbsolutePath = `${fileAbsolutePath}.meta`;
|
||||||
|
|
||||||
this.logger.log('[WATCH_FS_EVENT]', getActionName(action), fileName, `(directory: ${directory})`);
|
|
||||||
|
|
||||||
// Handle different event types
|
// Handle different event types
|
||||||
if (action === nsfw.actions.CREATED || action === nsfw.actions.MODIFIED) {
|
if (action === nsfw.actions.CREATED || action === nsfw.actions.MODIFIED) {
|
||||||
|
// Skip if it's a directory (nsfw sometimes reports directory changes)
|
||||||
|
try {
|
||||||
|
const stats = fs.statSync(fileAbsolutePath);
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
this.logger.log(`WatchFileSystemAdaptor Skipping directory: ${fileAbsolutePath}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// File might have been deleted already, skip
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Cancel any pending deletion for this file (e.g., git revert scenario)
|
// Cancel any pending deletion for this file (e.g., git revert scenario)
|
||||||
this.cancelPendingDeletion(fileAbsolutePath);
|
this.cancelPendingDeletion(fileAbsolutePath);
|
||||||
|
|
||||||
|
|
@ -508,13 +549,9 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
||||||
// watchPathBase is wiki/tiddlers, but wikiFolderLocation should be wiki
|
// watchPathBase is wiki/tiddlers, but wikiFolderLocation should be wiki
|
||||||
const wikiFolderLocation = path.dirname(this.watchPathBase);
|
const wikiFolderLocation = path.dirname(this.watchPathBase);
|
||||||
try {
|
try {
|
||||||
(git.notifyFileChange as ((path: string, options?: { onlyWhenGitLogOpened?: boolean }) => void))(
|
void git.notifyFileChange(wikiFolderLocation, { onlyWhenGitLogOpened: true });
|
||||||
wikiFolderLocation,
|
|
||||||
{ onlyWhenGitLogOpened: true },
|
|
||||||
);
|
|
||||||
this.logger.log(`[WATCH_FS_GIT_NOTIFY] Notified git service about file changes in ${wikiFolderLocation}`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.alert('[WATCH_FS_GIT_NOTIFY_ERROR] Failed to notify git service:', error);
|
this.logger.alert('WatchFileSystemAdaptor Failed to notify git service:', error);
|
||||||
}
|
}
|
||||||
this.gitNotificationTimer = undefined;
|
this.gitNotificationTimer = undefined;
|
||||||
}, GIT_NOTIFICATION_DELAY_MS);
|
}, GIT_NOTIFICATION_DELAY_MS);
|
||||||
|
|
@ -547,7 +584,7 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
||||||
try {
|
try {
|
||||||
tiddlersDescriptor = $tw.loadTiddlersFromFile(actualFileToLoad);
|
tiddlersDescriptor = $tw.loadTiddlersFromFile(actualFileToLoad);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.alert('[WATCH_FS_LOAD_ERROR] Failed to load file:', actualFileToLoad, error);
|
this.logger.alert('WatchFileSystemAdaptor Failed to load file:', actualFileToLoad, error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -573,7 +610,7 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
||||||
// not wrapped in a .fields property
|
// not wrapped in a .fields property
|
||||||
const tiddlerTitle = tiddler?.title;
|
const tiddlerTitle = tiddler?.title;
|
||||||
if (!tiddlerTitle) {
|
if (!tiddlerTitle) {
|
||||||
this.logger.alert(`[WATCH_FS_ERROR] Tiddler has no title. Tiddler object: ${JSON.stringify(tiddler)}`);
|
this.logger.alert(`WatchFileSystemAdaptor Tiddler has no title. Tiddler object: ${JSON.stringify(tiddler)}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -587,7 +624,8 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
||||||
} as IBootFilesIndexItemWithTitle);
|
} as IBootFilesIndexItemWithTitle);
|
||||||
|
|
||||||
// Add tiddler to wiki (this will update if it exists or add if new)
|
// Add tiddler to wiki (this will update if it exists or add if new)
|
||||||
$tw.syncadaptor!.wiki.addTiddler(tiddler);
|
const syncAdaptor = $tw.syncadaptor as { wiki: Wiki } | undefined | null;
|
||||||
|
syncAdaptor?.wiki.addTiddler(tiddler);
|
||||||
|
|
||||||
// Log appropriate event
|
// Log appropriate event
|
||||||
if (isNewFile) {
|
if (isNewFile) {
|
||||||
|
|
@ -625,7 +663,8 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if tiddler exists in wiki before trying to delete
|
// Check if tiddler exists in wiki before trying to delete
|
||||||
if (!$tw.syncadaptor!.wiki.tiddlerExists(tiddlerTitle)) {
|
const syncAdaptor = $tw.syncadaptor as { wiki: Wiki } | undefined | null;
|
||||||
|
if (!syncAdaptor?.wiki.tiddlerExists(tiddlerTitle)) {
|
||||||
// Tiddler doesn't exist in wiki, nothing to delete
|
// Tiddler doesn't exist in wiki, nothing to delete
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -634,7 +673,7 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
||||||
this.removeTiddlerFileInfo(tiddlerTitle);
|
this.removeTiddlerFileInfo(tiddlerTitle);
|
||||||
|
|
||||||
// Delete the tiddler from wiki to trigger change event
|
// Delete the tiddler from wiki to trigger change event
|
||||||
$tw.syncadaptor!.wiki.deleteTiddler(tiddlerTitle);
|
syncAdaptor?.wiki.deleteTiddler(tiddlerTitle);
|
||||||
this.logger.log(`[test-id-WATCH_FS_TIDDLER_DELETED] ${tiddlerTitle}`, { level: 'debug' });
|
this.logger.log(`[test-id-WATCH_FS_TIDDLER_DELETED] ${tiddlerTitle}`, { level: 'debug' });
|
||||||
|
|
||||||
// Delete system tiddler empty file if exists
|
// Delete system tiddler empty file if exists
|
||||||
|
|
@ -677,15 +716,15 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
||||||
this.pendingInclusions.clear();
|
this.pendingInclusions.clear();
|
||||||
|
|
||||||
if (this.watcher) {
|
if (this.watcher) {
|
||||||
this.logger.log('[WATCH_FS_CLEANUP] Closing filesystem watcher');
|
this.logger.log('WatchFileSystemAdaptor Closing filesystem watcher');
|
||||||
await this.watcher.stop();
|
await this.watcher.stop();
|
||||||
this.watcher = undefined;
|
this.watcher = undefined;
|
||||||
this.logger.log('[WATCH_FS_CLEANUP] Filesystem watcher closed');
|
this.logger.log('WatchFileSystemAdaptor Filesystem watcher closed');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close all sub-wiki watchers
|
// Close all sub-wiki watchers
|
||||||
for (const subWiki of this.inverseFilesIndex.getSubWikis()) {
|
for (const subWiki of this.inverseFilesIndex.getSubWikis()) {
|
||||||
this.logger.log(`[WATCH_FS_CLEANUP] Closing sub-wiki watcher: ${subWiki.id}`);
|
this.logger.log(`WatchFileSystemAdaptor Closing sub-wiki watcher: ${subWiki.id}`);
|
||||||
await subWiki.watcher.stop();
|
await subWiki.watcher.stop();
|
||||||
this.inverseFilesIndex.unregisterSubWiki(subWiki.id);
|
this.inverseFilesIndex.unregisterSubWiki(subWiki.id);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,3 @@
|
||||||
import nsfw from 'nsfw';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get human-readable action name from nsfw action code
|
|
||||||
*/
|
|
||||||
export function getActionName(action: number): string {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
||||||
if (action === nsfw.actions.CREATED) {
|
|
||||||
return 'add';
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
||||||
if (action === nsfw.actions.DELETED) {
|
|
||||||
return 'unlink';
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
||||||
if (action === nsfw.actions.MODIFIED) {
|
|
||||||
return 'change';
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
||||||
if (action === nsfw.actions.RENAMED) {
|
|
||||||
return 'rename';
|
|
||||||
}
|
|
||||||
return 'unknown';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if error is a file lock error that should be retried
|
* Check if error is a file lock error that should be retried
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ import path from 'path';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
import type { IWikiWorkspace } from '@services/workspaces/interface';
|
import type { IWikiWorkspace } from '@services/workspaces/interface';
|
||||||
import type { SyncAdaptor } from 'tiddlywiki';
|
|
||||||
import { IZxWorkerMessage, ZxWorkerControlActions } from '../interface';
|
import { IZxWorkerMessage, ZxWorkerControlActions } from '../interface';
|
||||||
import { executeScriptInTWContext, executeScriptInZxScriptContext, extractTWContextScripts, type IVariableContextList } from '../plugin/zxPlugin';
|
import { executeScriptInTWContext, executeScriptInZxScriptContext, extractTWContextScripts, type IVariableContextList } from '../plugin/zxPlugin';
|
||||||
import { wikiOperationsInWikiWorker } from '../wikiOperations/executor/wikiOperationInServer';
|
import { wikiOperationsInWikiWorker } from '../wikiOperations/executor/wikiOperationInServer';
|
||||||
|
|
@ -101,7 +100,7 @@ async function beforeExit(): Promise<void> {
|
||||||
// Cleanup watch-filesystem adaptor
|
// Cleanup watch-filesystem adaptor
|
||||||
const wikiInstance = getWikiInstance();
|
const wikiInstance = getWikiInstance();
|
||||||
// Call our custom cleanup method if it exists `src/services/wiki/plugin/watchFileSystemAdaptor/watch-filesystem-adaptor.ts`
|
// Call our custom cleanup method if it exists `src/services/wiki/plugin/watchFileSystemAdaptor/watch-filesystem-adaptor.ts`
|
||||||
const syncAdaptor = wikiInstance?.syncadaptor as SyncAdaptor & { cleanup?: () => Promise<void> };
|
const syncAdaptor = wikiInstance?.syncadaptor as { cleanup?: () => Promise<void> } | undefined;
|
||||||
if (syncAdaptor?.cleanup) {
|
if (syncAdaptor?.cleanup) {
|
||||||
await syncAdaptor.cleanup();
|
await syncAdaptor.cleanup();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { defaultServerIP } from '@/constants/urls';
|
||||||
import { DARK_LIGHT_CHANGE_ACTIONS_TAG } from '@services/theme/interface';
|
import { DARK_LIGHT_CHANGE_ACTIONS_TAG } from '@services/theme/interface';
|
||||||
import intercept from 'intercept-stdout';
|
import intercept from 'intercept-stdout';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
import type { Server } from 'node:http';
|
||||||
import inspector from 'node:inspector';
|
import inspector from 'node:inspector';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
@ -180,8 +181,8 @@ export function startNodeJSWiki({
|
||||||
: [homePath, '--version'];
|
: [homePath, '--version'];
|
||||||
wikiInstance.boot.argv = [...fullBootArgv];
|
wikiInstance.boot.argv = [...fullBootArgv];
|
||||||
|
|
||||||
wikiInstance.hooks.addHook('th-server-command-post-start', function(_listenCommand, server) {
|
wikiInstance.hooks.addHook('th-server-command-post-start', function(_server: unknown, nodeServer: Server) {
|
||||||
server.on('error', function(error: Error) {
|
nodeServer.on('error', function(error: Error) {
|
||||||
observer.next({ type: 'control', actions: WikiControlActions.error, message: error.message, argv: fullBootArgv });
|
observer.next({ type: 'control', actions: WikiControlActions.error, message: error.message, argv: fullBootArgv });
|
||||||
});
|
});
|
||||||
// Similar to how updateActiveWikiTheme calls WikiChannel.invokeActionsByTag
|
// Similar to how updateActiveWikiTheme calls WikiChannel.invokeActionsByTag
|
||||||
|
|
@ -189,7 +190,7 @@ export function startNodeJSWiki({
|
||||||
wikiInstance.rootWidget.invokeActionsByTag(DARK_LIGHT_CHANGE_ACTIONS_TAG, new Event('TidGi-invokeActionByTag') as unknown as IWidgetEvent, {
|
wikiInstance.rootWidget.invokeActionsByTag(DARK_LIGHT_CHANGE_ACTIONS_TAG, new Event('TidGi-invokeActionByTag') as unknown as IWidgetEvent, {
|
||||||
'dark-mode': shouldUseDarkColors ? 'yes' : 'no',
|
'dark-mode': shouldUseDarkColors ? 'yes' : 'no',
|
||||||
});
|
});
|
||||||
server.on('listening', function() {
|
nodeServer.on('listening', function() {
|
||||||
observer.next({
|
observer.next({
|
||||||
type: 'control',
|
type: 'control',
|
||||||
actions: WikiControlActions.listening,
|
actions: WikiControlActions.listening,
|
||||||
|
|
|
||||||
|
|
@ -207,7 +207,7 @@ export class Window implements IWindowService {
|
||||||
...(hideTitleBar && process.platform !== 'darwin' ? { titleBarOverlay: true } : {}),
|
...(hideTitleBar && process.platform !== 'darwin' ? { titleBarOverlay: true } : {}),
|
||||||
alwaysOnTop: windowName === WindowNames.tidgiMiniWindow ? tidgiMiniWindowAlwaysOnTop : alwaysOnTop,
|
alwaysOnTop: windowName === WindowNames.tidgiMiniWindow ? tidgiMiniWindowAlwaysOnTop : alwaysOnTop,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
devTools: !isTest,
|
devTools: true, // Always enable devTools, even in test mode for debugging
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
webSecurity: false,
|
webSecurity: false,
|
||||||
allowRunningInsecureContent: true,
|
allowRunningInsecureContent: true,
|
||||||
|
|
|
||||||
|
|
@ -290,7 +290,7 @@ export default function EditWorkspace(): React.JSX.Element {
|
||||||
// Show error notification
|
// Show error notification
|
||||||
void window.service.notification.show({
|
void window.service.notification.show({
|
||||||
title: t('EditWorkspace.MoveWorkspaceFailed'),
|
title: t('EditWorkspace.MoveWorkspaceFailed'),
|
||||||
body: t('EditWorkspace.MoveWorkspaceFailedMessage', { name: workspaceName, error: errorMessage }),
|
body: t('EditWorkspace.MoveWorkspaceFailedMessage', { name: workspace.name, error: errorMessage }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue