feat: angular

This commit is contained in:
mathuo 2025-08-29 20:12:43 +01:00
parent 839027391d
commit 3374dc9400
No known key found for this signature in database
GPG Key ID: C6EEDEFD6CA07281
18 changed files with 2142 additions and 66 deletions

View File

@ -1,7 +1,7 @@
import { JestConfigWithTsJest } from 'ts-jest';
const config: JestConfigWithTsJest = {
preset: 'ts-jest',
preset: 'jest-preset-angular',
roots: ['<rootDir>/packages/dockview-angular'],
modulePaths: ['<rootDir>/packages/dockview-angular/src'],
displayName: { name: 'dockview-angular', color: 'blue' },
@ -12,11 +12,9 @@ const config: JestConfigWithTsJest = {
'!<rootDir>/packages/dockview-angular/src/**/index.ts',
'!<rootDir>/packages/dockview-angular/src/public-api.ts',
],
setupFiles: [
'<rootDir>/packages/dockview-angular/src/__tests__/__mocks__/resizeObserver.js',
'<rootDir>/packages/dockview-angular/src/__tests__/__mocks__/angular-testing.js',
setupFilesAfterEnv: [
'<rootDir>/packages/dockview-angular/src/__tests__/setup-jest.ts'
],
setupFilesAfterEnv: ['<rootDir>/jest-setup.ts'],
coveragePathIgnorePatterns: ['/node_modules/'],
modulePathIgnorePatterns: [
'<rootDir>/packages/dockview-angular/src/__tests__/__mocks__',
@ -25,20 +23,19 @@ const config: JestConfigWithTsJest = {
coverageDirectory: '<rootDir>/packages/dockview-angular/coverage/',
testResultsProcessor: 'jest-sonar-reporter',
testEnvironment: 'jsdom',
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.test.json',
},
],
},
moduleNameMapper: {
'^@angular/(.*)$': '<rootDir>/../../node_modules/@angular/$1',
},
testMatch: [
'<rootDir>/packages/dockview-angular/src/**/*.spec.ts',
'<rootDir>/packages/dockview-angular/src/**/*.test.ts'
],
transformIgnorePatterns: [
'node_modules/(?!(.*\\.mjs$|@angular|rxjs))'
],
globals: {
'ts-jest': {
tsconfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '\\.(html|svg)$',
},
},
};
export default config;

View File

@ -56,11 +56,13 @@
"dockview-core": "^4.7.0"
},
"peerDependencies": {
"@angular/core": ">=14.0.0",
"@angular/common": ">=14.0.0",
"@angular/core": ">=14.0.0",
"rxjs": ">=7.0.0"
},
"devDependencies": {
"ng-packagr": "^17.0.0"
"jest-preset-angular": "^14.6.1",
"ng-packagr": "^17.0.0",
"zone.js": "^0.15.1"
}
}

View File

@ -1,7 +1,33 @@
// NOTE: These tests require Angular testing dependencies to be installed in the root node_modules
// For now they are commented out to demonstrate the build works
import { TestBed } from '@angular/core/testing';
import { Component, Injector, EnvironmentInjector } from '@angular/core';
import { AngularRenderer } from '../lib/utils/angular-renderer';
@Component({
selector: 'test-component',
template: '<div class="test-component">{{ title || "Test" }} - {{ value || "default" }}</div>',
})
class TestComponent {
title?: string;
value?: string;
}
describe('AngularRenderer', () => {
let injector: Injector;
let environmentInjector: EnvironmentInjector;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [TestComponent]
}).compileComponents();
injector = TestBed.inject(Injector);
environmentInjector = TestBed.inject(EnvironmentInjector);
});
afterEach(() => {
TestBed.resetTestingModule();
});
it('should be testable when Angular dependencies are available', () => {
expect(true).toBe(true);
});
@ -17,8 +43,7 @@ describe('AngularRenderer', () => {
renderer.init(parameters);
expect(renderer.element).toBeTruthy();
expect(renderer.element.tagName).toBe('DIV');
expect(renderer.element.classList.contains('test-component')).toBe(true);
expect(renderer.element.tagName).toBe('TEST-COMPONENT');
});
it('should update component properties', () => {
@ -28,13 +53,9 @@ describe('AngularRenderer', () => {
environmentInjector
});
// Initialize with initial parameters
renderer.init({ title: 'Initial Title' });
// Update properties
renderer.update({ title: 'Updated Title', value: 'new-value' });
// The component should have updated properties
expect(renderer.element).toBeTruthy();
});
@ -52,12 +73,10 @@ describe('AngularRenderer', () => {
renderer.dispose();
// After dispose, accessing element should throw
expect(() => renderer.element).toThrow('Angular renderer not initialized');
});
it('should handle component creation errors gracefully', () => {
// Use an invalid component to trigger error
const renderer = new AngularRenderer({
component: null as any,
injector,
@ -79,7 +98,6 @@ describe('AngularRenderer', () => {
renderer.init({ title: 'Test Title' });
renderer.dispose();
// Should not throw
expect(() => {
renderer.update({ title: 'Updated Title' });
}).not.toThrow();
@ -94,7 +112,6 @@ describe('AngularRenderer', () => {
renderer.init({ title: 'Test Title' });
// Multiple dispose calls should not throw
expect(() => {
renderer.dispose();
renderer.dispose();

View File

@ -4,11 +4,9 @@ import { By } from '@angular/platform-browser';
import { DockviewAngularComponent } from '../lib/dockview/dockview-angular.component';
import { DockviewApi } from 'dockview-core';
import { setupTestBed, getTestComponents, TestPanelComponent } from './__test_utils__/test-helpers';
import { setupTestBed, getTestComponents } from './__test_utils__/test-helpers';
// NOTE: These tests require Angular testing dependencies to be installed in the root node_modules
// For now they are commented out to demonstrate the build works
describe.skip('DockviewAngularComponent', () => {
describe('DockviewAngularComponent', () => {
let component: DockviewAngularComponent;
let fixture: ComponentFixture<DockviewAngularComponent>;
let debugElement: DebugElement;
@ -21,7 +19,6 @@ describe.skip('DockviewAngularComponent', () => {
component = fixture.componentInstance;
debugElement = fixture.debugElement;
// Set required inputs
component.components = getTestComponents();
});
@ -84,7 +81,6 @@ describe.skip('DockviewAngularComponent', () => {
const api = component.getDockviewApi();
const updateOptionsSpy = jest.spyOn(api!, 'updateOptions');
// Simulate input change
component.className = 'test-class';
component.ngOnChanges({
className: {
@ -105,12 +101,10 @@ describe.skip('DockviewAngularComponent', () => {
component.ngOnInit();
// API should be initialized without throwing
expect(component.getDockviewApi()).toBeDefined();
});
});
// Integration test with template
@Component({
template: `
<dv-dockview
@ -132,7 +126,7 @@ class TestHostComponent {
}
}
describe.skip('DockviewAngularComponent Integration', () => {
describe('DockviewAngularComponent Integration', () => {
let hostComponent: TestHostComponent;
let fixture: ComponentFixture<TestHostComponent>;
@ -168,7 +162,5 @@ describe.skip('DockviewAngularComponent Integration', () => {
fixture.detectChanges();
expect(hostComponent.api).toBeDefined();
// Additional assertions could be added here to verify the properties
// were passed to the core dockview component
});
});

View File

@ -0,0 +1,56 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
setupZoneTestEnv();
// Global mocks for browser APIs
Object.defineProperty(window, 'CSS', {value: null});
Object.defineProperty(window, 'getComputedStyle', {
value: () => {
return {
display: 'none',
appearance: ['-webkit-appearance']
};
}
});
Object.defineProperty(document, 'doctype', {
value: '<!DOCTYPE html>'
});
Object.defineProperty(document.body.style, 'transform', {
value: () => {
return {
enumerable: true,
configurable: true
};
}
});
// Mock ResizeObserver
global.ResizeObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
}));
// Mock getBoundingClientRect
Element.prototype.getBoundingClientRect = jest.fn(() => ({
width: 100,
height: 100,
top: 0,
left: 0,
bottom: 100,
right: 100,
x: 0,
y: 0,
toJSON: jest.fn(),
}));
// Mock scrollIntoView
HTMLElement.prototype.scrollIntoView = jest.fn();
HTMLElement.prototype.focus = jest.fn();
HTMLElement.prototype.blur = jest.fn();
// Mock requestAnimationFrame
global.requestAnimationFrame = jest.fn((cb) => setTimeout(cb, 16));
global.cancelAnimationFrame = jest.fn((id) => clearTimeout(id));

View File

@ -0,0 +1,35 @@
import * as React from 'react';
import { Button as ChakraButton, ChakraProvider, defaultSystem } from '@chakra-ui/react';
interface ButtonProps {
children: React.ReactNode;
onClick?: () => void;
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
className?: string;
}
export const Button: React.FC<ButtonProps> = ({
children,
onClick,
variant = 'secondary',
size = 'md',
disabled = false,
className = '',
}) => {
const chakraVariant = variant === 'primary' ? 'solid' : variant === 'ghost' ? 'ghost' : 'outline';
const colorPalette = variant === 'primary' ? 'blue' : 'gray';
return (
<ChakraButton
onClick={onClick}
variant={chakraVariant}
colorPalette={colorPalette}
size={size}
disabled={disabled}
>
{children}
</ChakraButton>
);
};

View File

@ -0,0 +1,15 @@
import * as React from 'react';
import { ButtonGroup as ChakraButtonGroup } from '@chakra-ui/react';
interface ButtonGroupProps {
children: React.ReactNode;
className?: string;
}
export const ButtonGroup: React.FC<ButtonGroupProps> = ({ children, className = '' }) => {
return (
<ChakraButtonGroup isAttached variant="outline" className={className}>
{children}
</ChakraButtonGroup>
);
};

View File

@ -0,0 +1,215 @@
// Modern Button Styles
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
font-weight: 500;
line-height: 1.5;
border-radius: 0.375rem;
border: 1px solid transparent;
text-decoration: none;
cursor: pointer;
transition: all 0.15s ease-in-out;
white-space: nowrap;
user-select: none;
&:focus {
outline: 2px solid var(--ifm-color-primary);
outline-offset: 2px;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
pointer-events: none;
}
}
.btn-primary {
color: white;
background-color: var(--ifm-color-primary);
border-color: var(--ifm-color-primary);
&:hover:not(:disabled) {
background-color: var(--ifm-color-primary-dark);
border-color: var(--ifm-color-primary-dark);
}
}
.btn-secondary {
color: var(--ifm-color-emphasis-800);
background-color: var(--ifm-background-surface-color);
border-color: var(--ifm-color-emphasis-300);
&:hover:not(:disabled) {
background-color: var(--ifm-color-emphasis-100);
border-color: var(--ifm-color-emphasis-400);
}
}
.btn-ghost {
color: var(--ifm-color-emphasis-700);
background-color: transparent;
border-color: transparent;
&:hover:not(:disabled) {
background-color: var(--ifm-color-emphasis-100);
color: var(--ifm-color-emphasis-800);
}
}
.btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
}
.btn-lg {
padding: 0.5rem 1rem;
font-size: 1rem;
}
// Button Group Styles
.btn-group {
display: inline-flex;
vertical-align: middle;
.btn {
position: relative;
flex: 1 1 auto;
&:not(:last-child) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right: none;
}
&:not(:first-child) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
&:hover {
z-index: 1;
}
&:focus {
z-index: 2;
}
}
}
.dockview-demo {
.group-control {
.action {
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
font-size: 18px;
cursor: pointer;
&:hover {
border-radius: 2px;
color: var(--dv-activegroup-visiblepanel-tab-color);
background-color: var(--dv-icon-hover-background-color);
}
}
}
.data-table {
overflow: auto;
flex: 1;
min-height: 0;
table {
font-size: 11px;
width: 100%;
border-collapse: collapse;
th, td {
padding: 4px 8px;
border: 1px solid var(--ifm-color-emphasis-200);
text-align: left;
vertical-align: top;
word-break: break-word;
max-width: 200px;
}
th:nth-child(3), td:nth-child(3) {
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
input {
outline: 1px solid #4c65d4;
border: none;
margin: 0px;
height: 25px;
&:focus {
outline: 1px solid #4c65d4 !important;
}
}
.action-container {
display: flex;
padding: 4px;
overflow: auto;
.text-button {
margin: 0px 4px;
}
.button-action {
margin: 0px 4px;
.selected {
background-color: #4864dc;
}
}
.button-group {
button {
margin-right: 0px;
}
}
.demo-button {
min-width: 50px;
padding: 0px 2px;
border-radius: 0px;
display: flex;
flex-grow: 1;
align-items: center;
outline: 1px solid #4c65d4;
}
.demo-icon-button {
outline: 1px solid #4c65d4;
flex-grow: 1;
display: flex;
align-items: center;
border-radius: 0px;
padding: 0px 4px;
border: none;
cursor: pointer;
&:disabled {
color: gray;
cursor: help;
}
span {
font-size: 16px;
}
}
}
}

View File

@ -0,0 +1,650 @@
import {
DockviewDefaultTab,
DockviewReact,
DockviewReadyEvent,
IDockviewPanelHeaderProps,
IDockviewPanelProps,
DockviewApi,
DockviewTheme,
} from 'dockview';
import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
import './DockviewDemo.scss';
import { defaultConfig } from './defaultLayout';
import { GridActions } from './gridActions';
import { PanelActions } from './panelActions';
import { GroupActions } from './groupActions';
import { LeftControls, PrefixHeaderControls, RightControls } from './controls';
import { Table, usePanelApiMetadata } from './debugPanel';
import { Button } from './Button';
import {
ChakraProvider,
createSystem,
defaultConfig as chakraDefaultConfig,
} from '@chakra-ui/react';
import { useColorMode } from '@docusaurus/theme-common';
const DebugContext = React.createContext<boolean>(false);
const Option = (props: {
title: string;
onClick: () => void;
value: string;
}) => {
return (
<div>
<span>{`${props.title}: `}</span>
<button onClick={props.onClick}>{props.value}</button>
</div>
);
};
const ShadowIframe = (props: IDockviewPanelProps) => {
return (
<iframe
onMouseDown={() => {
if (!props.api.isActive) {
props.api.setActive();
}
}}
style={{ border: 'none', width: '100%', height: '100%' }}
src="https://dockview.dev"
/>
);
};
const components = {
default: (props: IDockviewPanelProps) => {
const api = usePanelApiMetadata(props.api);
const [count, setCount] = React.useState<number>(0);
const isDebug = React.useContext(DebugContext);
return (
<div
style={{
padding: '10px',
backgroundColor: 'transparent',
color: 'var(--dv-activegroup-visiblepanel-tab-color)',
height: '100%',
display: 'flex',
flexDirection: 'column',
position: 'relative',
}}
>
{isDebug && (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: '10px',
}}
>
<span>{props.api.title}</span>
<Button
onClick={() => setCount(count + 1)}
size="sm"
variant="secondary"
>
count: {count}
</Button>
</div>
)}
{isDebug && (
<div
style={{
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
pointerEvents: 'none',
display: 'flex',
}}
>
<div
style={{
flexGrow: 1,
border: '2px dashed red',
position: 'relative',
}}
>
<div
style={{
position: 'absolute',
top: '2px',
left: '2px',
backgroundColor: 'red',
color: 'white',
fontSize: '10px',
padding: '2px 4px',
borderRadius: '2px',
}}
>
{props.api.id}
</div>
</div>
</div>
)}
<div
style={{
flexGrow: 1,
minHeight: 0,
overflow: 'auto',
display: 'flex',
flexDirection: 'column',
}}
>
{isDebug ? (
<Table api={api} />
) : (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
color: 'var(--dv-activegroup-visiblepanel-tab-color)',
fontSize: '14px',
textAlign: 'center',
padding: '20px',
}}
>
<div>
<div
style={{
marginBottom: '10px',
fontWeight: 'bold',
}}
>
{props.api.title}
</div>
<div style={{ opacity: 0.7 }}>
Click the engineering button to see debug
info
</div>
</div>
</div>
)}
</div>
</div>
);
},
iframe: ShadowIframe,
shadow: (props: IDockviewPanelProps) => {
const ref = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (!ref.current) {
return () => {
//
};
}
const element = ref.current;
const shadow = element.attachShadow({ mode: 'open' });
const shadowRoot = document.createElement('div');
shadowRoot.style.height = '100%';
shadow.appendChild(shadowRoot);
const root = ReactDOM.createRoot(shadowRoot);
root.render(<ShadowIframe {...props} />);
return () => {
root.unmount();
};
}, []);
return <div style={{ height: '100%' }} ref={ref}></div>;
},
};
const headerComponents = {
default: (props: IDockviewPanelHeaderProps) => {
const onContextMenu = (event: React.MouseEvent) => {
event.preventDefault();
alert('context menu');
};
return <DockviewDefaultTab onContextMenu={onContextMenu} {...props} />;
},
};
const colors = [
'rgba(255,0,0,0.2)',
'rgba(0,255,0,0.2)',
'rgba(0,0,255,0.2)',
'rgba(255,255,0,0.2)',
'rgba(0,255,255,0.2)',
'rgba(255,0,255,0.2)',
];
let count = 0;
const WatermarkComponent = () => {
return <div>custom watermark</div>;
};
const ThemeContext = React.createContext<DockviewTheme | undefined>(undefined);
const DockviewDemo = (props: { theme?: DockviewTheme }) => {
const [logLines, setLogLines] = React.useState<
{ text: string; timestamp?: Date; backgroundColor?: string }[]
>([]);
const [panels, setPanels] = React.useState<string[]>([]);
const [groups, setGroups] = React.useState<string[]>([]);
const [api, setApi] = React.useState<DockviewApi>();
const [activePanel, setActivePanel] = React.useState<string>();
const [activeGroup, setActiveGroup] = React.useState<string>();
const addLogLine = (text: string, backgroundColor?: string) => {
setLogLines((_) => [
..._,
{ text, timestamp: new Date(), backgroundColor },
]);
};
React.useEffect(() => {
if (!api) {
return () => {
//
};
}
const disposables = [
api.onDidAddPanel((event) => {
setPanels((_) => [..._, event.id]);
addLogLine(`Panel Added ${event.id}`);
}),
api.onDidActiveGroupChange((event) => {
setActiveGroup(event?.id);
addLogLine(`Group Activated ${event?.id}`);
}),
api.onDidActivePanelChange((event) => {
setActivePanel(event?.id);
addLogLine(
`Panel Activated ${event?.id}`,
colors[count++ % colors.length]
);
}),
api.onDidRemovePanel((event) => {
setPanels((_) => {
const next = [..._];
next.splice(
next.findIndex((x) => x === event.id),
1
);
return next;
});
addLogLine(`Panel Removed ${event.id}`);
}),
api.onDidAddGroup((event) => {
setGroups((_) => [..._, event.id]);
addLogLine(`Group Added ${event.id}`);
}),
api.onDidMovePanel((event) => {
addLogLine(`Panel Moved ${event.panel.id}`);
}),
api.onDidMaximizedGroupChange((event) => {
addLogLine(
`Group Maximized Changed ${event.group.api.id} [${event.isMaximized}]`
);
}),
api.onDidRemoveGroup((event) => {
setGroups((_) => {
const next = [..._];
next.splice(
next.findIndex((x) => x === event.id),
1
);
return next;
});
addLogLine(`Group Removed ${event.id}`);
}),
api.onDidActiveGroupChange((event) => {
setActiveGroup(event?.id);
addLogLine(`Group Activated ${event?.id}`);
}),
];
const loadLayout = () => {
const state = localStorage.getItem('dv-demo-state');
if (state) {
try {
api.fromJSON(JSON.parse(state));
return;
} catch {
localStorage.removeItem('dv-demo-state');
}
return;
}
defaultConfig(api);
};
loadLayout();
return () => {
disposables.forEach((disposable) => disposable.dispose());
};
}, [api]);
const onReady = (event: DockviewReadyEvent) => {
setApi(event.api);
};
const [watermark, setWatermark] = React.useState<boolean>(false);
const [gapCheck, setGapCheck] = React.useState<boolean>(false);
const css = React.useMemo(() => {
if (!gapCheck) {
return {};
}
return {
'--dv-group-gap-size': '0.5rem',
'--demo-border': '5px dashed purple',
} as React.CSSProperties;
}, [gapCheck]);
const [showLogs, setShowLogs] = React.useState<boolean>(false);
const [debug, setDebug] = React.useState<boolean>(false);
return (
<div
className="dockview-demo"
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
padding: '8px',
position: 'relative',
...css,
}}
>
<div>
<GridActions
api={api}
toggleCustomWatermark={() => setWatermark(!watermark)}
hasCustomWatermark={watermark}
/>
{api && (
<PanelActions
api={api}
panels={panels}
activePanel={activePanel}
/>
)}
{api && (
<GroupActions
api={api}
groups={groups}
activeGroup={activeGroup}
/>
)}
</div>
<div
className="action-container"
style={{
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
padding: '4px',
}}
>
<Button
onClick={() => {
setDebug(!debug);
}}
variant={debug ? 'primary' : 'secondary'}
size="sm"
>
<span className="material-symbols-outlined">
engineering
</span>
</Button>
{showLogs && (
<Button
onClick={() => {
setLogLines([]);
}}
variant="ghost"
size="sm"
>
<span className="material-symbols-outlined">undo</span>
</Button>
)}
<Button
onClick={() => {
setShowLogs(!showLogs);
}}
variant={showLogs ? 'primary' : 'secondary'}
size="sm"
>
<span style={{ paddingRight: '4px' }}>
{`${showLogs ? 'Hide' : 'Show'} Events Log`}
</span>
<span className="material-symbols-outlined">terminal</span>
</Button>
</div>
<div
style={{
flexGrow: 1,
height: 0,
display: 'flex',
}}
>
<div
style={{
flexGrow: 1,
overflow: 'hidden',
display: 'flex',
}}
>
<DebugContext.Provider value={debug}>
<ThemeContext.Provider value={props.theme}>
<DockviewReact
components={components}
defaultTabComponent={headerComponents.default}
rightHeaderActionsComponent={RightControls}
leftHeaderActionsComponent={LeftControls}
prefixHeaderActionsComponent={
PrefixHeaderControls
}
watermarkComponent={
watermark ? WatermarkComponent : undefined
}
onReady={onReady}
theme={props.theme}
/>
</ThemeContext.Provider>
</DebugContext.Provider>
</div>
{showLogs && (
<div
style={{
width: '400px',
backgroundColor: 'black',
color: 'white',
overflow: 'hidden',
fontFamily: 'monospace',
marginLeft: '10px',
flexShrink: 0,
display: 'flex',
flexDirection: 'column',
}}
>
<div style={{ flexGrow: 1, overflow: 'auto' }}>
{logLines.map((line, i) => {
return (
<div
style={{
height: '30px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
fontSize: '13px',
display: 'flex',
alignItems: 'center',
backgroundColor:
line.backgroundColor,
}}
key={i}
>
<span
style={{
display: 'flex',
alignItems: 'center',
minWidth: '20px',
maxWidth: '20px',
color: 'gray',
borderRight: '1px solid gray',
marginRight: '4px',
paddingLeft: '4px',
height: '100%',
}}
>
{logLines.length - i}
</span>
<span>
{line.timestamp && (
<span
style={{
fontSize: '0.7em',
padding: '0px 2px',
}}
>
{line.timestamp
.toISOString()
.substring(11, 23)}
</span>
)}
<span>{line.text}</span>
</span>
</div>
);
})}
</div>
<div
style={{
padding: '4px',
display: 'flex',
justifyContent: 'flex-end',
}}
>
<Button
onClick={() => setLogLines([])}
variant="secondary"
size="sm"
>
Clear
</Button>
</div>
</div>
)}
</div>
</div>
);
};
const DockviewDemoWithChakra = (props: { theme?: DockviewTheme }) => {
const { colorMode } = useColorMode();
const system = React.useMemo(
() =>
createSystem(chakraDefaultConfig, {
theme: {
tokens: {
colors: {
colorPalette: {
fg: {
value:
colorMode === 'dark'
? 'white'
: 'black',
},
solid: {
value:
colorMode === 'dark'
? 'white'
: 'black',
},
},
},
},
recipes: {
button: {
variants: {
variant: {
outline: {
bg: 'transparent',
border: '1px solid gray',
borderColor:
colorMode === 'dark'
? 'gray.700'
: 'gray.200',
color:
colorMode === 'dark'
? 'white'
: 'black',
borderRadius: 'md',
transition: 'all 0.25s ease-in-out',
px: '8px',
py: '2px',
height: '28px',
mr: '4px',
_hover: {
bg:
colorMode === 'dark'
? 'whiteAlpha.100'
: 'blackAlpha.100',
borderColor:
colorMode === 'dark'
? 'gray.600'
: 'gray.300',
color:
colorMode === 'dark'
? 'white'
: 'black',
},
_focus: {
boxShadow: 'outline',
bg: 'transparent',
},
},
},
},
},
},
},
}),
[colorMode]
);
return (
<ChakraProvider value={system}>
<DockviewDemo {...props} />
</ChakraProvider>
);
};
export default DockviewDemoWithChakra;

View File

@ -0,0 +1,148 @@
import { IDockviewHeaderActionsProps } from 'dockview';
import * as React from 'react';
import { nextId } from './defaultLayout';
const Icon = (props: {
icon: string;
title?: string;
onClick?: (event: React.MouseEvent) => void;
}) => {
return (
<div title={props.title} className="action" onClick={props.onClick}>
<span
style={{ fontSize: 'inherit' }}
className="material-symbols-outlined"
>
{props.icon}
</span>
</div>
);
};
const groupControlsComponents: Record<string, React.FC> = {
panel_1: () => {
return <Icon icon="file_download" />;
},
};
export const RightControls = (props: IDockviewHeaderActionsProps) => {
const Component = React.useMemo(() => {
if (!props.isGroupActive || !props.activePanel) {
return null;
}
return groupControlsComponents[props.activePanel.id];
}, [props.isGroupActive, props.activePanel]);
const [isMaximized, setIsMaximized] = React.useState<boolean>(
props.containerApi.hasMaximizedGroup()
);
const [isPopout, setIsPopout] = React.useState<boolean>(
props.api.location.type === 'popout'
);
React.useEffect(() => {
const disposable = props.containerApi.onDidMaximizedGroupChange(() => {
setIsMaximized(props.containerApi.hasMaximizedGroup());
});
const disposable2 = props.api.onDidLocationChange(() => {
setIsPopout(props.api.location.type === 'popout');
});
return () => {
disposable.dispose();
disposable2.dispose();
};
}, [props.containerApi]);
const onClick = () => {
if (props.containerApi.hasMaximizedGroup()) {
props.containerApi.exitMaximizedGroup();
} else {
props.activePanel?.api.maximize();
}
};
const onClick2 = () => {
if (props.api.location.type !== 'popout') {
props.containerApi.addPopoutGroup(props.group);
} else {
props.api.moveTo({ position: 'right' });
}
};
return (
<div
className="group-control"
style={{
display: 'flex',
alignItems: 'center',
padding: '0px 8px',
height: '100%',
color: 'var(--dv-activegroup-hiddenpanel-tab-color)',
}}
>
{props.isGroupActive && <Icon icon="star" />}
{Component && <Component />}
<Icon
title={isPopout ? 'Close Window' : 'Open In New Window'}
icon={isPopout ? 'close_fullscreen' : 'open_in_new'}
onClick={onClick2}
/>
{!isPopout && (
<Icon
title={isMaximized ? 'Minimize View' : 'Maximize View'}
icon={isMaximized ? 'collapse_content' : 'expand_content'}
onClick={onClick}
/>
)}
</div>
);
};
export const LeftControls = (props: IDockviewHeaderActionsProps) => {
const onClick = () => {
props.containerApi.addPanel({
id: `id_${Date.now().toString()}`,
component: 'default',
title: `Tab ${nextId()}`,
position: {
referenceGroup: props.group,
},
});
};
return (
<div
className="group-control"
style={{
display: 'flex',
alignItems: 'center',
padding: '0px 8px',
height: '100%',
color: 'var(--dv-activegroup-visiblepanel-tab-color)',
}}
>
<Icon onClick={onClick} icon="add" />
</div>
);
};
export const PrefixHeaderControls = (props: IDockviewHeaderActionsProps) => {
return (
<div
className="group-control"
style={{
display: 'flex',
alignItems: 'center',
padding: '0px 8px',
height: '100%',
color: 'var(--dv-activegroup-visiblepanel-tab-color)',
}}
>
<Icon icon="Menu" />
</div>
);
};

View File

@ -0,0 +1,164 @@
import {
DockviewGroupLocation,
DockviewPanelApi,
DockviewPanelRenderer,
} from 'dockview';
import * as React from 'react';
export interface PanelApiMetadata {
isActive: {
value: boolean;
count: number;
};
isVisible: {
value: boolean;
count: number;
};
renderer: {
value: DockviewPanelRenderer;
count: number;
};
isGroupActive: {
value: boolean;
count: number;
};
groupChanged: {
count: number;
};
location: {
value: DockviewGroupLocation;
count: number;
};
didFocus: {
count: number;
};
dimensions: {
count: number;
value: { height: number; width: number };
};
}
export const Table = (props: { api: PanelApiMetadata }) => {
return (
<div className="data-table">
<table>
<tr>
<th>{'Key'}</th>
<th>{'Count'}</th>
<th>{'Value'}</th>
</tr>
{Object.entries(props.api).map(([key, value]) => {
return (
<tr key={key}>
<th>{key}</th>
<th>{value.count}</th>
<th>{JSON.stringify(value.value, null, 4)}</th>
</tr>
);
})}
</table>
</div>
);
};
export function usePanelApiMetadata(api: DockviewPanelApi): PanelApiMetadata {
const [state, setState] = React.useState<PanelApiMetadata>({
isActive: { value: api.isActive, count: 0 },
isVisible: { value: api.isVisible, count: 0 },
renderer: { value: api.renderer, count: 0 },
isGroupActive: { value: api.isGroupActive, count: 0 },
groupChanged: { count: 0 },
location: { value: api.location, count: 0 },
didFocus: { count: 0 },
dimensions: {
count: 0,
value: { height: api.height, width: api.width },
},
});
React.useEffect(() => {
const d1 = api.onDidActiveChange((event) => {
setState((_) => ({
..._,
isActive: {
value: event.isActive,
count: _.isActive.count + 1,
},
}));
});
const d2 = api.onDidActiveGroupChange((event) => {
setState((_) => ({
..._,
isGroupActive: {
value: event.isActive,
count: _.isGroupActive.count + 1,
},
}));
});
const d3 = api.onDidDimensionsChange((event) => {
setState((_) => ({
..._,
dimensions: {
count: _.dimensions.count + 1,
value: { height: event.height, width: event.width },
},
}));
});
const d4 = api.onDidFocusChange((event) => {
setState((_) => ({
..._,
didFocus: {
count: _.didFocus.count + 1,
},
}));
});
const d5 = api.onDidGroupChange((event) => {
setState((_) => ({
..._,
groupChanged: {
count: _.groupChanged.count + 1,
},
}));
});
const d7 = api.onDidLocationChange((event) => {
setState((_) => ({
..._,
location: {
value: event.location,
count: _.location.count + 1,
},
}));
});
const d8 = api.onDidRendererChange((event) => {
setState((_) => ({
..._,
renderer: {
value: event.renderer,
count: _.renderer.count + 1,
},
}));
});
const d9 = api.onDidVisibilityChange((event) => {
setState((_) => ({
..._,
isVisible: {
value: event.isVisible,
count: _.isVisible.count + 1,
},
}));
});
return () => {
d1.dispose();
d2.dispose();
d3.dispose();
d4.dispose();
d5.dispose();
d7.dispose();
d8.dispose();
d9.dispose();
};
}, [api]);
return state;
}

View File

@ -0,0 +1,67 @@
import { DockviewApi } from 'dockview';
export const nextId = (() => {
let counter = 0;
return () => counter++;
})();
export function defaultConfig(api: DockviewApi) {
const panel1 = api.addPanel({
id: 'panel_1',
component: 'default',
renderer: 'always',
title: 'Panel 1',
});
api.addPanel({
id: 'panel_2',
component: 'default',
title: 'Panel 2',
position: { referencePanel: panel1 },
});
api.addPanel({
id: 'panel_3',
component: 'default',
title: 'Panel 3',
position: { referencePanel: panel1 },
});
const panel4 = api.addPanel({
id: 'panel_4',
component: 'default',
title: 'Panel 4',
position: { referencePanel: panel1, direction: 'right' },
});
const panel5 = api.addPanel({
id: 'panel_5',
component: 'default',
title: 'Panel 5',
position: { referencePanel: panel4 },
});
const panel6 = api.addPanel({
id: 'panel_6',
component: 'default',
title: 'Panel 6',
position: { referencePanel: panel5, direction: 'below' },
});
const panel7 = api.addPanel({
id: 'panel_7',
component: 'default',
title: 'Panel 7',
position: { referencePanel: panel6, direction: 'left' },
});
api.addPanel({
id: 'panel8',
component: 'default',
title: 'Panel 8',
position: { referencePanel: panel7, direction: 'below' },
});
panel1.api.setActive();
}

View File

@ -0,0 +1,270 @@
import { DockviewApi } from 'dockview';
import * as React from 'react';
import { defaultConfig, nextId } from './defaultLayout';
import { createRoot } from 'react-dom/client';
import { PanelBuilder } from './panelBuilder';
import { Button } from '@chakra-ui/react';
import { ButtonGroup } from './ButtonGroup';
let mount = document.querySelector('.popover-anchor') as HTMLElement | null;
if (!mount) {
mount = document.createElement('div');
mount.className = 'popover-anchor';
document.body.insertBefore(mount, document.body.firstChild);
}
const PopoverComponent = (props: {
close: () => void;
component: React.FC<{ close: () => void }>;
}) => {
const ref = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
const handler = (ev: MouseEvent) => {
let target = ev.target as HTMLElement;
while (target.parentElement) {
if (target === ref.current) {
return;
}
target = target.parentElement;
}
props.close();
};
window.addEventListener('mousedown', handler);
return () => {
window.removeEventListener('mousedown', handler);
};
}, []);
return (
<div
style={{
position: 'absolute',
top: 0,
left: 0,
zIndex: 9999,
height: '100%',
width: '100%',
}}
>
<div
ref={ref}
style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%,-50%)',
backgroundColor: 'black',
color: 'white',
padding: 10,
}}
>
<props.component close={props.close} />
</div>
</div>
);
};
function usePopover() {
return {
open: (Component: React.FC<{ close: () => void }>) => {
const el = document.createElement('div');
mount!.appendChild(el);
const root = createRoot(el);
root.render(
<PopoverComponent
component={Component}
close={() => {
root.unmount();
el.remove();
}}
/>
);
},
};
}
export const GridActions = (props: {
api?: DockviewApi;
hasCustomWatermark: boolean;
toggleCustomWatermark: () => void;
}) => {
const onClear = () => {
props.api?.clear();
};
const onLoad = () => {
const state = localStorage.getItem('dv-demo-state');
if (state) {
try {
props.api?.fromJSON(JSON.parse(state));
} catch (err) {
console.error('failed to load state', err);
localStorage.removeItem('dv-demo-state');
}
}
};
const onSave = () => {
if (props.api) {
const state = props.api.toJSON();
console.log(state);
localStorage.setItem('dv-demo-state', JSON.stringify(state));
}
};
const onReset = () => {
if (props.api) {
try {
props.api.clear();
defaultConfig(props.api);
} catch (err) {
localStorage.removeItem('dv-demo-state');
}
}
};
const popover = usePopover();
const onAddPanel = (options?: { advanced?: boolean; nested?: boolean }) => {
if (options?.advanced) {
popover.open(({ close }) => {
return <PanelBuilder api={props.api!} done={close} />;
});
} else {
props.api?.addPanel({
id: `id_${Date.now().toString()}`,
component: options?.nested ? 'nested' : 'default',
title: `Tab ${nextId()}`,
renderer: 'always',
});
}
};
const onAddGroup = () => {
props.api?.addGroup();
};
return (
<div className="action-container">
<ButtonGroup>
<Button
size="sm"
variant="outline"
css={{
position: 'relative',
display: 'flex',
alignItems: 'center',
padding: 0
}}
>
<span
onClick={() => onAddPanel()}
style={{
padding: '2px 8px',
cursor: 'pointer',
transition: 'background-color 0.15s',
flex: 1,
display: 'flex',
alignItems: 'center',
height: '100%'
}}
onMouseEnter={(e) => e.target.style.backgroundColor = 'rgba(255,255,255,0.1)'}
onMouseLeave={(e) => e.target.style.backgroundColor = 'transparent'}
>
Add Panel
</span>
<span
onClick={() => onAddPanel({ advanced: true })}
className="material-symbols-outlined"
style={{
padding: '4px',
cursor: 'pointer',
borderLeft: '1px solid rgba(255,255,255,0.2)',
borderRadius: '0 4px 4px 0',
transition: 'background-color 0.15s',
fontSize: '16px'
}}
onMouseEnter={(e) => e.target.style.backgroundColor = 'rgba(255,255,255,0.1)'}
onMouseLeave={(e) => e.target.style.backgroundColor = 'transparent'}
>
tune
</span>
</Button>
<Button
onClick={() => onAddPanel({ nested: true })}
size="sm"
variant="outline"
>
Add Nested Panel
</Button>
</ButtonGroup>
<ButtonGroup>
<Button
onClick={onAddGroup}
size="sm"
variant="outline"
>
Add Group
</Button>
</ButtonGroup>
<Button
onClick={props.toggleCustomWatermark}
size="sm"
variant={props.hasCustomWatermark ? "solid" : "outline"}
colorPalette="blue"
>
Use Custom Watermark
</Button>
<span style={{ flexGrow: 1 }} />
<ButtonGroup>
<span style={{
fontSize: '12px',
color: 'var(--chakra-colors-fg)',
alignSelf: 'center',
marginRight: '8px',
fontWeight: '500'
}}>
Layout:
</span>
<Button
onClick={onClear}
size="sm"
variant="outline"
colorPalette="red"
>
Clear
</Button>
<Button
onClick={onLoad}
size="sm"
variant="outline"
>
Load
</Button>
<Button
onClick={onSave}
size="sm"
variant="outline"
>
Save
</Button>
<Button
onClick={onReset}
size="sm"
variant="outline"
>
Reset
</Button>
</ButtonGroup>
</div>
);
};

View File

@ -0,0 +1,175 @@
import {
DockviewApi,
DockviewGroupLocation,
DockviewGroupPanel,
} from 'dockview';
import * as React from 'react';
import { Button, ButtonGroup } from '@chakra-ui/react';
const GroupAction = (props: {
groupId: string;
groups: string[];
api: DockviewApi;
activeGroup?: string;
}) => {
const onClick = () => {
props.api?.getGroup(props.groupId)?.focus();
};
const isActive = props.activeGroup === props.groupId;
const [group, setGroup] = React.useState<DockviewGroupPanel | undefined>(
undefined
);
React.useEffect(() => {
const disposable = props.api.onDidLayoutFromJSON(() => {
setGroup(props.api.getGroup(props.groupId));
});
setGroup(props.api.getGroup(props.groupId));
return () => {
disposable.dispose();
};
}, [props.api, props.groupId]);
const [location, setLocation] =
React.useState<DockviewGroupLocation | null>(null);
const [isMaximized, setIsMaximized] = React.useState<boolean>(false);
const [isVisible, setIsVisible] = React.useState<boolean>(true);
React.useEffect(() => {
if (!group) {
setLocation(null);
return;
}
const disposable = group.api.onDidLocationChange((event) => {
setLocation(event.location);
});
const disposable2 = props.api.onDidMaximizedGroupChange(() => {
setIsMaximized(group.api.isMaximized());
});
const disposable3 = group.api.onDidVisibilityChange(() => {
setIsVisible(group.api.isVisible);
});
setLocation(group.api.location);
setIsMaximized(group.api.isMaximized());
setIsVisible(group.api.isVisible);
return () => {
disposable.dispose();
disposable2.dispose();
disposable3.dispose();
};
}, [group]);
return (
<div className="button-action">
<div style={{ display: 'flex' }}>
<Button
onClick={onClick}
variant={isActive ? "solid" : "outline"}
colorPalette={isActive ? "blue" : undefined}
size="sm"
>
{props.groupId}
</Button>
</div>
<div style={{ display: 'flex' }}>
<ButtonGroup size="sm" variant="outline">
<Button
variant={location?.type === 'floating' ? "solid" : "outline"}
colorPalette={location?.type === 'floating' ? "blue" : undefined}
onClick={() => {
if (group) {
props.api.addFloatingGroup(group, {
width: 400,
height: 300,
x: 50,
y: 50,
position: {
bottom: 50,
right: 50,
},
});
}
}}
>
<span className="material-symbols-outlined">ad_group</span>
</Button>
<Button
variant={location?.type === 'popout' ? "solid" : "outline"}
colorPalette={location?.type === 'popout' ? "blue" : undefined}
onClick={() => {
if (group) {
props.api.addPopoutGroup(group);
}
}}
>
<span className="material-symbols-outlined">open_in_new</span>
</Button>
<Button
variant={isMaximized ? "solid" : "outline"}
colorPalette={isMaximized ? "blue" : undefined}
onClick={() => {
if (group) {
if (group.api.isMaximized()) {
group.api.exitMaximized();
} else {
group.api.maximize();
}
}
}}
>
<span className="material-symbols-outlined">fullscreen</span>
</Button>
<Button
onClick={() => {
console.log(group);
if (group) {
if (group.api.isVisible) {
group.api.setVisible(false);
} else {
group.api.setVisible(true);
}
}
}}
>
<span className="material-symbols-outlined">
{isVisible ? 'visibility' : 'visibility_off'}
</span>
</Button>
<Button
onClick={() => {
const panel = props.api?.getGroup(props.groupId);
panel?.api.close();
}}
>
<span className="material-symbols-outlined">close</span>
</Button>
</ButtonGroup>
</div>
</div>
);
};
export const GroupActions = (props: {
groups: string[];
api: DockviewApi;
activeGroup?: string;
}) => {
return (
<div className="action-container">
{props.groups.map((groupId) => {
return (
<GroupAction key={groupId} {...props} groupId={groupId} />
);
})}
</div>
);
};

View File

@ -0,0 +1,126 @@
import { DockviewApi, IDockviewPanel } from 'dockview';
import * as React from 'react';
import { Button, ButtonGroup } from '@chakra-ui/react';
const PanelAction = (props: {
panels: string[];
api: DockviewApi;
activePanel?: string;
panelId: string;
}) => {
const onClick = () => {
props.api.getPanel(props.panelId)?.focus();
};
React.useEffect(() => {
const panel = props.api.getPanel(props.panelId);
if (panel) {
const disposable = panel.api.onDidVisibilityChange((event) => {
setVisible(event.isVisible);
});
setVisible(panel.api.isVisible);
return () => {
disposable.dispose();
};
}
}, [props.api, props.panelId]);
const [panel, setPanel] = React.useState<IDockviewPanel | undefined>(
undefined
);
React.useEffect(() => {
const list = [
props.api.onDidLayoutFromJSON(() => {
setPanel(props.api.getPanel(props.panelId));
}),
];
if (panel) {
const disposable = panel.api.onDidVisibilityChange((event) => {
setVisible(event.isVisible);
});
setVisible(panel.api.isVisible);
list.push(disposable);
}
setPanel(props.api.getPanel(props.panelId));
return () => {
list.forEach((l) => l.dispose());
};
}, [props.api, props.panelId]);
const [visible, setVisible] = React.useState<boolean>(true);
return (
<div className="button-action">
<div style={{ display: 'flex' }}>
<Button
variant={props.activePanel === props.panelId ? "solid" : "outline"}
colorPalette={props.activePanel === props.panelId ? "blue" : undefined}
onClick={onClick}
size="sm"
>
{props.panelId}
</Button>
</div>
<div style={{ display: 'flex' }}>
<ButtonGroup size="sm" variant="outline">
<Button
onClick={() => {
const panel = props.api.getPanel(props.panelId);
if (panel) {
props.api.addFloatingGroup(panel);
}
}}
>
<span className="material-symbols-outlined">ad_group</span>
</Button>
<Button
onClick={() => {
const panel = props.api.getPanel(props.panelId);
if (panel) {
props.api.addPopoutGroup(panel);
}
}}
>
<span className="material-symbols-outlined">open_in_new</span>
</Button>
<Button
onClick={() => {
const panel = props.api.getPanel(props.panelId);
panel?.api.close();
}}
>
<span className="material-symbols-outlined">close</span>
</Button>
<Button
title="Panel visiblity cannot be edited manually."
disabled={true}
>
<span className="material-symbols-outlined">
{visible ? 'visibility' : 'visibility_off'}
</span>
</Button>
</ButtonGroup>
</div>
</div>
);
};
export const PanelActions = (props: {
panels: string[];
api: DockviewApi;
activePanel?: string;
}) => {
return (
<div className="action-container">
{props.panels.map((id) => {
return <PanelAction key={id} {...props} panelId={id} />;
})}
</div>
);
};

View File

@ -0,0 +1,115 @@
import { DockviewApi } from 'dockview';
import * as React from 'react';
import { nextId } from './defaultLayout';
export const PanelBuilder = (props: { api: DockviewApi; done: () => void }) => {
const [parameters, setParameters] = React.useState<{
initialWidth?: number;
initialHeight?: number;
maximumHeight?: number;
maximumWidth?: number;
minimumHeight?: number;
minimumWidth?: number;
}>({});
return (
<div>
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
}}
>
<div>{'Initial Width'}</div>
<input
type="number"
value={parameters.initialWidth}
onChange={(event) =>
setParameters((_) => ({
..._,
initialWidth: Number(event.target.value),
}))
}
/>
<div>{'Initial Height'}</div>
<input
type="number"
value={parameters.initialHeight}
onChange={(event) =>
setParameters((_) => ({
..._,
initialHeight: Number(event.target.value),
}))
}
/>
<div>{'Maximum Width'}</div>
<input
type="number"
value={parameters.maximumWidth}
onChange={(event) =>
setParameters((_) => ({
..._,
maximumWidth: Number(event.target.value),
}))
}
/>
<div>{'Maximum Height'}</div>
<input
type="number"
value={parameters.maximumHeight}
onChange={(event) =>
setParameters((_) => ({
..._,
maximumHeight: Number(event.target.value),
}))
}
/>
<div>{'Minimum Width'}</div>
<input
type="number"
value={parameters.minimumWidth}
onChange={(event) =>
setParameters((_) => ({
..._,
minimumWidth: Number(event.target.value),
}))
}
/>
<div>{'Minimum Height'}</div>
<input
type="number"
value={parameters.minimumHeight}
onChange={(event) =>
setParameters((_) => ({
..._,
minimumHeight: Number(event.target.value),
}))
}
/>
</div>
<div>
<button
onClick={() => {
props.done();
}}
>
Cancel
</button>
<button
onClick={() => {
props.api?.addPanel({
id: `id_${Date.now().toString()}`,
component: 'default',
title: `Tab ${nextId()}`,
renderer: 'always',
...parameters,
});
props.done();
}}
>
Go
</button>
</div>
</div>
);
};

9
tsconfig.spec.json Normal file
View File

@ -0,0 +1,9 @@
{
"extends": "./tsconfig.test.json",
"compilerOptions": {
"jsx": "react-jsx",
"noImplicitAny": false,
"sourceMap": true
},
"include": ["**/*.spec.ts", "**/*.test.ts", "**/__tests__/**/*", "./jest-setup.ts"]
}

View File

@ -6099,7 +6099,7 @@ browserslist@^4.22.2:
node-releases "^2.0.14"
update-browserslist-db "^1.0.13"
bs-logger@0.x:
bs-logger@^0.2.6:
version "0.2.6"
resolved "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz"
integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==
@ -8109,12 +8109,12 @@ es6-weak-map@^2.0.1:
es6-iterator "^2.0.3"
es6-symbol "^3.1.1"
esbuild-wasm@^0.19.5:
esbuild-wasm@^0.19.5, esbuild-wasm@>=0.15.13:
version "0.19.12"
resolved "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.19.12.tgz"
integrity sha512-Zmc4hk6FibJZBcTx5/8K/4jT3/oG1vkGTEeKJUQFCUQKimD6Q7+adp/bdVQyYJFolMKaXkQnVZdV4O5ZaTYmyQ==
esbuild@^0.19.0, esbuild@^0.19.3:
esbuild@^0.19.0, esbuild@^0.19.3, esbuild@>=0.15.13:
version "0.19.12"
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz"
integrity sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==
@ -9513,7 +9513,7 @@ handle-thing@^2.0.0:
resolved "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz"
integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==
handlebars@^4.7.7:
handlebars@^4.7.7, handlebars@^4.7.8:
version "4.7.8"
resolved "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz"
integrity sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==
@ -11041,6 +11041,20 @@ jest-pnp-resolver@^1.2.2:
resolved "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz"
integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==
jest-preset-angular@^14.6.1:
version "14.6.1"
resolved "https://registry.npmjs.org/jest-preset-angular/-/jest-preset-angular-14.6.1.tgz"
integrity sha512-7q5x42wKrsF2ykOwGVzcXpr9p1X4FQJMU/DnH1tpvCmeOm5XqENdwD/xDZug+nP6G8SJPdioauwdsK/PMY/MpQ==
dependencies:
bs-logger "^0.2.6"
esbuild-wasm ">=0.15.13"
jest-environment-jsdom "^29.7.0"
jest-util "^29.7.0"
pretty-format "^29.7.0"
ts-jest "^29.3.0"
optionalDependencies:
esbuild ">=0.15.13"
jest-regex-util@^29.6.3:
version "29.6.3"
resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz"
@ -11157,7 +11171,7 @@ jest-sonar-reporter@^2.0.0:
dependencies:
xml "^1.0.1"
jest-util@^29.0.0, jest-util@^29.7.0:
jest-util@^29.7.0:
version "29.7.0"
resolved "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz"
integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==
@ -11817,7 +11831,7 @@ lodash.ismatch@^4.4.0:
resolved "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz"
integrity sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==
lodash.memoize@^4.1.2, lodash.memoize@4.x:
lodash.memoize@^4.1.2:
version "4.1.2"
resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz"
integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==
@ -11944,7 +11958,7 @@ make-dir@~3.1.0:
dependencies:
semver "^6.0.0"
make-error@^1.1.1, make-error@1.x:
make-error@^1.1.1, make-error@^1.3.6:
version "1.3.6"
resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
@ -15953,12 +15967,10 @@ semver@^6.3.1:
resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4:
version "7.5.4"
resolved "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz"
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
dependencies:
lru-cache "^6.0.0"
semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@^7.7.2:
version "7.7.2"
resolved "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz"
integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
"semver@2 || 3 || 4 || 5":
version "5.7.2"
@ -17084,19 +17096,20 @@ ts-api-utils@^1.0.1:
resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz"
integrity sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==
ts-jest@^29.1.1:
version "29.1.1"
resolved "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz"
integrity sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==
ts-jest@^29.1.1, ts-jest@^29.3.0:
version "29.4.1"
resolved "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.1.tgz"
integrity sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw==
dependencies:
bs-logger "0.x"
fast-json-stable-stringify "2.x"
jest-util "^29.0.0"
bs-logger "^0.2.6"
fast-json-stable-stringify "^2.1.0"
handlebars "^4.7.8"
json5 "^2.2.3"
lodash.memoize "4.x"
make-error "1.x"
semver "^7.5.3"
yargs-parser "^21.0.1"
lodash.memoize "^4.1.2"
make-error "^1.3.6"
semver "^7.7.2"
type-fest "^4.41.0"
yargs-parser "^21.1.1"
ts-loader@^9.5.1:
version "9.5.1"
@ -17208,6 +17221,11 @@ type-fest@^2.5.0:
resolved "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz"
integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==
type-fest@^4.41.0:
version "4.41.0"
resolved "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz"
integrity sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==
type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz"
@ -18305,7 +18323,7 @@ yargs-parser@^20.2.3:
resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz"
integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
yargs-parser@^21.0.1, yargs-parser@^21.1.1, yargs-parser@21.1.1:
yargs-parser@^21.1.1, yargs-parser@21.1.1:
version "21.1.1"
resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz"
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
@ -18378,6 +18396,11 @@ yocto-queue@^1.0.0:
resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz"
integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==
zone.js@^0.15.1:
version "0.15.1"
resolved "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz"
integrity sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==
zwitch@^2.0.0:
version "2.0.4"
resolved "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz"