@lvce-editor/api 9.37.2 → 9.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/parts/AuthenticationProvider/AuthenticationProvider.d.ts +17 -0
- package/dist/parts/AuthenticationProvider/AuthenticationProvider.js +1 -0
- package/dist/parts/AuthenticationProviderRegistry/AuthenticationProviderRegistry.d.ts +9 -0
- package/dist/parts/AuthenticationProviderRegistry/AuthenticationProviderRegistry.js +98 -0
- package/dist/parts/AuthenticationProviderRegistrySnapshot/AuthenticationProviderRegistrySnapshot.d.ts +6 -0
- package/dist/parts/AuthenticationProviderRegistrySnapshot/AuthenticationProviderRegistrySnapshot.js +1 -0
- package/dist/parts/PrettyError/PrettyError.d.ts +7 -0
- package/dist/parts/PrettyError/PrettyError.js +35 -0
- package/dist/parts/View/View.d.ts +8 -0
- package/dist/parts/ViewRegistry/ViewRegistry.js +18 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,3 +67,34 @@ const registration = registerPortProvider({
|
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
Ports queries matching extensions with the current workspace URI. Each extension may register one provider per scheme. Return an empty array when no ports are available, and dispose the registration when deactivating. The provider supplies addresses; the API does not create tunnels or change their visibility. Results from disposed registrations are discarded.
|
|
70
|
+
|
|
71
|
+
## Authentication providers
|
|
72
|
+
|
|
73
|
+
Extensions can contribute a functional authentication provider with session callbacks. The provider's stable `id` and display `label` identify it; requested scopes are passed to the callbacks, and session results include an account, access token, and granted scopes.
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { activate, registerAuthenticationProvider } from '@lvce-editor/api'
|
|
77
|
+
|
|
78
|
+
await activate()
|
|
79
|
+
const registration = registerAuthenticationProvider({
|
|
80
|
+
id: 'example',
|
|
81
|
+
label: 'Example Account',
|
|
82
|
+
async getSessions(scopes) {
|
|
83
|
+
return []
|
|
84
|
+
},
|
|
85
|
+
async createSession(scopes) {
|
|
86
|
+
return {
|
|
87
|
+
id: 'account-session',
|
|
88
|
+
accessToken: 'token',
|
|
89
|
+
account: { id: 'account', label: 'Example Account' },
|
|
90
|
+
scopes,
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
async removeSession(sessionId) {},
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// Dispose the contribution when the extension deactivates.
|
|
97
|
+
registration.dispose()
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The editor can call `executeAuthenticationProviderGetSessions`, `executeAuthenticationProviderCreateSession`, and `executeAuthenticationProviderRemoveSession` by provider id. Session callbacks are responsible for account selection, consent, token storage, and revocation.
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,9 @@ export { activate } from './parts/Activation/Activation.ts';
|
|
|
2
2
|
export { getColorThemeNames } from './parts/ColorTheme/ColorTheme.ts';
|
|
3
3
|
export { markdownToVirtualDom } from './parts/Markdown/Markdown.ts';
|
|
4
4
|
export { getAccessToken, type GetAccessTokenOptions } from './parts/Authentication/Authentication.ts';
|
|
5
|
+
export { executeAuthenticationProviderCreateSession, executeAuthenticationProviderGetSessions, executeAuthenticationProviderRemoveSession, getAuthenticationProviderRegistrySnapshot, registerAuthenticationProvider, resetAuthenticationProviderRegistry, } from './parts/AuthenticationProviderRegistry/AuthenticationProviderRegistry.ts';
|
|
6
|
+
export type { AuthenticationAccount, AuthenticationProvider, AuthenticationSession } from './parts/AuthenticationProvider/AuthenticationProvider.ts';
|
|
7
|
+
export type { AuthenticationProviderRegistrySnapshot } from './parts/AuthenticationProviderRegistrySnapshot/AuthenticationProviderRegistrySnapshot.ts';
|
|
5
8
|
export { createElectronWebContentsView } from './parts/ElectronWebContentsView/ElectronWebContentsView.ts';
|
|
6
9
|
export { executeCommand } from './parts/ExecuteCommand/ExecuteCommand.ts';
|
|
7
10
|
export { showFileQuickPick, showQuickInput, showQuickPick } from './parts/QuickPick/QuickPick.ts';
|
|
@@ -19,6 +22,7 @@ export { exists, getFileHash, getFileHashes, mkdir, readAsObjectUrl, readDirWith
|
|
|
19
22
|
export { registerFileChangeHandler, type FileChangeHandler, type FileChanges } from './parts/FileChangeHandler/FileChangeHandler.ts';
|
|
20
23
|
export { executeFileSystemProviderGetOpenExternalPath, executeFileSystemProviderIsReadonly, executeFileSystemProviderMkdir, executeFileSystemProviderReadDirWithFileTypes, executeFileSystemProviderReadFile, executeFileSystemProviderRemove, executeFileSystemProviderRename, executeFileSystemProviderWriteFile, getFileSystemProviderRegistrySnapshot, registerFileSystemProvider, resetFileSystemProviderRegistry, } from './parts/FileSystemProviderRegistry/FileSystemProviderRegistry.ts';
|
|
21
24
|
export { closeUri, confirm, getRecentlyOpenedWorkspaceUris, getWorkspaceFolder, getWorkspaceUri, handleWorkspaceRefresh, openUri, setWorkspaceUri, showErrorMessage, showNotification, type ConfirmOptions, type NotificationType, type ShowErrorMessageOptions, } from './parts/Host/Host.ts';
|
|
25
|
+
export { preparePrettyError } from './parts/PrettyError/PrettyError.ts';
|
|
22
26
|
export { executeHoverProvider, getHoverProviderRegistrySnapshot, registerHoverProvider, resetHoverProviderRegistry } from './parts/Hover/Hover.ts';
|
|
23
27
|
export { executeSignatureHelpProvider, getSignatureHelpProviderRegistrySnapshot, registerSignatureHelpProvider, resetSignatureHelpProviderRegistry, } from './parts/SignatureHelp/SignatureHelp.ts';
|
|
24
28
|
export { getPlatform, getUserDataDir, type Platform } from './parts/Platform/Platform.ts';
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export { activate } from "./parts/Activation/Activation.js";
|
|
|
2
2
|
export { getColorThemeNames } from "./parts/ColorTheme/ColorTheme.js";
|
|
3
3
|
export { markdownToVirtualDom } from "./parts/Markdown/Markdown.js";
|
|
4
4
|
export { getAccessToken } from "./parts/Authentication/Authentication.js";
|
|
5
|
+
export { executeAuthenticationProviderCreateSession, executeAuthenticationProviderGetSessions, executeAuthenticationProviderRemoveSession, getAuthenticationProviderRegistrySnapshot, registerAuthenticationProvider, resetAuthenticationProviderRegistry, } from "./parts/AuthenticationProviderRegistry/AuthenticationProviderRegistry.js";
|
|
5
6
|
export { createElectronWebContentsView } from "./parts/ElectronWebContentsView/ElectronWebContentsView.js";
|
|
6
7
|
export { executeCommand } from "./parts/ExecuteCommand/ExecuteCommand.js";
|
|
7
8
|
export { showFileQuickPick, showQuickInput, showQuickPick } from "./parts/QuickPick/QuickPick.js";
|
|
@@ -19,6 +20,7 @@ export { exists, getFileHash, getFileHashes, mkdir, readAsObjectUrl, readDirWith
|
|
|
19
20
|
export { registerFileChangeHandler } from "./parts/FileChangeHandler/FileChangeHandler.js";
|
|
20
21
|
export { executeFileSystemProviderGetOpenExternalPath, executeFileSystemProviderIsReadonly, executeFileSystemProviderMkdir, executeFileSystemProviderReadDirWithFileTypes, executeFileSystemProviderReadFile, executeFileSystemProviderRemove, executeFileSystemProviderRename, executeFileSystemProviderWriteFile, getFileSystemProviderRegistrySnapshot, registerFileSystemProvider, resetFileSystemProviderRegistry, } from "./parts/FileSystemProviderRegistry/FileSystemProviderRegistry.js";
|
|
21
22
|
export { closeUri, confirm, getRecentlyOpenedWorkspaceUris, getWorkspaceFolder, getWorkspaceUri, handleWorkspaceRefresh, openUri, setWorkspaceUri, showErrorMessage, showNotification, } from "./parts/Host/Host.js";
|
|
23
|
+
export { preparePrettyError } from "./parts/PrettyError/PrettyError.js";
|
|
22
24
|
export { executeHoverProvider, getHoverProviderRegistrySnapshot, registerHoverProvider, resetHoverProviderRegistry } from "./parts/Hover/Hover.js";
|
|
23
25
|
export { executeSignatureHelpProvider, getSignatureHelpProviderRegistrySnapshot, registerSignatureHelpProvider, resetSignatureHelpProviderRegistry, } from "./parts/SignatureHelp/SignatureHelp.js";
|
|
24
26
|
export { getPlatform, getUserDataDir } from "./parts/Platform/Platform.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface AuthenticationAccount {
|
|
2
|
+
readonly id: string;
|
|
3
|
+
readonly label: string;
|
|
4
|
+
}
|
|
5
|
+
export interface AuthenticationSession {
|
|
6
|
+
readonly accessToken: string;
|
|
7
|
+
readonly account: AuthenticationAccount;
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly scopes: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
export interface AuthenticationProvider {
|
|
12
|
+
readonly createSession: (scopes: readonly string[]) => Promise<AuthenticationSession> | AuthenticationSession;
|
|
13
|
+
readonly getSessions: (scopes: readonly string[]) => Promise<readonly AuthenticationSession[]> | readonly AuthenticationSession[];
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly label: string;
|
|
16
|
+
readonly removeSession: (sessionId: string) => Promise<void> | void;
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AuthenticationProvider, AuthenticationSession } from '../AuthenticationProvider/AuthenticationProvider.ts';
|
|
2
|
+
import type { AuthenticationProviderRegistrySnapshot } from '../AuthenticationProviderRegistrySnapshot/AuthenticationProviderRegistrySnapshot.ts';
|
|
3
|
+
import type { Disposable } from '../Disposable/Disposable.ts';
|
|
4
|
+
export declare const registerAuthenticationProvider: (provider: AuthenticationProvider) => Disposable;
|
|
5
|
+
export declare const executeAuthenticationProviderGetSessions: (providerId: string, scopes: readonly string[]) => Promise<readonly AuthenticationSession[]>;
|
|
6
|
+
export declare const executeAuthenticationProviderCreateSession: (providerId: string, scopes: readonly string[]) => Promise<AuthenticationSession>;
|
|
7
|
+
export declare const executeAuthenticationProviderRemoveSession: (providerId: string, sessionId: string) => Promise<void>;
|
|
8
|
+
export declare const getAuthenticationProviderRegistrySnapshot: () => AuthenticationProviderRegistrySnapshot;
|
|
9
|
+
export declare const resetAuthenticationProviderRegistry: () => void;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import * as ExtensionApiCommandRegistry from "../ExtensionApiCommandRegistry/ExtensionApiCommandRegistry.js";
|
|
2
|
+
import { ExtensionApiError } from "../ExtensionApiError/ExtensionApiError.js";
|
|
3
|
+
const providers = Object.create(null);
|
|
4
|
+
const assertSession = (session, providerId) => {
|
|
5
|
+
if (!session || typeof session !== 'object') {
|
|
6
|
+
throw new ExtensionApiError(`authentication provider ${providerId} returned an invalid session`);
|
|
7
|
+
}
|
|
8
|
+
const value = session;
|
|
9
|
+
if (typeof value.id !== 'string' || value.id.length === 0) {
|
|
10
|
+
throw new ExtensionApiError(`authentication provider ${providerId} returned a session without an id`);
|
|
11
|
+
}
|
|
12
|
+
if (typeof value.accessToken !== 'string' || value.accessToken.length === 0) {
|
|
13
|
+
throw new ExtensionApiError(`authentication provider ${providerId} returned a session without an access token`);
|
|
14
|
+
}
|
|
15
|
+
if (!Array.isArray(value.scopes) || value.scopes.some((scope) => typeof scope !== 'string' || scope.length === 0)) {
|
|
16
|
+
throw new ExtensionApiError(`authentication provider ${providerId} returned a session with invalid scopes`);
|
|
17
|
+
}
|
|
18
|
+
if (!value.account || typeof value.account !== 'object') {
|
|
19
|
+
throw new ExtensionApiError(`authentication provider ${providerId} returned a session without an account`);
|
|
20
|
+
}
|
|
21
|
+
const account = value.account;
|
|
22
|
+
if (typeof account.id !== 'string' || account.id.length === 0 || typeof account.label !== 'string' || account.label.length === 0) {
|
|
23
|
+
throw new ExtensionApiError(`authentication provider ${providerId} returned a session with an invalid account`);
|
|
24
|
+
}
|
|
25
|
+
return session;
|
|
26
|
+
};
|
|
27
|
+
const assertScopes = (scopes) => {
|
|
28
|
+
if (!Array.isArray(scopes) || scopes.some((scope) => !(typeof scope === 'string' && scope.length > 0))) {
|
|
29
|
+
throw new ExtensionApiError('authentication scopes must be an array of non-empty strings');
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const getProvider = (providerId) => {
|
|
33
|
+
const provider = providers[providerId];
|
|
34
|
+
if (!provider) {
|
|
35
|
+
throw new ExtensionApiError(`authentication provider ${providerId} not found`);
|
|
36
|
+
}
|
|
37
|
+
return provider;
|
|
38
|
+
};
|
|
39
|
+
export const registerAuthenticationProvider = (provider) => {
|
|
40
|
+
if (!provider) {
|
|
41
|
+
throw new ExtensionApiError('authentication provider is not defined');
|
|
42
|
+
}
|
|
43
|
+
const providerRecord = provider;
|
|
44
|
+
if (typeof providerRecord.id !== 'string' || providerRecord.id.length === 0) {
|
|
45
|
+
throw new ExtensionApiError('authentication provider is missing id');
|
|
46
|
+
}
|
|
47
|
+
const { id } = provider;
|
|
48
|
+
if (typeof providerRecord.label !== 'string' || providerRecord.label.length === 0) {
|
|
49
|
+
throw new ExtensionApiError(`authentication provider ${id} is missing label`);
|
|
50
|
+
}
|
|
51
|
+
for (const methodName of ['getSessions', 'createSession', 'removeSession']) {
|
|
52
|
+
if (typeof providerRecord[methodName] !== 'function') {
|
|
53
|
+
throw new ExtensionApiError(`authentication provider ${id} is missing ${methodName} function`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (id in providers) {
|
|
57
|
+
throw new ExtensionApiError(`authentication provider ${id} is already registered`);
|
|
58
|
+
}
|
|
59
|
+
providers[id] = provider;
|
|
60
|
+
ExtensionApiCommandRegistry.registerCommandMap(commandMap);
|
|
61
|
+
return {
|
|
62
|
+
dispose() {
|
|
63
|
+
delete providers[id];
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
export const executeAuthenticationProviderGetSessions = async (providerId, scopes) => {
|
|
68
|
+
assertScopes(scopes);
|
|
69
|
+
const sessions = await getProvider(providerId).getSessions(scopes);
|
|
70
|
+
if (!Array.isArray(sessions)) {
|
|
71
|
+
throw new ExtensionApiError(`authentication provider ${providerId} returned invalid sessions`);
|
|
72
|
+
}
|
|
73
|
+
return sessions.map((session) => assertSession(session, providerId));
|
|
74
|
+
};
|
|
75
|
+
export const executeAuthenticationProviderCreateSession = async (providerId, scopes) => {
|
|
76
|
+
assertScopes(scopes);
|
|
77
|
+
return assertSession(await getProvider(providerId).createSession(scopes), providerId);
|
|
78
|
+
};
|
|
79
|
+
export const executeAuthenticationProviderRemoveSession = async (providerId, sessionId) => {
|
|
80
|
+
if (typeof sessionId !== 'string' || sessionId.length === 0) {
|
|
81
|
+
throw new ExtensionApiError('authentication session id is missing');
|
|
82
|
+
}
|
|
83
|
+
await getProvider(providerId).removeSession(sessionId);
|
|
84
|
+
};
|
|
85
|
+
export const getAuthenticationProviderRegistrySnapshot = () => ({
|
|
86
|
+
providers: Object.values(providers).map(({ id, label }) => ({ id, label })),
|
|
87
|
+
});
|
|
88
|
+
export const resetAuthenticationProviderRegistry = () => {
|
|
89
|
+
for (const id of Object.keys(providers)) {
|
|
90
|
+
delete providers[id];
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
const commandMap = {
|
|
94
|
+
'ExtensionApi.executeAuthenticationProviderCreateSession': executeAuthenticationProviderCreateSession,
|
|
95
|
+
'ExtensionApi.executeAuthenticationProviderGetSessions': executeAuthenticationProviderGetSessions,
|
|
96
|
+
'ExtensionApi.executeAuthenticationProviderRemoveSession': executeAuthenticationProviderRemoveSession,
|
|
97
|
+
'ExtensionApi.getAuthenticationProviderRegistrySnapshot': getAuthenticationProviderRegistrySnapshot,
|
|
98
|
+
};
|
package/dist/parts/AuthenticationProviderRegistrySnapshot/AuthenticationProviderRegistrySnapshot.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { executeCommand } from "../ExecuteCommand/ExecuteCommand.js";
|
|
2
|
+
const serializeError = (error) => {
|
|
3
|
+
if (error instanceof Error) {
|
|
4
|
+
return {
|
|
5
|
+
code: error.code,
|
|
6
|
+
codeFrame: error.codeFrame,
|
|
7
|
+
constructor: { name: error.constructor.name },
|
|
8
|
+
message: error.message,
|
|
9
|
+
name: error.name,
|
|
10
|
+
stack: error.stack,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
if (error && typeof error === 'object') {
|
|
14
|
+
const value = error;
|
|
15
|
+
const constructorName = Object.hasOwn(value, 'constructor') ? value.constructor?.name : undefined;
|
|
16
|
+
return {
|
|
17
|
+
code: value.code,
|
|
18
|
+
codeFrame: value.codeFrame,
|
|
19
|
+
constructor: constructorName ? { name: constructorName } : undefined,
|
|
20
|
+
message: value.message || value.errorMessage,
|
|
21
|
+
name: value.name || constructorName,
|
|
22
|
+
stack: value.stack || value.errorStack,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return { message: String(error) };
|
|
26
|
+
};
|
|
27
|
+
export const preparePrettyError = async (error) => {
|
|
28
|
+
const serialized = serializeError(error);
|
|
29
|
+
try {
|
|
30
|
+
return (await executeCommand('ErrorHandling.preparePrettyError', serialized));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return serialized;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
@@ -51,6 +51,8 @@ export interface VirtualDomViewInstance {
|
|
|
51
51
|
readonly getCss?: () => string | Promise<string>;
|
|
52
52
|
readonly getMenuEntries?: (menuId: string) => readonly MenuEntry[] | Promise<readonly MenuEntry[]>;
|
|
53
53
|
readonly handleEvent?: (event: ViewEvent) => unknown;
|
|
54
|
+
/** Whether this document has edits that have not been saved. */
|
|
55
|
+
readonly isDirty?: () => boolean;
|
|
54
56
|
readonly render: () => readonly VirtualDomNode[] | Promise<readonly VirtualDomNode[]>;
|
|
55
57
|
readonly renderActions?: () => readonly ViewAction[] | Promise<readonly ViewAction[]>;
|
|
56
58
|
readonly renderActionsDom?: () => readonly VirtualDomNode[];
|
|
@@ -59,6 +61,8 @@ export interface VirtualDomViewInstance {
|
|
|
59
61
|
readonly renderSelections?: () => readonly ViewSelection[] | Promise<readonly ViewSelection[]>;
|
|
60
62
|
readonly renderStatusBarItems?: () => readonly StatusBarItem[] | Promise<readonly StatusBarItem[]>;
|
|
61
63
|
readonly renderTitle?: () => string | Promise<string>;
|
|
64
|
+
/** Persist committed edits. Reject on failure and retain dirty state until persistence succeeds. */
|
|
65
|
+
readonly save?: () => void | Promise<void>;
|
|
62
66
|
readonly saveState?: () => unknown;
|
|
63
67
|
}
|
|
64
68
|
export type ViewCommand<State = VirtualDomViewInstance, Args extends readonly any[] = readonly any[]> = (state: State, ...args: Args) => State | Promise<State>;
|
|
@@ -87,6 +91,7 @@ export interface StatefulView<State = unknown> extends ViewBase<State> {
|
|
|
87
91
|
readonly getCss?: (state: State) => string | Promise<string>;
|
|
88
92
|
readonly getMenuEntries?: (state: State, menuId: string) => readonly MenuEntry[] | Promise<readonly MenuEntry[]>;
|
|
89
93
|
readonly handleEvent?: (state: State, event: ViewEvent) => State | Promise<State>;
|
|
94
|
+
readonly isDirty?: (state: State) => boolean;
|
|
90
95
|
readonly render: (state: State) => readonly VirtualDomNode[] | Promise<readonly VirtualDomNode[]>;
|
|
91
96
|
readonly renderActions?: (state: State) => readonly ViewAction[] | Promise<readonly ViewAction[]>;
|
|
92
97
|
readonly renderActionsDom?: (state: State) => readonly VirtualDomNode[];
|
|
@@ -95,6 +100,7 @@ export interface StatefulView<State = unknown> extends ViewBase<State> {
|
|
|
95
100
|
readonly renderSelections?: (state: State) => readonly ViewSelection[] | Promise<readonly ViewSelection[]>;
|
|
96
101
|
readonly renderStatusBarItems?: (state: State) => readonly StatusBarItem[] | Promise<readonly StatusBarItem[]>;
|
|
97
102
|
readonly renderTitle?: (state: State) => string | Promise<string>;
|
|
103
|
+
readonly save?: (state: State) => State | Promise<State>;
|
|
98
104
|
readonly saveState?: (state: State) => unknown;
|
|
99
105
|
}
|
|
100
106
|
export type View<State = unknown, ComponentState = unknown> = InstanceView<State, ComponentState> | StatefulView<State>;
|
|
@@ -116,6 +122,7 @@ export interface ViewRenderResultDom {
|
|
|
116
122
|
readonly css?: string;
|
|
117
123
|
readonly dom: readonly VirtualDomNode[];
|
|
118
124
|
readonly focusSelector?: string;
|
|
125
|
+
readonly modified?: boolean;
|
|
119
126
|
readonly scrollPosition?: ViewScrollPosition;
|
|
120
127
|
readonly selections?: readonly ViewSelection[];
|
|
121
128
|
readonly title?: string;
|
|
@@ -124,6 +131,7 @@ export interface ViewRenderResultDom {
|
|
|
124
131
|
export interface ViewRenderResultPatches {
|
|
125
132
|
readonly css?: string;
|
|
126
133
|
readonly focusSelector?: string;
|
|
134
|
+
readonly modified?: boolean;
|
|
127
135
|
readonly patches: readonly unknown[];
|
|
128
136
|
readonly scrollPosition?: ViewScrollPosition;
|
|
129
137
|
readonly selections?: readonly ViewSelection[];
|
|
@@ -236,6 +236,16 @@ const createStatefulViewInstance = (view, uid) => {
|
|
|
236
236
|
ViewletStates.replace(uid, newState);
|
|
237
237
|
};
|
|
238
238
|
}
|
|
239
|
+
if (view.isDirty) {
|
|
240
|
+
instance.isDirty = () => view.isDirty(ViewletStates.get(uid));
|
|
241
|
+
}
|
|
242
|
+
if (view.save) {
|
|
243
|
+
instance.save = async () => {
|
|
244
|
+
const newState = await view.save(ViewletStates.get(uid));
|
|
245
|
+
assertViewState(view.id, newState);
|
|
246
|
+
ViewletStates.replace(uid, newState);
|
|
247
|
+
};
|
|
248
|
+
}
|
|
239
249
|
if (view.renderActions) {
|
|
240
250
|
instance.renderActions = () => view.renderActions(ViewletStates.get(uid));
|
|
241
251
|
}
|
|
@@ -506,7 +516,14 @@ const withRenderMetadata = async (uid, viewId, result, instance, contextChange)
|
|
|
506
516
|
const resultWithScrollPosition = await withScrollPosition(resultWithSelections, instance);
|
|
507
517
|
const resultWithTitle = await withTitle(resultWithScrollPosition, instance);
|
|
508
518
|
await ViewStatusBarItems.renderViewStatusBarItems(uid, viewId, instance);
|
|
509
|
-
|
|
519
|
+
if (typeof instance.isDirty !== 'function') {
|
|
520
|
+
return resultWithTitle;
|
|
521
|
+
}
|
|
522
|
+
const modified = instance.isDirty();
|
|
523
|
+
if (typeof modified !== 'boolean') {
|
|
524
|
+
throw new ExtensionApiError('view isDirty result must be a boolean');
|
|
525
|
+
}
|
|
526
|
+
return { ...resultWithTitle, modified };
|
|
510
527
|
};
|
|
511
528
|
const maybeClearContext = async (uid, viewId) => {
|
|
512
529
|
if (!contexts[uid]) {
|