mirror of
https://github.com/tiddly-gittly/TidGi-Desktop.git
synced 2025-12-05 18:20:39 -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']`
|
||||
|
||||
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
|
||||
|
||||
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']"
|
||||
# Advance to step 2 (Edit Prompt)
|
||||
When I click on a "next button" element with selector "[data-testid='next-button']"
|
||||
# Step 4: Verify second step content (Edit Prompt)
|
||||
And I should see a "edit prompt title" element with selector "*:has-text('编辑提示词')"
|
||||
# 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 "h6:has-text('编辑提示词')"
|
||||
# Step 4.1: Wait for PromptConfigForm to load
|
||||
# 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']"
|
||||
|
|
@ -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]) 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])"
|
||||
# Wait for form content save to backend
|
||||
And I wait for 0.2 seconds
|
||||
And I wait for 0.2 seconds for "form content save to backend"
|
||||
# Step 5: Advance to step 3 (Immediate Use)
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
const currentWindow = this.currentWindow;
|
||||
|
||||
try {
|
||||
await currentWindow?.waitForSelector(selector, { timeout: 10000 });
|
||||
const isVisible = await currentWindow?.isVisible(selector);
|
||||
|
|
|
|||
82
package.json
82
package.json
|
|
@ -2,7 +2,7 @@
|
|||
"name": "tidgi",
|
||||
"productName": "TidGi",
|
||||
"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",
|
||||
"packageManager": "pnpm@10.18.2",
|
||||
"scripts": {
|
||||
|
|
@ -33,10 +33,10 @@
|
|||
"author": "Lin Onetwo <linonetwo012@gmail.com>, Quang Lam <quang.lam2807@gmail.com>",
|
||||
"main": ".vite/build/main.js",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^2.0.35",
|
||||
"@ai-sdk/deepseek": "^1.0.23",
|
||||
"@ai-sdk/openai": "^2.0.53",
|
||||
"@ai-sdk/openai-compatible": "^1.0.22",
|
||||
"@ai-sdk/anthropic": "^2.0.45",
|
||||
"@ai-sdk/deepseek": "^1.0.29",
|
||||
"@ai-sdk/openai": "^2.0.71",
|
||||
"@ai-sdk/openai-compatible": "^1.0.27",
|
||||
"@algolia/autocomplete-js": "^1.19.4",
|
||||
"@algolia/autocomplete-theme-classic": "^1.19.4",
|
||||
"@dnd-kit/core": "6.3.1",
|
||||
|
|
@ -46,75 +46,75 @@
|
|||
"@dr.pogodin/react-helmet": "^3.0.4",
|
||||
"@fontsource/roboto": "^5.2.8",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@mui/icons-material": "^7.3.4",
|
||||
"@mui/material": "^7.3.4",
|
||||
"@mui/system": "^7.3.3",
|
||||
"@mui/types": "^7.4.7",
|
||||
"@mui/x-date-pickers": "^8.14.1",
|
||||
"@rjsf/core": "6.0.0-beta.8",
|
||||
"@rjsf/mui": "6.0.0-beta.10",
|
||||
"@rjsf/utils": "6.0.0-beta.10",
|
||||
"@rjsf/validator-ajv8": "6.0.0-beta.8",
|
||||
"@mui/icons-material": "^7.3.5",
|
||||
"@mui/material": "^7.3.5",
|
||||
"@mui/system": "^7.3.5",
|
||||
"@mui/types": "^7.4.8",
|
||||
"@mui/x-date-pickers": "^8.19.0",
|
||||
"@rjsf/core": "6.1.2",
|
||||
"@rjsf/mui": "6.1.2",
|
||||
"@rjsf/utils": "6.1.2",
|
||||
"@rjsf/validator-ajv8": "6.1.2",
|
||||
"@tomplum/react-git-log": "^3.5.0",
|
||||
"ai": "^5.0.76",
|
||||
"ai": "^5.0.98",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"app-path": "^4.0.0",
|
||||
"beautiful-react-hooks": "5.0.3",
|
||||
"best-effort-json-parser": "1.2.1",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"better-sqlite3": "^12.4.5",
|
||||
"bluebird": "3.7.2",
|
||||
"date-fns": "3.6.0",
|
||||
"default-gateway": "6.0.3",
|
||||
"dugite": "3.0.0-rc12",
|
||||
"dugite": "3.0.0",
|
||||
"electron-dl": "^4.0.0",
|
||||
"electron-ipc-cat": "2.2.3",
|
||||
"electron-settings": "5.0.0",
|
||||
"electron-unhandled": "4.0.1",
|
||||
"electron-window-state": "5.0.3",
|
||||
"espree": "^10.4.0",
|
||||
"espree": "^11.0.0",
|
||||
"exponential-backoff": "^3.1.3",
|
||||
"fs-extra": "11.3.2",
|
||||
"git-sync-js": "^2.2.1",
|
||||
"git-sync-js": "^2.3.0",
|
||||
"graphql-hooks": "8.2.0",
|
||||
"html-minifier-terser": "^7.2.0",
|
||||
"i18next": "25.6.0",
|
||||
"i18next": "25.6.3",
|
||||
"i18next-electron-fs-backend": "3.0.3",
|
||||
"i18next-fs-backend": "2.6.0",
|
||||
"immer": "^10.1.3",
|
||||
"i18next-fs-backend": "2.6.1",
|
||||
"immer": "^10.2.0",
|
||||
"intercept-stdout": "0.1.2",
|
||||
"inversify": "7.10.3",
|
||||
"inversify": "7.10.4",
|
||||
"ipaddr.js": "2.2.0",
|
||||
"jimp": "1.6.0",
|
||||
"json5": "^2.2.3",
|
||||
"lodash": "4.17.21",
|
||||
"material-ui-popup-state": "^5.3.6",
|
||||
"menubar": "9.5.2",
|
||||
"monaco-editor": "^0.54.0",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"nanoid": "^5.1.6",
|
||||
"new-github-issue-url": "^1.1.0",
|
||||
"node-fetch": "3.3.2",
|
||||
"nsfw": "^2.2.5",
|
||||
"oidc-client-ts": "^3.3.0",
|
||||
"ollama-ai-provider-v2": "^1.5.1",
|
||||
"oidc-client-ts": "^3.4.1",
|
||||
"ollama-ai-provider-v2": "^1.5.5",
|
||||
"react": "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-window": "^2.2.1",
|
||||
"react-window": "^2.2.3",
|
||||
"reflect-metadata": "0.2.2",
|
||||
"registry-js": "1.16.1",
|
||||
"rotating-file-stream": "^3.2.7",
|
||||
"rxjs": "7.8.2",
|
||||
"semver": "7.7.3",
|
||||
"serialize-error": "^12.0.0",
|
||||
"simplebar": "6.3.2",
|
||||
"simplebar": "6.3.3",
|
||||
"simplebar-react": "3.3.2",
|
||||
"source-map-support": "0.5.21",
|
||||
"sqlite-vec": "0.1.7-alpha.2",
|
||||
"strip-ansi": "^7.1.2",
|
||||
"tapable": "^2.3.0",
|
||||
"tiddlywiki": "5.3.8",
|
||||
"type-fest": "5.1.0",
|
||||
"type-fest": "5.2.0",
|
||||
"typeorm": "^0.3.27",
|
||||
"typescript-styled-is": "^2.1.0",
|
||||
"v8-compile-cache-lib": "^3.0.1",
|
||||
|
|
@ -153,37 +153,37 @@
|
|||
"@types/html-minifier-terser": "^7.0.2",
|
||||
"@types/intercept-stdout": "0.1.4",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/node": "24.9.1",
|
||||
"@types/react": "19.2.2",
|
||||
"@types/react-dom": "19.2.2",
|
||||
"@types/node": "24.10.1",
|
||||
"@types/react": "19.2.6",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@types/react-jsonschema-form": "^1.7.13",
|
||||
"@types/semver": "7.7.1",
|
||||
"@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/ui": "^3.2.4",
|
||||
"chai": "6.2.0",
|
||||
"cross-env": "10.1.0",
|
||||
"dprint": "^0.50.2",
|
||||
"electron": "38.3.0",
|
||||
"electron": "39.2.3",
|
||||
"electron-chrome-web-store": "^0.13.0",
|
||||
"esbuild": "^0.25.11",
|
||||
"esbuild": "^0.27.0",
|
||||
"eslint-config-tidgi": "^2.2.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jsdom": "^27.0.1",
|
||||
"jsdom": "^27.2.0",
|
||||
"memory-fs": "^0.5.0",
|
||||
"node-loader": "2.1.0",
|
||||
"oauth2-mock-server": "^8.1.0",
|
||||
"oauth2-mock-server": "^8.2.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"playwright": "^1.56.1",
|
||||
"rimraf": "^6.0.1",
|
||||
"rimraf": "^6.1.2",
|
||||
"ts-node": "10.9.2",
|
||||
"tsx": "^4.20.6",
|
||||
"tw5-typed": "^0.6.8",
|
||||
"tw5-typed": "^1.0.5",
|
||||
"typescript": "5.9.3",
|
||||
"typesync": "0.14.3",
|
||||
"unplugin-swc": "^1.5.8",
|
||||
"vite": "^7.1.11",
|
||||
"vite": "^7.2.4",
|
||||
"vite-bundle-analyzer": "^1.2.3",
|
||||
"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]);
|
||||
|
||||
// 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(() => {
|
||||
setCurrentStep(tab.currentStep ?? 0);
|
||||
}, [tab.currentStep]);
|
||||
if (tab.currentStep !== undefined) {
|
||||
setCurrentStep(tab.currentStep);
|
||||
}
|
||||
}, []); // Only run once on mount
|
||||
|
||||
// Cleanup when component unmounts or tab closes
|
||||
useEffect(() => {
|
||||
|
|
@ -359,26 +362,26 @@ export const CreateNewAgentContent: React.FC<CreateNewAgentContentProps> = ({ ta
|
|||
case 'editPrompt':
|
||||
return (
|
||||
<StepContainer>
|
||||
<Typography variant='h6' gutterBottom>
|
||||
<Typography variant='h6' gutterBottom data-testid='edit-prompt-title'>
|
||||
{t('CreateAgent.EditPrompt')}
|
||||
</Typography>
|
||||
<Typography variant='body2' color='text.secondary' gutterBottom>
|
||||
{t('CreateAgent.EditPromptDescription')}
|
||||
</Typography>
|
||||
|
||||
{temporaryAgentDefinition
|
||||
{temporaryAgentDefinition && promptSchema
|
||||
? (
|
||||
<Box sx={{ mt: 2, height: 400, overflow: 'auto' }}>
|
||||
<PromptConfigForm
|
||||
schema={promptSchema || undefined}
|
||||
formData={temporaryAgentDefinition.handlerConfig as HandlerConfig}
|
||||
schema={promptSchema}
|
||||
formData={(temporaryAgentDefinition.handlerConfig || {}) as HandlerConfig}
|
||||
onChange={(updatedConfig) => {
|
||||
void handleAgentDefinitionChange({
|
||||
...temporaryAgentDefinition,
|
||||
handlerConfig: updatedConfig as Record<string, unknown>,
|
||||
});
|
||||
}}
|
||||
loading={!promptSchema}
|
||||
loading={false}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -219,22 +219,27 @@ describe('CreateNewAgentContent', () => {
|
|||
it('should show correct step content based on currentStep', () => {
|
||||
// Test step 1 (currentStep: 0) - Setup Agent (name + template)
|
||||
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.getByLabelText('智能体名称')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('template-search-input')).toBeInTheDocument();
|
||||
|
||||
// Clean up before next render
|
||||
unmount();
|
||||
|
||||
// Test step 2 (currentStep: 1) - Edit Prompt
|
||||
const step2Tab = { ...mockTab, currentStep: 1 };
|
||||
rerender(<TestComponent tab={step2Tab} />);
|
||||
const { unmount: unmount2 } = render(<TestComponent tab={step2Tab} />);
|
||||
|
||||
// Should show editPrompt placeholder when no template selected
|
||||
expect(screen.getByText('请先选择一个模板')).toBeInTheDocument();
|
||||
|
||||
unmount2();
|
||||
|
||||
// Test step 3 (currentStep: 2) - Immediate Use
|
||||
const step3Tab = { ...mockTab, currentStep: 2 };
|
||||
rerender(<TestComponent tab={step3Tab} />);
|
||||
render(<TestComponent tab={step3Tab} />);
|
||||
|
||||
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 './StyledCard';
|
||||
export * from './StyledCollapsible';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { FieldProps } from '@rjsf/utils';
|
||||
import type { FieldProps } from '@rjsf/utils';
|
||||
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.
|
||||
|
|
@ -12,7 +12,7 @@ import { ConditionalFieldConfig, ExtendedFormContext } from '../index';
|
|||
* 4. Uses useMemo to prevent unnecessary recalculations
|
||||
*/
|
||||
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;
|
||||
|
||||
|
|
@ -28,8 +28,10 @@ export const ConditionalField: React.FC<FieldProps> = (props) => {
|
|||
if (!rootFormData) return true;
|
||||
|
||||
// Parse the field's path to find its parent object where sibling fields are located
|
||||
const fieldPath = idSchema.$id.replace(/^root_/, '');
|
||||
const pathParts = fieldPath.split('_');
|
||||
// In RJSF 6.x, fieldPathId.$id is a string that contains the path
|
||||
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
|
||||
|
||||
// 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
|
||||
return hideWhen ? !conditionMet : conditionMet;
|
||||
}, [condition, registry.formContext, idSchema.$id]);
|
||||
}, [condition, registry.formContext, fieldPathId?.$id]);
|
||||
|
||||
// Hidden fields return nothing
|
||||
if (!shouldShow) {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { ErrorDisplay } from './components/ErrorDisplay';
|
|||
import { ArrayItemProvider } from './context/ArrayItemContext';
|
||||
import { useDefaultUiSchema } from './defaultUiSchema';
|
||||
import { fields } from './fields';
|
||||
import { ArrayFieldTemplate, FieldTemplate, ObjectFieldTemplate, RootObjectFieldTemplate } from './templates';
|
||||
import { ArrayFieldItemTemplate, ArrayFieldTemplate, FieldTemplate, ObjectFieldTemplate, RootObjectFieldTemplate } from './templates';
|
||||
import { widgets } from './widgets';
|
||||
|
||||
/**
|
||||
|
|
@ -67,7 +67,7 @@ export const PromptConfigForm: React.FC<PromptConfigFormProps> = ({
|
|||
|
||||
const templates = useMemo(() => {
|
||||
const rootObjectFieldTemplate = (props: ObjectFieldTemplateProps) => {
|
||||
const isRootLevel = props.idSchema.$id === 'root';
|
||||
const isRootLevel = props.fieldPathId?.$id === 'root';
|
||||
return isRootLevel
|
||||
? <RootObjectFieldTemplate {...props} />
|
||||
: <ObjectFieldTemplate {...props} />;
|
||||
|
|
@ -75,6 +75,8 @@ export const PromptConfigForm: React.FC<PromptConfigFormProps> = ({
|
|||
|
||||
return {
|
||||
ArrayFieldTemplate,
|
||||
|
||||
ArrayFieldItemTemplate,
|
||||
FieldTemplate,
|
||||
ObjectFieldTemplate: rootObjectFieldTemplate,
|
||||
};
|
||||
|
|
@ -145,7 +147,7 @@ export const PromptConfigForm: React.FC<PromptConfigFormProps> = ({
|
|||
widgets={widgets}
|
||||
fields={fields}
|
||||
showErrorList={false}
|
||||
liveValidate
|
||||
liveValidate='onChange'
|
||||
noHtml5Validate
|
||||
>
|
||||
<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 { ArrayFieldTemplateProps } from '@rjsf/utils';
|
||||
import React from 'react';
|
||||
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) => {
|
||||
const { items, onAddClick, canAdd, title, schema, formData } = props;
|
||||
const { items, onAddClick, canAdd, title, schema } = props;
|
||||
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 itemIds = items.map((item) => item.key);
|
||||
const isItemsCollapsible = true;
|
||||
|
||||
return (
|
||||
<ArrayContainer>
|
||||
|
|
@ -64,26 +43,9 @@ export const ArrayFieldTemplate: React.FC<ArrayFieldTemplateProps> = (props) =>
|
|||
</EmptyState>
|
||||
)
|
||||
: (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
modifiers={[restrictToVerticalAxis]}
|
||||
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>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
{items}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{canAdd && (
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ export const RootObjectFieldTemplate: React.FC<ObjectFieldTemplateProps> = (prop
|
|||
const { properties, schema } = props;
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const { t } = useTranslation('agent');
|
||||
|
||||
const { formFieldsToScrollTo } = useAgentChatStore(
|
||||
useShallow((state) => ({
|
||||
formFieldsToScrollTo: state.formFieldsToScrollTo,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export { ArrayFieldItemTemplate } from './ArrayFieldItemTemplate';
|
||||
export { ArrayFieldTemplate } from './ArrayFieldTemplate';
|
||||
export { FieldTemplate } from './FieldTemplate';
|
||||
export { ObjectFieldTemplate } from './ObjectFieldTemplate';
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
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 { useTranslation } from 'react-i18next';
|
||||
import is, { isNot } from 'typescript-styled-is';
|
||||
import { Route, Switch } from 'wouter';
|
||||
|
||||
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;
|
||||
display: flex;
|
||||
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%;
|
||||
`;
|
||||
|
||||
${
|
||||
$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 {
|
||||
const { t } = useTranslation();
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { WindowNames } from '@services/windows/WindowProperties';
|
|||
import { browserViewMetaData } from './common/browserViewMetaData';
|
||||
import './view';
|
||||
import { syncTidgiStateWhenWikiLoads } from './appState';
|
||||
import { consoleLogToLogFile } from './fixer/consoleLogToLogFile';
|
||||
import { fixAlertConfirm } from './fixer/fixAlertConfirm';
|
||||
|
||||
declare global {
|
||||
|
|
@ -26,6 +27,9 @@ declare global {
|
|||
|
||||
switch (browserViewMetaData.windowName) {
|
||||
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.
|
||||
* @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 'electron-ipc-cat/fixContextIsolation';
|
||||
import { useHashLocation } from 'wouter/use-hash-location';
|
||||
import { ErrorBoundary } from './components/ErrorBoundary';
|
||||
import { RootStyle } from './components/RootStyle';
|
||||
import { initTestKeyboardShortcutFallback } from './helpers/testKeyboardShortcuts';
|
||||
import { Pages } from './windows';
|
||||
|
|
@ -33,22 +34,24 @@ function App(): JSX.Element {
|
|||
|
||||
return (
|
||||
<StrictMode>
|
||||
<ThemeProvider theme={theme?.shouldUseDarkColors === true ? darkTheme : lightTheme}>
|
||||
<StyledEngineProvider injectFirst>
|
||||
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
||||
<CssBaseline />
|
||||
<Suspense fallback={<div />}>
|
||||
<I18nextProvider i18n={i18next}>
|
||||
<RootStyle>
|
||||
<Router hook={useHashLocation}>
|
||||
<Pages />
|
||||
</Router>
|
||||
</RootStyle>
|
||||
</I18nextProvider>
|
||||
</Suspense>
|
||||
</LocalizationProvider>
|
||||
</StyledEngineProvider>
|
||||
</ThemeProvider>
|
||||
<ErrorBoundary>
|
||||
<ThemeProvider theme={theme?.shouldUseDarkColors === true ? darkTheme : lightTheme}>
|
||||
<StyledEngineProvider injectFirst>
|
||||
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
||||
<CssBaseline />
|
||||
<Suspense fallback={<div />}>
|
||||
<I18nextProvider i18n={i18next}>
|
||||
<RootStyle>
|
||||
<Router hook={useHashLocation}>
|
||||
<Pages />
|
||||
</Router>
|
||||
</RootStyle>
|
||||
</I18nextProvider>
|
||||
</Suspense>
|
||||
</LocalizationProvider>
|
||||
</StyledEngineProvider>
|
||||
</ThemeProvider>
|
||||
</ErrorBoundary>
|
||||
</StrictMode>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,12 @@ const labeledLoggers = new Map<string, winston.Logger>();
|
|||
* @returns A winston logger instance for the specified label
|
||||
*/
|
||||
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);
|
||||
if (existingLogger) {
|
||||
return existingLogger;
|
||||
|
|
|
|||
|
|
@ -4,12 +4,10 @@ import type { IWikiWorkspace, IWorkspace } from '@services/workspaces/interface'
|
|||
import { backOff } from 'exponential-backoff';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import type { FileInfo } from 'tiddlywiki';
|
||||
import type { IFileInfo } from 'tiddlywiki';
|
||||
import type { Tiddler, Wiki } from 'tiddlywiki';
|
||||
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.
|
||||
* This class can be used standalone or extended for additional functionality like file watching.
|
||||
|
|
@ -110,7 +108,7 @@ export class FileSystemAdaptor {
|
|||
return true;
|
||||
}
|
||||
|
||||
getTiddlerInfo(tiddler: Tiddler): FileInfo | undefined {
|
||||
getTiddlerInfo(tiddler: Tiddler): IFileInfo | undefined {
|
||||
const title = tiddler.fields.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.
|
||||
* 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) {
|
||||
throw new Error('filesystem adaptor requires a valid wiki folder');
|
||||
}
|
||||
|
|
@ -165,7 +163,7 @@ export class FileSystemAdaptor {
|
|||
* Generate file info for sub-wiki directory
|
||||
* 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;
|
||||
|
||||
// Resolve symlinks to ensure consistent path handling across platforms
|
||||
|
|
@ -186,14 +184,14 @@ export class FileSystemAdaptor {
|
|||
pathFilters: undefined,
|
||||
extFilters: this.extensionFilters,
|
||||
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
|
||||
*/
|
||||
protected generateDefaultFileInfo(tiddler: Tiddler, fileInfo: FileInfo | undefined): FileInfo {
|
||||
protected generateDefaultFileInfo(tiddler: Tiddler, fileInfo: IFileInfo | undefined): IFileInfo {
|
||||
let pathFilters: string[] | undefined;
|
||||
|
||||
if (this.wiki.tiddlerExists('$:/config/FileSystemPaths')) {
|
||||
|
|
@ -206,7 +204,7 @@ export class FileSystemAdaptor {
|
|||
pathFilters,
|
||||
extFilters: this.extensionFilters,
|
||||
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
|
||||
* 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 {
|
||||
const fileInfo = await this.getTiddlerFileInfo(tiddler);
|
||||
|
||||
|
|
@ -240,7 +242,7 @@ export class FileSystemAdaptor {
|
|||
bootInfo: savedFileInfo, // New file info to be kept
|
||||
title: tiddler.fields.title,
|
||||
};
|
||||
$tw.utils.cleanupTiddlerFiles(cleanupOptions, (cleanupError: Error | null, _cleanedFileInfo?: FileInfo) => {
|
||||
$tw.utils.cleanupTiddlerFiles(cleanupOptions, (cleanupError: Error | null, _cleanedFileInfo?: IFileInfo) => {
|
||||
if (cleanupError) {
|
||||
reject(cleanupError);
|
||||
return;
|
||||
|
|
@ -260,7 +262,10 @@ export class FileSystemAdaptor {
|
|||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
|
|
@ -268,7 +273,11 @@ export class FileSystemAdaptor {
|
|||
* Delete a tiddler from the filesystem
|
||||
* 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];
|
||||
|
||||
if (!fileInfo) {
|
||||
|
|
@ -278,7 +287,7 @@ export class FileSystemAdaptor {
|
|||
|
||||
try {
|
||||
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) {
|
||||
const errorCode = (error as NodeJS.ErrnoException).code;
|
||||
const errorSyscall = (error as NodeJS.ErrnoException).syscall;
|
||||
|
|
@ -339,9 +348,9 @@ export class FileSystemAdaptor {
|
|||
*/
|
||||
protected async saveTiddlerWithRetry(
|
||||
tiddler: Tiddler,
|
||||
fileInfo: FileInfo,
|
||||
fileInfo: IFileInfo,
|
||||
options: { maxRetries?: number; initialDelay?: number; maxDelay?: number } = {},
|
||||
): Promise<FileInfo> {
|
||||
): Promise<IFileInfo> {
|
||||
const maxRetries = options.maxRetries ?? 10;
|
||||
const initialDelay = options.initialDelay ?? 50;
|
||||
const maxDelay = options.maxDelay ?? 2000;
|
||||
|
|
@ -349,8 +358,8 @@ export class FileSystemAdaptor {
|
|||
try {
|
||||
return await backOff(
|
||||
async () => {
|
||||
return await new Promise<FileInfo>((resolve, reject) => {
|
||||
$tw.utils.saveTiddlerToFile(tiddler, fileInfo, (saveError: Error | null, savedFileInfo?: FileInfo) => {
|
||||
return await new Promise<IFileInfo>((resolve, reject) => {
|
||||
$tw.utils.saveTiddlerToFile(tiddler, fileInfo, (saveError: Error | null, savedFileInfo?: IFileInfo) => {
|
||||
if (saveError) {
|
||||
reject(saveError);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import type nsfw from 'nsfw';
|
||||
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 {
|
||||
id: string;
|
||||
|
|
|
|||
|
|
@ -2,10 +2,9 @@ import { git, workspace } from '@services/wiki/wikiWorker/services';
|
|||
import fs from 'fs';
|
||||
import nsfw from 'nsfw';
|
||||
import path from 'path';
|
||||
import type { Tiddler, Wiki } from 'tiddlywiki';
|
||||
import { FileSystemAdaptor, type IFileSystemAdaptorCallback } from './FileSystemAdaptor';
|
||||
import type { IFileInfo, Tiddler, Wiki } from 'tiddlywiki';
|
||||
import { FileSystemAdaptor } from './FileSystemAdaptor';
|
||||
import { type IBootFilesIndexItemWithTitle, InverseFilesIndex } from './InverseFilesIndex';
|
||||
import { getActionName } from './utilities';
|
||||
|
||||
/**
|
||||
* 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)
|
||||
* 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 {
|
||||
// Get existing file info (if tiddler already exists on disk)
|
||||
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)
|
||||
if (oldFileInfo) {
|
||||
this.excludeFile(oldFileInfo.filepath);
|
||||
this.logger.log(`[WATCH_FS_SAVE] Excluded existing file: ${oldFileInfo.filepath}`);
|
||||
} else {
|
||||
// For new tiddlers, we can't pre-exclude them since we don't know the path yet
|
||||
this.logger.log(`[WATCH_FS_NEW_TIDDLER] Saving new tiddler: ${tiddler.fields.title}`);
|
||||
// Also exclude the .meta file if it exists
|
||||
const metaFilePath = `${oldFileInfo.filepath}.meta`;
|
||||
this.excludeFile(metaFilePath);
|
||||
}
|
||||
|
||||
// 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
|
||||
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
|
||||
// The old file should be deleted by now via cleanupTiddlerFiles
|
||||
if (oldFileInfo && oldFileInfo.filepath !== finalFileInfo.filepath) {
|
||||
this.scheduleFileInclusion(oldFileInfo.filepath);
|
||||
this.scheduleFileInclusion(`${oldFileInfo.filepath}.meta`);
|
||||
}
|
||||
} catch (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)
|
||||
* 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];
|
||||
|
||||
if (!fileInfo) {
|
||||
|
|
@ -195,11 +229,11 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
|||
try {
|
||||
const currentWorkspace = await workspace.get(this.workspaceID);
|
||||
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;
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -225,7 +259,7 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
|||
{
|
||||
debounceMS: 100,
|
||||
errorCallback: (error) => {
|
||||
this.logger.alert('[WATCH_FS_ERROR] NSFW error:', error);
|
||||
this.logger.alert('WatchFileSystemAdaptor NSFW error:', error);
|
||||
},
|
||||
// Start with base excluded paths
|
||||
// @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
|
||||
this.logger.log('[test-id-WATCH_FS_STABILIZED] Watcher has stabilized', { level: 'debug' });
|
||||
} 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
|
||||
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
|
||||
for (const subWiki of subWikis) {
|
||||
|
|
@ -270,7 +304,7 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
|||
|
||||
// Check if the path exists before trying to watch
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -283,7 +317,7 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
|||
{
|
||||
debounceMS: 100,
|
||||
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();
|
||||
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) {
|
||||
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) {
|
||||
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
|
||||
*/
|
||||
private excludeFile(absoluteFilePath: string): void {
|
||||
this.logger.log(`[WATCH_FS_EXCLUDE] Excluding file: ${absoluteFilePath}`);
|
||||
this.inverseFilesIndex.excludeFile(absoluteFilePath);
|
||||
}
|
||||
|
||||
|
|
@ -339,7 +372,6 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
|||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
this.logger.log(`[WATCH_FS_INCLUDE] Including file: ${absoluteFilePath}`);
|
||||
this.inverseFilesIndex.includeFile(absoluteFilePath);
|
||||
this.pendingInclusions.delete(absoluteFilePath);
|
||||
// Notify git service when file is included after being saved
|
||||
|
|
@ -359,7 +391,7 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
|||
if (existingTimer) {
|
||||
clearTimeout(existingTimer);
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
if (isExcluded) {
|
||||
this.logger.log(`[WATCH_FS_SKIP_EXCLUDED] Skipping excluded file: ${fileAbsolutePath}`);
|
||||
this.logger.log(`WatchFileSystemAdaptor Skipping excluded file: ${fileAbsolutePath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -428,10 +459,20 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
|||
const fileMimeType = $tw.utils.getFileExtensionInfo(fileExtension)?.type ?? 'text/vnd.tiddlywiki';
|
||||
const metaFileAbsolutePath = `${fileAbsolutePath}.meta`;
|
||||
|
||||
this.logger.log('[WATCH_FS_EVENT]', getActionName(action), fileName, `(directory: ${directory})`);
|
||||
|
||||
// Handle different event types
|
||||
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)
|
||||
this.cancelPendingDeletion(fileAbsolutePath);
|
||||
|
||||
|
|
@ -508,13 +549,9 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
|||
// watchPathBase is wiki/tiddlers, but wikiFolderLocation should be wiki
|
||||
const wikiFolderLocation = path.dirname(this.watchPathBase);
|
||||
try {
|
||||
(git.notifyFileChange as ((path: string, options?: { onlyWhenGitLogOpened?: boolean }) => void))(
|
||||
wikiFolderLocation,
|
||||
{ onlyWhenGitLogOpened: true },
|
||||
);
|
||||
this.logger.log(`[WATCH_FS_GIT_NOTIFY] Notified git service about file changes in ${wikiFolderLocation}`);
|
||||
void git.notifyFileChange(wikiFolderLocation, { onlyWhenGitLogOpened: true });
|
||||
} 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;
|
||||
}, GIT_NOTIFICATION_DELAY_MS);
|
||||
|
|
@ -547,7 +584,7 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
|||
try {
|
||||
tiddlersDescriptor = $tw.loadTiddlersFromFile(actualFileToLoad);
|
||||
} 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;
|
||||
}
|
||||
|
||||
|
|
@ -573,7 +610,7 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
|||
// not wrapped in a .fields property
|
||||
const tiddlerTitle = tiddler?.title;
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -587,7 +624,8 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
|||
} as IBootFilesIndexItemWithTitle);
|
||||
|
||||
// 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
|
||||
if (isNewFile) {
|
||||
|
|
@ -625,7 +663,8 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
|||
}
|
||||
|
||||
// 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
|
||||
return;
|
||||
}
|
||||
|
|
@ -634,7 +673,7 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
|||
this.removeTiddlerFileInfo(tiddlerTitle);
|
||||
|
||||
// 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' });
|
||||
|
||||
// Delete system tiddler empty file if exists
|
||||
|
|
@ -677,15 +716,15 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
|||
this.pendingInclusions.clear();
|
||||
|
||||
if (this.watcher) {
|
||||
this.logger.log('[WATCH_FS_CLEANUP] Closing filesystem watcher');
|
||||
this.logger.log('WatchFileSystemAdaptor Closing filesystem watcher');
|
||||
await this.watcher.stop();
|
||||
this.watcher = undefined;
|
||||
this.logger.log('[WATCH_FS_CLEANUP] Filesystem watcher closed');
|
||||
this.logger.log('WatchFileSystemAdaptor Filesystem watcher closed');
|
||||
}
|
||||
|
||||
// Close all sub-wiki watchers
|
||||
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();
|
||||
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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import path from 'path';
|
|||
import { Observable } from 'rxjs';
|
||||
|
||||
import type { IWikiWorkspace } from '@services/workspaces/interface';
|
||||
import type { SyncAdaptor } from 'tiddlywiki';
|
||||
import { IZxWorkerMessage, ZxWorkerControlActions } from '../interface';
|
||||
import { executeScriptInTWContext, executeScriptInZxScriptContext, extractTWContextScripts, type IVariableContextList } from '../plugin/zxPlugin';
|
||||
import { wikiOperationsInWikiWorker } from '../wikiOperations/executor/wikiOperationInServer';
|
||||
|
|
@ -101,7 +100,7 @@ async function beforeExit(): Promise<void> {
|
|||
// Cleanup watch-filesystem adaptor
|
||||
const wikiInstance = getWikiInstance();
|
||||
// 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) {
|
||||
await syncAdaptor.cleanup();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { defaultServerIP } from '@/constants/urls';
|
|||
import { DARK_LIGHT_CHANGE_ACTIONS_TAG } from '@services/theme/interface';
|
||||
import intercept from 'intercept-stdout';
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { Server } from 'node:http';
|
||||
import inspector from 'node:inspector';
|
||||
import path from 'path';
|
||||
import { Observable } from 'rxjs';
|
||||
|
|
@ -180,8 +181,8 @@ export function startNodeJSWiki({
|
|||
: [homePath, '--version'];
|
||||
wikiInstance.boot.argv = [...fullBootArgv];
|
||||
|
||||
wikiInstance.hooks.addHook('th-server-command-post-start', function(_listenCommand, server) {
|
||||
server.on('error', function(error: Error) {
|
||||
wikiInstance.hooks.addHook('th-server-command-post-start', function(_server: unknown, nodeServer: Server) {
|
||||
nodeServer.on('error', function(error: Error) {
|
||||
observer.next({ type: 'control', actions: WikiControlActions.error, message: error.message, argv: fullBootArgv });
|
||||
});
|
||||
// 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, {
|
||||
'dark-mode': shouldUseDarkColors ? 'yes' : 'no',
|
||||
});
|
||||
server.on('listening', function() {
|
||||
nodeServer.on('listening', function() {
|
||||
observer.next({
|
||||
type: 'control',
|
||||
actions: WikiControlActions.listening,
|
||||
|
|
|
|||
|
|
@ -207,7 +207,7 @@ export class Window implements IWindowService {
|
|||
...(hideTitleBar && process.platform !== 'darwin' ? { titleBarOverlay: true } : {}),
|
||||
alwaysOnTop: windowName === WindowNames.tidgiMiniWindow ? tidgiMiniWindowAlwaysOnTop : alwaysOnTop,
|
||||
webPreferences: {
|
||||
devTools: !isTest,
|
||||
devTools: true, // Always enable devTools, even in test mode for debugging
|
||||
nodeIntegration: false,
|
||||
webSecurity: false,
|
||||
allowRunningInsecureContent: true,
|
||||
|
|
|
|||
|
|
@ -290,7 +290,7 @@ export default function EditWorkspace(): React.JSX.Element {
|
|||
// Show error notification
|
||||
void window.service.notification.show({
|
||||
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