diff --git a/.github/workflows/home-view-unit-test.yaml b/.github/workflows/home-view-unit-test.yaml index 1e234a7f7..143ab9141 100644 --- a/.github/workflows/home-view-unit-test.yaml +++ b/.github/workflows/home-view-unit-test.yaml @@ -1,7 +1,7 @@ -name: Lint +name: Unit-Test on: [workflow_call] jobs: - lint: + test: runs-on: ubuntu-latest defaults: run: @@ -13,5 +13,6 @@ jobs: node-version: "20" cache: "npm" cache-dependency-path: "**/package-lock.json" + - run: npm install --prefix ../.. - run: npm install - run: npm test diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index d9da3a397..db09cb9be 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -135,6 +135,11 @@ "title": "Select Active Configuration For Deployment", "category": "Posit Publisher" }, + { + "command": "posit.publisher.homeView.associateDeployment", + "title": "Associate with a Different Deployment on Connect", + "category": "Posit Publisher" + }, { "command": "posit.publisher.homeView.createConfigForDeployment", "title": "Create New Configuration For Destination", @@ -325,6 +330,10 @@ "command": "posit.publisher.homeView.showSelectConfigForDeployment", "when": "webviewId == 'posit.publisher.homeView' && webviewSection == 'even-easier-deploy-more-menu-matching-configs'" }, + { + "command": "posit.publisher.homeView.associateDeployment", + "when": "webviewId == 'posit.publisher.homeView' && webviewSection == 'even-easier-deploy-more-menu-matching-configs' && posit.publish.selection.hasCredentialMatch == 'true' && posit.publish.selection.isPreContentRecord == 'false'" + }, { "command": "posit.publisher.homeView.createConfigForDeployment", "when": "webviewId == 'posit.publisher.homeView' && webviewSection == 'even-easier-deploy-more-menu-no-matching-configs'" diff --git a/extensions/vscode/src/actions/showAssociateGUID.ts b/extensions/vscode/src/actions/showAssociateGUID.ts index 7d89f9d74..73ba7164a 100644 --- a/extensions/vscode/src/actions/showAssociateGUID.ts +++ b/extensions/vscode/src/actions/showAssociateGUID.ts @@ -6,6 +6,7 @@ import { PublisherState } from "src/state"; import { showProgress } from "src/utils/progress"; import { Views } from "src/constants"; import { useApi } from "src/api"; +import { getSummaryStringFromError } from "src/utils/errors"; export async function showAssociateGUID(state: PublisherState) { const urlOrGuid = ""; @@ -43,13 +44,26 @@ export async function showAssociateGUID(state: PublisherState) { return undefined; } await showProgress("Updating Content Record", Views.HomeView, async () => { - const api = await useApi(); - await api.contentRecords.patch( - targetContentRecord.deploymentName, - targetContentRecord.projectDir, - { - guid, - }, - ); + try { + const api = await useApi(); + await api.contentRecords.patch( + targetContentRecord.deploymentName, + targetContentRecord.projectDir, + { + guid, + }, + ); + window.showInformationMessage( + `Your deployment is now locally associated with Content GUID ${guid} as requested.`, + ); + } catch (error: unknown) { + const summary = getSummaryStringFromError( + "showAssociateGUID, contentRecords.patch", + error, + ); + window.showErrorMessage( + `Unable to associate deployment with Content GUID ${guid}. ${summary}`, + ); + } }); } diff --git a/extensions/vscode/src/constants.ts b/extensions/vscode/src/constants.ts index e072f448c..81138b10a 100644 --- a/extensions/vscode/src/constants.ts +++ b/extensions/vscode/src/constants.ts @@ -66,6 +66,7 @@ const homeViewCommands = { Refresh: "posit.publisher.homeView.refresh", ShowSelectConfigForDeployment: "posit.publisher.homeView.showSelectConfigForDeployment", + AssociateDeployment: "posit.publisher.homeView.associateDeployment", CreateConfigForDeployment: "posit.publisher.homeView.createConfigForDeployment", SelectDeployment: "posit.publisher.homeView.selectDeployment", diff --git a/extensions/vscode/src/extension.ts b/extensions/vscode/src/extension.ts index 412d444a7..9e18beff3 100644 --- a/extensions/vscode/src/extension.ts +++ b/extensions/vscode/src/extension.ts @@ -31,6 +31,20 @@ enum InitializationInProgress { false = "false", } +const SELECTION_HAS_CREDENTIAL_MATCH_CONTEXT = + "posit.publish.selection.hasCredentialMatch"; +export enum SelectionCredentialMatch { + true = "true", + false = "false", +} + +const SELECTION_IS_PRE_CONTENT_RECORD_CONTEXT = + "posit.publish.selection.isPreContentRecord"; +export enum SelectionIsPreContentRecord { + true = "true", + false = "false", +} + // Once the extension is activate, hang on to the service so that we can stop it on deactivation. let service: Service; @@ -42,6 +56,26 @@ function setInitializationInProgressContext(context: InitializationInProgress) { commands.executeCommand("setContext", INITIALIZING_CONTEXT, context); } +export function setSelectionHasCredentialMatch( + context: SelectionCredentialMatch, +) { + commands.executeCommand( + "setContext", + SELECTION_HAS_CREDENTIAL_MATCH_CONTEXT, + context, + ); +} + +export function setSelectionIsPreContentRecord( + context: SelectionIsPreContentRecord, +) { + commands.executeCommand( + "setContext", + SELECTION_IS_PRE_CONTENT_RECORD_CONTEXT, + context, + ); +} + // This method is called when your extension is activated // Your extension is activated the very first time the command is executed export async function activate(context: ExtensionContext) { diff --git a/extensions/vscode/src/types/messages/webviewToHostMessages.ts b/extensions/vscode/src/types/messages/webviewToHostMessages.ts index b83a064d5..9f830db54 100644 --- a/extensions/vscode/src/types/messages/webviewToHostMessages.ts +++ b/extensions/vscode/src/types/messages/webviewToHostMessages.ts @@ -27,6 +27,8 @@ export enum WebviewToHostMessageType { NEW_CREDENTIAL = "newCredential", VIEW_PUBLISHING_LOG = "viewPublishingLog", SHOW_ASSOCIATE_GUID = "ShowAssociateGUID", + UPDATE_SELECTION_CREDENTIAL_STATE = "UpdateSelectionCredentialStateMsg", + UPDATE_SELECTION_IS_PRE_CONTENT_RECORD = "UpdateSelectionIsPreContentRecordMsg", } export type AnyWebviewToHostMessage< @@ -62,7 +64,9 @@ export type WebviewToHostMessage = | NewCredentialForDeploymentMsg | NewCredentialMsg | ViewPublishingLog - | ShowAssociateGUIDMsg; + | ShowAssociateGUIDMsg + | UpdateSelectionCredentialStateMsg + | UpdateSelectionIsPreContentRecordMsg; // eslint-disable-next-line @typescript-eslint/no-explicit-any export function isWebviewToHostMessage(msg: any): msg is WebviewToHostMessage { @@ -89,7 +93,9 @@ export function isWebviewToHostMessage(msg: any): msg is WebviewToHostMessage { msg.kind === WebviewToHostMessageType.NEW_CREDENTIAL_FOR_DEPLOYMENT || msg.kind === WebviewToHostMessageType.NEW_CREDENTIAL || msg.kind === WebviewToHostMessageType.VIEW_PUBLISHING_LOG || - msg.kind === WebviewToHostMessageType.SHOW_ASSOCIATE_GUID + msg.kind === WebviewToHostMessageType.SHOW_ASSOCIATE_GUID || + msg.kind === WebviewToHostMessageType.UPDATE_SELECTION_CREDENTIAL_STATE || + msg.kind === WebviewToHostMessageType.UPDATE_SELECTION_IS_PRE_CONTENT_RECORD ); } @@ -210,3 +216,17 @@ export type ViewPublishingLog = export type ShowAssociateGUIDMsg = AnyWebviewToHostMessage; + +export type UpdateSelectionCredentialStateMsg = AnyWebviewToHostMessage< + WebviewToHostMessageType.UPDATE_SELECTION_CREDENTIAL_STATE, + { + state: string; + } +>; + +export type UpdateSelectionIsPreContentRecordMsg = AnyWebviewToHostMessage< + WebviewToHostMessageType.UPDATE_SELECTION_IS_PRE_CONTENT_RECORD, + { + state: string; + } +>; diff --git a/extensions/vscode/src/views/homeView.ts b/extensions/vscode/src/views/homeView.ts index ec52885f7..9d2762085 100644 --- a/extensions/vscode/src/views/homeView.ts +++ b/extensions/vscode/src/views/homeView.ts @@ -82,6 +82,12 @@ import { showAssociateGUID } from "src/actions/showAssociateGUID"; import { extensionSettings } from "src/extension"; import { openFileInEditor } from "src/commands"; import { showImmediateDeploymentFailureMessage } from "./publishFailures"; +import { + SelectionCredentialMatch, + setSelectionHasCredentialMatch, + SelectionIsPreContentRecord, + setSelectionIsPreContentRecord, +} from "../extension"; enum HomeViewInitialized { initialized = "initialized", @@ -174,6 +180,10 @@ export class HomeViewProvider implements WebviewViewProvider, Disposable { return this.showPublishingLog(); case WebviewToHostMessageType.SHOW_ASSOCIATE_GUID: return showAssociateGUID(this.state); + case WebviewToHostMessageType.UPDATE_SELECTION_CREDENTIAL_STATE: + return this.updateSelectionCredentialState(msg.content.state); + case WebviewToHostMessageType.UPDATE_SELECTION_IS_PRE_CONTENT_RECORD: + return this.updateSelectionIsPreContentRecordState(msg.content.state); default: window.showErrorMessage( `Internal Error: onConduitMessage unhandled msg: ${JSON.stringify(msg)}`, @@ -189,6 +199,22 @@ export class HomeViewProvider implements WebviewViewProvider, Disposable { ); } + private async updateSelectionCredentialState(state: string) { + const match = + state === SelectionCredentialMatch.true + ? SelectionCredentialMatch.true + : SelectionCredentialMatch.false; + return await setSelectionHasCredentialMatch(match); + } + + private async updateSelectionIsPreContentRecordState(state: string) { + const match = + state === SelectionIsPreContentRecord.true + ? SelectionIsPreContentRecord.true + : SelectionIsPreContentRecord.false; + return await setSelectionIsPreContentRecord(match); + } + private async initiateDeployment( deploymentName: string, credentialName: string, @@ -1782,6 +1808,11 @@ export class HomeViewProvider implements WebviewViewProvider, Disposable { this.showSelectOrCreateConfigForDeployment, this, ), + commands.registerCommand( + Commands.HomeView.AssociateDeployment, + () => showAssociateGUID(this.state), + this, + ), commands.registerCommand( Commands.HomeView.CreateConfigForDeployment, this.showSelectOrCreateConfigForDeployment, diff --git a/extensions/vscode/webviews/homeView/src/stores/home.ts b/extensions/vscode/webviews/homeView/src/stores/home.ts index 8ed986e0b..68c88b6b7 100644 --- a/extensions/vscode/webviews/homeView/src/stores/home.ts +++ b/extensions/vscode/webviews/homeView/src/stores/home.ts @@ -9,6 +9,7 @@ import { PreContentRecord, Configuration, ConfigurationError, + isPreContentRecord, } from "../../../../src/api"; import { isConfigurationError } from "../../../../src/api/types/configurations"; import { WebviewToHostMessageType } from "../../../../src/types/messages/webviewToHostMessages"; @@ -179,6 +180,34 @@ export const useHomeStore = defineStore("home", () => { selectedContentRecord.value = contentRecord; } + watch([serverCredential], () => updateSelectionCredentialStatus()); + + const updateSelectionCredentialStatus = () => { + const hostConduit = useHostConduitService(); + hostConduit.sendMsg({ + kind: WebviewToHostMessageType.UPDATE_SELECTION_CREDENTIAL_STATE, + content: { + state: serverCredential.value !== undefined ? "true" : "false", + }, + }); + }; + + watch([selectedContentRecord], () => + updateSelectionIsPreContentRecordState(), + ); + + const updateSelectionIsPreContentRecordState = () => { + const hostConduit = useHostConduitService(); + hostConduit.sendMsg({ + kind: WebviewToHostMessageType.UPDATE_SELECTION_IS_PRE_CONTENT_RECORD, + content: { + state: isPreContentRecord(selectedContentRecord.value) + ? "true" + : "false", + }, + }); + }; + watch([selectedConfiguration], () => updateParentViewSelectionState()); const updateParentViewSelectionState = () => {