mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-09-30 01:07:01 +08:00
[Patch agent] Add saving to workspace
If all modified files have a binding to a file on disk, allow saving the changes to disk. Bug: 406217277 Change-Id: Ib2f0adffc885b467ba885eb00d7b8765a0ba2d9b Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6403240 Commit-Queue: Wolfgang Beyer <wolfi@chromium.org> Reviewed-by: Ergün Erdoğmuş <ergunsh@chromium.org> Auto-Submit: Wolfgang Beyer <wolfi@chromium.org>
This commit is contained in:
committed by
Devtools-frontend LUCI CQ
parent
48963ad553
commit
32caf01d79
@@ -12,7 +12,6 @@ import {
|
||||
cleanup,
|
||||
createPatchWidget,
|
||||
createPatchWidgetWithDiffView,
|
||||
createTestFilesystem,
|
||||
initializePersistenceImplForTests,
|
||||
MockAidaAbortError,
|
||||
mockAidaClient,
|
||||
@@ -20,7 +19,7 @@ import {
|
||||
} from '../../testing/AiAssistanceHelpers.js';
|
||||
import {updateHostConfig} from '../../testing/EnvironmentHelpers.js';
|
||||
import {describeWithMockConnection} from '../../testing/MockConnection.js';
|
||||
import {createContentProviderUISourceCode} from '../../testing/UISourceCodeHelpers.js';
|
||||
import {createContentProviderUISourceCode, createFileSystemUISourceCode} from '../../testing/UISourceCodeHelpers.js';
|
||||
|
||||
import * as AiAssistance from './ai_assistance.js';
|
||||
|
||||
@@ -37,7 +36,7 @@ describeWithMockConnection('PatchWidget', () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
describe('applyToWorkspace', () => {
|
||||
describe('applyToPageTree', () => {
|
||||
beforeEach(() => {
|
||||
createContentProviderUISourceCode({
|
||||
url: Platform.DevToolsPath.urlString`file://test/index.html`,
|
||||
@@ -129,7 +128,7 @@ describeWithMockConnection('PatchWidget', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should show FRE dialog on applyToWorkspace click if the setting is false', async () => {
|
||||
it('should show FRE dialog on applyToPageTree click if the setting is false', async () => {
|
||||
Common.Settings.moduleSetting('ai-assistance-patching-fre-completed').set(false);
|
||||
const {view, widget} = await createPatchWidget();
|
||||
widget.changeSummary = 'body { background-color: red; }';
|
||||
@@ -139,7 +138,7 @@ describeWithMockConnection('PatchWidget', () => {
|
||||
assert.isTrue(showFreDialogStub.called, 'Expected FreDialog to be shown but it\'s not shown');
|
||||
});
|
||||
|
||||
it('should not show FRE dialog on applyToWorkspace click if the setting is true', async () => {
|
||||
it('should not show FRE dialog on applyToPageTree click if the setting is true', async () => {
|
||||
Common.Settings.moduleSetting('ai-assistance-patching-fre-completed').set(true);
|
||||
const {view, widget} = await createPatchWidget();
|
||||
widget.changeSummary = 'body { background-color: red; }';
|
||||
@@ -166,7 +165,7 @@ Files:
|
||||
* /index.html`);
|
||||
});
|
||||
|
||||
it('should show error state when applyToWorkspace fails', async () => {
|
||||
it('should show error state when applyToPageTree fails', async () => {
|
||||
const {view, widget} = await createPatchWidget({aidaClient: mockAidaClient([[MockAidaFetchError]])});
|
||||
widget.changeSummary = 'body { background-color: red; }';
|
||||
|
||||
@@ -188,6 +187,7 @@ Files:
|
||||
});
|
||||
|
||||
describe('diff view', () => {
|
||||
const origContent = 'window.foo = () => "foo";\n';
|
||||
let fileSystemUISourceCode: Workspace.UISourceCode.UISourceCode;
|
||||
let commitWorkingCopyStub:
|
||||
sinon.SinonStub<Parameters<typeof Workspace.UISourceCode.UISourceCode.prototype.commitWorkingCopy>>;
|
||||
@@ -195,8 +195,6 @@ Files:
|
||||
sinon.SinonStub<Parameters<typeof Workspace.UISourceCode.UISourceCode.prototype.resetWorkingCopy>>;
|
||||
|
||||
beforeEach(() => {
|
||||
fileSystemUISourceCode = createTestFilesystem('file://test').uiSourceCode;
|
||||
Common.Settings.Settings.instance().createSetting('ai-assistance-patching-selected-project-id', 'file://test');
|
||||
updateHostConfig({
|
||||
devToolsFreestyler: {
|
||||
enabled: true,
|
||||
@@ -204,12 +202,32 @@ Files:
|
||||
},
|
||||
});
|
||||
|
||||
const url = Platform.DevToolsPath.urlString`https://example.com/script.js`;
|
||||
createContentProviderUISourceCode({
|
||||
url,
|
||||
content: origContent,
|
||||
mimeType: 'text/javascript',
|
||||
projectType: Workspace.Workspace.projectTypes.Network,
|
||||
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(null, origContent.length),
|
||||
});
|
||||
|
||||
commitWorkingCopyStub =
|
||||
sinon.stub(Workspace.UISourceCode.UISourceCode.prototype, 'commitWorkingCopy').callThrough();
|
||||
resetWorkingCopyStub =
|
||||
sinon.stub(Workspace.UISourceCode.UISourceCode.prototype, 'resetWorkingCopy').callThrough();
|
||||
});
|
||||
|
||||
const createBoundFileSystemUISourceCode = () => {
|
||||
const localUrl = Platform.DevToolsPath.urlString`file:///var/www/script.js`;
|
||||
({uiSourceCode: fileSystemUISourceCode} = createFileSystemUISourceCode({
|
||||
url: localUrl,
|
||||
mimeType: 'text/javascript',
|
||||
content: origContent,
|
||||
autoMapping: true,
|
||||
metadata: new Workspace.UISourceCode.UISourceCodeMetadata(null, origContent.length),
|
||||
}));
|
||||
};
|
||||
|
||||
it('on apply should call handle function and stash changes', async () => {
|
||||
const {
|
||||
view,
|
||||
@@ -223,22 +241,31 @@ Files:
|
||||
assert.isTrue(changeManager.stashChanges.calledOnce);
|
||||
});
|
||||
|
||||
it('save all should commit the working copy of the changed UI codes to the disk and render savedToDisk view',
|
||||
async () => {
|
||||
const {view, widget} = await createPatchWidgetWithDiffView();
|
||||
const changeManager = sinon.createStubInstance(AiAssistanceModel.ChangeManager);
|
||||
widget.changeManager = changeManager;
|
||||
fileSystemUISourceCode.setWorkingCopy('working copy');
|
||||
it('"save to workspace" is not available if there is no matching file system mapping', async () => {
|
||||
const {view, widget} = await createPatchWidgetWithDiffView();
|
||||
const changeManager = sinon.createStubInstance(AiAssistanceModel.ChangeManager);
|
||||
widget.changeManager = changeManager;
|
||||
assert.isUndefined(view.input.onSaveToWorkspace);
|
||||
});
|
||||
|
||||
view.input.onSaveAll();
|
||||
const nextInput = await view.nextInput;
|
||||
it('"save to workspace" should commit the working copy of the files to disk and update the view', async () => {
|
||||
createBoundFileSystemUISourceCode();
|
||||
fileSystemUISourceCode.setWorkingCopy('working copy');
|
||||
const {view, widget} = await createPatchWidgetWithDiffView();
|
||||
const changeManager = sinon.createStubInstance(AiAssistanceModel.ChangeManager);
|
||||
widget.changeManager = changeManager;
|
||||
|
||||
assert.isTrue(nextInput.savedToDisk);
|
||||
assert.isTrue(commitWorkingCopyStub.called, 'Expected commitWorkingCopy to be called but it is not called');
|
||||
assert.isTrue(changeManager.dropStashedChanges.calledOnce);
|
||||
});
|
||||
assert.isDefined(view.input.onSaveToWorkspace);
|
||||
view.input.onSaveToWorkspace();
|
||||
const nextInput = await view.nextInput;
|
||||
|
||||
assert.isTrue(nextInput.savedToDisk);
|
||||
assert.isTrue(commitWorkingCopyStub.called, 'Expected commitWorkingCopy to be called but it is not called');
|
||||
assert.isTrue(changeManager.dropStashedChanges.calledOnce);
|
||||
});
|
||||
|
||||
it('discard should discard the working copy and render the view without patchSuggestion', async () => {
|
||||
createBoundFileSystemUISourceCode();
|
||||
const {view, widget} = await createPatchWidgetWithDiffView();
|
||||
const changeManager = sinon.createStubInstance(AiAssistanceModel.ChangeManager);
|
||||
widget.changeManager = changeManager;
|
||||
|
||||
@@ -13,6 +13,7 @@ import * as i18n from '../../core/i18n/i18n.js';
|
||||
import type * as Platform from '../../core/platform/platform.js';
|
||||
import * as Root from '../../core/root/root.js';
|
||||
import * as AiAssistanceModel from '../../models/ai_assistance/ai_assistance.js';
|
||||
import * as Persistence from '../../models/persistence/persistence.js';
|
||||
import * as WorkspaceDiff from '../../models/workspace_diff/workspace_diff.js';
|
||||
import * as Buttons from '../../ui/components/buttons/buttons.js';
|
||||
import * as UI from '../../ui/legacy/legacy.js';
|
||||
@@ -48,9 +49,9 @@ const UIStringsNotTranslate = {
|
||||
*/
|
||||
discard: 'Discard',
|
||||
/**
|
||||
*@description Button text to save all the suggested changes to file system
|
||||
*@description Button text to save all the suggested changes to the file system
|
||||
*/
|
||||
saveAll: 'Save all',
|
||||
saveToWorkspace: 'Save to workspace',
|
||||
/**
|
||||
*@description Header text after the user saved the changes to the disk.
|
||||
*/
|
||||
@@ -143,7 +144,7 @@ export interface ViewInput {
|
||||
onApplyToPageTree: () => void;
|
||||
onCancel: () => void;
|
||||
onDiscard: () => void;
|
||||
onSaveAll: () => void;
|
||||
onSaveToWorkspace?: () => void;
|
||||
}
|
||||
|
||||
export interface ViewOutput {
|
||||
@@ -167,6 +168,7 @@ export class PatchWidget extends UI.Widget.Widget {
|
||||
#noLogging: boolean; // Whether the enterprise setting is `ALLOW_WITHOUT_LOGGING` or not.
|
||||
#patchSuggestionState = PatchSuggestionState.INITIAL;
|
||||
#workspaceDiff = WorkspaceDiff.WorkspaceDiff.workspaceDiff();
|
||||
#persistence = Persistence.Persistence.PersistenceImpl.instance();
|
||||
|
||||
constructor(element?: HTMLElement, view?: View, opts?: {
|
||||
aidaClient: Host.AidaClient.AidaClient,
|
||||
@@ -278,12 +280,14 @@ export class PatchWidget extends UI.Widget.Widget {
|
||||
.variant=${Buttons.Button.Variant.OUTLINED}>
|
||||
${lockedString(UIStringsNotTranslate.discard)}
|
||||
</devtools-button>
|
||||
<devtools-button
|
||||
@click=${input.onSaveAll}
|
||||
.jslogContext=${'patch-widget.save-all'}
|
||||
.variant=${Buttons.Button.Variant.PRIMARY}>
|
||||
${lockedString(UIStringsNotTranslate.saveAll)}
|
||||
</devtools-button>
|
||||
${input.onSaveToWorkspace ? html`
|
||||
<devtools-button
|
||||
@click=${input.onSaveToWorkspace}
|
||||
.jslogContext=${'patch-widget.save-to-workspace'}
|
||||
.variant=${Buttons.Button.Variant.PRIMARY}>
|
||||
${lockedString(UIStringsNotTranslate.saveToWorkspace)}
|
||||
</devtools-button>
|
||||
` : nothing}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -377,7 +381,7 @@ export class PatchWidget extends UI.Widget.Widget {
|
||||
this.#applyPatchAbortController?.abort();
|
||||
},
|
||||
onDiscard: this.#onDiscard.bind(this),
|
||||
onSaveAll: this.#onSaveAll.bind(this),
|
||||
onSaveToWorkspace: this.#canSaveToWorkspace() ? this.#onSaveToWorkspace.bind(this) : undefined,
|
||||
},
|
||||
this.#viewOutput, this.contentElement);
|
||||
}
|
||||
@@ -487,10 +491,23 @@ ${processedFiles.map(filename => `* ${filename}`).join('\n')}`;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
#onSaveAll(): void {
|
||||
// TODO: What should we do for the inspector stylesheet?
|
||||
#canSaveToWorkspace(): boolean {
|
||||
if (this.#patchSuggestionState !== PatchSuggestionState.SUCCESS) {
|
||||
return false;
|
||||
}
|
||||
// TODO(crbug.com/406699819): investigate why the inspector-stylesheet shows up here
|
||||
const filteredModifiedUISourceCodes =
|
||||
this.#workspaceDiff.modifiedUISourceCodes().filter(sourceCode => sourceCode.origin() !== 'inspector://');
|
||||
return filteredModifiedUISourceCodes.length > 0 &&
|
||||
filteredModifiedUISourceCodes.every(sourceCode => this.#persistence.binding(sourceCode));
|
||||
}
|
||||
|
||||
#onSaveToWorkspace(): void {
|
||||
this.#workspaceDiff.modifiedUISourceCodes().forEach(modifiedUISourceCode => {
|
||||
modifiedUISourceCode.commitWorkingCopy();
|
||||
const binding = this.#persistence.binding(modifiedUISourceCode);
|
||||
if (binding) {
|
||||
binding.fileSystem.commitWorkingCopy();
|
||||
}
|
||||
});
|
||||
|
||||
this.#savedToDisk = true;
|
||||
|
||||
@@ -2633,7 +2633,7 @@ export const knownContextValues = new Set([
|
||||
'passive',
|
||||
'paste',
|
||||
'patch-widget.discard',
|
||||
'patch-widget.save-all',
|
||||
'patch-widget.save-to-workspace',
|
||||
'path',
|
||||
'pattern',
|
||||
'pause',
|
||||
|
||||
Reference in New Issue
Block a user